From 87339aea8606b972dbe59e9b5a97bccd0014a0d3 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Wed, 5 Aug 2026 21:57:18 +0800 Subject: [PATCH 1/6] Add daemon-backed Pi npm plugin --- .github/workflows/ci.yml | 43 + README.md | 1 + bt-daemon/docs/protocol.md | 2 +- bt-daemon/src/translate/mod.rs | 5 +- bt-daemon/src/translate/pi.rs | 543 ++ bt-daemon/tests/pi_translator.rs | 124 + src/plugins/pi/build.sh | 21 + src/plugins/pi/content/.gitignore | 25 + src/plugins/pi/content/.npmrc | 1 + src/plugins/pi/content/.tool-versions | 3 + src/plugins/pi/content/AGENTS.md | 84 + src/plugins/pi/content/CONTRIBUTING.md | 202 + src/plugins/pi/content/LICENSE | 21 + src/plugins/pi/content/README.md | 136 + src/plugins/pi/content/docs/PUBLISHING.md | 120 + src/plugins/pi/content/package.json | 78 + src/plugins/pi/content/pnpm-lock.yaml | 4725 +++++++++++++++++ src/plugins/pi/content/pnpm-workspace.yaml | 6 + .../content/scripts/resolve-pi-versions.mjs | 154 + .../pi/content/scripts/sync-version.mjs | 23 + src/plugins/pi/content/src/client.ts | 282 + src/plugins/pi/content/src/config.test.ts | 445 ++ src/plugins/pi/content/src/config.ts | 507 ++ .../pi/content/src/daemon-adapter.test.ts | 96 + .../pi/content/src/index.integration.test.ts | 726 +++ src/plugins/pi/content/src/index.test.ts | 1230 +++++ src/plugins/pi/content/src/index.ts | 143 + .../pi/content/src/legacy-processor.ts | 1628 ++++++ src/plugins/pi/content/src/state.test.ts | 131 + src/plugins/pi/content/src/state.ts | 197 + src/plugins/pi/content/src/types.ts | 193 + src/plugins/pi/content/src/utils.test.ts | 240 + src/plugins/pi/content/src/utils.ts | 425 ++ src/plugins/pi/content/src/version.ts | 2 + src/plugins/pi/content/tsconfig.json | 15 + src/plugins/pi/content/vite.config.ts | 23 + src/plugins/pi/publish.sh | 18 + src/plugins/pi/validate.sh | 28 + src/runtime/js-daemon-client/src/index.ts | 7 +- 39 files changed, 12648 insertions(+), 5 deletions(-) create mode 100644 bt-daemon/src/translate/pi.rs create mode 100644 bt-daemon/tests/pi_translator.rs create mode 100755 src/plugins/pi/build.sh create mode 100644 src/plugins/pi/content/.gitignore create mode 100644 src/plugins/pi/content/.npmrc create mode 100644 src/plugins/pi/content/.tool-versions create mode 100644 src/plugins/pi/content/AGENTS.md create mode 100644 src/plugins/pi/content/CONTRIBUTING.md create mode 100644 src/plugins/pi/content/LICENSE create mode 100644 src/plugins/pi/content/README.md create mode 100644 src/plugins/pi/content/docs/PUBLISHING.md create mode 100644 src/plugins/pi/content/package.json create mode 100644 src/plugins/pi/content/pnpm-lock.yaml create mode 100644 src/plugins/pi/content/pnpm-workspace.yaml create mode 100644 src/plugins/pi/content/scripts/resolve-pi-versions.mjs create mode 100644 src/plugins/pi/content/scripts/sync-version.mjs create mode 100644 src/plugins/pi/content/src/client.ts create mode 100644 src/plugins/pi/content/src/config.test.ts create mode 100644 src/plugins/pi/content/src/config.ts create mode 100644 src/plugins/pi/content/src/daemon-adapter.test.ts create mode 100644 src/plugins/pi/content/src/index.integration.test.ts create mode 100644 src/plugins/pi/content/src/index.test.ts create mode 100644 src/plugins/pi/content/src/index.ts create mode 100644 src/plugins/pi/content/src/legacy-processor.ts create mode 100644 src/plugins/pi/content/src/state.test.ts create mode 100644 src/plugins/pi/content/src/state.ts create mode 100644 src/plugins/pi/content/src/types.ts create mode 100644 src/plugins/pi/content/src/utils.test.ts create mode 100644 src/plugins/pi/content/src/utils.ts create mode 100644 src/plugins/pi/content/src/version.ts create mode 100644 src/plugins/pi/content/tsconfig.json create mode 100644 src/plugins/pi/content/vite.config.ts create mode 100755 src/plugins/pi/publish.sh create mode 100755 src/plugins/pi/validate.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6412501..b8cffc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,10 @@ jobs: uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.14 + - name: Set up pinned pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + version: 10.33.0 - name: Build and validate plugins run: make test @@ -76,6 +80,45 @@ jobs: node -e 'import(process.argv[1]).then((m) => { if (typeof m.default !== "function") process.exit(1) })' \ "$PWD/install-test/node_modules/@braintrust/trace-opencode/dist/index.js" + resolve-pi-compatibility: + name: Resolve Pi compatibility window + runs-on: ubuntu-24.04 + timeout-minutes: 10 + outputs: + versions: ${{ steps.versions.outputs.versions }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - id: versions + name: Resolve latest patch from five Pi minor lines + env: + PI_VERSION_COUNT: 5 + run: node src/plugins/pi/content/scripts/resolve-pi-versions.mjs + + pi-compatibility: + name: Pi package (${{ matrix.pi-target.version }}) + needs: resolve-pi-compatibility + strategy: + fail-fast: false + matrix: + pi-target: ${{ fromJson(needs.resolve-pi-compatibility.outputs.versions) }} + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Set up pinned pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + version: 10.33.0 + - name: Build validated Pi tarball + run: make validate-pi + - name: Test resolved Pi compatibility target + working-directory: dist/pi + env: + PI_COMPAT_VERSION: ${{ matrix.pi-target.version }} + run: | + pnpm add -D '${{ matrix.pi-target.piAiSpec }}' '${{ matrix.pi-target.piCodingAgentSpec }}' --lockfile=false + pnpm run test:integration + daemon: name: Daemon (${{ matrix.os }}) strategy: diff --git a/README.md b/README.md index 0ac7496..aecd10e 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ For further instructions, see the instructions for your desired coding agent | Claude Code | [braintrustdata/braintrust-claude-plugin](https://github.com/braintrustdata/braintrust-claude-plugin) | | Codex | [braintrustdata/braintrust-codex-plugin](https://github.com/braintrustdata/braintrust-codex-plugin) | | OpenCode | npm: [`@braintrust/trace-opencode`](https://www.npmjs.com/package/@braintrust/trace-opencode) | +| Pi | npm: [`@braintrust/pi-extension`](https://www.npmjs.com/package/@braintrust/pi-extension) | ## Development & releasing diff --git a/bt-daemon/docs/protocol.md b/bt-daemon/docs/protocol.md index 4ed3065..ab96622 100644 --- a/bt-daemon/docs/protocol.md +++ b/bt-daemon/docs/protocol.md @@ -92,7 +92,7 @@ Result: { "protocol_version": 1, "daemon_version": "0.1.0", - "capabilities": { "sources": ["codex", "claude-code", "opencode", "debug"] } + "capabilities": { "sources": ["codex", "claude-code", "opencode", "pi", "debug"] } } ``` If `protocol_version` is incompatible the daemon returns an application error; diff --git a/bt-daemon/src/translate/mod.rs b/bt-daemon/src/translate/mod.rs index 92d978e..d182161 100644 --- a/bt-daemon/src/translate/mod.rs +++ b/bt-daemon/src/translate/mod.rs @@ -12,11 +12,13 @@ mod codex; mod debug; mod git; mod opencode; +mod pi; pub use claude::ClaudeTranslatorFactory; pub use codex::CodexTranslatorFactory; pub use debug::DebugTranslatorFactory; pub use opencode::OpenCodeTranslatorFactory; +pub use pi::PiTranslatorFactory; use crate::wire::{Envelope, SessionConfig}; use serde::{Deserialize, Serialize}; @@ -125,7 +127,8 @@ impl Registry { let git = Arc::new(git::GitMetadataCache::default()); r.register(Box::new(ClaudeTranslatorFactory::new(git.clone()))); r.register(Box::new(CodexTranslatorFactory::new(git.clone()))); - r.register(Box::new(OpenCodeTranslatorFactory::new(git))); + r.register(Box::new(OpenCodeTranslatorFactory::new(git.clone()))); + r.register(Box::new(PiTranslatorFactory::new(git))); r } diff --git a/bt-daemon/src/translate/pi.rs b/bt-daemon/src/translate/pi.rs new file mode 100644 index 0000000..e7946aa --- /dev/null +++ b/bt-daemon/src/translate/pi.rs @@ -0,0 +1,543 @@ +//! Pi coding-agent translator. Pi forwards native extension callbacks and this +//! state machine owns all span construction and recovery. + +use super::git::GitMetadataCache; +use super::{AgentTranslator, SessionCtx, SpanOp, SpanRow, SpanType, TranslatorFactory}; +use crate::ids; +use crate::wire::Envelope; +use serde_json::{json, Map, Value}; +use std::collections::HashMap; +use std::sync::Arc; + +pub struct PiTranslatorFactory { + git: Arc, +} +impl PiTranslatorFactory { + pub(super) fn new(git: Arc) -> Self { + Self { git } + } +} +impl TranslatorFactory for PiTranslatorFactory { + fn source(&self) -> &str { + "pi" + } + fn create(&self, session_id: &str) -> Box { + Box::new(PiTranslator { + session_id: session_id.into(), + root_span_id: ids::span_id(session_id, "root"), + effective_root_span_id: String::new(), + external_parent: None, + opened: false, + turn: None, + turn_seq: 0, + llm_seq: 0, + total_tools: 0, + pending_llms: Vec::new(), + tools: HashMap::new(), + compaction: None, + branch_summary: None, + last_ts: 0, + thinking_level: None, + git: self.git.clone(), + }) + } +} + +struct PendingLlm { + start_ms: i64, + input: Value, + first_token_ms: Option, + provider: Option, +} +struct ToolStart { + start_ms: i64, + name: String, + args: Value, +} +struct PiTranslator { + session_id: String, + root_span_id: String, + effective_root_span_id: String, + external_parent: Option, + opened: bool, + turn: Option<(String, Value)>, + turn_seq: u32, + llm_seq: u32, + total_tools: u32, + pending_llms: Vec, + tools: HashMap, + compaction: Option<(String, i64, Value)>, + branch_summary: Option<(String, i64, Value)>, + last_ts: i64, + thinking_level: Option, + git: Arc, +} + +impl AgentTranslator for PiTranslator { + fn handle(&mut self, envelope: &Envelope, ctx: &SessionCtx) -> anyhow::Result> { + self.last_ts = self.last_ts.max(envelope.ts_ms); + let event = envelope.payload.get("event").unwrap_or(&envelope.payload); + let mut ops = self.ensure_root(envelope, ctx); + match envelope.event.as_str() { + "before_agent_start" => ops.extend(self.start_turn(event, envelope.ts_ms)), + "context" => self.capture_context(event, envelope.ts_ms), + "before_provider_request" => self.provider_request(event), + "message_update" => self.streaming_update(event, envelope.ts_ms), + "thinking_level_select" => { + self.thinking_level = event + .get("level") + .and_then(Value::as_str) + .map(str::to_owned) + } + "message_end" => ops.extend(self.message_end(event, envelope.ts_ms)), + "tool_execution_start" => self.tool_start(event, envelope.ts_ms), + "tool_execution_end" => ops.extend(self.tool_end(event, envelope.ts_ms)), + "agent_end" if event.get("willRetry").and_then(Value::as_bool) != Some(true) => { + ops.extend(self.close_turn(envelope.ts_ms, None)); + } + "session_before_compact" => { + self.compaction = Some(( + ids::span_id(&self.session_id, &format!("compaction:{}", envelope.ts_ms)), + envelope.ts_ms, + event.clone(), + )) + } + "session_compact" => { + ops.extend(self.finish_special("Compaction", true, event, envelope.ts_ms)) + } + "session_before_tree" + if event + .pointer("/preparation/userWantsSummary") + .and_then(Value::as_bool) + == Some(true) => + { + self.branch_summary = Some(( + ids::span_id( + &self.session_id, + &format!("branch-summary:{}", envelope.ts_ms), + ), + envelope.ts_ms, + event.clone(), + )) + } + "session_tree" + if event.get("summaryEntry").is_some() || self.branch_summary.is_some() => + { + ops.extend(self.finish_special("Branch Summary", false, event, envelope.ts_ms)) + } + "session_shutdown" => { + ops.extend(self.close_turn(envelope.ts_ms, None)); + ops.push(self.close_root(envelope.ts_ms)); + } + _ => {} + } + let cwd = envelope.payload.get("cwd").and_then(Value::as_str); + self.git.enrich_rows(cwd, &mut ops); + Ok(ops) + } + + fn flush(&mut self, _ctx: &SessionCtx) -> anyhow::Result> { + let mut ops = self.close_turn(self.last_ts, Some("Interrupted before completion".into())); + if self.opened { + ops.push(self.close_root(self.last_ts)); + } + Ok(ops) + } +} + +impl PiTranslator { + fn ensure_root(&mut self, envelope: &Envelope, ctx: &SessionCtx) -> Vec { + if self.opened { + return Vec::new(); + } + self.opened = true; + let attached = ctx + .config + .as_ref() + .map(|c| c.attached_span_ids()) + .unwrap_or_default(); + let settings = envelope.payload.get("trace_settings"); + self.external_parent = attached.0.or_else(|| { + settings + .and_then(|value| value.get("parent_span_id")) + .and_then(Value::as_str) + .map(str::to_owned) + }); + self.effective_root_span_id = attached + .1 + .or_else(|| { + settings + .and_then(|value| value.get("root_span_id")) + .and_then(Value::as_str) + .map(str::to_owned) + }) + .or_else(|| self.external_parent.clone()) + .unwrap_or_else(|| self.root_span_id.clone()); + let mut metadata = Map::new(); + metadata.insert("session_id".into(), json!(self.session_id)); + metadata.insert("source".into(), json!("pi")); + metadata.insert("pi_version".into(), json!(envelope.source_version)); + metadata.insert( + "extension_version".into(), + envelope + .payload + .get("extension_version") + .cloned() + .unwrap_or(Value::Null), + ); + metadata.insert( + "native_session_id".into(), + envelope + .payload + .get("native_session_id") + .cloned() + .unwrap_or(Value::Null), + ); + metadata.insert( + "session_file".into(), + envelope + .payload + .get("session_file") + .cloned() + .unwrap_or(Value::Null), + ); + if let Some(extra) = ctx + .config + .as_ref() + .and_then(|c| c.additional_metadata.as_ref()) + .and_then(Value::as_object) + { + metadata.extend(extra.clone()); + } + if let Some(extra) = settings + .and_then(|value| value.get("additional_metadata")) + .and_then(Value::as_object) + { + metadata.extend(extra.clone()); + } + vec![SpanOp::Insert(SpanRow { + span_id: self.root_span_id.clone(), + root_span_id: self.effective_root_span_id.clone(), + parent_span_ids: self.external_parent.clone().into_iter().collect(), + name: "Pi".into(), + span_type: SpanType::Task, + start_ms: Some(envelope.ts_ms), + metadata: Some(Value::Object(metadata)), + ..Default::default() + })] + } + + fn start_turn(&mut self, event: &Value, ts: i64) -> Vec { + let mut ops = self.close_turn(ts, None); + self.turn_seq += 1; + self.llm_seq = 0; + self.pending_llms.clear(); + self.tools.clear(); + let id = ids::span_id(&self.session_id, &format!("turn:{}", self.turn_seq)); + let input = event.get("prompt").cloned().unwrap_or(Value::Null); + let skills = event + .get("prompt") + .and_then(Value::as_str) + .map(explicit_skills) + .unwrap_or_default(); + self.turn = Some((id.clone(), input.clone())); + ops.push(SpanOp::Insert(SpanRow{span_id:id,root_span_id:self.effective_root_span_id.clone(),parent_span_ids:vec![self.root_span_id.clone()],name:format!("Turn {}",self.turn_seq),span_type:SpanType::Task,start_ms:Some(ts),input:Some(input),metadata:Some(json!({"turn_number":self.turn_seq,"loaded_skill_names":skills,"thinking_level":self.thinking_level})),..Default::default()})); + ops + } + fn capture_context(&mut self, event: &Value, ts: i64) { + let input = event.get("messages").cloned().unwrap_or_else(|| json!([])); + self.pending_llms.push(PendingLlm { + start_ms: ts, + input, + first_token_ms: None, + provider: None, + }); + } + fn provider_request(&mut self, event: &Value) { + if let Some(call) = self.pending_llms.last_mut() { + call.provider = Some(event.clone()) + } + } + fn streaming_update(&mut self, event: &Value, ts: i64) { + if let Some(call) = self.pending_llms.last_mut() { + let kind = event + .pointer("/assistantMessageEvent/type") + .or_else(|| event.get("type")) + .and_then(Value::as_str) + .unwrap_or(""); + if matches!(kind, "text_delta" | "thinking_delta" | "text" | "thinking") + && call.first_token_ms.is_none() + { + call.first_token_ms = Some(ts) + } + } + } + fn message_end(&mut self, event: &Value, ts: i64) -> Vec { + let message = event.get("message").unwrap_or(event); + if message.get("role").and_then(Value::as_str) != Some("assistant") { + return vec![]; + }; + let Some((turn, _)) = &self.turn else { + return vec![]; + }; + self.llm_seq += 1; + let pending = if self.pending_llms.is_empty() { + PendingLlm { + start_ms: ts, + input: json!([]), + first_token_ms: None, + provider: None, + } + } else { + self.pending_llms.remove(0) + }; + let model = response_model(message); + let usage = message.get("usage").cloned().unwrap_or_else(|| json!({})); + let prompt = num(&usage, "input") + + num(&usage, "cacheRead") + + num(&usage, "cacheWrite") + + num(&usage, "cacheWrite1h"); + let completion = num(&usage, "output"); + let reasoning = num(&usage, "reasoning"); + let total = usage + .get("totalTokens") + .and_then(Value::as_i64) + .unwrap_or(prompt + completion + reasoning); + let output = normalize_assistant(message); + let error = message + .get("errorMessage") + .and_then(Value::as_str) + .map(str::to_owned) + .filter(|_| { + matches!( + message.get("stopReason").and_then(Value::as_str), + Some("error" | "aborted") + ) + }); + let ttft = pending + .first_token_ms + .map(|first| (first - pending.start_ms) as f64 / 1000.0); + vec![SpanOp::Insert(SpanRow { + span_id: ids::span_id( + &self.session_id, + &format!("llm:{}:{}", self.turn_seq, self.llm_seq), + ), + root_span_id: self.effective_root_span_id.clone(), + parent_span_ids: vec![turn.clone()], + name: model.clone().unwrap_or_else(|| "llm".into()), + span_type: SpanType::Llm, + start_ms: Some(pending.start_ms), + end_ms: Some(ts), + input: Some(pending.input), + output: Some(json!([output])), + metadata: Some( + json!({"model":model,"provider":message.get("provider"),"api":message.get("api"),"stop_reason":message.get("stopReason"),"thinking_level":self.thinking_level,"provider_request":pending.provider,"response_id":message.get("responseId")}), + ), + metrics: Some( + json!({"prompt_tokens":prompt,"completion_tokens":completion,"reasoning_tokens":reasoning,"tokens":total,"prompt_cached_tokens":num(&usage,"cacheRead"),"prompt_cache_creation_tokens":num(&usage,"cacheWrite")+num(&usage,"cacheWrite1h"),"time_to_first_token":ttft,"cost":usage.get("cost")}), + ), + error, + ..Default::default() + })] + } + fn tool_start(&mut self, event: &Value, ts: i64) { + let Some(id) = event.get("toolCallId").and_then(Value::as_str) else { + return; + }; + self.tools.insert( + id.into(), + ToolStart { + start_ms: ts, + name: event + .get("toolName") + .and_then(Value::as_str) + .unwrap_or("tool") + .into(), + args: event.get("args").cloned().unwrap_or(Value::Null), + }, + ); + } + fn tool_end(&mut self, event: &Value, ts: i64) -> Vec { + let Some((turn, _)) = &self.turn else { + return vec![]; + }; + let call = event + .get("toolCallId") + .and_then(Value::as_str) + .unwrap_or(""); + let tracked = self.tools.remove(call).unwrap_or(ToolStart { + start_ms: ts, + name: event + .get("toolName") + .and_then(Value::as_str) + .unwrap_or("tool") + .into(), + args: Value::Null, + }); + self.total_tools += 1; + let failed = event.get("isError").and_then(Value::as_bool) == Some(true); + let skill = skill_from_read(&tracked.name, &tracked.args); + let name = skill + .as_ref() + .map(|s| format!("skill: {s}")) + .unwrap_or_else(|| tracked.name.clone()); + vec![SpanOp::Insert(SpanRow { + span_id: ids::span_id(&self.session_id, &format!("tool:{}:{call}", self.turn_seq)), + root_span_id: self.effective_root_span_id.clone(), + parent_span_ids: vec![turn.clone()], + name, + span_type: SpanType::Tool, + start_ms: Some(tracked.start_ms), + end_ms: Some(ts), + input: Some(tracked.args), + output: event.get("result").cloned(), + metadata: Some( + json!({"tool_name":if skill.is_some(){"skill"}else{&tracked.name},"original_tool_name":tracked.name,"tool_call_id":call,"tool_approval":"approved","tool_outcome":if failed{"error"}else{"success"},"skill_name":skill}), + ), + error: failed.then(|| format_error(event.get("result"))), + ..Default::default() + })] + } + fn finish_special( + &mut self, + name: &str, + compaction: bool, + event: &Value, + ts: i64, + ) -> Vec { + let active = if compaction { + self.compaction.take() + } else { + self.branch_summary.take() + }; + let (id, start, input) = active.unwrap_or_else(|| { + ( + ids::span_id(&self.session_id, &format!("special:{name}:{ts}")), + ts, + Value::Null, + ) + }); + vec![SpanOp::Insert(SpanRow { + span_id: id, + root_span_id: self.effective_root_span_id.clone(), + parent_span_ids: vec![self.root_span_id.clone()], + name: name.into(), + span_type: SpanType::Task, + start_ms: Some(start), + end_ms: Some(ts), + input: Some(input), + output: Some(event.clone()), + metadata: Some( + json!({"event_type":if compaction{"session_compact"}else{"session_tree"}}), + ), + tags: Some(vec![if compaction { + "compaction".into() + } else { + "branch-summary".into() + }]), + ..Default::default() + })] + } + fn close_turn(&mut self, ts: i64, error: Option) -> Vec { + let Some((id, _)) = self.turn.take() else { + return vec![]; + }; + vec![SpanOp::Merge(SpanRow { + span_id: id, + root_span_id: self.effective_root_span_id.clone(), + end_ms: Some(ts), + error, + ..Default::default() + })] + } + fn close_root(&mut self, ts: i64) -> SpanOp { + self.opened = false; + SpanOp::Merge(SpanRow { + span_id: self.root_span_id.clone(), + root_span_id: self.effective_root_span_id.clone(), + end_ms: Some(ts), + metadata: Some( + json!({"total_turns":self.turn_seq,"total_tool_calls":self.total_tools}), + ), + ..Default::default() + }) + } +} + +fn num(v: &Value, key: &str) -> i64 { + v.get(key).and_then(Value::as_i64).unwrap_or(0) +} +fn response_model(v: &Value) -> Option { + for key in [ + "responseModel", + "routedModel", + "resolvedModel", + "actualModel", + "concreteModel", + "outputModel", + "model", + ] { + if let Some(s) = v.get(key).and_then(Value::as_str) { + return Some(s.into()); + } + } + None +} +fn normalize_assistant(v: &Value) -> Value { + let mut text = String::new(); + let mut reasoning = String::new(); + let mut calls = Vec::new(); + for part in v + .get("content") + .and_then(Value::as_array) + .into_iter() + .flatten() + { + match part.get("type").and_then(Value::as_str){Some("text")=>if let Some(s)=part.get("text").and_then(Value::as_str){text.push_str(s)},Some("thinking")=>if let Some(s)=part.get("thinking").and_then(Value::as_str){reasoning.push_str(s)},Some("toolCall")=>calls.push(json!({"id":part.get("id"),"type":"function","function":{"name":part.get("name"),"arguments":serde_json::to_string(part.get("arguments").unwrap_or(&Value::Null)).unwrap_or_default()}})),_=>{}} + } + let mut out = json!({"role":"assistant","content":text}); + if !reasoning.is_empty() { + out["reasoning"] = json!([{"id":"reasoning","content":reasoning}]) + } + if !calls.is_empty() { + out["tool_calls"] = Value::Array(calls) + } + out +} +fn skill_from_read(tool: &str, args: &Value) -> Option { + if tool != "read" { + return None; + } + let path = args + .get("path") + .or_else(|| args.get("filePath")) + .or_else(|| args.get("file_path")) + .and_then(Value::as_str)?; + if !path.to_ascii_lowercase().ends_with("/skill.md") { + return None; + } + path.rsplit('/').nth(1).map(str::to_owned) +} +fn explicit_skills(input: &str) -> Vec { + input + .split_whitespace() + .filter_map(|s| s.strip_prefix("/skill:")) + .map(|s| { + s.trim_matches(|c: char| matches!(c, ',' | ')' | '.' | ';')) + .to_string() + }) + .filter(|s| !s.is_empty()) + .collect() +} +fn format_error(v: Option<&Value>) -> String { + match v { + Some(Value::String(s)) => s.lines().next().unwrap_or(s).into(), + Some(v) => v + .get("error") + .or_else(|| v.get("message")) + .and_then(Value::as_str) + .unwrap_or("Tool execution failed") + .into(), + None => "Tool execution failed".into(), + } +} diff --git a/bt-daemon/tests/pi_translator.rs b/bt-daemon/tests/pi_translator.rs new file mode 100644 index 0000000..e1645d4 --- /dev/null +++ b/bt-daemon/tests/pi_translator.rs @@ -0,0 +1,124 @@ +use bt_daemon::wire::Envelope; +use bt_daemon::{Registry, SessionCtx, SpanOp, SpanRow, SpanType}; +use serde_json::json; +use std::collections::HashMap; + +fn event(name: &str, ts_ms: i64, native: serde_json::Value) -> Envelope { + Envelope { + source: "pi".into(), + source_version: Some("0.80.10".into()), + session_id: "pi-session".into(), + event: name.into(), + ts_ms, + payload: json!({"event":native,"extension_version":"0.10.0","cwd":"."}), + config: None, + } +} + +fn reduce(ops: Vec) -> HashMap { + let mut rows = HashMap::new(); + for op in ops { + match op { + SpanOp::Insert(row) => { + rows.insert(row.span_id.clone(), row); + } + SpanOp::Merge(update) => { + let row: &mut SpanRow = rows.entry(update.span_id.clone()).or_default(); + if update.end_ms.is_some() { + row.end_ms = update.end_ms; + } + if update.metadata.is_some() { + row.metadata = update.metadata; + } + if update.error.is_some() { + row.error = update.error; + } + } + } + } + rows +} + +#[test] +fn pi_builds_turn_llm_tool_compaction_and_shutdown_spans() { + let registry = Registry::default_agents(); + assert!(registry.sources().contains(&"pi".to_string())); + let mut translator = registry.create("pi", "pi-session"); + let ctx = SessionCtx { + session_id: "pi-session".into(), + config: None, + }; + let events = vec![ + event("session_start", 1, json!({"reason":"new"})), + event( + "before_agent_start", + 2, + json!({"prompt":"/skill:review inspect"}), + ), + event( + "context", + 3, + json!({"messages":[{"role":"user","content":"inspect"}]}), + ), + event( + "message_update", + 4, + json!({"assistantMessageEvent":{"type":"text_delta"}}), + ), + event( + "message_end", + 5, + json!({"message":{"role":"assistant","provider":"openai","model":"gpt-5","content":[{"type":"thinking","thinking":"reason"},{"type":"text","text":"done"}],"usage":{"input":5,"output":2,"cacheRead":3,"reasoning":1,"totalTokens":11}}}), + ), + event( + "tool_execution_start", + 6, + json!({"toolCallId":"call-1","toolName":"read","args":{"path":"skills/review/SKILL.md"}}), + ), + event( + "tool_execution_end", + 7, + json!({"toolCallId":"call-1","toolName":"read","result":"contents","isError":false}), + ), + event( + "session_before_compact", + 8, + json!({"preparation":{"tokensBefore":100}}), + ), + event( + "session_compact", + 9, + json!({"compactionEntry":{"summary":"short"}}), + ), + event("agent_end", 10, json!({"messages":[]})), + event("session_shutdown", 11, json!({"reason":"quit"})), + ]; + let mut ops = Vec::new(); + for event in events { + ops.extend(translator.handle(&event, &ctx).unwrap()); + } + let rows = reduce(ops); + assert_eq!( + rows.values() + .filter(|r| r.span_type == SpanType::Llm) + .count(), + 1 + ); + let llm = rows + .values() + .find(|r| r.span_type == SpanType::Llm) + .unwrap(); + assert_eq!(llm.metrics.as_ref().unwrap()["prompt_tokens"], 8); + assert_eq!(llm.metrics.as_ref().unwrap()["time_to_first_token"], 0.001); + let tool = rows + .values() + .find(|r| r.span_type == SpanType::Tool) + .unwrap(); + assert_eq!(tool.name, "skill: review"); + assert_eq!(tool.metadata.as_ref().unwrap()["tool_outcome"], "success"); + assert!(rows.values().any(|r| r.name == "Compaction")); + assert!(rows + .values() + .filter(|r| r.span_type == SpanType::Task) + .all(|r| r.end_ms.is_some())); +} diff --git a/src/plugins/pi/build.sh b/src/plugins/pi/build.sh new file mode 100755 index 0000000..d7bdbdc --- /dev/null +++ b/src/plugins/pi/build.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +TARGET_DIR="${1:?usage: build.sh }" +PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$PLUGIN_DIR/../../.." && pwd)" +STAGE="$(mktemp -d)" +trap 'rm -rf "$STAGE"' EXIT + +rm -rf "$TARGET_DIR" +mkdir -p "$TARGET_DIR" "$STAGE/src/runtime" +TARGET_DIR="$(cd "$TARGET_DIR" && pwd)" +rsync -a --exclude node_modules --exclude dist "$PLUGIN_DIR/content/" "$STAGE/" +cp "$REPO_ROOT/src/runtime/js-daemon-client/src/index.ts" "$STAGE/src/runtime/daemon-client.ts" + +(cd "$STAGE" && pnpm install --frozen-lockfile && pnpm exec vp fmt src/runtime/daemon-client.ts && pnpm run sync:version && pnpm run pack) +rm -rf "$STAGE/node_modules" +rsync -a "$STAGE/" "$TARGET_DIR/" +(cd "$TARGET_DIR" && pnpm pack --pack-destination "$TARGET_DIR" >/dev/null) + +echo "Built Pi npm package and tarball in $TARGET_DIR" diff --git a/src/plugins/pi/content/.gitignore b/src/plugins/pi/content/.gitignore new file mode 100644 index 0000000..ef4592e --- /dev/null +++ b/src/plugins/pi/content/.gitignore @@ -0,0 +1,25 @@ +# Dependencies +node_modules/ + +# Local package artifacts +*.tgz + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Local environment overrides +.env +.env.* + +# Extension/project-local config with potential secrets +.pi/braintrust.json + +# Local cache/state +.cache/ +dist/ + +# OS files +.DS_Store diff --git a/src/plugins/pi/content/.npmrc b/src/plugins/pi/content/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/src/plugins/pi/content/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/src/plugins/pi/content/.tool-versions b/src/plugins/pi/content/.tool-versions new file mode 100644 index 0000000..1dbe639 --- /dev/null +++ b/src/plugins/pi/content/.tool-versions @@ -0,0 +1,3 @@ +node 24 +pnpm 10.33.0 +npm:vite-plus 0.1.14 diff --git a/src/plugins/pi/content/AGENTS.md b/src/plugins/pi/content/AGENTS.md new file mode 100644 index 0000000..95547a4 --- /dev/null +++ b/src/plugins/pi/content/AGENTS.md @@ -0,0 +1,84 @@ +# AGENTS.md + +This file is consumed by agents. Keep it in sync with [CONTRIBUTING.md](./CONTRIBUTING.md), which is written for humans. For contributor-facing setup, repository conventions, architecture, and validation commands, see `CONTRIBUTING.md`; this file keeps the agent-specific guidance concise. + +## Project + +This repository contains `@braintrust/pi-extension`, a **pi extension/package** that adds **automatic Braintrust tracing** to pi sessions today. + +This project is intentionally **tracing-only**: + +- no MCP integration +- no Braintrust query tools exposed to the model +- no UI-heavy features unless directly required for tracing/debugging + +## Goals + +When working in this repo, optimize for: + +1. **Correct trace structure** in Braintrust +2. **Low overhead / non-blocking behavior** during pi sessions +3. **Safe failure behavior** — tracing must never break the pi session +4. **Simple install/use as a pi package** + +## Agent reminders + +- Read `README.md` first for external behavior. +- Use `CONTRIBUTING.md` for development workflow, repo layout, and validation details. +- This repo is managed by [mise](https://mise.jdx.dev/) using `.tool-versions`; prefer that as the toolchain source of truth. +- Preserve package identity in `package.json` as a pi package. +- Keep the extension entrypoint at `src/index.ts` unless there is a strong reason to change it. +- Keep `src/version.ts` generated from `package.json`; do not edit it by hand. After a package version bump, run `pnpm run sync:version` and commit the generated change. +- Prefer evolving the current design over rewriting it. +- Tracing must be best-effort: log failures and continue. +- Preserve the intended trace hierarchy unless there is a strong reason to change it: + +```text +Session (task) +├── Turn N (task) +│ ├── LLM call(s) (llm) +│ └── Tool call(s) (tool) +``` + +- Configuration precedence should remain: + 1. defaults + 2. `~/.pi/agent/braintrust.json` + 3. `.pi/braintrust.json` + 4. environment variables +- `.pi/braintrust.json` is extension-specific, not a built-in pi config file. +- Prefer integration coverage for lifecycle/session regressions: use the real pi SDK harness in `src/index.integration.test.ts` when testing restore, reload, session switch/fork, headless behavior, or cross-version compatibility. + +## What to avoid + +Unless explicitly requested, do **not**: + +- add MCP-related code +- add Braintrust query tools for the agent +- add unrelated pi commands/UI features +- add heavy dependencies +- make tracing depend on interactive-only pi behavior + +## Validation + +After code changes, at minimum run: + +```bash +pnpm run check +pnpm test +pnpm run smoke +``` + +If the change affects extension lifecycle behavior, session persistence/restore, reload/fork/switch flows, or pi compatibility assumptions, also run: + +```bash +pnpm run test:integration +``` + +If packaging changes or release version bumps were made, also run: + +```bash +pnpm run sync:version +pnpm run check +pnpm run pack +pnpm pack --dry-run +``` diff --git a/src/plugins/pi/content/CONTRIBUTING.md b/src/plugins/pi/content/CONTRIBUTING.md new file mode 100644 index 0000000..186385e --- /dev/null +++ b/src/plugins/pi/content/CONTRIBUTING.md @@ -0,0 +1,202 @@ +# Contributing to @braintrust/pi-extension + +Thanks for contributing. + +This document is for repository contributors and humans. Keep it in sync with [AGENTS.md](./AGENTS.md), which is consumed by agents. For installation and usage, see [README.md](./README.md). For publishing and release steps, see [docs/PUBLISHING.md](./docs/PUBLISHING.md). For agent-specific instructions, see [AGENTS.md](./AGENTS.md). + +## Project scope + +This repository contains `@braintrust/pi-extension`, a **pi extension/package** that adds **automatic Braintrust tracing** to pi sessions today. + +This project is intentionally **tracing-only**: + +- no MCP integration +- no Braintrust query tools exposed to the model +- no UI-heavy features unless directly required for tracing/debugging + +## Goals + +When working in this repo, optimize for: + +1. **Correct trace structure** in Braintrust +2. **Low overhead / non-blocking behavior** during pi sessions +3. **Safe failure behavior** — tracing must never break the pi session +4. **Simple install/use as a pi package** + +## Development setup + +This repo is set up for [Vite+](https://viteplus.dev/guide/). + +This repo is managed by [mise](https://mise.jdx.dev/). Use the pinned project toolchain from `.tool-versions`, including `node`, `pnpm`, and `vite-plus` (which provides `vp`): + +```bash +mise install +pnpm install +pnpm run sync:version +pnpm run check +pnpm run pack +pnpm run smoke +``` + +Or through package scripts: + +```bash +pnpm run check +pnpm run typecheck +pnpm run test:integration +pnpm run pack +pnpm run smoke +``` + +Notes: + +- `pnpm run sync:version` regenerates `src/version.ts` from `package.json` after a version bump. +- `pnpm run check` verifies `src/version.ts` is in sync, then runs the main formatting, linting, and type-check entrypoint. +- `pnpm run typecheck` delegates to `vp check`. +- `pnpm run pack` builds an optional library bundle in `dist/`. +- pi loads the extension directly from `src/index.ts`, so local development does not require a build step. +- Publishing is handled by `.github/workflows/publish.yml` using npm trusted publishing with OIDC and provenance attestations; see [docs/PUBLISHING.md](./docs/PUBLISHING.md). + +## Repository layout + +- `src/index.ts` + - main pi extension entrypoint + - subscribes to pi lifecycle events + - creates session / turn / llm / tool spans +- `src/client.ts` + - thin wrapper around the official Braintrust JavaScript SDK + - initializes the project logger + - creates, updates, and flushes spans safely +- `src/config.ts` + - loads config from: + 1. defaults + 2. `~/.pi/agent/braintrust.json` + 3. `.pi/braintrust.json` + 4. env vars +- `src/state.ts` + - persistent local session bookkeeping +- `src/utils.ts` + - content normalization, truncation, IDs, small helpers +- `src/types.ts` + - shared TypeScript types for config, spans, state, and normalized messages +- `src/version.ts` + - generated extension version constant derived from `package.json` + - do not edit by hand; run `pnpm run sync:version` after changing the package version + +## Repo conventions + +### Language / module system + +- Use **TypeScript ESM** (`.ts`). +- Keep dependencies minimal. +- Prefer Node built-ins over adding packages. +- This repo uses **Vite+** for checks and packaging. +- pi loads the extension directly from TypeScript; no build output directory is required for local development. +- `src/version.ts` is generated at package/build time from `package.json`; `prepack` regenerates it and `check` fails if it is stale. + +### Style + +- Keep code small and readable. +- Prefer small helper functions over deeply nested event handlers. +- Avoid clever abstractions unless they clearly improve tracing correctness. +- Do not introduce formatting churn. + +### Error handling + +- Tracing must be **best-effort**. +- Never throw from normal extension event paths if it can be avoided. +- Log failures and continue. +- If Braintrust is unavailable, pi should still work normally. + +### Performance + +- Avoid blocking network calls in critical event handlers whenever possible. +- Prefer the Braintrust SDK's async/background flushing path. +- Keep payloads reasonably small. +- Truncate large values before sending. + +## Trace model + +The intended hierarchy is: + +```text +Session (task) +├── Turn N (task) +│ ├── LLM call(s) (llm) +│ └── Tool call(s) (tool) +``` + +Expected behavior: + +- one **session/root span** per pi session that produces at least one turn +- one **turn span** per user prompt / agent run +- one **llm span** per assistant/model response +- one **tool span** per tool execution + +If changing event wiring, preserve this structure unless there is a strong reason to change it. + +## Configuration expectations + +This extension uses its own config files: + +- global: `~/.pi/agent/braintrust.json` +- project: `.pi/braintrust.json` + +This is modeled after the OpenCode Braintrust plugin, not Claude Code’s settings-based setup. + +Important: + +- `.pi/braintrust.json` is **extension-specific**, not a built-in pi config file. +- environment variables should remain the highest-precedence source. + +## What to avoid + +Unless the change explicitly calls for it, do **not**: + +- add MCP-related code +- add Braintrust query tools for the agent +- add unrelated pi commands/UI features +- add heavy dependencies +- make tracing depend on interactive-only pi behavior + +## Safe ways to extend + +Good additions include: + +- better parent/root span linking +- better session/fork/subagent handling +- improved metadata and token accounting +- more robust recovery across session switches +- test coverage / smoke checks +- publish/install polish for pi packages + +## Validation + +After code changes, at minimum run: + +```bash +pnpm run check +pnpm test +pnpm run smoke +``` + +If packaging changes or release version bumps were made, also run: + +```bash +pnpm run sync:version +pnpm run check +pnpm run pack +pnpm pack --dry-run +``` + +Test commands: + +- `pnpm test` — full unit + integration test suite +- `pnpm run test:integration` — integration coverage for real pi extension loading/session flows + +## Additional notes + +- Read `README.md` first for external behavior. +- Preserve package identity in `package.json` as a pi package. +- Keep the extension entrypoint at `src/index.ts` unless there is a strong reason to change it. +- Prefer evolving the current design over rewriting it. diff --git a/src/plugins/pi/content/LICENSE b/src/plugins/pi/content/LICENSE new file mode 100644 index 0000000..893ee37 --- /dev/null +++ b/src/plugins/pi/content/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Braintrust + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/plugins/pi/content/README.md b/src/plugins/pi/content/README.md new file mode 100644 index 0000000..e34588b --- /dev/null +++ b/src/plugins/pi/content/README.md @@ -0,0 +1,136 @@ +# @braintrust/pi-extension + +[![npm version](https://img.shields.io/npm/v/%40braintrust%2Fpi-extension)](https://www.npmjs.com/package/@braintrust/pi-extension) + +Braintrust extension for [pi](https://github.com/earendil-works/pi-coding-agent). + +Today this extension automatically traces pi sessions, turns, model calls, and tool executions to Braintrust. + +## What gets traced + +- **Session spans**: one root span per pi session that actually produces at least one turn +- **Turn spans**: one span per user prompt / agent run +- **LLM spans**: one span per model response inside a turn, including canonical token, cache, reasoning, estimated-cost, and time-to-first-token metrics +- **Tool spans**: one span per tool execution, including tools activated through pi's dynamic/deferred tool-loading flow +- **Compaction spans**: one span per session compaction, including trigger/retry metadata when available +- **Branch summary spans**: one span per summarized `/tree` navigation branch + +Trace shape: + +```text +Session (task) +├── Turn 1 (task) +│ ├── anthropic/claude-sonnet-4 (llm) +│ │ ├── read: package.json (tool) +│ │ └── bash: pnpm test (tool) +│ └── anthropic/claude-sonnet-4 (llm) +├── Compaction (task) +├── Branch Summary (task) +└── Turn 2 (task) +``` + +## Install + +### From npm + +```bash +pi install npm:@braintrust/pi-extension +``` + +### From this repo + +```bash +pi install . +``` + +Or load it just for one run: + +```bash +pi -e . +``` + +## Compatibility + +This package supports the **latest patch release from each of the last five stable pi minor versions**, currently excluding pi versions before `0.65.0`. + +Our GitHub Actions compatibility job automatically resolves and tests that compatibility window, so new pi releases are picked up without manually updating the matrix. + +## Quick start + +Tracing is disabled by default. + +Set these environment variables: + +```bash +export TRACE_TO_BRAINTRUST=true +bt auth login +export BRAINTRUST_PROJECT=pi +``` + +Then start pi normally. + +In interactive mode, the footer shows a `Braintrust` status indicator while tracing is active, and a widget below the editor shows a shortened clickable trace link when available. + +## Configuration + +You can configure the extension with environment variables or JSON config files. + +Config precedence is: + +1. defaults +2. `~/.pi/agent/braintrust.json` +3. `.pi/braintrust.json` +4. environment variables + +### Config file locations + +- Global: `~/.pi/agent/braintrust.json` +- Project: `.pi/braintrust.json` + +Example: + +```json +{ + "trace_to_braintrust": true, + "project": "pi", + "debug": true, + "additional_metadata": { + "team": "platform" + } +} +``` + +## Supported settings + +| Config key | Env var | Default | +|---|---|---| +| `trace_to_braintrust` | `TRACE_TO_BRAINTRUST` | `false` | +| `api_key` | `BRAINTRUST_API_KEY` | legacy; tracing authentication is owned by `bt auth login` | +| `api_url` | `BRAINTRUST_API_URL` | `https://api.braintrust.dev` | +| `app_url` | `BRAINTRUST_APP_URL` | `https://www.braintrust.dev` | +| `org_name` | `BRAINTRUST_ORG_NAME` | unset | +| `project` | `BRAINTRUST_PROJECT` | `pi` | +| `debug` | `BRAINTRUST_DEBUG` | `false` | +| `additional_metadata` | `BRAINTRUST_ADDITIONAL_METADATA` | `{}` | +| `log_file` | `BRAINTRUST_LOG_FILE` | unset | +| `state_dir` | `BRAINTRUST_STATE_DIR` | `~/.pi/agent/state/braintrust-pi-extension` | +| `show_ui` | `BRAINTRUST_SHOW_UI` | `true` | +| `show_trace_link` | `BRAINTRUST_SHOW_TRACE_LINK` | `true` | +| `parent_span_id` | `PI_PARENT_SPAN_ID` | unset | +| `root_span_id` | `PI_ROOT_SPAN_ID` | unset | + +## Notes + +- Project config overrides global config. +- Environment variables override both config files. +- Project config follows pi's configured project config directory, which defaults to `.pi`. +- The extension does not persist local span state; recovery and incomplete-operation cleanup are owned by the daemon journal. +- Span construction and Braintrust delivery run in the installed `bt` tracing daemon. +- Provider request tracing is allowlisted to effective model, thinking, output-limit, and tool-count settings; full provider payloads and thinking signatures are never logged. +- If Braintrust is unavailable, pi should continue working normally. +- If `PI_PARENT_SPAN_ID` is set, the pi session span is attached under an existing Braintrust trace. +- `PI_ROOT_SPAN_ID` can be used when the parent span is not the trace root. + +## Contributing + +See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup, validation, and repository conventions. diff --git a/src/plugins/pi/content/docs/PUBLISHING.md b/src/plugins/pi/content/docs/PUBLISHING.md new file mode 100644 index 0000000..ba4add8 --- /dev/null +++ b/src/plugins/pi/content/docs/PUBLISHING.md @@ -0,0 +1,120 @@ +# Publishing @braintrust/pi-extension + +This document describes how this package is released to npm and what the GitHub Actions publish workflow does. + +## Overview + +Publishing is handled by the manual workflow at: + +- `.github/workflows/publish.yml` + +It runs the **shared, centrally-maintained release actions** from +[`braintrustdata/sdk-actions`](https://github.com/braintrustdata/sdk-actions), pinned by commit +SHA. Bumping that SHA pulls in upstream release-tooling improvements. This repo adds no release +logic of its own — the workflow is just the shared actions wired together. + +Key properties: + +- The **version comes entirely from `package.json` at the released SHA** — the workflow never + computes, synthesizes, or overrides it. Everything published corresponds to a commit you can + check out, so **you must merge a version bump before releasing.** +- **`channel`** picks the npm dist-tag: `latest` (stable — also git tag + GitHub release) or + `rc`/`next`/`beta` (that dist-tag only). A prerelease is simply a committed prerelease version + (e.g. `0.8.0-rc.1`) published to a non-`latest` tag. +- npm **trusted publishing** via GitHub Actions OIDC + provenance — no long-lived `NPM_TOKEN`. +- You release an **explicit commit SHA** (not a branch), pinning the release to a reviewed commit. +- The publish job is gated by a **GitHub Environment** (manual approval) and posts **Slack** + notifications before approval and on completion. +- The package ships its `src/` directly (no build step); `npm publish`'s `prepack` regenerates + `src/version.ts`. + +The environments, npm trusted publisher, and Slack secret/variable are one-time setup — +see the `SETUP REQUIREMENTS` comment block at the top of `.github/workflows/publish.yml`. + +## Release steps + +1. **Bump the version first.** Update `package.json` to the target version (stable e.g. `0.8.0`, + or a prerelease e.g. `0.8.0-rc.1`), run `pnpm run sync:version`, commit, and **merge to `main`**. + The workflow reads the version from the commit you release — it cannot be set at dispatch. +2. In GitHub, open **Actions → Publish package** and **Run workflow**. +3. Set the inputs: + - `channel`: `latest` for a stable release; `rc`/`next`/`beta` for a prerelease. + - `sha`: the full 40-character SHA of the version-bump commit. + - `prev_release` (optional): release-notes anchor; defaults to the previous release tag. + - `dry_run` (optional): `true` to run the pipeline with `pnpm publish --dry-run` (no publish). +4. When the `publish` job requests it, **approve the `publish` environment**. +5. After it succeeds, verify: + - the version exists on npm (`npm view @braintrust/pi-extension dist-tags`) + - the provenance attestation is present on npm + - (channel `latest` only) the `pi-extension-v` tag was pushed and the GitHub release created + +CI already runs the full quality suite (version-sync, format, lint, types, tests, pack, smoke) +on every push, so the release workflow does **not** re-run it — it is concerned only with release +policy and publishing. + +## What the workflow does + +Flow: `validate → prepare → notify-pending → [approval gate] → publish`. + +1. **`validate`** (shared): reads the version from `package.json` at the SHA, then checks release + policy — the tag isn't already taken, the npm version isn't already published, the `channel` + is allowed, and the SHA is well-formed and on `main`. Runs with `build: false` (nothing to build). +2. **`prepare`** (shared): generates the release notes + PR list. +3. **`notify-pending`** (shared): posts a Slack notification that a release is pending approval. +4. **`publish`** (shared, gated): checks out the SHA and publishes the committed version to npm + with provenance (OIDC) on the chosen `channel`; for `channel: latest` it also pushes the + `pi-extension-v` tag and creates the GitHub release. Posts a Slack completion notification. + +`latest` → `latest` dist-tag + git tag + GitHub release. `rc`/`next`/`beta` → that dist-tag only, +no tag or release. + +## Authentication model + +The workflow uses GitHub Actions OIDC for npm trusted publishing. The `publish` job requests: + +```yaml +permissions: + contents: write # push the git tag + create the GitHub release + id-token: write # OIDC token for npm trusted publishing + provenance +``` + +## Tagging and release naming + +- git tag: `pi-extension-v` (channel `latest` only) +- GitHub release name: `@braintrust/pi-extension v` + +Example for `0.8.0`: tag `pi-extension-v0.8.0`, release `@braintrust/pi-extension v0.8.0`. + +## Failure modes + +- the target tag already exists, or the npm version is already published → `validate` fails early +- the channel isn't in the allowlist, or the SHA is malformed → `validate` fails +- the npm trusted publisher isn't configured for the `publish` environment → publish fails + with `ENEEDAUTH`. A `dry_run` does **not** exercise OIDC, so the first **real** publish is the + first true test — canary with a prerelease (`channel: rc`) before any stable release. +- the tag/GitHub release are created only after a successful publish, so a failed publish leaves + no orphaned tag + +## Local preflight checks + +The release workflow relies on CI for quality, but before bumping a version it's still useful to run: + +```bash +pnpm run sync:version +pnpm run check +pnpm test +pnpm run pack +pnpm pack --dry-run +pnpm run smoke +``` + +## Notes + +- The package is public; `package.json` includes `publishConfig.access = "public"`. +- The workflow is manual (`workflow_dispatch`) and releases an explicit `sha`, not a branch. +- `src/version.ts` is generated from `package.json` (by `scripts/sync-version.mjs`, run via + `prepack`) so trace metadata reports the version without runtime package-metadata loading. +- The shared actions are pinned to a `braintrustdata/sdk-actions` SHA in `publish.yml`; bumping it + adopts upstream improvements. +- If the release process changes, update this file, `AGENTS.md`, `CONTRIBUTING.md`, and + `.github/workflows/publish.yml` as needed. diff --git a/src/plugins/pi/content/package.json b/src/plugins/pi/content/package.json new file mode 100644 index 0000000..80901d2 --- /dev/null +++ b/src/plugins/pi/content/package.json @@ -0,0 +1,78 @@ +{ + "name": "@braintrust/pi-extension", + "version": "0.10.0", + "description": "Braintrust extension for pi. Includes automatic tracing for pi sessions, turns, LLM calls, and tool executions to Braintrust.", + "keywords": [ + "braintrust", + "llm", + "observability", + "pi", + "pi-package", + "tracing" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/braintrustdata/plugin-monorepo.git", + "directory": "src/plugins/pi/content" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "type": "module", + "main": "dist/index.mjs", + "types": "dist/index.d.mts", + "publishConfig": { + "access": "public" + }, + "scripts": { + "preinstall": "node -e \"const userAgent = process.env.npm_config_user_agent || ''; if (process.env.INIT_CWD === process.cwd() && !userAgent.includes('pnpm/')) { console.error('Use pnpm in this repo.'); process.exit(1); }\"", + "check": "node scripts/sync-version.mjs --check && vp check", + "fmt": "vp fmt", + "lint": "vp lint", + "prepack": "node scripts/sync-version.mjs", + "pack": "vp pack", + "sync:version": "node scripts/sync-version.mjs", + "test": "vitest run", + "test:integration": "vitest run src/index.integration.test.ts", + "test:watch": "vitest", + "typecheck": "vp check", + "smoke": "vp exec node -e \"import('./src/index.ts')\"" + }, + "dependencies": { + "valibot": "^1.3.1" + }, + "devDependencies": { + "@earendil-works/pi-ai": "^0.80.2", + "@earendil-works/pi-coding-agent": "^0.80.2", + "@types/node": "^26.0.0", + "braintrust": "^3.9.0", + "typebox": "^1.1.38", + "typescript": "^6.0.2", + "vite-plus": "^0.2.1", + "vitest": "^4.1.4" + }, + "peerDependencies": { + "@earendil-works/pi-coding-agent": "*" + }, + "devEngines": { + "packageManager": { + "name": "pnpm", + "version": ">=10.33.0", + "onFail": "error" + } + }, + "engines": { + "npm": "please-use-pnpm", + "pnpm": ">=10.33.0", + "yarn": "please-use-pnpm" + }, + "packageManager": "pnpm@10.33.0", + "pi": { + "extensions": [ + "./dist/index.mjs" + ] + } +} diff --git a/src/plugins/pi/content/pnpm-lock.yaml b/src/plugins/pi/content/pnpm-lock.yaml new file mode 100644 index 0000000..2c007e5 --- /dev/null +++ b/src/plugins/pi/content/pnpm-lock.yaml @@ -0,0 +1,4725 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + valibot: + specifier: ^1.3.1 + version: 1.4.2(typescript@6.0.3) + devDependencies: + '@earendil-works/pi-ai': + specifier: ^0.80.2 + version: 0.80.10(ws@8.21.1)(zod@4.4.3) + '@earendil-works/pi-coding-agent': + specifier: ^0.80.2 + version: 0.80.10(ws@8.21.1)(zod@4.4.3) + '@types/node': + specifier: ^26.0.0 + version: 26.1.1 + braintrust: + specifier: ^3.9.0 + version: 3.23.1(@aws-sdk/credential-provider-web-identity@3.972.65)(zod@4.4.3) + typebox: + specifier: ^1.1.38 + version: 1.1.38 + typescript: + specifier: ^6.0.2 + version: 6.0.3 + vite-plus: + specifier: ^0.2.1 + version: 0.2.5(@opentelemetry/api@1.9.0)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@6.0.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0) + vitest: + specifier: ^4.1.4 + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.1)(@vitest/browser-preview@4.1.10)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) + +packages: + + '@anthropic-ai/sdk@0.91.1': + resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-bedrock-runtime@3.1048.0': + resolution: {integrity: sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.975.3': + resolution: {integrity: sha512-7ur3kCKuvPLqlsZ2XlvnNBVQ7KkpSu6Y6dOTwSPHLrFpTEfZM8isLBJc4cgv96WB7GifeVM436mpycwxBd2vEA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.59': + resolution: {integrity: sha512-Ny5e4Mfh3QPmiAc0AiUe+cbTXDlxkU3Rc+EpWOfyWeWEy6yp7Fa1KmfNeCc+1a8by9zQ9gtohmiQUkMPScF3ng==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.61': + resolution: {integrity: sha512-8jAjgStl5Ytq4+HF3X/9f+EmRinaRbGRRtQGktlPfBRVx73H+R1y48vIeXerQtYGFaUqkEp3fT6jP854rVO2yQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.4': + resolution: {integrity: sha512-e6ZvVsj90aRALf1kHP+J4iqC1496ZpVgqI/+u0LJ5HL7q7ATauGy4gdDvRCP13L1pN/fMiZLah162PGIYkbUVQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.66': + resolution: {integrity: sha512-g2fsqm87r/nKthLZ0VkkDBElkGg0PvSa8d97HQ6EilMbJTZ6hxa8FxkSZyJfgPfFdZn0TTmkOffQmTSUcAHIng==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.70': + resolution: {integrity: sha512-3xzvkGdykBunxqh8WudmUpSyLWvIhfI6aBQo1b5rb3mDO5mNLadK+0hiI0qBQBMVynJbfLO+Ajy9dztMwy9O8w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.59': + resolution: {integrity: sha512-DlZF2/MhLlatDdlrIy3CUCpfdbLrKx+3SMjVo+WyHnPpwzkc/M3vwAHw4OVJf7DMvO+4vfRqSCMc/E9I1auN0g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.3': + resolution: {integrity: sha512-hmdDHoy2G5Es2e8IgelNMYUuSQI6uCIAKZMJ2u2PdKDhxvbk1uWD/g4+R7R5c/tJfKEB1+KjjWiaoCr/S+ZTiQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.65': + resolution: {integrity: sha512-gHQb/Kt0chjk/JQDa/GJDqmAvEuVn8n7z10wK2h0LFM9TUDRkohgOO4aEF+s2sBLM0br7Cl5W6P7phgjrrJvLQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/eventstream-handler-node@3.972.29': + resolution: {integrity: sha512-t3tKQRTVXsI2QNPE3CaNjHl0wRO9Xi3acZkAyti2RQsiFmZ9Gi0kArX2ighlRJ1BtDVuul413gThAgzyTfgmWA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-eventstream@3.972.24': + resolution: {integrity: sha512-oykin4mDWxNOuYQ7SF1cHzgYeuFEkF4cdRwgvjFFbIklkx09qIFBiOgsORafG9sXZFO3TayMmQuAQYgADXhI8w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-websocket@3.972.41': + resolution: {integrity: sha512-LSbGvvYmjc4Br9BPYI2dTLnIclmrSiQbahkP4D6nRGVEv4qsCZ8csVuKBPVEEFCVD+EEngGh8ROls6XpumtwMg==} + engines: {node: '>= 14.0.0'} + + '@aws-sdk/nested-clients@3.997.33': + resolution: {integrity: sha512-dVZOroI/r3/ENvqNGgjMPul+jjlz9GddfVusgTXlVjfZj5isibOxecLkGQbRPp8XOuX+RAfjXLFgPkD1JS5xrw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.41': + resolution: {integrity: sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1048.0': + resolution: {integrity: sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1088.0': + resolution: {integrity: sha512-4ObatWt2qpJg5FBk4LOOKrTQYzaqeewAtdO3r9ZO8lH9YqLtpTzLyIdy0mJ+nVdfYOnqISkKNfmzP22bNDhwyw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.2': + resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.8': + resolution: {integrity: sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.36': + resolution: {integrity: sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + 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/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@blazediff/core@1.9.1': + resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} + + '@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'} + + '@earendil-works/pi-agent-core@0.80.10': + resolution: {integrity: sha512-nwnOR3SuLYGRFfyQm8ri4Nj5VGVAvAM9GuqQd3u7BUQj0d6hmD2F8w7OHAAjThE3CuySIdM+v8E22QJG6/RfCg==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-ai@0.80.10': + resolution: {integrity: sha512-Moe/H8c87yacDGK9dPbWphZNjVsrb3nTrIHycOQJAkFEnY9PYxOOd74+ny44kATfPU9Dm7aTHefar3pZF+UKUA==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-coding-agent@0.80.10': + resolution: {integrity: sha512-aL4apbupCHiVLSXASXvRzH4Q2vmtfrDa+0s909CJuVu/GgGylbDzr7oyF1mPmip5E+VxYYxKWmph4hV04wUcQg==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-tui@0.80.10': + resolution: {integrity: sha512-c2JO29PbhKPEQ6fgHQKAl0WhwuFqzWfzspMmP+8B5tpDuP+0mvarRbKKg8gq4b+pQx/QX+6aVS4ko7deoyjQjg==} + engines: {node: '>=22.19.0'} + + '@emnapi/core@1.9.1': + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + + '@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/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-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-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/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-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/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-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/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-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-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-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-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-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-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-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-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/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-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/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-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/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/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/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-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-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] + + '@google/genai@1.52.0': + resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + + '@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==} + + '@mariozechner/clipboard-darwin-arm64@0.3.9': + resolution: {integrity: sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@mariozechner/clipboard-darwin-universal@0.3.9': + resolution: {integrity: sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==} + engines: {node: '>= 10'} + os: [darwin] + + '@mariozechner/clipboard-darwin-x64@0.3.9': + resolution: {integrity: sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + resolution: {integrity: sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + resolution: {integrity: sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + resolution: {integrity: sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + resolution: {integrity: sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + resolution: {integrity: sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + resolution: {integrity: sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + resolution: {integrity: sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@mariozechner/clipboard@0.3.9': + resolution: {integrity: sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==} + engines: {node: '>= 10'} + + '@mistralai/mistralai@2.2.6': + resolution: {integrity: sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + + '@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==} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/semantic-conventions@1.43.0': + resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} + engines: {node: '>=14'} + + '@oxc-project/runtime@0.139.0': + resolution: {integrity: sha512-WnuGdceWtRdqD7f3alOIDXN6bnGuGtFjtQf/dHjzgn2im7eKaYRJTEl2T1kFEWPhBWCDk+UDYgsTLUE5L6jc0w==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + + '@oxfmt/binding-android-arm-eabi@0.58.0': + resolution: {integrity: sha512-Uz62sHduGGPftXtILGyxdSW4PX82rUg+rfdNqhsgxe881g4rIoXlIqmZQ6HVKcF4f+F8qMhdD03Bx5u7gmeTdg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.58.0': + resolution: {integrity: sha512-rD0lRaJp1b+9vw6X4A2dJWKukd6X8yxiicN4JxXcXayolmUypRZxk+lKR+fVOu5q/iYc0fh5fR4bgmfOfVlbaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.58.0': + resolution: {integrity: sha512-uzbPPk7O6M+w2K65vcQ1woga3wgP8zghjL1KOG5b6qJ8dvYHZJ1VShaslg2KOK6yQIwCQtcMCXqLBM6sqXUNTg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.58.0': + resolution: {integrity: sha512-L0nKYDxU32oxeQqJj21W9SlIMnf81VZEhyah6iDvFhf5q0oynq498Fopth7blErUJVBpVtxQ98RMCfMPqpJX6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.58.0': + resolution: {integrity: sha512-woNwfD58dC5PGS9LSLSD5JYfo/EFK5iG9vhDWkcCg3q78ag7KC8bpDqgvPHrMoXpx83OLXxoSOhu6z8FsVTHlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.58.0': + resolution: {integrity: sha512-Sqs8nMLxuQpY21NKJ1u4stPDmO5hskBCNNh2E3AdCfI1QqWtf4m+Qn4mGEIUO4KGmuq3SWc/SZ80uy5IiwTCDw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.58.0': + resolution: {integrity: sha512-Vd4exzBI5B5hB9m22JiTQzIL23WvHo/Pe+sNXPNeBLXSP9swCBPKCEBRwKpmpQzYhlgYaCgfPcGXPKAJBRIiZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.58.0': + resolution: {integrity: sha512-bUWi5mHV+4Vi56RLHE1h6q/HHfwAIT3XoB9vJAVeRzfu5NriXM8y6eeJu0vlKa0C9kq2rq1sOWRClhdLHPocrg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.58.0': + resolution: {integrity: sha512-2ZHxemzgHcjtktAuVUwSoyXmGo/t+aF5tS1ciPpPei4rhSyrz3JOqDosXXrmhN/yLUSzJjtuW7ToTWqfQpCj2w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.58.0': + resolution: {integrity: sha512-AwKkVwjVmFQ3bcO7j0McGYAqCKH2a326fswfofng/E8VewCT/raeeGQr4huVhY704deK8AWASSTlxzMj0eZc6Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.58.0': + resolution: {integrity: sha512-xsRpTxfUnJF8D3AUKko/qyWdjw4GZVHlCVFuGlzSCTeewLmykKINW8em1+wx+axsDVtJJcMtvsiaXggXxrlHgw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.58.0': + resolution: {integrity: sha512-Z4AYOTcy7nYEIiXwD62PlerimyYRcfJOgUbQAEBjXz098kxKuERBlRntofGy69HHhe9E0TLVNMl1yspVNu+efw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.58.0': + resolution: {integrity: sha512-A3nhhtZPC/TKVWOPj9q/H3p2znJDCcHWYlJBhWL8hGq/bFmBaNBHC8Np6E581yVq1w9Mi3rMDNzDalWvtUfJtQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.58.0': + resolution: {integrity: sha512-2g+tVkgwqphw8R4hgo+kF4oz8+P5RwVOtr9+irsC7uwEp0e9j7Crw8kDGKL20uYlLPD7g02DqA61mC/UNYx98A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.58.0': + resolution: {integrity: sha512-rc15P6AbyyB7426aN8AakLd02Trb3a6ML/mmfAQeVHJEfVofWLcWIrBdy6zDEY+DIaL/s8E4GGPboVw+oP3+EA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.58.0': + resolution: {integrity: sha512-ZWoTM27/HYPOh9iq86DAbhPu9nXb8qKvvGU/h8OfliyVUFAMMNTLDkGsWDKKnDqIkqvZ9+dXlgUOsH1LYO3O7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.58.0': + resolution: {integrity: sha512-LHZnqFXe2dEfkRI4XdZS/57nEOT/I4UCRX5IyM9v4GYW9XwQCjGe1IUK59SuKw3POwvcgWQ4pme2cYXmNqTNPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.58.0': + resolution: {integrity: sha512-mZKpg20TpheCJym1rarcZCUJeW1sSruw8zAAaCYWvuVfwIUDN1CXdrPU/JgCWReXTCTrEfCB8Wyo3hh9jSZ2EA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.58.0': + resolution: {integrity: sha512-N/wUU4N5PZ2orBtI+Ko7MnMfYLfE7K91UrGMY/c/pYyHR3lA9kwst1XugkZx+92YcRh/Eo+iv2eTESSWXfiZPA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint-tsgolint/darwin-arm64@0.24.0': + resolution: {integrity: sha512-C2uMmwK5Bc4ri4ysZ6sA8Rcu+A5zBQTp6ml2u0CLLbRZp4kMFPV3yWk8B5DK9Aw7y9bbjogIm75tUwGLFzlsYQ==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@0.24.0': + resolution: {integrity: sha512-Wgvt/1lRbDxmoNqWQKKcL+UIiqLmdJ+EWLpQa1qzoNVAfNB0PJpa82/8dH1twT/3rSs4zrP5TXPWl4juB71WuQ==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@0.24.0': + resolution: {integrity: sha512-PB1rxII7KV83+ASY4sSkXtqvpij6ME66+QCRL49uksi/ofs2Rf/UVboYr095n0Rkbl2wgvlsHGl6DHC361jQUQ==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@0.24.0': + resolution: {integrity: sha512-xcz3CxKmjTQLREtE/UShh+ruWmm9nAb7UM9zKcD65BStiuYgOakAKkPHl4YS5DztpVcDrE0+HqbOolTlRKYWmw==} + cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@0.24.0': + resolution: {integrity: sha512-A2i6ZGBec3i20S7RaxkgHc6r3HYtD5Mn7j/mb22NkTz14u0JuudvTu6JggAnbGMcv8+dBKQI//EasxSPJLD8pw==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@0.24.0': + resolution: {integrity: sha512-0ZbGd9qRB6zs82moekaKdEvncRANq49EAwfNX62JpTS46feXUhKAuoyVDvZMj6Rywejylrmmu79Wo6faYCo4Ew==} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.73.0': + resolution: {integrity: sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.73.0': + resolution: {integrity: sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.73.0': + resolution: {integrity: sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.73.0': + resolution: {integrity: sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.73.0': + resolution: {integrity: sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.73.0': + resolution: {integrity: sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.73.0': + resolution: {integrity: sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.73.0': + resolution: {integrity: sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.73.0': + resolution: {integrity: sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.73.0': + resolution: {integrity: sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.73.0': + resolution: {integrity: sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.73.0': + resolution: {integrity: sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.73.0': + resolution: {integrity: sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.73.0': + resolution: {integrity: sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.73.0': + resolution: {integrity: sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.73.0': + resolution: {integrity: sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.73.0': + resolution: {integrity: sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.73.0': + resolution: {integrity: sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.73.0': + resolution: {integrity: sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint/plugins@1.73.0': + resolution: {integrity: sha512-OhgMQeMmZA0dcFcX4/priaJZWdFECxiClgq6mRX6aatZEcV9PbKC3P3/v8U1hVjviT1i5U+vR8lAtBV6m4FXAA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + + '@silvia-odwyer/photon-node@0.3.4': + resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} + + '@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==} + + '@smithy/core@3.29.5': + resolution: {integrity: sha512-i0dk2t5B+CwV/dcJdUHILYkOQF5lof8f44dFCfDWToGCxjT9YQ+CgHqTAvJxzc3+zqQwm2QtVoJ5IqiNar/CnQ==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.4.10': + resolution: {integrity: sha512-MJenAe4OKRZUo1LdYYFDCsSHxaHvInIU/z52GsheO9vl1/VSySVCr0zkyKD6TFiGkSUaWGxvKZ/70OvgUZR5HQ==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.6.7': + resolution: {integrity: sha512-3zpg8yqqyXzoK2TsRDdkqVOj2RDBFfLXwCczOZ5c7TWB4eiaebfSCsbMjDPYB3PJ9ihV62QaeadZ+wLadZtNGA==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.7.3': + resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.9.7': + resolution: {integrity: sha512-wCU8HCLjAtAVqxxe0j2xff9LcEPw3yjBbg5IdQDIYFnxnPxbxcSLc7rgex7kqm9L/WYOnJEgaWQlfDkZleozMA==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.6.6': + resolution: {integrity: sha512-efP6DN3UTFrzIsGO42/xcabv8jU7+9nwEdphFUH7yL0k010ERyAWaO41KFQIDLcFZLZ8xzIQr4wplFxNzslSGQ==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.16.1': + resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@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@26.1.1': + resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + + '@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/browser-preview@4.1.10': + resolution: {integrity: sha512-14MJrL59ZFkqXLjwfSk6RzTDy5Czf9UG4+8q8L6Gxjs2aPjEce/cVNYV14bXAc2BvMjUNu904+ZEZA1Xc1wtvQ==} + peerDependencies: + vitest: 4.1.10 + + '@vitest/browser@4.1.10': + resolution: {integrity: sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng==} + peerDependencies: + vitest: 4.1.10 + + '@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==} + + '@voidzero-dev/vite-plus-core@0.2.5': + resolution: {integrity: sha512-fxMGImIOyOipwCX6udOD1S9Q1xXfaimv6kcRgLWBxLsy7oryAyXqVfoYr7bmmAdSDlIutHRgvA6eiqfJjARTHA==} + engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@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 + publint: ^0.3.8 + 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 + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 + unplugin-unused: ^0.5.0 + unrun: '*' + yaml: ^2.4.2 + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + publint: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + unrun: + optional: true + yaml: + optional: true + + '@voidzero-dev/vite-plus-darwin-arm64@0.2.5': + resolution: {integrity: sha512-M62R3gmoHZbhL+UHHTevJi9a3aJyY+Eid8GAOtxEsRMkHmJ8IwOSOBERXM3C4CULvEa/ORYKiUQnqo5ewF44Fw==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [darwin] + + '@voidzero-dev/vite-plus-darwin-x64@0.2.5': + resolution: {integrity: sha512-a1h1dv/7QcnlqlN6yZBIPjgHSxbeyY/IcTxepTAOpPB7eAi1RPb1+cCkwo7c3MnDPJb3iZzwD0rm2+fOUoZp0w==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [darwin] + + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.2.5': + resolution: {integrity: sha512-t8bS8fA2a3OSAEaHdgJFmzd0TkWh9yAxIoKAprsOleIcUEmzDxhH8drTj9TPyTrChKpv0aJTsK5ZK3RzcCUkdg==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@voidzero-dev/vite-plus-linux-arm64-musl@0.2.5': + resolution: {integrity: sha512-vnsjQI3LEUYFMR3LCMqtAxaZav8BNypSAf8YzcFu9+Qtd1dcCrAUz9RrEBCIIqEiw0p0O+SrX2CcMHSSnzWbKA==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@voidzero-dev/vite-plus-linux-x64-gnu@0.2.5': + resolution: {integrity: sha512-3xlXrxIz8UKGcGefifkhoMpsTIMdgqikwQuDUqgG5O7/b2tetpK9aoT4C9b2fQGkhYUpCJwdD83AtywQ4EhoWA==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@voidzero-dev/vite-plus-linux-x64-musl@0.2.5': + resolution: {integrity: sha512-XylGiayBoD7vt1/SKfmh5FoBNdKI5EWlIb5Sd9A1oTQW8DLi97VcEgVZ7vh/8kG1OEe+9z1lyRis6RDql2KDUw==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.2.5': + resolution: {integrity: sha512-eN1zvUAqXVXOC72WOVu9gz/jxr9tBrga/5lCsDjHeZDJ/bzhDVJ4eYZUDZzasPE1QCbAhmuIJsv0WzEUxdGAzA==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [win32] + + '@voidzero-dev/vite-plus-win32-x64-msvc@0.2.5': + resolution: {integrity: sha512-jgXVYK8crlR5cQ07vX5Qw2K+boNLKMxarPgE3/AyPPRUtGC8iCpZz0RCPoJHDmTh7848Ra5Fnt4LEfbcUv1ByQ==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [win32] + + '@yuku-codegen/binding-darwin-arm64@0.5.48': + resolution: {integrity: sha512-yo96Oef12WzqnphInfz/eexVse3+kWgfGS5g2S3rFS3dcGn1ENW9xLFDZUP9rh+yP76DOq38wBoFi1+I9+6qBg==} + cpu: [arm64] + os: [darwin] + + '@yuku-codegen/binding-darwin-x64@0.5.48': + resolution: {integrity: sha512-aRCTw0EZC4bVosmw//0OMYP5tGWFE0Cu5yUBFkUbhXx/iBzvORcJ2xPNlOp/vtCCo9Ys4vp8b0DigJV6uOVb2g==} + cpu: [x64] + os: [darwin] + + '@yuku-codegen/binding-freebsd-x64@0.5.48': + resolution: {integrity: sha512-CA0AQAEApDkbw51PdLWMtKPJ41/7rvXsS3SJs+phG7fHJI+MuFzWuLbkucZfZoEOiDscmcsfYIdgL8BsfuyKKQ==} + cpu: [x64] + os: [freebsd] + + '@yuku-codegen/binding-linux-arm-gnu@0.5.48': + resolution: {integrity: sha512-DuSQlk8bH4gpmW3/00P0NLagAcMv8jOxjT40cQmxKRkktr+SUOALCfkT89tdDq3qtY95NR2GXOZ7AjNh7KKqCw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-arm-musl@0.5.48': + resolution: {integrity: sha512-bxj4Ee+wlaJcWJwft2ReJXWw5sfl1qavDz6+dlRdU1xfTEtjPSNiAWhiCHnJR0R4Ygd57DnzSQmAVGvFv6RcGw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-linux-arm64-gnu@0.5.48': + resolution: {integrity: sha512-mk5JVWh+0JOe5ue8k17kbYX8uGBoKt3ZqoCyxNh4nYAAcX7+X1tFUiU7jbjctu4vHeejCBFSTdQ021+V31cUCQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-arm64-musl@0.5.48': + resolution: {integrity: sha512-4q3vkrNghbllyxOm2KesFLxCPKHF7r3JyQ7BWZccY1j2Y05yKoIFhoWCqIuQ2W/dpte9RI0+OVfwyxnrKg6fkA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-linux-x64-gnu@0.5.48': + resolution: {integrity: sha512-csd4M1EVrGaohM8acM6gq1zpUA/Rwe2ulUMBKUcwQXm/k6n7cq1A++qdew78SOVb4do3JH1WE+WFwoGQAcWc1w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-x64-musl@0.5.48': + resolution: {integrity: sha512-KcDuEOT+GFoVKdvAWOv1v9iYjwnmvMZlO+j1Rw+5PYdeFLGWGzv/DD11y4SAAdwXIFcil4T0hibeIaF82WStMg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-win32-arm64@0.5.48': + resolution: {integrity: sha512-HI8qNrI8dWM5BuqIMKsqornRvTNFrE6sm5zToIJ9YIa9zt5+29P7fJ7Nr39EVf6dAWSb6q7JSpScJnRsQ+FgZA==} + cpu: [arm64] + os: [win32] + + '@yuku-codegen/binding-win32-x64@0.5.48': + resolution: {integrity: sha512-X5YWJLO6EfBZpeBqO0AYESnUizbpFDWArcvVD61w0PEWQ3CaFRLnbQXs+kpM4ZZfGMfIE22zfA08QSY67q7TNQ==} + cpu: [x64] + os: [win32] + + '@yuku-parser/binding-darwin-arm64@0.5.48': + resolution: {integrity: sha512-If8mb7HH3vqghJ2NNZ8SuHfhsnjVzOxJpB8xcNOXS5WjYrs2mUhHIh5KOIvK13hDOzh0htGeGK3A6MsiEqE7HQ==} + cpu: [arm64] + os: [darwin] + + '@yuku-parser/binding-darwin-x64@0.5.48': + resolution: {integrity: sha512-EimvPXfspzxf1K11eB6tCW5oiQEXB8g84T2wP1TwzQagdDKo33bkmmVF0B32vTIpXnk/Ifu5IB61izZ1MylljA==} + cpu: [x64] + os: [darwin] + + '@yuku-parser/binding-freebsd-x64@0.5.48': + resolution: {integrity: sha512-0GcUMrumLHheThY9r5Tp46gaZYzn0irWPS1Zba6WY+vVQfhUtzGiWgXxI6tuXX0N32kEaaEVRpkKctvo6Kx3aQ==} + cpu: [x64] + os: [freebsd] + + '@yuku-parser/binding-linux-arm-gnu@0.5.48': + resolution: {integrity: sha512-8S5T5wjCC73dmmpQeZ49aYsSunIUM3D4Fc6rdK96c+Ayg/p3FmeSPF3xuLZHejcTmqJIIvnbfPlUF+rB6DITjQ==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-arm-musl@0.5.48': + resolution: {integrity: sha512-tTmbxvnUHcK2/crS9547vk2SMmsajH1yqJ8ltXhIuHJgqR1v+d9n9KT+kSayo/5CS76LegeYxhMFjEivBH2hFA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-linux-arm64-gnu@0.5.48': + resolution: {integrity: sha512-KGYCBMqI2zfwyhgq5tpPVNe7jpUeYTBm8DhjdS+zqWNumde/PEC170QE5RHxcOAlsirIDeIUk0jqx+r/axoFSw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-arm64-musl@0.5.48': + resolution: {integrity: sha512-2wTSMsCSXLTc2lZUjMAuU5X4cje55u205WJqfV5NWNF6j9pW/tXyxr15dJeekj8ziLqBXzIsj4DbRh4sY/WcjA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-linux-x64-gnu@0.5.48': + resolution: {integrity: sha512-d/6v9UnGglVu1WC2JQyv/5aWSi5fXZeGSlidCfmHp4+N65N1GDKUnFtys5MK5eAPeAjTgSHGGtOc/yCcKTlv3A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-x64-musl@0.5.48': + resolution: {integrity: sha512-gX19gw6u4ApPy7SYMPKfFlEkrtj6WlORvrTKK3sBQqjyV+8+mUAkQgxXNjHw4RnOiAmVYg7TOlZcg8d+Qqod9A==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-win32-arm64@0.5.48': + resolution: {integrity: sha512-w6cQQLbqj3Jcom5Q7ifm103NUOQ9d+Cb4VU5lkrZDjMnwVJ9Hzzg1vCQR7miJuF44vhCXldbme5UryE3giEKlA==} + cpu: [arm64] + os: [win32] + + '@yuku-parser/binding-win32-x64@0.5.48': + resolution: {integrity: sha512-4gO0HmG7fzFxrw1rs0dUdnnaY9YgennjETqDWrTSp7x9fmTUOAoN4VsMfP7YyliQeG1WJJHc55O+rOhmsLppow==} + cpu: [x64] + os: [win32] + + '@yuku-toolchain/types@0.5.43': + resolution: {integrity: sha512-kSpvPntnXw5+lYjO71ffBEnQ5ycQ74KGIYknh0TS4xeyCuBkOqxyJumxZkMhLBBUCLjDAbx2+Icnr3Zh4ftjpQ==} + + 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 + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + 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@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + + 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-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.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'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + 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.*} + + 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'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + 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 + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + 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'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + 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'} + + 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.0: + resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} + + 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 + + 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'} + + eventsource-parser@1.1.2: + resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==} + engines: {node: '>=14.18'} + + 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'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + 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'} + + 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==} + + gaxios@7.2.0: + resolution: {integrity: sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + 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'} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + google-auth-library@10.9.0: + resolution: {integrity: sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + + 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'} + + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + hosted-git-info@9.0.3: + resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==} + engines: {node: ^20.17.0 || >=22.9.0} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + 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==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + marked@18.0.5: + resolution: {integrity: sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==} + engines: {node: '>= 20'} + hasBin: true + + 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'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + 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.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + 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==} + + openai@6.26.0: + resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + oxfmt@0.58.0: + resolution: {integrity: sha512-8feG/7NVEHDVwc1OUpP6Pks+TnaDFUw2jLLFIMi5bcmmwxAX2wBQvjSzj62RRTYBf2Op1Wt8xbkmagmPTR5ETg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + + oxlint-tsgolint@0.24.0: + resolution: {integrity: sha512-giCk5sEvG02d5tzPmFMX3hem8ndzEEu1xvGYS5OwNfO2WGl6ZVxt5LjE0yiMDoz94INI7XkXwgFAQiydPvVHDw==} + hasBin: true + + oxlint@1.73.0: + resolution: {integrity: sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.24.0' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + partial-json@0.1.7: + resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + 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'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + + postcss@8.5.19: + resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} + engines: {node: ^10 || ^12 || >=14} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} + engines: {node: '>=12.0.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + 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'} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + 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.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + 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==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + 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==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + simple-git@3.36.0: + resolution: {integrity: sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + 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==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + termi-link@1.1.0: + resolution: {integrity: sha512-2qSN6TnomHgVLtk+htSWbaYs4Rd2MH/RU7VpHTy6MBstyNyWbM4yKd1DCYpE3fDg8dmGWojXCngNi/MHCzGuAA==} + engines: {node: '>=12'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + 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'} + + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.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'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + typebox@1.1.38: + resolution: {integrity: sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + undici@8.5.0: + resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==} + engines: {node: '>=22.19.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'} + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + valibot@1.4.2: + resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite-plus@0.2.5: + resolution: {integrity: sha512-QNJ0FnN8rfs5u8lZKZ1uR2Tegjg3VkT0AGTxSLGHg4fYmGxRNfAV0YX1parZrVa4VybSI56SWCoo7wQ6D7pMew==} + engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} + hasBin: true + peerDependencies: + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + peerDependenciesMeta: + '@vitest/browser-playwright': + optional: true + '@vitest/browser-webdriverio': + optional: true + + vite@8.0.3: + resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.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 + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.1: + resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yuku-codegen@0.5.48: + resolution: {integrity: sha512-p7HxD5Xl4jzDzqMrGePAOeSHmRY4g58h4HuGq15weQFPxuPWd/W6e7nqp/+Lea6JfpOdBwJOAyXFqIZ/J9Zfnw==} + + yuku-parser@0.5.48: + resolution: {integrity: sha512-OWBfhrpgK9+/4+IXG9oT8Bao4AhViQA7vdyNNH7EUg8dQYgwa70XtIBWTpCEme1P1ECyoDNYkn0wT63f8XRcVA==} + + 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: + + '@anthropic-ai/sdk@0.91.1(zod@4.4.3)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.4.3 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.974.2 + '@aws-sdk/util-locate-window': 3.965.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.974.2 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-bedrock-runtime@3.1048.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/credential-provider-node': 3.972.70 + '@aws-sdk/eventstream-handler-node': 3.972.29 + '@aws-sdk/middleware-eventstream': 3.972.24 + '@aws-sdk/middleware-websocket': 3.972.41 + '@aws-sdk/token-providers': 3.1048.0 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/fetch-http-handler': 5.6.7 + '@smithy/node-http-handler': 4.9.7 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/core@3.975.3': + dependencies: + '@aws-sdk/types': 3.974.2 + '@aws-sdk/xml-builder': 3.972.36 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.29.5 + '@smithy/signature-v4': 5.6.6 + '@smithy/types': 4.16.1 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.59': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.61': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/fetch-http-handler': 5.6.7 + '@smithy/node-http-handler': 4.9.7 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.973.4': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/credential-provider-env': 3.972.59 + '@aws-sdk/credential-provider-http': 3.972.61 + '@aws-sdk/credential-provider-login': 3.972.66 + '@aws-sdk/credential-provider-process': 3.972.59 + '@aws-sdk/credential-provider-sso': 3.973.3 + '@aws-sdk/credential-provider-web-identity': 3.972.65 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/credential-provider-imds': 4.4.10 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.66': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.70': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.59 + '@aws-sdk/credential-provider-http': 3.972.61 + '@aws-sdk/credential-provider-ini': 3.973.4 + '@aws-sdk/credential-provider-process': 3.972.59 + '@aws-sdk/credential-provider-sso': 3.973.3 + '@aws-sdk/credential-provider-web-identity': 3.972.65 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/credential-provider-imds': 4.4.10 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.59': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.973.3': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/token-providers': 3.1088.0 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.65': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/eventstream-handler-node@3.972.29': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-eventstream@3.972.24': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.41': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/fetch-http-handler': 5.6.7 + '@smithy/signature-v4': 5.6.6 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.33': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/signature-v4-multi-region': 3.996.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/fetch-http-handler': 5.6.7 + '@smithy/node-http-handler': 4.9.7 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.41': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/signature-v4': 5.6.6 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1048.0': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1088.0': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/types@3.974.2': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.8': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.36': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.3.0': {} + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/runtime@7.29.7': {} + + '@blazediff/core@1.9.1': {} + + '@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 + + '@earendil-works/pi-agent-core@0.80.10(ws@8.21.1)(zod@4.4.3)': + dependencies: + '@earendil-works/pi-ai': 0.80.10(ws@8.21.1)(zod@4.4.3) + ignore: 7.0.5 + typebox: 1.1.38 + yaml: 2.9.0 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-ai@0.80.10(ws@8.21.1)(zod@4.4.3)': + dependencies: + '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) + '@aws-sdk/client-bedrock-runtime': 3.1048.0 + '@google/genai': 1.52.0 + '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0) + '@opentelemetry/api': 1.9.0 + '@smithy/node-http-handler': 4.7.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + openai: 6.26.0(ws@8.21.1)(zod@4.4.3) + partial-json: 0.1.7 + typebox: 1.1.38 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-coding-agent@0.80.10(ws@8.21.1)(zod@4.4.3)': + dependencies: + '@earendil-works/pi-agent-core': 0.80.10(ws@8.21.1)(zod@4.4.3) + '@earendil-works/pi-ai': 0.80.10(ws@8.21.1)(zod@4.4.3) + '@earendil-works/pi-tui': 0.80.10 + '@silvia-odwyer/photon-node': 0.3.4 + chalk: 5.6.2 + cross-spawn: 7.0.6 + diff: 8.0.4 + glob: 13.0.6 + highlight.js: 10.7.3 + hosted-git-info: 9.0.3 + ignore: 7.0.5 + jiti: 2.7.0 + minimatch: 10.2.5 + proper-lockfile: 4.1.2 + semver: 7.8.0 + typebox: 1.1.38 + undici: 8.5.0 + yaml: 2.9.0 + optionalDependencies: + '@mariozechner/clipboard': 0.3.9 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-tui@0.80.10': + dependencies: + get-east-asian-width: 1.6.0 + marked: 18.0.5 + + '@emnapi/core@1.9.1': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/aix-ppc64@0.28.0': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.28.0': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-arm@0.28.0': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/android-x64@0.28.0': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.28.0': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.28.0': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.28.0': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.28.0': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.28.0': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-arm@0.28.0': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.28.0': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.28.0': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.28.0': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.28.0': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.28.0': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.28.0': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/linux-x64@0.28.0': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.28.0': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.28.0': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.28.0': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.28.0': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.28.0': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.28.0': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.28.0': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.28.0': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@esbuild/win32-x64@0.28.0': + optional: true + + '@google/genai@1.52.0': + dependencies: + google-auth-library: 10.9.0 + p-retry: 4.6.2 + protobufjs: 7.6.5 + ws: 8.21.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@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': {} + + '@mariozechner/clipboard-darwin-arm64@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-universal@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-x64@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard@0.3.9': + optionalDependencies: + '@mariozechner/clipboard-darwin-arm64': 0.3.9 + '@mariozechner/clipboard-darwin-universal': 0.3.9 + '@mariozechner/clipboard-darwin-x64': 0.3.9 + '@mariozechner/clipboard-linux-arm64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-arm64-musl': 0.3.9 + '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-musl': 0.3.9 + '@mariozechner/clipboard-win32-arm64-msvc': 0.3.9 + '@mariozechner/clipboard-win32-x64-msvc': 0.3.9 + optional: true + + '@mistralai/mistralai@2.2.6(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/semantic-conventions': 1.43.0 + ws: 8.21.1 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + optionalDependencies: + '@opentelemetry/api': 1.9.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@next/env@14.2.35': {} + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/semantic-conventions@1.43.0': {} + + '@oxc-project/runtime@0.139.0': {} + + '@oxc-project/types@0.122.0': {} + + '@oxc-project/types@0.139.0': {} + + '@oxfmt/binding-android-arm-eabi@0.58.0': + optional: true + + '@oxfmt/binding-android-arm64@0.58.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.58.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.58.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.58.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.58.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.58.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.58.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.58.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.58.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.58.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.58.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.58.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.58.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.58.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.58.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.58.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.58.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.58.0': + optional: true + + '@oxlint-tsgolint/darwin-arm64@0.24.0': + optional: true + + '@oxlint-tsgolint/darwin-x64@0.24.0': + optional: true + + '@oxlint-tsgolint/linux-arm64@0.24.0': + optional: true + + '@oxlint-tsgolint/linux-x64@0.24.0': + optional: true + + '@oxlint-tsgolint/win32-arm64@0.24.0': + optional: true + + '@oxlint-tsgolint/win32-x64@0.24.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.73.0': + optional: true + + '@oxlint/binding-android-arm64@1.73.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.73.0': + optional: true + + '@oxlint/binding-darwin-x64@1.73.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.73.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.73.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.73.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.73.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.73.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.73.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.73.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.73.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.73.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.73.0': + optional: true + + '@oxlint/plugins@1.73.0': {} + + '@polka/url@1.0.0-next.29': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.2': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.12': {} + + '@silvia-odwyer/photon-node@0.3.4': {} + + '@simple-git/args-pathspec@1.0.3': {} + + '@simple-git/argv-parser@1.1.1': + dependencies: + '@simple-git/args-pathspec': 1.0.3 + + '@smithy/core@3.29.5': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.4.10': + dependencies: + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.6.7': + dependencies: + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.7.3': + dependencies: + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.9.7': + dependencies: + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/signature-v4@5.6.6': + dependencies: + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/types@4.16.1': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@standard-schema/spec@1.1.0': {} + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@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@26.1.1': + dependencies: + undici-types: 8.3.0 + + '@types/retry@0.12.0': {} + + '@vercel/functions@1.6.0(@aws-sdk/credential-provider-web-identity@3.972.65)': + optionalDependencies: + '@aws-sdk/credential-provider-web-identity': 3.972.65 + + '@vitest/browser-preview@4.1.10(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10)': + dependencies: + '@testing-library/dom': 10.4.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/browser': 4.1.10(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10) + vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.1)(@vitest/browser-preview@4.1.10)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@4.1.10(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.10(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.1)(@vitest/browser-preview@4.1.10)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) + ws: 8.21.1 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@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.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0) + + '@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 + + '@voidzero-dev/vite-plus-core@0.2.5(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.9.0)': + dependencies: + '@oxc-project/runtime': 0.139.0 + '@oxc-project/types': 0.139.0 + lightningcss: 1.32.0 + postcss: 8.5.19 + yuku-codegen: 0.5.48 + yuku-parser: 0.5.48 + optionalDependencies: + '@types/node': 26.1.1 + esbuild: 0.27.7 + fsevents: 2.3.3 + jiti: 2.7.0 + tsx: 4.21.0 + typescript: 6.0.3 + yaml: 2.9.0 + + '@voidzero-dev/vite-plus-darwin-arm64@0.2.5': + optional: true + + '@voidzero-dev/vite-plus-darwin-x64@0.2.5': + optional: true + + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.2.5': + optional: true + + '@voidzero-dev/vite-plus-linux-arm64-musl@0.2.5': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-gnu@0.2.5': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-musl@0.2.5': + optional: true + + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.2.5': + optional: true + + '@voidzero-dev/vite-plus-win32-x64-msvc@0.2.5': + optional: true + + '@yuku-codegen/binding-darwin-arm64@0.5.48': + optional: true + + '@yuku-codegen/binding-darwin-x64@0.5.48': + optional: true + + '@yuku-codegen/binding-freebsd-x64@0.5.48': + optional: true + + '@yuku-codegen/binding-linux-arm-gnu@0.5.48': + optional: true + + '@yuku-codegen/binding-linux-arm-musl@0.5.48': + optional: true + + '@yuku-codegen/binding-linux-arm64-gnu@0.5.48': + optional: true + + '@yuku-codegen/binding-linux-arm64-musl@0.5.48': + optional: true + + '@yuku-codegen/binding-linux-x64-gnu@0.5.48': + optional: true + + '@yuku-codegen/binding-linux-x64-musl@0.5.48': + optional: true + + '@yuku-codegen/binding-win32-arm64@0.5.48': + optional: true + + '@yuku-codegen/binding-win32-x64@0.5.48': + optional: true + + '@yuku-parser/binding-darwin-arm64@0.5.48': + optional: true + + '@yuku-parser/binding-darwin-x64@0.5.48': + optional: true + + '@yuku-parser/binding-freebsd-x64@0.5.48': + optional: true + + '@yuku-parser/binding-linux-arm-gnu@0.5.48': + optional: true + + '@yuku-parser/binding-linux-arm-musl@0.5.48': + optional: true + + '@yuku-parser/binding-linux-arm64-gnu@0.5.48': + optional: true + + '@yuku-parser/binding-linux-arm64-musl@0.5.48': + optional: true + + '@yuku-parser/binding-linux-x64-gnu@0.5.48': + optional: true + + '@yuku-parser/binding-linux-x64-musl@0.5.48': + optional: true + + '@yuku-parser/binding-win32-arm64@0.5.48': + optional: true + + '@yuku-parser/binding-win32-x64@0.5.48': + optional: true + + '@yuku-toolchain/types@0.5.43': {} + + 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: {} + + agent-base@7.1.4: {} + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@5.2.0: {} + + argparse@2.0.1: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + assertion-error@2.0.1: {} + + astring@1.9.0: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + bignumber.js@9.3.1: {} + + 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 + + bowser@2.14.1: {} + + brace-expansion@5.0.7: + dependencies: + balanced-match: 4.0.4 + + braintrust@3.23.1(@aws-sdk/credential-provider-web-identity@3.972.65)(zod@4.4.3): + dependencies: + '@next/env': 14.2.35 + '@vercel/functions': 1.6.0(@aws-sdk/credential-provider-web-identity@3.972.65) + 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-equal-constant-time@1.0.1: {} + + bytes@3.1.2: {} + + 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: {} + + chalk@5.6.2: {} + + 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 + + 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: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + data-uri-to-buffer@4.0.1: {} + + dc-browser@1.0.4: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + depd@2.0.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + diff@8.0.4: {} + + dom-accessibility-api@0.5.16: {} + + 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 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.3.0: {} + + 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 + optional: true + + 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 + + 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: {} + + eventsource-parser@1.1.2: {} + + 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 + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.3: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + 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 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gaxios@7.2.0: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.2.0 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + get-east-asian-width@1.6.0: {} + + 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 + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + optional: true + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + google-auth-library@10.9.0: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.2.0 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + highlight.js@10.7.3: {} + + hosted-git-info@9.0.3: + dependencies: + lru-cache: 11.5.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 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@7.0.5: {} + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-promise@4.0.0: {} + + isexe@2.0.0: {} + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.7 + ts-algebra: 2.0.0 + + json-schema-traverse@1.0.0: {} + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + long@5.3.2: {} + + lru-cache@11.5.2: {} + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + marked@18.0.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 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.7 + + minipass@7.1.3: {} + + module-details-from-path@1.0.4: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + mustache@4.2.0: {} + + nanoid@3.3.16: {} + + negotiator@1.0.0: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + obug@2.1.3: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openai@6.26.0(ws@8.21.1)(zod@4.4.3): + optionalDependencies: + ws: 8.21.1 + zod: 4.4.3 + + oxfmt@0.58.0(vite-plus@0.2.5(@opentelemetry/api@1.9.0)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@6.0.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0)): + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.58.0 + '@oxfmt/binding-android-arm64': 0.58.0 + '@oxfmt/binding-darwin-arm64': 0.58.0 + '@oxfmt/binding-darwin-x64': 0.58.0 + '@oxfmt/binding-freebsd-x64': 0.58.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.58.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.58.0 + '@oxfmt/binding-linux-arm64-gnu': 0.58.0 + '@oxfmt/binding-linux-arm64-musl': 0.58.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.58.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.58.0 + '@oxfmt/binding-linux-riscv64-musl': 0.58.0 + '@oxfmt/binding-linux-s390x-gnu': 0.58.0 + '@oxfmt/binding-linux-x64-gnu': 0.58.0 + '@oxfmt/binding-linux-x64-musl': 0.58.0 + '@oxfmt/binding-openharmony-arm64': 0.58.0 + '@oxfmt/binding-win32-arm64-msvc': 0.58.0 + '@oxfmt/binding-win32-ia32-msvc': 0.58.0 + '@oxfmt/binding-win32-x64-msvc': 0.58.0 + vite-plus: 0.2.5(@opentelemetry/api@1.9.0)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@6.0.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0) + + oxlint-tsgolint@0.24.0: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 0.24.0 + '@oxlint-tsgolint/darwin-x64': 0.24.0 + '@oxlint-tsgolint/linux-arm64': 0.24.0 + '@oxlint-tsgolint/linux-x64': 0.24.0 + '@oxlint-tsgolint/win32-arm64': 0.24.0 + '@oxlint-tsgolint/win32-x64': 0.24.0 + + oxlint@1.73.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.5(@opentelemetry/api@1.9.0)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@6.0.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0)): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.73.0 + '@oxlint/binding-android-arm64': 1.73.0 + '@oxlint/binding-darwin-arm64': 1.73.0 + '@oxlint/binding-darwin-x64': 1.73.0 + '@oxlint/binding-freebsd-x64': 1.73.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.73.0 + '@oxlint/binding-linux-arm-musleabihf': 1.73.0 + '@oxlint/binding-linux-arm64-gnu': 1.73.0 + '@oxlint/binding-linux-arm64-musl': 1.73.0 + '@oxlint/binding-linux-ppc64-gnu': 1.73.0 + '@oxlint/binding-linux-riscv64-gnu': 1.73.0 + '@oxlint/binding-linux-riscv64-musl': 1.73.0 + '@oxlint/binding-linux-s390x-gnu': 1.73.0 + '@oxlint/binding-linux-x64-gnu': 1.73.0 + '@oxlint/binding-linux-x64-musl': 1.73.0 + '@oxlint/binding-openharmony-arm64': 1.73.0 + '@oxlint/binding-win32-arm64-msvc': 1.73.0 + '@oxlint/binding-win32-ia32-msvc': 1.73.0 + '@oxlint/binding-win32-x64-msvc': 1.73.0 + oxlint-tsgolint: 0.24.0 + vite-plus: 0.2.5(@opentelemetry/api@1.9.0)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@6.0.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0) + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + parseurl@1.3.3: {} + + partial-json@0.1.7: {} + + path-key@3.1.1: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + + path-to-regexp@8.4.2: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + pluralize@8.0.0: {} + + pngjs@7.0.0: {} + + postcss@8.5.19: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + protobufjs@7.6.5: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 26.1.1 + long: 5.3.2 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + 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 + + react-is@17.0.2: {} + + require-from-string@2.0.2: {} + + resolve-pkg-maps@1.0.0: + optional: true + + retry@0.12.0: {} + + retry@0.13.1: {} + + rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + 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.2.1: {} + + safer-buffer@2.1.2: {} + + semifies@1.0.0: {} + + semver@7.8.0: {} + + 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: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.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: {} + + signal-exit@3.0.7: {} + + 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 + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + source-map@0.7.6: {} + + stackback@0.0.2: {} + + statuses@2.0.2: {} + + std-env@4.2.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + termi-link@1.1.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinypool@2.1.0: {} + + tinyrainbow@3.1.0: {} + + toidentifier@1.0.1: {} + + totalist@3.0.1: {} + + ts-algebra@2.0.0: {} + + tslib@2.8.1: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.14.0 + optionalDependencies: + fsevents: 2.3.3 + optional: true + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typebox@1.1.38: {} + + typescript@6.0.3: {} + + undici-types@8.3.0: {} + + undici@8.5.0: {} + + 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 + + uuid@11.1.1: {} + + valibot@1.4.2(typescript@6.0.3): + optionalDependencies: + typescript: 6.0.3 + + vary@1.1.2: {} + + vite-plus@0.2.5(@opentelemetry/api@1.9.0)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@6.0.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0): + dependencies: + '@oxc-project/types': 0.139.0 + '@oxlint/plugins': 1.73.0 + '@vitest/browser': 4.1.10(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/browser-preview': 4.1.10(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) + '@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 + '@voidzero-dev/vite-plus-core': 0.2.5(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.9.0) + oxfmt: 0.58.0(vite-plus@0.2.5(@opentelemetry/api@1.9.0)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@6.0.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0)) + oxlint: 1.73.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.5(@opentelemetry/api@1.9.0)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@6.0.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0)) + oxlint-tsgolint: 0.24.0 + vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.1)(@vitest/browser-preview@4.1.10)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) + optionalDependencies: + '@voidzero-dev/vite-plus-darwin-arm64': 0.2.5 + '@voidzero-dev/vite-plus-darwin-x64': 0.2.5 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.2.5 + '@voidzero-dev/vite-plus-linux-arm64-musl': 0.2.5 + '@voidzero-dev/vite-plus-linux-x64-gnu': 0.2.5 + '@voidzero-dev/vite-plus-linux-x64-musl': 0.2.5 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.2.5 + '@voidzero-dev/vite-plus-win32-x64-msvc': 0.2.5 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@types/node' + - '@vitejs/devtools' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - bufferutil + - esbuild + - happy-dom + - jiti + - jsdom + - less + - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - svelte + - terser + - tsx + - typescript + - unplugin-unused + - unrun + - utf-8-validate + - vite + - yaml + + vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.5 + postcss: 8.5.19 + rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.1.1 + esbuild: 0.27.7 + fsevents: 2.3.3 + jiti: 2.7.0 + tsx: 4.21.0 + yaml: 2.9.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.1)(@vitest/browser-preview@4.1.10)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) + '@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.0 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.3 + 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.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 26.1.1 + '@vitest/browser-preview': 4.1.10(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10) + transitivePeerDependencies: + - msw + + web-streams-polyfill@3.3.3: {} + + webpack-virtual-modules@0.6.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrappy@1.0.2: {} + + ws@8.21.1: {} + + yaml@2.9.0: {} + + yuku-codegen@0.5.48: + dependencies: + '@yuku-toolchain/types': 0.5.43 + optionalDependencies: + '@yuku-codegen/binding-darwin-arm64': 0.5.48 + '@yuku-codegen/binding-darwin-x64': 0.5.48 + '@yuku-codegen/binding-freebsd-x64': 0.5.48 + '@yuku-codegen/binding-linux-arm-gnu': 0.5.48 + '@yuku-codegen/binding-linux-arm-musl': 0.5.48 + '@yuku-codegen/binding-linux-arm64-gnu': 0.5.48 + '@yuku-codegen/binding-linux-arm64-musl': 0.5.48 + '@yuku-codegen/binding-linux-x64-gnu': 0.5.48 + '@yuku-codegen/binding-linux-x64-musl': 0.5.48 + '@yuku-codegen/binding-win32-arm64': 0.5.48 + '@yuku-codegen/binding-win32-x64': 0.5.48 + + yuku-parser@0.5.48: + dependencies: + '@yuku-toolchain/types': 0.5.43 + optionalDependencies: + '@yuku-parser/binding-darwin-arm64': 0.5.48 + '@yuku-parser/binding-darwin-x64': 0.5.48 + '@yuku-parser/binding-freebsd-x64': 0.5.48 + '@yuku-parser/binding-linux-arm-gnu': 0.5.48 + '@yuku-parser/binding-linux-arm-musl': 0.5.48 + '@yuku-parser/binding-linux-arm64-gnu': 0.5.48 + '@yuku-parser/binding-linux-arm64-musl': 0.5.48 + '@yuku-parser/binding-linux-x64-gnu': 0.5.48 + '@yuku-parser/binding-linux-x64-musl': 0.5.48 + '@yuku-parser/binding-win32-arm64': 0.5.48 + '@yuku-parser/binding-win32-x64': 0.5.48 + + 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/pi/content/pnpm-workspace.yaml b/src/plugins/pi/content/pnpm-workspace.yaml new file mode 100644 index 0000000..f66b35b --- /dev/null +++ b/src/plugins/pi/content/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +allowBuilds: + "@google/genai": false + braintrust: false + esbuild: true + koffi: true + protobufjs: true diff --git a/src/plugins/pi/content/scripts/resolve-pi-versions.mjs b/src/plugins/pi/content/scripts/resolve-pi-versions.mjs new file mode 100644 index 0000000..aabf6b5 --- /dev/null +++ b/src/plugins/pi/content/scripts/resolve-pi-versions.mjs @@ -0,0 +1,154 @@ +#!/usr/bin/env node + +import { appendFileSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { execFileSync } from "node:child_process"; + +const packageFamilies = [ + { + name: "pi-coding-agent", + alias: "@earendil-works/pi-coding-agent", + sources: ["@earendil-works/pi-coding-agent", "@mariozechner/pi-coding-agent"], + }, + { + name: "pi-ai", + alias: "@earendil-works/pi-ai", + sources: ["@earendil-works/pi-ai", "@mariozechner/pi-ai"], + }, +]; +const count = Number(process.env.PI_VERSION_COUNT ?? "5"); +const stableVersionPattern = /^\d+\.\d+\.\d+$/; + +if (!Number.isInteger(count) || count <= 0) { + throw new Error( + `PI_VERSION_COUNT must be a positive integer, got: ${process.env.PI_VERSION_COUNT ?? "undefined"}`, + ); +} + +const npmQueryDirectory = mkdtempSync(join(tmpdir(), "pi-version-query-")); + +function npmViewVersions(packageName) { + const output = execFileSync("npm", ["view", packageName, "versions", "--json"], { + cwd: npmQueryDirectory, + encoding: "utf8", + stdio: ["ignore", "pipe", "inherit"], + }).trim(); + + const parsed = JSON.parse(output); + const versions = Array.isArray(parsed) ? parsed : [parsed]; + return versions.filter((version) => stableVersionPattern.test(version)); +} + +function compareVersions(a, b) { + const aParts = a.split(".").map(Number); + const bParts = b.split(".").map(Number); + + for (let index = 0; index < 3; index += 1) { + const delta = aParts[index] - bParts[index]; + if (delta !== 0) { + return delta; + } + } + + return 0; +} + +const familyVersions = packageFamilies.map((family) => { + const versionsBySource = family.sources.map((packageName) => ({ + packageName, + versions: npmViewVersions(packageName), + })); + const versions = [...new Set(versionsBySource.flatMap(({ versions }) => versions))].sort( + compareVersions, + ); + return { ...family, versions, versionsBySource }; +}); + +const sharedVersions = + familyVersions + .map(({ versions }) => versions) + .reduce((commonVersions, packageVersions) => { + if (commonVersions === null) { + return packageVersions; + } + + const packageVersionSet = new Set(packageVersions); + return commonVersions.filter((version) => packageVersionSet.has(version)); + }, null) + ?.sort(compareVersions) ?? []; + +const releaseLines = []; + +for (const version of sharedVersions) { + const [major, minor] = version.split("."); + const line = `${major}.${minor}`; + const previousReleaseLine = releaseLines.at(-1); + + if (previousReleaseLine?.line === line) { + previousReleaseLine.version = version; + continue; + } + + releaseLines.push({ line, version }); +} + +const selectedVersions = releaseLines.map(({ version }) => version).slice(-count); + +if (selectedVersions.length < count) { + throw new Error( + `Expected at least ${count} shared stable pi release lines across ${packageFamilies + .map(({ sources }) => sources.join("/")) + .join(", ")}, found ${selectedVersions.length}`, + ); +} + +function installSpecFor(family, version) { + const source = family.versionsBySource.find(({ versions }) => + versions.includes(version), + )?.packageName; + if (!source) { + throw new Error(`No source package found for ${family.name}@${version}`); + } + return source === family.alias + ? `${family.alias}@${version}` + : `${family.alias}@npm:${source}@${version}`; +} + +const selectedTargets = selectedVersions.map((version) => ({ + version, + piAiSpec: installSpecFor( + familyVersions.find(({ name }) => name === "pi-ai"), + version, + ), + piCodingAgentSpec: installSpecFor( + familyVersions.find(({ name }) => name === "pi-coding-agent"), + version, + ), +})); + +const outputs = { + versions: JSON.stringify(selectedTargets), + latest: selectedVersions[selectedVersions.length - 1], + oldest: selectedVersions[0], +}; + +if (process.env.GITHUB_OUTPUT) { + for (const [key, value] of Object.entries(outputs)) { + appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`); + } +} + +if (process.env.GITHUB_STEP_SUMMARY) { + appendFileSync( + process.env.GITHUB_STEP_SUMMARY, + [ + "## Supported pi compatibility versions", + "", + `Testing the latest ${count} shared stable pi release lines: ${selectedVersions.join(", ")}`, + "", + ].join("\n"), + ); +} + +console.log(`Resolved pi compatibility versions: ${selectedVersions.join(", ")}`); diff --git a/src/plugins/pi/content/scripts/sync-version.mjs b/src/plugins/pi/content/scripts/sync-version.mjs new file mode 100644 index 0000000..b664cb6 --- /dev/null +++ b/src/plugins/pi/content/scripts/sync-version.mjs @@ -0,0 +1,23 @@ +#!/usr/bin/env node +import { readFileSync, writeFileSync } from "node:fs"; + +const check = process.argv.includes("--check"); +const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); +const version = packageJson.version; + +if (typeof version !== "string" || version.length === 0) { + throw new Error("package.json version must be a non-empty string"); +} + +const target = new URL("../src/version.ts", import.meta.url); +const content = `// This file is generated by scripts/sync-version.mjs. Do not edit by hand.\nexport const EXTENSION_VERSION = ${JSON.stringify(version)};\n`; + +if (check) { + const current = readFileSync(target, "utf8"); + if (current !== content) { + console.error("src/version.ts is out of sync with package.json. Run `pnpm run sync:version`."); + process.exit(1); + } +} else { + writeFileSync(target, content); +} diff --git a/src/plugins/pi/content/src/client.ts b/src/plugins/pi/content/src/client.ts new file mode 100644 index 0000000..afdc7a4 --- /dev/null +++ b/src/plugins/pi/content/src/client.ts @@ -0,0 +1,282 @@ +import { + ERR_PERMALINK, + NOOP_SPAN_PERMALINK, + initLogger, + type Logger as BraintrustSdkLogger, + type Span as BraintrustSdkSpan, +} from "braintrust"; +import type { Logger, TraceConfig } from "./types.ts"; +import { toUnixSeconds } from "./utils.ts"; +import { EXTENSION_VERSION } from "./version.ts"; + +export type BraintrustSpanHandle = BraintrustSdkSpan; + +export interface StartTraceSpanArgs { + spanId: string; + rootSpanId: string; + parentSpanId?: string; + name: string; + type: "task" | "llm" | "tool"; + startedAt: number; + input?: unknown; + output?: unknown; + error?: string; + metadata?: Record; + metrics?: Record; +} + +export interface UpdateTraceSpanArgs { + id: string; + spanId?: string; + rootSpanId?: string; + input?: unknown; + output?: unknown; + error?: string; + metadata?: Record; + metrics?: Record; +} + +function compactRecord>(value: T): Partial { + return Object.fromEntries( + Object.entries(value).filter(([, entry]) => entry !== undefined), + ) as Partial; +} + +function detectEnvironment(): { type?: string; name?: string } | undefined { + if (process.env.BRAINTRUST_ENVIRONMENT_TYPE || process.env.BRAINTRUST_ENVIRONMENT_NAME) { + return { + ...(process.env.BRAINTRUST_ENVIRONMENT_TYPE + ? { type: process.env.BRAINTRUST_ENVIRONMENT_TYPE } + : {}), + ...(process.env.BRAINTRUST_ENVIRONMENT_NAME + ? { name: process.env.BRAINTRUST_ENVIRONMENT_NAME } + : {}), + }; + } + if (process.env.GITHUB_ACTIONS) return { type: "ci", name: "github_actions" }; + if (process.env.CI) return { type: "ci", name: "ci" }; + if (process.env.VERCEL) return { type: "server", name: "vercel" }; + if (process.env.NODE_ENV === "production" || process.env.NODE_ENV === "staging") { + return { type: "server", name: process.env.NODE_ENV }; + } + if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "local") { + return { type: "local", name: process.env.NODE_ENV }; + } + return undefined; +} + +function spanOriginContext() { + const environment = detectEnvironment(); + return { + span_origin: { + name: "braintrust.plugin.pi", + version: EXTENSION_VERSION, + instrumentation: { name: "pi-extension" }, + ...(environment ? { environment } : {}), + }, + }; +} + +function isUsablePermalink(url: string | undefined): url is string { + return Boolean(url && url !== NOOP_SPAN_PERMALINK && !url.startsWith(ERR_PERMALINK)); +} + +export class BraintrustClient { + readonly config: TraceConfig; + readonly logger?: Logger; + sdkLogger?: BraintrustSdkLogger; + initPromise?: Promise; + + constructor(config: TraceConfig, logger?: Logger) { + this.config = config; + this.logger = logger; + } + + #ensureLogger(): BraintrustSdkLogger { + if (this.sdkLogger) return this.sdkLogger; + + if (this.config.apiUrl && !process.env.BRAINTRUST_API_URL) { + process.env.BRAINTRUST_API_URL = this.config.apiUrl; + } + + this.sdkLogger = initLogger({ + projectName: this.config.projectName, + apiKey: this.config.apiKey, + appUrl: this.config.appUrl, + orgName: this.config.orgName, + asyncFlush: true, + setCurrent: false, + debugLogLevel: this.config.debug ? "debug" : false, + }); + + return this.sdkLogger; + } + + async initialize(): Promise { + if (!this.initPromise) { + this.initPromise = this.#doInitialize(); + } + return this.initPromise; + } + + async #doInitialize(): Promise { + if (!this.config.apiKey) { + throw new Error("BRAINTRUST_API_KEY is not set"); + } + + const sdkLogger = this.#ensureLogger(); + await sdkLogger.id; + const project = await sdkLogger.project; + this.logger?.info("braintrust sdk logger initialized", { + appUrl: this.config.appUrl, + apiUrl: this.config.apiUrl, + project: project.name, + projectId: project.id, + }); + } + + startSpan(args: StartTraceSpanArgs): BraintrustSpanHandle | undefined { + try { + const sdkLogger = this.#ensureLogger(); + const span = sdkLogger.startSpan({ + spanId: args.spanId, + parentSpanIds: args.parentSpanId + ? { + spanId: args.parentSpanId, + rootSpanId: args.rootSpanId, + } + : undefined, + name: args.name, + type: args.type, + startTime: toUnixSeconds(args.startedAt), + event: compactRecord({ + input: args.input, + output: args.output, + error: args.error, + metadata: args.metadata, + metrics: args.metrics, + context: spanOriginContext(), + }), + }); + return span; + } catch (error) { + this.logger?.error("failed to start Braintrust span", { + error: String(error), + spanId: args.spanId, + parentSpanId: args.parentSpanId, + rootSpanId: args.rootSpanId, + name: args.name, + type: args.type, + }); + return undefined; + } + } + + logSpan(span: BraintrustSpanHandle | undefined, event: Omit): void { + if (!span) return; + + try { + span.log( + compactRecord({ + input: event.input, + output: event.output, + error: event.error, + metadata: event.metadata, + metrics: event.metrics, + }), + ); + } catch (error) { + this.logger?.error("failed to log Braintrust span", { + error: String(error), + spanId: span.spanId, + rootSpanId: span.rootSpanId, + }); + } + } + + endSpan(span: BraintrustSpanHandle | undefined, endedAt = Date.now()): void { + if (!span) return; + + try { + span.end({ endTime: toUnixSeconds(endedAt) }); + } catch (error) { + this.logger?.error("failed to end Braintrust span", { + error: String(error), + spanId: span.spanId, + rootSpanId: span.rootSpanId, + }); + } + } + + getSpanLink(span: BraintrustSpanHandle | undefined): string | undefined { + if (!span) return undefined; + + try { + const link = span.link(); + return isUsablePermalink(link) ? link : undefined; + } catch (error) { + this.logger?.warn("failed to build Braintrust span link", { + error: String(error), + spanId: span.spanId, + rootSpanId: span.rootSpanId, + }); + return undefined; + } + } + + async getSpanPermalink(span: BraintrustSpanHandle | undefined): Promise { + if (!span) return undefined; + + const link = this.getSpanLink(span); + if (link) return link; + + try { + const permalink = await span.permalink(); + return isUsablePermalink(permalink) ? permalink : undefined; + } catch (error) { + this.logger?.warn("failed to build Braintrust span permalink", { + error: String(error), + spanId: span.spanId, + rootSpanId: span.rootSpanId, + }); + return undefined; + } + } + + updateSpan(args: UpdateTraceSpanArgs): void { + try { + const sdkLogger = this.#ensureLogger(); + sdkLogger.updateSpan({ + id: args.id, + span_id: args.spanId, + root_span_id: args.rootSpanId, + input: args.input, + output: args.output, + error: args.error, + metadata: args.metadata, + metrics: args.metrics, + context: spanOriginContext(), + }); + } catch (error) { + this.logger?.error("failed to update Braintrust span", { + error: String(error), + id: args.id, + spanId: args.spanId, + rootSpanId: args.rootSpanId, + }); + } + } + + async flush(): Promise { + const sdkLogger = this.sdkLogger; + if (!sdkLogger) return; + + try { + await sdkLogger.flush(); + } catch (error) { + this.logger?.error("failed to flush Braintrust logs", { + error: String(error), + }); + } + } +} diff --git a/src/plugins/pi/content/src/config.test.ts b/src/plugins/pi/content/src/config.test.ts new file mode 100644 index 0000000..132131f --- /dev/null +++ b/src/plugins/pi/content/src/config.test.ts @@ -0,0 +1,445 @@ +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createLogger, loadConfig } from "./config.ts"; +import type { TraceConfig } from "./types.ts"; + +const ENV_KEYS = [ + "HOME", + "BRAINTRUST_API_KEY", + "BRAINTRUST_API_URL", + "BRAINTRUST_APP_URL", + "BRAINTRUST_ORG_NAME", + "BRAINTRUST_PROJECT", + "TRACE_TO_BRAINTRUST", + "BRAINTRUST_DEBUG", + "BRAINTRUST_LOG_FILE", + "BRAINTRUST_STATE_DIR", + "BRAINTRUST_SHOW_UI", + "BRAINTRUST_SHOW_TRACE_LINK", + "PI_PARENT_SPAN_ID", + "PI_ROOT_SPAN_ID", + "BRAINTRUST_ADDITIONAL_METADATA", +] as const; + +const originalEnv = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]])); +const tempDirs: string[] = []; + +beforeEach(() => { + for (const key of ENV_KEYS) { + delete process.env[key]; + } +}); + +afterEach(() => { + for (const key of ENV_KEYS) { + const value = originalEnv[key]; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + + while (tempDirs.length > 0) { + rmSync(tempDirs.pop()!, { recursive: true, force: true }); + } +}); + +function makeTempDir(prefix: string): string { + const dir = mkdtempSync(join(tmpdir(), prefix)); + tempDirs.push(dir); + return dir; +} + +function writeJson(path: string, value: unknown): void { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8"); +} + +describe("loadConfig", () => { + it("applies global config, project config, then env overrides", () => { + const home = makeTempDir("pi-extension-home-"); + const cwd = join(home, "workspace"); + const projectStateDir = join(home, "project-state"); + const envStateDir = join(home, "env-state"); + + process.env.HOME = home; + process.env.BRAINTRUST_PROJECT = "from-env"; + process.env.TRACE_TO_BRAINTRUST = "false"; + process.env.BRAINTRUST_ADDITIONAL_METADATA = '{"origin":"env","team":"platform"}'; + process.env.BRAINTRUST_STATE_DIR = envStateDir; + + writeJson(join(home, ".pi", "agent", "braintrust.json"), { + api_key: "global-key", + api_url: "https://global.example", + project: "from-global", + trace_to_braintrust: false, + debug: true, + additional_metadata: { origin: "global" }, + }); + + writeJson(join(cwd, ".pi", "braintrust.json"), { + api_url: "https://project.example", + project: "from-project", + trace_to_braintrust: true, + state_dir: projectStateDir, + additional_metadata: { origin: "project" }, + }); + + const config = loadConfig(cwd); + + expect(config.apiKey).toBe("global-key"); + expect(config.apiUrl).toBe("https://project.example"); + expect(config.projectName).toBe("from-env"); + expect(config.enabled).toBe(false); + expect(config.additionalMetadata).toEqual({ origin: "env", team: "platform" }); + expect(config.stateDir).toBe(envStateDir); + expect(config.configIssues).toEqual([]); + expect(existsSync(envStateDir)).toBe(true); + }); + + it("uses pi CONFIG_DIR_NAME for project-local config", async () => { + vi.resetModules(); + vi.doMock("@earendil-works/pi-coding-agent", () => ({ + CONFIG_DIR_NAME: ".custom-pi", + })); + try { + const { loadConfig: loadConfigWithCustomDir } = await import("./config.ts"); + + const home = makeTempDir("pi-extension-home-"); + const cwd = join(home, "workspace"); + + process.env.HOME = home; + process.env.BRAINTRUST_STATE_DIR = join(home, "state"); + + writeJson(join(cwd, ".custom-pi", "braintrust.json"), { + project: "from-custom-config-dir", + }); + + const config = loadConfigWithCustomDir(cwd); + + expect(config.projectName).toBe("from-custom-config-dir"); + } finally { + vi.doUnmock("@earendil-works/pi-coding-agent"); + vi.resetModules(); + } + }); + + it("falls back to .pi when older pi hosts do not export CONFIG_DIR_NAME", async () => { + vi.resetModules(); + vi.doMock("@earendil-works/pi-coding-agent", () => ({ + CONFIG_DIR_NAME: undefined, + })); + try { + const { loadConfig: loadConfigWithoutConfigDir } = await import("./config.ts"); + + const home = makeTempDir("pi-extension-home-"); + const cwd = join(home, "workspace"); + + process.env.HOME = home; + process.env.BRAINTRUST_STATE_DIR = join(home, "state"); + + writeJson(join(cwd, ".pi", "braintrust.json"), { + project: "from-default-config-dir", + }); + + const config = loadConfigWithoutConfigDir(cwd); + + expect(config.projectName).toBe("from-default-config-dir"); + } finally { + vi.doUnmock("@earendil-works/pi-coding-agent"); + vi.resetModules(); + } + }); + + it("records config file parse errors without throwing away other valid config sources", () => { + const home = makeTempDir("pi-extension-home-"); + const cwd = join(home, "workspace"); + + process.env.HOME = home; + + mkdirSync(dirname(join(home, ".pi", "agent", "braintrust.json")), { recursive: true }); + writeFileSync( + join(home, ".pi", "agent", "braintrust.json"), + '{\n "trace_to_braintrust": true,\n "api_key": "bad-json",\n}\n', + "utf8", + ); + + writeJson(join(cwd, ".pi", "braintrust.json"), { + trace_to_braintrust: true, + project: "from-project", + }); + + const config = loadConfig(cwd); + + expect(config.enabled).toBe(true); + expect(config.projectName).toBe("from-project"); + expect(config.configIssues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + path: join(home, ".pi", "agent", "braintrust.json"), + severity: "error", + message: expect.stringContaining("JSON"), + }), + expect.objectContaining({ + path: "BRAINTRUST_API_KEY", + severity: "warning", + }), + ]), + ); + }); + + it("ignores malformed config value types without crashing", () => { + const home = makeTempDir("pi-extension-home-"); + const cwd = join(home, "workspace"); + + process.env.HOME = home; + process.env.BRAINTRUST_ADDITIONAL_METADATA = "not-json"; + + writeJson(join(home, ".pi", "agent", "braintrust.json"), { + api_key: { nested: true }, + project: ["wrong-type"], + trace_to_braintrust: "definitely", + debug: { nope: true }, + state_dir: { bad: true }, + additional_metadata: ["bad"], + parent_span_id: { bad: true }, + root_span_id: ["bad"], + }); + + const config = loadConfig(cwd); + + expect(config.apiKey).toBe(""); + expect(config.projectName).toBe("pi"); + expect(config.enabled).toBe(false); + expect(config.debug).toBe(false); + expect(config.additionalMetadata).toEqual({}); + expect(config.parentSpanId).toBeUndefined(); + expect(config.rootSpanId).toBeUndefined(); + expect(config.configIssues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + path: join(home, ".pi", "agent", "braintrust.json"), + message: expect.stringContaining("additional_metadata"), + severity: "error", + }), + expect.objectContaining({ + path: "BRAINTRUST_ADDITIONAL_METADATA", + severity: "error", + }), + ]), + ); + expect(config.stateDir.endsWith(join(".pi", "agent", "state", "braintrust-pi-extension"))).toBe( + true, + ); + expect(existsSync(config.stateDir)).toBe(true); + }); + + it("mirrors the parent span id to the root span id when only parent is provided", () => { + const home = makeTempDir("pi-extension-home-"); + process.env.HOME = home; + process.env.BRAINTRUST_STATE_DIR = join(home, "state"); + process.env.PI_PARENT_SPAN_ID = "parent-123"; + + const config = loadConfig(home); + + expect(config.parentSpanId).toBe("parent-123"); + expect(config.rootSpanId).toBe("parent-123"); + }); + + it("mirrors the root span id to the parent span id when only root is provided", () => { + const home = makeTempDir("pi-extension-home-"); + process.env.HOME = home; + process.env.BRAINTRUST_STATE_DIR = join(home, "state"); + process.env.PI_ROOT_SPAN_ID = "root-123"; + + const config = loadConfig(home); + + expect(config.rootSpanId).toBe("root-123"); + expect(config.parentSpanId).toBe("root-123"); + }); + + it("keeps lower-precedence URLs when higher-precedence URL values are invalid", () => { + const home = makeTempDir("pi-extension-home-"); + const cwd = join(home, "workspace"); + + process.env.HOME = home; + process.env.BRAINTRUST_APP_URL = "ftp://braintrust.example"; + + writeJson(join(home, ".pi", "agent", "braintrust.json"), { + api_url: "https://global.example", + }); + + writeJson(join(cwd, ".pi", "braintrust.json"), { + api_url: "not-a-url", + }); + + const config = loadConfig(cwd); + + expect(config.apiUrl).toBe("https://global.example"); + expect(config.appUrl).toBe("https://www.braintrust.dev"); + expect(config.configIssues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + path: join(cwd, ".pi", "braintrust.json"), + message: "api_url must be a valid http(s) URL", + severity: "error", + }), + expect.objectContaining({ + path: "BRAINTRUST_APP_URL", + message: "BRAINTRUST_APP_URL must be a valid http(s) URL", + severity: "error", + }), + ]), + ); + }); + + it("warns when parent_span_id and root_span_id are both explicitly set to the same value", () => { + const home = makeTempDir("pi-extension-home-"); + process.env.HOME = home; + process.env.BRAINTRUST_STATE_DIR = join(home, "state"); + process.env.PI_PARENT_SPAN_ID = "span-123"; + process.env.PI_ROOT_SPAN_ID = "span-123"; + + const config = loadConfig(home); + + expect(config.configIssues).toContainEqual({ + path: "parent_span_id/root_span_id", + message: + "parent_span_id and root_span_id are identical; set only one unless the parent span is also the trace root", + severity: "warning", + }); + }); + + it("defaults showUi and showTraceLink to true", () => { + const home = makeTempDir("pi-extension-home-"); + process.env.HOME = home; + process.env.BRAINTRUST_STATE_DIR = join(home, "state"); + + const config = loadConfig(home); + + expect(config.showUi).toBe(true); + expect(config.showTraceLink).toBe(true); + }); + + it("applies show_ui and show_trace_link from config files", () => { + const home = makeTempDir("pi-extension-home-"); + const cwd = join(home, "workspace"); + + process.env.HOME = home; + process.env.BRAINTRUST_STATE_DIR = join(home, "state"); + + writeJson(join(home, ".pi", "agent", "braintrust.json"), { + show_ui: false, + }); + + const config = loadConfig(cwd); + + expect(config.showUi).toBe(false); + expect(config.showTraceLink).toBe(true); + }); + + it("overrides show_ui and show_trace_link from environment variables", () => { + const home = makeTempDir("pi-extension-home-"); + const cwd = join(home, "workspace"); + + process.env.HOME = home; + process.env.BRAINTRUST_STATE_DIR = join(home, "state"); + process.env.BRAINTRUST_SHOW_UI = "true"; + process.env.BRAINTRUST_SHOW_TRACE_LINK = "false"; + + writeJson(join(home, ".pi", "agent", "braintrust.json"), { + show_ui: false, + }); + + const config = loadConfig(cwd); + + expect(config.showUi).toBe(true); + expect(config.showTraceLink).toBe(false); + }); + + it("warns when tracing is enabled without an API key", () => { + const home = makeTempDir("pi-extension-home-"); + process.env.HOME = home; + process.env.BRAINTRUST_STATE_DIR = join(home, "state"); + process.env.TRACE_TO_BRAINTRUST = "true"; + + const config = loadConfig(home); + + expect(config.enabled).toBe(true); + expect(config.configIssues).toContainEqual({ + path: "BRAINTRUST_API_KEY", + message: "TRACE_TO_BRAINTRUST is enabled but BRAINTRUST_API_KEY is not set", + severity: "warning", + }); + }); +}); + +describe("createLogger", () => { + function makeLoggerConfig(overrides: Partial = {}): TraceConfig { + return { + enabled: true, + apiKey: "key", + apiUrl: undefined, + appUrl: "https://www.braintrust.dev", + orgName: undefined, + projectName: "pi", + debug: true, + logFile: undefined, + stateDir: makeTempDir("pi-extension-state-"), + additionalMetadata: {}, + parentSpanId: undefined, + rootSpanId: undefined, + showUi: true, + showTraceLink: true, + configIssues: [], + ...overrides, + }; + } + + it("writes json log lines to the default log file when debug is enabled", async () => { + const config = makeLoggerConfig(); + + const logger = createLogger(config); + logger.debug("debug message", { nested: { value: 1 } }); + logger.warn("warn message"); + await logger.flush(); + + const lines = readFileSync(logger.filePath, "utf8").trim().split("\n"); + expect(lines).toHaveLength(2); + expect(JSON.parse(lines[0])).toMatchObject({ + level: "debug", + message: "debug message", + data: { nested: { value: 1 } }, + }); + expect(JSON.parse(lines[1])).toMatchObject({ + level: "warn", + message: "warn message", + }); + }); + + it("writes warnings and errors to the default log file when debug is disabled", async () => { + const config = makeLoggerConfig({ debug: false, logFile: undefined }); + const logger = createLogger(config); + + logger.debug("debug message"); + logger.info("info message"); + logger.warn("warn message", { reason: "test" }); + logger.error("error message"); + await logger.flush(); + + const lines = readFileSync(logger.filePath, "utf8").trim().split("\n"); + expect(lines).toHaveLength(2); + expect(JSON.parse(lines[0])).toMatchObject({ + level: "warn", + message: "warn message", + data: { reason: "test" }, + }); + expect(JSON.parse(lines[1])).toMatchObject({ + level: "error", + message: "error message", + }); + }); +}); diff --git a/src/plugins/pi/content/src/config.ts b/src/plugins/pi/content/src/config.ts new file mode 100644 index 0000000..d0a8ebb --- /dev/null +++ b/src/plugins/pi/content/src/config.ts @@ -0,0 +1,507 @@ +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join } from "node:path"; +import * as piCodingAgent from "@earendil-works/pi-coding-agent"; +import * as v from "valibot"; +import type { ConfigIssue, JsonObject, Logger, LogLevel, TraceConfig } from "./types.ts"; +import { ensureDir, writeJsonLog } from "./utils.ts"; + +const PROJECT_CONFIG_DIR_NAME = + typeof (piCodingAgent as { CONFIG_DIR_NAME?: unknown }).CONFIG_DIR_NAME === "string" + ? (piCodingAgent as { CONFIG_DIR_NAME: string }).CONFIG_DIR_NAME + : ".pi"; + +const DEFAULT_STATE_DIR = join(homedir(), ".pi", "agent", "state", "braintrust-pi-extension"); + +const HTTP_URL_SCHEMA = v.pipe( + v.string(), + v.url(), + v.check((value) => { + try { + const protocol = new URL(value).protocol; + return protocol === "http:" || protocol === "https:"; + } catch { + return false; + } + }, "must use http:// or https://"), +); +const JSON_OBJECT_SCHEMA = v.custom( + (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value), + "must be a JSON object", +); +const NON_EMPTY_STRING_SCHEMA = v.pipe(v.string(), v.minLength(1)); +const STRING_SCHEMA = v.string(); + +interface ConfigFileResult { + value?: JsonObject; + error?: string; +} + +interface ApplyConfigResult { + parentSpanConfigured: boolean; + rootSpanConfigured: boolean; +} + +function readConfigFile(path: string): ConfigFileResult { + if (!existsSync(path)) return {}; + + try { + const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown; + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return { + error: "expected a JSON object", + }; + } + return { value: parsed as JsonObject }; + } catch (error) { + return { + error: error instanceof Error ? error.message : String(error), + }; + } +} + +function pushConfigIssue( + issues: ConfigIssue[], + path: string, + message: string, + severity: ConfigIssue["severity"] = "error", +): void { + issues.push({ path, message, severity }); +} + +function validateOptionalString( + value: unknown, + issues: ConfigIssue[], + path: string, + key: string, +): string | undefined { + if (value === undefined) return undefined; + const parsed = v.safeParse(STRING_SCHEMA, value); + if (!parsed.success) { + pushConfigIssue(issues, path, `${key} must be a string`); + return undefined; + } + return parsed.output; +} + +function validateOptionalNonEmptyString( + value: unknown, + issues: ConfigIssue[], + path: string, + key: string, +): string | undefined { + if (value === undefined) return undefined; + const parsed = v.safeParse(NON_EMPTY_STRING_SCHEMA, value); + if (!parsed.success) { + pushConfigIssue(issues, path, `${key} must be a non-empty string`); + return undefined; + } + return parsed.output; +} + +function validateOptionalBoolean( + value: unknown, + issues: ConfigIssue[], + path: string, + key: string, +): boolean | undefined { + if (value === undefined) return undefined; + if (typeof value === "boolean") return value; + + if (typeof value === "string" || typeof value === "number" || typeof value === "bigint") { + const normalized = String(value).trim().toLowerCase(); + if (["true", "1", "yes", "y", "on"].includes(normalized)) return true; + if (["false", "0", "no", "n", "off"].includes(normalized)) return false; + } + + pushConfigIssue(issues, path, `${key} must be a boolean`); + return undefined; +} + +function validateOptionalUrl( + value: unknown, + issues: ConfigIssue[], + path: string, + key: string, +): string | undefined { + if (value === undefined) return undefined; + + const stringValue = validateOptionalString(value, issues, path, key); + if (stringValue === undefined) return undefined; + + const parsed = v.safeParse(HTTP_URL_SCHEMA, stringValue); + if (!parsed.success) { + pushConfigIssue(issues, path, `${key} must be a valid http(s) URL`); + return undefined; + } + + return parsed.output; +} + +function validateOptionalMetadata( + value: unknown, + issues: ConfigIssue[], + path: string, + key: string, +): JsonObject | undefined { + if (value === undefined) return undefined; + + const parsed = v.safeParse(JSON_OBJECT_SCHEMA, value); + if (!parsed.success) { + pushConfigIssue(issues, path, `${key} must be a JSON object`); + return undefined; + } + + return parsed.output as JsonObject; +} + +function applyConfig( + target: TraceConfig, + source: JsonObject | undefined, + path: string, + issues: ConfigIssue[], +): ApplyConfigResult { + if (!source) { + return { + parentSpanConfigured: false, + rootSpanConfigured: false, + }; + } + + const apiKey = validateOptionalString(source.api_key, issues, path, "api_key"); + if (apiKey !== undefined) target.apiKey = apiKey; + + const apiUrl = validateOptionalUrl(source.api_url, issues, path, "api_url"); + if (apiUrl !== undefined) target.apiUrl = apiUrl; + + const appUrl = validateOptionalUrl(source.app_url, issues, path, "app_url"); + if (appUrl !== undefined) target.appUrl = appUrl; + + const orgName = validateOptionalNonEmptyString(source.org_name, issues, path, "org_name"); + if (orgName !== undefined) target.orgName = orgName; + + const projectName = validateOptionalNonEmptyString(source.project, issues, path, "project"); + if (projectName !== undefined) target.projectName = projectName; + + const enabled = validateOptionalBoolean( + source.trace_to_braintrust, + issues, + path, + "trace_to_braintrust", + ); + if (enabled !== undefined) target.enabled = enabled; + + const debug = validateOptionalBoolean(source.debug, issues, path, "debug"); + if (debug !== undefined) target.debug = debug; + + const logFile = validateOptionalString(source.log_file, issues, path, "log_file"); + if (logFile !== undefined) target.logFile = logFile; + + const stateDir = validateOptionalNonEmptyString(source.state_dir, issues, path, "state_dir"); + if (stateDir !== undefined) target.stateDir = stateDir; + + const parentSpanId = validateOptionalNonEmptyString( + source.parent_span_id, + issues, + path, + "parent_span_id", + ); + if (parentSpanId !== undefined) target.parentSpanId = parentSpanId; + + const rootSpanId = validateOptionalNonEmptyString( + source.root_span_id, + issues, + path, + "root_span_id", + ); + if (rootSpanId !== undefined) target.rootSpanId = rootSpanId; + + const additionalMetadata = validateOptionalMetadata( + source.additional_metadata, + issues, + path, + "additional_metadata", + ); + if (additionalMetadata !== undefined) target.additionalMetadata = additionalMetadata; + + const showUi = validateOptionalBoolean(source.show_ui, issues, path, "show_ui"); + if (showUi !== undefined) target.showUi = showUi; + + const showTraceLink = validateOptionalBoolean( + source.show_trace_link, + issues, + path, + "show_trace_link", + ); + if (showTraceLink !== undefined) target.showTraceLink = showTraceLink; + + return { + parentSpanConfigured: parentSpanId !== undefined, + rootSpanConfigured: rootSpanId !== undefined, + }; +} + +export function loadConfig(cwd = process.cwd()): TraceConfig { + const config: TraceConfig = { + enabled: false, + apiKey: "", + apiUrl: undefined, + appUrl: "https://www.braintrust.dev", + orgName: undefined, + projectName: "pi", + debug: false, + logFile: undefined, + stateDir: DEFAULT_STATE_DIR, + additionalMetadata: {}, + parentSpanId: undefined, + rootSpanId: undefined, + showUi: true, + showTraceLink: true, + configIssues: [], + }; + + const globalConfigPath = join(homedir(), ".pi", "agent", "braintrust.json"); + const projectConfigPath = join(cwd, PROJECT_CONFIG_DIR_NAME, "braintrust.json"); + + let parentSpanConfigured = false; + let rootSpanConfigured = false; + + const globalConfig = readConfigFile(globalConfigPath); + if (globalConfig.error) { + pushConfigIssue(config.configIssues, globalConfigPath, globalConfig.error); + } + { + const applied = applyConfig(config, globalConfig.value, globalConfigPath, config.configIssues); + parentSpanConfigured ||= applied.parentSpanConfigured; + rootSpanConfigured ||= applied.rootSpanConfigured; + } + + const projectConfig = readConfigFile(projectConfigPath); + if (projectConfig.error) { + pushConfigIssue(config.configIssues, projectConfigPath, projectConfig.error); + } + { + const applied = applyConfig( + config, + projectConfig.value, + projectConfigPath, + config.configIssues, + ); + parentSpanConfigured ||= applied.parentSpanConfigured; + rootSpanConfigured ||= applied.rootSpanConfigured; + } + + const envApiKey = validateOptionalString( + process.env.BRAINTRUST_API_KEY, + config.configIssues, + "BRAINTRUST_API_KEY", + "BRAINTRUST_API_KEY", + ); + if (envApiKey !== undefined) config.apiKey = envApiKey; + + const envApiUrl = validateOptionalUrl( + process.env.BRAINTRUST_API_URL, + config.configIssues, + "BRAINTRUST_API_URL", + "BRAINTRUST_API_URL", + ); + if (envApiUrl !== undefined) config.apiUrl = envApiUrl; + + const envAppUrl = validateOptionalUrl( + process.env.BRAINTRUST_APP_URL, + config.configIssues, + "BRAINTRUST_APP_URL", + "BRAINTRUST_APP_URL", + ); + if (envAppUrl !== undefined) config.appUrl = envAppUrl; + + const envOrgName = validateOptionalNonEmptyString( + process.env.BRAINTRUST_ORG_NAME, + config.configIssues, + "BRAINTRUST_ORG_NAME", + "BRAINTRUST_ORG_NAME", + ); + if (envOrgName !== undefined) config.orgName = envOrgName; + + const envProjectName = validateOptionalNonEmptyString( + process.env.BRAINTRUST_PROJECT, + config.configIssues, + "BRAINTRUST_PROJECT", + "BRAINTRUST_PROJECT", + ); + if (envProjectName !== undefined) config.projectName = envProjectName; + + const envEnabled = validateOptionalBoolean( + process.env.TRACE_TO_BRAINTRUST, + config.configIssues, + "TRACE_TO_BRAINTRUST", + "TRACE_TO_BRAINTRUST", + ); + if (envEnabled !== undefined) config.enabled = envEnabled; + + const envDebug = validateOptionalBoolean( + process.env.BRAINTRUST_DEBUG, + config.configIssues, + "BRAINTRUST_DEBUG", + "BRAINTRUST_DEBUG", + ); + if (envDebug !== undefined) config.debug = envDebug; + + const envLogFile = validateOptionalString( + process.env.BRAINTRUST_LOG_FILE, + config.configIssues, + "BRAINTRUST_LOG_FILE", + "BRAINTRUST_LOG_FILE", + ); + if (envLogFile !== undefined) config.logFile = envLogFile; + + const envStateDir = validateOptionalNonEmptyString( + process.env.BRAINTRUST_STATE_DIR, + config.configIssues, + "BRAINTRUST_STATE_DIR", + "BRAINTRUST_STATE_DIR", + ); + if (envStateDir !== undefined) config.stateDir = envStateDir; + + const envShowUi = validateOptionalBoolean( + process.env.BRAINTRUST_SHOW_UI, + config.configIssues, + "BRAINTRUST_SHOW_UI", + "BRAINTRUST_SHOW_UI", + ); + if (envShowUi !== undefined) config.showUi = envShowUi; + + const envShowTraceLink = validateOptionalBoolean( + process.env.BRAINTRUST_SHOW_TRACE_LINK, + config.configIssues, + "BRAINTRUST_SHOW_TRACE_LINK", + "BRAINTRUST_SHOW_TRACE_LINK", + ); + if (envShowTraceLink !== undefined) config.showTraceLink = envShowTraceLink; + + const envParentSpanId = validateOptionalNonEmptyString( + process.env.PI_PARENT_SPAN_ID, + config.configIssues, + "PI_PARENT_SPAN_ID", + "PI_PARENT_SPAN_ID", + ); + if (envParentSpanId !== undefined) { + config.parentSpanId = envParentSpanId; + parentSpanConfigured = true; + } + + const envRootSpanId = validateOptionalNonEmptyString( + process.env.PI_ROOT_SPAN_ID, + config.configIssues, + "PI_ROOT_SPAN_ID", + "PI_ROOT_SPAN_ID", + ); + if (envRootSpanId !== undefined) { + config.rootSpanId = envRootSpanId; + rootSpanConfigured = true; + } + + if (process.env.BRAINTRUST_ADDITIONAL_METADATA !== undefined) { + try { + const parsed = JSON.parse(process.env.BRAINTRUST_ADDITIONAL_METADATA) as unknown; + const metadata = validateOptionalMetadata( + parsed, + config.configIssues, + "BRAINTRUST_ADDITIONAL_METADATA", + "BRAINTRUST_ADDITIONAL_METADATA", + ); + if (metadata !== undefined) config.additionalMetadata = metadata; + } catch (error) { + pushConfigIssue( + config.configIssues, + "BRAINTRUST_ADDITIONAL_METADATA", + error instanceof Error ? error.message : String(error), + ); + } + } + + if (config.parentSpanId && !config.rootSpanId) config.rootSpanId = config.parentSpanId; + if (config.rootSpanId && !config.parentSpanId) config.parentSpanId = config.rootSpanId; + + if ( + parentSpanConfigured && + rootSpanConfigured && + config.parentSpanId && + config.rootSpanId && + config.parentSpanId === config.rootSpanId + ) { + pushConfigIssue( + config.configIssues, + "parent_span_id/root_span_id", + "parent_span_id and root_span_id are identical; set only one unless the parent span is also the trace root", + "warning", + ); + } + + if (config.enabled && !config.apiKey) { + pushConfigIssue( + config.configIssues, + "BRAINTRUST_API_KEY", + "TRACE_TO_BRAINTRUST is enabled but BRAINTRUST_API_KEY is not set", + "warning", + ); + } + + ensureDir(config.stateDir); + return config; +} + +export function createLogger(config: TraceConfig): Logger { + const explicitLogFile = + config.logFile && config.logFile !== "true" && config.logFile !== "auto" + ? config.logFile + : join(config.stateDir, "braintrust-pi-extension.log"); + const infoLoggingEnabled = config.debug || Boolean(config.logFile); + + let logDirEnsured = false; + + function shouldLog(level: LogLevel): boolean { + return level === "warn" || level === "error" || infoLoggingEnabled; + } + + function ensureLogDir(): void { + if (logDirEnsured) return; + ensureDir(dirname(explicitLogFile)); + logDirEnsured = true; + } + + let pendingWrite = Promise.resolve(); + + function emit(level: LogLevel, message: string, data?: unknown): void { + if (!shouldLog(level)) return; + ensureLogDir(); + pendingWrite = pendingWrite + .catch(() => {}) + .then(async () => { + try { + await writeJsonLog(explicitLogFile, level, message, data); + } catch { + // Logging is best-effort and must never affect pi session execution. + } + }); + } + + return { + filePath: explicitLogFile, + debug(message, data) { + if (config.debug) emit("debug", message, data); + }, + info(message, data) { + emit("info", message, data); + }, + warn(message, data) { + emit("warn", message, data); + }, + error(message, data) { + emit("error", message, data); + }, + async flush() { + await pendingWrite.catch(() => {}); + }, + }; +} diff --git a/src/plugins/pi/content/src/daemon-adapter.test.ts b/src/plugins/pi/content/src/daemon-adapter.test.ts new file mode 100644 index 0000000..7dfced3 --- /dev/null +++ b/src/plugins/pi/content/src/daemon-adapter.test.ts @@ -0,0 +1,96 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mockState = vi.hoisted(() => ({ + logs: [] as Array>, + flushes: [] as string[], + closed: 0, +})); + +vi.mock("./runtime/daemon-client.ts", () => ({ + DaemonClient: class { + async log(envelope: Record): Promise { + mockState.logs.push(envelope); + return true; + } + async flush(sessionId: string): Promise { + mockState.flushes.push(sessionId); + return true; + } + async status(sessionId: string): Promise> { + return { + daemon_version: "test", + uptime_ms: 1, + sessions: [ + { + session_id: sessionId, + source: "pi", + queued: 0, + spans_emitted: 1, + permalink: "https://www.braintrust.dev/trace/1", + }, + ], + }; + } + close(): void { + mockState.closed += 1; + } + }, +})); + +vi.mock("./config.ts", () => ({ + loadConfig: () => ({ enabled: true, showUi: true, showTraceLink: true }), +})); + +describe("Pi daemon adapter", () => { + beforeEach(() => { + mockState.logs.length = 0; + mockState.flushes.length = 0; + mockState.closed = 0; + }); + + it("forwards native events and keeps the trace-link UI", async () => { + const handlers = new Map Promise>(); + const statuses: unknown[] = []; + const widgets: unknown[] = []; + const pi = { + on: (name: string, handler: (...args: unknown[]) => Promise) => + handlers.set(name, handler), + }; + const ctx = { + cwd: "/tmp/project", + model: { provider: "openai", id: "gpt-5" }, + hasUI: true, + ui: { + setStatus: (...args: unknown[]) => statuses.push(args), + setWidget: (...args: unknown[]) => widgets.push(args), + }, + sessionManager: { + getSessionFile: () => "/tmp/session.jsonl", + getSessionId: () => "native-session", + }, + }; + const { default: extension } = await import("./index.ts"); + extension(pi as never); + + await handlers.get("session_start")?.({ reason: "new" }, ctx); + await handlers.get("before_agent_start")?.({ prompt: "hello" }, ctx); + await handlers.get("agent_end")?.({ messages: [] }); + await handlers.get("session_shutdown")?.({ reason: "quit" }, ctx); + + expect(mockState.logs.map((log) => log.event)).toEqual([ + "session_start", + "before_agent_start", + "agent_end", + "session_shutdown", + ]); + expect( + mockState.logs.every((log) => log.source === "pi" && typeof log.ts_ms === "number"), + ).toBe(true); + expect(mockState.flushes).toHaveLength(2); + expect( + widgets.some((args) => JSON.stringify(args).includes("https://www.braintrust.dev/trace/1")), + ).toBe(true); + expect(statuses.length).toBeGreaterThan(0); + expect(mockState.closed).toBe(1); + }); +}); diff --git a/src/plugins/pi/content/src/index.integration.test.ts b/src/plugins/pi/content/src/index.integration.test.ts new file mode 100644 index 0000000..cd471f2 --- /dev/null +++ b/src/plugins/pi/content/src/index.integration.test.ts @@ -0,0 +1,726 @@ +import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + createAssistantMessageEventStream, + type Api, + type AssistantMessage, + type AssistantMessageEventStream, + type Context, + type Model, + type SimpleStreamOptions, + type ToolCall, +} from "@earendil-works/pi-ai"; +import * as piCodingAgent from "@earendil-works/pi-coding-agent"; +import { + createAgentSession, + DefaultResourceLoader, + SessionManager, + type ExtensionAPI, +} from "@earendil-works/pi-coding-agent"; +import { Type } from "typebox"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import braintrustPiExtension from "./legacy-processor.ts"; + +const mockState = vi.hoisted(() => ({ + startSpans: [] as Array>, + logSpans: [] as Array>, + endSpans: [] as Array>, + updateSpans: [] as Array>, + initializeCalls: 0, + flushCalls: 0, + failInitialize: false, +})); + +vi.mock("./client.ts", () => { + class MockBraintrustClient { + async initialize(): Promise { + mockState.initializeCalls += 1; + if (mockState.failInitialize) { + throw new Error("simulated Braintrust init failure"); + } + } + + startSpan(args: Record): Record { + mockState.startSpans.push(args); + return { + id: `record-${String(args.spanId)}`, + spanId: args.spanId, + rootSpanId: args.rootSpanId, + }; + } + + logSpan(span: Record | undefined, event: Record): void { + mockState.logSpans.push({ span, event }); + } + + endSpan(span: Record | undefined, endedAt?: number): void { + mockState.endSpans.push({ span, endedAt }); + } + + getSpanLink(span: Record | undefined): string | undefined { + if (!span) return undefined; + return `https://www.braintrust.dev/app/test-org/p/pi/logs?oid=${String(span.id)}`; + } + + async getSpanPermalink(span: Record | undefined): Promise { + return this.getSpanLink(span); + } + + updateSpan(args: Record): void { + mockState.updateSpans.push(args); + } + + async flush(): Promise { + mockState.flushCalls += 1; + } + } + + return { + BraintrustClient: MockBraintrustClient, + }; +}); + +const ENV_KEYS = [ + "HOME", + "TRACE_TO_BRAINTRUST", + "BRAINTRUST_API_KEY", + "BRAINTRUST_STATE_DIR", + "BRAINTRUST_PROJECT", + "BRAINTRUST_ORG_NAME", + "BRAINTRUST_ADDITIONAL_METADATA", +] as const; + +const originalEnv = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]])); +const originalProcessCwd = process.cwd(); +const tempDirs: string[] = []; + +const TEST_API = "pi-extension-test-api" as Api; + +const TEST_MODEL: Model = { + id: "pi-extension-test-model", + name: "PI Extension Test Model", + api: TEST_API, + provider: "pi-extension-test-provider", + baseUrl: "https://example.invalid", + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32_000, + maxTokens: 4_096, +}; + +beforeEach(() => { + mockState.startSpans.length = 0; + mockState.logSpans.length = 0; + mockState.endSpans.length = 0; + mockState.updateSpans.length = 0; + mockState.initializeCalls = 0; + mockState.flushCalls = 0; + mockState.failInitialize = false; + + for (const key of ENV_KEYS) { + delete process.env[key]; + } +}); + +afterEach(() => { + for (const key of ENV_KEYS) { + const value = originalEnv[key]; + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + + process.chdir(originalProcessCwd); + + while (tempDirs.length > 0) { + rmSync(tempDirs.pop()!, { recursive: true, force: true }); + } +}); + +function makeTempDir(prefix: string): string { + const dir = mkdtempSync(join(tmpdir(), prefix)); + tempDirs.push(dir); + return dir; +} + +// Injected by CI (see .github/workflows/ci.yml). When unset (e.g. local dev) we +// assume the currently installed pi is at least as new as any version we branch +// on below. +const PI_COMPAT_VERSION = process.env.PI_COMPAT_VERSION; + +function piCompatAtLeast(target: string): boolean { + if (!PI_COMPAT_VERSION) return true; + const parse = (v: string) => + v + .split("-")[0] + .split(".") + .map((part) => Number.parseInt(part, 10) || 0); + const actual = parse(PI_COMPAT_VERSION); + const wanted = parse(target); + for (let i = 0; i < Math.max(actual.length, wanted.length); i += 1) { + const a = actual[i] ?? 0; + const w = wanted[i] ?? 0; + if (a !== w) return a > w; + } + return true; +} + +function buildAssistantMessage(model: Model): AssistantMessage { + return { + role: "assistant", + content: [], + api: model.api, + provider: model.provider, + model: model.id, + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + total: 0, + }, + }, + stopReason: "stop", + timestamp: Date.now(), + }; +} + +function userText(context: Context): string { + for (let index = context.messages.length - 1; index >= 0; index -= 1) { + const message = context.messages[index]; + if (!message || message.role !== "user") continue; + if (typeof message.content === "string") return message.content; + return message.content + .filter((part) => part.type === "text") + .map((part) => part.text) + .join("\n"); + } + return ""; +} + +function hasToolResults(context: Context): boolean { + return context.messages.some((message) => message.role === "toolResult"); +} + +function lastToolResult(context: Context) { + return [...context.messages].reverse().find((message) => message.role === "toolResult"); +} + +function pushText( + stream: AssistantMessageEventStream, + output: AssistantMessage, + text: string, +): void { + output.content.push({ type: "text", text: "" }); + const contentIndex = output.content.length - 1; + stream.push({ type: "text_start", contentIndex, partial: output }); + const block = output.content[contentIndex]; + if (block?.type === "text") { + block.text += text; + } + stream.push({ type: "text_delta", contentIndex, delta: text, partial: output }); + stream.push({ type: "text_end", contentIndex, content: text, partial: output }); +} + +function pushToolCall( + stream: AssistantMessageEventStream, + output: AssistantMessage, + toolCall: ToolCall, +): void { + output.content.push(toolCall); + const contentIndex = output.content.length - 1; + stream.push({ type: "toolcall_start", contentIndex, partial: output }); + stream.push({ + type: "toolcall_delta", + contentIndex, + delta: JSON.stringify(toolCall.arguments), + partial: output, + }); + stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output }); +} + +function streamTestModel( + model: Model, + context: Context, + _options?: SimpleStreamOptions, +): AssistantMessageEventStream { + const stream = createAssistantMessageEventStream(); + + queueMicrotask(() => { + const output = buildAssistantMessage(model); + stream.push({ type: "start", partial: output }); + + const prompt = userText(context); + if (prompt.includes("deferred-tools")) { + const toolResult = lastToolResult(context); + if (toolResult?.role === "toolResult" && toolResult.toolName === "tool_search") { + pushToolCall(stream, output, { + type: "toolCall", + id: "calculator-1", + name: "Calculator", + arguments: { expression: "100 + 500" }, + }); + output.stopReason = "toolUse"; + } else if (toolResult?.role === "toolResult" && toolResult.toolName === "Calculator") { + pushText(stream, output, "600"); + output.stopReason = "stop"; + } else { + pushToolCall(stream, output, { + type: "toolCall", + id: "tool-search-1", + name: "tool_search", + arguments: { query: "calculator" }, + }); + output.stopReason = "toolUse"; + } + stream.push({ type: "done", reason: output.stopReason, message: output }); + stream.end(); + return; + } + + if (hasToolResults(context)) { + pushText(stream, output, "parallel tools finished"); + output.stopReason = "stop"; + stream.push({ type: "done", reason: "stop", message: output }); + stream.end(); + return; + } + + if (prompt.includes("parallel-tools")) { + pushToolCall(stream, output, { + type: "toolCall", + id: "tool-1", + name: "bash", + arguments: { + command: "sleep 0.05; echo slow", + }, + }); + pushToolCall(stream, output, { + type: "toolCall", + id: "tool-2", + name: "bash", + arguments: { + command: "echo fast", + }, + }); + output.stopReason = "toolUse"; + stream.push({ type: "done", reason: "toolUse", message: output }); + stream.end(); + return; + } + + pushText(stream, output, "plain response"); + output.stopReason = "stop"; + stream.push({ type: "done", reason: "stop", message: output }); + stream.end(); + }); + + return stream; +} + +function testHarnessExtension(pi: ExtensionAPI): void { + pi.registerTool({ + name: "Calculator", + label: "Calculator", + description: "Evaluate a simple arithmetic expression.", + parameters: Type.Object({ expression: Type.String() }), + async execute(_toolCallId, params) { + return { + content: [{ type: "text", text: params.expression === "100 + 500" ? "600" : "0" }], + details: {}, + }; + }, + }); + + pi.registerTool({ + name: "tool_search", + label: "Tool Search", + description: "Find and activate tools for a capability.", + parameters: Type.Object({ query: Type.String() }), + async execute() { + const active = pi.getActiveTools(); + if (!active.includes("Calculator")) pi.setActiveTools([...active, "Calculator"]); + return { + content: [{ type: "text", text: "Loaded Calculator" }], + details: { matches: ["Calculator"] }, + }; + }, + }); + + pi.registerProvider("pi-extension-test-provider", { + baseUrl: TEST_MODEL.baseUrl, + apiKey: "pi-extension-test-key", + api: TEST_API, + streamSimple: streamTestModel, + models: [TEST_MODEL], + }); + + pi.on("session_start", () => { + pi.setActiveTools(pi.getActiveTools().filter((name) => name !== "Calculator")); + }); + + pi.registerCommand("test-reload", { + description: "Reload the runtime for integration tests", + handler: async (_args, ctx) => { + await ctx.reload(); + return; + }, + }); +} + +async function waitForAsyncWork(): Promise { + await new Promise((resolve) => setTimeout(resolve, 0)); +} + +interface TestSessionController { + prompt(text: string): Promise; + newSession(): Promise; + switchSession(sessionPath: string): Promise; + fork(entryId: string): Promise<{ cancelled: boolean; selectedText: string }>; + dispose(): Promise; + readonly sessionFile: string | undefined; + readonly sessionManager: SessionManager; +} + +async function createHarness(options?: { + rootDir?: string; + sessionManager?: SessionManager; + sessionMode?: "inMemory" | "persistent"; + sessionFile?: string; + sessionsDir?: string; +}) { + const home = options?.rootDir ?? makeTempDir("pi-extension-home-"); + const cwd = join(home, "workspace"); + const agentDir = join(home, "agent"); + const stateDir = join(home, "state"); + + mkdirSync(cwd, { recursive: true }); + mkdirSync(agentDir, { recursive: true }); + + process.env.HOME = home; + process.env.TRACE_TO_BRAINTRUST = "true"; + process.env.BRAINTRUST_API_KEY = "test-key"; + process.env.BRAINTRUST_PROJECT = "pi"; + process.env.BRAINTRUST_ORG_NAME = "test-org"; + process.env.BRAINTRUST_STATE_DIR = stateDir; + + const sessionManager = + options?.sessionManager ?? + (options?.sessionFile + ? SessionManager.open(options.sessionFile, options.sessionsDir) + : options?.sessionMode === "persistent" + ? SessionManager.create(cwd, options.sessionsDir) + : SessionManager.inMemory(cwd)); + + const compat = piCodingAgent as any; + if ( + typeof compat.createAgentSessionRuntime === "function" && + typeof compat.createAgentSessionServices === "function" && + typeof compat.createAgentSessionFromServices === "function" + ) { + const runtime = await compat.createAgentSessionRuntime( + async ({ + cwd: runtimeCwd, + agentDir: runtimeAgentDir, + sessionManager: runtimeSessionManager, + sessionStartEvent, + }: { + cwd: string; + agentDir: string; + sessionManager: SessionManager; + sessionStartEvent?: unknown; + }) => { + const services = await compat.createAgentSessionServices({ + cwd: runtimeCwd, + agentDir: runtimeAgentDir, + resourceLoaderOptions: { + extensionFactories: [testHarnessExtension, braintrustPiExtension], + }, + }); + + return { + ...(await compat.createAgentSessionFromServices({ + services, + sessionManager: runtimeSessionManager, + sessionStartEvent, + model: TEST_MODEL, + })), + services, + diagnostics: services.diagnostics, + }; + }, + { + cwd, + agentDir, + sessionManager, + }, + ); + + const bindRuntimeSession = async (): Promise => { + await runtime.session.bindExtensions({}); + }; + + await bindRuntimeSession(); + + const session: TestSessionController = { + prompt: (text) => runtime.session.prompt(text), + newSession: async () => { + const result = await runtime.newSession(); + if (!result.cancelled) { + await bindRuntimeSession(); + } + return !result.cancelled; + }, + switchSession: async (sessionPath) => { + const result = await runtime.switchSession(sessionPath); + if (!result.cancelled) { + await bindRuntimeSession(); + } + return !result.cancelled; + }, + fork: async (entryId) => { + const result = await runtime.fork(entryId); + if (!result.cancelled) { + await bindRuntimeSession(); + } + return { + cancelled: result.cancelled, + selectedText: result.selectedText ?? "", + }; + }, + dispose: async () => { + await runtime.dispose(); + }, + get sessionFile() { + return runtime.session.sessionFile; + }, + get sessionManager() { + return runtime.session.sessionManager as SessionManager; + }, + }; + + return { agentDir, cwd, session, stateDir }; + } + + // TODO: Remove this legacy fallback once our supported pi compatibility window no + // longer includes pi <0.65.0, which introduced the session runtime API and the + // session_start-only post-transition model. + const resourceLoader = new DefaultResourceLoader({ + cwd, + agentDir, + extensionFactories: [testHarnessExtension, braintrustPiExtension], + }); + await resourceLoader.reload(); + + const { session: legacySession } = await createAgentSession({ + cwd, + agentDir, + model: TEST_MODEL, + resourceLoader, + sessionManager, + }); + const legacyRuntimeSession = legacySession as typeof legacySession & { + newSession(): Promise; + switchSession(sessionPath: string): Promise; + fork(entryId: string): Promise<{ cancelled: boolean; selectedText?: string }>; + sessionManager: SessionManager; + }; + + const session: TestSessionController = { + prompt: (text) => legacyRuntimeSession.prompt(text), + newSession: () => legacyRuntimeSession.newSession(), + switchSession: (sessionPath) => legacyRuntimeSession.switchSession(sessionPath), + fork: async (entryId) => { + const result = await legacyRuntimeSession.fork(entryId); + return { + cancelled: result.cancelled, + selectedText: result.selectedText ?? "", + }; + }, + dispose: async () => { + legacyRuntimeSession.dispose(); + }, + get sessionFile() { + return legacyRuntimeSession.sessionFile; + }, + get sessionManager() { + return legacyRuntimeSession.sessionManager; + }, + }; + + return { agentDir, cwd, session, stateDir }; +} + +function rootTaskSpans(): Array> { + return mockState.startSpans.filter( + (span) => span.type === "task" && span.parentSpanId === undefined, + ); +} + +describe("braintrustPiExtension integration", () => { + it("restores persisted trace state when reopening the same pi session", async () => { + const rootDir = makeTempDir("pi-extension-home-"); + const sessionsDir = makeTempDir("pi-extension-sessions-"); + const first = await createHarness({ + rootDir, + sessionMode: "persistent", + sessionsDir, + }); + await first.session.prompt("create a traced turn"); + const firstSessionFile = first.session.sessionFile; + + expect(firstSessionFile).toBeTruthy(); + expect(rootTaskSpans()).toHaveLength(1); + + await first.session.dispose(); + await waitForAsyncWork(); + + const reopened = await createHarness({ + rootDir, + sessionFile: firstSessionFile!, + sessionsDir, + }); + await reopened.session.prompt("resume the same traced session"); + await reopened.session.dispose(); + await waitForAsyncWork(); + + expect(rootTaskSpans()).toHaveLength(1); + expect( + mockState.startSpans.filter( + (span) => span.type === "task" && span.parentSpanId !== undefined, + ), + ).toHaveLength(2); + }); + + it("keeps one root span across session switch, fork, and resume flows", async () => { + const sessionsDir = makeTempDir("pi-extension-sessions-"); + const { session } = await createHarness({ + sessionMode: "persistent", + sessionsDir, + }); + + await session.prompt("session a"); + const sessionAFile = session.sessionFile; + + const switched = await session.newSession(); + expect(switched).toBe(true); + await session.prompt("session b"); + const sessionBFile = session.sessionFile; + + const forkEntryId = session.sessionManager + .getBranch() + .find((entry) => entry.type === "message")?.id; + expect(forkEntryId).toBeTruthy(); + const forked = await session.fork(forkEntryId!); + expect(forked.cancelled).toBe(false); + await session.prompt("session c from fork"); + const sessionCFile = session.sessionFile; + + expect(sessionAFile).toBeTruthy(); + expect(sessionBFile).toBeTruthy(); + expect(sessionCFile).toBeTruthy(); + expect(sessionCFile).not.toBe(sessionBFile); + + const resumed = await session.switchSession(sessionAFile!); + expect(resumed).toBe(true); + await session.prompt("back on session a"); + + await session.dispose(); + await waitForAsyncWork(); + + expect(rootTaskSpans()).toHaveLength(3); + expect( + rootTaskSpans().find( + (span) => + (span.metadata as Record | undefined)?.opened_via === "session_fork", + ), + ).toMatchObject({ + metadata: { + opened_via: "session_fork", + parent_session_file: sessionBFile, + }, + }); + }); + + it("preserves pi's parallel tool end ordering when creating tool spans", async () => { + const { session } = await createHarness(); + + await session.prompt("parallel-tools"); + await session.dispose(); + await waitForAsyncWork(); + + const llmSpans = mockState.startSpans.filter((span) => span.type === "llm"); + const toolSpans = mockState.startSpans.filter((span) => span.type === "tool"); + const firstLlmSpanId = llmSpans[0]?.spanId; + + expect(toolSpans).toHaveLength(2); + // pi < 0.68.1 emits `tool_execution_end` in assistant source order, so the + // extension logs tool spans as [tool-1, tool-2]. Starting with pi 0.68.1 the + // agent emits parallel tool completions eagerly (completion order), so the + // fast `tool-2` finishes before the slow `tool-1` and spans are logged as + // [tool-2, tool-1]. See pi-coding-agent changelog 0.68.1 / issue #3503. + // TODO: drop the pi < 0.68.1 branch once we stop testing against it. + const expectedToolCallIdOrder = piCompatAtLeast("0.68.1") + ? ["tool-2", "tool-1"] + : ["tool-1", "tool-2"]; + expect( + toolSpans.map((span) => (span.metadata as Record | undefined)?.tool_call_id), + ).toEqual(expectedToolCallIdOrder); + expect(toolSpans.map((span) => span.parentSpanId)).toEqual([firstLlmSpanId, firstLlmSpanId]); + + const firstLlmLog = mockState.logSpans.find( + (entry) => (entry.span as { spanId?: unknown } | undefined)?.spanId === firstLlmSpanId, + ); + const firstLlmEvent = firstLlmLog?.event as Record | undefined; + const firstLlmMetrics = firstLlmEvent?.metrics as Record | undefined; + expect(firstLlmMetrics?.time_to_first_token).toEqual(expect.any(Number)); + }); + + it("traces tools activated during a deferred tool-loading turn", async () => { + const { session } = await createHarness(); + + await session.prompt("deferred-tools"); + await session.dispose(); + await waitForAsyncWork(); + + const toolSpans = mockState.startSpans.filter((span) => span.type === "tool"); + expect(toolSpans.map((span) => span.name)).toEqual(["tool_search", "Calculator"]); + + if (piCompatAtLeast("0.80.7")) { + expect(toolSpans[0]?.metadata).toMatchObject({ + activated_tools: ["Calculator"], + activated_tool_count: 1, + }); + const llmSpans = mockState.startSpans.filter((span) => span.type === "llm"); + expect(llmSpans[1]?.metadata).toMatchObject({ + activated_tools: ["Calculator"], + }); + } + }); + + it("stops tracing new work after Braintrust initialization fails", async () => { + mockState.failInitialize = true; + + const { session } = await createHarness(); + await waitForAsyncWork(); + await session.prompt("plain-response"); + await session.dispose(); + await waitForAsyncWork(); + + expect(mockState.initializeCalls).toBe(1); + expect(mockState.startSpans).toEqual([]); + expect(mockState.flushCalls).toBe(0); + }); +}); diff --git a/src/plugins/pi/content/src/index.test.ts b/src/plugins/pi/content/src/index.test.ts new file mode 100644 index 0000000..9b41f8d --- /dev/null +++ b/src/plugins/pi/content/src/index.test.ts @@ -0,0 +1,1230 @@ +import { readFileSync } from "node:fs"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const packageVersion = (JSON.parse(readFileSync("package.json", "utf8")) as { version: string }) + .version; + +const mockState = vi.hoisted(() => ({ + startSpans: [] as Array>, + logSpans: [] as Array>, + endSpans: [] as Array>, + updateSpans: [] as Array>, + statuses: [] as Array<{ key: string; text: string | undefined }>, + widgets: [] as Array<{ key: string; content: string[] | undefined }>, + initializeCalls: 0, + flushCalls: 0, + config: { + enabled: true, + apiKey: "test-key", + apiUrl: undefined, + appUrl: "https://www.braintrust.dev", + orgName: undefined, + projectName: "pi", + debug: false, + logFile: undefined, + stateDir: "/tmp/braintrust-pi-extension-test", + additionalMetadata: {}, + parentSpanId: undefined, + rootSpanId: undefined, + showUi: true, + showTraceLink: true, + configIssues: [] as Array<{ path: string; message: string; severity: "error" | "warning" }>, + }, +})); + +vi.mock("./client.ts", () => { + class MockBraintrustClient { + async initialize(): Promise { + mockState.initializeCalls += 1; + } + + startSpan(args: Record): Record { + mockState.startSpans.push(args); + return { + id: `record-${String(args.spanId)}`, + spanId: args.spanId, + rootSpanId: args.rootSpanId, + }; + } + + logSpan(span: Record | undefined, event: Record): void { + mockState.logSpans.push({ span, event }); + } + + endSpan(span: Record | undefined, endedAt?: number): void { + mockState.endSpans.push({ span, endedAt }); + } + + getSpanLink(span: Record | undefined): string | undefined { + if (!span) return undefined; + return "https://www.braintrust.dev/app/test-org/p/pi/logs?oid=trace-row-1"; + } + + async getSpanPermalink(span: Record | undefined): Promise { + return this.getSpanLink(span); + } + + updateSpan(args: Record): void { + mockState.updateSpans.push(args); + } + + async flush(): Promise { + mockState.flushCalls += 1; + } + } + + return { + BraintrustClient: MockBraintrustClient, + }; +}); + +vi.mock("./config.ts", () => ({ + loadConfig: () => ({ ...mockState.config }), + createLogger: () => ({ + filePath: "/tmp/braintrust-pi-extension-test.log", + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + flush: async () => {}, + }), +})); + +vi.mock("./state.ts", () => ({ + createStateStore: () => ({ + get: () => undefined, + set: () => undefined, + patch: () => undefined, + delete: () => undefined, + schedulePersist: () => undefined, + flush: async () => {}, + }), +})); + +beforeEach(() => { + mockState.startSpans.length = 0; + mockState.logSpans.length = 0; + mockState.endSpans.length = 0; + mockState.updateSpans.length = 0; + mockState.statuses.length = 0; + mockState.widgets.length = 0; + mockState.initializeCalls = 0; + mockState.flushCalls = 0; + mockState.config = { + enabled: true, + apiKey: "test-key", + apiUrl: undefined, + appUrl: "https://www.braintrust.dev", + orgName: undefined, + projectName: "pi", + debug: false, + logFile: undefined, + stateDir: "/tmp/braintrust-pi-extension-test", + additionalMetadata: {}, + parentSpanId: undefined, + rootSpanId: undefined, + showUi: true, + showTraceLink: true, + configIssues: [], + }; + vi.resetModules(); +}); + +async function createHarness() { + const handlers = new Map unknown>(); + const { default: braintrustPiExtension } = await import("./legacy-processor.ts"); + + braintrustPiExtension({ + on(eventName: string, handler: (...args: unknown[]) => unknown) { + handlers.set(eventName, handler); + }, + getActiveTools() { + return ["read", "bash", "tool_search", "Calculator"]; + }, + } as never); + + const ctx = { + cwd: "/tmp/workspace", + hasUI: true, + model: "anthropic/claude-sonnet-4", + ui: { + theme: { + fg: (_color: string, text: string) => text, + underline: (text: string) => text, + }, + setStatus: (key: string, text: string | undefined) => { + mockState.statuses.push({ key, text }); + }, + setWidget: (key: string, content: string[] | undefined, _options?: unknown) => { + mockState.widgets.push({ key, content }); + }, + }, + sessionManager: { + getSessionFile: () => "/tmp/session.json", + getSessionId: () => "session-1", + getSessionName: () => "Checkout fix", + }, + mode: "json", + }; + + async function emit(eventName: string, event: Record = {}): Promise { + const handler = handlers.get(eventName); + if (!handler) throw new Error(`No handler registered for ${eventName}`); + await handler(event, ctx); + } + + return { emit, handlers, ctx }; +} + +describe("braintrustPiExtension", () => { + it("uses session_start reasons instead of legacy session transition events", async () => { + const { handlers } = await createHarness(); + + expect(handlers.has("session_switch")).toBe(false); + expect(handlers.has("session_fork")).toBe(false); + }); + + it("shows a trace url only after the session produces a turn", async () => { + const { emit } = await createHarness(); + + await emit("session_start"); + + expect(mockState.statuses[0]).toEqual({ + key: "braintrust-tracing", + text: "Braintrust tracing pi", + }); + expect(mockState.startSpans).toEqual([]); + expect(mockState.widgets.at(-1)).toEqual({ + key: "braintrust-trace-link", + content: undefined, + }); + + await emit("before_agent_start", { + prompt: "Inspect the package", + images: [], + }); + + expect(mockState.widgets.at(-1)?.key).toBe("braintrust-trace-link"); + expect(mockState.widgets.at(-1)?.content?.[0]).toContain("Braintrust trace ↗"); + expect(mockState.widgets.at(-1)?.content?.[1]).toBe( + "braintrust.dev/app/test-org/p/pi/logs?oid=trace-row-1", + ); + expect(mockState.startSpans[0]?.metadata).toMatchObject({ extension_version: packageVersion }); + + await emit("session_shutdown"); + + expect(mockState.statuses.at(-1)).toEqual({ + key: "braintrust-tracing", + text: undefined, + }); + expect(mockState.widgets.at(-1)).toEqual({ + key: "braintrust-trace-link", + content: undefined, + }); + }); + + it("surfaces malformed Braintrust config in the UI", async () => { + mockState.config.configIssues = [ + { + path: "/Users/test/.pi/agent/braintrust.json", + message: "Expected double-quoted property name in JSON at position 42", + severity: "error", + }, + ]; + + const { emit } = await createHarness(); + + await emit("session_start"); + + expect(mockState.statuses[0]).toEqual({ + key: "braintrust-tracing", + text: "Braintrust tracing pi (config warning)", + }); + expect(mockState.widgets.at(-1)?.key).toBe("braintrust-trace-link"); + expect(mockState.widgets.at(-1)?.content).toContain("Braintrust config error"); + expect(mockState.widgets.at(-1)?.content?.[1]).toContain(".pi/agent/braintrust.json"); + }); + + it("does not create a root span for an idle session", async () => { + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("session_shutdown"); + + expect(mockState.startSpans).toEqual([]); + expect(mockState.endSpans).toEqual([]); + expect(mockState.updateSpans).toEqual([]); + }); + + it("annotates root spans with pi version, mode, and session name metadata", async () => { + const { emit } = await createHarness(); + + await emit("before_agent_start", { + prompt: "Inspect the package", + images: [], + }); + + expect(mockState.startSpans[0]).toMatchObject({ + name: "Checkout fix", + type: "task", + metadata: { + source: "pi", + extension_version: packageVersion, + pi_version: expect.any(String), + pi_mode: "json", + session_name: "Checkout fix", + }, + }); + }); + + it("annotates turn spans with idle input event metadata", async () => { + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("input", { + text: "/skill:review inspect this", + images: [{ type: "image", source: "screen.png" }], + source: "interactive", + }); + await emit("before_agent_start", { + prompt: "Expanded skill content", + images: [], + }); + + const turnSpan = mockState.startSpans.find( + (span) => span.type === "task" && span.name === "Turn 1", + ); + + expect(turnSpan?.metadata).toMatchObject({ + input_source: "interactive", + input_streaming_behavior: "idle", + input_image_count: 1, + raw_input: "/skill:review inspect this", + }); + }); + + it("annotates turn spans with follow-up input event metadata", async () => { + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("input", { + text: "queue this next", + images: [], + source: "rpc", + streamingBehavior: "followUp", + }); + await emit("before_agent_start", { + prompt: "queue this next", + images: [], + }); + + const turnSpan = mockState.startSpans.find( + (span) => span.type === "task" && span.name === "Turn 1", + ); + + expect(turnSpan?.metadata).toMatchObject({ + input_source: "rpc", + input_streaming_behavior: "followUp", + input_image_count: 0, + raw_input: "queue this next", + }); + }); + + it("records resolved model, thinking level, and provider response metadata on llm spans", async () => { + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("thinking_level_select", { level: "high", previousLevel: "off" }); + await emit("before_agent_start", { + prompt: "Use a routed model", + images: [], + }); + await emit("context", { messages: [{ role: "user", content: "Use a routed model" }] }); + await emit("after_provider_response", { + status: 200, + headers: { + "x-ratelimit-remaining-requests": "42", + "retry-after": "5", + "x-request-id": "request-123", + authorization: "secret", + }, + }); + await emit("message_end", { + message: { + role: "assistant", + provider: "openrouter", + model: "auto", + responseModel: "anthropic/claude-sonnet-4-5", + timestamp: 1_700_000_000_000, + content: [{ type: "text", text: "Done." }], + }, + }); + + const turnSpan = mockState.startSpans.find( + (span) => span.type === "task" && span.name === "Turn 1", + ); + const llmSpan = mockState.startSpans.find((span) => span.type === "llm"); + + expect(turnSpan?.metadata).toMatchObject({ thinking_level: "high" }); + expect(llmSpan).toMatchObject({ name: "anthropic/claude-sonnet-4-5" }); + expect(llmSpan?.metadata).toMatchObject({ + model: "anthropic/claude-sonnet-4-5", + requested_model: "auto", + response_model: "anthropic/claude-sonnet-4-5", + thinking_level: "high", + provider_response_status: 200, + provider_response_headers: { + "x-ratelimit-remaining-requests": "42", + "retry-after": "5", + "x-request-id": "request-123", + }, + }); + const llmMetadata = llmSpan?.metadata as + | { provider_response_headers?: Record } + | undefined; + expect(llmMetadata?.provider_response_headers?.authorization).toBeUndefined(); + }); + + it("records canonical usage, cost, effective thinking, and streaming metrics", async () => { + vi.useFakeTimers(); + try { + const { emit, ctx } = await createHarness(); + (ctx as { model: unknown }).model = { + provider: "kimi-coding", + id: "kimi-k3", + name: "Kimi K3", + api: "anthropic-messages", + reasoning: true, + contextWindow: 262_144, + maxTokens: 65_536, + thinkingLevelMap: { max: "max", high: null }, + compat: { deferredToolsMode: "kimi" }, + }; + + vi.setSystemTime(1_000); + await emit("before_agent_start", { prompt: "Think carefully", images: [] }); + vi.setSystemTime(2_000); + await emit("context", { messages: [{ role: "user", content: "Think carefully" }] }); + await emit("before_provider_request", { + payload: { + model: "kimi-k3", + max_tokens: 65_536, + thinking: { type: "adaptive" }, + output_config: { effort: "max" }, + tools: [{ name: "bash", defer_loading: true }], + messages: [{ role: "assistant", signature: "provider-secret" }], + }, + }); + vi.setSystemTime(2_250); + await emit("message_update", { assistantMessageEvent: { type: "thinking_delta" } }); + vi.setSystemTime(2_750); + await emit("message_update", { assistantMessageEvent: { type: "text_delta" } }); + vi.setSystemTime(3_000); + await emit("message_end", { + message: { + role: "assistant", + api: "anthropic-messages", + provider: "kimi-coding", + model: "kimi-k3", + responseId: "response-1", + content: [ + { type: "thinking", thinking: "A short plan", thinkingSignature: "opaque" }, + { type: "text", text: "Done." }, + ], + usage: { + input: 100, + output: 30, + cacheRead: 20, + cacheWrite: 15, + cacheWrite1h: 10, + reasoning: 12, + totalTokens: 165, + cost: { total: 0.0042 }, + }, + stopReason: "stop", + }, + }); + + const llmSpan = mockState.startSpans.find((span) => span.type === "llm"); + expect(llmSpan?.metadata).toMatchObject({ + provider: "kimi-coding", + model: "kimi-k3", + "pi_coding_agent.api": "anthropic-messages", + "pi_coding_agent.model_name": "Kimi K3", + "pi_coding_agent.response_id": "response-1", + model_supports_reasoning: true, + model_context_window: 262_144, + model_max_tokens: 65_536, + supported_thinking_levels: ["max"], + deferred_tools_mode: "kimi", + provider_request_model: "kimi-k3", + provider_request_max_tokens: 65_536, + effective_thinking_type: "adaptive", + effective_thinking_effort: "max", + effective_thinking_uses_token_budget: false, + effective_thinking_enabled: true, + provider_request_tool_count: 1, + provider_request_deferred_tool_count: 1, + "pi_coding_agent.time_to_first_thinking": 0.25, + "pi_coding_agent.time_to_first_text": 0.75, + "pi_coding_agent.thinking_duration": 0.5, + thinking_block_count: 1, + empty_thinking_block_count: 0, + active_tool_count: 4, + }); + expect(JSON.stringify(llmSpan?.metadata)).not.toContain("provider-secret"); + expect(JSON.stringify(llmSpan?.metadata)).not.toContain("opaque"); + + const llmLog = mockState.logSpans.find( + (entry) => (entry.span as { spanId?: unknown } | undefined)?.spanId === llmSpan?.spanId, + ); + const llmEvent = llmLog?.event as Record | undefined; + const llmMetrics = llmEvent?.metrics as Record | undefined; + expect(JSON.stringify(llmEvent)).not.toContain("opaque"); + expect(llmMetrics).toEqual({ + prompt_tokens: 135, + completion_tokens: 30, + tokens: 165, + prompt_cached_tokens: 20, + completion_reasoning_tokens: 12, + estimated_cost: 0.0042, + prompt_cache_creation_1h_tokens: 10, + prompt_cache_creation_5m_tokens: 5, + time_to_first_token: 0.25, + }); + expect(llmMetrics?.prompt_cache_creation_tokens).toBe(undefined); + } finally { + vi.useRealTimers(); + } + }); + + it("records tools activated through deferred tool loading", async () => { + const { emit, ctx } = await createHarness(); + (ctx as { model: unknown }).model = { + provider: "kimi-coding", + id: "kimi-k3", + compat: { deferredToolsMode: "kimi" }, + }; + + await emit("before_agent_start", { prompt: "Use a calculator", images: [] }); + await emit("message_end", { + message: { + role: "assistant", + provider: "kimi-coding", + model: "kimi-k3", + content: [ + { + type: "toolCall", + id: "tool-search-1", + name: "tool_search", + arguments: { query: "calculator" }, + }, + ], + }, + }); + await emit("tool_execution_start", { + toolCallId: "tool-search-1", + toolName: "tool_search", + args: { query: "calculator" }, + }); + await emit("tool_execution_end", { + toolCallId: "tool-search-1", + toolName: "tool_search", + isError: false, + result: { + content: [{ type: "text", text: "Loaded Calculator" }], + addedToolNames: ["Calculator", "Calculator"], + }, + }); + + const loaderSpan = mockState.startSpans.find((span) => span.name === "tool_search"); + expect(loaderSpan?.metadata).toMatchObject({ + activated_tools: ["Calculator"], + activated_tool_count: 1, + active_tool_count: 4, + deferred_tools_mode: "kimi", + "pi_coding_agent.active_tools": ["read", "bash", "tool_search", "Calculator"], + }); + + await emit("context", { + messages: [{ role: "toolResult", toolName: "tool_search", content: "Loaded" }], + }); + await emit("message_end", { + message: { + role: "assistant", + provider: "kimi-coding", + model: "kimi-k3", + content: [{ type: "text", text: "The tool is ready." }], + }, + }); + + const llmSpans = mockState.startSpans.filter((span) => span.type === "llm"); + expect(llmSpans.at(-1)?.metadata).toMatchObject({ + activated_tools: ["Calculator"], + active_tool_count: 4, + }); + }); + + it("traces session compaction as a task span", async () => { + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("session_before_compact", { + customInstructions: "Keep debugging context", + branchEntries: [{ id: "entry-1" }, { id: "entry-2" }], + reason: "overflow", + willRetry: true, + preparation: { + messages: [{ role: "user", content: "long context" }], + tokensBefore: 120_000, + }, + }); + await emit("session_compact", { + fromExtension: true, + reason: "overflow", + willRetry: true, + compactionEntry: { + id: "compact-1", + summary: "Short summary", + tokensBefore: 120_000, + estimatedTokensAfter: 24_000, + }, + }); + + const rootSpan = mockState.startSpans.find( + (span) => span.type === "task" && span.parentSpanId === undefined, + ); + const compactionSpan = mockState.startSpans.find((span) => span.name === "Compaction"); + + expect(rootSpan).toBeDefined(); + expect(compactionSpan).toMatchObject({ + type: "task", + parentSpanId: rootSpan?.spanId, + input: { + custom_instructions: "Keep debugging context", + branch_entry_count: 2, + preparation: { + messages: [{ role: "user", content: "long context" }], + }, + }, + metadata: { + event_type: "session_before_compact", + compaction_reason: "overflow", + will_retry: true, + tokens_before: 120_000, + }, + }); + const compactionLog = mockState.logSpans.find( + (entry) => + (entry.span as { spanId?: unknown } | undefined)?.spanId === compactionSpan?.spanId, + ); + expect(compactionLog?.event).toEqual({ + output: { + id: "compact-1", + summary: "Short summary", + tokensBefore: 120_000, + estimatedTokensAfter: 24_000, + }, + metadata: { + event_type: "session_compact", + from_extension: true, + compaction_reason: "overflow", + will_retry: true, + tokens_before: 120_000, + estimated_tokens_after: 24_000, + }, + }); + expect( + mockState.endSpans.some( + (entry) => + (entry.span as { spanId?: unknown } | undefined)?.spanId === compactionSpan?.spanId, + ), + ).toBe(true); + }); + + it("traces branch summary events as a task span", async () => { + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("session_before_tree", { + preparation: { + targetId: "target-entry", + oldLeafId: "old-leaf", + commonAncestorId: "ancestor", + entriesToSummarize: [{ id: "entry-1" }, { id: "entry-2" }], + userWantsSummary: true, + customInstructions: "Focus on the debugging branch", + replaceInstructions: false, + label: "debug-branch", + }, + }); + await emit("session_tree", { + newLeafId: "target-entry", + oldLeafId: "old-leaf", + fromExtension: false, + summaryEntry: { + id: "summary-1", + summary: "Debugging branch summary", + fromId: "old-leaf", + details: { modifiedFiles: ["src/index.ts"] }, + }, + }); + + const rootSpan = mockState.startSpans.find( + (span) => span.type === "task" && span.parentSpanId === undefined, + ); + const branchSummarySpan = mockState.startSpans.find((span) => span.name === "Branch Summary"); + + expect(branchSummarySpan).toMatchObject({ + type: "task", + parentSpanId: rootSpan?.spanId, + input: { + target_id: "target-entry", + old_leaf_id: "old-leaf", + common_ancestor_id: "ancestor", + entries_to_summarize: 2, + user_wants_summary: true, + custom_instructions: "Focus on the debugging branch", + replace_instructions: false, + label: "debug-branch", + }, + metadata: { + event_type: "session_before_tree", + user_wants_summary: true, + }, + }); + + const branchSummaryLog = mockState.logSpans.find( + (entry) => + (entry.span as { spanId?: unknown } | undefined)?.spanId === branchSummarySpan?.spanId, + ); + expect(branchSummaryLog?.event).toEqual({ + output: { + id: "summary-1", + summary: "Debugging branch summary", + fromId: "old-leaf", + details: { modifiedFiles: ["src/index.ts"] }, + }, + metadata: { + event_type: "session_tree", + from_extension: false, + new_leaf_id: "target-entry", + old_leaf_id: "old-leaf", + }, + }); + expect( + mockState.endSpans.some( + (entry) => + (entry.span as { spanId?: unknown } | undefined)?.spanId === branchSummarySpan?.spanId, + ), + ).toBe(true); + }); + + it("parents tool spans under the llm span that emitted the matching tool call", async () => { + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("before_agent_start", { + prompt: "Inspect the package", + images: [], + }); + await emit("message_end", { + message: { + role: "assistant", + provider: "anthropic", + model: "claude-sonnet-4", + timestamp: 1_700_000_000_000, + content: [ + { type: "text", text: "I'll inspect package.json." }, + { + type: "toolCall", + id: "tool-1", + name: "read", + arguments: { path: "package.json" }, + }, + ], + }, + }); + await emit("tool_execution_start", { + toolCallId: "tool-1", + toolName: "read", + args: { path: "package.json" }, + }); + await emit("tool_execution_end", { + toolCallId: "tool-1", + toolName: "read", + isError: false, + result: { + content: [{ type: "text", text: '{"name":"@braintrust/pi-extension"}' }], + }, + }); + + const llmSpan = mockState.startSpans.find((span) => span.type === "llm"); + const toolSpan = mockState.startSpans.find((span) => span.type === "tool"); + + expect(llmSpan).toBeDefined(); + expect(toolSpan).toBeDefined(); + expect(toolSpan?.parentSpanId).toBe(llmSpan?.spanId); + expect(toolSpan?.metadata).toMatchObject({ + tool_name: "read", + tool_call_id: "tool-1", + tool_approval: "approved", + parent_llm_span_id: llmSpan?.spanId, + }); + }); + + it("marks failed tool spans with approval metadata and an error", async () => { + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("before_agent_start", { + prompt: "Run a command", + images: [], + }); + await emit("tool_execution_start", { + toolCallId: "tool-error", + toolName: "bash", + args: { command: "exit 1" }, + }); + await emit("tool_execution_end", { + toolCallId: "tool-error", + toolName: "bash", + isError: true, + result: { + content: [{ type: "text", text: "Exit code 1" }], + }, + }); + + const toolSpan = mockState.startSpans.find((span) => span.type === "tool"); + const toolLog = mockState.logSpans.find( + (entry) => (entry.span as { spanId?: unknown } | undefined)?.spanId === toolSpan?.spanId, + ); + + expect(toolSpan?.metadata).toMatchObject({ + tool_name: "bash", + tool_call_id: "tool-error", + is_error: true, + tool_approval: "approved", + }); + expect((toolLog?.event as { error?: unknown } | undefined)?.error).toBe("Exit code 1"); + }); + + it("normalizes SKILL.md reads as skill tool spans", async () => { + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("before_agent_start", { + prompt: "Use the review skill", + images: [], + }); + await emit("message_end", { + message: { + role: "assistant", + provider: "anthropic", + model: "claude-sonnet-4", + timestamp: 1_700_000_000_000, + content: [ + { + type: "toolCall", + id: "tool-skill", + name: "read", + arguments: { path: "/home/user/.agents/skills/review/SKILL.md" }, + }, + ], + }, + }); + await emit("tool_execution_start", { + toolCallId: "tool-skill", + toolName: "read", + args: { path: "/home/user/.agents/skills/review/SKILL.md" }, + }); + await emit("tool_execution_end", { + toolCallId: "tool-skill", + toolName: "read", + isError: false, + result: { content: [{ type: "text", text: "---\nname: review\n---" }] }, + }); + + const skillSpan = mockState.startSpans.find((span) => span.name === "skill: review"); + expect(skillSpan).toMatchObject({ + type: "tool", + metadata: { + tool_name: "skill", + original_tool_name: "read", + tool_call_id: "tool-skill", + skill_name: "review", + skill_path: "/home/user/.agents/skills/review/SKILL.md", + }, + }); + }); + + it("marks matching explicit /skill loads on turn and skill spans", async () => { + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("input", { + text: "/skill:review inspect this", + images: [], + source: "interactive", + }); + await emit("before_agent_start", { + prompt: "Expanded skill content", + images: [], + }); + await emit("message_end", { + message: { + role: "assistant", + provider: "anthropic", + model: "claude-sonnet-4", + timestamp: 1_700_000_000_000, + content: [ + { + type: "toolCall", + id: "tool-skill", + name: "read", + arguments: { path: "/home/user/.agents/skills/review/SKILL.md" }, + }, + ], + }, + }); + await emit("tool_execution_start", { + toolCallId: "tool-skill", + toolName: "read", + args: { path: "/home/user/.agents/skills/review/SKILL.md" }, + }); + await emit("tool_execution_end", { + toolCallId: "tool-skill", + toolName: "read", + isError: false, + result: { content: [{ type: "text", text: "---\nname: review\n---" }] }, + }); + + const turnSpan = mockState.startSpans.find( + (span) => span.type === "task" && span.name === "Turn 1", + ); + expect(turnSpan?.metadata).toMatchObject({ + raw_input: "/skill:review inspect this", + loaded_skill_names: ["review"], + loaded_skills: [{ name: "review" }], + }); + + const skillSpan = mockState.startSpans.find((span) => span.name === "skill: review"); + expect(skillSpan?.metadata).toMatchObject({ + tool_name: "skill", + skill_name: "review", + skill_load_trigger: "explicit", + }); + }); + + it("does not mark natural-language skill reads as explicit", async () => { + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("input", { + text: "Use the review skill", + images: [], + source: "interactive", + }); + await emit("before_agent_start", { + prompt: "Use the review skill", + images: [], + }); + await emit("message_end", { + message: { + role: "assistant", + provider: "anthropic", + model: "claude-sonnet-4", + timestamp: 1_700_000_000_000, + content: [ + { + type: "toolCall", + id: "tool-skill", + name: "read", + arguments: { path: "/home/user/.agents/skills/review/SKILL.md" }, + }, + ], + }, + }); + await emit("tool_execution_start", { + toolCallId: "tool-skill", + toolName: "read", + args: { path: "/home/user/.agents/skills/review/SKILL.md" }, + }); + await emit("tool_execution_end", { + toolCallId: "tool-skill", + toolName: "read", + isError: false, + result: { content: [{ type: "text", text: "---\nname: review\n---" }] }, + }); + + const turnSpan = mockState.startSpans.find( + (span) => span.type === "task" && span.name === "Turn 1", + ); + const turnMetadata = turnSpan?.metadata as Record | undefined; + expect(turnMetadata?.loaded_skill_names).toBeUndefined(); + expect(turnMetadata?.loaded_skills).toBeUndefined(); + + const skillSpan = mockState.startSpans.find((span) => span.name === "skill: review"); + const skillMetadata = skillSpan?.metadata as Record | undefined; + expect(skillMetadata?.skill_load_trigger).toBeUndefined(); + }); + + it("preserves fork metadata when the root span is created lazily", async () => { + const { emit } = await createHarness(); + + await emit("session_start", { + reason: "fork", + previousSessionFile: "/tmp/parent-session.json", + }); + await emit("before_agent_start", { + prompt: "Continue from the fork", + images: [], + }); + + expect(mockState.startSpans[0]).toMatchObject({ + type: "task", + metadata: { + opened_via: "session_fork", + parent_session_file: "/tmp/parent-session.json", + }, + }); + }); + + it("adds the git repo slug to root span metadata when available", async () => { + const { default: braintrustPiExtension } = await import("./legacy-processor.ts"); + const handlers = new Map unknown>(); + + braintrustPiExtension({ + on(eventName: string, handler: (...args: unknown[]) => unknown) { + handlers.set(eventName, handler); + }, + } as never); + + const ctx = { + cwd: process.cwd(), + hasUI: true, + model: "anthropic/claude-sonnet-4", + ui: { + theme: { + fg: (_color: string, text: string) => text, + underline: (text: string) => text, + }, + setStatus: (key: string, text: string | undefined) => { + mockState.statuses.push({ key, text }); + }, + setWidget: (key: string, content: string[] | undefined, _options?: unknown) => { + mockState.widgets.push({ key, content }); + }, + }, + sessionManager: { + getSessionFile: () => "/tmp/session.json", + getSessionId: () => "session-1", + getSessionName: () => undefined, + }, + mode: "tui", + }; + + const emit = async (eventName: string, event: Record = {}): Promise => { + const handler = handlers.get(eventName); + if (!handler) throw new Error(`No handler registered for ${eventName}`); + await handler(event, ctx); + }; + + await emit("before_agent_start", { + prompt: "Inspect the package", + images: [], + }); + + expect(mockState.startSpans[0]).toMatchObject({ + type: "task", + metadata: { + repo: "braintrustdata/braintrust-coding-agent-plugins", + git_origin_url: expect.stringMatching( + /^(git@github\.com:|https:\/\/github\.com\/)braintrustdata\/braintrust-coding-agent-plugins(?:\.git)?$/, + ), + git_commit_sha: expect.stringMatching(/^[0-9a-f]{40}$/), + }, + }); + }); + + it("falls back to the turn span when no matching tool call was emitted by the llm", async () => { + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("before_agent_start", { + prompt: "Run the test suite", + images: [], + }); + await emit("message_end", { + message: { + role: "assistant", + provider: "anthropic", + model: "claude-sonnet-4", + timestamp: 1_700_000_000_001, + content: [{ type: "text", text: "Running the test suite." }], + }, + }); + await emit("tool_execution_start", { + toolCallId: "tool-missing", + toolName: "bash", + args: { command: "npm test" }, + }); + await emit("tool_execution_end", { + toolCallId: "tool-missing", + toolName: "bash", + isError: false, + result: { + content: [{ type: "text", text: "tests passed" }], + }, + }); + + const turnSpan = mockState.startSpans.find( + (span) => span.type === "task" && span.name === "Turn 1", + ); + const toolSpan = mockState.startSpans.find((span) => span.type === "tool"); + + expect(turnSpan).toBeDefined(); + expect(toolSpan).toBeDefined(); + expect(toolSpan?.parentSpanId).toBe(turnSpan?.spanId); + expect(toolSpan?.metadata).toMatchObject({ + tool_name: "bash", + tool_call_id: "tool-missing", + }); + expect((toolSpan?.metadata as Record | undefined)?.parent_llm_span_id).toBe( + undefined, + ); + }); + + it("keeps the turn open when agent_end will retry", async () => { + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("before_agent_start", { + prompt: "Retry after a transient failure", + images: [], + }); + await emit("agent_end", { willRetry: true, messages: [] }); + + expect(mockState.endSpans).toEqual([]); + + await emit("message_end", { + message: { + role: "assistant", + provider: "anthropic", + model: "claude-sonnet-4", + timestamp: 1_700_000_000_000, + content: [{ type: "text", text: "Recovered after retry." }], + }, + }); + await emit("agent_end", { + messages: [ + { + role: "assistant", + content: [{ type: "text", text: "Recovered after retry." }], + }, + ], + }); + + expect( + mockState.logSpans.some( + (entry) => + ((entry.event as Record).metadata as Record | undefined) + ?.finish_reason === "agent_end", + ), + ).toBe(true); + }); + + it("records the structured shutdown reason on the finalized root span", async () => { + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("before_agent_start", { + prompt: "Inspect the package", + images: [], + }); + await emit("session_shutdown", { reason: "quit" }); + + const rootFinalizeLog = mockState.logSpans + .map((entry) => entry.event as Record) + .find( + (event) => + (event.metadata as Record | undefined)?.last_close_reason === "quit", + ); + expect(rootFinalizeLog).toBeDefined(); + expect(mockState.endSpans.length).toBeGreaterThan(0); + expect(mockState.flushCalls).toBeGreaterThan(0); + }); + + it("does not finalize the root span on reload shutdowns", async () => { + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("before_agent_start", { + prompt: "Inspect the package", + images: [], + }); + + const startsBefore = mockState.startSpans.length; + const endsBefore = mockState.endSpans.length; + const flushesBefore = mockState.flushCalls; + + await emit("session_shutdown", { reason: "reload" }); + + // No additional span endings during reload, but pending writes are still flushed. + expect(mockState.startSpans.length).toBe(startsBefore); + expect(mockState.endSpans.length).toBe(endsBefore); + expect(mockState.flushCalls).toBeGreaterThan(flushesBefore); + const reloadClose = mockState.logSpans + .map((entry) => entry.event as Record) + .some( + (event) => + (event.metadata as Record | undefined)?.last_close_reason === "reload", + ); + expect(reloadClose).toBe(false); + }); + + it("hides all UI when showUi is false", async () => { + mockState.config.showUi = false; + + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("before_agent_start", { + prompt: "Inspect the package", + images: [], + }); + + const statusUpdates = mockState.statuses.filter( + (s) => s.key === "braintrust-tracing" && s.text !== undefined, + ); + const widgetUpdates = mockState.widgets.filter( + (w) => w.key === "braintrust-trace-link" && w.content !== undefined, + ); + + expect(statusUpdates).toEqual([]); + expect(widgetUpdates).toEqual([]); + }); + + it("hides just the trace link when showTraceLink is false", async () => { + mockState.config.showTraceLink = false; + + const { emit } = await createHarness(); + + await emit("session_start"); + await emit("before_agent_start", { + prompt: "Inspect the package", + images: [], + }); + + const statusUpdates = mockState.statuses.filter( + (s) => s.key === "braintrust-tracing" && s.text !== undefined, + ); + expect(statusUpdates.length).toBeGreaterThan(0); + expect(statusUpdates[0]?.text).toContain("Braintrust"); + + const widgetUpdates = mockState.widgets.filter( + (w) => w.key === "braintrust-trace-link" && w.content !== undefined, + ); + expect(widgetUpdates).toEqual([]); + }); +}); diff --git a/src/plugins/pi/content/src/index.ts b/src/plugins/pi/content/src/index.ts new file mode 100644 index 0000000..d93ec79 --- /dev/null +++ b/src/plugins/pi/content/src/index.ts @@ -0,0 +1,143 @@ +import { + VERSION as PI_VERSION, + type ExtensionAPI, + type ExtensionContext, +} from "@earendil-works/pi-coding-agent"; +import { loadConfig } from "./config.ts"; +import { DaemonClient } from "./runtime/daemon-client.ts"; +import { sessionKeyFor } from "./utils.ts"; +import { EXTENSION_VERSION } from "./version.ts"; + +const STATUS_KEY = "braintrust-tracing"; +const WIDGET_KEY = "braintrust-trace-link"; + +function nativePayload(value: unknown): unknown { + try { + return JSON.parse(JSON.stringify(value)); + } catch { + return { serialization_error: "Pi event payload was not JSON serializable" }; + } +} + +function sessionDescriptor(ctx: ExtensionContext): { + sessionId: string; + sessionFile?: string; + nativeSessionId?: string; +} { + const sessionFile = ctx.sessionManager.getSessionFile(); + const nativeSessionId = ctx.sessionManager.getSessionId(); + return { + sessionId: sessionKeyFor(sessionFile, nativeSessionId, ctx.cwd), + sessionFile, + nativeSessionId, + }; +} + +export default function braintrustPiExtension(pi: ExtensionAPI): void { + const config = loadConfig(process.cwd()); + if (!config.enabled) return; + + let sessionId: string | undefined; + let lastContext: ExtensionContext | undefined; + const client = new DaemonClient({ + source: "pi", + pluginVersion: EXTENSION_VERSION, + warn: (message) => { + if (lastContext?.hasUI && config.showUi) { + lastContext.ui.setStatus(STATUS_KEY, `Braintrust tracing unavailable: ${message}`); + } + }, + }); + + const remember = (ctx: ExtensionContext): ReturnType => { + lastContext = ctx; + const descriptor = sessionDescriptor(ctx); + sessionId = descriptor.sessionId; + return descriptor; + }; + + const refreshUi = async (ctx: ExtensionContext): Promise => { + if (!ctx.hasUI || !config.showUi || !sessionId) return; + const status = await client.status(sessionId); + const daemonSession = status?.sessions.find((session) => session.session_id === sessionId); + if (daemonSession?.last_error) { + ctx.ui.setStatus(STATUS_KEY, `Braintrust: ${daemonSession.last_error}`); + } else { + ctx.ui.setStatus(STATUS_KEY, "Braintrust: tracing"); + } + ctx.ui.setWidget( + WIDGET_KEY, + config.showTraceLink && daemonSession?.permalink + ? ["Braintrust trace", daemonSession.permalink] + : undefined, + { placement: "belowEditor" }, + ); + }; + + const forward = async ( + name: string, + event: unknown, + ctx?: ExtensionContext, + flush = false, + ): Promise => { + const descriptor = ctx ? remember(ctx) : undefined; + if (!sessionId) return; + await client.log({ + source: "pi", + source_version: PI_VERSION, + session_id: sessionId, + event: name, + ts_ms: Date.now(), + payload: { + event: nativePayload(event), + extension_version: EXTENSION_VERSION, + session_file: descriptor?.sessionFile, + native_session_id: descriptor?.nativeSessionId, + cwd: ctx?.cwd, + model: nativePayload(ctx?.model), + trace_settings: { + project: config.projectName, + additional_metadata: config.additionalMetadata, + parent_span_id: config.parentSpanId, + root_span_id: config.rootSpanId, + }, + }, + }); + if (flush) { + await client.flush(sessionId); + if (ctx) await refreshUi(ctx); + } + }; + + pi.on("session_start", async (event, ctx) => { + await forward("session_start", event, ctx); + await refreshUi(ctx); + }); + pi.on("input", async (event) => forward("input", event)); + pi.on("before_agent_start", async (event, ctx) => forward("before_agent_start", event, ctx)); + pi.on("context", async (event, ctx) => forward("context", event, ctx)); + pi.on("before_provider_request", async (event) => forward("before_provider_request", event)); + pi.on("after_provider_response", async (event) => forward("after_provider_response", event)); + pi.on("message_update", async (event) => forward("message_update", event)); + pi.on("thinking_level_select", async (event) => forward("thinking_level_select", event)); + pi.on("message_end", async (event) => forward("message_end", event)); + pi.on("tool_execution_start", async (event) => forward("tool_execution_start", event)); + pi.on("tool_execution_end", async (event, ctx) => forward("tool_execution_end", event, ctx)); + pi.on("session_before_compact", async (event, ctx) => + forward("session_before_compact", event, ctx), + ); + pi.on("session_compact", async (event, ctx) => forward("session_compact", event, ctx, true)); + pi.on("session_before_tree", async (event, ctx) => forward("session_before_tree", event, ctx)); + pi.on("session_tree", async (event, ctx) => forward("session_tree", event, ctx, true)); + pi.on("agent_end", async (event) => forward("agent_end", event, undefined, true)); + pi.on("session_shutdown", async (event, ctx) => { + await forward("session_shutdown", event, ctx, true); + if (ctx.hasUI) { + ctx.ui.setStatus(STATUS_KEY, undefined); + ctx.ui.setWidget(WIDGET_KEY, undefined); + } + client.close(); + sessionId = undefined; + lastContext = undefined; + }); +} diff --git a/src/plugins/pi/content/src/legacy-processor.ts b/src/plugins/pi/content/src/legacy-processor.ts new file mode 100644 index 0000000..0a39a95 --- /dev/null +++ b/src/plugins/pi/content/src/legacy-processor.ts @@ -0,0 +1,1628 @@ +import { hostname, userInfo } from "node:os"; +import { basename, dirname, resolve } from "node:path"; +import { + VERSION as PI_VERSION, + type AgentEndEvent, + type ExtensionAPI, + type ExtensionContext, +} from "@earendil-works/pi-coding-agent"; +import { BraintrustClient, type BraintrustSpanHandle } from "./client.ts"; +import { createLogger, loadConfig } from "./config.ts"; +import { createStateStore } from "./state.ts"; +import type { + AgentMessageLike, + AssistantMessageLike, + ConfigIssue, + ImageLike, + NormalizedAgentMessage, + NormalizedAssistantMessage, + TraceConfig, +} from "./types.ts"; +import { EXTENSION_VERSION } from "./version.ts"; +import { + buildTurnInput, + extractErrorText, + formatToolSpanName, + generateUuid, + gitMetadataForCwd, + isPlainObject, + normalizeAssistantMessage, + normalizeContextMessages, + normalizeToolResult, + repoSlugForCwd, + rootSpanName, + sessionKeyFor, + shortHash, + toUnixSeconds, + truncateValue, +} from "./utils.ts"; + +const TRACING_STATUS_KEY = "braintrust-tracing"; +const TRACING_WIDGET_KEY = "braintrust-trace-link"; + +interface SessionDescriptor { + sessionFile: string | undefined; + sessionId: string | undefined; + sessionKey: string; +} + +interface ProviderResponseMetadata { + status?: number; + headers?: Record; +} + +interface PendingLlmCall { + startedAt: number; + input: NormalizedAgentMessage[]; + activeToolNames?: string[]; + activatedToolNames: string[]; + modelMetadata: Record; + providerRequest?: Record; + providerResponse?: ProviderResponseMetadata; + firstTokenAt?: number; + firstThinkingAt?: number; + lastThinkingAt?: number; + firstTextAt?: number; +} + +interface TrackedToolStart { + startedAt: number; + args: unknown; + toolName: string; +} + +interface SkillLoadMetadata { + skill_name: string; + skill_path?: string; +} + +interface ExplicitSkillRequestMetadata { + loaded_skill_names: string[]; + loaded_skills: Array<{ name: string }>; +} + +interface ActiveCompaction { + spanId: string; + span?: BraintrustSpanHandle; + startedAt: number; + input: unknown; +} + +interface ActiveBranchSummary { + spanId: string; + span?: BraintrustSpanHandle; + startedAt: number; + input: unknown; +} + +interface PendingInputEvent { + text?: string; + source?: string; + streamingBehavior?: string; + imageCount?: number; +} + +interface ActiveTurn { + spanId: string; + span?: BraintrustSpanHandle; + prompt: string; + explicitSkillNames: string[]; + llmCalls: PendingLlmCall[]; + llmCallCount: number; + toolCallCount: number; + toolStarts: Map; + toolParentSpanIds: Map; + activatedToolNames: Set; + lastAssistantMessage?: AssistantMessageLike; + lastOutput?: NormalizedAssistantMessage; + error?: string; + thinkingLevel?: string; +} + +interface ActiveSession { + sessionKey: string; + sessionFile: string | undefined; + sessionId: string | undefined; + openedVia?: string; + parentSessionFile?: string; + rootSpanId?: string; + rootSpan?: BraintrustSpanHandle; + rootSpanRecordId?: string; + traceRootSpanId?: string; + parentSpanId?: string; + traceUrl?: string; + traceUrlPromise?: Promise; + startedAt?: number; + totalTurns: number; + totalToolCalls: number; + thinkingLevel?: string; + currentTurn?: ActiveTurn; + currentCompaction?: ActiveCompaction; + currentBranchSummary?: ActiveBranchSummary; +} + +let pendingInputEvent: PendingInputEvent | undefined; + +function stringArg(args: unknown, keys: readonly string[]): string | undefined { + if (!isPlainObject(args)) return undefined; + for (const key of keys) { + const value = args[key]; + if (typeof value === "string" && value.trim() !== "") return value; + } + return undefined; +} + +function skillLoadFromRead(toolName: string, args: unknown): SkillLoadMetadata | undefined { + if (toolName !== "read") return undefined; + const path = stringArg(args, ["path", "filePath", "file_path", "file"]); + if (!path || basename(path).toLowerCase() !== "skill.md") return undefined; + + const skillName = basename(dirname(path)); + return { + skill_name: skillName, + skill_path: path, + }; +} + +function normalizeExplicitSkillName(name: string): string | undefined { + const normalized = name + .trim() + .replace(/^[/$]+/, "") + .replace(/[),.;:]+$/, ""); + return normalized ? normalized : undefined; +} + +function explicitSkillRequestMetadata( + names: readonly string[], +): ExplicitSkillRequestMetadata | undefined { + const seen = new Set(); + const loaded_skill_names: string[] = []; + for (const name of names) { + const normalized = normalizeExplicitSkillName(name); + if (!normalized || seen.has(normalized)) continue; + seen.add(normalized); + loaded_skill_names.push(normalized); + } + if (loaded_skill_names.length === 0) return undefined; + return { + loaded_skill_names, + loaded_skills: loaded_skill_names.map((name) => ({ name })), + }; +} + +function explicitSkillNamesFromPiInput(input: PendingInputEvent | undefined): string[] { + if (!input?.text) return []; + const names: string[] = []; + for (const match of input.text.matchAll(/(?:^|\s)\/skill:([^\s]+)/g)) { + const name = normalizeExplicitSkillName(match[1] ?? ""); + if (name) names.push(name); + } + return explicitSkillRequestMetadata(names)?.loaded_skill_names ?? []; +} + +function skillLoadTriggerForTurn( + turn: ActiveTurn, + skillLoad: SkillLoadMetadata | undefined, +): "explicit" | undefined { + if (!skillLoad) return undefined; + return turn.explicitSkillNames.includes(skillLoad.skill_name) ? "explicit" : undefined; +} + +function hasSessionRoot(session: ActiveSession | undefined): session is ActiveSession & { + rootSpanId: string; + traceRootSpanId: string; + startedAt: number; +} { + return Boolean( + session && + typeof session.rootSpanId === "string" && + typeof session.traceRootSpanId === "string" && + typeof session.startedAt === "number", + ); +} + +function getUsername(): string { + try { + return userInfo().username; + } catch { + return process.env.USER || process.env.USERNAME || "unknown"; + } +} + +function getSessionDescriptor(ctx: ExtensionContext): SessionDescriptor { + const sessionFile = ctx.sessionManager.getSessionFile(); + const sessionId = ctx.sessionManager.getSessionId(); + const sessionKey = sessionKeyFor( + sessionFile ? resolve(sessionFile) : undefined, + sessionId, + ctx.cwd, + ); + + return { + sessionFile, + sessionId, + sessionKey, + }; +} + +function safeModelName(model: unknown): string | undefined { + if (!model) return undefined; + if (typeof model === "string") return model; + if (!isPlainObject(model)) return undefined; + const provider = model.provider ?? model.providerId ?? model.providerID; + const id = model.id ?? model.modelId ?? model.modelID; + if (typeof provider === "string" && typeof id === "string") return `${provider}/${id}`; + if (typeof id === "string") return id; + return undefined; +} + +function modelTraceMetadata(model: unknown): Record { + if (!isPlainObject(model)) return {}; + + const metadata: Record = {}; + if (typeof model.api === "string") metadata["pi_coding_agent.api"] = model.api; + if (typeof model.name === "string") metadata["pi_coding_agent.model_name"] = model.name; + if (typeof model.reasoning === "boolean") metadata.model_supports_reasoning = model.reasoning; + if (typeof model.contextWindow === "number") metadata.model_context_window = model.contextWindow; + if (typeof model.maxTokens === "number") metadata.model_max_tokens = model.maxTokens; + + if (isPlainObject(model.thinkingLevelMap)) { + metadata.supported_thinking_levels = Object.entries(model.thinkingLevelMap) + .filter(([, value]) => value !== null) + .map(([level]) => level); + } + + if (isPlainObject(model.compat) && typeof model.compat.deferredToolsMode === "string") { + metadata.deferred_tools_mode = model.compat.deferredToolsMode; + } + + return metadata; +} + +function activeToolNames(pi: ExtensionAPI): string[] | undefined { + try { + if (typeof pi.getActiveTools !== "function") return undefined; + return pi.getActiveTools(); + } catch { + return undefined; + } +} + +function stringProperty( + value: Record, + keys: readonly string[], +): string | undefined { + for (const key of keys) { + const item = value[key]; + if (typeof item === "string" && item.trim()) return item; + } + return undefined; +} + +function metadataForPendingInput(input: PendingInputEvent | undefined): Record { + if (!input) return {}; + return { + input_source: input.source, + input_streaming_behavior: input.streamingBehavior ?? "idle", + input_image_count: input.imageCount, + raw_input: input.text === undefined ? undefined : truncateValue(input.text), + }; +} + +function responseModelName(message: AssistantMessageLike): string | undefined { + return stringProperty(message as unknown as Record, [ + "responseModel", + "routedModel", + "resolvedModel", + "actualModel", + "concreteModel", + "outputModel", + ]); +} + +function providerResponseMetadata(event: unknown): ProviderResponseMetadata | undefined { + if (!isPlainObject(event)) return undefined; + const metadata: ProviderResponseMetadata = {}; + if (typeof event.status === "number") metadata.status = event.status; + + const headers = event.headers; + if (isPlainObject(headers)) { + const allowedHeaders: Record = {}; + const safeHeaders = new Set(["retry-after", "x-request-id", "request-id", "cf-ray"]); + for (const [key, value] of Object.entries(headers)) { + const normalizedKey = key.toLowerCase(); + if (!normalizedKey.startsWith("x-ratelimit-") && !safeHeaders.has(normalizedKey)) { + continue; + } + if (typeof value === "string") allowedHeaders[normalizedKey] = value; + else if (typeof value === "number" || typeof value === "boolean") { + allowedHeaders[normalizedKey] = String(value); + } + } + if (Object.keys(allowedHeaders).length > 0) metadata.headers = allowedHeaders; + } + + return metadata.status !== undefined || metadata.headers ? metadata : undefined; +} + +function providerRequestMetadata(event: unknown): Record | undefined { + if (!isPlainObject(event) || !isPlainObject(event.payload)) return undefined; + const payload = event.payload; + const metadata: Record = {}; + + if (typeof payload.model === "string") metadata.provider_request_model = payload.model; + const maxTokens = numberProperty(payload, ["max_tokens", "max_completion_tokens"]); + if (maxTokens !== undefined) metadata.provider_request_max_tokens = maxTokens; + if (typeof payload.temperature === "number") { + metadata.provider_request_temperature = payload.temperature; + } + + const thinking = isPlainObject(payload.thinking) ? payload.thinking : undefined; + const reasoning = isPlainObject(payload.reasoning) ? payload.reasoning : undefined; + const outputConfig = isPlainObject(payload.output_config) ? payload.output_config : undefined; + const chatTemplate = isPlainObject(payload.chat_template_kwargs) + ? payload.chat_template_kwargs + : undefined; + + const thinkingType = stringProperty(thinking ?? {}, ["type"]); + const thinkingEffort = + stringProperty(outputConfig ?? {}, ["effort"]) ?? + stringProperty(reasoning ?? {}, ["effort"]) ?? + stringProperty(payload, ["reasoning_effort"]); + const thinkingBudget = numberProperty(thinking, ["budget_tokens", "budgetTokens"]); + + if (thinkingType) metadata.effective_thinking_type = thinkingType; + if (thinkingEffort) metadata.effective_thinking_effort = thinkingEffort; + if (thinkingBudget !== undefined) { + metadata.effective_thinking_budget_tokens = thinkingBudget; + metadata.effective_thinking_uses_token_budget = true; + } else if (thinkingType === "adaptive") { + metadata.effective_thinking_uses_token_budget = false; + } + + const thinkingEnabled = + typeof payload.enable_thinking === "boolean" + ? payload.enable_thinking + : typeof reasoning?.enabled === "boolean" + ? reasoning.enabled + : typeof chatTemplate?.enable_thinking === "boolean" + ? chatTemplate.enable_thinking + : thinkingType === "adaptive" || thinkingType === "enabled" + ? true + : thinkingType === "disabled" + ? false + : undefined; + if (thinkingEnabled !== undefined) metadata.effective_thinking_enabled = thinkingEnabled; + + const immediateTools = Array.isArray(payload.tools) ? payload.tools : []; + const deferredAnthropicTools = immediateTools.filter( + (tool) => isPlainObject(tool) && tool.defer_loading === true, + ).length; + let deferredKimiTools = 0; + if (Array.isArray(payload.messages)) { + for (const message of payload.messages) { + if (isPlainObject(message) && Array.isArray(message.tools)) { + deferredKimiTools += message.tools.length; + } + } + } + const deferredToolCount = deferredAnthropicTools + deferredKimiTools; + const toolCount = immediateTools.length + deferredKimiTools; + if (toolCount > 0) metadata.provider_request_tool_count = toolCount; + if (deferredToolCount > 0) { + metadata.provider_request_deferred_tool_count = deferredToolCount; + } + + return Object.keys(metadata).length > 0 ? metadata : undefined; +} + +function addedToolNames(result: unknown): string[] { + if (!isPlainObject(result) || !Array.isArray(result.addedToolNames)) return []; + return [ + ...new Set(result.addedToolNames.filter((name): name is string => typeof name === "string")), + ]; +} + +function tokenMetric(value: unknown): number | undefined { + return typeof value === "number" && + Number.isFinite(value) && + value >= 0 && + Number.isInteger(value) + ? value + : undefined; +} + +function nonNegativeMetric(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined; +} + +function compactMetrics(metrics: Record): Record { + return Object.fromEntries( + Object.entries(metrics).filter((entry): entry is [string, number] => entry[1] !== undefined), + ); +} + +function usageMetrics(usage: AssistantMessageLike["usage"]): Record { + if (!usage) return {}; + + const inputTokens = tokenMetric(usage.input); + const completionTokens = tokenMetric(usage.output); + const cachedTokens = tokenMetric(usage.cacheRead); + const cacheCreationTokens = tokenMetric(usage.cacheWrite); + const cacheCreation1hTokens = tokenMetric(usage.cacheWrite1h); + const promptTokens = + inputTokens === undefined + ? undefined + : inputTokens + (cachedTokens ?? 0) + (cacheCreationTokens ?? 0); + const totalTokens = + promptTokens !== undefined && completionTokens !== undefined + ? promptTokens + completionTokens + : tokenMetric(usage.totalTokens); + + const metrics: Record = { + prompt_tokens: promptTokens, + completion_tokens: completionTokens, + tokens: totalTokens, + prompt_cached_tokens: cachedTokens, + completion_reasoning_tokens: tokenMetric(usage.reasoning), + estimated_cost: nonNegativeMetric(usage.cost?.total), + }; + + if (cacheCreation1hTokens === undefined) { + metrics.prompt_cache_creation_tokens = cacheCreationTokens; + } else { + metrics.prompt_cache_creation_1h_tokens = cacheCreation1hTokens; + if (cacheCreationTokens !== undefined) { + metrics.prompt_cache_creation_5m_tokens = Math.max( + 0, + cacheCreationTokens - cacheCreation1hTokens, + ); + } + } + + return compactMetrics(metrics); +} + +function pendingLlmCall(turn: ActiveTurn): PendingLlmCall | undefined { + return turn.llmCalls.at(-1); +} + +function recordStreamingTiming(turn: ActiveTurn, event: unknown, observedAt: number): void { + const pending = pendingLlmCall(turn); + if (!pending || !isPlainObject(event) || typeof event.type !== "string") return; + if (event.type === "start") return; + + pending.firstTokenAt ??= observedAt; + if (event.type.startsWith("thinking_")) { + pending.firstThinkingAt ??= observedAt; + pending.lastThinkingAt = observedAt; + } + if (event.type.startsWith("text_")) pending.firstTextAt ??= observedAt; +} + +function secondsBetween(startedAt: number, endedAt: number | undefined): number | undefined { + if (endedAt === undefined) return undefined; + return Math.max(0, endedAt - startedAt) / 1000; +} + +function streamingTimingMetadata( + pending: PendingLlmCall, + endedAt: number, +): Record { + const thinkingEndedAt = pending.firstTextAt ?? pending.lastThinkingAt ?? endedAt; + return { + "pi_coding_agent.time_to_first_thinking": secondsBetween( + pending.startedAt, + pending.firstThinkingAt, + ), + "pi_coding_agent.time_to_first_text": secondsBetween(pending.startedAt, pending.firstTextAt), + "pi_coding_agent.thinking_duration": pending.firstThinkingAt + ? secondsBetween(pending.firstThinkingAt, thinkingEndedAt) + : undefined, + }; +} + +function thinkingBlockMetadata(message: AssistantMessageLike): Record { + const thinkingBlocks = (message.content ?? []).filter( + (part) => isPlainObject(part) && part.type === "thinking", + ); + if (thinkingBlocks.length === 0) return {}; + + return { + thinking_block_count: thinkingBlocks.length, + empty_thinking_block_count: thinkingBlocks.filter( + (part) => + isPlainObject(part) && (typeof part.thinking !== "string" || part.thinking.length === 0), + ).length, + }; +} + +function getPreviousSessionFile(event: unknown): string | undefined { + if (!isPlainObject(event)) return undefined; + return typeof event.previousSessionFile === "string" ? event.previousSessionFile : undefined; +} + +function getEventReason(event: unknown): string | undefined { + if (!isPlainObject(event)) return undefined; + return typeof event.reason === "string" ? event.reason : undefined; +} + +function booleanProperty(value: unknown, key: string): boolean | undefined { + if (!isPlainObject(value)) return undefined; + return typeof value[key] === "boolean" ? value[key] : undefined; +} + +function numberProperty(value: unknown, keys: readonly string[]): number | undefined { + if (!isPlainObject(value)) return undefined; + for (const key of keys) { + const item = value[key]; + if (typeof item === "number" && Number.isFinite(item)) return item; + } + return undefined; +} + +function compactionMetadata(event: unknown, eventType: string): Record { + const metadata: Record = { event_type: eventType }; + const eventObject = isPlainObject(event) ? event : undefined; + const preparation = isPlainObject(eventObject?.preparation) ? eventObject.preparation : undefined; + const compactionEntry = isPlainObject(eventObject?.compactionEntry) + ? eventObject.compactionEntry + : undefined; + + const reason = getEventReason(event); + if (reason) metadata.compaction_reason = reason; + + const willRetry = booleanProperty(event, "willRetry"); + if (willRetry !== undefined) metadata.will_retry = willRetry; + + const tokensBefore = + numberProperty(compactionEntry, ["tokensBefore", "tokens_before"]) ?? + numberProperty(preparation, ["tokensBefore", "tokens_before"]); + if (tokensBefore !== undefined) metadata.tokens_before = tokensBefore; + + const estimatedTokensAfter = + numberProperty(compactionEntry, [ + "estimatedTokensAfter", + "estimated_tokens_after", + "tokensAfter", + "tokens_after", + "postCompactionTokens", + "post_compaction_tokens", + ]) ?? + numberProperty(preparation, [ + "estimatedTokensAfter", + "estimated_tokens_after", + "tokensAfter", + "tokens_after", + "postCompactionTokens", + "post_compaction_tokens", + ]); + if (estimatedTokensAfter !== undefined) { + metadata.estimated_tokens_after = estimatedTokensAfter; + } + + return metadata; +} + +function treePreparationInput(event: unknown): unknown { + const preparation = + isPlainObject(event) && isPlainObject(event.preparation) ? event.preparation : undefined; + if (!preparation) return undefined; + + return truncateValue({ + target_id: typeof preparation.targetId === "string" ? preparation.targetId : undefined, + old_leaf_id: typeof preparation.oldLeafId === "string" ? preparation.oldLeafId : undefined, + common_ancestor_id: + typeof preparation.commonAncestorId === "string" ? preparation.commonAncestorId : undefined, + entries_to_summarize: Array.isArray(preparation.entriesToSummarize) + ? preparation.entriesToSummarize.length + : undefined, + user_wants_summary: + typeof preparation.userWantsSummary === "boolean" ? preparation.userWantsSummary : undefined, + custom_instructions: + typeof preparation.customInstructions === "string" + ? preparation.customInstructions + : undefined, + replace_instructions: + typeof preparation.replaceInstructions === "boolean" + ? preparation.replaceInstructions + : undefined, + label: typeof preparation.label === "string" ? preparation.label : undefined, + }); +} + +function isAssistantMessage(message: unknown): message is AssistantMessageLike { + return isPlainObject(message) && message.role === "assistant"; +} + +function findLastAssistant( + messages: AgentEndEvent["messages"] = [], +): AssistantMessageLike | undefined { + for (let index = messages.length - 1; index >= 0; index -= 1) { + const message = messages[index]; + if (isAssistantMessage(message)) return message; + } + return undefined; +} + +function getSessionName(ctx: ExtensionContext): string | undefined { + try { + const name = ctx.sessionManager.getSessionName?.(); + return typeof name === "string" && name.trim() ? name : undefined; + } catch { + return undefined; + } +} + +function standardRootMetadata(ctx: ExtensionContext, config: TraceConfig): Record { + const descriptor = getSessionDescriptor(ctx); + const sessionName = getSessionName(ctx); + const gitMetadata = gitMetadataForCwd(ctx.cwd); + return { + ...config.additionalMetadata, + source: "pi", + extension_version: EXTENSION_VERSION, + pi_version: PI_VERSION, + pi_mode: ctx.mode, + session_name: sessionName, + session_id: descriptor.sessionId, + session_key: descriptor.sessionKey, + session_file: descriptor.sessionFile, + workspace: basename(ctx.cwd), + directory: ctx.cwd, + repo: repoSlugForCwd(ctx.cwd), + ...gitMetadata, + hostname: hostname(), + username: getUsername(), + os: process.platform, + }; +} + +function projectTraceUrl(config: TraceConfig, traceId: string | undefined): string | undefined { + if (!traceId || !config.orgName) return undefined; + return `${config.appUrl}/app/${encodeURIComponent(config.orgName)}/p/${encodeURIComponent(config.projectName)}/logs?oid=${encodeURIComponent(traceId)}`; +} + +function primaryConfigIssue(config: TraceConfig): ConfigIssue | undefined { + return config.configIssues.find((issue) => issue.severity === "error") ?? config.configIssues[0]; +} + +function configIssueStatusLabel(issue: ConfigIssue | undefined): string | undefined { + if (!issue) return undefined; + return issue.severity === "warning" ? "config warning" : "config error"; +} + +function setTracingStatus( + ctx: ExtensionContext, + config: TraceConfig, + options: { + active: boolean; + initError?: string; + missingApiKey?: boolean; + configIssue?: ConfigIssue; + }, +): void { + if (!ctx.hasUI || !config.showUi) { + if (ctx.hasUI) ctx.ui.setStatus(TRACING_STATUS_KEY, undefined); + return; + } + + const theme = ctx.ui.theme; + + if (options.initError) { + ctx.ui.setStatus( + TRACING_STATUS_KEY, + theme.fg("warning", "Braintrust") + theme.fg("dim", " setup failed"), + ); + return; + } + + if (options.active) { + ctx.ui.setStatus( + TRACING_STATUS_KEY, + theme.fg("accent", "Braintrust") + + theme.fg( + "dim", + ` tracing ${config.projectName}${options.configIssue ? " (config warning)" : ""}`, + ), + ); + return; + } + + if (options.missingApiKey) { + ctx.ui.setStatus( + TRACING_STATUS_KEY, + theme.fg("warning", "Braintrust") + theme.fg("dim", " missing API key"), + ); + return; + } + + const configIssueLabel = configIssueStatusLabel(options.configIssue); + if (configIssueLabel) { + ctx.ui.setStatus( + TRACING_STATUS_KEY, + theme.fg("warning", "Braintrust") + theme.fg("dim", ` ${configIssueLabel}`), + ); + return; + } + + ctx.ui.setStatus(TRACING_STATUS_KEY, undefined); +} + +function makeHyperlink(url: string, text: string): string { + return `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`; +} + +function truncateMiddle(value: string, maxLength: number): string { + if (value.length <= maxLength) return value; + const left = Math.max(1, Math.floor((maxLength - 1) / 2)); + const right = Math.max(1, maxLength - left - 1); + return `${value.slice(0, left)}…${value.slice(-right)}`; +} + +function shortenTraceUrl(traceUrl: string): string { + try { + const parsed = new URL(traceUrl); + const host = parsed.host.replace(/^www\./, ""); + const oid = parsed.searchParams.get("oid") ?? parsed.searchParams.get("id"); + const shortOid = oid ? truncateMiddle(oid, 16) : undefined; + const suffix = shortOid ? `?oid=${shortOid}` : ""; + return truncateMiddle(`${host}${parsed.pathname}${suffix}`, 72); + } catch { + return truncateMiddle(traceUrl, 72); + } +} + +function displayPath(path: string): string { + const home = process.env.HOME; + if (home && path.startsWith(home)) return `~${path.slice(home.length)}`; + return path; +} + +function setTraceWidget( + ctx: ExtensionContext, + config: TraceConfig, + traceUrl: string | undefined, + configIssue: ConfigIssue | undefined, +): void { + if (!ctx.hasUI || !config.showUi) { + if (ctx.hasUI) ctx.ui.setWidget(TRACING_WIDGET_KEY, undefined); + return; + } + + const theme = ctx.ui.theme; + const lines: string[] = []; + + if (traceUrl && config.showTraceLink) { + const label = makeHyperlink( + traceUrl, + theme.fg("accent", theme.underline("Braintrust trace ↗")), + ); + lines.push(label, theme.fg("dim", shortenTraceUrl(traceUrl))); + } + + if (configIssue) { + const issueLabel = + traceUrl || configIssue.severity === "warning" + ? "Braintrust config warning" + : "Braintrust config error"; + lines.push( + theme.fg("warning", issueLabel), + theme.fg( + "dim", + truncateMiddle(`${displayPath(configIssue.path)}: ${configIssue.message}`, 120), + ), + ); + } + + ctx.ui.setWidget(TRACING_WIDGET_KEY, lines.length > 0 ? lines : undefined, { + placement: "belowEditor", + }); +} + +export default function braintrustPiExtension(pi: ExtensionAPI): void { + const config = loadConfig(process.cwd()); + const logger = createLogger(config); + const store = createStateStore(config.stateDir, logger); + + let client: BraintrustClient | undefined; + let clientInitializationError: string | undefined; + + if (config.enabled && config.apiKey) { + client = new BraintrustClient(config, logger); + client.initialize().catch((error: unknown) => { + clientInitializationError = String(error); + logger.error("failed to initialize Braintrust client", { error: clientInitializationError }); + }); + } else if (config.enabled && !config.apiKey) { + logger.warn("TRACE_TO_BRAINTRUST is enabled but BRAINTRUST_API_KEY is missing"); + } + + function tracingEnabled(): boolean { + return Boolean(config.enabled && client && !clientInitializationError); + } + + function refreshTracingUi(ctx: ExtensionContext): void { + const configIssue = primaryConfigIssue(config); + setTracingStatus(ctx, config, { + active: tracingEnabled(), + initError: clientInitializationError, + missingApiKey: Boolean(config.enabled && !config.apiKey), + configIssue, + }); + setTraceWidget(ctx, config, activeSession?.traceUrl, configIssue); + } + + function persistTraceUrl(session: ActiveSession, traceUrl: string): void { + session.traceUrl = traceUrl; + store.patch(session.sessionKey, { + traceUrl, + lastSeenAt: Date.now(), + }); + } + + function refreshTraceUrl(ctx: ExtensionContext, session: ActiveSession): void { + if (!client || session.traceUrlPromise || !hasSessionRoot(session)) return; + + const quickUrl = + session.traceUrl ?? + client.getSpanLink(session.rootSpan) ?? + projectTraceUrl(config, session.rootSpanRecordId); + + if (quickUrl && quickUrl !== session.traceUrl) { + persistTraceUrl(session, quickUrl); + if (activeSession?.sessionKey === session.sessionKey) refreshTracingUi(ctx); + } + + if (!session.rootSpan || session.traceUrl) return; + + session.traceUrlPromise = client + .getSpanPermalink(session.rootSpan) + .then((traceUrl) => { + if (!traceUrl || traceUrl === session.traceUrl) return; + persistTraceUrl(session, traceUrl); + if (activeSession?.sessionKey === session.sessionKey) refreshTracingUi(ctx); + }) + .finally(() => { + session.traceUrlPromise = undefined; + }); + } + + let activeSession: ActiveSession | undefined; + + function materializeSessionRoot( + ctx: ExtensionContext, + session: ActiveSession, + ): ActiveSession | undefined { + if (!client) return undefined; + if (hasSessionRoot(session)) return session; + + const startedAt = Date.now(); + const rootSpanId = generateUuid(); + const traceRootSpanId = config.rootSpanId ?? rootSpanId; + const parentSpanId = config.parentSpanId; + const rootSpan = client.startSpan({ + spanId: rootSpanId, + rootSpanId: traceRootSpanId, + parentSpanId, + startedAt, + name: getSessionName(ctx) ?? rootSpanName(ctx.cwd), + type: "task", + metadata: { + ...standardRootMetadata(ctx, config), + opened_via: session.openedVia, + parent_session_file: session.parentSessionFile, + }, + }); + + session.rootSpanId = rootSpanId; + session.rootSpan = rootSpan; + session.rootSpanRecordId = rootSpan?.id; + session.traceRootSpanId = traceRootSpanId; + session.parentSpanId = parentSpanId; + session.traceUrl = client.getSpanLink(rootSpan) ?? projectTraceUrl(config, rootSpan?.id); + session.startedAt = startedAt; + + store.set(session.sessionKey, { + rootSpanId, + rootSpanRecordId: rootSpan?.id, + traceRootSpanId, + parentSpanId, + traceUrl: session.traceUrl, + startedAt, + totalTurns: session.totalTurns, + totalToolCalls: session.totalToolCalls, + lastSeenAt: startedAt, + sessionFile: session.sessionFile, + }); + store.schedulePersist(0); + + refreshTracingUi(ctx); + refreshTraceUrl(ctx, session); + return session; + } + + async function ensureSession( + ctx: ExtensionContext, + options: { + reason?: string; + parentSessionFile?: string | undefined; + createIfMissingRoot?: boolean; + } = {}, + ): Promise { + if (!tracingEnabled() || !client) return undefined; + + const descriptor = getSessionDescriptor(ctx); + if (activeSession?.sessionKey === descriptor.sessionKey) { + activeSession.sessionFile = descriptor.sessionFile; + activeSession.sessionId = descriptor.sessionId; + if (!hasSessionRoot(activeSession)) { + activeSession.openedVia ??= options.reason; + activeSession.parentSessionFile ??= options.parentSessionFile; + } + + if (options.createIfMissingRoot === false) { + refreshTracingUi(ctx); + return activeSession; + } + + return materializeSessionRoot(ctx, activeSession); + } + + const persisted = store.get(descriptor.sessionKey); + if (persisted) { + activeSession = { + sessionKey: descriptor.sessionKey, + sessionFile: descriptor.sessionFile, + sessionId: descriptor.sessionId, + openedVia: options.reason, + parentSessionFile: options.parentSessionFile, + rootSpanId: persisted.rootSpanId, + rootSpan: undefined, + rootSpanRecordId: persisted.rootSpanRecordId, + traceRootSpanId: persisted.traceRootSpanId ?? persisted.rootSpanId, + parentSpanId: persisted.parentSpanId, + traceUrl: persisted.traceUrl ?? projectTraceUrl(config, persisted.rootSpanRecordId), + startedAt: persisted.startedAt, + totalTurns: persisted.totalTurns ?? 0, + totalToolCalls: persisted.totalToolCalls ?? 0, + currentTurn: undefined, + }; + store.patch(descriptor.sessionKey, { + traceUrl: activeSession.traceUrl, + lastSeenAt: Date.now(), + sessionFile: descriptor.sessionFile, + }); + refreshTracingUi(ctx); + return activeSession; + } + + activeSession = { + sessionKey: descriptor.sessionKey, + sessionFile: descriptor.sessionFile, + sessionId: descriptor.sessionId, + openedVia: options.reason, + parentSessionFile: options.parentSessionFile, + totalTurns: 0, + totalToolCalls: 0, + currentTurn: undefined, + }; + + if (options.createIfMissingRoot === false) { + refreshTracingUi(ctx); + return activeSession; + } + + return materializeSessionRoot(ctx, activeSession); + } + + async function finishTurn( + reason: string, + endedAt = Date.now(), + finalAssistantMessage?: AssistantMessageLike, + ): Promise { + if (!activeSession?.currentTurn || !client) return; + + const turn = activeSession.currentTurn; + const finalAssistant = finalAssistantMessage ?? turn.lastAssistantMessage; + const finalOutput = finalAssistant + ? normalizeAssistantMessage(finalAssistant) + : turn.lastOutput; + const error = + turn.error || + (finalAssistant?.stopReason === "error" || finalAssistant?.stopReason === "aborted" + ? extractErrorText(finalAssistant, finalAssistant.errorMessage) + : undefined); + + client.logSpan(turn.span, { + output: finalOutput, + error, + metadata: { + llm_calls: turn.llmCallCount, + tool_calls: turn.toolCallCount, + finish_reason: reason, + }, + }); + client.endSpan(turn.span, endedAt); + + activeSession.currentTurn = undefined; + store.patch(activeSession.sessionKey, { + totalTurns: activeSession.totalTurns, + totalToolCalls: activeSession.totalToolCalls, + lastSeenAt: endedAt, + }); + await store.flush(); + } + + async function finalizeSession(reason: string, endedAt = Date.now()): Promise { + if (!activeSession || !client) return; + + await finishTurn(reason, endedAt); + if (!hasSessionRoot(activeSession)) return; + + const summaryMetadata = { + total_turns: activeSession.totalTurns, + total_tool_calls: activeSession.totalToolCalls, + last_close_reason: reason, + }; + + if (activeSession.rootSpan) { + client.logSpan(activeSession.rootSpan, { + metadata: summaryMetadata, + }); + client.endSpan(activeSession.rootSpan, endedAt); + } else if (activeSession.rootSpanRecordId) { + client.updateSpan({ + id: activeSession.rootSpanRecordId, + spanId: activeSession.rootSpanId, + rootSpanId: activeSession.traceRootSpanId, + metadata: summaryMetadata, + metrics: { + end: toUnixSeconds(endedAt), + }, + }); + } + + store.patch(activeSession.sessionKey, { + totalTurns: activeSession.totalTurns, + totalToolCalls: activeSession.totalToolCalls, + lastSeenAt: endedAt, + }); + await store.flush(); + } + + async function rolloverSession( + ctx: ExtensionContext, + reason: string, + previousSessionFile: string | undefined, + ): Promise { + if (!tracingEnabled()) return; + + const previousKey = activeSession?.sessionKey; + const nextKey = getSessionDescriptor(ctx).sessionKey; + if (previousKey && previousKey !== nextKey) { + await finalizeSession(reason); + activeSession = undefined; + } + + await ensureSession(ctx, { + reason, + parentSessionFile: previousSessionFile, + createIfMissingRoot: false, + }); + } + + pi.on("session_start", async (event, ctx) => { + refreshTracingUi(ctx); + + const reason = getEventReason(event); + if (reason === "new" || reason === "resume" || reason === "fork") { + await rolloverSession( + ctx, + reason === "fork" ? "session_fork" : "session_switch", + getPreviousSessionFile(event), + ); + return; + } + + await ensureSession(ctx, { + reason: "session_start", + createIfMissingRoot: false, + }); + }); + + pi.on("input", (event) => { + if (!isPlainObject(event)) return; + pendingInputEvent = { + text: typeof event.text === "string" ? event.text : undefined, + source: typeof event.source === "string" ? event.source : undefined, + streamingBehavior: + typeof event.streamingBehavior === "string" ? event.streamingBehavior : undefined, + imageCount: Array.isArray(event.images) ? event.images.length : undefined, + }; + }); + + pi.on("before_agent_start", async (event, ctx) => { + refreshTracingUi(ctx); + const session = await ensureSession(ctx, { reason: "agent_start" }); + if (!session || !client || !hasSessionRoot(session)) return; + + if (session.currentTurn) { + await finishTurn("replaced_by_new_prompt", Date.now()); + } + + const startedAt = Date.now(); + session.totalTurns += 1; + const turnSpanId = generateUuid(); + const turnInput = buildTurnInput( + event.prompt, + event.images as readonly ImageLike[] | undefined, + ); + const explicitSkillNames = explicitSkillNamesFromPiInput(pendingInputEvent); + const explicitSkillMetadata = explicitSkillRequestMetadata(explicitSkillNames); + const turnSpan = client.startSpan({ + spanId: turnSpanId, + rootSpanId: session.traceRootSpanId, + parentSpanId: session.rootSpanId, + startedAt, + input: turnInput, + metadata: { + turn_number: session.totalTurns, + active_model: safeModelName(ctx.model), + thinking_level: session.thinkingLevel, + ...metadataForPendingInput(pendingInputEvent), + ...explicitSkillMetadata, + }, + name: `Turn ${session.totalTurns}`, + type: "task", + }); + + pendingInputEvent = undefined; + + session.currentTurn = { + spanId: turnSpanId, + span: turnSpan, + prompt: turnInput, + explicitSkillNames, + llmCalls: [], + llmCallCount: 0, + toolCallCount: 0, + toolStarts: new Map(), + toolParentSpanIds: new Map(), + activatedToolNames: new Set(), + lastAssistantMessage: undefined, + lastOutput: undefined, + error: undefined, + thinkingLevel: session.thinkingLevel, + }; + + store.patch(session.sessionKey, { + totalTurns: session.totalTurns, + lastSeenAt: startedAt, + }); + }); + + pi.on("context", async (event, ctx) => { + if (!activeSession?.currentTurn) return; + const tools = activeToolNames(pi); + activeSession.currentTurn.llmCalls.push({ + startedAt: Date.now(), + input: normalizeContextMessages(event.messages as unknown as readonly AgentMessageLike[]), + activeToolNames: tools, + activatedToolNames: [...activeSession.currentTurn.activatedToolNames], + modelMetadata: modelTraceMetadata(ctx.model), + }); + }); + + pi.on("before_provider_request", async (event) => { + if (!activeSession?.currentTurn) return; + const metadata = providerRequestMetadata(event); + if (!metadata) return; + const pending = pendingLlmCall(activeSession.currentTurn); + if (pending) pending.providerRequest = metadata; + }); + + pi.on("after_provider_response", async (event) => { + if (!activeSession?.currentTurn) return; + const metadata = providerResponseMetadata(event); + if (!metadata) return; + const pending = [...activeSession.currentTurn.llmCalls] + .reverse() + .find((call) => !call.providerResponse); + if (pending) pending.providerResponse = metadata; + }); + + pi.on("message_update", async (event) => { + if (!activeSession?.currentTurn || !isPlainObject(event)) return; + recordStreamingTiming(activeSession.currentTurn, event.assistantMessageEvent, Date.now()); + }); + + pi.on("thinking_level_select", async (event) => { + if (!isPlainObject(event) || typeof event.level !== "string") return; + if (activeSession) activeSession.thinkingLevel = event.level; + if (activeSession?.currentTurn) activeSession.currentTurn.thinkingLevel = event.level; + }); + + pi.on("session_before_compact", async (event, ctx) => { + const session = await ensureSession(ctx, { reason: "compact" }); + if (!session || !client || !hasSessionRoot(session)) return; + + const startedAt = Date.now(); + const compactionSpanId = generateUuid(); + const input = truncateValue({ + custom_instructions: isPlainObject(event) ? event.customInstructions : undefined, + branch_entry_count: Array.isArray(event.branchEntries) + ? event.branchEntries.length + : undefined, + preparation: isPlainObject(event) ? event.preparation : undefined, + }); + const compactionSpan = client.startSpan({ + spanId: compactionSpanId, + rootSpanId: session.traceRootSpanId, + parentSpanId: session.rootSpanId, + startedAt, + input, + metadata: compactionMetadata(event, "session_before_compact"), + name: "Compaction", + type: "task", + }); + + session.currentCompaction = { + spanId: compactionSpanId, + span: compactionSpan, + startedAt, + input, + }; + }); + + pi.on("session_compact", async (event, ctx) => { + const session = await ensureSession(ctx, { reason: "compact" }); + if (!session || !client || !hasSessionRoot(session)) return; + + const compaction = session.currentCompaction ?? { + spanId: generateUuid(), + span: undefined, + startedAt: Date.now(), + input: undefined, + }; + if (!compaction.span) { + compaction.span = client.startSpan({ + spanId: compaction.spanId, + rootSpanId: session.traceRootSpanId, + parentSpanId: session.rootSpanId, + startedAt: compaction.startedAt, + input: compaction.input, + metadata: { + event_type: "session_compact", + }, + name: "Compaction", + type: "task", + }); + } + + client.logSpan(compaction.span, { + output: truncateValue(isPlainObject(event) ? event.compactionEntry : undefined), + metadata: { + ...compactionMetadata(event, "session_compact"), + from_extension: isPlainObject(event) ? event.fromExtension : undefined, + }, + }); + client.endSpan(compaction.span, Date.now()); + session.currentCompaction = undefined; + + store.patch(session.sessionKey, { + lastSeenAt: Date.now(), + }); + }); + + pi.on("session_before_tree", async (event, ctx) => { + const userWantsSummary = isPlainObject(event) + ? booleanProperty(event.preparation, "userWantsSummary") + : undefined; + if (userWantsSummary !== true) return; + + const session = await ensureSession(ctx, { reason: "tree" }); + if (!session || !client || !hasSessionRoot(session)) return; + + const startedAt = Date.now(); + const branchSummarySpanId = generateUuid(); + const input = treePreparationInput(event); + const branchSummarySpan = client.startSpan({ + spanId: branchSummarySpanId, + rootSpanId: session.traceRootSpanId, + parentSpanId: session.rootSpanId, + startedAt, + input, + metadata: { + event_type: "session_before_tree", + user_wants_summary: userWantsSummary, + }, + name: "Branch Summary", + type: "task", + }); + + session.currentBranchSummary = { + spanId: branchSummarySpanId, + span: branchSummarySpan, + startedAt, + input, + }; + }); + + pi.on("session_tree", async (event, ctx) => { + const hasSummaryEntry = isPlainObject(event) && event.summaryEntry !== undefined; + if (!activeSession?.currentBranchSummary && !hasSummaryEntry) return; + + const session = await ensureSession(ctx, { reason: "tree" }); + if (!session || !client || !hasSessionRoot(session)) return; + + const branchSummary = session.currentBranchSummary ?? { + spanId: generateUuid(), + span: undefined, + startedAt: Date.now(), + input: undefined, + }; + if (!branchSummary.span) { + branchSummary.span = client.startSpan({ + spanId: branchSummary.spanId, + rootSpanId: session.traceRootSpanId, + parentSpanId: session.rootSpanId, + startedAt: branchSummary.startedAt, + input: branchSummary.input, + metadata: { + event_type: "session_tree", + }, + name: "Branch Summary", + type: "task", + }); + } + + client.logSpan(branchSummary.span, { + output: truncateValue(isPlainObject(event) ? event.summaryEntry : undefined), + metadata: { + event_type: "session_tree", + from_extension: isPlainObject(event) ? event.fromExtension : undefined, + new_leaf_id: + isPlainObject(event) && typeof event.newLeafId === "string" ? event.newLeafId : undefined, + old_leaf_id: + isPlainObject(event) && typeof event.oldLeafId === "string" ? event.oldLeafId : undefined, + }, + }); + client.endSpan(branchSummary.span, Date.now()); + session.currentBranchSummary = undefined; + + store.patch(session.sessionKey, { + lastSeenAt: Date.now(), + }); + }); + + pi.on("message_end", async (event) => { + const session = activeSession; + if ( + !session?.currentTurn || + !isAssistantMessage(event.message) || + !client || + !hasSessionRoot(session) + ) { + return; + } + const message = event.message; + + const pending: PendingLlmCall = session.currentTurn.llmCalls.shift() ?? { + startedAt: Date.now(), + input: [{ role: "user", content: session.currentTurn.prompt }], + activatedToolNames: [...session.currentTurn.activatedToolNames], + modelMetadata: {}, + }; + + const requestedModelName = safeModelName(message) ?? message.model; + const responseModel = responseModelName(message); + const modelName = responseModel ?? requestedModelName; + const endedAt = Date.now(); + const normalizedOutput = normalizeAssistantMessage(message); + const error = + message.stopReason === "error" || message.stopReason === "aborted" + ? extractErrorText(message, message.errorMessage) + : undefined; + + session.currentTurn.llmCallCount += 1; + session.currentTurn.lastAssistantMessage = message; + session.currentTurn.lastOutput = normalizedOutput; + if (error) session.currentTurn.error = error; + + const llmSpanId = generateUuid(); + const llmSpan = client.startSpan({ + spanId: llmSpanId, + rootSpanId: session.traceRootSpanId, + parentSpanId: session.currentTurn.spanId, + startedAt: pending.startedAt, + input: pending.input, + metadata: { + ...pending.modelMetadata, + ...pending.providerRequest, + ...streamingTimingMetadata(pending, endedAt), + ...thinkingBlockMetadata(message), + api: message.api, + provider: message.provider, + model: modelName, + requested_model: requestedModelName, + response_model: responseModel, + "pi_coding_agent.response_id": message.responseId, + stop_reason: message.stopReason, + thinking_level: session.currentTurn.thinkingLevel ?? session.thinkingLevel, + "pi_coding_agent.active_tools": pending.activeToolNames, + active_tool_count: pending.activeToolNames?.length, + activated_tools: + pending.activatedToolNames.length > 0 ? pending.activatedToolNames : undefined, + provider_response_status: pending.providerResponse?.status, + provider_response_headers: pending.providerResponse?.headers, + cache_read_tokens: message.usage?.cacheRead, + cache_write_tokens: message.usage?.cacheWrite, + }, + name: modelName || "llm", + type: "llm", + }); + + for (const part of message.content ?? []) { + if (!isPlainObject(part) || part.type !== "toolCall" || typeof part.id !== "string") { + continue; + } + session.currentTurn.toolParentSpanIds.set(part.id, llmSpanId); + } + + client.logSpan(llmSpan, { + output: [normalizedOutput], + error, + metrics: compactMetrics({ + ...usageMetrics(message.usage), + time_to_first_token: secondsBetween(pending.startedAt, pending.firstTokenAt), + }), + }); + client.endSpan(llmSpan, endedAt); + }); + + pi.on("tool_execution_start", async (event) => { + if (!activeSession?.currentTurn) return; + activeSession.currentTurn.toolStarts.set(event.toolCallId, { + startedAt: Date.now(), + args: event.args, + toolName: event.toolName, + }); + }); + + pi.on("tool_execution_end", async (event, ctx) => { + const session = activeSession; + if (!session?.currentTurn || !client || !hasSessionRoot(session)) return; + + const tracked = session.currentTurn.toolStarts.get(event.toolCallId) ?? { + startedAt: Date.now(), + args: undefined, + toolName: event.toolName, + }; + session.currentTurn.toolStarts.delete(event.toolCallId); + const parentLlmSpanId = session.currentTurn.toolParentSpanIds.get(event.toolCallId); + session.currentTurn.toolParentSpanIds.delete(event.toolCallId); + + const endedAt = Date.now(); + session.totalToolCalls += 1; + session.currentTurn.toolCallCount += 1; + + const output = normalizeToolResult(event.result); + const error = event.isError + ? extractErrorText(event.result, `${event.toolName} failed`) + : undefined; + + if (error && !session.currentTurn.error) { + session.currentTurn.error = error; + } + + const activatedTools = addedToolNames(event.result); + for (const toolName of activatedTools) session.currentTurn.activatedToolNames.add(toolName); + const currentActiveTools = activatedTools.length > 0 ? activeToolNames(pi) : undefined; + const currentModelMetadata = activatedTools.length > 0 ? modelTraceMetadata(ctx.model) : {}; + + const skillLoad = skillLoadFromRead(event.toolName, tracked.args); + const skillLoadTrigger = skillLoadTriggerForTurn(session.currentTurn, skillLoad); + const metadataToolName = skillLoad ? "skill" : event.toolName; + const spanName = skillLoad + ? `skill: ${skillLoad.skill_name}` + : formatToolSpanName(event.toolName, tracked.args); + + const toolSpan = client.startSpan({ + spanId: generateUuid(), + rootSpanId: session.traceRootSpanId, + parentSpanId: parentLlmSpanId ?? session.currentTurn.spanId, + startedAt: tracked.startedAt, + input: tracked.args, + metadata: { + tool_name: metadataToolName, + original_tool_name: skillLoad ? event.toolName : undefined, + tool_call_id: event.toolCallId, + is_error: event.isError, + tool_approval: "approved", + parent_llm_span_id: parentLlmSpanId, + activated_tools: activatedTools.length > 0 ? activatedTools : undefined, + activated_tool_count: activatedTools.length > 0 ? activatedTools.length : undefined, + "pi_coding_agent.active_tools": currentActiveTools, + active_tool_count: currentActiveTools?.length, + deferred_tools_mode: currentModelMetadata.deferred_tools_mode, + ...skillLoad, + skill_load_trigger: skillLoadTrigger, + }, + name: spanName, + type: "tool", + }); + + client.logSpan(toolSpan, { + output, + error, + }); + client.endSpan(toolSpan, endedAt); + + store.patch(session.sessionKey, { + totalTurns: session.totalTurns, + totalToolCalls: session.totalToolCalls, + lastSeenAt: endedAt, + }); + }); + + pi.on("agent_end", async (event) => { + if (!activeSession?.currentTurn) return; + if (isPlainObject(event) && event.willRetry === true) return; + const finalAssistant = findLastAssistant(event.messages); + await finishTurn("agent_end", Date.now(), finalAssistant); + }); + + pi.on("session_shutdown", async (event, ctx) => { + if (ctx.hasUI) { + ctx.ui.setStatus(TRACING_STATUS_KEY, undefined); + ctx.ui.setWidget(TRACING_WIDGET_KEY, undefined); + } + + // pi 0.68.0+ exposes a structured reason ("quit" | "reload" | "new" | "resume" + // | "fork"). Older pi hosts pass no payload, so we fall back to the generic + // label to stay backwards-compatible and keep the existing metadata shape. + const reason = getEventReason(event) ?? "session_shutdown"; + logger.debug("session_shutdown", { reason }); + + if (client && !clientInitializationError) { + // On reload the same pi session is about to resume in a freshly imported + // extension instance, which restores its state from the persisted store and + // keeps writing to the existing root span. Finalizing here would close that + // root span out from under the reloaded instance, so we just flush pending + // writes and let the new instance continue the trace. + if (reason !== "reload") { + await finalizeSession(reason); + } + await client.flush(); + } + activeSession = undefined; + pendingInputEvent = undefined; + await store.flush(); + await logger.flush(); + }); + + for (const configIssue of config.configIssues) { + if (configIssue.severity === "error") { + logger.error("Braintrust config issue", configIssue); + } else { + logger.warn("Braintrust config issue", configIssue); + } + } + + logger.debug("Braintrust pi tracing extension loaded", { + enabled: config.enabled, + project: config.projectName, + hasApiKey: Boolean(config.apiKey), + logFile: logger.filePath, + configIssues: config.configIssues, + configHash: shortHash( + JSON.stringify({ + enabled: config.enabled, + project: config.projectName, + debug: config.debug, + stateDir: config.stateDir, + }), + ), + }); +} diff --git a/src/plugins/pi/content/src/state.test.ts b/src/plugins/pi/content/src/state.test.ts new file mode 100644 index 0000000..ce8cac9 --- /dev/null +++ b/src/plugins/pi/content/src/state.test.ts @@ -0,0 +1,131 @@ +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { createStateStore } from "./state.ts"; + +const tempDirs: string[] = []; + +afterEach(() => { + while (tempDirs.length > 0) { + rmSync(tempDirs.pop()!, { recursive: true, force: true }); + } +}); + +function makeTempDir(prefix: string): string { + const dir = mkdtempSync(join(tmpdir(), prefix)); + tempDirs.push(dir); + return dir; +} + +describe("createStateStore", () => { + it("loads valid sessions and prunes expired ones on startup", async () => { + const stateDir = makeTempDir("pi-extension-state-"); + const now = Date.now(); + const oldTimestamp = now - 31 * 24 * 60 * 60 * 1000; + + writeFileSync( + join(stateDir, "sessions.json"), + `${JSON.stringify( + { + version: 1, + sessions: { + fresh: { + rootSpanId: "fresh-root", + startedAt: now, + totalTurns: 2, + }, + expired: { + rootSpanId: "old-root", + startedAt: oldTimestamp, + lastSeenAt: oldTimestamp, + }, + invalid: { + startedAt: now, + }, + }, + }, + null, + 2, + )}\n`, + "utf8", + ); + + const store = createStateStore(stateDir); + await store.flush(); + + expect(store.get("fresh")).toMatchObject({ rootSpanId: "fresh-root", totalTurns: 2 }); + expect(store.get("expired")).toBeUndefined(); + expect(store.get("invalid")).toBeUndefined(); + + const persisted = JSON.parse(readFileSync(join(stateDir, "sessions.json"), "utf8")) as { + sessions: Record; + }; + expect(Object.keys(persisted.sessions)).toEqual(["fresh"]); + }); + + it("only writes to disk when persistence is scheduled or flushed", async () => { + const stateDir = makeTempDir("pi-extension-state-"); + const store = createStateStore(stateDir); + + store.set("session-1", { + rootSpanId: "root-1", + startedAt: 1, + totalTurns: 1, + }); + + expect(existsSync(join(stateDir, "sessions.json"))).toBe(false); + + store.schedulePersist(0); + await store.flush(); + + const persisted = JSON.parse(readFileSync(join(stateDir, "sessions.json"), "utf8")) as { + sessions: Record; + }; + expect(persisted.sessions["session-1"]).toEqual({ + rootSpanId: "root-1", + startedAt: 1, + totalTurns: 1, + }); + }); + + it("persists set, patch, and delete operations", async () => { + const stateDir = makeTempDir("pi-extension-state-"); + const store = createStateStore(stateDir); + + store.set("session-1", { + rootSpanId: "root-1", + startedAt: 1, + totalTurns: 1, + }); + store.patch("session-1", { + totalTurns: 3, + totalToolCalls: 5, + }); + await store.flush(); + + expect(store.get("session-1")).toEqual({ + rootSpanId: "root-1", + startedAt: 1, + totalTurns: 3, + totalToolCalls: 5, + }); + + let persisted = JSON.parse(readFileSync(join(stateDir, "sessions.json"), "utf8")) as { + sessions: Record; + }; + expect(persisted.sessions["session-1"]).toEqual({ + rootSpanId: "root-1", + startedAt: 1, + totalTurns: 3, + totalToolCalls: 5, + }); + + store.delete("session-1"); + await store.flush(); + persisted = JSON.parse(readFileSync(join(stateDir, "sessions.json"), "utf8")) as { + sessions: Record; + }; + expect(persisted.sessions).toEqual({}); + }); +}); diff --git a/src/plugins/pi/content/src/state.ts b/src/plugins/pi/content/src/state.ts new file mode 100644 index 0000000..5111fbf --- /dev/null +++ b/src/plugins/pi/content/src/state.ts @@ -0,0 +1,197 @@ +import { existsSync, readFileSync } from "node:fs"; +import { rename, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { Logger, PersistedSessionState, StateStore } from "./types.ts"; +import { ensureDir, isPlainObject } from "./utils.ts"; + +const STATE_VERSION = 1; +const RETENTION_MS = 30 * 24 * 60 * 60 * 1000; +const DEFAULT_PERSIST_DEBOUNCE_MS = 50; + +interface StoreData { + version: number; + sessions: Record; +} + +interface StoreBackend { + stateFile: string; + state: StoreData; + dirty: boolean; + scheduledPersistTimer?: ReturnType; + pendingPersist: Promise; + lastPersistedSnapshot?: string; + logger?: Logger; +} + +const storeBackends = new Map(); + +function normalizeSessions(value: unknown): Record { + if (!isPlainObject(value)) return {}; + + const sessions: Record = {}; + for (const [key, session] of Object.entries(value)) { + if ( + isPlainObject(session) && + typeof session.rootSpanId === "string" && + typeof session.startedAt === "number" + ) { + sessions[key] = { + rootSpanId: session.rootSpanId, + rootSpanRecordId: + typeof session.rootSpanRecordId === "string" ? session.rootSpanRecordId : undefined, + traceRootSpanId: + typeof session.traceRootSpanId === "string" ? session.traceRootSpanId : undefined, + parentSpanId: typeof session.parentSpanId === "string" ? session.parentSpanId : undefined, + traceUrl: typeof session.traceUrl === "string" ? session.traceUrl : undefined, + startedAt: session.startedAt, + totalTurns: typeof session.totalTurns === "number" ? session.totalTurns : undefined, + totalToolCalls: + typeof session.totalToolCalls === "number" ? session.totalToolCalls : undefined, + lastSeenAt: typeof session.lastSeenAt === "number" ? session.lastSeenAt : undefined, + sessionFile: typeof session.sessionFile === "string" ? session.sessionFile : undefined, + }; + } + } + + return sessions; +} + +function serializedState(backend: StoreBackend): string { + return `${JSON.stringify(backend.state, null, 2)}\n`; +} + +function markDirty(backend: StoreBackend): void { + backend.dirty = true; +} + +function pruneExpired(backend: StoreBackend): boolean { + const cutoff = Date.now() - RETENTION_MS; + let changed = false; + + for (const [key, value] of Object.entries(backend.state.sessions)) { + if ((value.lastSeenAt ?? value.startedAt ?? 0) < cutoff) { + delete backend.state.sessions[key]; + changed = true; + } + } + + if (changed) markDirty(backend); + return changed; +} + +async function persistDirty(backend: StoreBackend): Promise { + if (!backend.dirty) { + await backend.pendingPersist.catch(() => {}); + return; + } + + const snapshot = serializedState(backend); + backend.dirty = false; + + backend.pendingPersist = backend.pendingPersist + .catch(() => {}) + .then(async () => { + if (snapshot === backend.lastPersistedSnapshot) return; + + const tempFile = `${backend.stateFile}.tmp`; + try { + await writeFile(tempFile, snapshot, "utf8"); + await rename(tempFile, backend.stateFile); + backend.lastPersistedSnapshot = snapshot; + } catch (error) { + markDirty(backend); + backend.logger?.warn("failed to persist state store", { error: String(error) }); + } + }); + + await backend.pendingPersist.catch(() => {}); + if (backend.dirty) await persistDirty(backend); +} + +function schedulePersist(backend: StoreBackend, delayMs = DEFAULT_PERSIST_DEBOUNCE_MS): void { + if (backend.scheduledPersistTimer) clearTimeout(backend.scheduledPersistTimer); + backend.scheduledPersistTimer = setTimeout(() => { + backend.scheduledPersistTimer = undefined; + void persistDirty(backend); + }, delayMs); + backend.scheduledPersistTimer.unref?.(); +} + +function createBackend(stateFile: string, logger?: Logger): StoreBackend { + let state: StoreData = { + version: STATE_VERSION, + sessions: {}, + }; + + if (existsSync(stateFile)) { + try { + const parsed = JSON.parse(readFileSync(stateFile, "utf8")) as unknown; + if (isPlainObject(parsed)) { + state = { + version: STATE_VERSION, + sessions: normalizeSessions(parsed.sessions), + }; + } + } catch (error) { + logger?.warn("failed to load state store", { error: String(error) }); + } + } + + const backend: StoreBackend = { + stateFile, + state, + dirty: false, + pendingPersist: Promise.resolve(), + logger, + }; + + backend.lastPersistedSnapshot = serializedState(backend); + if (pruneExpired(backend)) schedulePersist(backend, 0); + return backend; +} + +export function createStateStore(stateDir: string, logger?: Logger): StateStore { + ensureDir(stateDir); + const stateFile = join(stateDir, "sessions.json"); + + let backend = storeBackends.get(stateFile); + if (!backend) { + backend = createBackend(stateFile, logger); + storeBackends.set(stateFile, backend); + } else if (!backend.logger) { + backend.logger = logger; + } + + return { + get(sessionKey) { + return backend.state.sessions[sessionKey]; + }, + set(sessionKey, value) { + backend.state.sessions[sessionKey] = value; + markDirty(backend); + return backend.state.sessions[sessionKey]; + }, + patch(sessionKey, patch) { + backend.state.sessions[sessionKey] = { + ...backend.state.sessions[sessionKey], + ...patch, + } as PersistedSessionState; + markDirty(backend); + return backend.state.sessions[sessionKey]; + }, + delete(sessionKey) { + delete backend.state.sessions[sessionKey]; + markDirty(backend); + }, + schedulePersist(delayMs) { + schedulePersist(backend, delayMs); + }, + async flush() { + if (backend.scheduledPersistTimer) { + clearTimeout(backend.scheduledPersistTimer); + backend.scheduledPersistTimer = undefined; + } + await persistDirty(backend); + }, + }; +} diff --git a/src/plugins/pi/content/src/types.ts b/src/plugins/pi/content/src/types.ts new file mode 100644 index 0000000..bfd1320 --- /dev/null +++ b/src/plugins/pi/content/src/types.ts @@ -0,0 +1,193 @@ +export type JsonPrimitive = string | number | boolean | null; +export type JsonValue = JsonPrimitive | JsonObject | JsonValue[]; + +export interface JsonObject { + [key: string]: JsonValue | undefined; +} + +export type LogLevel = "debug" | "info" | "warn" | "error"; + +export interface ConfigIssue { + path: string; + message: string; + severity: "error" | "warning"; +} + +export interface TraceConfig { + enabled: boolean; + apiKey: string; + apiUrl?: string; + appUrl: string; + orgName?: string; + projectName: string; + debug: boolean; + logFile?: string; + stateDir: string; + additionalMetadata: JsonObject; + parentSpanId?: string; + rootSpanId?: string; + showUi: boolean; + showTraceLink: boolean; + configIssues: ConfigIssue[]; +} + +export interface Logger { + filePath: string; + debug(message: string, data?: unknown): void; + info(message: string, data?: unknown): void; + warn(message: string, data?: unknown): void; + error(message: string, data?: unknown): void; + flush(): Promise; +} + +export interface PersistedSessionState { + rootSpanId: string; + rootSpanRecordId?: string; + traceRootSpanId?: string; + parentSpanId?: string; + traceUrl?: string; + startedAt: number; + totalTurns?: number; + totalToolCalls?: number; + lastSeenAt?: number; + sessionFile?: string; +} + +export interface StateStore { + get(sessionKey: string): PersistedSessionState | undefined; + set(sessionKey: string, value: PersistedSessionState): PersistedSessionState; + patch(sessionKey: string, patch: Partial): PersistedSessionState; + delete(sessionKey: string): void; + schedulePersist(delayMs?: number): void; + flush(): Promise; +} + +export interface TextContentLike { + type: "text"; + text?: string; +} + +export interface ThinkingContentLike { + type: "thinking"; + thinking?: string; + thinkingSignature?: string; + redacted?: boolean; +} + +export interface ToolCallContentLike { + type: "toolCall"; + id: string; + name?: string; + arguments?: Record; +} + +export interface ImageContentLike { + type: "image"; + mimeType?: string; +} + +export type ContentPartLike = + | TextContentLike + | ThinkingContentLike + | ToolCallContentLike + | ImageContentLike + | { + type?: string; + [key: string]: unknown; + }; + +export interface ImageLike { + mimeType?: string; + source?: { + mediaType?: string; + }; +} + +export interface UsageLike { + input?: number; + output?: number; + cacheRead?: number; + cacheWrite?: number; + cacheWrite1h?: number; + reasoning?: number; + totalTokens?: number; + cost?: { + input?: number; + output?: number; + cacheRead?: number; + cacheWrite?: number; + total?: number; + }; +} + +export interface UserMessageLike { + role: "user"; + content?: unknown; +} + +export interface AssistantMessageLike { + role: "assistant"; + content?: ContentPartLike[]; + api?: string; + provider?: string; + model?: string; + responseModel?: string; + responseId?: string; + usage?: UsageLike; + stopReason?: string; + errorMessage?: string; + timestamp?: number; +} + +export interface ToolResultMessageLike { + role: "toolResult"; + toolCallId?: string; + toolName?: string; + content?: unknown; + details?: unknown; + isError?: boolean; +} + +export type AgentMessageLike = + | UserMessageLike + | AssistantMessageLike + | ToolResultMessageLike + | { + role?: string; + content?: unknown; + }; + +export interface NormalizedUserMessage { + role: "user"; + content: string; +} + +export interface NormalizedAssistantMessage { + role: "assistant"; + content: string; + tool_calls?: Array<{ + id?: string; + type: "function"; + function: { + name?: string; + arguments: string; + }; + }>; + reasoning?: Array<{ + id: string; + content: string; + }>; +} + +export interface NormalizedToolMessage { + role: "tool"; + content: string; + tool_call_id?: string; + name?: string; + is_error: boolean; +} + +export type NormalizedAgentMessage = + | NormalizedUserMessage + | NormalizedAssistantMessage + | NormalizedToolMessage; diff --git a/src/plugins/pi/content/src/utils.test.ts b/src/plugins/pi/content/src/utils.test.ts new file mode 100644 index 0000000..b4b26f3 --- /dev/null +++ b/src/plugins/pi/content/src/utils.test.ts @@ -0,0 +1,240 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + buildTurnInput, + extractErrorText, + formatToolSpanName, + gitMetadataForCwd, + normalizeAssistantMessage, + normalizeContextMessages, + normalizeToolResult, + repoSlugForCwd, + redactGitRemoteUrl, + rootSpanName, + sessionKeyFor, +} from "./utils.ts"; + +const tempDirs: string[] = []; + +afterEach(() => { + vi.restoreAllMocks(); + while (tempDirs.length > 0) { + rmSync(tempDirs.pop()!, { recursive: true, force: true }); + } +}); + +function makeTempDir(prefix: string): string { + const dir = mkdtempSync(join(tmpdir(), prefix)); + tempDirs.push(dir); + return dir; +} + +function makeGitRepo(): { dir: string; commit: string } { + const dir = makeTempDir("pi-extension-git-metadata-"); + execFileSync("git", ["init"], { cwd: dir, stdio: "ignore" }); + execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: dir }); + execFileSync("git", ["config", "user.name", "Test User"], { cwd: dir }); + writeFileSync(join(dir, "README.md"), "hello\n"); + execFileSync("git", ["add", "README.md"], { cwd: dir }); + execFileSync("git", ["commit", "-m", "init"], { cwd: dir, stdio: "ignore" }); + execFileSync("git", ["branch", "-M", "main"], { cwd: dir }); + execFileSync("git", ["remote", "add", "origin", "https://token@github.com/acme/app.git"], { + cwd: dir, + }); + const commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: dir, encoding: "utf8" }).trim(); + return { dir, commit }; +} + +describe("utils", () => { + it("normalizes assistant messages with text, reasoning, and tool calls", () => { + const normalized = normalizeAssistantMessage({ + role: "assistant", + content: [ + { type: "text", text: "First line" }, + { type: "thinking", thinking: "Plan the next step" }, + { + type: "toolCall", + id: "call-1", + name: "read", + arguments: { path: "package.json" }, + }, + ], + }); + + expect(normalized).toEqual({ + role: "assistant", + content: "First line", + reasoning: [{ id: "thinking", content: "Plan the next step" }], + tool_calls: [ + { + id: "call-1", + type: "function", + function: { + name: "read", + arguments: '{"path":"package.json"}', + }, + }, + ], + }); + }); + + it("omits empty signed thinking content without exposing its signature", () => { + const normalized = normalizeAssistantMessage({ + role: "assistant", + content: [ + { type: "thinking", thinking: "", thinkingSignature: "opaque-signature" }, + { type: "text", text: "Visible answer" }, + ], + }); + + expect(normalized).toEqual({ + role: "assistant", + content: "Visible answer", + }); + expect(JSON.stringify(normalized)).not.toContain("opaque-signature"); + }); + + it("normalizes mixed context messages into Braintrust-friendly shapes", () => { + const messages = normalizeContextMessages([ + { role: "user", content: [{ type: "text", text: "Use the docs" }] }, + { + role: "assistant", + content: [ + { type: "text", text: "I will inspect the config" }, + { type: "thinking", redacted: true }, + ], + }, + { + role: "toolResult", + toolCallId: "tool-1", + toolName: "read", + isError: false, + content: [{ type: "text", text: "README contents" }], + }, + { role: "system", content: "ignored" }, + ]); + + expect(messages).toEqual([ + { role: "user", content: "Use the docs" }, + { + role: "assistant", + content: "I will inspect the config", + reasoning: [{ id: "thinking", content: "[thinking redacted]" }], + }, + { + role: "tool", + content: "README contents", + tool_call_id: "tool-1", + name: "read", + is_error: false, + }, + ]); + }); + + it("normalizes structured tool results and extracts readable error text", () => { + const result = normalizeToolResult({ + content: [ + { type: "text", text: "command failed" }, + { type: "thinking", redacted: true }, + ], + details: { exitCode: 1, stderr: "boom" }, + isError: true, + }); + + expect(result).toEqual({ + content: "command failed\n[thinking redacted]", + details: { exitCode: 1, stderr: "boom" }, + isError: true, + }); + + expect( + extractErrorText( + { + content: [{ type: "text", text: "tool exploded" }], + }, + "fallback message", + ), + ).toBe("tool exploded"); + }); + + it("formats tool span names and builds turn inputs", () => { + expect(formatToolSpanName("read", { path: "/tmp/project/package.json" })).toBe( + "read: package.json", + ); + expect(formatToolSpanName("bash", { command: "npm test -- --runInBand" })).toBe( + "bash: npm test -- --runInBand", + ); + + expect( + buildTurnInput("Summarize these screenshots", [ + { source: { mediaType: "image/png" } }, + { mimeType: "image/jpeg" }, + ]), + ).toBe("Summarize these screenshots\n[image/png]\n[image/jpeg]"); + }); + + it("scopes ephemeral session keys by project cwd", () => { + const projectA = makeTempDir("pi-extension-project-a-"); + const projectB = makeTempDir("pi-extension-project-b-"); + + expect(sessionKeyFor(undefined, "automation-session", projectA)).toBe( + sessionKeyFor(undefined, "automation-session", projectA), + ); + expect(sessionKeyFor(undefined, "automation-session", projectA)).not.toBe( + sessionKeyFor(undefined, "automation-session", projectB), + ); + expect(sessionKeyFor("/tmp/session.json", "automation-session", projectA)).toBe( + "file:/tmp/session.json", + ); + }); + + it("prefers owner/repo from git origin for the root span name", () => { + const repoDir = makeTempDir("pi-extension-git-"); + execFileSync("git", ["init"], { cwd: repoDir, stdio: "ignore" }); + execFileSync( + "git", + ["remote", "add", "origin", "git@github.com:braintrustdata/braintrust-pi-extension.git"], + { cwd: repoDir, stdio: "ignore" }, + ); + + expect(repoSlugForCwd(repoDir)).toBe("braintrustdata/braintrust-pi-extension"); + expect(rootSpanName(repoDir)).toBe("pi: braintrustdata/braintrust-pi-extension"); + }); + + it("redacts URL-style git remote credentials", () => { + expect(redactGitRemoteUrl("https://token:secret@github.com/acme/app.git")).toBe( + "https://github.com/acme/app.git", + ); + expect(redactGitRemoteUrl("git@github.com:acme/app.git")).toBe("git@github.com:acme/app.git"); + }); + + it("captures minimal git attribution metadata", () => { + const repo = makeGitRepo(); + + expect(gitMetadataForCwd(repo.dir)).toEqual({ + git_origin_url: "https://github.com/acme/app.git", + git_branch: "main", + git_commit_sha: repo.commit, + }); + }); + + it("omits git_branch when HEAD is detached", () => { + const repo = makeGitRepo(); + execFileSync("git", ["checkout", "--detach", "HEAD"], { cwd: repo.dir, stdio: "ignore" }); + + expect(gitMetadataForCwd(repo.dir)).toEqual({ + git_origin_url: "https://github.com/acme/app.git", + git_commit_sha: repo.commit, + }); + }); + + it("falls back to the cwd basename when no git origin is available", () => { + const dir = makeTempDir("pi-extension-no-git-"); + + expect(repoSlugForCwd(dir)).toBeUndefined(); + expect(rootSpanName(dir)).toBe(`pi: ${dir.split("/").at(-1)}`); + }); +}); diff --git a/src/plugins/pi/content/src/utils.ts b/src/plugins/pi/content/src/utils.ts new file mode 100644 index 0000000..bc8d836 --- /dev/null +++ b/src/plugins/pi/content/src/utils.ts @@ -0,0 +1,425 @@ +import * as childProcess from "node:child_process"; +import { createHash, randomUUID } from "node:crypto"; +import { mkdirSync } from "node:fs"; +import { appendFile, mkdir } from "node:fs/promises"; +import { basename, dirname, resolve } from "node:path"; +import type { + AgentMessageLike, + AssistantMessageLike, + ContentPartLike, + ImageLike, + NormalizedAgentMessage, + NormalizedAssistantMessage, + ToolResultMessageLike, +} from "./types.ts"; + +export function isPlainObject(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +export function ensureDir(path: string | undefined): void { + if (!path) return; + mkdirSync(path, { recursive: true }); +} + +export function toUnixSeconds(timestampMs: number): number { + return timestampMs / 1000; +} + +export function generateUuid(): string { + return randomUUID(); +} + +export function sessionKeyFor( + sessionFile: string | undefined, + sessionId: string | undefined, + cwd = process.cwd(), +): string { + if (sessionFile) return `file:${sessionFile}`; + const projectKey = shortHash(resolve(cwd)); + return `ephemeral:${projectKey}:${sessionId ?? generateUuid()}`; +} + +export function coerceToString(value: unknown): string | undefined { + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") { + return String(value); + } + return undefined; +} + +export function shortHash(value: unknown): string { + return createHash("sha256").update(safeStringify(value)).digest("hex").slice(0, 12); +} + +export function parseBoolean(value: unknown, fallback = false): boolean { + if (value === undefined || value === null || value === "") return fallback; + if (typeof value === "boolean") return value; + const normalized = coerceToString(value)?.trim().toLowerCase(); + if (!normalized) return fallback; + if (["true", "1", "yes", "y", "on"].includes(normalized)) return true; + if (["false", "0", "no", "n", "off"].includes(normalized)) return false; + return fallback; +} + +export function safeJsonParse(value: string, fallback: T): T { + try { + return JSON.parse(value) as T; + } catch { + return fallback; + } +} + +export function safeStringify(value: unknown): string { + const primitive = coerceToString(value); + if (primitive !== undefined) return primitive; + try { + return JSON.stringify(value) ?? ""; + } catch { + return ""; + } +} + +export function truncateString(value: unknown, maxLength = 10_000): string { + const text = typeof value === "string" ? value : safeStringify(value); + if (text.length <= maxLength) return text; + return `${text.slice(0, maxLength)}… [truncated ${text.length - maxLength} chars]`; +} + +export function truncateValue(value: unknown, maxLength = 10_000): unknown { + if (value === null || value === undefined) return value; + if (typeof value === "string") return truncateString(value, maxLength); + if (typeof value === "number" || typeof value === "boolean") return value; + if (Array.isArray(value)) return value.map((item) => truncateValue(item, maxLength)); + if (isPlainObject(value)) { + const result: Record = {}; + for (const [key, item] of Object.entries(value)) { + result[key] = truncateValue(item, maxLength); + } + return result; + } + return truncateString(safeStringify(value), maxLength); +} + +function contentPartToText(item: ContentPartLike): string { + if (item.type === "text") { + return typeof item.text === "string" ? item.text : ""; + } + + if (item.type === "thinking") { + return item.redacted + ? "[thinking redacted]" + : typeof item.thinking === "string" + ? item.thinking + : ""; + } + + if (item.type === "image") { + return `[image:${typeof item.mimeType === "string" ? item.mimeType : "unknown"}]`; + } + + if (item.type === "toolCall") { + const toolName = typeof item.name === "string" ? item.name : "unknown"; + return `[toolCall:${toolName}] ${safeStringify(item.arguments ?? {})}`; + } + + return `[${typeof item.type === "string" ? item.type : "content"}] ${safeStringify(item)}`; +} + +export function contentToText(content: unknown): string { + if (content === null || content === undefined) return ""; + if (typeof content === "string") return truncateString(content); + if (!Array.isArray(content)) return truncateString(safeStringify(content)); + + const lines: string[] = []; + for (const item of content) { + if (!isPlainObject(item)) { + lines.push(safeStringify(item)); + continue; + } + + lines.push(contentPartToText(item as ContentPartLike)); + } + + return truncateString(lines.filter(Boolean).join("\n")); +} + +export function normalizeUserContent(content: unknown): string { + return contentToText(content); +} + +export function normalizeAssistantMessage( + message: AssistantMessageLike, +): NormalizedAssistantMessage { + const textParts: string[] = []; + const thinkingParts: string[] = []; + const toolCalls: NonNullable = []; + + for (const part of message.content ?? []) { + if (!isPlainObject(part)) continue; + + if (part.type === "text") { + textParts.push(typeof part.text === "string" ? part.text : ""); + continue; + } + + if (part.type === "thinking") { + if (part.redacted) { + thinkingParts.push("[thinking redacted]"); + } else if (typeof part.thinking === "string" && part.thinking.length > 0) { + thinkingParts.push(part.thinking); + } + continue; + } + + if (part.type === "toolCall") { + toolCalls.push({ + id: typeof part.id === "string" ? part.id : undefined, + type: "function", + function: { + name: typeof part.name === "string" ? part.name : undefined, + arguments: truncateString(safeStringify(part.arguments ?? {})), + }, + }); + } + } + + const normalized: NormalizedAssistantMessage = { + role: "assistant", + content: truncateString(textParts.join("\n")), + }; + + if (toolCalls.length > 0) normalized.tool_calls = toolCalls; + if (thinkingParts.length > 0) { + normalized.reasoning = [{ id: "thinking", content: truncateString(thinkingParts.join("\n")) }]; + } + + return normalized; +} + +export function normalizeAgentMessage( + message: AgentMessageLike, +): NormalizedAgentMessage | undefined { + if (!message || typeof message !== "object" || !("role" in message)) return undefined; + + if (message.role === "user") { + return { + role: "user", + content: normalizeUserContent(message.content), + }; + } + + if (message.role === "assistant") { + return normalizeAssistantMessage(message as AssistantMessageLike); + } + + if (message.role === "toolResult") { + const toolMessage = message as ToolResultMessageLike; + return { + role: "tool", + content: contentToText(toolMessage.content), + tool_call_id: toolMessage.toolCallId, + name: toolMessage.toolName, + is_error: Boolean(toolMessage.isError), + }; + } + + return undefined; +} + +export function normalizeContextMessages( + messages: readonly AgentMessageLike[] | undefined, +): NormalizedAgentMessage[] { + return (messages ?? []) + .map(normalizeAgentMessage) + .filter((message): message is NormalizedAgentMessage => Boolean(message)); +} + +export function normalizeToolResult(result: unknown): unknown { + if (result === null || result === undefined) return undefined; + if (typeof result === "string") return truncateString(result); + + if (isPlainObject(result)) { + const normalized: Record = {}; + + if ("content" in result && result.content !== undefined) { + normalized.content = contentToText(result.content); + } + + if ("details" in result && result.details !== undefined) { + normalized.details = truncateValue(result.details); + } + + if ("isError" in result) { + normalized.isError = Boolean(result.isError); + } + + if (Object.keys(normalized).length > 0) return normalized; + } + + return truncateValue(result); +} + +export function extractErrorText(value: unknown, fallback: string | undefined): string | undefined { + if (!value) return fallback; + if (typeof value === "string") return truncateString(value); + if (isPlainObject(value)) { + if (typeof value.errorMessage === "string") return truncateString(value.errorMessage); + if (typeof value.message === "string") return truncateString(value.message); + if (Array.isArray(value.content)) { + const text = contentToText(value.content); + if (text) return text; + } + } + return fallback; +} + +export function formatToolSpanName(toolName: string, args: unknown = {}): string { + const objectArgs = isPlainObject(args) ? args : {}; + const pathLike = + objectArgs.path ?? + objectArgs.file ?? + objectArgs.filePath ?? + objectArgs.target ?? + objectArgs.sessionDir; + if (typeof pathLike === "string") { + return `${toolName}: ${basename(pathLike)}`; + } + + if (toolName === "bash" && typeof objectArgs.command === "string") { + const command = objectArgs.command.replace(/\s+/g, " ").trim(); + return `bash: ${truncateString(command, 60)}`; + } + + return toolName; +} + +export function buildTurnInput( + prompt: string, + images: readonly ImageLike[] | undefined = [], +): string { + const input = [prompt ?? ""]; + for (const image of images ?? []) { + const type = image?.source?.mediaType ?? image?.mimeType ?? "image"; + input.push(`[${type}]`); + } + return truncateString(input.filter(Boolean).join("\n")); +} + +const gitRemoteRepoCache = new Map(); + +export interface GitMetadata { + git_origin_url?: string; + git_branch?: string; + git_commit_sha?: string; +} + +export function redactGitRemoteUrl(remoteUrl: string): string { + const trimmed = remoteUrl.trim(); + if (!trimmed) return trimmed; + + try { + const parsed = new URL(trimmed); + if (parsed.username || parsed.password) { + parsed.username = ""; + parsed.password = ""; + } + return parsed.toString(); + } catch { + return trimmed; + } +} + +function runGit(cwd: string, args: string[]): string | undefined { + try { + const result = childProcess.spawnSync("git", ["-C", cwd, ...args], { + encoding: "utf8", + env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" }, + timeout: 500, + windowsHide: true, + }); + + if (result.status === 0 && typeof result.stdout === "string") { + const value = result.stdout.trim(); + return value.length > 0 ? value : undefined; + } + } catch { + return undefined; + } + + return undefined; +} + +function parseGitRemoteRepo(remoteUrl: string): string | undefined { + const trimmed = remoteUrl.trim(); + if (!trimmed) return undefined; + + const scpLikeMatch = trimmed.match(/^[^@\s]+@[^:\s]+:(.+)$/); + const path = scpLikeMatch + ? scpLikeMatch[1] + : (() => { + try { + return new URL(trimmed).pathname; + } catch { + return undefined; + } + })(); + + if (!path) return undefined; + + const segments = path + .replace(/^\/+/, "") + .replace(/\.git$/i, "") + .split("/") + .filter(Boolean); + + if (segments.length < 2) return undefined; + return `${segments.at(-2)}/${segments.at(-1)}`; +} + +export function gitMetadataForCwd(cwd: string | undefined): GitMetadata { + if (!cwd) return {}; + + const origin = runGit(cwd, ["remote", "get-url", "origin"]); + const branch = runGit(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"]); + const commit = runGit(cwd, ["rev-parse", "HEAD"]); + + return { + ...(origin ? { git_origin_url: redactGitRemoteUrl(origin) } : {}), + ...(branch ? { git_branch: branch } : {}), + ...(commit ? { git_commit_sha: commit } : {}), + }; +} + +export function repoSlugForCwd(cwd: string): string | undefined { + const resolvedCwd = cwd || process.cwd(); + if (gitRemoteRepoCache.has(resolvedCwd)) { + return gitRemoteRepoCache.get(resolvedCwd); + } + + let repo: string | undefined; + + const origin = runGit(resolvedCwd, ["remote", "get-url", "origin"]); + if (origin) repo = parseGitRemoteRepo(origin); + + gitRemoteRepoCache.set(resolvedCwd, repo); + return repo; +} + +export function rootSpanName(cwd: string): string { + return `pi: ${repoSlugForCwd(cwd) ?? basename(cwd || process.cwd())}`; +} + +export async function writeJsonLog( + filePath: string, + level: string, + message: string, + data?: unknown, +): Promise { + await mkdir(dirname(filePath), { recursive: true }); + await appendFile( + filePath, + `${JSON.stringify({ timestamp: new Date().toISOString(), level, message, data: truncateValue(data) })}\n`, + "utf8", + ); +} diff --git a/src/plugins/pi/content/src/version.ts b/src/plugins/pi/content/src/version.ts new file mode 100644 index 0000000..6b442c4 --- /dev/null +++ b/src/plugins/pi/content/src/version.ts @@ -0,0 +1,2 @@ +// This file is generated by scripts/sync-version.mjs. Do not edit by hand. +export const EXTENSION_VERSION = "0.10.0"; diff --git a/src/plugins/pi/content/tsconfig.json b/src/plugins/pi/content/tsconfig.json new file mode 100644 index 0000000..8aa6f8e --- /dev/null +++ b/src/plugins/pi/content/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "allowImportingTsExtensions": true, + "noEmit": true, + "strict": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "types": ["node"], + "lib": ["ES2023", "DOM"] + }, + "include": ["src/**/*.ts"] +} diff --git a/src/plugins/pi/content/vite.config.ts b/src/plugins/pi/content/vite.config.ts new file mode 100644 index 0000000..7852e3c --- /dev/null +++ b/src/plugins/pi/content/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + fmt: { + ignorePatterns: ["AGENTS.md", "README.md", "dist/**"], + }, + lint: { + ignorePatterns: ["dist/**"], + options: { + typeAware: true, + typeCheck: true, + }, + }, + test: { + include: ["src/**/*.test.ts"], + }, + pack: { + entry: ["src/index.ts"], + dts: true, + format: ["esm"], + sourcemap: true, + }, +}); diff --git a/src/plugins/pi/publish.sh b/src/plugins/pi/publish.sh new file mode 100755 index 0000000..26a4c24 --- /dev/null +++ b/src/plugins/pi/publish.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BUILD_DIR="${BUILD_DIR:-$PLUGIN_DIR/../../../dist/pi}" +NPM_TAG="${NPM_TAG:-latest}" +case "$NPM_TAG" in latest|rc|next|beta) ;; *) echo "unsupported NPM_TAG: $NPM_TAG" >&2; exit 1;; esac +tarball="$(find "$BUILD_DIR" -maxdepth 1 -name 'braintrust-pi-extension-*.tgz' -print -quit)" +[[ -n "$tarball" ]] || { echo "build Pi before publishing" >&2; exit 1; } +version="$(node -p "require('$BUILD_DIR/package.json').version")" +if npm view "@braintrust/pi-extension@$version" version >/dev/null 2>&1; then + echo "@braintrust/pi-extension@$version is already published" >&2; exit 1 +fi +if [[ "${DRY_RUN:-}" == "1" ]]; then npm publish "$tarball" --tag "$NPM_TAG" --dry-run; exit 0; fi +[[ "${RELEASE_CONFIRM:-}" == "publish-@braintrust/pi-extension@$version" ]] || { + echo "set RELEASE_CONFIRM=publish-@braintrust/pi-extension@$version for a release-approved publish" >&2; exit 1; +} +npm publish "$tarball" --tag "$NPM_TAG" diff --git a/src/plugins/pi/validate.sh b/src/plugins/pi/validate.sh new file mode 100755 index 0000000..da4bdc2 --- /dev/null +++ b/src/plugins/pi/validate.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +TARGET_DIR="${1:?usage: validate.sh }" +fail() { echo "validate: $*" >&2; exit 1; } +TARGET_DIR="$(cd "$TARGET_DIR" && pwd)" + +[[ -f "$TARGET_DIR/package.json" ]] || fail "missing package.json" +[[ -f "$TARGET_DIR/dist/index.mjs" ]] || fail "missing production entrypoint" +tarball="$(find "$TARGET_DIR" -maxdepth 1 -name 'braintrust-pi-extension-*.tgz' -print -quit)" +[[ -n "$tarball" ]] || fail "missing npm tarball" + +(cd "$TARGET_DIR" && pnpm install --frozen-lockfile && pnpm run check && pnpm test && pnpm run smoke && pnpm run pack) +pack_check_dir="$(mktemp -d)" +(cd "$TARGET_DIR" && pnpm pack --pack-destination "$pack_check_dir" >/dev/null) + +contents="$(tar -tzf "$tarball")" +for required in package/dist/index.mjs package/dist/index.d.mts package/README.md package/LICENSE; do + grep -q "^${required}$" <<<"$contents" || fail "tarball omits $required" +done +if grep -q '^package/src/' <<<"$contents"; then fail "tarball contains local tracing source"; fi +node -e 'const p=require(process.argv[1]); if (p.dependencies?.braintrust) process.exit(1)' "$TARGET_DIR/package.json" \ + || fail "Braintrust SDK remains a Pi runtime dependency" +if rg -n "from [\"']braintrust[\"']|from [\"']\.\/(client|state)[\"']" "$TARGET_DIR/src/index.ts"; then + fail "Pi daemon adapter imports the Braintrust SDK or local span persistence" +fi + +echo "validate: Pi npm package OK ($TARGET_DIR)" diff --git a/src/runtime/js-daemon-client/src/index.ts b/src/runtime/js-daemon-client/src/index.ts index 6aef0bc..64b139f 100644 --- a/src/runtime/js-daemon-client/src/index.ts +++ b/src/runtime/js-daemon-client/src/index.ts @@ -148,9 +148,10 @@ export class DaemonClient { async status(sessionId?: string): Promise { return this.serial(async () => { try { - return (await this.request("status.get", { - ...(sessionId ? { session_id: sessionId } : {}), - })) as DaemonStatus + return (await this.request( + "status.get", + sessionId ? { session_id: sessionId } : {}, + )) as DaemonStatus } catch (error) { this.warnOnce(`status:${String(error)}`) return undefined From a87c30de3c0ecdf3e8468cdb678d131c44e4d16e Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Wed, 5 Aug 2026 21:59:52 +0800 Subject: [PATCH 2/6] Make Pi validation portable in CI --- src/plugins/pi/validate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/pi/validate.sh b/src/plugins/pi/validate.sh index da4bdc2..e830620 100755 --- a/src/plugins/pi/validate.sh +++ b/src/plugins/pi/validate.sh @@ -21,7 +21,7 @@ done if grep -q '^package/src/' <<<"$contents"; then fail "tarball contains local tracing source"; fi node -e 'const p=require(process.argv[1]); if (p.dependencies?.braintrust) process.exit(1)' "$TARGET_DIR/package.json" \ || fail "Braintrust SDK remains a Pi runtime dependency" -if rg -n "from [\"']braintrust[\"']|from [\"']\.\/(client|state)[\"']" "$TARGET_DIR/src/index.ts"; then +if grep -n -E "from [\"']braintrust[\"']|from [\"']\.\/(client|state)[\"']" "$TARGET_DIR/src/index.ts"; then fail "Pi daemon adapter imports the Braintrust SDK or local span persistence" fi From 9f62dffa4c3c7e638bc58eb13e9ec63dfed7603b Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Thu, 6 Aug 2026 02:19:26 +0800 Subject: [PATCH 3/6] Route Pi through daemon profiles and add real-agent coverage --- .github/workflows/ci.yml | 9 +- bt-daemon/tests/agent_integration.rs | 38 +++++- bt-daemon/tests/pi_translator.rs | 1 + bt-daemon/tests/support/agents/mod.rs | 3 + bt-daemon/tests/support/agents/pi.rs | 124 ++++++++++++++++++ src/plugins/pi/content/README.md | 2 + src/plugins/pi/content/src/config.test.ts | 4 + src/plugins/pi/content/src/config.ts | 12 ++ .../pi/content/src/daemon-adapter.test.ts | 18 ++- src/plugins/pi/content/src/index.ts | 15 ++- src/plugins/pi/content/src/types.ts | 1 + 11 files changed, 217 insertions(+), 10 deletions(-) create mode 100644 bt-daemon/tests/support/agents/pi.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8cffc9..e25ec32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -141,18 +141,23 @@ jobs: rustup default stable rustup component add clippy rustfmt - name: Install latest coding agents - run: npm install --prefix "${{ runner.temp }}/coding-agents" --no-save --no-package-lock --no-audit --no-fund --cache "${{ runner.temp }}/npm-cache" @openai/codex@latest @anthropic-ai/claude-code@latest opencode-ai@latest + run: npm install --prefix "${{ runner.temp }}/coding-agents" --no-save --no-package-lock --no-audit --no-fund --cache "${{ runner.temp }}/npm-cache" @openai/codex@latest @anthropic-ai/claude-code@latest opencode-ai@latest @earendil-works/pi-coding-agent@latest - name: Set up pinned Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.14 - name: Build OpenCode plugin for integration harness run: make build-opencode + - name: Build Pi extension for integration harness + run: make build-pi + - name: Install built Pi extension for integration harness + run: npm install --prefix "${{ runner.temp }}/coding-agents" --no-save --no-package-lock --no-audit --no-fund ./dist/pi/braintrust-pi-extension-*.tgz - name: Report coding-agent versions run: | npm exec --prefix "${{ runner.temp }}/coding-agents" -- codex --version npm exec --prefix "${{ runner.temp }}/coding-agents" -- claude --version npm exec --prefix "${{ runner.temp }}/coding-agents" -- opencode --version + npm exec --prefix "${{ runner.temp }}/coding-agents" -- pi --version - name: Check formatting if: runner.os == 'Linux' run: cargo fmt --manifest-path bt-daemon/Cargo.toml -- --check @@ -167,6 +172,8 @@ jobs: CODEX_BIN: ${{ runner.temp }}/coding-agents/node_modules/.bin/codex${{ matrix.agent_suffix }} CLAUDE_BIN: ${{ runner.temp }}/coding-agents/node_modules/.bin/claude${{ matrix.agent_suffix }} OPENCODE_BIN: ${{ runner.temp }}/coding-agents/node_modules/.bin/opencode${{ matrix.agent_suffix }} + PI_BIN: ${{ runner.temp }}/coding-agents/node_modules/.bin/pi${{ matrix.agent_suffix }} + PI_EXTENSION_PATH: ${{ runner.temp }}/coding-agents/node_modules/@braintrust/pi-extension/dist/index.mjs run: cargo test --manifest-path bt-daemon/Cargo.toml --all-features --locked --test agent_integration -- --ignored --nocapture --test-threads=1 - name: Lint daemon run: cargo clippy --manifest-path bt-daemon/Cargo.toml --all-targets --all-features --locked -- -D warnings diff --git a/bt-daemon/tests/agent_integration.rs b/bt-daemon/tests/agent_integration.rs index 5329ffa..1634713 100644 --- a/bt-daemon/tests/agent_integration.rs +++ b/bt-daemon/tests/agent_integration.rs @@ -3,7 +3,9 @@ mod support; use axum::http::StatusCode; use serde_json::{json, Value}; use support::agent_process::AgentTestWorld; -use support::agents::{ClaudeAgent, ClaudeRun, CodexAgent, CodexRun, OpenCodeAgent, OpenCodeRun}; +use support::agents::{ + ClaudeAgent, ClaudeRun, CodexAgent, CodexRun, OpenCodeAgent, OpenCodeRun, PiAgent, PiRun, +}; use support::inference::{ AnthropicMock, AnthropicRequest, AnthropicTurn, MockReply, OpenAiMock, OpenAiRequest, OpenAiTurn, @@ -276,6 +278,40 @@ async fn opencode_session_emits_traces() { } } +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore = "requires the Pi CLI and built extension"] +async fn pi_session_emits_traces() { + let inference = OpenAiMock::new(|_context, request| { + assert!(request.model().is_some()); + MockReply::response(OpenAiTurn::text("PI_MOCK_OK")) + }); + let inference_server = TestServer::start(inference.router()).await; + let world = AgentTestWorld::start().await; + let pi = PiAgent::new(&world); + + let output = pi + .run( + &world, + PiRun::new("Reply with the exact text PI_MOCK_OK.") + .mock_inference(inference_server.uri()), + ) + .await; + output.assert_success(); + if world.uses_mock_inference() { + output.assert_contains("PI_MOCK_OK"); + assert!(!inference.requests().is_empty(), "{}", output.text()); + } + + let rows = world.wait_for_trace_delivery().await; + if world.uses_mock_ingest() { + assert!( + rows.iter() + .any(|row| row_contains(row, &["braintrust.plugin.pi", "test_harness"])), + "Pi trace origin metadata was not emitted" + ); + } +} + #[test] fn request_helpers_recognize_tool_results_and_advertised_tools() { let openai = OpenAiRequest { diff --git a/bt-daemon/tests/pi_translator.rs b/bt-daemon/tests/pi_translator.rs index e1645d4..9a13662 100644 --- a/bt-daemon/tests/pi_translator.rs +++ b/bt-daemon/tests/pi_translator.rs @@ -11,6 +11,7 @@ fn event(name: &str, ts_ms: i64, native: serde_json::Value) -> Envelope { event: name.into(), ts_ms, payload: json!({"event":native,"extension_version":"0.10.0","cwd":"."}), + route: None, config: None, } } diff --git a/bt-daemon/tests/support/agents/mod.rs b/bt-daemon/tests/support/agents/mod.rs index 9c2aae0..bc15c2c 100644 --- a/bt-daemon/tests/support/agents/mod.rs +++ b/bt-daemon/tests/support/agents/mod.rs @@ -1,6 +1,7 @@ mod claude; mod codex; mod opencode; +mod pi; mod test_plugin; #[allow(unused_imports)] @@ -9,6 +10,8 @@ pub use claude::{ClaudeAgent, ClaudeRun}; pub use codex::{CodexAgent, CodexRun}; #[allow(unused_imports)] pub use opencode::{OpenCodeAgent, OpenCodeRun}; +#[allow(unused_imports)] +pub use pi::{PiAgent, PiRun}; use std::ffi::OsString; use std::path::PathBuf; diff --git a/bt-daemon/tests/support/agents/pi.rs b/bt-daemon/tests/support/agents/pi.rs new file mode 100644 index 0000000..1fd2364 --- /dev/null +++ b/bt-daemon/tests/support/agents/pi.rs @@ -0,0 +1,124 @@ +use super::{command_from_env, AgentOutput, ProcessOptions}; +use crate::support::agent_process::AgentTestWorld; +use serde_json::json; +use std::ffi::OsString; +use std::path::{Path, PathBuf}; + +pub struct PiAgent { + config_dir: PathBuf, + session_dir: PathBuf, + extension: PathBuf, +} + +pub struct PiRun { + prompt: OsString, + mock_inference: Option, + options: ProcessOptions, +} + +impl PiRun { + pub fn new(prompt: impl Into) -> Self { + Self { + prompt: prompt.into(), + mock_inference: None, + options: ProcessOptions::default(), + } + } + + pub fn mock_inference(mut self, base_url: impl Into) -> Self { + self.mock_inference = Some(base_url.into()); + self + } + + pub fn arg(mut self, value: impl Into) -> Self { + self.options.arg(value); + self + } + + pub fn env(mut self, key: impl Into, value: impl Into) -> Self { + self.options.env(key, value); + self + } +} + +impl PiAgent { + pub fn new(world: &AgentTestWorld) -> Self { + let config_dir = world.temp_path("pi-config"); + let session_dir = world.temp_path("pi-sessions"); + for directory in [&config_dir, &session_dir] { + std::fs::create_dir_all(directory).expect("create Pi test directory"); + } + let extension = std::env::var_os("PI_EXTENSION_PATH") + .map(PathBuf::from) + .unwrap_or_else(|| { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("repository root") + .join("dist/pi/dist/index.mjs") + }); + assert!( + extension.is_file(), + "build the Pi extension before running integration tests: {}", + extension.display() + ); + Self { + config_dir, + session_dir, + extension, + } + } + + pub async fn run(&self, world: &AgentTestWorld, run: PiRun) -> AgentOutput { + let mut command = command_from_env("PI_BIN", "pi"); + if world.uses_mock_inference() { + let base_url = run + .mock_inference + .as_ref() + .expect("mock Pi runs require a mock inference endpoint"); + std::fs::write( + self.config_dir.join("models.json"), + serde_json::to_vec_pretty(&json!({ + "providers": { + "mock": { + "baseUrl": format!("{base_url}/v1"), + "api": "openai-responses", + "apiKey": "test-key", + "models": [{"id": "mock-model", "name": "Mock Model"}] + } + } + })) + .unwrap(), + ) + .expect("write Pi mock model config"); + } + command + .args([ + "--print", + "--mode", + "json", + "--provider", + "mock", + "--model", + "mock-model", + "--api-key", + "test-key", + "--no-extensions", + "--no-skills", + "--no-context-files", + "--no-tools", + "--extension", + ]) + .arg(&self.extension) + .arg("--session-dir") + .arg(&self.session_dir) + .current_dir(world.workspace()) + .env("PI_CODING_AGENT_DIR", &self.config_dir) + .env("TRACE_TO_BRAINTRUST", "true") + .env("BRAINTRUST_PROJECT", "agent-e2e") + .env("PI_OFFLINE", "true"); + world.configure(&mut command); + run.options.apply(&mut command); + command.arg(run.prompt); + world.output(&mut command).await.into() + } +} diff --git a/src/plugins/pi/content/README.md b/src/plugins/pi/content/README.md index e34588b..aec4e24 100644 --- a/src/plugins/pi/content/README.md +++ b/src/plugins/pi/content/README.md @@ -92,6 +92,7 @@ Example: ```json { "trace_to_braintrust": true, + "profile": "work", "project": "pi", "debug": true, "additional_metadata": { @@ -109,6 +110,7 @@ Example: | `api_url` | `BRAINTRUST_API_URL` | `https://api.braintrust.dev` | | `app_url` | `BRAINTRUST_APP_URL` | `https://www.braintrust.dev` | | `org_name` | `BRAINTRUST_ORG_NAME` | unset | +| `profile` | `BRAINTRUST_PROFILE` | default `bt` profile | | `project` | `BRAINTRUST_PROJECT` | `pi` | | `debug` | `BRAINTRUST_DEBUG` | `false` | | `additional_metadata` | `BRAINTRUST_ADDITIONAL_METADATA` | `{}` | diff --git a/src/plugins/pi/content/src/config.test.ts b/src/plugins/pi/content/src/config.test.ts index 132131f..c95f6ec 100644 --- a/src/plugins/pi/content/src/config.test.ts +++ b/src/plugins/pi/content/src/config.test.ts @@ -10,6 +10,7 @@ const ENV_KEYS = [ "BRAINTRUST_API_KEY", "BRAINTRUST_API_URL", "BRAINTRUST_APP_URL", + "BRAINTRUST_PROFILE", "BRAINTRUST_ORG_NAME", "BRAINTRUST_PROJECT", "TRACE_TO_BRAINTRUST", @@ -66,6 +67,7 @@ describe("loadConfig", () => { const envStateDir = join(home, "env-state"); process.env.HOME = home; + process.env.BRAINTRUST_PROFILE = "from-env"; process.env.BRAINTRUST_PROJECT = "from-env"; process.env.TRACE_TO_BRAINTRUST = "false"; process.env.BRAINTRUST_ADDITIONAL_METADATA = '{"origin":"env","team":"platform"}'; @@ -74,6 +76,7 @@ describe("loadConfig", () => { writeJson(join(home, ".pi", "agent", "braintrust.json"), { api_key: "global-key", api_url: "https://global.example", + profile: "from-global", project: "from-global", trace_to_braintrust: false, debug: true, @@ -92,6 +95,7 @@ describe("loadConfig", () => { expect(config.apiKey).toBe("global-key"); expect(config.apiUrl).toBe("https://project.example"); + expect(config.profile).toBe("from-env"); expect(config.projectName).toBe("from-env"); expect(config.enabled).toBe(false); expect(config.additionalMetadata).toEqual({ origin: "env", team: "platform" }); diff --git a/src/plugins/pi/content/src/config.ts b/src/plugins/pi/content/src/config.ts index d0a8ebb..8b219f8 100644 --- a/src/plugins/pi/content/src/config.ts +++ b/src/plugins/pi/content/src/config.ts @@ -177,6 +177,9 @@ function applyConfig( const appUrl = validateOptionalUrl(source.app_url, issues, path, "app_url"); if (appUrl !== undefined) target.appUrl = appUrl; + const profile = validateOptionalNonEmptyString(source.profile, issues, path, "profile"); + if (profile !== undefined) target.profile = profile; + const orgName = validateOptionalNonEmptyString(source.org_name, issues, path, "org_name"); if (orgName !== undefined) target.orgName = orgName; @@ -247,6 +250,7 @@ export function loadConfig(cwd = process.cwd()): TraceConfig { apiKey: "", apiUrl: undefined, appUrl: "https://www.braintrust.dev", + profile: undefined, orgName: undefined, projectName: "pi", debug: false, @@ -315,6 +319,14 @@ export function loadConfig(cwd = process.cwd()): TraceConfig { ); if (envAppUrl !== undefined) config.appUrl = envAppUrl; + const envProfile = validateOptionalNonEmptyString( + process.env.BRAINTRUST_PROFILE, + config.configIssues, + "BRAINTRUST_PROFILE", + "BRAINTRUST_PROFILE", + ); + if (envProfile !== undefined) config.profile = envProfile; + const envOrgName = validateOptionalNonEmptyString( process.env.BRAINTRUST_ORG_NAME, config.configIssues, diff --git a/src/plugins/pi/content/src/daemon-adapter.test.ts b/src/plugins/pi/content/src/daemon-adapter.test.ts index 7dfced3..e2fea68 100644 --- a/src/plugins/pi/content/src/daemon-adapter.test.ts +++ b/src/plugins/pi/content/src/daemon-adapter.test.ts @@ -31,14 +31,22 @@ vi.mock("./runtime/daemon-client.ts", () => ({ ], }; } - close(): void { + async close(): Promise { mockState.closed += 1; } }, })); vi.mock("./config.ts", () => ({ - loadConfig: () => ({ enabled: true, showUi: true, showTraceLink: true }), + loadConfig: () => ({ + enabled: true, + profile: "work", + orgName: "acme", + projectName: "agents", + additionalMetadata: { team: "platform" }, + showUi: true, + showTraceLink: true, + }), })); describe("Pi daemon adapter", () => { @@ -86,6 +94,12 @@ describe("Pi daemon adapter", () => { expect( mockState.logs.every((log) => log.source === "pi" && typeof log.ts_ms === "number"), ).toBe(true); + expect(mockState.logs[0]?.route).toEqual({ + auth: { profile: "work", org_name: "acme" }, + destination: { type: "project_logs", project_name: "agents" }, + flush_mode: "flush_on_turn_end", + additional_metadata: { team: "platform" }, + }); expect(mockState.flushes).toHaveLength(2); expect( widgets.some((args) => JSON.stringify(args).includes("https://www.braintrust.dev/trace/1")), diff --git a/src/plugins/pi/content/src/index.ts b/src/plugins/pi/content/src/index.ts index d93ec79..f6376a8 100644 --- a/src/plugins/pi/content/src/index.ts +++ b/src/plugins/pi/content/src/index.ts @@ -95,12 +95,15 @@ export default function braintrustPiExtension(pi: ExtensionAPI): void { native_session_id: descriptor?.nativeSessionId, cwd: ctx?.cwd, model: nativePayload(ctx?.model), - trace_settings: { - project: config.projectName, - additional_metadata: config.additionalMetadata, - parent_span_id: config.parentSpanId, - root_span_id: config.rootSpanId, + }, + route: { + auth: { + ...(config.profile ? { profile: config.profile } : {}), + ...(config.orgName ? { org_name: config.orgName } : {}), }, + destination: { type: "project_logs", project_name: config.projectName }, + flush_mode: "flush_on_turn_end", + additional_metadata: config.additionalMetadata, }, }); if (flush) { @@ -136,7 +139,7 @@ export default function braintrustPiExtension(pi: ExtensionAPI): void { ctx.ui.setStatus(STATUS_KEY, undefined); ctx.ui.setWidget(WIDGET_KEY, undefined); } - client.close(); + await client.close(); sessionId = undefined; lastContext = undefined; }); diff --git a/src/plugins/pi/content/src/types.ts b/src/plugins/pi/content/src/types.ts index bfd1320..d5034f7 100644 --- a/src/plugins/pi/content/src/types.ts +++ b/src/plugins/pi/content/src/types.ts @@ -18,6 +18,7 @@ export interface TraceConfig { apiKey: string; apiUrl?: string; appUrl: string; + profile?: string; orgName?: string; projectName: string; debug: boolean; From 285d8c3f0af00082563f536deee36b64ea360c10 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Thu, 6 Aug 2026 02:27:01 +0800 Subject: [PATCH 4/6] Make Pi integration builds portable --- .github/workflows/ci.yml | 4 ++++ src/plugins/pi/build.sh | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e25ec32..1f5660c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -146,6 +146,10 @@ jobs: uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.14 + - name: Set up pinned pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + version: 10.33.0 - name: Build OpenCode plugin for integration harness run: make build-opencode - name: Build Pi extension for integration harness diff --git a/src/plugins/pi/build.sh b/src/plugins/pi/build.sh index d7bdbdc..32fb239 100755 --- a/src/plugins/pi/build.sh +++ b/src/plugins/pi/build.sh @@ -10,12 +10,13 @@ trap 'rm -rf "$STAGE"' EXIT rm -rf "$TARGET_DIR" mkdir -p "$TARGET_DIR" "$STAGE/src/runtime" TARGET_DIR="$(cd "$TARGET_DIR" && pwd)" -rsync -a --exclude node_modules --exclude dist "$PLUGIN_DIR/content/" "$STAGE/" +tar --exclude=node_modules --exclude=dist -cf - -C "$PLUGIN_DIR/content" . \ + | tar -xf - -C "$STAGE" cp "$REPO_ROOT/src/runtime/js-daemon-client/src/index.ts" "$STAGE/src/runtime/daemon-client.ts" (cd "$STAGE" && pnpm install --frozen-lockfile && pnpm exec vp fmt src/runtime/daemon-client.ts && pnpm run sync:version && pnpm run pack) rm -rf "$STAGE/node_modules" -rsync -a "$STAGE/" "$TARGET_DIR/" +cp -R "$STAGE/." "$TARGET_DIR/" (cd "$TARGET_DIR" && pnpm pack --pack-destination "$TARGET_DIR" >/dev/null) echo "Built Pi npm package and tarball in $TARGET_DIR" From c84a508b6ca04f0fe58195519410b219185423f7 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Thu, 6 Aug 2026 02:29:30 +0800 Subject: [PATCH 5/6] Isolate Pi tarball installation in agent CI --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f5660c..144443a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,7 +155,7 @@ jobs: - name: Build Pi extension for integration harness run: make build-pi - name: Install built Pi extension for integration harness - run: npm install --prefix "${{ runner.temp }}/coding-agents" --no-save --no-package-lock --no-audit --no-fund ./dist/pi/braintrust-pi-extension-*.tgz + run: npm install --prefix "${{ runner.temp }}/pi-extension" --no-save --no-package-lock --no-audit --no-fund ./dist/pi/braintrust-pi-extension-*.tgz - name: Report coding-agent versions run: | npm exec --prefix "${{ runner.temp }}/coding-agents" -- codex --version @@ -177,7 +177,7 @@ jobs: CLAUDE_BIN: ${{ runner.temp }}/coding-agents/node_modules/.bin/claude${{ matrix.agent_suffix }} OPENCODE_BIN: ${{ runner.temp }}/coding-agents/node_modules/.bin/opencode${{ matrix.agent_suffix }} PI_BIN: ${{ runner.temp }}/coding-agents/node_modules/.bin/pi${{ matrix.agent_suffix }} - PI_EXTENSION_PATH: ${{ runner.temp }}/coding-agents/node_modules/@braintrust/pi-extension/dist/index.mjs + PI_EXTENSION_PATH: ${{ runner.temp }}/pi-extension/node_modules/@braintrust/pi-extension/dist/index.mjs run: cargo test --manifest-path bt-daemon/Cargo.toml --all-features --locked --test agent_integration -- --ignored --nocapture --test-threads=1 - name: Lint daemon run: cargo clippy --manifest-path bt-daemon/Cargo.toml --all-targets --all-features --locked -- -D warnings From 3da7d5418bb4c232d8b3afead215d286602b978b Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Thu, 6 Aug 2026 23:59:30 +0800 Subject: [PATCH 6/6] Remove legacy Pi tracing runtime --- .github/workflows/ci.yml | 11 +- bt-daemon/tests/pi_translator.rs | 1 + src/plugins/pi/content/AGENTS.md | 84 - src/plugins/pi/content/CONTRIBUTING.md | 206 +-- src/plugins/pi/content/README.md | 16 +- src/plugins/pi/content/docs/PUBLISHING.md | 125 +- src/plugins/pi/content/package.json | 7 - src/plugins/pi/content/pnpm-lock.yaml | 1250 ------------- .../content/scripts/resolve-pi-versions.mjs | 79 +- src/plugins/pi/content/src/client.ts | 282 --- src/plugins/pi/content/src/config.test.ts | 475 +---- src/plugins/pi/content/src/config.ts | 549 +----- .../pi/content/src/daemon-adapter.test.ts | 28 + .../pi/content/src/index.integration.test.ts | 726 -------- src/plugins/pi/content/src/index.test.ts | 1230 ------------- src/plugins/pi/content/src/index.ts | 13 +- .../pi/content/src/legacy-processor.ts | 1628 ----------------- src/plugins/pi/content/src/state.test.ts | 131 -- src/plugins/pi/content/src/state.ts | 197 -- src/plugins/pi/content/src/types.ts | 194 -- src/plugins/pi/content/src/utils.test.ts | 240 --- src/plugins/pi/content/src/utils.ts | 425 ----- src/plugins/pi/validate.sh | 20 +- 23 files changed, 255 insertions(+), 7662 deletions(-) delete mode 100644 src/plugins/pi/content/AGENTS.md delete mode 100644 src/plugins/pi/content/src/client.ts delete mode 100644 src/plugins/pi/content/src/index.integration.test.ts delete mode 100644 src/plugins/pi/content/src/index.test.ts delete mode 100644 src/plugins/pi/content/src/legacy-processor.ts delete mode 100644 src/plugins/pi/content/src/state.test.ts delete mode 100644 src/plugins/pi/content/src/state.ts delete mode 100644 src/plugins/pi/content/src/types.ts delete mode 100644 src/plugins/pi/content/src/utils.test.ts delete mode 100644 src/plugins/pi/content/src/utils.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 144443a..64b3ef5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,12 +112,13 @@ jobs: - name: Build validated Pi tarball run: make validate-pi - name: Test resolved Pi compatibility target - working-directory: dist/pi - env: - PI_COMPAT_VERSION: ${{ matrix.pi-target.version }} run: | - pnpm add -D '${{ matrix.pi-target.piAiSpec }}' '${{ matrix.pi-target.piCodingAgentSpec }}' --lockfile=false - pnpm run test:integration + mkdir pi-install-test + pnpm --dir pi-install-test add --lockfile=false \ + "$PWD"/dist/pi/braintrust-pi-extension-*.tgz \ + '${{ matrix.pi-target.piCodingAgentSpec }}' + node -e 'import(process.argv[1]).then((m) => { if (typeof m.default !== "function") process.exit(1) })' \ + "$PWD/pi-install-test/node_modules/@braintrust/pi-extension/dist/index.mjs" daemon: name: Daemon (${{ matrix.os }}) diff --git a/bt-daemon/tests/pi_translator.rs b/bt-daemon/tests/pi_translator.rs index 9a13662..6864dd0 100644 --- a/bt-daemon/tests/pi_translator.rs +++ b/bt-daemon/tests/pi_translator.rs @@ -7,6 +7,7 @@ fn event(name: &str, ts_ms: i64, native: serde_json::Value) -> Envelope { Envelope { source: "pi".into(), source_version: Some("0.80.10".into()), + plugin_version: Some("0.10.0".into()), session_id: "pi-session".into(), event: name.into(), ts_ms, diff --git a/src/plugins/pi/content/AGENTS.md b/src/plugins/pi/content/AGENTS.md deleted file mode 100644 index 95547a4..0000000 --- a/src/plugins/pi/content/AGENTS.md +++ /dev/null @@ -1,84 +0,0 @@ -# AGENTS.md - -This file is consumed by agents. Keep it in sync with [CONTRIBUTING.md](./CONTRIBUTING.md), which is written for humans. For contributor-facing setup, repository conventions, architecture, and validation commands, see `CONTRIBUTING.md`; this file keeps the agent-specific guidance concise. - -## Project - -This repository contains `@braintrust/pi-extension`, a **pi extension/package** that adds **automatic Braintrust tracing** to pi sessions today. - -This project is intentionally **tracing-only**: - -- no MCP integration -- no Braintrust query tools exposed to the model -- no UI-heavy features unless directly required for tracing/debugging - -## Goals - -When working in this repo, optimize for: - -1. **Correct trace structure** in Braintrust -2. **Low overhead / non-blocking behavior** during pi sessions -3. **Safe failure behavior** — tracing must never break the pi session -4. **Simple install/use as a pi package** - -## Agent reminders - -- Read `README.md` first for external behavior. -- Use `CONTRIBUTING.md` for development workflow, repo layout, and validation details. -- This repo is managed by [mise](https://mise.jdx.dev/) using `.tool-versions`; prefer that as the toolchain source of truth. -- Preserve package identity in `package.json` as a pi package. -- Keep the extension entrypoint at `src/index.ts` unless there is a strong reason to change it. -- Keep `src/version.ts` generated from `package.json`; do not edit it by hand. After a package version bump, run `pnpm run sync:version` and commit the generated change. -- Prefer evolving the current design over rewriting it. -- Tracing must be best-effort: log failures and continue. -- Preserve the intended trace hierarchy unless there is a strong reason to change it: - -```text -Session (task) -├── Turn N (task) -│ ├── LLM call(s) (llm) -│ └── Tool call(s) (tool) -``` - -- Configuration precedence should remain: - 1. defaults - 2. `~/.pi/agent/braintrust.json` - 3. `.pi/braintrust.json` - 4. environment variables -- `.pi/braintrust.json` is extension-specific, not a built-in pi config file. -- Prefer integration coverage for lifecycle/session regressions: use the real pi SDK harness in `src/index.integration.test.ts` when testing restore, reload, session switch/fork, headless behavior, or cross-version compatibility. - -## What to avoid - -Unless explicitly requested, do **not**: - -- add MCP-related code -- add Braintrust query tools for the agent -- add unrelated pi commands/UI features -- add heavy dependencies -- make tracing depend on interactive-only pi behavior - -## Validation - -After code changes, at minimum run: - -```bash -pnpm run check -pnpm test -pnpm run smoke -``` - -If the change affects extension lifecycle behavior, session persistence/restore, reload/fork/switch flows, or pi compatibility assumptions, also run: - -```bash -pnpm run test:integration -``` - -If packaging changes or release version bumps were made, also run: - -```bash -pnpm run sync:version -pnpm run check -pnpm run pack -pnpm pack --dry-run -``` diff --git a/src/plugins/pi/content/CONTRIBUTING.md b/src/plugins/pi/content/CONTRIBUTING.md index 186385e..be83224 100644 --- a/src/plugins/pi/content/CONTRIBUTING.md +++ b/src/plugins/pi/content/CONTRIBUTING.md @@ -1,202 +1,28 @@ -# Contributing to @braintrust/pi-extension +# Contributing -Thanks for contributing. +`@braintrust/pi-extension` is developed in the Braintrust coding-agent plugin +monorepo under `src/plugins/pi/content`. -This document is for repository contributors and humans. Keep it in sync with [AGENTS.md](./AGENTS.md), which is consumed by agents. For installation and usage, see [README.md](./README.md). For publishing and release steps, see [docs/PUBLISHING.md](./docs/PUBLISHING.md). For agent-specific instructions, see [AGENTS.md](./AGENTS.md). +The JavaScript package is intentionally a thin adapter: it reads non-secret +routing and UI preferences, forwards native pi events through the shared daemon +client, and displays daemon status. It must not construct spans, persist tracing +state, resolve credentials, or call the Braintrust API. -## Project scope - -This repository contains `@braintrust/pi-extension`, a **pi extension/package** that adds **automatic Braintrust tracing** to pi sessions today. - -This project is intentionally **tracing-only**: - -- no MCP integration -- no Braintrust query tools exposed to the model -- no UI-heavy features unless directly required for tracing/debugging - -## Goals - -When working in this repo, optimize for: - -1. **Correct trace structure** in Braintrust -2. **Low overhead / non-blocking behavior** during pi sessions -3. **Safe failure behavior** — tracing must never break the pi session -4. **Simple install/use as a pi package** - -## Development setup - -This repo is set up for [Vite+](https://viteplus.dev/guide/). - -This repo is managed by [mise](https://mise.jdx.dev/). Use the pinned project toolchain from `.tool-versions`, including `node`, `pnpm`, and `vite-plus` (which provides `vp`): - -```bash -mise install -pnpm install -pnpm run sync:version -pnpm run check -pnpm run pack -pnpm run smoke -``` - -Or through package scripts: - -```bash -pnpm run check -pnpm run typecheck -pnpm run test:integration -pnpm run pack -pnpm run smoke -``` - -Notes: - -- `pnpm run sync:version` regenerates `src/version.ts` from `package.json` after a version bump. -- `pnpm run check` verifies `src/version.ts` is in sync, then runs the main formatting, linting, and type-check entrypoint. -- `pnpm run typecheck` delegates to `vp check`. -- `pnpm run pack` builds an optional library bundle in `dist/`. -- pi loads the extension directly from `src/index.ts`, so local development does not require a build step. -- Publishing is handled by `.github/workflows/publish.yml` using npm trusted publishing with OIDC and provenance attestations; see [docs/PUBLISHING.md](./docs/PUBLISHING.md). - -## Repository layout - -- `src/index.ts` - - main pi extension entrypoint - - subscribes to pi lifecycle events - - creates session / turn / llm / tool spans -- `src/client.ts` - - thin wrapper around the official Braintrust JavaScript SDK - - initializes the project logger - - creates, updates, and flushes spans safely -- `src/config.ts` - - loads config from: - 1. defaults - 2. `~/.pi/agent/braintrust.json` - 3. `.pi/braintrust.json` - 4. env vars -- `src/state.ts` - - persistent local session bookkeeping -- `src/utils.ts` - - content normalization, truncation, IDs, small helpers -- `src/types.ts` - - shared TypeScript types for config, spans, state, and normalized messages -- `src/version.ts` - - generated extension version constant derived from `package.json` - - do not edit by hand; run `pnpm run sync:version` after changing the package version - -## Repo conventions - -### Language / module system - -- Use **TypeScript ESM** (`.ts`). -- Keep dependencies minimal. -- Prefer Node built-ins over adding packages. -- This repo uses **Vite+** for checks and packaging. -- pi loads the extension directly from TypeScript; no build output directory is required for local development. -- `src/version.ts` is generated at package/build time from `package.json`; `prepack` regenerates it and `check` fails if it is stale. - -### Style - -- Keep code small and readable. -- Prefer small helper functions over deeply nested event handlers. -- Avoid clever abstractions unless they clearly improve tracing correctness. -- Do not introduce formatting churn. - -### Error handling - -- Tracing must be **best-effort**. -- Never throw from normal extension event paths if it can be avoided. -- Log failures and continue. -- If Braintrust is unavailable, pi should still work normally. - -### Performance - -- Avoid blocking network calls in critical event handlers whenever possible. -- Prefer the Braintrust SDK's async/background flushing path. -- Keep payloads reasonably small. -- Truncate large values before sending. - -## Trace model - -The intended hierarchy is: - -```text -Session (task) -├── Turn N (task) -│ ├── LLM call(s) (llm) -│ └── Tool call(s) (tool) -``` - -Expected behavior: - -- one **session/root span** per pi session that produces at least one turn -- one **turn span** per user prompt / agent run -- one **llm span** per assistant/model response -- one **tool span** per tool execution - -If changing event wiring, preserve this structure unless there is a strong reason to change it. - -## Configuration expectations - -This extension uses its own config files: - -- global: `~/.pi/agent/braintrust.json` -- project: `.pi/braintrust.json` - -This is modeled after the OpenCode Braintrust plugin, not Claude Code’s settings-based setup. - -Important: - -- `.pi/braintrust.json` is **extension-specific**, not a built-in pi config file. -- environment variables should remain the highest-precedence source. - -## What to avoid - -Unless the change explicitly calls for it, do **not**: - -- add MCP-related code -- add Braintrust query tools for the agent -- add unrelated pi commands/UI features -- add heavy dependencies -- make tracing depend on interactive-only pi behavior - -## Safe ways to extend - -Good additions include: - -- better parent/root span linking -- better session/fork/subagent handling -- improved metadata and token accounting -- more robust recovery across session switches -- test coverage / smoke checks -- publish/install polish for pi packages - -## Validation - -After code changes, at minimum run: +Run the package checks from this directory with: ```bash +pnpm install --frozen-lockfile pnpm run check pnpm test pnpm run smoke -``` - -If packaging changes or release version bumps were made, also run: - -```bash -pnpm run sync:version -pnpm run check pnpm run pack -pnpm pack --dry-run ``` -Test commands: - -- `pnpm test` — full unit + integration test suite -- `pnpm run test:integration` — integration coverage for real pi extension loading/session flows - -## Additional notes +From the monorepo root, `make validate-pi` performs the complete package and +tarball validation. The Rust translator and real-agent harness live under +`bt-daemon/` and are exercised by the root CI workflow. -- Read `README.md` first for external behavior. -- Preserve package identity in `package.json` as a pi package. -- Keep the extension entrypoint at `src/index.ts` unless there is a strong reason to change it. -- Prefer evolving the current design over rewriting it. +Configuration precedence remains defaults, the global pi extension config, +project-local pi extension config, then environment variables. Configuration +may select a `bt` profile, organization, project, metadata, and UI behavior, but +must never contain credentials. diff --git a/src/plugins/pi/content/README.md b/src/plugins/pi/content/README.md index aec4e24..ad7a1a7 100644 --- a/src/plugins/pi/content/README.md +++ b/src/plugins/pi/content/README.md @@ -5,6 +5,9 @@ Braintrust extension for [pi](https://github.com/earendil-works/pi-coding-agent). Today this extension automatically traces pi sessions, turns, model calls, and tool executions to Braintrust. +The extension forwards native pi events to the installed `bt` tracing daemon; +all span construction, authentication, recovery, and Braintrust delivery happen +inside the daemon. ## What gets traced @@ -94,7 +97,6 @@ Example: "trace_to_braintrust": true, "profile": "work", "project": "pi", - "debug": true, "additional_metadata": { "team": "platform" } @@ -106,20 +108,12 @@ Example: | Config key | Env var | Default | |---|---|---| | `trace_to_braintrust` | `TRACE_TO_BRAINTRUST` | `false` | -| `api_key` | `BRAINTRUST_API_KEY` | legacy; tracing authentication is owned by `bt auth login` | -| `api_url` | `BRAINTRUST_API_URL` | `https://api.braintrust.dev` | -| `app_url` | `BRAINTRUST_APP_URL` | `https://www.braintrust.dev` | | `org_name` | `BRAINTRUST_ORG_NAME` | unset | | `profile` | `BRAINTRUST_PROFILE` | default `bt` profile | | `project` | `BRAINTRUST_PROJECT` | `pi` | -| `debug` | `BRAINTRUST_DEBUG` | `false` | | `additional_metadata` | `BRAINTRUST_ADDITIONAL_METADATA` | `{}` | -| `log_file` | `BRAINTRUST_LOG_FILE` | unset | -| `state_dir` | `BRAINTRUST_STATE_DIR` | `~/.pi/agent/state/braintrust-pi-extension` | | `show_ui` | `BRAINTRUST_SHOW_UI` | `true` | | `show_trace_link` | `BRAINTRUST_SHOW_TRACE_LINK` | `true` | -| `parent_span_id` | `PI_PARENT_SPAN_ID` | unset | -| `root_span_id` | `PI_ROOT_SPAN_ID` | unset | ## Notes @@ -128,10 +122,10 @@ Example: - Project config follows pi's configured project config directory, which defaults to `.pi`. - The extension does not persist local span state; recovery and incomplete-operation cleanup are owned by the daemon journal. - Span construction and Braintrust delivery run in the installed `bt` tracing daemon. +- The extension never reads or stores Braintrust credentials. Profile selection is + non-secret, optional, and resolved by the daemon through `bt` authentication. - Provider request tracing is allowlisted to effective model, thinking, output-limit, and tool-count settings; full provider payloads and thinking signatures are never logged. - If Braintrust is unavailable, pi should continue working normally. -- If `PI_PARENT_SPAN_ID` is set, the pi session span is attached under an existing Braintrust trace. -- `PI_ROOT_SPAN_ID` can be used when the parent span is not the trace root. ## Contributing diff --git a/src/plugins/pi/content/docs/PUBLISHING.md b/src/plugins/pi/content/docs/PUBLISHING.md index ba4add8..567fd5e 100644 --- a/src/plugins/pi/content/docs/PUBLISHING.md +++ b/src/plugins/pi/content/docs/PUBLISHING.md @@ -1,120 +1,17 @@ -# Publishing @braintrust/pi-extension +# Publishing -This document describes how this package is released to npm and what the GitHub Actions publish workflow does. +This package is built and validated from the Braintrust coding-agent plugin +monorepo. The source manifest owns the committed package version, and +`src/plugins/pi/build.sh` creates the npm tarball from a clean staging tree. -## Overview - -Publishing is handled by the manual workflow at: - -- `.github/workflows/publish.yml` - -It runs the **shared, centrally-maintained release actions** from -[`braintrustdata/sdk-actions`](https://github.com/braintrustdata/sdk-actions), pinned by commit -SHA. Bumping that SHA pulls in upstream release-tooling improvements. This repo adds no release -logic of its own — the workflow is just the shared actions wired together. - -Key properties: - -- The **version comes entirely from `package.json` at the released SHA** — the workflow never - computes, synthesizes, or overrides it. Everything published corresponds to a commit you can - check out, so **you must merge a version bump before releasing.** -- **`channel`** picks the npm dist-tag: `latest` (stable — also git tag + GitHub release) or - `rc`/`next`/`beta` (that dist-tag only). A prerelease is simply a committed prerelease version - (e.g. `0.8.0-rc.1`) published to a non-`latest` tag. -- npm **trusted publishing** via GitHub Actions OIDC + provenance — no long-lived `NPM_TOKEN`. -- You release an **explicit commit SHA** (not a branch), pinning the release to a reviewed commit. -- The publish job is gated by a **GitHub Environment** (manual approval) and posts **Slack** - notifications before approval and on completion. -- The package ships its `src/` directly (no build step); `npm publish`'s `prepack` regenerates - `src/version.ts`. - -The environments, npm trusted publisher, and Slack secret/variable are one-time setup — -see the `SETUP REQUIREMENTS` comment block at the top of `.github/workflows/publish.yml`. - -## Release steps - -1. **Bump the version first.** Update `package.json` to the target version (stable e.g. `0.8.0`, - or a prerelease e.g. `0.8.0-rc.1`), run `pnpm run sync:version`, commit, and **merge to `main`**. - The workflow reads the version from the commit you release — it cannot be set at dispatch. -2. In GitHub, open **Actions → Publish package** and **Run workflow**. -3. Set the inputs: - - `channel`: `latest` for a stable release; `rc`/`next`/`beta` for a prerelease. - - `sha`: the full 40-character SHA of the version-bump commit. - - `prev_release` (optional): release-notes anchor; defaults to the previous release tag. - - `dry_run` (optional): `true` to run the pipeline with `pnpm publish --dry-run` (no publish). -4. When the `publish` job requests it, **approve the `publish` environment**. -5. After it succeeds, verify: - - the version exists on npm (`npm view @braintrust/pi-extension dist-tags`) - - the provenance attestation is present on npm - - (channel `latest` only) the `pi-extension-v` tag was pushed and the GitHub release created - -CI already runs the full quality suite (version-sync, format, lint, types, tests, pack, smoke) -on every push, so the release workflow does **not** re-run it — it is concerned only with release -policy and publishing. - -## What the workflow does - -Flow: `validate → prepare → notify-pending → [approval gate] → publish`. - -1. **`validate`** (shared): reads the version from `package.json` at the SHA, then checks release - policy — the tag isn't already taken, the npm version isn't already published, the `channel` - is allowed, and the SHA is well-formed and on `main`. Runs with `build: false` (nothing to build). -2. **`prepare`** (shared): generates the release notes + PR list. -3. **`notify-pending`** (shared): posts a Slack notification that a release is pending approval. -4. **`publish`** (shared, gated): checks out the SHA and publishes the committed version to npm - with provenance (OIDC) on the chosen `channel`; for `channel: latest` it also pushes the - `pi-extension-v` tag and creates the GitHub release. Posts a Slack completion notification. - -`latest` → `latest` dist-tag + git tag + GitHub release. `rc`/`next`/`beta` → that dist-tag only, -no tag or release. - -## Authentication model - -The workflow uses GitHub Actions OIDC for npm trusted publishing. The `publish` job requests: - -```yaml -permissions: - contents: write # push the git tag + create the GitHub release - id-token: write # OIDC token for npm trusted publishing + provenance -``` - -## Tagging and release naming - -- git tag: `pi-extension-v` (channel `latest` only) -- GitHub release name: `@braintrust/pi-extension v` - -Example for `0.8.0`: tag `pi-extension-v0.8.0`, release `@braintrust/pi-extension v0.8.0`. - -## Failure modes - -- the target tag already exists, or the npm version is already published → `validate` fails early -- the channel isn't in the allowlist, or the SHA is malformed → `validate` fails -- the npm trusted publisher isn't configured for the `publish` environment → publish fails - with `ENEEDAUTH`. A `dry_run` does **not** exercise OIDC, so the first **real** publish is the - first true test — canary with a prerelease (`channel: rc`) before any stable release. -- the tag/GitHub release are created only after a successful publish, so a failed publish leaves - no orphaned tag - -## Local preflight checks - -The release workflow relies on CI for quality, but before bumping a version it's still useful to run: +Local release preflight from the monorepo root: ```bash -pnpm run sync:version -pnpm run check -pnpm test -pnpm run pack -pnpm pack --dry-run -pnpm run smoke +make validate-pi +DRY_RUN=1 NPM_TAG=next src/plugins/pi/publish.sh dist/pi/braintrust-pi-extension-*.tgz ``` -## Notes - -- The package is public; `package.json` includes `publishConfig.access = "public"`. -- The workflow is manual (`workflow_dispatch`) and releases an explicit `sha`, not a branch. -- `src/version.ts` is generated from `package.json` (by `scripts/sync-version.mjs`, run via - `prepack`) so trace metadata reports the version without runtime package-metadata loading. -- The shared actions are pinned to a `braintrustdata/sdk-actions` SHA in `publish.yml`; bumping it - adopts upstream improvements. -- If the release process changes, update this file, `AGENTS.md`, `CONTRIBUTING.md`, and - `.github/workflows/publish.yml` as needed. +The publisher accepts only a previously built tarball and rejects an existing +npm version. A real publish requires the release-only confirmation documented +by `publish.sh`. Normal development and CI use only the dry-run path; they do +not publish, tag, or create a GitHub release. diff --git a/src/plugins/pi/content/package.json b/src/plugins/pi/content/package.json index 80901d2..4584119 100644 --- a/src/plugins/pi/content/package.json +++ b/src/plugins/pi/content/package.json @@ -36,20 +36,13 @@ "pack": "vp pack", "sync:version": "node scripts/sync-version.mjs", "test": "vitest run", - "test:integration": "vitest run src/index.integration.test.ts", "test:watch": "vitest", "typecheck": "vp check", "smoke": "vp exec node -e \"import('./src/index.ts')\"" }, - "dependencies": { - "valibot": "^1.3.1" - }, "devDependencies": { - "@earendil-works/pi-ai": "^0.80.2", "@earendil-works/pi-coding-agent": "^0.80.2", "@types/node": "^26.0.0", - "braintrust": "^3.9.0", - "typebox": "^1.1.38", "typescript": "^6.0.2", "vite-plus": "^0.2.1", "vitest": "^4.1.4" diff --git a/src/plugins/pi/content/pnpm-lock.yaml b/src/plugins/pi/content/pnpm-lock.yaml index 2c007e5..1864205 100644 --- a/src/plugins/pi/content/pnpm-lock.yaml +++ b/src/plugins/pi/content/pnpm-lock.yaml @@ -7,26 +7,13 @@ settings: importers: .: - dependencies: - valibot: - specifier: ^1.3.1 - version: 1.4.2(typescript@6.0.3) devDependencies: - '@earendil-works/pi-ai': - specifier: ^0.80.2 - version: 0.80.10(ws@8.21.1)(zod@4.4.3) '@earendil-works/pi-coding-agent': specifier: ^0.80.2 version: 0.80.10(ws@8.21.1)(zod@4.4.3) '@types/node': specifier: ^26.0.0 version: 26.1.1 - braintrust: - specifier: ^3.9.0 - version: 3.23.1(@aws-sdk/credential-provider-web-identity@3.972.65)(zod@4.4.3) - typebox: - specifier: ^1.1.38 - version: 1.1.38 typescript: specifier: ^6.0.2 version: 6.0.3 @@ -160,48 +147,6 @@ packages: '@blazediff/core@1.9.1': resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} - '@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'} - '@earendil-works/pi-agent-core@0.80.10': resolution: {integrity: sha512-nwnOR3SuLYGRFfyQm8ri4Nj5VGVAvAM9GuqQd3u7BUQj0d6hmD2F8w7OHAAjThE3CuySIdM+v8E22QJG6/RfCg==} engines: {node: '>=22.19.0'} @@ -235,312 +180,156 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.28.0': - resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} - 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-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-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/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-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/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-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/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-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-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-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-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-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-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-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-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/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-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/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-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/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/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/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-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-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] - '@google/genai@1.52.0': resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} engines: {node: '>=20.0.0'} @@ -550,28 +339,9 @@ packages: '@modelcontextprotocol/sdk': optional: true - '@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==} - '@mariozechner/clipboard-darwin-arm64@0.3.9': resolution: {integrity: sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==} engines: {node: '>= 10'} @@ -654,9 +424,6 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@next/env@14.2.35': - resolution: {integrity: sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==} - '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -1084,12 +851,6 @@ packages: '@silvia-odwyer/photon-node@0.3.4': resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} - '@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==} - '@smithy/core@3.29.5': resolution: {integrity: sha512-i0dk2t5B+CwV/dcJdUHILYkOQF5lof8f44dFCfDWToGCxjT9YQ+CgHqTAvJxzc3+zqQwm2QtVoJ5IqiNar/CnQ==} engines: {node: '>=18.0.0'} @@ -1164,15 +925,6 @@ packages: '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - '@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/browser-preview@4.1.10': resolution: {integrity: sha512-14MJrL59ZFkqXLjwfSk6RzTDy5Czf9UG4+8q8L6Gxjs2aPjEce/cVNYV14bXAc2BvMjUNu904+ZEZA1Xc1wtvQ==} peerDependencies: @@ -1446,27 +1198,10 @@ packages: '@yuku-toolchain/types@0.5.43': resolution: {integrity: sha512-kSpvPntnXw5+lYjO71ffBEnQ5ycQ74KGIYknh0TS4xeyCuBkOqxyJumxZkMhLBBUCLjDAbx2+Icnr3Zh4ftjpQ==} - 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 - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} - 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'} @@ -1475,9 +1210,6 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -1485,10 +1217,6 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - astring@1.9.0: - resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} - hasBin: true - balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -1499,10 +1227,6 @@ packages: bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} - body-parser@2.3.0: - resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} - engines: {node: '>=18'} - bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} @@ -1510,27 +1234,9 @@ packages: 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-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.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'} @@ -1539,44 +1245,9 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - 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.*} - - 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'} - - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1585,9 +1256,6 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} - 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'} @@ -1597,10 +1265,6 @@ packages: supports-color: optional: true - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1616,91 +1280,27 @@ packages: dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - 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'} - ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - 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'} - - 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.0: resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} - 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 - - 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'} - - eventsource-parser@1.1.2: - resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==} - engines: {node: '>=14.18'} - 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'} - extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-uri@3.1.3: - resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1714,30 +1314,15 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} - finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} - formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} - 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'} - 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==} - gaxios@7.2.0: resolution: {integrity: sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg==} engines: {node: '>=18'} @@ -1750,14 +1335,6 @@ packages: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} - 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'} - get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} @@ -1773,21 +1350,9 @@ packages: resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} engines: {node: '>=14'} - 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'} - highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} @@ -1795,10 +1360,6 @@ packages: resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==} engines: {node: ^20.17.0 || >=22.9.0} - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1807,28 +1368,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - iconv-lite@0.7.3: - resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} - engines: {node: '>=0.10.0'} - ignore@7.0.5: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - - 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==} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1846,9 +1389,6 @@ packages: resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} engines: {node: '>=16'} - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} @@ -1948,30 +1488,6 @@ packages: engines: {node: '>= 20'} hasBin: true - 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'} - minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -1980,9 +1496,6 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} - module-details-from-path@1.0.4: - resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} - mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -1990,19 +1503,11 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - mustache@4.2.0: - resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} - hasBin: true - nanoid@3.3.16: resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -2012,25 +1517,10 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - 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.3: resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} 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==} - openai@6.26.0: resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} hasBin: true @@ -2077,10 +1567,6 @@ packages: resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} engines: {node: '>=8'} - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - partial-json@0.1.7: resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} @@ -2092,9 +1578,6 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} - path-to-regexp@8.4.2: - resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -2105,10 +1588,6 @@ packages: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} - pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - pngjs@7.0.0: resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} engines: {node: '>=14.19.0'} @@ -2128,29 +1607,9 @@ packages: resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} engines: {node: '>=12.0.0'} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - - 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'} - react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -2167,35 +1626,14 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - 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.0: resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} 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==} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2204,31 +1642,12 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - 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==} signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - simple-git@3.36.0: - resolution: {integrity: sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==} - sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -2237,32 +1656,12 @@ packages: 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==} - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - termi-link@1.1.0: - resolution: {integrity: sha512-2qSN6TnomHgVLtk+htSWbaYs4Rd2MH/RU7VpHTy6MBstyNyWbM4yKd1DCYpE3fDg8dmGWojXCngNi/MHCzGuAA==} - engines: {node: '>=12'} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2282,10 +1681,6 @@ packages: 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'} - totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -2301,10 +1696,6 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - type-is@2.1.0: - resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} - engines: {node: '>= 18'} - typebox@1.1.38: resolution: {integrity: sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==} @@ -2320,30 +1711,6 @@ packages: resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==} engines: {node: '>=22.19.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'} - - uuid@11.1.1: - resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} - hasBin: true - - valibot@1.4.2: - resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} - peerDependencies: - typescript: '>=5' - peerDependenciesMeta: - typescript: - optional: true - - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - vite-plus@0.2.5: resolution: {integrity: sha512-QNJ0FnN8rfs5u8lZKZ1uR2Tegjg3VkT0AGTxSLGHg4fYmGxRNfAV0YX1parZrVa4VybSI56SWCoo7wQ6D7pMew==} engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} @@ -2445,9 +1812,6 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} - webpack-virtual-modules@0.6.2: - resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2458,9 +1822,6 @@ packages: engines: {node: '>=8'} hasBin: true - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.21.1: resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} engines: {node: '>=10.0.0'} @@ -2726,30 +2087,6 @@ snapshots: '@blazediff/core@1.9.1': {} - '@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 - '@earendil-works/pi-agent-core@0.80.10(ws@8.21.1)(zod@4.4.3)': dependencies: '@earendil-works/pi-ai': 0.80.10(ws@8.21.1)(zod@4.4.3) @@ -2839,159 +2176,81 @@ snapshots: '@esbuild/aix-ppc64@0.27.7': optional: true - '@esbuild/aix-ppc64@0.28.0': - optional: true - '@esbuild/android-arm64@0.27.7': optional: true - '@esbuild/android-arm64@0.28.0': - optional: true - '@esbuild/android-arm@0.27.7': optional: true - '@esbuild/android-arm@0.28.0': - optional: true - '@esbuild/android-x64@0.27.7': optional: true - '@esbuild/android-x64@0.28.0': - optional: true - '@esbuild/darwin-arm64@0.27.7': optional: true - '@esbuild/darwin-arm64@0.28.0': - optional: true - '@esbuild/darwin-x64@0.27.7': optional: true - '@esbuild/darwin-x64@0.28.0': - optional: true - '@esbuild/freebsd-arm64@0.27.7': optional: true - '@esbuild/freebsd-arm64@0.28.0': - optional: true - '@esbuild/freebsd-x64@0.27.7': optional: true - '@esbuild/freebsd-x64@0.28.0': - optional: true - '@esbuild/linux-arm64@0.27.7': optional: true - '@esbuild/linux-arm64@0.28.0': - optional: true - '@esbuild/linux-arm@0.27.7': optional: true - '@esbuild/linux-arm@0.28.0': - optional: true - '@esbuild/linux-ia32@0.27.7': optional: true - '@esbuild/linux-ia32@0.28.0': - optional: true - '@esbuild/linux-loong64@0.27.7': optional: true - '@esbuild/linux-loong64@0.28.0': - optional: true - '@esbuild/linux-mips64el@0.27.7': optional: true - '@esbuild/linux-mips64el@0.28.0': - optional: true - '@esbuild/linux-ppc64@0.27.7': optional: true - '@esbuild/linux-ppc64@0.28.0': - optional: true - '@esbuild/linux-riscv64@0.27.7': optional: true - '@esbuild/linux-riscv64@0.28.0': - optional: true - '@esbuild/linux-s390x@0.27.7': optional: true - '@esbuild/linux-s390x@0.28.0': - optional: true - '@esbuild/linux-x64@0.27.7': optional: true - '@esbuild/linux-x64@0.28.0': - optional: true - '@esbuild/netbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-arm64@0.28.0': - optional: true - '@esbuild/netbsd-x64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.28.0': - optional: true - '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/openbsd-arm64@0.28.0': - optional: true - '@esbuild/openbsd-x64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.28.0': - optional: true - '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/openharmony-arm64@0.28.0': - optional: true - '@esbuild/sunos-x64@0.27.7': optional: true - '@esbuild/sunos-x64@0.28.0': - optional: true - '@esbuild/win32-arm64@0.27.7': optional: true - '@esbuild/win32-arm64@0.28.0': - optional: true - '@esbuild/win32-ia32@0.27.7': optional: true - '@esbuild/win32-ia32@0.28.0': - optional: true - '@esbuild/win32-x64@0.27.7': optional: true - '@esbuild/win32-x64@0.28.0': - optional: true - '@google/genai@1.52.0': dependencies: google-auth-library: 10.9.0 @@ -3003,33 +2262,8 @@ snapshots: - supports-color - utf-8-validate - '@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': {} - '@mariozechner/clipboard-darwin-arm64@0.3.9': optional: true @@ -3093,8 +2327,6 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@next/env@14.2.35': {} - '@opentelemetry/api@1.9.0': {} '@opentelemetry/semantic-conventions@1.43.0': {} @@ -3315,12 +2547,6 @@ snapshots: '@silvia-odwyer/photon-node@0.3.4': {} - '@simple-git/args-pathspec@1.0.3': {} - - '@simple-git/argv-parser@1.1.1': - dependencies: - '@simple-git/args-pathspec': 1.0.3 - '@smithy/core@3.29.5': dependencies: '@smithy/types': 4.16.1 @@ -3413,10 +2639,6 @@ snapshots: '@types/retry@0.12.0': {} - '@vercel/functions@1.6.0(@aws-sdk/credential-provider-web-identity@3.972.65)': - optionalDependencies: - '@aws-sdk/credential-provider-web-identity': 3.972.65 - '@vitest/browser-preview@4.1.10(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10)': dependencies: '@testing-library/dom': 10.4.1 @@ -3596,158 +2818,38 @@ snapshots: '@yuku-toolchain/types@0.5.43': {} - 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: {} - agent-base@7.1.4: {} - ajv@8.20.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.3 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - ansi-regex@5.0.1: {} ansi-styles@5.2.0: {} - argparse@2.0.1: {} - aria-query@5.3.0: dependencies: dequal: 2.0.3 assertion-error@2.0.1: {} - astring@1.9.0: {} - balanced-match@4.0.4: {} base64-js@1.5.1: {} bignumber.js@9.3.1: {} - 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 - bowser@2.14.1: {} brace-expansion@5.0.7: dependencies: balanced-match: 4.0.4 - braintrust@3.23.1(@aws-sdk/credential-provider-web-identity@3.972.65)(zod@4.4.3): - dependencies: - '@next/env': 14.2.35 - '@vercel/functions': 1.6.0(@aws-sdk/credential-provider-web-identity@3.972.65) - 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-equal-constant-time@1.0.1: {} - bytes@3.1.2: {} - - 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: {} chalk@5.6.2: {} - 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 - - 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: {} - - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3756,14 +2858,10 @@ snapshots: data-uri-to-buffer@4.0.1: {} - dc-browser@1.0.4: {} - debug@4.4.3: dependencies: ms: 2.1.3 - depd@2.0.0: {} - dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -3772,34 +2870,12 @@ snapshots: dom-accessibility-api@0.5.16: {} - 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 - ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 - ee-first@1.1.1: {} - - emoji-regex@8.0.0: {} - - encodeurl@2.0.0: {} - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - es-module-lexer@2.3.0: {} - es-object-atoms@1.1.2: - dependencies: - es-errors: 1.3.0 - esbuild@0.27.7: optionalDependencies: '@esbuild/aix-ppc64': 0.27.7 @@ -3830,92 +2906,14 @@ snapshots: '@esbuild/win32-x64': 0.27.7 optional: true - 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 - - 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: {} - - eventsource-parser@1.1.2: {} - 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 - extend@3.0.2: {} - fast-deep-equal@3.1.3: {} - - fast-uri@3.1.3: {} - fdir@6.5.0(picomatch@4.0.5): optionalDependencies: picomatch: 4.0.5 @@ -3925,30 +2923,13 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 - 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 - formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 - forwarded@0.2.0: {} - - fresh@2.0.0: {} - fsevents@2.3.3: optional: true - function-bind@1.1.2: {} - gaxios@7.2.0: dependencies: extend: 3.0.2 @@ -3967,24 +2948,6 @@ snapshots: get-east-asian-width@1.6.0: {} - 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 - get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -4009,30 +2972,14 @@ snapshots: google-logging-utils@1.1.3: {} - gopd@1.2.0: {} - graceful-fs@4.2.11: {} - has-symbols@1.1.0: {} - - hasown@2.0.4: - dependencies: - function-bind: 1.1.2 - highlight.js@10.7.3: {} hosted-git-info@9.0.3: dependencies: lru-cache: 11.5.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 - http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -4047,20 +2994,8 @@ snapshots: transitivePeerDependencies: - supports-color - iconv-lite@0.7.3: - dependencies: - safer-buffer: 2.1.2 - ignore@7.0.5: {} - inherits@2.0.4: {} - - ipaddr.js@1.9.1: {} - - is-fullwidth-code-point@3.0.0: {} - - is-promise@4.0.0: {} - isexe@2.0.0: {} jiti@2.7.0: {} @@ -4076,8 +3011,6 @@ snapshots: '@babel/runtime': 7.29.7 ts-algebra: 2.0.0 - json-schema-traverse@1.0.0: {} - jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -4150,38 +3083,18 @@ snapshots: marked@18.0.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 - minimatch@10.2.5: dependencies: brace-expansion: 5.0.7 minipass@7.1.3: {} - module-details-from-path@1.0.4: {} - mrmime@2.0.1: {} ms@2.1.3: {} - mustache@4.2.0: {} - nanoid@3.3.16: {} - negotiator@1.0.0: {} - node-domexception@1.0.0: {} node-fetch@3.3.2: @@ -4190,20 +3103,8 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - object-assign@4.1.1: {} - - object-inspect@1.13.4: {} - obug@2.1.3: {} - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - openai@6.26.0(ws@8.21.1)(zod@4.4.3): optionalDependencies: ws: 8.21.1 @@ -4272,8 +3173,6 @@ snapshots: '@types/retry': 0.12.0 retry: 0.13.1 - parseurl@1.3.3: {} - partial-json@0.1.7: {} path-key@3.1.1: {} @@ -4283,16 +3182,12 @@ snapshots: lru-cache: 11.5.2 minipass: 7.1.3 - path-to-regexp@8.4.2: {} - pathe@2.0.3: {} picocolors@1.1.1: {} picomatch@4.0.5: {} - pluralize@8.0.0: {} - pngjs@7.0.0: {} postcss@8.5.19: @@ -4327,29 +3222,8 @@ snapshots: '@types/node': 26.1.1 long: 5.3.2 - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - 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 - react-is@17.0.2: {} - require-from-string@2.0.2: {} - resolve-pkg-maps@1.0.0: optional: true @@ -4381,99 +3255,20 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - 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.2.1: {} - safer-buffer@2.1.2: {} - - semifies@1.0.0: {} - semver@7.8.0: {} - 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: {} - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.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: {} signal-exit@3.0.7: {} - 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 - sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -4482,26 +3277,10 @@ snapshots: source-map-js@1.2.1: {} - source-map@0.7.6: {} - stackback@0.0.2: {} - statuses@2.0.2: {} - std-env@4.2.0: {} - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - termi-link@1.1.0: {} - tinybench@2.9.0: {} tinyexec@1.2.4: {} @@ -4515,8 +3294,6 @@ snapshots: tinyrainbow@3.1.0: {} - toidentifier@1.0.1: {} - totalist@3.0.1: {} ts-algebra@2.0.0: {} @@ -4531,12 +3308,6 @@ snapshots: fsevents: 2.3.3 optional: true - type-is@2.1.0: - dependencies: - content-type: 2.0.0 - media-typer: 1.1.0 - mime-types: 3.0.2 - typebox@1.1.38: {} typescript@6.0.3: {} @@ -4545,23 +3316,6 @@ snapshots: undici@8.5.0: {} - 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 - - uuid@11.1.1: {} - - valibot@1.4.2(typescript@6.0.3): - optionalDependencies: - typescript: 6.0.3 - - vary@1.1.2: {} - vite-plus@0.2.5(@opentelemetry/api@1.9.0)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@6.0.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@26.1.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0): dependencies: '@oxc-project/types': 0.139.0 @@ -4669,8 +3423,6 @@ snapshots: web-streams-polyfill@3.3.3: {} - webpack-virtual-modules@0.6.2: {} - which@2.0.2: dependencies: isexe: 2.0.0 @@ -4680,8 +3432,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - wrappy@1.0.2: {} - ws@8.21.1: {} yaml@2.9.0: {} diff --git a/src/plugins/pi/content/scripts/resolve-pi-versions.mjs b/src/plugins/pi/content/scripts/resolve-pi-versions.mjs index aabf6b5..8871e46 100644 --- a/src/plugins/pi/content/scripts/resolve-pi-versions.mjs +++ b/src/plugins/pi/content/scripts/resolve-pi-versions.mjs @@ -5,18 +5,10 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { execFileSync } from "node:child_process"; -const packageFamilies = [ - { - name: "pi-coding-agent", - alias: "@earendil-works/pi-coding-agent", - sources: ["@earendil-works/pi-coding-agent", "@mariozechner/pi-coding-agent"], - }, - { - name: "pi-ai", - alias: "@earendil-works/pi-ai", - sources: ["@earendil-works/pi-ai", "@mariozechner/pi-ai"], - }, -]; +const packageFamily = { + alias: "@earendil-works/pi-coding-agent", + sources: ["@earendil-works/pi-coding-agent", "@mariozechner/pi-coding-agent"], +}; const count = Number(process.env.PI_VERSION_COUNT ?? "5"); const stableVersionPattern = /^\d+\.\d+\.\d+$/; @@ -54,33 +46,17 @@ function compareVersions(a, b) { return 0; } -const familyVersions = packageFamilies.map((family) => { - const versionsBySource = family.sources.map((packageName) => ({ - packageName, - versions: npmViewVersions(packageName), - })); - const versions = [...new Set(versionsBySource.flatMap(({ versions }) => versions))].sort( - compareVersions, - ); - return { ...family, versions, versionsBySource }; -}); - -const sharedVersions = - familyVersions - .map(({ versions }) => versions) - .reduce((commonVersions, packageVersions) => { - if (commonVersions === null) { - return packageVersions; - } - - const packageVersionSet = new Set(packageVersions); - return commonVersions.filter((version) => packageVersionSet.has(version)); - }, null) - ?.sort(compareVersions) ?? []; +const versionsBySource = packageFamily.sources.map((packageName) => ({ + packageName, + versions: npmViewVersions(packageName), +})); +const availableVersions = [...new Set(versionsBySource.flatMap(({ versions }) => versions))].sort( + compareVersions, +); const releaseLines = []; -for (const version of sharedVersions) { +for (const version of availableVersions) { const [major, minor] = version.split("."); const line = `${major}.${minor}`; const previousReleaseLine = releaseLines.at(-1); @@ -97,34 +73,25 @@ const selectedVersions = releaseLines.map(({ version }) => version).slice(-count if (selectedVersions.length < count) { throw new Error( - `Expected at least ${count} shared stable pi release lines across ${packageFamilies - .map(({ sources }) => sources.join("/")) - .join(", ")}, found ${selectedVersions.length}`, + `Expected at least ${count} stable pi release lines across ${packageFamily.sources.join( + "/", + )}, found ${selectedVersions.length}`, ); } -function installSpecFor(family, version) { - const source = family.versionsBySource.find(({ versions }) => - versions.includes(version), - )?.packageName; +function installSpecFor(version) { + const source = versionsBySource.find(({ versions }) => versions.includes(version))?.packageName; if (!source) { - throw new Error(`No source package found for ${family.name}@${version}`); + throw new Error(`No source package found for pi-coding-agent@${version}`); } - return source === family.alias - ? `${family.alias}@${version}` - : `${family.alias}@npm:${source}@${version}`; + return source === packageFamily.alias + ? `${packageFamily.alias}@${version}` + : `${packageFamily.alias}@npm:${source}@${version}`; } const selectedTargets = selectedVersions.map((version) => ({ version, - piAiSpec: installSpecFor( - familyVersions.find(({ name }) => name === "pi-ai"), - version, - ), - piCodingAgentSpec: installSpecFor( - familyVersions.find(({ name }) => name === "pi-coding-agent"), - version, - ), + piCodingAgentSpec: installSpecFor(version), })); const outputs = { @@ -145,7 +112,7 @@ if (process.env.GITHUB_STEP_SUMMARY) { [ "## Supported pi compatibility versions", "", - `Testing the latest ${count} shared stable pi release lines: ${selectedVersions.join(", ")}`, + `Testing the latest ${count} stable pi release lines: ${selectedVersions.join(", ")}`, "", ].join("\n"), ); diff --git a/src/plugins/pi/content/src/client.ts b/src/plugins/pi/content/src/client.ts deleted file mode 100644 index afdc7a4..0000000 --- a/src/plugins/pi/content/src/client.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { - ERR_PERMALINK, - NOOP_SPAN_PERMALINK, - initLogger, - type Logger as BraintrustSdkLogger, - type Span as BraintrustSdkSpan, -} from "braintrust"; -import type { Logger, TraceConfig } from "./types.ts"; -import { toUnixSeconds } from "./utils.ts"; -import { EXTENSION_VERSION } from "./version.ts"; - -export type BraintrustSpanHandle = BraintrustSdkSpan; - -export interface StartTraceSpanArgs { - spanId: string; - rootSpanId: string; - parentSpanId?: string; - name: string; - type: "task" | "llm" | "tool"; - startedAt: number; - input?: unknown; - output?: unknown; - error?: string; - metadata?: Record; - metrics?: Record; -} - -export interface UpdateTraceSpanArgs { - id: string; - spanId?: string; - rootSpanId?: string; - input?: unknown; - output?: unknown; - error?: string; - metadata?: Record; - metrics?: Record; -} - -function compactRecord>(value: T): Partial { - return Object.fromEntries( - Object.entries(value).filter(([, entry]) => entry !== undefined), - ) as Partial; -} - -function detectEnvironment(): { type?: string; name?: string } | undefined { - if (process.env.BRAINTRUST_ENVIRONMENT_TYPE || process.env.BRAINTRUST_ENVIRONMENT_NAME) { - return { - ...(process.env.BRAINTRUST_ENVIRONMENT_TYPE - ? { type: process.env.BRAINTRUST_ENVIRONMENT_TYPE } - : {}), - ...(process.env.BRAINTRUST_ENVIRONMENT_NAME - ? { name: process.env.BRAINTRUST_ENVIRONMENT_NAME } - : {}), - }; - } - if (process.env.GITHUB_ACTIONS) return { type: "ci", name: "github_actions" }; - if (process.env.CI) return { type: "ci", name: "ci" }; - if (process.env.VERCEL) return { type: "server", name: "vercel" }; - if (process.env.NODE_ENV === "production" || process.env.NODE_ENV === "staging") { - return { type: "server", name: process.env.NODE_ENV }; - } - if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "local") { - return { type: "local", name: process.env.NODE_ENV }; - } - return undefined; -} - -function spanOriginContext() { - const environment = detectEnvironment(); - return { - span_origin: { - name: "braintrust.plugin.pi", - version: EXTENSION_VERSION, - instrumentation: { name: "pi-extension" }, - ...(environment ? { environment } : {}), - }, - }; -} - -function isUsablePermalink(url: string | undefined): url is string { - return Boolean(url && url !== NOOP_SPAN_PERMALINK && !url.startsWith(ERR_PERMALINK)); -} - -export class BraintrustClient { - readonly config: TraceConfig; - readonly logger?: Logger; - sdkLogger?: BraintrustSdkLogger; - initPromise?: Promise; - - constructor(config: TraceConfig, logger?: Logger) { - this.config = config; - this.logger = logger; - } - - #ensureLogger(): BraintrustSdkLogger { - if (this.sdkLogger) return this.sdkLogger; - - if (this.config.apiUrl && !process.env.BRAINTRUST_API_URL) { - process.env.BRAINTRUST_API_URL = this.config.apiUrl; - } - - this.sdkLogger = initLogger({ - projectName: this.config.projectName, - apiKey: this.config.apiKey, - appUrl: this.config.appUrl, - orgName: this.config.orgName, - asyncFlush: true, - setCurrent: false, - debugLogLevel: this.config.debug ? "debug" : false, - }); - - return this.sdkLogger; - } - - async initialize(): Promise { - if (!this.initPromise) { - this.initPromise = this.#doInitialize(); - } - return this.initPromise; - } - - async #doInitialize(): Promise { - if (!this.config.apiKey) { - throw new Error("BRAINTRUST_API_KEY is not set"); - } - - const sdkLogger = this.#ensureLogger(); - await sdkLogger.id; - const project = await sdkLogger.project; - this.logger?.info("braintrust sdk logger initialized", { - appUrl: this.config.appUrl, - apiUrl: this.config.apiUrl, - project: project.name, - projectId: project.id, - }); - } - - startSpan(args: StartTraceSpanArgs): BraintrustSpanHandle | undefined { - try { - const sdkLogger = this.#ensureLogger(); - const span = sdkLogger.startSpan({ - spanId: args.spanId, - parentSpanIds: args.parentSpanId - ? { - spanId: args.parentSpanId, - rootSpanId: args.rootSpanId, - } - : undefined, - name: args.name, - type: args.type, - startTime: toUnixSeconds(args.startedAt), - event: compactRecord({ - input: args.input, - output: args.output, - error: args.error, - metadata: args.metadata, - metrics: args.metrics, - context: spanOriginContext(), - }), - }); - return span; - } catch (error) { - this.logger?.error("failed to start Braintrust span", { - error: String(error), - spanId: args.spanId, - parentSpanId: args.parentSpanId, - rootSpanId: args.rootSpanId, - name: args.name, - type: args.type, - }); - return undefined; - } - } - - logSpan(span: BraintrustSpanHandle | undefined, event: Omit): void { - if (!span) return; - - try { - span.log( - compactRecord({ - input: event.input, - output: event.output, - error: event.error, - metadata: event.metadata, - metrics: event.metrics, - }), - ); - } catch (error) { - this.logger?.error("failed to log Braintrust span", { - error: String(error), - spanId: span.spanId, - rootSpanId: span.rootSpanId, - }); - } - } - - endSpan(span: BraintrustSpanHandle | undefined, endedAt = Date.now()): void { - if (!span) return; - - try { - span.end({ endTime: toUnixSeconds(endedAt) }); - } catch (error) { - this.logger?.error("failed to end Braintrust span", { - error: String(error), - spanId: span.spanId, - rootSpanId: span.rootSpanId, - }); - } - } - - getSpanLink(span: BraintrustSpanHandle | undefined): string | undefined { - if (!span) return undefined; - - try { - const link = span.link(); - return isUsablePermalink(link) ? link : undefined; - } catch (error) { - this.logger?.warn("failed to build Braintrust span link", { - error: String(error), - spanId: span.spanId, - rootSpanId: span.rootSpanId, - }); - return undefined; - } - } - - async getSpanPermalink(span: BraintrustSpanHandle | undefined): Promise { - if (!span) return undefined; - - const link = this.getSpanLink(span); - if (link) return link; - - try { - const permalink = await span.permalink(); - return isUsablePermalink(permalink) ? permalink : undefined; - } catch (error) { - this.logger?.warn("failed to build Braintrust span permalink", { - error: String(error), - spanId: span.spanId, - rootSpanId: span.rootSpanId, - }); - return undefined; - } - } - - updateSpan(args: UpdateTraceSpanArgs): void { - try { - const sdkLogger = this.#ensureLogger(); - sdkLogger.updateSpan({ - id: args.id, - span_id: args.spanId, - root_span_id: args.rootSpanId, - input: args.input, - output: args.output, - error: args.error, - metadata: args.metadata, - metrics: args.metrics, - context: spanOriginContext(), - }); - } catch (error) { - this.logger?.error("failed to update Braintrust span", { - error: String(error), - id: args.id, - spanId: args.spanId, - rootSpanId: args.rootSpanId, - }); - } - } - - async flush(): Promise { - const sdkLogger = this.sdkLogger; - if (!sdkLogger) return; - - try { - await sdkLogger.flush(); - } catch (error) { - this.logger?.error("failed to flush Braintrust logs", { - error: String(error), - }); - } - } -} diff --git a/src/plugins/pi/content/src/config.test.ts b/src/plugins/pi/content/src/config.test.ts index c95f6ec..3730c94 100644 --- a/src/plugins/pi/content/src/config.test.ts +++ b/src/plugins/pi/content/src/config.test.ts @@ -1,449 +1,116 @@ -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createLogger, loadConfig } from "./config.ts"; -import type { TraceConfig } from "./types.ts"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { loadConfig } from "./config.ts"; -const ENV_KEYS = [ - "HOME", - "BRAINTRUST_API_KEY", - "BRAINTRUST_API_URL", - "BRAINTRUST_APP_URL", +const ENVIRONMENT_KEYS = [ "BRAINTRUST_PROFILE", "BRAINTRUST_ORG_NAME", "BRAINTRUST_PROJECT", - "TRACE_TO_BRAINTRUST", - "BRAINTRUST_DEBUG", - "BRAINTRUST_LOG_FILE", - "BRAINTRUST_STATE_DIR", + "BRAINTRUST_ADDITIONAL_METADATA", "BRAINTRUST_SHOW_UI", "BRAINTRUST_SHOW_TRACE_LINK", - "PI_PARENT_SPAN_ID", - "PI_ROOT_SPAN_ID", - "BRAINTRUST_ADDITIONAL_METADATA", + "TRACE_TO_BRAINTRUST", ] as const; -const originalEnv = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]])); -const tempDirs: string[] = []; +const originalEnvironment = new Map(); +const originalHome = process.env.HOME; +let home: string; +let cwd: string; + +function writeJson(path: string, value: unknown): void { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, JSON.stringify(value)); +} beforeEach(() => { - for (const key of ENV_KEYS) { + home = mkdtempSync(join(tmpdir(), "pi-config-home-")); + cwd = mkdtempSync(join(tmpdir(), "pi-config-project-")); + process.env.HOME = home; + for (const key of ENVIRONMENT_KEYS) { + originalEnvironment.set(key, process.env[key]); delete process.env[key]; } }); afterEach(() => { - for (const key of ENV_KEYS) { - const value = originalEnv[key]; - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } - } - - while (tempDirs.length > 0) { - rmSync(tempDirs.pop()!, { recursive: true, force: true }); + rmSync(home, { recursive: true, force: true }); + rmSync(cwd, { recursive: true, force: true }); + for (const key of ENVIRONMENT_KEYS) { + const value = originalEnvironment.get(key); + if (value === undefined) delete process.env[key]; + else process.env[key] = value; } + if (originalHome === undefined) delete process.env.HOME; + else process.env.HOME = originalHome; }); -function makeTempDir(prefix: string): string { - const dir = mkdtempSync(join(tmpdir(), prefix)); - tempDirs.push(dir); - return dir; -} - -function writeJson(path: string, value: unknown): void { - mkdirSync(dirname(path), { recursive: true }); - writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8"); -} - describe("loadConfig", () => { - it("applies global config, project config, then env overrides", () => { - const home = makeTempDir("pi-extension-home-"); - const cwd = join(home, "workspace"); - const projectStateDir = join(home, "project-state"); - const envStateDir = join(home, "env-state"); - - process.env.HOME = home; - process.env.BRAINTRUST_PROFILE = "from-env"; - process.env.BRAINTRUST_PROJECT = "from-env"; - process.env.TRACE_TO_BRAINTRUST = "false"; - process.env.BRAINTRUST_ADDITIONAL_METADATA = '{"origin":"env","team":"platform"}'; - process.env.BRAINTRUST_STATE_DIR = envStateDir; - - writeJson(join(home, ".pi", "agent", "braintrust.json"), { - api_key: "global-key", - api_url: "https://global.example", - profile: "from-global", - project: "from-global", - trace_to_braintrust: false, - debug: true, - additional_metadata: { origin: "global" }, - }); - - writeJson(join(cwd, ".pi", "braintrust.json"), { - api_url: "https://project.example", - project: "from-project", - trace_to_braintrust: true, - state_dir: projectStateDir, - additional_metadata: { origin: "project" }, - }); - - const config = loadConfig(cwd); - - expect(config.apiKey).toBe("global-key"); - expect(config.apiUrl).toBe("https://project.example"); - expect(config.profile).toBe("from-env"); - expect(config.projectName).toBe("from-env"); - expect(config.enabled).toBe(false); - expect(config.additionalMetadata).toEqual({ origin: "env", team: "platform" }); - expect(config.stateDir).toBe(envStateDir); - expect(config.configIssues).toEqual([]); - expect(existsSync(envStateDir)).toBe(true); - }); - - it("uses pi CONFIG_DIR_NAME for project-local config", async () => { - vi.resetModules(); - vi.doMock("@earendil-works/pi-coding-agent", () => ({ - CONFIG_DIR_NAME: ".custom-pi", - })); - try { - const { loadConfig: loadConfigWithCustomDir } = await import("./config.ts"); - - const home = makeTempDir("pi-extension-home-"); - const cwd = join(home, "workspace"); - - process.env.HOME = home; - process.env.BRAINTRUST_STATE_DIR = join(home, "state"); - - writeJson(join(cwd, ".custom-pi", "braintrust.json"), { - project: "from-custom-config-dir", - }); - - const config = loadConfigWithCustomDir(cwd); - - expect(config.projectName).toBe("from-custom-config-dir"); - } finally { - vi.doUnmock("@earendil-works/pi-coding-agent"); - vi.resetModules(); - } - }); - - it("falls back to .pi when older pi hosts do not export CONFIG_DIR_NAME", async () => { - vi.resetModules(); - vi.doMock("@earendil-works/pi-coding-agent", () => ({ - CONFIG_DIR_NAME: undefined, - })); - try { - const { loadConfig: loadConfigWithoutConfigDir } = await import("./config.ts"); - - const home = makeTempDir("pi-extension-home-"); - const cwd = join(home, "workspace"); - - process.env.HOME = home; - process.env.BRAINTRUST_STATE_DIR = join(home, "state"); - - writeJson(join(cwd, ".pi", "braintrust.json"), { - project: "from-default-config-dir", - }); - - const config = loadConfigWithoutConfigDir(cwd); - - expect(config.projectName).toBe("from-default-config-dir"); - } finally { - vi.doUnmock("@earendil-works/pi-coding-agent"); - vi.resetModules(); - } - }); - - it("records config file parse errors without throwing away other valid config sources", () => { - const home = makeTempDir("pi-extension-home-"); - const cwd = join(home, "workspace"); - - process.env.HOME = home; - - mkdirSync(dirname(join(home, ".pi", "agent", "braintrust.json")), { recursive: true }); - writeFileSync( - join(home, ".pi", "agent", "braintrust.json"), - '{\n "trace_to_braintrust": true,\n "api_key": "bad-json",\n}\n', - "utf8", - ); - - writeJson(join(cwd, ".pi", "braintrust.json"), { - trace_to_braintrust: true, - project: "from-project", - }); - - const config = loadConfig(cwd); - - expect(config.enabled).toBe(true); - expect(config.projectName).toBe("from-project"); - expect(config.configIssues).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - path: join(home, ".pi", "agent", "braintrust.json"), - severity: "error", - message: expect.stringContaining("JSON"), - }), - expect.objectContaining({ - path: "BRAINTRUST_API_KEY", - severity: "warning", - }), - ]), - ); - }); - - it("ignores malformed config value types without crashing", () => { - const home = makeTempDir("pi-extension-home-"); - const cwd = join(home, "workspace"); - - process.env.HOME = home; - process.env.BRAINTRUST_ADDITIONAL_METADATA = "not-json"; - - writeJson(join(home, ".pi", "agent", "braintrust.json"), { - api_key: { nested: true }, - project: ["wrong-type"], - trace_to_braintrust: "definitely", - debug: { nope: true }, - state_dir: { bad: true }, - additional_metadata: ["bad"], - parent_span_id: { bad: true }, - root_span_id: ["bad"], + it("defaults to disabled tracing with the daemon default profile", () => { + expect(loadConfig(cwd)).toEqual({ + enabled: false, + projectName: "pi", + showUi: true, + showTraceLink: true, }); - - const config = loadConfig(cwd); - - expect(config.apiKey).toBe(""); - expect(config.projectName).toBe("pi"); - expect(config.enabled).toBe(false); - expect(config.debug).toBe(false); - expect(config.additionalMetadata).toEqual({}); - expect(config.parentSpanId).toBeUndefined(); - expect(config.rootSpanId).toBeUndefined(); - expect(config.configIssues).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - path: join(home, ".pi", "agent", "braintrust.json"), - message: expect.stringContaining("additional_metadata"), - severity: "error", - }), - expect.objectContaining({ - path: "BRAINTRUST_ADDITIONAL_METADATA", - severity: "error", - }), - ]), - ); - expect(config.stateDir.endsWith(join(".pi", "agent", "state", "braintrust-pi-extension"))).toBe( - true, - ); - expect(existsSync(config.stateDir)).toBe(true); - }); - - it("mirrors the parent span id to the root span id when only parent is provided", () => { - const home = makeTempDir("pi-extension-home-"); - process.env.HOME = home; - process.env.BRAINTRUST_STATE_DIR = join(home, "state"); - process.env.PI_PARENT_SPAN_ID = "parent-123"; - - const config = loadConfig(home); - - expect(config.parentSpanId).toBe("parent-123"); - expect(config.rootSpanId).toBe("parent-123"); }); - it("mirrors the root span id to the parent span id when only root is provided", () => { - const home = makeTempDir("pi-extension-home-"); - process.env.HOME = home; - process.env.BRAINTRUST_STATE_DIR = join(home, "state"); - process.env.PI_ROOT_SPAN_ID = "root-123"; - - const config = loadConfig(home); - - expect(config.rootSpanId).toBe("root-123"); - expect(config.parentSpanId).toBe("root-123"); - }); - - it("keeps lower-precedence URLs when higher-precedence URL values are invalid", () => { - const home = makeTempDir("pi-extension-home-"); - const cwd = join(home, "workspace"); - - process.env.HOME = home; - process.env.BRAINTRUST_APP_URL = "ftp://braintrust.example"; - + it("layers global, project, and invocation selection", () => { writeJson(join(home, ".pi", "agent", "braintrust.json"), { - api_url: "https://global.example", + trace_to_braintrust: true, + profile: "global", + org_name: "global-org", + project: "global-project", + additional_metadata: { source: "global" }, }); - writeJson(join(cwd, ".pi", "braintrust.json"), { - api_url: "not-a-url", + profile: "project", + project: "project-project", + show_trace_link: false, }); + process.env.BRAINTRUST_PROFILE = "run"; + process.env.BRAINTRUST_ORG_NAME = "run-org"; + process.env.BRAINTRUST_PROJECT = "run-project"; + process.env.BRAINTRUST_ADDITIONAL_METADATA = '{"source":"run"}'; - const config = loadConfig(cwd); - - expect(config.apiUrl).toBe("https://global.example"); - expect(config.appUrl).toBe("https://www.braintrust.dev"); - expect(config.configIssues).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - path: join(cwd, ".pi", "braintrust.json"), - message: "api_url must be a valid http(s) URL", - severity: "error", - }), - expect.objectContaining({ - path: "BRAINTRUST_APP_URL", - message: "BRAINTRUST_APP_URL must be a valid http(s) URL", - severity: "error", - }), - ]), - ); - }); - - it("warns when parent_span_id and root_span_id are both explicitly set to the same value", () => { - const home = makeTempDir("pi-extension-home-"); - process.env.HOME = home; - process.env.BRAINTRUST_STATE_DIR = join(home, "state"); - process.env.PI_PARENT_SPAN_ID = "span-123"; - process.env.PI_ROOT_SPAN_ID = "span-123"; - - const config = loadConfig(home); - - expect(config.configIssues).toContainEqual({ - path: "parent_span_id/root_span_id", - message: - "parent_span_id and root_span_id are identical; set only one unless the parent span is also the trace root", - severity: "warning", + expect(loadConfig(cwd)).toEqual({ + enabled: true, + profile: "run", + orgName: "run-org", + projectName: "run-project", + additionalMetadata: { source: "run" }, + showUi: true, + showTraceLink: false, }); }); - it("defaults showUi and showTraceLink to true", () => { - const home = makeTempDir("pi-extension-home-"); - process.env.HOME = home; - process.env.BRAINTRUST_STATE_DIR = join(home, "state"); - - const config = loadConfig(home); - - expect(config.showUi).toBe(true); - expect(config.showTraceLink).toBe(true); - }); - - it("applies show_ui and show_trace_link from config files", () => { - const home = makeTempDir("pi-extension-home-"); - const cwd = join(home, "workspace"); - - process.env.HOME = home; - process.env.BRAINTRUST_STATE_DIR = join(home, "state"); + it("accepts invocation-local enablement and UI overrides", () => { + process.env.TRACE_TO_BRAINTRUST = "true"; + process.env.BRAINTRUST_SHOW_UI = "0"; + process.env.BRAINTRUST_SHOW_TRACE_LINK = "no"; - writeJson(join(home, ".pi", "agent", "braintrust.json"), { - show_ui: false, + expect(loadConfig(cwd)).toMatchObject({ + enabled: true, + showUi: false, + showTraceLink: false, }); - - const config = loadConfig(cwd); - - expect(config.showUi).toBe(false); - expect(config.showTraceLink).toBe(true); }); - it("overrides show_ui and show_trace_link from environment variables", () => { - const home = makeTempDir("pi-extension-home-"); - const cwd = join(home, "workspace"); - - process.env.HOME = home; - process.env.BRAINTRUST_STATE_DIR = join(home, "state"); - process.env.BRAINTRUST_SHOW_UI = "true"; - process.env.BRAINTRUST_SHOW_TRACE_LINK = "false"; - + it("ignores malformed files, values, and metadata", () => { writeJson(join(home, ".pi", "agent", "braintrust.json"), { - show_ui: false, - }); - - const config = loadConfig(cwd); - - expect(config.showUi).toBe(true); - expect(config.showTraceLink).toBe(false); - }); - - it("warns when tracing is enabled without an API key", () => { - const home = makeTempDir("pi-extension-home-"); - process.env.HOME = home; - process.env.BRAINTRUST_STATE_DIR = join(home, "state"); - process.env.TRACE_TO_BRAINTRUST = "true"; - - const config = loadConfig(home); - - expect(config.enabled).toBe(true); - expect(config.configIssues).toContainEqual({ - path: "BRAINTRUST_API_KEY", - message: "TRACE_TO_BRAINTRUST is enabled but BRAINTRUST_API_KEY is not set", - severity: "warning", + trace_to_braintrust: "sometimes", + project: [], + additional_metadata: [], }); - }); -}); + writeFileSync(join(cwd, ".pi-invalid"), "ignored"); + process.env.BRAINTRUST_ADDITIONAL_METADATA = "not-json"; -describe("createLogger", () => { - function makeLoggerConfig(overrides: Partial = {}): TraceConfig { - return { - enabled: true, - apiKey: "key", - apiUrl: undefined, - appUrl: "https://www.braintrust.dev", - orgName: undefined, + expect(loadConfig(cwd)).toEqual({ + enabled: false, projectName: "pi", - debug: true, - logFile: undefined, - stateDir: makeTempDir("pi-extension-state-"), - additionalMetadata: {}, - parentSpanId: undefined, - rootSpanId: undefined, showUi: true, showTraceLink: true, - configIssues: [], - ...overrides, - }; - } - - it("writes json log lines to the default log file when debug is enabled", async () => { - const config = makeLoggerConfig(); - - const logger = createLogger(config); - logger.debug("debug message", { nested: { value: 1 } }); - logger.warn("warn message"); - await logger.flush(); - - const lines = readFileSync(logger.filePath, "utf8").trim().split("\n"); - expect(lines).toHaveLength(2); - expect(JSON.parse(lines[0])).toMatchObject({ - level: "debug", - message: "debug message", - data: { nested: { value: 1 } }, - }); - expect(JSON.parse(lines[1])).toMatchObject({ - level: "warn", - message: "warn message", - }); - }); - - it("writes warnings and errors to the default log file when debug is disabled", async () => { - const config = makeLoggerConfig({ debug: false, logFile: undefined }); - const logger = createLogger(config); - - logger.debug("debug message"); - logger.info("info message"); - logger.warn("warn message", { reason: "test" }); - logger.error("error message"); - await logger.flush(); - - const lines = readFileSync(logger.filePath, "utf8").trim().split("\n"); - expect(lines).toHaveLength(2); - expect(JSON.parse(lines[0])).toMatchObject({ - level: "warn", - message: "warn message", - data: { reason: "test" }, - }); - expect(JSON.parse(lines[1])).toMatchObject({ - level: "error", - message: "error message", }); }); }); diff --git a/src/plugins/pi/content/src/config.ts b/src/plugins/pi/content/src/config.ts index 8b219f8..51d318a 100644 --- a/src/plugins/pi/content/src/config.ts +++ b/src/plugins/pi/content/src/config.ts @@ -1,519 +1,102 @@ import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; -import { dirname, join } from "node:path"; +import { join } from "node:path"; import * as piCodingAgent from "@earendil-works/pi-coding-agent"; -import * as v from "valibot"; -import type { ConfigIssue, JsonObject, Logger, LogLevel, TraceConfig } from "./types.ts"; -import { ensureDir, writeJsonLog } from "./utils.ts"; + +export interface PiConfig { + enabled: boolean; + profile?: string; + orgName?: string; + projectName: string; + additionalMetadata?: Record; + showUi: boolean; + showTraceLink: boolean; +} + +type ConfigRecord = Record; const PROJECT_CONFIG_DIR_NAME = typeof (piCodingAgent as { CONFIG_DIR_NAME?: unknown }).CONFIG_DIR_NAME === "string" ? (piCodingAgent as { CONFIG_DIR_NAME: string }).CONFIG_DIR_NAME : ".pi"; -const DEFAULT_STATE_DIR = join(homedir(), ".pi", "agent", "state", "braintrust-pi-extension"); - -const HTTP_URL_SCHEMA = v.pipe( - v.string(), - v.url(), - v.check((value) => { - try { - const protocol = new URL(value).protocol; - return protocol === "http:" || protocol === "https:"; - } catch { - return false; - } - }, "must use http:// or https://"), -); -const JSON_OBJECT_SCHEMA = v.custom( - (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value), - "must be a JSON object", -); -const NON_EMPTY_STRING_SCHEMA = v.pipe(v.string(), v.minLength(1)); -const STRING_SCHEMA = v.string(); - -interface ConfigFileResult { - value?: JsonObject; - error?: string; +function record(value: unknown): ConfigRecord | undefined { + return value !== null && typeof value === "object" && !Array.isArray(value) + ? (value as ConfigRecord) + : undefined; } -interface ApplyConfigResult { - parentSpanConfigured: boolean; - rootSpanConfigured: boolean; +function nonEmptyString(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value : undefined; } -function readConfigFile(path: string): ConfigFileResult { - if (!existsSync(path)) return {}; - - try { - const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown; - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { - return { - error: "expected a JSON object", - }; - } - return { value: parsed as JsonObject }; - } catch (error) { - return { - error: error instanceof Error ? error.message : String(error), - }; - } -} - -function pushConfigIssue( - issues: ConfigIssue[], - path: string, - message: string, - severity: ConfigIssue["severity"] = "error", -): void { - issues.push({ path, message, severity }); -} - -function validateOptionalString( - value: unknown, - issues: ConfigIssue[], - path: string, - key: string, -): string | undefined { - if (value === undefined) return undefined; - const parsed = v.safeParse(STRING_SCHEMA, value); - if (!parsed.success) { - pushConfigIssue(issues, path, `${key} must be a string`); - return undefined; - } - return parsed.output; -} - -function validateOptionalNonEmptyString( - value: unknown, - issues: ConfigIssue[], - path: string, - key: string, -): string | undefined { - if (value === undefined) return undefined; - const parsed = v.safeParse(NON_EMPTY_STRING_SCHEMA, value); - if (!parsed.success) { - pushConfigIssue(issues, path, `${key} must be a non-empty string`); - return undefined; - } - return parsed.output; -} - -function validateOptionalBoolean( - value: unknown, - issues: ConfigIssue[], - path: string, - key: string, -): boolean | undefined { - if (value === undefined) return undefined; +function boolean(value: unknown): boolean | undefined { if (typeof value === "boolean") return value; - - if (typeof value === "string" || typeof value === "number" || typeof value === "bigint") { - const normalized = String(value).trim().toLowerCase(); - if (["true", "1", "yes", "y", "on"].includes(normalized)) return true; - if (["false", "0", "no", "n", "off"].includes(normalized)) return false; + if (typeof value !== "string" && typeof value !== "number") return undefined; + switch (String(value).trim().toLowerCase()) { + case "1": + case "true": + case "yes": + case "on": + return true; + case "0": + case "false": + case "no": + case "off": + return false; + default: + return undefined; } - - pushConfigIssue(issues, path, `${key} must be a boolean`); - return undefined; } -function validateOptionalUrl( - value: unknown, - issues: ConfigIssue[], - path: string, - key: string, -): string | undefined { - if (value === undefined) return undefined; - - const stringValue = validateOptionalString(value, issues, path, key); - if (stringValue === undefined) return undefined; - - const parsed = v.safeParse(HTTP_URL_SCHEMA, stringValue); - if (!parsed.success) { - pushConfigIssue(issues, path, `${key} must be a valid http(s) URL`); +function readConfig(path: string): ConfigRecord | undefined { + if (!existsSync(path)) return undefined; + try { + return record(JSON.parse(readFileSync(path, "utf8"))); + } catch { return undefined; } - - return parsed.output; } -function validateOptionalMetadata( - value: unknown, - issues: ConfigIssue[], - path: string, - key: string, -): JsonObject | undefined { - if (value === undefined) return undefined; - - const parsed = v.safeParse(JSON_OBJECT_SCHEMA, value); - if (!parsed.success) { - pushConfigIssue(issues, path, `${key} must be a JSON object`); - return undefined; - } - - return parsed.output as JsonObject; +function applyConfig(config: PiConfig, source: ConfigRecord | undefined): void { + if (!source) return; + config.profile = nonEmptyString(source.profile) ?? config.profile; + config.orgName = nonEmptyString(source.org_name) ?? config.orgName; + config.projectName = nonEmptyString(source.project) ?? config.projectName; + config.enabled = boolean(source.trace_to_braintrust) ?? config.enabled; + config.additionalMetadata = record(source.additional_metadata) ?? config.additionalMetadata; + config.showUi = boolean(source.show_ui) ?? config.showUi; + config.showTraceLink = boolean(source.show_trace_link) ?? config.showTraceLink; } -function applyConfig( - target: TraceConfig, - source: JsonObject | undefined, - path: string, - issues: ConfigIssue[], -): ApplyConfigResult { - if (!source) { - return { - parentSpanConfigured: false, - rootSpanConfigured: false, - }; +function environmentMetadata(value: string | undefined): ConfigRecord | undefined { + if (!value) return undefined; + try { + return record(JSON.parse(value)); + } catch { + return undefined; } - - const apiKey = validateOptionalString(source.api_key, issues, path, "api_key"); - if (apiKey !== undefined) target.apiKey = apiKey; - - const apiUrl = validateOptionalUrl(source.api_url, issues, path, "api_url"); - if (apiUrl !== undefined) target.apiUrl = apiUrl; - - const appUrl = validateOptionalUrl(source.app_url, issues, path, "app_url"); - if (appUrl !== undefined) target.appUrl = appUrl; - - const profile = validateOptionalNonEmptyString(source.profile, issues, path, "profile"); - if (profile !== undefined) target.profile = profile; - - const orgName = validateOptionalNonEmptyString(source.org_name, issues, path, "org_name"); - if (orgName !== undefined) target.orgName = orgName; - - const projectName = validateOptionalNonEmptyString(source.project, issues, path, "project"); - if (projectName !== undefined) target.projectName = projectName; - - const enabled = validateOptionalBoolean( - source.trace_to_braintrust, - issues, - path, - "trace_to_braintrust", - ); - if (enabled !== undefined) target.enabled = enabled; - - const debug = validateOptionalBoolean(source.debug, issues, path, "debug"); - if (debug !== undefined) target.debug = debug; - - const logFile = validateOptionalString(source.log_file, issues, path, "log_file"); - if (logFile !== undefined) target.logFile = logFile; - - const stateDir = validateOptionalNonEmptyString(source.state_dir, issues, path, "state_dir"); - if (stateDir !== undefined) target.stateDir = stateDir; - - const parentSpanId = validateOptionalNonEmptyString( - source.parent_span_id, - issues, - path, - "parent_span_id", - ); - if (parentSpanId !== undefined) target.parentSpanId = parentSpanId; - - const rootSpanId = validateOptionalNonEmptyString( - source.root_span_id, - issues, - path, - "root_span_id", - ); - if (rootSpanId !== undefined) target.rootSpanId = rootSpanId; - - const additionalMetadata = validateOptionalMetadata( - source.additional_metadata, - issues, - path, - "additional_metadata", - ); - if (additionalMetadata !== undefined) target.additionalMetadata = additionalMetadata; - - const showUi = validateOptionalBoolean(source.show_ui, issues, path, "show_ui"); - if (showUi !== undefined) target.showUi = showUi; - - const showTraceLink = validateOptionalBoolean( - source.show_trace_link, - issues, - path, - "show_trace_link", - ); - if (showTraceLink !== undefined) target.showTraceLink = showTraceLink; - - return { - parentSpanConfigured: parentSpanId !== undefined, - rootSpanConfigured: rootSpanId !== undefined, - }; } -export function loadConfig(cwd = process.cwd()): TraceConfig { - const config: TraceConfig = { +export function loadConfig(cwd = process.cwd()): PiConfig { + const config: PiConfig = { enabled: false, - apiKey: "", - apiUrl: undefined, - appUrl: "https://www.braintrust.dev", - profile: undefined, - orgName: undefined, projectName: "pi", - debug: false, - logFile: undefined, - stateDir: DEFAULT_STATE_DIR, - additionalMetadata: {}, - parentSpanId: undefined, - rootSpanId: undefined, showUi: true, showTraceLink: true, - configIssues: [], }; - const globalConfigPath = join(homedir(), ".pi", "agent", "braintrust.json"); - const projectConfigPath = join(cwd, PROJECT_CONFIG_DIR_NAME, "braintrust.json"); - - let parentSpanConfigured = false; - let rootSpanConfigured = false; - - const globalConfig = readConfigFile(globalConfigPath); - if (globalConfig.error) { - pushConfigIssue(config.configIssues, globalConfigPath, globalConfig.error); - } - { - const applied = applyConfig(config, globalConfig.value, globalConfigPath, config.configIssues); - parentSpanConfigured ||= applied.parentSpanConfigured; - rootSpanConfigured ||= applied.rootSpanConfigured; - } - - const projectConfig = readConfigFile(projectConfigPath); - if (projectConfig.error) { - pushConfigIssue(config.configIssues, projectConfigPath, projectConfig.error); - } - { - const applied = applyConfig( - config, - projectConfig.value, - projectConfigPath, - config.configIssues, - ); - parentSpanConfigured ||= applied.parentSpanConfigured; - rootSpanConfigured ||= applied.rootSpanConfigured; - } - - const envApiKey = validateOptionalString( - process.env.BRAINTRUST_API_KEY, - config.configIssues, - "BRAINTRUST_API_KEY", - "BRAINTRUST_API_KEY", - ); - if (envApiKey !== undefined) config.apiKey = envApiKey; - - const envApiUrl = validateOptionalUrl( - process.env.BRAINTRUST_API_URL, - config.configIssues, - "BRAINTRUST_API_URL", - "BRAINTRUST_API_URL", - ); - if (envApiUrl !== undefined) config.apiUrl = envApiUrl; - - const envAppUrl = validateOptionalUrl( - process.env.BRAINTRUST_APP_URL, - config.configIssues, - "BRAINTRUST_APP_URL", - "BRAINTRUST_APP_URL", - ); - if (envAppUrl !== undefined) config.appUrl = envAppUrl; - - const envProfile = validateOptionalNonEmptyString( - process.env.BRAINTRUST_PROFILE, - config.configIssues, - "BRAINTRUST_PROFILE", - "BRAINTRUST_PROFILE", - ); - if (envProfile !== undefined) config.profile = envProfile; - - const envOrgName = validateOptionalNonEmptyString( - process.env.BRAINTRUST_ORG_NAME, - config.configIssues, - "BRAINTRUST_ORG_NAME", - "BRAINTRUST_ORG_NAME", - ); - if (envOrgName !== undefined) config.orgName = envOrgName; - - const envProjectName = validateOptionalNonEmptyString( - process.env.BRAINTRUST_PROJECT, - config.configIssues, - "BRAINTRUST_PROJECT", - "BRAINTRUST_PROJECT", - ); - if (envProjectName !== undefined) config.projectName = envProjectName; - - const envEnabled = validateOptionalBoolean( - process.env.TRACE_TO_BRAINTRUST, - config.configIssues, - "TRACE_TO_BRAINTRUST", - "TRACE_TO_BRAINTRUST", - ); - if (envEnabled !== undefined) config.enabled = envEnabled; - - const envDebug = validateOptionalBoolean( - process.env.BRAINTRUST_DEBUG, - config.configIssues, - "BRAINTRUST_DEBUG", - "BRAINTRUST_DEBUG", - ); - if (envDebug !== undefined) config.debug = envDebug; - - const envLogFile = validateOptionalString( - process.env.BRAINTRUST_LOG_FILE, - config.configIssues, - "BRAINTRUST_LOG_FILE", - "BRAINTRUST_LOG_FILE", - ); - if (envLogFile !== undefined) config.logFile = envLogFile; - - const envStateDir = validateOptionalNonEmptyString( - process.env.BRAINTRUST_STATE_DIR, - config.configIssues, - "BRAINTRUST_STATE_DIR", - "BRAINTRUST_STATE_DIR", - ); - if (envStateDir !== undefined) config.stateDir = envStateDir; - - const envShowUi = validateOptionalBoolean( - process.env.BRAINTRUST_SHOW_UI, - config.configIssues, - "BRAINTRUST_SHOW_UI", - "BRAINTRUST_SHOW_UI", - ); - if (envShowUi !== undefined) config.showUi = envShowUi; + applyConfig(config, readConfig(join(homedir(), ".pi", "agent", "braintrust.json"))); + applyConfig(config, readConfig(join(cwd, PROJECT_CONFIG_DIR_NAME, "braintrust.json"))); - const envShowTraceLink = validateOptionalBoolean( - process.env.BRAINTRUST_SHOW_TRACE_LINK, - config.configIssues, - "BRAINTRUST_SHOW_TRACE_LINK", - "BRAINTRUST_SHOW_TRACE_LINK", - ); - if (envShowTraceLink !== undefined) config.showTraceLink = envShowTraceLink; + config.profile = nonEmptyString(process.env.BRAINTRUST_PROFILE) ?? config.profile; + config.orgName = nonEmptyString(process.env.BRAINTRUST_ORG_NAME) ?? config.orgName; + config.projectName = nonEmptyString(process.env.BRAINTRUST_PROJECT) ?? config.projectName; + config.enabled = boolean(process.env.TRACE_TO_BRAINTRUST) ?? config.enabled; + config.additionalMetadata = + environmentMetadata(process.env.BRAINTRUST_ADDITIONAL_METADATA) ?? config.additionalMetadata; + config.showUi = boolean(process.env.BRAINTRUST_SHOW_UI) ?? config.showUi; + config.showTraceLink = boolean(process.env.BRAINTRUST_SHOW_TRACE_LINK) ?? config.showTraceLink; - const envParentSpanId = validateOptionalNonEmptyString( - process.env.PI_PARENT_SPAN_ID, - config.configIssues, - "PI_PARENT_SPAN_ID", - "PI_PARENT_SPAN_ID", - ); - if (envParentSpanId !== undefined) { - config.parentSpanId = envParentSpanId; - parentSpanConfigured = true; - } - - const envRootSpanId = validateOptionalNonEmptyString( - process.env.PI_ROOT_SPAN_ID, - config.configIssues, - "PI_ROOT_SPAN_ID", - "PI_ROOT_SPAN_ID", - ); - if (envRootSpanId !== undefined) { - config.rootSpanId = envRootSpanId; - rootSpanConfigured = true; - } - - if (process.env.BRAINTRUST_ADDITIONAL_METADATA !== undefined) { - try { - const parsed = JSON.parse(process.env.BRAINTRUST_ADDITIONAL_METADATA) as unknown; - const metadata = validateOptionalMetadata( - parsed, - config.configIssues, - "BRAINTRUST_ADDITIONAL_METADATA", - "BRAINTRUST_ADDITIONAL_METADATA", - ); - if (metadata !== undefined) config.additionalMetadata = metadata; - } catch (error) { - pushConfigIssue( - config.configIssues, - "BRAINTRUST_ADDITIONAL_METADATA", - error instanceof Error ? error.message : String(error), - ); - } - } - - if (config.parentSpanId && !config.rootSpanId) config.rootSpanId = config.parentSpanId; - if (config.rootSpanId && !config.parentSpanId) config.parentSpanId = config.rootSpanId; - - if ( - parentSpanConfigured && - rootSpanConfigured && - config.parentSpanId && - config.rootSpanId && - config.parentSpanId === config.rootSpanId - ) { - pushConfigIssue( - config.configIssues, - "parent_span_id/root_span_id", - "parent_span_id and root_span_id are identical; set only one unless the parent span is also the trace root", - "warning", - ); - } - - if (config.enabled && !config.apiKey) { - pushConfigIssue( - config.configIssues, - "BRAINTRUST_API_KEY", - "TRACE_TO_BRAINTRUST is enabled but BRAINTRUST_API_KEY is not set", - "warning", - ); - } - - ensureDir(config.stateDir); return config; } - -export function createLogger(config: TraceConfig): Logger { - const explicitLogFile = - config.logFile && config.logFile !== "true" && config.logFile !== "auto" - ? config.logFile - : join(config.stateDir, "braintrust-pi-extension.log"); - const infoLoggingEnabled = config.debug || Boolean(config.logFile); - - let logDirEnsured = false; - - function shouldLog(level: LogLevel): boolean { - return level === "warn" || level === "error" || infoLoggingEnabled; - } - - function ensureLogDir(): void { - if (logDirEnsured) return; - ensureDir(dirname(explicitLogFile)); - logDirEnsured = true; - } - - let pendingWrite = Promise.resolve(); - - function emit(level: LogLevel, message: string, data?: unknown): void { - if (!shouldLog(level)) return; - ensureLogDir(); - pendingWrite = pendingWrite - .catch(() => {}) - .then(async () => { - try { - await writeJsonLog(explicitLogFile, level, message, data); - } catch { - // Logging is best-effort and must never affect pi session execution. - } - }); - } - - return { - filePath: explicitLogFile, - debug(message, data) { - if (config.debug) emit("debug", message, data); - }, - info(message, data) { - emit("info", message, data); - }, - warn(message, data) { - emit("warn", message, data); - }, - error(message, data) { - emit("error", message, data); - }, - async flush() { - await pendingWrite.catch(() => {}); - }, - }; -} diff --git a/src/plugins/pi/content/src/daemon-adapter.test.ts b/src/plugins/pi/content/src/daemon-adapter.test.ts index e2fea68..7e6b4d1 100644 --- a/src/plugins/pi/content/src/daemon-adapter.test.ts +++ b/src/plugins/pi/content/src/daemon-adapter.test.ts @@ -80,6 +80,26 @@ describe("Pi daemon adapter", () => { const { default: extension } = await import("./index.ts"); extension(pi as never); + expect([...handlers.keys()]).toEqual([ + "session_start", + "input", + "before_agent_start", + "context", + "before_provider_request", + "after_provider_response", + "message_update", + "thinking_level_select", + "message_end", + "tool_execution_start", + "tool_execution_end", + "session_before_compact", + "session_compact", + "session_before_tree", + "session_tree", + "agent_end", + "session_shutdown", + ]); + await handlers.get("session_start")?.({ reason: "new" }, ctx); await handlers.get("before_agent_start")?.({ prompt: "hello" }, ctx); await handlers.get("agent_end")?.({ messages: [] }); @@ -100,6 +120,14 @@ describe("Pi daemon adapter", () => { flush_mode: "flush_on_turn_end", additional_metadata: { team: "platform" }, }); + expect(mockState.logs[0]?.payload).toMatchObject({ + event: { reason: "new" }, + extension_version: expect.any(String), + session_file: "/tmp/session.jsonl", + native_session_id: "native-session", + cwd: "/tmp/project", + model: { provider: "openai", id: "gpt-5" }, + }); expect(mockState.flushes).toHaveLength(2); expect( widgets.some((args) => JSON.stringify(args).includes("https://www.braintrust.dev/trace/1")), diff --git a/src/plugins/pi/content/src/index.integration.test.ts b/src/plugins/pi/content/src/index.integration.test.ts deleted file mode 100644 index cd471f2..0000000 --- a/src/plugins/pi/content/src/index.integration.test.ts +++ /dev/null @@ -1,726 +0,0 @@ -import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { - createAssistantMessageEventStream, - type Api, - type AssistantMessage, - type AssistantMessageEventStream, - type Context, - type Model, - type SimpleStreamOptions, - type ToolCall, -} from "@earendil-works/pi-ai"; -import * as piCodingAgent from "@earendil-works/pi-coding-agent"; -import { - createAgentSession, - DefaultResourceLoader, - SessionManager, - type ExtensionAPI, -} from "@earendil-works/pi-coding-agent"; -import { Type } from "typebox"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import braintrustPiExtension from "./legacy-processor.ts"; - -const mockState = vi.hoisted(() => ({ - startSpans: [] as Array>, - logSpans: [] as Array>, - endSpans: [] as Array>, - updateSpans: [] as Array>, - initializeCalls: 0, - flushCalls: 0, - failInitialize: false, -})); - -vi.mock("./client.ts", () => { - class MockBraintrustClient { - async initialize(): Promise { - mockState.initializeCalls += 1; - if (mockState.failInitialize) { - throw new Error("simulated Braintrust init failure"); - } - } - - startSpan(args: Record): Record { - mockState.startSpans.push(args); - return { - id: `record-${String(args.spanId)}`, - spanId: args.spanId, - rootSpanId: args.rootSpanId, - }; - } - - logSpan(span: Record | undefined, event: Record): void { - mockState.logSpans.push({ span, event }); - } - - endSpan(span: Record | undefined, endedAt?: number): void { - mockState.endSpans.push({ span, endedAt }); - } - - getSpanLink(span: Record | undefined): string | undefined { - if (!span) return undefined; - return `https://www.braintrust.dev/app/test-org/p/pi/logs?oid=${String(span.id)}`; - } - - async getSpanPermalink(span: Record | undefined): Promise { - return this.getSpanLink(span); - } - - updateSpan(args: Record): void { - mockState.updateSpans.push(args); - } - - async flush(): Promise { - mockState.flushCalls += 1; - } - } - - return { - BraintrustClient: MockBraintrustClient, - }; -}); - -const ENV_KEYS = [ - "HOME", - "TRACE_TO_BRAINTRUST", - "BRAINTRUST_API_KEY", - "BRAINTRUST_STATE_DIR", - "BRAINTRUST_PROJECT", - "BRAINTRUST_ORG_NAME", - "BRAINTRUST_ADDITIONAL_METADATA", -] as const; - -const originalEnv = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]])); -const originalProcessCwd = process.cwd(); -const tempDirs: string[] = []; - -const TEST_API = "pi-extension-test-api" as Api; - -const TEST_MODEL: Model = { - id: "pi-extension-test-model", - name: "PI Extension Test Model", - api: TEST_API, - provider: "pi-extension-test-provider", - baseUrl: "https://example.invalid", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32_000, - maxTokens: 4_096, -}; - -beforeEach(() => { - mockState.startSpans.length = 0; - mockState.logSpans.length = 0; - mockState.endSpans.length = 0; - mockState.updateSpans.length = 0; - mockState.initializeCalls = 0; - mockState.flushCalls = 0; - mockState.failInitialize = false; - - for (const key of ENV_KEYS) { - delete process.env[key]; - } -}); - -afterEach(() => { - for (const key of ENV_KEYS) { - const value = originalEnv[key]; - if (value === undefined) delete process.env[key]; - else process.env[key] = value; - } - - process.chdir(originalProcessCwd); - - while (tempDirs.length > 0) { - rmSync(tempDirs.pop()!, { recursive: true, force: true }); - } -}); - -function makeTempDir(prefix: string): string { - const dir = mkdtempSync(join(tmpdir(), prefix)); - tempDirs.push(dir); - return dir; -} - -// Injected by CI (see .github/workflows/ci.yml). When unset (e.g. local dev) we -// assume the currently installed pi is at least as new as any version we branch -// on below. -const PI_COMPAT_VERSION = process.env.PI_COMPAT_VERSION; - -function piCompatAtLeast(target: string): boolean { - if (!PI_COMPAT_VERSION) return true; - const parse = (v: string) => - v - .split("-")[0] - .split(".") - .map((part) => Number.parseInt(part, 10) || 0); - const actual = parse(PI_COMPAT_VERSION); - const wanted = parse(target); - for (let i = 0; i < Math.max(actual.length, wanted.length); i += 1) { - const a = actual[i] ?? 0; - const w = wanted[i] ?? 0; - if (a !== w) return a > w; - } - return true; -} - -function buildAssistantMessage(model: Model): AssistantMessage { - return { - role: "assistant", - content: [], - api: model.api, - provider: model.provider, - model: model.id, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - total: 0, - }, - }, - stopReason: "stop", - timestamp: Date.now(), - }; -} - -function userText(context: Context): string { - for (let index = context.messages.length - 1; index >= 0; index -= 1) { - const message = context.messages[index]; - if (!message || message.role !== "user") continue; - if (typeof message.content === "string") return message.content; - return message.content - .filter((part) => part.type === "text") - .map((part) => part.text) - .join("\n"); - } - return ""; -} - -function hasToolResults(context: Context): boolean { - return context.messages.some((message) => message.role === "toolResult"); -} - -function lastToolResult(context: Context) { - return [...context.messages].reverse().find((message) => message.role === "toolResult"); -} - -function pushText( - stream: AssistantMessageEventStream, - output: AssistantMessage, - text: string, -): void { - output.content.push({ type: "text", text: "" }); - const contentIndex = output.content.length - 1; - stream.push({ type: "text_start", contentIndex, partial: output }); - const block = output.content[contentIndex]; - if (block?.type === "text") { - block.text += text; - } - stream.push({ type: "text_delta", contentIndex, delta: text, partial: output }); - stream.push({ type: "text_end", contentIndex, content: text, partial: output }); -} - -function pushToolCall( - stream: AssistantMessageEventStream, - output: AssistantMessage, - toolCall: ToolCall, -): void { - output.content.push(toolCall); - const contentIndex = output.content.length - 1; - stream.push({ type: "toolcall_start", contentIndex, partial: output }); - stream.push({ - type: "toolcall_delta", - contentIndex, - delta: JSON.stringify(toolCall.arguments), - partial: output, - }); - stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output }); -} - -function streamTestModel( - model: Model, - context: Context, - _options?: SimpleStreamOptions, -): AssistantMessageEventStream { - const stream = createAssistantMessageEventStream(); - - queueMicrotask(() => { - const output = buildAssistantMessage(model); - stream.push({ type: "start", partial: output }); - - const prompt = userText(context); - if (prompt.includes("deferred-tools")) { - const toolResult = lastToolResult(context); - if (toolResult?.role === "toolResult" && toolResult.toolName === "tool_search") { - pushToolCall(stream, output, { - type: "toolCall", - id: "calculator-1", - name: "Calculator", - arguments: { expression: "100 + 500" }, - }); - output.stopReason = "toolUse"; - } else if (toolResult?.role === "toolResult" && toolResult.toolName === "Calculator") { - pushText(stream, output, "600"); - output.stopReason = "stop"; - } else { - pushToolCall(stream, output, { - type: "toolCall", - id: "tool-search-1", - name: "tool_search", - arguments: { query: "calculator" }, - }); - output.stopReason = "toolUse"; - } - stream.push({ type: "done", reason: output.stopReason, message: output }); - stream.end(); - return; - } - - if (hasToolResults(context)) { - pushText(stream, output, "parallel tools finished"); - output.stopReason = "stop"; - stream.push({ type: "done", reason: "stop", message: output }); - stream.end(); - return; - } - - if (prompt.includes("parallel-tools")) { - pushToolCall(stream, output, { - type: "toolCall", - id: "tool-1", - name: "bash", - arguments: { - command: "sleep 0.05; echo slow", - }, - }); - pushToolCall(stream, output, { - type: "toolCall", - id: "tool-2", - name: "bash", - arguments: { - command: "echo fast", - }, - }); - output.stopReason = "toolUse"; - stream.push({ type: "done", reason: "toolUse", message: output }); - stream.end(); - return; - } - - pushText(stream, output, "plain response"); - output.stopReason = "stop"; - stream.push({ type: "done", reason: "stop", message: output }); - stream.end(); - }); - - return stream; -} - -function testHarnessExtension(pi: ExtensionAPI): void { - pi.registerTool({ - name: "Calculator", - label: "Calculator", - description: "Evaluate a simple arithmetic expression.", - parameters: Type.Object({ expression: Type.String() }), - async execute(_toolCallId, params) { - return { - content: [{ type: "text", text: params.expression === "100 + 500" ? "600" : "0" }], - details: {}, - }; - }, - }); - - pi.registerTool({ - name: "tool_search", - label: "Tool Search", - description: "Find and activate tools for a capability.", - parameters: Type.Object({ query: Type.String() }), - async execute() { - const active = pi.getActiveTools(); - if (!active.includes("Calculator")) pi.setActiveTools([...active, "Calculator"]); - return { - content: [{ type: "text", text: "Loaded Calculator" }], - details: { matches: ["Calculator"] }, - }; - }, - }); - - pi.registerProvider("pi-extension-test-provider", { - baseUrl: TEST_MODEL.baseUrl, - apiKey: "pi-extension-test-key", - api: TEST_API, - streamSimple: streamTestModel, - models: [TEST_MODEL], - }); - - pi.on("session_start", () => { - pi.setActiveTools(pi.getActiveTools().filter((name) => name !== "Calculator")); - }); - - pi.registerCommand("test-reload", { - description: "Reload the runtime for integration tests", - handler: async (_args, ctx) => { - await ctx.reload(); - return; - }, - }); -} - -async function waitForAsyncWork(): Promise { - await new Promise((resolve) => setTimeout(resolve, 0)); -} - -interface TestSessionController { - prompt(text: string): Promise; - newSession(): Promise; - switchSession(sessionPath: string): Promise; - fork(entryId: string): Promise<{ cancelled: boolean; selectedText: string }>; - dispose(): Promise; - readonly sessionFile: string | undefined; - readonly sessionManager: SessionManager; -} - -async function createHarness(options?: { - rootDir?: string; - sessionManager?: SessionManager; - sessionMode?: "inMemory" | "persistent"; - sessionFile?: string; - sessionsDir?: string; -}) { - const home = options?.rootDir ?? makeTempDir("pi-extension-home-"); - const cwd = join(home, "workspace"); - const agentDir = join(home, "agent"); - const stateDir = join(home, "state"); - - mkdirSync(cwd, { recursive: true }); - mkdirSync(agentDir, { recursive: true }); - - process.env.HOME = home; - process.env.TRACE_TO_BRAINTRUST = "true"; - process.env.BRAINTRUST_API_KEY = "test-key"; - process.env.BRAINTRUST_PROJECT = "pi"; - process.env.BRAINTRUST_ORG_NAME = "test-org"; - process.env.BRAINTRUST_STATE_DIR = stateDir; - - const sessionManager = - options?.sessionManager ?? - (options?.sessionFile - ? SessionManager.open(options.sessionFile, options.sessionsDir) - : options?.sessionMode === "persistent" - ? SessionManager.create(cwd, options.sessionsDir) - : SessionManager.inMemory(cwd)); - - const compat = piCodingAgent as any; - if ( - typeof compat.createAgentSessionRuntime === "function" && - typeof compat.createAgentSessionServices === "function" && - typeof compat.createAgentSessionFromServices === "function" - ) { - const runtime = await compat.createAgentSessionRuntime( - async ({ - cwd: runtimeCwd, - agentDir: runtimeAgentDir, - sessionManager: runtimeSessionManager, - sessionStartEvent, - }: { - cwd: string; - agentDir: string; - sessionManager: SessionManager; - sessionStartEvent?: unknown; - }) => { - const services = await compat.createAgentSessionServices({ - cwd: runtimeCwd, - agentDir: runtimeAgentDir, - resourceLoaderOptions: { - extensionFactories: [testHarnessExtension, braintrustPiExtension], - }, - }); - - return { - ...(await compat.createAgentSessionFromServices({ - services, - sessionManager: runtimeSessionManager, - sessionStartEvent, - model: TEST_MODEL, - })), - services, - diagnostics: services.diagnostics, - }; - }, - { - cwd, - agentDir, - sessionManager, - }, - ); - - const bindRuntimeSession = async (): Promise => { - await runtime.session.bindExtensions({}); - }; - - await bindRuntimeSession(); - - const session: TestSessionController = { - prompt: (text) => runtime.session.prompt(text), - newSession: async () => { - const result = await runtime.newSession(); - if (!result.cancelled) { - await bindRuntimeSession(); - } - return !result.cancelled; - }, - switchSession: async (sessionPath) => { - const result = await runtime.switchSession(sessionPath); - if (!result.cancelled) { - await bindRuntimeSession(); - } - return !result.cancelled; - }, - fork: async (entryId) => { - const result = await runtime.fork(entryId); - if (!result.cancelled) { - await bindRuntimeSession(); - } - return { - cancelled: result.cancelled, - selectedText: result.selectedText ?? "", - }; - }, - dispose: async () => { - await runtime.dispose(); - }, - get sessionFile() { - return runtime.session.sessionFile; - }, - get sessionManager() { - return runtime.session.sessionManager as SessionManager; - }, - }; - - return { agentDir, cwd, session, stateDir }; - } - - // TODO: Remove this legacy fallback once our supported pi compatibility window no - // longer includes pi <0.65.0, which introduced the session runtime API and the - // session_start-only post-transition model. - const resourceLoader = new DefaultResourceLoader({ - cwd, - agentDir, - extensionFactories: [testHarnessExtension, braintrustPiExtension], - }); - await resourceLoader.reload(); - - const { session: legacySession } = await createAgentSession({ - cwd, - agentDir, - model: TEST_MODEL, - resourceLoader, - sessionManager, - }); - const legacyRuntimeSession = legacySession as typeof legacySession & { - newSession(): Promise; - switchSession(sessionPath: string): Promise; - fork(entryId: string): Promise<{ cancelled: boolean; selectedText?: string }>; - sessionManager: SessionManager; - }; - - const session: TestSessionController = { - prompt: (text) => legacyRuntimeSession.prompt(text), - newSession: () => legacyRuntimeSession.newSession(), - switchSession: (sessionPath) => legacyRuntimeSession.switchSession(sessionPath), - fork: async (entryId) => { - const result = await legacyRuntimeSession.fork(entryId); - return { - cancelled: result.cancelled, - selectedText: result.selectedText ?? "", - }; - }, - dispose: async () => { - legacyRuntimeSession.dispose(); - }, - get sessionFile() { - return legacyRuntimeSession.sessionFile; - }, - get sessionManager() { - return legacyRuntimeSession.sessionManager; - }, - }; - - return { agentDir, cwd, session, stateDir }; -} - -function rootTaskSpans(): Array> { - return mockState.startSpans.filter( - (span) => span.type === "task" && span.parentSpanId === undefined, - ); -} - -describe("braintrustPiExtension integration", () => { - it("restores persisted trace state when reopening the same pi session", async () => { - const rootDir = makeTempDir("pi-extension-home-"); - const sessionsDir = makeTempDir("pi-extension-sessions-"); - const first = await createHarness({ - rootDir, - sessionMode: "persistent", - sessionsDir, - }); - await first.session.prompt("create a traced turn"); - const firstSessionFile = first.session.sessionFile; - - expect(firstSessionFile).toBeTruthy(); - expect(rootTaskSpans()).toHaveLength(1); - - await first.session.dispose(); - await waitForAsyncWork(); - - const reopened = await createHarness({ - rootDir, - sessionFile: firstSessionFile!, - sessionsDir, - }); - await reopened.session.prompt("resume the same traced session"); - await reopened.session.dispose(); - await waitForAsyncWork(); - - expect(rootTaskSpans()).toHaveLength(1); - expect( - mockState.startSpans.filter( - (span) => span.type === "task" && span.parentSpanId !== undefined, - ), - ).toHaveLength(2); - }); - - it("keeps one root span across session switch, fork, and resume flows", async () => { - const sessionsDir = makeTempDir("pi-extension-sessions-"); - const { session } = await createHarness({ - sessionMode: "persistent", - sessionsDir, - }); - - await session.prompt("session a"); - const sessionAFile = session.sessionFile; - - const switched = await session.newSession(); - expect(switched).toBe(true); - await session.prompt("session b"); - const sessionBFile = session.sessionFile; - - const forkEntryId = session.sessionManager - .getBranch() - .find((entry) => entry.type === "message")?.id; - expect(forkEntryId).toBeTruthy(); - const forked = await session.fork(forkEntryId!); - expect(forked.cancelled).toBe(false); - await session.prompt("session c from fork"); - const sessionCFile = session.sessionFile; - - expect(sessionAFile).toBeTruthy(); - expect(sessionBFile).toBeTruthy(); - expect(sessionCFile).toBeTruthy(); - expect(sessionCFile).not.toBe(sessionBFile); - - const resumed = await session.switchSession(sessionAFile!); - expect(resumed).toBe(true); - await session.prompt("back on session a"); - - await session.dispose(); - await waitForAsyncWork(); - - expect(rootTaskSpans()).toHaveLength(3); - expect( - rootTaskSpans().find( - (span) => - (span.metadata as Record | undefined)?.opened_via === "session_fork", - ), - ).toMatchObject({ - metadata: { - opened_via: "session_fork", - parent_session_file: sessionBFile, - }, - }); - }); - - it("preserves pi's parallel tool end ordering when creating tool spans", async () => { - const { session } = await createHarness(); - - await session.prompt("parallel-tools"); - await session.dispose(); - await waitForAsyncWork(); - - const llmSpans = mockState.startSpans.filter((span) => span.type === "llm"); - const toolSpans = mockState.startSpans.filter((span) => span.type === "tool"); - const firstLlmSpanId = llmSpans[0]?.spanId; - - expect(toolSpans).toHaveLength(2); - // pi < 0.68.1 emits `tool_execution_end` in assistant source order, so the - // extension logs tool spans as [tool-1, tool-2]. Starting with pi 0.68.1 the - // agent emits parallel tool completions eagerly (completion order), so the - // fast `tool-2` finishes before the slow `tool-1` and spans are logged as - // [tool-2, tool-1]. See pi-coding-agent changelog 0.68.1 / issue #3503. - // TODO: drop the pi < 0.68.1 branch once we stop testing against it. - const expectedToolCallIdOrder = piCompatAtLeast("0.68.1") - ? ["tool-2", "tool-1"] - : ["tool-1", "tool-2"]; - expect( - toolSpans.map((span) => (span.metadata as Record | undefined)?.tool_call_id), - ).toEqual(expectedToolCallIdOrder); - expect(toolSpans.map((span) => span.parentSpanId)).toEqual([firstLlmSpanId, firstLlmSpanId]); - - const firstLlmLog = mockState.logSpans.find( - (entry) => (entry.span as { spanId?: unknown } | undefined)?.spanId === firstLlmSpanId, - ); - const firstLlmEvent = firstLlmLog?.event as Record | undefined; - const firstLlmMetrics = firstLlmEvent?.metrics as Record | undefined; - expect(firstLlmMetrics?.time_to_first_token).toEqual(expect.any(Number)); - }); - - it("traces tools activated during a deferred tool-loading turn", async () => { - const { session } = await createHarness(); - - await session.prompt("deferred-tools"); - await session.dispose(); - await waitForAsyncWork(); - - const toolSpans = mockState.startSpans.filter((span) => span.type === "tool"); - expect(toolSpans.map((span) => span.name)).toEqual(["tool_search", "Calculator"]); - - if (piCompatAtLeast("0.80.7")) { - expect(toolSpans[0]?.metadata).toMatchObject({ - activated_tools: ["Calculator"], - activated_tool_count: 1, - }); - const llmSpans = mockState.startSpans.filter((span) => span.type === "llm"); - expect(llmSpans[1]?.metadata).toMatchObject({ - activated_tools: ["Calculator"], - }); - } - }); - - it("stops tracing new work after Braintrust initialization fails", async () => { - mockState.failInitialize = true; - - const { session } = await createHarness(); - await waitForAsyncWork(); - await session.prompt("plain-response"); - await session.dispose(); - await waitForAsyncWork(); - - expect(mockState.initializeCalls).toBe(1); - expect(mockState.startSpans).toEqual([]); - expect(mockState.flushCalls).toBe(0); - }); -}); diff --git a/src/plugins/pi/content/src/index.test.ts b/src/plugins/pi/content/src/index.test.ts deleted file mode 100644 index 9b41f8d..0000000 --- a/src/plugins/pi/content/src/index.test.ts +++ /dev/null @@ -1,1230 +0,0 @@ -import { readFileSync } from "node:fs"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const packageVersion = (JSON.parse(readFileSync("package.json", "utf8")) as { version: string }) - .version; - -const mockState = vi.hoisted(() => ({ - startSpans: [] as Array>, - logSpans: [] as Array>, - endSpans: [] as Array>, - updateSpans: [] as Array>, - statuses: [] as Array<{ key: string; text: string | undefined }>, - widgets: [] as Array<{ key: string; content: string[] | undefined }>, - initializeCalls: 0, - flushCalls: 0, - config: { - enabled: true, - apiKey: "test-key", - apiUrl: undefined, - appUrl: "https://www.braintrust.dev", - orgName: undefined, - projectName: "pi", - debug: false, - logFile: undefined, - stateDir: "/tmp/braintrust-pi-extension-test", - additionalMetadata: {}, - parentSpanId: undefined, - rootSpanId: undefined, - showUi: true, - showTraceLink: true, - configIssues: [] as Array<{ path: string; message: string; severity: "error" | "warning" }>, - }, -})); - -vi.mock("./client.ts", () => { - class MockBraintrustClient { - async initialize(): Promise { - mockState.initializeCalls += 1; - } - - startSpan(args: Record): Record { - mockState.startSpans.push(args); - return { - id: `record-${String(args.spanId)}`, - spanId: args.spanId, - rootSpanId: args.rootSpanId, - }; - } - - logSpan(span: Record | undefined, event: Record): void { - mockState.logSpans.push({ span, event }); - } - - endSpan(span: Record | undefined, endedAt?: number): void { - mockState.endSpans.push({ span, endedAt }); - } - - getSpanLink(span: Record | undefined): string | undefined { - if (!span) return undefined; - return "https://www.braintrust.dev/app/test-org/p/pi/logs?oid=trace-row-1"; - } - - async getSpanPermalink(span: Record | undefined): Promise { - return this.getSpanLink(span); - } - - updateSpan(args: Record): void { - mockState.updateSpans.push(args); - } - - async flush(): Promise { - mockState.flushCalls += 1; - } - } - - return { - BraintrustClient: MockBraintrustClient, - }; -}); - -vi.mock("./config.ts", () => ({ - loadConfig: () => ({ ...mockState.config }), - createLogger: () => ({ - filePath: "/tmp/braintrust-pi-extension-test.log", - debug: () => {}, - info: () => {}, - warn: () => {}, - error: () => {}, - flush: async () => {}, - }), -})); - -vi.mock("./state.ts", () => ({ - createStateStore: () => ({ - get: () => undefined, - set: () => undefined, - patch: () => undefined, - delete: () => undefined, - schedulePersist: () => undefined, - flush: async () => {}, - }), -})); - -beforeEach(() => { - mockState.startSpans.length = 0; - mockState.logSpans.length = 0; - mockState.endSpans.length = 0; - mockState.updateSpans.length = 0; - mockState.statuses.length = 0; - mockState.widgets.length = 0; - mockState.initializeCalls = 0; - mockState.flushCalls = 0; - mockState.config = { - enabled: true, - apiKey: "test-key", - apiUrl: undefined, - appUrl: "https://www.braintrust.dev", - orgName: undefined, - projectName: "pi", - debug: false, - logFile: undefined, - stateDir: "/tmp/braintrust-pi-extension-test", - additionalMetadata: {}, - parentSpanId: undefined, - rootSpanId: undefined, - showUi: true, - showTraceLink: true, - configIssues: [], - }; - vi.resetModules(); -}); - -async function createHarness() { - const handlers = new Map unknown>(); - const { default: braintrustPiExtension } = await import("./legacy-processor.ts"); - - braintrustPiExtension({ - on(eventName: string, handler: (...args: unknown[]) => unknown) { - handlers.set(eventName, handler); - }, - getActiveTools() { - return ["read", "bash", "tool_search", "Calculator"]; - }, - } as never); - - const ctx = { - cwd: "/tmp/workspace", - hasUI: true, - model: "anthropic/claude-sonnet-4", - ui: { - theme: { - fg: (_color: string, text: string) => text, - underline: (text: string) => text, - }, - setStatus: (key: string, text: string | undefined) => { - mockState.statuses.push({ key, text }); - }, - setWidget: (key: string, content: string[] | undefined, _options?: unknown) => { - mockState.widgets.push({ key, content }); - }, - }, - sessionManager: { - getSessionFile: () => "/tmp/session.json", - getSessionId: () => "session-1", - getSessionName: () => "Checkout fix", - }, - mode: "json", - }; - - async function emit(eventName: string, event: Record = {}): Promise { - const handler = handlers.get(eventName); - if (!handler) throw new Error(`No handler registered for ${eventName}`); - await handler(event, ctx); - } - - return { emit, handlers, ctx }; -} - -describe("braintrustPiExtension", () => { - it("uses session_start reasons instead of legacy session transition events", async () => { - const { handlers } = await createHarness(); - - expect(handlers.has("session_switch")).toBe(false); - expect(handlers.has("session_fork")).toBe(false); - }); - - it("shows a trace url only after the session produces a turn", async () => { - const { emit } = await createHarness(); - - await emit("session_start"); - - expect(mockState.statuses[0]).toEqual({ - key: "braintrust-tracing", - text: "Braintrust tracing pi", - }); - expect(mockState.startSpans).toEqual([]); - expect(mockState.widgets.at(-1)).toEqual({ - key: "braintrust-trace-link", - content: undefined, - }); - - await emit("before_agent_start", { - prompt: "Inspect the package", - images: [], - }); - - expect(mockState.widgets.at(-1)?.key).toBe("braintrust-trace-link"); - expect(mockState.widgets.at(-1)?.content?.[0]).toContain("Braintrust trace ↗"); - expect(mockState.widgets.at(-1)?.content?.[1]).toBe( - "braintrust.dev/app/test-org/p/pi/logs?oid=trace-row-1", - ); - expect(mockState.startSpans[0]?.metadata).toMatchObject({ extension_version: packageVersion }); - - await emit("session_shutdown"); - - expect(mockState.statuses.at(-1)).toEqual({ - key: "braintrust-tracing", - text: undefined, - }); - expect(mockState.widgets.at(-1)).toEqual({ - key: "braintrust-trace-link", - content: undefined, - }); - }); - - it("surfaces malformed Braintrust config in the UI", async () => { - mockState.config.configIssues = [ - { - path: "/Users/test/.pi/agent/braintrust.json", - message: "Expected double-quoted property name in JSON at position 42", - severity: "error", - }, - ]; - - const { emit } = await createHarness(); - - await emit("session_start"); - - expect(mockState.statuses[0]).toEqual({ - key: "braintrust-tracing", - text: "Braintrust tracing pi (config warning)", - }); - expect(mockState.widgets.at(-1)?.key).toBe("braintrust-trace-link"); - expect(mockState.widgets.at(-1)?.content).toContain("Braintrust config error"); - expect(mockState.widgets.at(-1)?.content?.[1]).toContain(".pi/agent/braintrust.json"); - }); - - it("does not create a root span for an idle session", async () => { - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("session_shutdown"); - - expect(mockState.startSpans).toEqual([]); - expect(mockState.endSpans).toEqual([]); - expect(mockState.updateSpans).toEqual([]); - }); - - it("annotates root spans with pi version, mode, and session name metadata", async () => { - const { emit } = await createHarness(); - - await emit("before_agent_start", { - prompt: "Inspect the package", - images: [], - }); - - expect(mockState.startSpans[0]).toMatchObject({ - name: "Checkout fix", - type: "task", - metadata: { - source: "pi", - extension_version: packageVersion, - pi_version: expect.any(String), - pi_mode: "json", - session_name: "Checkout fix", - }, - }); - }); - - it("annotates turn spans with idle input event metadata", async () => { - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("input", { - text: "/skill:review inspect this", - images: [{ type: "image", source: "screen.png" }], - source: "interactive", - }); - await emit("before_agent_start", { - prompt: "Expanded skill content", - images: [], - }); - - const turnSpan = mockState.startSpans.find( - (span) => span.type === "task" && span.name === "Turn 1", - ); - - expect(turnSpan?.metadata).toMatchObject({ - input_source: "interactive", - input_streaming_behavior: "idle", - input_image_count: 1, - raw_input: "/skill:review inspect this", - }); - }); - - it("annotates turn spans with follow-up input event metadata", async () => { - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("input", { - text: "queue this next", - images: [], - source: "rpc", - streamingBehavior: "followUp", - }); - await emit("before_agent_start", { - prompt: "queue this next", - images: [], - }); - - const turnSpan = mockState.startSpans.find( - (span) => span.type === "task" && span.name === "Turn 1", - ); - - expect(turnSpan?.metadata).toMatchObject({ - input_source: "rpc", - input_streaming_behavior: "followUp", - input_image_count: 0, - raw_input: "queue this next", - }); - }); - - it("records resolved model, thinking level, and provider response metadata on llm spans", async () => { - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("thinking_level_select", { level: "high", previousLevel: "off" }); - await emit("before_agent_start", { - prompt: "Use a routed model", - images: [], - }); - await emit("context", { messages: [{ role: "user", content: "Use a routed model" }] }); - await emit("after_provider_response", { - status: 200, - headers: { - "x-ratelimit-remaining-requests": "42", - "retry-after": "5", - "x-request-id": "request-123", - authorization: "secret", - }, - }); - await emit("message_end", { - message: { - role: "assistant", - provider: "openrouter", - model: "auto", - responseModel: "anthropic/claude-sonnet-4-5", - timestamp: 1_700_000_000_000, - content: [{ type: "text", text: "Done." }], - }, - }); - - const turnSpan = mockState.startSpans.find( - (span) => span.type === "task" && span.name === "Turn 1", - ); - const llmSpan = mockState.startSpans.find((span) => span.type === "llm"); - - expect(turnSpan?.metadata).toMatchObject({ thinking_level: "high" }); - expect(llmSpan).toMatchObject({ name: "anthropic/claude-sonnet-4-5" }); - expect(llmSpan?.metadata).toMatchObject({ - model: "anthropic/claude-sonnet-4-5", - requested_model: "auto", - response_model: "anthropic/claude-sonnet-4-5", - thinking_level: "high", - provider_response_status: 200, - provider_response_headers: { - "x-ratelimit-remaining-requests": "42", - "retry-after": "5", - "x-request-id": "request-123", - }, - }); - const llmMetadata = llmSpan?.metadata as - | { provider_response_headers?: Record } - | undefined; - expect(llmMetadata?.provider_response_headers?.authorization).toBeUndefined(); - }); - - it("records canonical usage, cost, effective thinking, and streaming metrics", async () => { - vi.useFakeTimers(); - try { - const { emit, ctx } = await createHarness(); - (ctx as { model: unknown }).model = { - provider: "kimi-coding", - id: "kimi-k3", - name: "Kimi K3", - api: "anthropic-messages", - reasoning: true, - contextWindow: 262_144, - maxTokens: 65_536, - thinkingLevelMap: { max: "max", high: null }, - compat: { deferredToolsMode: "kimi" }, - }; - - vi.setSystemTime(1_000); - await emit("before_agent_start", { prompt: "Think carefully", images: [] }); - vi.setSystemTime(2_000); - await emit("context", { messages: [{ role: "user", content: "Think carefully" }] }); - await emit("before_provider_request", { - payload: { - model: "kimi-k3", - max_tokens: 65_536, - thinking: { type: "adaptive" }, - output_config: { effort: "max" }, - tools: [{ name: "bash", defer_loading: true }], - messages: [{ role: "assistant", signature: "provider-secret" }], - }, - }); - vi.setSystemTime(2_250); - await emit("message_update", { assistantMessageEvent: { type: "thinking_delta" } }); - vi.setSystemTime(2_750); - await emit("message_update", { assistantMessageEvent: { type: "text_delta" } }); - vi.setSystemTime(3_000); - await emit("message_end", { - message: { - role: "assistant", - api: "anthropic-messages", - provider: "kimi-coding", - model: "kimi-k3", - responseId: "response-1", - content: [ - { type: "thinking", thinking: "A short plan", thinkingSignature: "opaque" }, - { type: "text", text: "Done." }, - ], - usage: { - input: 100, - output: 30, - cacheRead: 20, - cacheWrite: 15, - cacheWrite1h: 10, - reasoning: 12, - totalTokens: 165, - cost: { total: 0.0042 }, - }, - stopReason: "stop", - }, - }); - - const llmSpan = mockState.startSpans.find((span) => span.type === "llm"); - expect(llmSpan?.metadata).toMatchObject({ - provider: "kimi-coding", - model: "kimi-k3", - "pi_coding_agent.api": "anthropic-messages", - "pi_coding_agent.model_name": "Kimi K3", - "pi_coding_agent.response_id": "response-1", - model_supports_reasoning: true, - model_context_window: 262_144, - model_max_tokens: 65_536, - supported_thinking_levels: ["max"], - deferred_tools_mode: "kimi", - provider_request_model: "kimi-k3", - provider_request_max_tokens: 65_536, - effective_thinking_type: "adaptive", - effective_thinking_effort: "max", - effective_thinking_uses_token_budget: false, - effective_thinking_enabled: true, - provider_request_tool_count: 1, - provider_request_deferred_tool_count: 1, - "pi_coding_agent.time_to_first_thinking": 0.25, - "pi_coding_agent.time_to_first_text": 0.75, - "pi_coding_agent.thinking_duration": 0.5, - thinking_block_count: 1, - empty_thinking_block_count: 0, - active_tool_count: 4, - }); - expect(JSON.stringify(llmSpan?.metadata)).not.toContain("provider-secret"); - expect(JSON.stringify(llmSpan?.metadata)).not.toContain("opaque"); - - const llmLog = mockState.logSpans.find( - (entry) => (entry.span as { spanId?: unknown } | undefined)?.spanId === llmSpan?.spanId, - ); - const llmEvent = llmLog?.event as Record | undefined; - const llmMetrics = llmEvent?.metrics as Record | undefined; - expect(JSON.stringify(llmEvent)).not.toContain("opaque"); - expect(llmMetrics).toEqual({ - prompt_tokens: 135, - completion_tokens: 30, - tokens: 165, - prompt_cached_tokens: 20, - completion_reasoning_tokens: 12, - estimated_cost: 0.0042, - prompt_cache_creation_1h_tokens: 10, - prompt_cache_creation_5m_tokens: 5, - time_to_first_token: 0.25, - }); - expect(llmMetrics?.prompt_cache_creation_tokens).toBe(undefined); - } finally { - vi.useRealTimers(); - } - }); - - it("records tools activated through deferred tool loading", async () => { - const { emit, ctx } = await createHarness(); - (ctx as { model: unknown }).model = { - provider: "kimi-coding", - id: "kimi-k3", - compat: { deferredToolsMode: "kimi" }, - }; - - await emit("before_agent_start", { prompt: "Use a calculator", images: [] }); - await emit("message_end", { - message: { - role: "assistant", - provider: "kimi-coding", - model: "kimi-k3", - content: [ - { - type: "toolCall", - id: "tool-search-1", - name: "tool_search", - arguments: { query: "calculator" }, - }, - ], - }, - }); - await emit("tool_execution_start", { - toolCallId: "tool-search-1", - toolName: "tool_search", - args: { query: "calculator" }, - }); - await emit("tool_execution_end", { - toolCallId: "tool-search-1", - toolName: "tool_search", - isError: false, - result: { - content: [{ type: "text", text: "Loaded Calculator" }], - addedToolNames: ["Calculator", "Calculator"], - }, - }); - - const loaderSpan = mockState.startSpans.find((span) => span.name === "tool_search"); - expect(loaderSpan?.metadata).toMatchObject({ - activated_tools: ["Calculator"], - activated_tool_count: 1, - active_tool_count: 4, - deferred_tools_mode: "kimi", - "pi_coding_agent.active_tools": ["read", "bash", "tool_search", "Calculator"], - }); - - await emit("context", { - messages: [{ role: "toolResult", toolName: "tool_search", content: "Loaded" }], - }); - await emit("message_end", { - message: { - role: "assistant", - provider: "kimi-coding", - model: "kimi-k3", - content: [{ type: "text", text: "The tool is ready." }], - }, - }); - - const llmSpans = mockState.startSpans.filter((span) => span.type === "llm"); - expect(llmSpans.at(-1)?.metadata).toMatchObject({ - activated_tools: ["Calculator"], - active_tool_count: 4, - }); - }); - - it("traces session compaction as a task span", async () => { - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("session_before_compact", { - customInstructions: "Keep debugging context", - branchEntries: [{ id: "entry-1" }, { id: "entry-2" }], - reason: "overflow", - willRetry: true, - preparation: { - messages: [{ role: "user", content: "long context" }], - tokensBefore: 120_000, - }, - }); - await emit("session_compact", { - fromExtension: true, - reason: "overflow", - willRetry: true, - compactionEntry: { - id: "compact-1", - summary: "Short summary", - tokensBefore: 120_000, - estimatedTokensAfter: 24_000, - }, - }); - - const rootSpan = mockState.startSpans.find( - (span) => span.type === "task" && span.parentSpanId === undefined, - ); - const compactionSpan = mockState.startSpans.find((span) => span.name === "Compaction"); - - expect(rootSpan).toBeDefined(); - expect(compactionSpan).toMatchObject({ - type: "task", - parentSpanId: rootSpan?.spanId, - input: { - custom_instructions: "Keep debugging context", - branch_entry_count: 2, - preparation: { - messages: [{ role: "user", content: "long context" }], - }, - }, - metadata: { - event_type: "session_before_compact", - compaction_reason: "overflow", - will_retry: true, - tokens_before: 120_000, - }, - }); - const compactionLog = mockState.logSpans.find( - (entry) => - (entry.span as { spanId?: unknown } | undefined)?.spanId === compactionSpan?.spanId, - ); - expect(compactionLog?.event).toEqual({ - output: { - id: "compact-1", - summary: "Short summary", - tokensBefore: 120_000, - estimatedTokensAfter: 24_000, - }, - metadata: { - event_type: "session_compact", - from_extension: true, - compaction_reason: "overflow", - will_retry: true, - tokens_before: 120_000, - estimated_tokens_after: 24_000, - }, - }); - expect( - mockState.endSpans.some( - (entry) => - (entry.span as { spanId?: unknown } | undefined)?.spanId === compactionSpan?.spanId, - ), - ).toBe(true); - }); - - it("traces branch summary events as a task span", async () => { - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("session_before_tree", { - preparation: { - targetId: "target-entry", - oldLeafId: "old-leaf", - commonAncestorId: "ancestor", - entriesToSummarize: [{ id: "entry-1" }, { id: "entry-2" }], - userWantsSummary: true, - customInstructions: "Focus on the debugging branch", - replaceInstructions: false, - label: "debug-branch", - }, - }); - await emit("session_tree", { - newLeafId: "target-entry", - oldLeafId: "old-leaf", - fromExtension: false, - summaryEntry: { - id: "summary-1", - summary: "Debugging branch summary", - fromId: "old-leaf", - details: { modifiedFiles: ["src/index.ts"] }, - }, - }); - - const rootSpan = mockState.startSpans.find( - (span) => span.type === "task" && span.parentSpanId === undefined, - ); - const branchSummarySpan = mockState.startSpans.find((span) => span.name === "Branch Summary"); - - expect(branchSummarySpan).toMatchObject({ - type: "task", - parentSpanId: rootSpan?.spanId, - input: { - target_id: "target-entry", - old_leaf_id: "old-leaf", - common_ancestor_id: "ancestor", - entries_to_summarize: 2, - user_wants_summary: true, - custom_instructions: "Focus on the debugging branch", - replace_instructions: false, - label: "debug-branch", - }, - metadata: { - event_type: "session_before_tree", - user_wants_summary: true, - }, - }); - - const branchSummaryLog = mockState.logSpans.find( - (entry) => - (entry.span as { spanId?: unknown } | undefined)?.spanId === branchSummarySpan?.spanId, - ); - expect(branchSummaryLog?.event).toEqual({ - output: { - id: "summary-1", - summary: "Debugging branch summary", - fromId: "old-leaf", - details: { modifiedFiles: ["src/index.ts"] }, - }, - metadata: { - event_type: "session_tree", - from_extension: false, - new_leaf_id: "target-entry", - old_leaf_id: "old-leaf", - }, - }); - expect( - mockState.endSpans.some( - (entry) => - (entry.span as { spanId?: unknown } | undefined)?.spanId === branchSummarySpan?.spanId, - ), - ).toBe(true); - }); - - it("parents tool spans under the llm span that emitted the matching tool call", async () => { - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("before_agent_start", { - prompt: "Inspect the package", - images: [], - }); - await emit("message_end", { - message: { - role: "assistant", - provider: "anthropic", - model: "claude-sonnet-4", - timestamp: 1_700_000_000_000, - content: [ - { type: "text", text: "I'll inspect package.json." }, - { - type: "toolCall", - id: "tool-1", - name: "read", - arguments: { path: "package.json" }, - }, - ], - }, - }); - await emit("tool_execution_start", { - toolCallId: "tool-1", - toolName: "read", - args: { path: "package.json" }, - }); - await emit("tool_execution_end", { - toolCallId: "tool-1", - toolName: "read", - isError: false, - result: { - content: [{ type: "text", text: '{"name":"@braintrust/pi-extension"}' }], - }, - }); - - const llmSpan = mockState.startSpans.find((span) => span.type === "llm"); - const toolSpan = mockState.startSpans.find((span) => span.type === "tool"); - - expect(llmSpan).toBeDefined(); - expect(toolSpan).toBeDefined(); - expect(toolSpan?.parentSpanId).toBe(llmSpan?.spanId); - expect(toolSpan?.metadata).toMatchObject({ - tool_name: "read", - tool_call_id: "tool-1", - tool_approval: "approved", - parent_llm_span_id: llmSpan?.spanId, - }); - }); - - it("marks failed tool spans with approval metadata and an error", async () => { - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("before_agent_start", { - prompt: "Run a command", - images: [], - }); - await emit("tool_execution_start", { - toolCallId: "tool-error", - toolName: "bash", - args: { command: "exit 1" }, - }); - await emit("tool_execution_end", { - toolCallId: "tool-error", - toolName: "bash", - isError: true, - result: { - content: [{ type: "text", text: "Exit code 1" }], - }, - }); - - const toolSpan = mockState.startSpans.find((span) => span.type === "tool"); - const toolLog = mockState.logSpans.find( - (entry) => (entry.span as { spanId?: unknown } | undefined)?.spanId === toolSpan?.spanId, - ); - - expect(toolSpan?.metadata).toMatchObject({ - tool_name: "bash", - tool_call_id: "tool-error", - is_error: true, - tool_approval: "approved", - }); - expect((toolLog?.event as { error?: unknown } | undefined)?.error).toBe("Exit code 1"); - }); - - it("normalizes SKILL.md reads as skill tool spans", async () => { - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("before_agent_start", { - prompt: "Use the review skill", - images: [], - }); - await emit("message_end", { - message: { - role: "assistant", - provider: "anthropic", - model: "claude-sonnet-4", - timestamp: 1_700_000_000_000, - content: [ - { - type: "toolCall", - id: "tool-skill", - name: "read", - arguments: { path: "/home/user/.agents/skills/review/SKILL.md" }, - }, - ], - }, - }); - await emit("tool_execution_start", { - toolCallId: "tool-skill", - toolName: "read", - args: { path: "/home/user/.agents/skills/review/SKILL.md" }, - }); - await emit("tool_execution_end", { - toolCallId: "tool-skill", - toolName: "read", - isError: false, - result: { content: [{ type: "text", text: "---\nname: review\n---" }] }, - }); - - const skillSpan = mockState.startSpans.find((span) => span.name === "skill: review"); - expect(skillSpan).toMatchObject({ - type: "tool", - metadata: { - tool_name: "skill", - original_tool_name: "read", - tool_call_id: "tool-skill", - skill_name: "review", - skill_path: "/home/user/.agents/skills/review/SKILL.md", - }, - }); - }); - - it("marks matching explicit /skill loads on turn and skill spans", async () => { - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("input", { - text: "/skill:review inspect this", - images: [], - source: "interactive", - }); - await emit("before_agent_start", { - prompt: "Expanded skill content", - images: [], - }); - await emit("message_end", { - message: { - role: "assistant", - provider: "anthropic", - model: "claude-sonnet-4", - timestamp: 1_700_000_000_000, - content: [ - { - type: "toolCall", - id: "tool-skill", - name: "read", - arguments: { path: "/home/user/.agents/skills/review/SKILL.md" }, - }, - ], - }, - }); - await emit("tool_execution_start", { - toolCallId: "tool-skill", - toolName: "read", - args: { path: "/home/user/.agents/skills/review/SKILL.md" }, - }); - await emit("tool_execution_end", { - toolCallId: "tool-skill", - toolName: "read", - isError: false, - result: { content: [{ type: "text", text: "---\nname: review\n---" }] }, - }); - - const turnSpan = mockState.startSpans.find( - (span) => span.type === "task" && span.name === "Turn 1", - ); - expect(turnSpan?.metadata).toMatchObject({ - raw_input: "/skill:review inspect this", - loaded_skill_names: ["review"], - loaded_skills: [{ name: "review" }], - }); - - const skillSpan = mockState.startSpans.find((span) => span.name === "skill: review"); - expect(skillSpan?.metadata).toMatchObject({ - tool_name: "skill", - skill_name: "review", - skill_load_trigger: "explicit", - }); - }); - - it("does not mark natural-language skill reads as explicit", async () => { - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("input", { - text: "Use the review skill", - images: [], - source: "interactive", - }); - await emit("before_agent_start", { - prompt: "Use the review skill", - images: [], - }); - await emit("message_end", { - message: { - role: "assistant", - provider: "anthropic", - model: "claude-sonnet-4", - timestamp: 1_700_000_000_000, - content: [ - { - type: "toolCall", - id: "tool-skill", - name: "read", - arguments: { path: "/home/user/.agents/skills/review/SKILL.md" }, - }, - ], - }, - }); - await emit("tool_execution_start", { - toolCallId: "tool-skill", - toolName: "read", - args: { path: "/home/user/.agents/skills/review/SKILL.md" }, - }); - await emit("tool_execution_end", { - toolCallId: "tool-skill", - toolName: "read", - isError: false, - result: { content: [{ type: "text", text: "---\nname: review\n---" }] }, - }); - - const turnSpan = mockState.startSpans.find( - (span) => span.type === "task" && span.name === "Turn 1", - ); - const turnMetadata = turnSpan?.metadata as Record | undefined; - expect(turnMetadata?.loaded_skill_names).toBeUndefined(); - expect(turnMetadata?.loaded_skills).toBeUndefined(); - - const skillSpan = mockState.startSpans.find((span) => span.name === "skill: review"); - const skillMetadata = skillSpan?.metadata as Record | undefined; - expect(skillMetadata?.skill_load_trigger).toBeUndefined(); - }); - - it("preserves fork metadata when the root span is created lazily", async () => { - const { emit } = await createHarness(); - - await emit("session_start", { - reason: "fork", - previousSessionFile: "/tmp/parent-session.json", - }); - await emit("before_agent_start", { - prompt: "Continue from the fork", - images: [], - }); - - expect(mockState.startSpans[0]).toMatchObject({ - type: "task", - metadata: { - opened_via: "session_fork", - parent_session_file: "/tmp/parent-session.json", - }, - }); - }); - - it("adds the git repo slug to root span metadata when available", async () => { - const { default: braintrustPiExtension } = await import("./legacy-processor.ts"); - const handlers = new Map unknown>(); - - braintrustPiExtension({ - on(eventName: string, handler: (...args: unknown[]) => unknown) { - handlers.set(eventName, handler); - }, - } as never); - - const ctx = { - cwd: process.cwd(), - hasUI: true, - model: "anthropic/claude-sonnet-4", - ui: { - theme: { - fg: (_color: string, text: string) => text, - underline: (text: string) => text, - }, - setStatus: (key: string, text: string | undefined) => { - mockState.statuses.push({ key, text }); - }, - setWidget: (key: string, content: string[] | undefined, _options?: unknown) => { - mockState.widgets.push({ key, content }); - }, - }, - sessionManager: { - getSessionFile: () => "/tmp/session.json", - getSessionId: () => "session-1", - getSessionName: () => undefined, - }, - mode: "tui", - }; - - const emit = async (eventName: string, event: Record = {}): Promise => { - const handler = handlers.get(eventName); - if (!handler) throw new Error(`No handler registered for ${eventName}`); - await handler(event, ctx); - }; - - await emit("before_agent_start", { - prompt: "Inspect the package", - images: [], - }); - - expect(mockState.startSpans[0]).toMatchObject({ - type: "task", - metadata: { - repo: "braintrustdata/braintrust-coding-agent-plugins", - git_origin_url: expect.stringMatching( - /^(git@github\.com:|https:\/\/github\.com\/)braintrustdata\/braintrust-coding-agent-plugins(?:\.git)?$/, - ), - git_commit_sha: expect.stringMatching(/^[0-9a-f]{40}$/), - }, - }); - }); - - it("falls back to the turn span when no matching tool call was emitted by the llm", async () => { - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("before_agent_start", { - prompt: "Run the test suite", - images: [], - }); - await emit("message_end", { - message: { - role: "assistant", - provider: "anthropic", - model: "claude-sonnet-4", - timestamp: 1_700_000_000_001, - content: [{ type: "text", text: "Running the test suite." }], - }, - }); - await emit("tool_execution_start", { - toolCallId: "tool-missing", - toolName: "bash", - args: { command: "npm test" }, - }); - await emit("tool_execution_end", { - toolCallId: "tool-missing", - toolName: "bash", - isError: false, - result: { - content: [{ type: "text", text: "tests passed" }], - }, - }); - - const turnSpan = mockState.startSpans.find( - (span) => span.type === "task" && span.name === "Turn 1", - ); - const toolSpan = mockState.startSpans.find((span) => span.type === "tool"); - - expect(turnSpan).toBeDefined(); - expect(toolSpan).toBeDefined(); - expect(toolSpan?.parentSpanId).toBe(turnSpan?.spanId); - expect(toolSpan?.metadata).toMatchObject({ - tool_name: "bash", - tool_call_id: "tool-missing", - }); - expect((toolSpan?.metadata as Record | undefined)?.parent_llm_span_id).toBe( - undefined, - ); - }); - - it("keeps the turn open when agent_end will retry", async () => { - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("before_agent_start", { - prompt: "Retry after a transient failure", - images: [], - }); - await emit("agent_end", { willRetry: true, messages: [] }); - - expect(mockState.endSpans).toEqual([]); - - await emit("message_end", { - message: { - role: "assistant", - provider: "anthropic", - model: "claude-sonnet-4", - timestamp: 1_700_000_000_000, - content: [{ type: "text", text: "Recovered after retry." }], - }, - }); - await emit("agent_end", { - messages: [ - { - role: "assistant", - content: [{ type: "text", text: "Recovered after retry." }], - }, - ], - }); - - expect( - mockState.logSpans.some( - (entry) => - ((entry.event as Record).metadata as Record | undefined) - ?.finish_reason === "agent_end", - ), - ).toBe(true); - }); - - it("records the structured shutdown reason on the finalized root span", async () => { - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("before_agent_start", { - prompt: "Inspect the package", - images: [], - }); - await emit("session_shutdown", { reason: "quit" }); - - const rootFinalizeLog = mockState.logSpans - .map((entry) => entry.event as Record) - .find( - (event) => - (event.metadata as Record | undefined)?.last_close_reason === "quit", - ); - expect(rootFinalizeLog).toBeDefined(); - expect(mockState.endSpans.length).toBeGreaterThan(0); - expect(mockState.flushCalls).toBeGreaterThan(0); - }); - - it("does not finalize the root span on reload shutdowns", async () => { - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("before_agent_start", { - prompt: "Inspect the package", - images: [], - }); - - const startsBefore = mockState.startSpans.length; - const endsBefore = mockState.endSpans.length; - const flushesBefore = mockState.flushCalls; - - await emit("session_shutdown", { reason: "reload" }); - - // No additional span endings during reload, but pending writes are still flushed. - expect(mockState.startSpans.length).toBe(startsBefore); - expect(mockState.endSpans.length).toBe(endsBefore); - expect(mockState.flushCalls).toBeGreaterThan(flushesBefore); - const reloadClose = mockState.logSpans - .map((entry) => entry.event as Record) - .some( - (event) => - (event.metadata as Record | undefined)?.last_close_reason === "reload", - ); - expect(reloadClose).toBe(false); - }); - - it("hides all UI when showUi is false", async () => { - mockState.config.showUi = false; - - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("before_agent_start", { - prompt: "Inspect the package", - images: [], - }); - - const statusUpdates = mockState.statuses.filter( - (s) => s.key === "braintrust-tracing" && s.text !== undefined, - ); - const widgetUpdates = mockState.widgets.filter( - (w) => w.key === "braintrust-trace-link" && w.content !== undefined, - ); - - expect(statusUpdates).toEqual([]); - expect(widgetUpdates).toEqual([]); - }); - - it("hides just the trace link when showTraceLink is false", async () => { - mockState.config.showTraceLink = false; - - const { emit } = await createHarness(); - - await emit("session_start"); - await emit("before_agent_start", { - prompt: "Inspect the package", - images: [], - }); - - const statusUpdates = mockState.statuses.filter( - (s) => s.key === "braintrust-tracing" && s.text !== undefined, - ); - expect(statusUpdates.length).toBeGreaterThan(0); - expect(statusUpdates[0]?.text).toContain("Braintrust"); - - const widgetUpdates = mockState.widgets.filter( - (w) => w.key === "braintrust-trace-link" && w.content !== undefined, - ); - expect(widgetUpdates).toEqual([]); - }); -}); diff --git a/src/plugins/pi/content/src/index.ts b/src/plugins/pi/content/src/index.ts index f6376a8..1a0ffd7 100644 --- a/src/plugins/pi/content/src/index.ts +++ b/src/plugins/pi/content/src/index.ts @@ -1,3 +1,5 @@ +import { createHash, randomUUID } from "node:crypto"; +import { resolve } from "node:path"; import { VERSION as PI_VERSION, type ExtensionAPI, @@ -5,12 +7,21 @@ import { } from "@earendil-works/pi-coding-agent"; import { loadConfig } from "./config.ts"; import { DaemonClient } from "./runtime/daemon-client.ts"; -import { sessionKeyFor } from "./utils.ts"; import { EXTENSION_VERSION } from "./version.ts"; const STATUS_KEY = "braintrust-tracing"; const WIDGET_KEY = "braintrust-trace-link"; +function sessionKeyFor( + sessionFile: string | undefined, + sessionId: string | undefined, + cwd: string, +): string { + if (sessionFile) return `file:${sessionFile}`; + const projectKey = createHash("sha256").update(resolve(cwd)).digest("hex").slice(0, 12); + return `ephemeral:${projectKey}:${sessionId ?? randomUUID()}`; +} + function nativePayload(value: unknown): unknown { try { return JSON.parse(JSON.stringify(value)); diff --git a/src/plugins/pi/content/src/legacy-processor.ts b/src/plugins/pi/content/src/legacy-processor.ts deleted file mode 100644 index 0a39a95..0000000 --- a/src/plugins/pi/content/src/legacy-processor.ts +++ /dev/null @@ -1,1628 +0,0 @@ -import { hostname, userInfo } from "node:os"; -import { basename, dirname, resolve } from "node:path"; -import { - VERSION as PI_VERSION, - type AgentEndEvent, - type ExtensionAPI, - type ExtensionContext, -} from "@earendil-works/pi-coding-agent"; -import { BraintrustClient, type BraintrustSpanHandle } from "./client.ts"; -import { createLogger, loadConfig } from "./config.ts"; -import { createStateStore } from "./state.ts"; -import type { - AgentMessageLike, - AssistantMessageLike, - ConfigIssue, - ImageLike, - NormalizedAgentMessage, - NormalizedAssistantMessage, - TraceConfig, -} from "./types.ts"; -import { EXTENSION_VERSION } from "./version.ts"; -import { - buildTurnInput, - extractErrorText, - formatToolSpanName, - generateUuid, - gitMetadataForCwd, - isPlainObject, - normalizeAssistantMessage, - normalizeContextMessages, - normalizeToolResult, - repoSlugForCwd, - rootSpanName, - sessionKeyFor, - shortHash, - toUnixSeconds, - truncateValue, -} from "./utils.ts"; - -const TRACING_STATUS_KEY = "braintrust-tracing"; -const TRACING_WIDGET_KEY = "braintrust-trace-link"; - -interface SessionDescriptor { - sessionFile: string | undefined; - sessionId: string | undefined; - sessionKey: string; -} - -interface ProviderResponseMetadata { - status?: number; - headers?: Record; -} - -interface PendingLlmCall { - startedAt: number; - input: NormalizedAgentMessage[]; - activeToolNames?: string[]; - activatedToolNames: string[]; - modelMetadata: Record; - providerRequest?: Record; - providerResponse?: ProviderResponseMetadata; - firstTokenAt?: number; - firstThinkingAt?: number; - lastThinkingAt?: number; - firstTextAt?: number; -} - -interface TrackedToolStart { - startedAt: number; - args: unknown; - toolName: string; -} - -interface SkillLoadMetadata { - skill_name: string; - skill_path?: string; -} - -interface ExplicitSkillRequestMetadata { - loaded_skill_names: string[]; - loaded_skills: Array<{ name: string }>; -} - -interface ActiveCompaction { - spanId: string; - span?: BraintrustSpanHandle; - startedAt: number; - input: unknown; -} - -interface ActiveBranchSummary { - spanId: string; - span?: BraintrustSpanHandle; - startedAt: number; - input: unknown; -} - -interface PendingInputEvent { - text?: string; - source?: string; - streamingBehavior?: string; - imageCount?: number; -} - -interface ActiveTurn { - spanId: string; - span?: BraintrustSpanHandle; - prompt: string; - explicitSkillNames: string[]; - llmCalls: PendingLlmCall[]; - llmCallCount: number; - toolCallCount: number; - toolStarts: Map; - toolParentSpanIds: Map; - activatedToolNames: Set; - lastAssistantMessage?: AssistantMessageLike; - lastOutput?: NormalizedAssistantMessage; - error?: string; - thinkingLevel?: string; -} - -interface ActiveSession { - sessionKey: string; - sessionFile: string | undefined; - sessionId: string | undefined; - openedVia?: string; - parentSessionFile?: string; - rootSpanId?: string; - rootSpan?: BraintrustSpanHandle; - rootSpanRecordId?: string; - traceRootSpanId?: string; - parentSpanId?: string; - traceUrl?: string; - traceUrlPromise?: Promise; - startedAt?: number; - totalTurns: number; - totalToolCalls: number; - thinkingLevel?: string; - currentTurn?: ActiveTurn; - currentCompaction?: ActiveCompaction; - currentBranchSummary?: ActiveBranchSummary; -} - -let pendingInputEvent: PendingInputEvent | undefined; - -function stringArg(args: unknown, keys: readonly string[]): string | undefined { - if (!isPlainObject(args)) return undefined; - for (const key of keys) { - const value = args[key]; - if (typeof value === "string" && value.trim() !== "") return value; - } - return undefined; -} - -function skillLoadFromRead(toolName: string, args: unknown): SkillLoadMetadata | undefined { - if (toolName !== "read") return undefined; - const path = stringArg(args, ["path", "filePath", "file_path", "file"]); - if (!path || basename(path).toLowerCase() !== "skill.md") return undefined; - - const skillName = basename(dirname(path)); - return { - skill_name: skillName, - skill_path: path, - }; -} - -function normalizeExplicitSkillName(name: string): string | undefined { - const normalized = name - .trim() - .replace(/^[/$]+/, "") - .replace(/[),.;:]+$/, ""); - return normalized ? normalized : undefined; -} - -function explicitSkillRequestMetadata( - names: readonly string[], -): ExplicitSkillRequestMetadata | undefined { - const seen = new Set(); - const loaded_skill_names: string[] = []; - for (const name of names) { - const normalized = normalizeExplicitSkillName(name); - if (!normalized || seen.has(normalized)) continue; - seen.add(normalized); - loaded_skill_names.push(normalized); - } - if (loaded_skill_names.length === 0) return undefined; - return { - loaded_skill_names, - loaded_skills: loaded_skill_names.map((name) => ({ name })), - }; -} - -function explicitSkillNamesFromPiInput(input: PendingInputEvent | undefined): string[] { - if (!input?.text) return []; - const names: string[] = []; - for (const match of input.text.matchAll(/(?:^|\s)\/skill:([^\s]+)/g)) { - const name = normalizeExplicitSkillName(match[1] ?? ""); - if (name) names.push(name); - } - return explicitSkillRequestMetadata(names)?.loaded_skill_names ?? []; -} - -function skillLoadTriggerForTurn( - turn: ActiveTurn, - skillLoad: SkillLoadMetadata | undefined, -): "explicit" | undefined { - if (!skillLoad) return undefined; - return turn.explicitSkillNames.includes(skillLoad.skill_name) ? "explicit" : undefined; -} - -function hasSessionRoot(session: ActiveSession | undefined): session is ActiveSession & { - rootSpanId: string; - traceRootSpanId: string; - startedAt: number; -} { - return Boolean( - session && - typeof session.rootSpanId === "string" && - typeof session.traceRootSpanId === "string" && - typeof session.startedAt === "number", - ); -} - -function getUsername(): string { - try { - return userInfo().username; - } catch { - return process.env.USER || process.env.USERNAME || "unknown"; - } -} - -function getSessionDescriptor(ctx: ExtensionContext): SessionDescriptor { - const sessionFile = ctx.sessionManager.getSessionFile(); - const sessionId = ctx.sessionManager.getSessionId(); - const sessionKey = sessionKeyFor( - sessionFile ? resolve(sessionFile) : undefined, - sessionId, - ctx.cwd, - ); - - return { - sessionFile, - sessionId, - sessionKey, - }; -} - -function safeModelName(model: unknown): string | undefined { - if (!model) return undefined; - if (typeof model === "string") return model; - if (!isPlainObject(model)) return undefined; - const provider = model.provider ?? model.providerId ?? model.providerID; - const id = model.id ?? model.modelId ?? model.modelID; - if (typeof provider === "string" && typeof id === "string") return `${provider}/${id}`; - if (typeof id === "string") return id; - return undefined; -} - -function modelTraceMetadata(model: unknown): Record { - if (!isPlainObject(model)) return {}; - - const metadata: Record = {}; - if (typeof model.api === "string") metadata["pi_coding_agent.api"] = model.api; - if (typeof model.name === "string") metadata["pi_coding_agent.model_name"] = model.name; - if (typeof model.reasoning === "boolean") metadata.model_supports_reasoning = model.reasoning; - if (typeof model.contextWindow === "number") metadata.model_context_window = model.contextWindow; - if (typeof model.maxTokens === "number") metadata.model_max_tokens = model.maxTokens; - - if (isPlainObject(model.thinkingLevelMap)) { - metadata.supported_thinking_levels = Object.entries(model.thinkingLevelMap) - .filter(([, value]) => value !== null) - .map(([level]) => level); - } - - if (isPlainObject(model.compat) && typeof model.compat.deferredToolsMode === "string") { - metadata.deferred_tools_mode = model.compat.deferredToolsMode; - } - - return metadata; -} - -function activeToolNames(pi: ExtensionAPI): string[] | undefined { - try { - if (typeof pi.getActiveTools !== "function") return undefined; - return pi.getActiveTools(); - } catch { - return undefined; - } -} - -function stringProperty( - value: Record, - keys: readonly string[], -): string | undefined { - for (const key of keys) { - const item = value[key]; - if (typeof item === "string" && item.trim()) return item; - } - return undefined; -} - -function metadataForPendingInput(input: PendingInputEvent | undefined): Record { - if (!input) return {}; - return { - input_source: input.source, - input_streaming_behavior: input.streamingBehavior ?? "idle", - input_image_count: input.imageCount, - raw_input: input.text === undefined ? undefined : truncateValue(input.text), - }; -} - -function responseModelName(message: AssistantMessageLike): string | undefined { - return stringProperty(message as unknown as Record, [ - "responseModel", - "routedModel", - "resolvedModel", - "actualModel", - "concreteModel", - "outputModel", - ]); -} - -function providerResponseMetadata(event: unknown): ProviderResponseMetadata | undefined { - if (!isPlainObject(event)) return undefined; - const metadata: ProviderResponseMetadata = {}; - if (typeof event.status === "number") metadata.status = event.status; - - const headers = event.headers; - if (isPlainObject(headers)) { - const allowedHeaders: Record = {}; - const safeHeaders = new Set(["retry-after", "x-request-id", "request-id", "cf-ray"]); - for (const [key, value] of Object.entries(headers)) { - const normalizedKey = key.toLowerCase(); - if (!normalizedKey.startsWith("x-ratelimit-") && !safeHeaders.has(normalizedKey)) { - continue; - } - if (typeof value === "string") allowedHeaders[normalizedKey] = value; - else if (typeof value === "number" || typeof value === "boolean") { - allowedHeaders[normalizedKey] = String(value); - } - } - if (Object.keys(allowedHeaders).length > 0) metadata.headers = allowedHeaders; - } - - return metadata.status !== undefined || metadata.headers ? metadata : undefined; -} - -function providerRequestMetadata(event: unknown): Record | undefined { - if (!isPlainObject(event) || !isPlainObject(event.payload)) return undefined; - const payload = event.payload; - const metadata: Record = {}; - - if (typeof payload.model === "string") metadata.provider_request_model = payload.model; - const maxTokens = numberProperty(payload, ["max_tokens", "max_completion_tokens"]); - if (maxTokens !== undefined) metadata.provider_request_max_tokens = maxTokens; - if (typeof payload.temperature === "number") { - metadata.provider_request_temperature = payload.temperature; - } - - const thinking = isPlainObject(payload.thinking) ? payload.thinking : undefined; - const reasoning = isPlainObject(payload.reasoning) ? payload.reasoning : undefined; - const outputConfig = isPlainObject(payload.output_config) ? payload.output_config : undefined; - const chatTemplate = isPlainObject(payload.chat_template_kwargs) - ? payload.chat_template_kwargs - : undefined; - - const thinkingType = stringProperty(thinking ?? {}, ["type"]); - const thinkingEffort = - stringProperty(outputConfig ?? {}, ["effort"]) ?? - stringProperty(reasoning ?? {}, ["effort"]) ?? - stringProperty(payload, ["reasoning_effort"]); - const thinkingBudget = numberProperty(thinking, ["budget_tokens", "budgetTokens"]); - - if (thinkingType) metadata.effective_thinking_type = thinkingType; - if (thinkingEffort) metadata.effective_thinking_effort = thinkingEffort; - if (thinkingBudget !== undefined) { - metadata.effective_thinking_budget_tokens = thinkingBudget; - metadata.effective_thinking_uses_token_budget = true; - } else if (thinkingType === "adaptive") { - metadata.effective_thinking_uses_token_budget = false; - } - - const thinkingEnabled = - typeof payload.enable_thinking === "boolean" - ? payload.enable_thinking - : typeof reasoning?.enabled === "boolean" - ? reasoning.enabled - : typeof chatTemplate?.enable_thinking === "boolean" - ? chatTemplate.enable_thinking - : thinkingType === "adaptive" || thinkingType === "enabled" - ? true - : thinkingType === "disabled" - ? false - : undefined; - if (thinkingEnabled !== undefined) metadata.effective_thinking_enabled = thinkingEnabled; - - const immediateTools = Array.isArray(payload.tools) ? payload.tools : []; - const deferredAnthropicTools = immediateTools.filter( - (tool) => isPlainObject(tool) && tool.defer_loading === true, - ).length; - let deferredKimiTools = 0; - if (Array.isArray(payload.messages)) { - for (const message of payload.messages) { - if (isPlainObject(message) && Array.isArray(message.tools)) { - deferredKimiTools += message.tools.length; - } - } - } - const deferredToolCount = deferredAnthropicTools + deferredKimiTools; - const toolCount = immediateTools.length + deferredKimiTools; - if (toolCount > 0) metadata.provider_request_tool_count = toolCount; - if (deferredToolCount > 0) { - metadata.provider_request_deferred_tool_count = deferredToolCount; - } - - return Object.keys(metadata).length > 0 ? metadata : undefined; -} - -function addedToolNames(result: unknown): string[] { - if (!isPlainObject(result) || !Array.isArray(result.addedToolNames)) return []; - return [ - ...new Set(result.addedToolNames.filter((name): name is string => typeof name === "string")), - ]; -} - -function tokenMetric(value: unknown): number | undefined { - return typeof value === "number" && - Number.isFinite(value) && - value >= 0 && - Number.isInteger(value) - ? value - : undefined; -} - -function nonNegativeMetric(value: unknown): number | undefined { - return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined; -} - -function compactMetrics(metrics: Record): Record { - return Object.fromEntries( - Object.entries(metrics).filter((entry): entry is [string, number] => entry[1] !== undefined), - ); -} - -function usageMetrics(usage: AssistantMessageLike["usage"]): Record { - if (!usage) return {}; - - const inputTokens = tokenMetric(usage.input); - const completionTokens = tokenMetric(usage.output); - const cachedTokens = tokenMetric(usage.cacheRead); - const cacheCreationTokens = tokenMetric(usage.cacheWrite); - const cacheCreation1hTokens = tokenMetric(usage.cacheWrite1h); - const promptTokens = - inputTokens === undefined - ? undefined - : inputTokens + (cachedTokens ?? 0) + (cacheCreationTokens ?? 0); - const totalTokens = - promptTokens !== undefined && completionTokens !== undefined - ? promptTokens + completionTokens - : tokenMetric(usage.totalTokens); - - const metrics: Record = { - prompt_tokens: promptTokens, - completion_tokens: completionTokens, - tokens: totalTokens, - prompt_cached_tokens: cachedTokens, - completion_reasoning_tokens: tokenMetric(usage.reasoning), - estimated_cost: nonNegativeMetric(usage.cost?.total), - }; - - if (cacheCreation1hTokens === undefined) { - metrics.prompt_cache_creation_tokens = cacheCreationTokens; - } else { - metrics.prompt_cache_creation_1h_tokens = cacheCreation1hTokens; - if (cacheCreationTokens !== undefined) { - metrics.prompt_cache_creation_5m_tokens = Math.max( - 0, - cacheCreationTokens - cacheCreation1hTokens, - ); - } - } - - return compactMetrics(metrics); -} - -function pendingLlmCall(turn: ActiveTurn): PendingLlmCall | undefined { - return turn.llmCalls.at(-1); -} - -function recordStreamingTiming(turn: ActiveTurn, event: unknown, observedAt: number): void { - const pending = pendingLlmCall(turn); - if (!pending || !isPlainObject(event) || typeof event.type !== "string") return; - if (event.type === "start") return; - - pending.firstTokenAt ??= observedAt; - if (event.type.startsWith("thinking_")) { - pending.firstThinkingAt ??= observedAt; - pending.lastThinkingAt = observedAt; - } - if (event.type.startsWith("text_")) pending.firstTextAt ??= observedAt; -} - -function secondsBetween(startedAt: number, endedAt: number | undefined): number | undefined { - if (endedAt === undefined) return undefined; - return Math.max(0, endedAt - startedAt) / 1000; -} - -function streamingTimingMetadata( - pending: PendingLlmCall, - endedAt: number, -): Record { - const thinkingEndedAt = pending.firstTextAt ?? pending.lastThinkingAt ?? endedAt; - return { - "pi_coding_agent.time_to_first_thinking": secondsBetween( - pending.startedAt, - pending.firstThinkingAt, - ), - "pi_coding_agent.time_to_first_text": secondsBetween(pending.startedAt, pending.firstTextAt), - "pi_coding_agent.thinking_duration": pending.firstThinkingAt - ? secondsBetween(pending.firstThinkingAt, thinkingEndedAt) - : undefined, - }; -} - -function thinkingBlockMetadata(message: AssistantMessageLike): Record { - const thinkingBlocks = (message.content ?? []).filter( - (part) => isPlainObject(part) && part.type === "thinking", - ); - if (thinkingBlocks.length === 0) return {}; - - return { - thinking_block_count: thinkingBlocks.length, - empty_thinking_block_count: thinkingBlocks.filter( - (part) => - isPlainObject(part) && (typeof part.thinking !== "string" || part.thinking.length === 0), - ).length, - }; -} - -function getPreviousSessionFile(event: unknown): string | undefined { - if (!isPlainObject(event)) return undefined; - return typeof event.previousSessionFile === "string" ? event.previousSessionFile : undefined; -} - -function getEventReason(event: unknown): string | undefined { - if (!isPlainObject(event)) return undefined; - return typeof event.reason === "string" ? event.reason : undefined; -} - -function booleanProperty(value: unknown, key: string): boolean | undefined { - if (!isPlainObject(value)) return undefined; - return typeof value[key] === "boolean" ? value[key] : undefined; -} - -function numberProperty(value: unknown, keys: readonly string[]): number | undefined { - if (!isPlainObject(value)) return undefined; - for (const key of keys) { - const item = value[key]; - if (typeof item === "number" && Number.isFinite(item)) return item; - } - return undefined; -} - -function compactionMetadata(event: unknown, eventType: string): Record { - const metadata: Record = { event_type: eventType }; - const eventObject = isPlainObject(event) ? event : undefined; - const preparation = isPlainObject(eventObject?.preparation) ? eventObject.preparation : undefined; - const compactionEntry = isPlainObject(eventObject?.compactionEntry) - ? eventObject.compactionEntry - : undefined; - - const reason = getEventReason(event); - if (reason) metadata.compaction_reason = reason; - - const willRetry = booleanProperty(event, "willRetry"); - if (willRetry !== undefined) metadata.will_retry = willRetry; - - const tokensBefore = - numberProperty(compactionEntry, ["tokensBefore", "tokens_before"]) ?? - numberProperty(preparation, ["tokensBefore", "tokens_before"]); - if (tokensBefore !== undefined) metadata.tokens_before = tokensBefore; - - const estimatedTokensAfter = - numberProperty(compactionEntry, [ - "estimatedTokensAfter", - "estimated_tokens_after", - "tokensAfter", - "tokens_after", - "postCompactionTokens", - "post_compaction_tokens", - ]) ?? - numberProperty(preparation, [ - "estimatedTokensAfter", - "estimated_tokens_after", - "tokensAfter", - "tokens_after", - "postCompactionTokens", - "post_compaction_tokens", - ]); - if (estimatedTokensAfter !== undefined) { - metadata.estimated_tokens_after = estimatedTokensAfter; - } - - return metadata; -} - -function treePreparationInput(event: unknown): unknown { - const preparation = - isPlainObject(event) && isPlainObject(event.preparation) ? event.preparation : undefined; - if (!preparation) return undefined; - - return truncateValue({ - target_id: typeof preparation.targetId === "string" ? preparation.targetId : undefined, - old_leaf_id: typeof preparation.oldLeafId === "string" ? preparation.oldLeafId : undefined, - common_ancestor_id: - typeof preparation.commonAncestorId === "string" ? preparation.commonAncestorId : undefined, - entries_to_summarize: Array.isArray(preparation.entriesToSummarize) - ? preparation.entriesToSummarize.length - : undefined, - user_wants_summary: - typeof preparation.userWantsSummary === "boolean" ? preparation.userWantsSummary : undefined, - custom_instructions: - typeof preparation.customInstructions === "string" - ? preparation.customInstructions - : undefined, - replace_instructions: - typeof preparation.replaceInstructions === "boolean" - ? preparation.replaceInstructions - : undefined, - label: typeof preparation.label === "string" ? preparation.label : undefined, - }); -} - -function isAssistantMessage(message: unknown): message is AssistantMessageLike { - return isPlainObject(message) && message.role === "assistant"; -} - -function findLastAssistant( - messages: AgentEndEvent["messages"] = [], -): AssistantMessageLike | undefined { - for (let index = messages.length - 1; index >= 0; index -= 1) { - const message = messages[index]; - if (isAssistantMessage(message)) return message; - } - return undefined; -} - -function getSessionName(ctx: ExtensionContext): string | undefined { - try { - const name = ctx.sessionManager.getSessionName?.(); - return typeof name === "string" && name.trim() ? name : undefined; - } catch { - return undefined; - } -} - -function standardRootMetadata(ctx: ExtensionContext, config: TraceConfig): Record { - const descriptor = getSessionDescriptor(ctx); - const sessionName = getSessionName(ctx); - const gitMetadata = gitMetadataForCwd(ctx.cwd); - return { - ...config.additionalMetadata, - source: "pi", - extension_version: EXTENSION_VERSION, - pi_version: PI_VERSION, - pi_mode: ctx.mode, - session_name: sessionName, - session_id: descriptor.sessionId, - session_key: descriptor.sessionKey, - session_file: descriptor.sessionFile, - workspace: basename(ctx.cwd), - directory: ctx.cwd, - repo: repoSlugForCwd(ctx.cwd), - ...gitMetadata, - hostname: hostname(), - username: getUsername(), - os: process.platform, - }; -} - -function projectTraceUrl(config: TraceConfig, traceId: string | undefined): string | undefined { - if (!traceId || !config.orgName) return undefined; - return `${config.appUrl}/app/${encodeURIComponent(config.orgName)}/p/${encodeURIComponent(config.projectName)}/logs?oid=${encodeURIComponent(traceId)}`; -} - -function primaryConfigIssue(config: TraceConfig): ConfigIssue | undefined { - return config.configIssues.find((issue) => issue.severity === "error") ?? config.configIssues[0]; -} - -function configIssueStatusLabel(issue: ConfigIssue | undefined): string | undefined { - if (!issue) return undefined; - return issue.severity === "warning" ? "config warning" : "config error"; -} - -function setTracingStatus( - ctx: ExtensionContext, - config: TraceConfig, - options: { - active: boolean; - initError?: string; - missingApiKey?: boolean; - configIssue?: ConfigIssue; - }, -): void { - if (!ctx.hasUI || !config.showUi) { - if (ctx.hasUI) ctx.ui.setStatus(TRACING_STATUS_KEY, undefined); - return; - } - - const theme = ctx.ui.theme; - - if (options.initError) { - ctx.ui.setStatus( - TRACING_STATUS_KEY, - theme.fg("warning", "Braintrust") + theme.fg("dim", " setup failed"), - ); - return; - } - - if (options.active) { - ctx.ui.setStatus( - TRACING_STATUS_KEY, - theme.fg("accent", "Braintrust") + - theme.fg( - "dim", - ` tracing ${config.projectName}${options.configIssue ? " (config warning)" : ""}`, - ), - ); - return; - } - - if (options.missingApiKey) { - ctx.ui.setStatus( - TRACING_STATUS_KEY, - theme.fg("warning", "Braintrust") + theme.fg("dim", " missing API key"), - ); - return; - } - - const configIssueLabel = configIssueStatusLabel(options.configIssue); - if (configIssueLabel) { - ctx.ui.setStatus( - TRACING_STATUS_KEY, - theme.fg("warning", "Braintrust") + theme.fg("dim", ` ${configIssueLabel}`), - ); - return; - } - - ctx.ui.setStatus(TRACING_STATUS_KEY, undefined); -} - -function makeHyperlink(url: string, text: string): string { - return `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`; -} - -function truncateMiddle(value: string, maxLength: number): string { - if (value.length <= maxLength) return value; - const left = Math.max(1, Math.floor((maxLength - 1) / 2)); - const right = Math.max(1, maxLength - left - 1); - return `${value.slice(0, left)}…${value.slice(-right)}`; -} - -function shortenTraceUrl(traceUrl: string): string { - try { - const parsed = new URL(traceUrl); - const host = parsed.host.replace(/^www\./, ""); - const oid = parsed.searchParams.get("oid") ?? parsed.searchParams.get("id"); - const shortOid = oid ? truncateMiddle(oid, 16) : undefined; - const suffix = shortOid ? `?oid=${shortOid}` : ""; - return truncateMiddle(`${host}${parsed.pathname}${suffix}`, 72); - } catch { - return truncateMiddle(traceUrl, 72); - } -} - -function displayPath(path: string): string { - const home = process.env.HOME; - if (home && path.startsWith(home)) return `~${path.slice(home.length)}`; - return path; -} - -function setTraceWidget( - ctx: ExtensionContext, - config: TraceConfig, - traceUrl: string | undefined, - configIssue: ConfigIssue | undefined, -): void { - if (!ctx.hasUI || !config.showUi) { - if (ctx.hasUI) ctx.ui.setWidget(TRACING_WIDGET_KEY, undefined); - return; - } - - const theme = ctx.ui.theme; - const lines: string[] = []; - - if (traceUrl && config.showTraceLink) { - const label = makeHyperlink( - traceUrl, - theme.fg("accent", theme.underline("Braintrust trace ↗")), - ); - lines.push(label, theme.fg("dim", shortenTraceUrl(traceUrl))); - } - - if (configIssue) { - const issueLabel = - traceUrl || configIssue.severity === "warning" - ? "Braintrust config warning" - : "Braintrust config error"; - lines.push( - theme.fg("warning", issueLabel), - theme.fg( - "dim", - truncateMiddle(`${displayPath(configIssue.path)}: ${configIssue.message}`, 120), - ), - ); - } - - ctx.ui.setWidget(TRACING_WIDGET_KEY, lines.length > 0 ? lines : undefined, { - placement: "belowEditor", - }); -} - -export default function braintrustPiExtension(pi: ExtensionAPI): void { - const config = loadConfig(process.cwd()); - const logger = createLogger(config); - const store = createStateStore(config.stateDir, logger); - - let client: BraintrustClient | undefined; - let clientInitializationError: string | undefined; - - if (config.enabled && config.apiKey) { - client = new BraintrustClient(config, logger); - client.initialize().catch((error: unknown) => { - clientInitializationError = String(error); - logger.error("failed to initialize Braintrust client", { error: clientInitializationError }); - }); - } else if (config.enabled && !config.apiKey) { - logger.warn("TRACE_TO_BRAINTRUST is enabled but BRAINTRUST_API_KEY is missing"); - } - - function tracingEnabled(): boolean { - return Boolean(config.enabled && client && !clientInitializationError); - } - - function refreshTracingUi(ctx: ExtensionContext): void { - const configIssue = primaryConfigIssue(config); - setTracingStatus(ctx, config, { - active: tracingEnabled(), - initError: clientInitializationError, - missingApiKey: Boolean(config.enabled && !config.apiKey), - configIssue, - }); - setTraceWidget(ctx, config, activeSession?.traceUrl, configIssue); - } - - function persistTraceUrl(session: ActiveSession, traceUrl: string): void { - session.traceUrl = traceUrl; - store.patch(session.sessionKey, { - traceUrl, - lastSeenAt: Date.now(), - }); - } - - function refreshTraceUrl(ctx: ExtensionContext, session: ActiveSession): void { - if (!client || session.traceUrlPromise || !hasSessionRoot(session)) return; - - const quickUrl = - session.traceUrl ?? - client.getSpanLink(session.rootSpan) ?? - projectTraceUrl(config, session.rootSpanRecordId); - - if (quickUrl && quickUrl !== session.traceUrl) { - persistTraceUrl(session, quickUrl); - if (activeSession?.sessionKey === session.sessionKey) refreshTracingUi(ctx); - } - - if (!session.rootSpan || session.traceUrl) return; - - session.traceUrlPromise = client - .getSpanPermalink(session.rootSpan) - .then((traceUrl) => { - if (!traceUrl || traceUrl === session.traceUrl) return; - persistTraceUrl(session, traceUrl); - if (activeSession?.sessionKey === session.sessionKey) refreshTracingUi(ctx); - }) - .finally(() => { - session.traceUrlPromise = undefined; - }); - } - - let activeSession: ActiveSession | undefined; - - function materializeSessionRoot( - ctx: ExtensionContext, - session: ActiveSession, - ): ActiveSession | undefined { - if (!client) return undefined; - if (hasSessionRoot(session)) return session; - - const startedAt = Date.now(); - const rootSpanId = generateUuid(); - const traceRootSpanId = config.rootSpanId ?? rootSpanId; - const parentSpanId = config.parentSpanId; - const rootSpan = client.startSpan({ - spanId: rootSpanId, - rootSpanId: traceRootSpanId, - parentSpanId, - startedAt, - name: getSessionName(ctx) ?? rootSpanName(ctx.cwd), - type: "task", - metadata: { - ...standardRootMetadata(ctx, config), - opened_via: session.openedVia, - parent_session_file: session.parentSessionFile, - }, - }); - - session.rootSpanId = rootSpanId; - session.rootSpan = rootSpan; - session.rootSpanRecordId = rootSpan?.id; - session.traceRootSpanId = traceRootSpanId; - session.parentSpanId = parentSpanId; - session.traceUrl = client.getSpanLink(rootSpan) ?? projectTraceUrl(config, rootSpan?.id); - session.startedAt = startedAt; - - store.set(session.sessionKey, { - rootSpanId, - rootSpanRecordId: rootSpan?.id, - traceRootSpanId, - parentSpanId, - traceUrl: session.traceUrl, - startedAt, - totalTurns: session.totalTurns, - totalToolCalls: session.totalToolCalls, - lastSeenAt: startedAt, - sessionFile: session.sessionFile, - }); - store.schedulePersist(0); - - refreshTracingUi(ctx); - refreshTraceUrl(ctx, session); - return session; - } - - async function ensureSession( - ctx: ExtensionContext, - options: { - reason?: string; - parentSessionFile?: string | undefined; - createIfMissingRoot?: boolean; - } = {}, - ): Promise { - if (!tracingEnabled() || !client) return undefined; - - const descriptor = getSessionDescriptor(ctx); - if (activeSession?.sessionKey === descriptor.sessionKey) { - activeSession.sessionFile = descriptor.sessionFile; - activeSession.sessionId = descriptor.sessionId; - if (!hasSessionRoot(activeSession)) { - activeSession.openedVia ??= options.reason; - activeSession.parentSessionFile ??= options.parentSessionFile; - } - - if (options.createIfMissingRoot === false) { - refreshTracingUi(ctx); - return activeSession; - } - - return materializeSessionRoot(ctx, activeSession); - } - - const persisted = store.get(descriptor.sessionKey); - if (persisted) { - activeSession = { - sessionKey: descriptor.sessionKey, - sessionFile: descriptor.sessionFile, - sessionId: descriptor.sessionId, - openedVia: options.reason, - parentSessionFile: options.parentSessionFile, - rootSpanId: persisted.rootSpanId, - rootSpan: undefined, - rootSpanRecordId: persisted.rootSpanRecordId, - traceRootSpanId: persisted.traceRootSpanId ?? persisted.rootSpanId, - parentSpanId: persisted.parentSpanId, - traceUrl: persisted.traceUrl ?? projectTraceUrl(config, persisted.rootSpanRecordId), - startedAt: persisted.startedAt, - totalTurns: persisted.totalTurns ?? 0, - totalToolCalls: persisted.totalToolCalls ?? 0, - currentTurn: undefined, - }; - store.patch(descriptor.sessionKey, { - traceUrl: activeSession.traceUrl, - lastSeenAt: Date.now(), - sessionFile: descriptor.sessionFile, - }); - refreshTracingUi(ctx); - return activeSession; - } - - activeSession = { - sessionKey: descriptor.sessionKey, - sessionFile: descriptor.sessionFile, - sessionId: descriptor.sessionId, - openedVia: options.reason, - parentSessionFile: options.parentSessionFile, - totalTurns: 0, - totalToolCalls: 0, - currentTurn: undefined, - }; - - if (options.createIfMissingRoot === false) { - refreshTracingUi(ctx); - return activeSession; - } - - return materializeSessionRoot(ctx, activeSession); - } - - async function finishTurn( - reason: string, - endedAt = Date.now(), - finalAssistantMessage?: AssistantMessageLike, - ): Promise { - if (!activeSession?.currentTurn || !client) return; - - const turn = activeSession.currentTurn; - const finalAssistant = finalAssistantMessage ?? turn.lastAssistantMessage; - const finalOutput = finalAssistant - ? normalizeAssistantMessage(finalAssistant) - : turn.lastOutput; - const error = - turn.error || - (finalAssistant?.stopReason === "error" || finalAssistant?.stopReason === "aborted" - ? extractErrorText(finalAssistant, finalAssistant.errorMessage) - : undefined); - - client.logSpan(turn.span, { - output: finalOutput, - error, - metadata: { - llm_calls: turn.llmCallCount, - tool_calls: turn.toolCallCount, - finish_reason: reason, - }, - }); - client.endSpan(turn.span, endedAt); - - activeSession.currentTurn = undefined; - store.patch(activeSession.sessionKey, { - totalTurns: activeSession.totalTurns, - totalToolCalls: activeSession.totalToolCalls, - lastSeenAt: endedAt, - }); - await store.flush(); - } - - async function finalizeSession(reason: string, endedAt = Date.now()): Promise { - if (!activeSession || !client) return; - - await finishTurn(reason, endedAt); - if (!hasSessionRoot(activeSession)) return; - - const summaryMetadata = { - total_turns: activeSession.totalTurns, - total_tool_calls: activeSession.totalToolCalls, - last_close_reason: reason, - }; - - if (activeSession.rootSpan) { - client.logSpan(activeSession.rootSpan, { - metadata: summaryMetadata, - }); - client.endSpan(activeSession.rootSpan, endedAt); - } else if (activeSession.rootSpanRecordId) { - client.updateSpan({ - id: activeSession.rootSpanRecordId, - spanId: activeSession.rootSpanId, - rootSpanId: activeSession.traceRootSpanId, - metadata: summaryMetadata, - metrics: { - end: toUnixSeconds(endedAt), - }, - }); - } - - store.patch(activeSession.sessionKey, { - totalTurns: activeSession.totalTurns, - totalToolCalls: activeSession.totalToolCalls, - lastSeenAt: endedAt, - }); - await store.flush(); - } - - async function rolloverSession( - ctx: ExtensionContext, - reason: string, - previousSessionFile: string | undefined, - ): Promise { - if (!tracingEnabled()) return; - - const previousKey = activeSession?.sessionKey; - const nextKey = getSessionDescriptor(ctx).sessionKey; - if (previousKey && previousKey !== nextKey) { - await finalizeSession(reason); - activeSession = undefined; - } - - await ensureSession(ctx, { - reason, - parentSessionFile: previousSessionFile, - createIfMissingRoot: false, - }); - } - - pi.on("session_start", async (event, ctx) => { - refreshTracingUi(ctx); - - const reason = getEventReason(event); - if (reason === "new" || reason === "resume" || reason === "fork") { - await rolloverSession( - ctx, - reason === "fork" ? "session_fork" : "session_switch", - getPreviousSessionFile(event), - ); - return; - } - - await ensureSession(ctx, { - reason: "session_start", - createIfMissingRoot: false, - }); - }); - - pi.on("input", (event) => { - if (!isPlainObject(event)) return; - pendingInputEvent = { - text: typeof event.text === "string" ? event.text : undefined, - source: typeof event.source === "string" ? event.source : undefined, - streamingBehavior: - typeof event.streamingBehavior === "string" ? event.streamingBehavior : undefined, - imageCount: Array.isArray(event.images) ? event.images.length : undefined, - }; - }); - - pi.on("before_agent_start", async (event, ctx) => { - refreshTracingUi(ctx); - const session = await ensureSession(ctx, { reason: "agent_start" }); - if (!session || !client || !hasSessionRoot(session)) return; - - if (session.currentTurn) { - await finishTurn("replaced_by_new_prompt", Date.now()); - } - - const startedAt = Date.now(); - session.totalTurns += 1; - const turnSpanId = generateUuid(); - const turnInput = buildTurnInput( - event.prompt, - event.images as readonly ImageLike[] | undefined, - ); - const explicitSkillNames = explicitSkillNamesFromPiInput(pendingInputEvent); - const explicitSkillMetadata = explicitSkillRequestMetadata(explicitSkillNames); - const turnSpan = client.startSpan({ - spanId: turnSpanId, - rootSpanId: session.traceRootSpanId, - parentSpanId: session.rootSpanId, - startedAt, - input: turnInput, - metadata: { - turn_number: session.totalTurns, - active_model: safeModelName(ctx.model), - thinking_level: session.thinkingLevel, - ...metadataForPendingInput(pendingInputEvent), - ...explicitSkillMetadata, - }, - name: `Turn ${session.totalTurns}`, - type: "task", - }); - - pendingInputEvent = undefined; - - session.currentTurn = { - spanId: turnSpanId, - span: turnSpan, - prompt: turnInput, - explicitSkillNames, - llmCalls: [], - llmCallCount: 0, - toolCallCount: 0, - toolStarts: new Map(), - toolParentSpanIds: new Map(), - activatedToolNames: new Set(), - lastAssistantMessage: undefined, - lastOutput: undefined, - error: undefined, - thinkingLevel: session.thinkingLevel, - }; - - store.patch(session.sessionKey, { - totalTurns: session.totalTurns, - lastSeenAt: startedAt, - }); - }); - - pi.on("context", async (event, ctx) => { - if (!activeSession?.currentTurn) return; - const tools = activeToolNames(pi); - activeSession.currentTurn.llmCalls.push({ - startedAt: Date.now(), - input: normalizeContextMessages(event.messages as unknown as readonly AgentMessageLike[]), - activeToolNames: tools, - activatedToolNames: [...activeSession.currentTurn.activatedToolNames], - modelMetadata: modelTraceMetadata(ctx.model), - }); - }); - - pi.on("before_provider_request", async (event) => { - if (!activeSession?.currentTurn) return; - const metadata = providerRequestMetadata(event); - if (!metadata) return; - const pending = pendingLlmCall(activeSession.currentTurn); - if (pending) pending.providerRequest = metadata; - }); - - pi.on("after_provider_response", async (event) => { - if (!activeSession?.currentTurn) return; - const metadata = providerResponseMetadata(event); - if (!metadata) return; - const pending = [...activeSession.currentTurn.llmCalls] - .reverse() - .find((call) => !call.providerResponse); - if (pending) pending.providerResponse = metadata; - }); - - pi.on("message_update", async (event) => { - if (!activeSession?.currentTurn || !isPlainObject(event)) return; - recordStreamingTiming(activeSession.currentTurn, event.assistantMessageEvent, Date.now()); - }); - - pi.on("thinking_level_select", async (event) => { - if (!isPlainObject(event) || typeof event.level !== "string") return; - if (activeSession) activeSession.thinkingLevel = event.level; - if (activeSession?.currentTurn) activeSession.currentTurn.thinkingLevel = event.level; - }); - - pi.on("session_before_compact", async (event, ctx) => { - const session = await ensureSession(ctx, { reason: "compact" }); - if (!session || !client || !hasSessionRoot(session)) return; - - const startedAt = Date.now(); - const compactionSpanId = generateUuid(); - const input = truncateValue({ - custom_instructions: isPlainObject(event) ? event.customInstructions : undefined, - branch_entry_count: Array.isArray(event.branchEntries) - ? event.branchEntries.length - : undefined, - preparation: isPlainObject(event) ? event.preparation : undefined, - }); - const compactionSpan = client.startSpan({ - spanId: compactionSpanId, - rootSpanId: session.traceRootSpanId, - parentSpanId: session.rootSpanId, - startedAt, - input, - metadata: compactionMetadata(event, "session_before_compact"), - name: "Compaction", - type: "task", - }); - - session.currentCompaction = { - spanId: compactionSpanId, - span: compactionSpan, - startedAt, - input, - }; - }); - - pi.on("session_compact", async (event, ctx) => { - const session = await ensureSession(ctx, { reason: "compact" }); - if (!session || !client || !hasSessionRoot(session)) return; - - const compaction = session.currentCompaction ?? { - spanId: generateUuid(), - span: undefined, - startedAt: Date.now(), - input: undefined, - }; - if (!compaction.span) { - compaction.span = client.startSpan({ - spanId: compaction.spanId, - rootSpanId: session.traceRootSpanId, - parentSpanId: session.rootSpanId, - startedAt: compaction.startedAt, - input: compaction.input, - metadata: { - event_type: "session_compact", - }, - name: "Compaction", - type: "task", - }); - } - - client.logSpan(compaction.span, { - output: truncateValue(isPlainObject(event) ? event.compactionEntry : undefined), - metadata: { - ...compactionMetadata(event, "session_compact"), - from_extension: isPlainObject(event) ? event.fromExtension : undefined, - }, - }); - client.endSpan(compaction.span, Date.now()); - session.currentCompaction = undefined; - - store.patch(session.sessionKey, { - lastSeenAt: Date.now(), - }); - }); - - pi.on("session_before_tree", async (event, ctx) => { - const userWantsSummary = isPlainObject(event) - ? booleanProperty(event.preparation, "userWantsSummary") - : undefined; - if (userWantsSummary !== true) return; - - const session = await ensureSession(ctx, { reason: "tree" }); - if (!session || !client || !hasSessionRoot(session)) return; - - const startedAt = Date.now(); - const branchSummarySpanId = generateUuid(); - const input = treePreparationInput(event); - const branchSummarySpan = client.startSpan({ - spanId: branchSummarySpanId, - rootSpanId: session.traceRootSpanId, - parentSpanId: session.rootSpanId, - startedAt, - input, - metadata: { - event_type: "session_before_tree", - user_wants_summary: userWantsSummary, - }, - name: "Branch Summary", - type: "task", - }); - - session.currentBranchSummary = { - spanId: branchSummarySpanId, - span: branchSummarySpan, - startedAt, - input, - }; - }); - - pi.on("session_tree", async (event, ctx) => { - const hasSummaryEntry = isPlainObject(event) && event.summaryEntry !== undefined; - if (!activeSession?.currentBranchSummary && !hasSummaryEntry) return; - - const session = await ensureSession(ctx, { reason: "tree" }); - if (!session || !client || !hasSessionRoot(session)) return; - - const branchSummary = session.currentBranchSummary ?? { - spanId: generateUuid(), - span: undefined, - startedAt: Date.now(), - input: undefined, - }; - if (!branchSummary.span) { - branchSummary.span = client.startSpan({ - spanId: branchSummary.spanId, - rootSpanId: session.traceRootSpanId, - parentSpanId: session.rootSpanId, - startedAt: branchSummary.startedAt, - input: branchSummary.input, - metadata: { - event_type: "session_tree", - }, - name: "Branch Summary", - type: "task", - }); - } - - client.logSpan(branchSummary.span, { - output: truncateValue(isPlainObject(event) ? event.summaryEntry : undefined), - metadata: { - event_type: "session_tree", - from_extension: isPlainObject(event) ? event.fromExtension : undefined, - new_leaf_id: - isPlainObject(event) && typeof event.newLeafId === "string" ? event.newLeafId : undefined, - old_leaf_id: - isPlainObject(event) && typeof event.oldLeafId === "string" ? event.oldLeafId : undefined, - }, - }); - client.endSpan(branchSummary.span, Date.now()); - session.currentBranchSummary = undefined; - - store.patch(session.sessionKey, { - lastSeenAt: Date.now(), - }); - }); - - pi.on("message_end", async (event) => { - const session = activeSession; - if ( - !session?.currentTurn || - !isAssistantMessage(event.message) || - !client || - !hasSessionRoot(session) - ) { - return; - } - const message = event.message; - - const pending: PendingLlmCall = session.currentTurn.llmCalls.shift() ?? { - startedAt: Date.now(), - input: [{ role: "user", content: session.currentTurn.prompt }], - activatedToolNames: [...session.currentTurn.activatedToolNames], - modelMetadata: {}, - }; - - const requestedModelName = safeModelName(message) ?? message.model; - const responseModel = responseModelName(message); - const modelName = responseModel ?? requestedModelName; - const endedAt = Date.now(); - const normalizedOutput = normalizeAssistantMessage(message); - const error = - message.stopReason === "error" || message.stopReason === "aborted" - ? extractErrorText(message, message.errorMessage) - : undefined; - - session.currentTurn.llmCallCount += 1; - session.currentTurn.lastAssistantMessage = message; - session.currentTurn.lastOutput = normalizedOutput; - if (error) session.currentTurn.error = error; - - const llmSpanId = generateUuid(); - const llmSpan = client.startSpan({ - spanId: llmSpanId, - rootSpanId: session.traceRootSpanId, - parentSpanId: session.currentTurn.spanId, - startedAt: pending.startedAt, - input: pending.input, - metadata: { - ...pending.modelMetadata, - ...pending.providerRequest, - ...streamingTimingMetadata(pending, endedAt), - ...thinkingBlockMetadata(message), - api: message.api, - provider: message.provider, - model: modelName, - requested_model: requestedModelName, - response_model: responseModel, - "pi_coding_agent.response_id": message.responseId, - stop_reason: message.stopReason, - thinking_level: session.currentTurn.thinkingLevel ?? session.thinkingLevel, - "pi_coding_agent.active_tools": pending.activeToolNames, - active_tool_count: pending.activeToolNames?.length, - activated_tools: - pending.activatedToolNames.length > 0 ? pending.activatedToolNames : undefined, - provider_response_status: pending.providerResponse?.status, - provider_response_headers: pending.providerResponse?.headers, - cache_read_tokens: message.usage?.cacheRead, - cache_write_tokens: message.usage?.cacheWrite, - }, - name: modelName || "llm", - type: "llm", - }); - - for (const part of message.content ?? []) { - if (!isPlainObject(part) || part.type !== "toolCall" || typeof part.id !== "string") { - continue; - } - session.currentTurn.toolParentSpanIds.set(part.id, llmSpanId); - } - - client.logSpan(llmSpan, { - output: [normalizedOutput], - error, - metrics: compactMetrics({ - ...usageMetrics(message.usage), - time_to_first_token: secondsBetween(pending.startedAt, pending.firstTokenAt), - }), - }); - client.endSpan(llmSpan, endedAt); - }); - - pi.on("tool_execution_start", async (event) => { - if (!activeSession?.currentTurn) return; - activeSession.currentTurn.toolStarts.set(event.toolCallId, { - startedAt: Date.now(), - args: event.args, - toolName: event.toolName, - }); - }); - - pi.on("tool_execution_end", async (event, ctx) => { - const session = activeSession; - if (!session?.currentTurn || !client || !hasSessionRoot(session)) return; - - const tracked = session.currentTurn.toolStarts.get(event.toolCallId) ?? { - startedAt: Date.now(), - args: undefined, - toolName: event.toolName, - }; - session.currentTurn.toolStarts.delete(event.toolCallId); - const parentLlmSpanId = session.currentTurn.toolParentSpanIds.get(event.toolCallId); - session.currentTurn.toolParentSpanIds.delete(event.toolCallId); - - const endedAt = Date.now(); - session.totalToolCalls += 1; - session.currentTurn.toolCallCount += 1; - - const output = normalizeToolResult(event.result); - const error = event.isError - ? extractErrorText(event.result, `${event.toolName} failed`) - : undefined; - - if (error && !session.currentTurn.error) { - session.currentTurn.error = error; - } - - const activatedTools = addedToolNames(event.result); - for (const toolName of activatedTools) session.currentTurn.activatedToolNames.add(toolName); - const currentActiveTools = activatedTools.length > 0 ? activeToolNames(pi) : undefined; - const currentModelMetadata = activatedTools.length > 0 ? modelTraceMetadata(ctx.model) : {}; - - const skillLoad = skillLoadFromRead(event.toolName, tracked.args); - const skillLoadTrigger = skillLoadTriggerForTurn(session.currentTurn, skillLoad); - const metadataToolName = skillLoad ? "skill" : event.toolName; - const spanName = skillLoad - ? `skill: ${skillLoad.skill_name}` - : formatToolSpanName(event.toolName, tracked.args); - - const toolSpan = client.startSpan({ - spanId: generateUuid(), - rootSpanId: session.traceRootSpanId, - parentSpanId: parentLlmSpanId ?? session.currentTurn.spanId, - startedAt: tracked.startedAt, - input: tracked.args, - metadata: { - tool_name: metadataToolName, - original_tool_name: skillLoad ? event.toolName : undefined, - tool_call_id: event.toolCallId, - is_error: event.isError, - tool_approval: "approved", - parent_llm_span_id: parentLlmSpanId, - activated_tools: activatedTools.length > 0 ? activatedTools : undefined, - activated_tool_count: activatedTools.length > 0 ? activatedTools.length : undefined, - "pi_coding_agent.active_tools": currentActiveTools, - active_tool_count: currentActiveTools?.length, - deferred_tools_mode: currentModelMetadata.deferred_tools_mode, - ...skillLoad, - skill_load_trigger: skillLoadTrigger, - }, - name: spanName, - type: "tool", - }); - - client.logSpan(toolSpan, { - output, - error, - }); - client.endSpan(toolSpan, endedAt); - - store.patch(session.sessionKey, { - totalTurns: session.totalTurns, - totalToolCalls: session.totalToolCalls, - lastSeenAt: endedAt, - }); - }); - - pi.on("agent_end", async (event) => { - if (!activeSession?.currentTurn) return; - if (isPlainObject(event) && event.willRetry === true) return; - const finalAssistant = findLastAssistant(event.messages); - await finishTurn("agent_end", Date.now(), finalAssistant); - }); - - pi.on("session_shutdown", async (event, ctx) => { - if (ctx.hasUI) { - ctx.ui.setStatus(TRACING_STATUS_KEY, undefined); - ctx.ui.setWidget(TRACING_WIDGET_KEY, undefined); - } - - // pi 0.68.0+ exposes a structured reason ("quit" | "reload" | "new" | "resume" - // | "fork"). Older pi hosts pass no payload, so we fall back to the generic - // label to stay backwards-compatible and keep the existing metadata shape. - const reason = getEventReason(event) ?? "session_shutdown"; - logger.debug("session_shutdown", { reason }); - - if (client && !clientInitializationError) { - // On reload the same pi session is about to resume in a freshly imported - // extension instance, which restores its state from the persisted store and - // keeps writing to the existing root span. Finalizing here would close that - // root span out from under the reloaded instance, so we just flush pending - // writes and let the new instance continue the trace. - if (reason !== "reload") { - await finalizeSession(reason); - } - await client.flush(); - } - activeSession = undefined; - pendingInputEvent = undefined; - await store.flush(); - await logger.flush(); - }); - - for (const configIssue of config.configIssues) { - if (configIssue.severity === "error") { - logger.error("Braintrust config issue", configIssue); - } else { - logger.warn("Braintrust config issue", configIssue); - } - } - - logger.debug("Braintrust pi tracing extension loaded", { - enabled: config.enabled, - project: config.projectName, - hasApiKey: Boolean(config.apiKey), - logFile: logger.filePath, - configIssues: config.configIssues, - configHash: shortHash( - JSON.stringify({ - enabled: config.enabled, - project: config.projectName, - debug: config.debug, - stateDir: config.stateDir, - }), - ), - }); -} diff --git a/src/plugins/pi/content/src/state.test.ts b/src/plugins/pi/content/src/state.test.ts deleted file mode 100644 index ce8cac9..0000000 --- a/src/plugins/pi/content/src/state.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; -import { createStateStore } from "./state.ts"; - -const tempDirs: string[] = []; - -afterEach(() => { - while (tempDirs.length > 0) { - rmSync(tempDirs.pop()!, { recursive: true, force: true }); - } -}); - -function makeTempDir(prefix: string): string { - const dir = mkdtempSync(join(tmpdir(), prefix)); - tempDirs.push(dir); - return dir; -} - -describe("createStateStore", () => { - it("loads valid sessions and prunes expired ones on startup", async () => { - const stateDir = makeTempDir("pi-extension-state-"); - const now = Date.now(); - const oldTimestamp = now - 31 * 24 * 60 * 60 * 1000; - - writeFileSync( - join(stateDir, "sessions.json"), - `${JSON.stringify( - { - version: 1, - sessions: { - fresh: { - rootSpanId: "fresh-root", - startedAt: now, - totalTurns: 2, - }, - expired: { - rootSpanId: "old-root", - startedAt: oldTimestamp, - lastSeenAt: oldTimestamp, - }, - invalid: { - startedAt: now, - }, - }, - }, - null, - 2, - )}\n`, - "utf8", - ); - - const store = createStateStore(stateDir); - await store.flush(); - - expect(store.get("fresh")).toMatchObject({ rootSpanId: "fresh-root", totalTurns: 2 }); - expect(store.get("expired")).toBeUndefined(); - expect(store.get("invalid")).toBeUndefined(); - - const persisted = JSON.parse(readFileSync(join(stateDir, "sessions.json"), "utf8")) as { - sessions: Record; - }; - expect(Object.keys(persisted.sessions)).toEqual(["fresh"]); - }); - - it("only writes to disk when persistence is scheduled or flushed", async () => { - const stateDir = makeTempDir("pi-extension-state-"); - const store = createStateStore(stateDir); - - store.set("session-1", { - rootSpanId: "root-1", - startedAt: 1, - totalTurns: 1, - }); - - expect(existsSync(join(stateDir, "sessions.json"))).toBe(false); - - store.schedulePersist(0); - await store.flush(); - - const persisted = JSON.parse(readFileSync(join(stateDir, "sessions.json"), "utf8")) as { - sessions: Record; - }; - expect(persisted.sessions["session-1"]).toEqual({ - rootSpanId: "root-1", - startedAt: 1, - totalTurns: 1, - }); - }); - - it("persists set, patch, and delete operations", async () => { - const stateDir = makeTempDir("pi-extension-state-"); - const store = createStateStore(stateDir); - - store.set("session-1", { - rootSpanId: "root-1", - startedAt: 1, - totalTurns: 1, - }); - store.patch("session-1", { - totalTurns: 3, - totalToolCalls: 5, - }); - await store.flush(); - - expect(store.get("session-1")).toEqual({ - rootSpanId: "root-1", - startedAt: 1, - totalTurns: 3, - totalToolCalls: 5, - }); - - let persisted = JSON.parse(readFileSync(join(stateDir, "sessions.json"), "utf8")) as { - sessions: Record; - }; - expect(persisted.sessions["session-1"]).toEqual({ - rootSpanId: "root-1", - startedAt: 1, - totalTurns: 3, - totalToolCalls: 5, - }); - - store.delete("session-1"); - await store.flush(); - persisted = JSON.parse(readFileSync(join(stateDir, "sessions.json"), "utf8")) as { - sessions: Record; - }; - expect(persisted.sessions).toEqual({}); - }); -}); diff --git a/src/plugins/pi/content/src/state.ts b/src/plugins/pi/content/src/state.ts deleted file mode 100644 index 5111fbf..0000000 --- a/src/plugins/pi/content/src/state.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { existsSync, readFileSync } from "node:fs"; -import { rename, writeFile } from "node:fs/promises"; -import { join } from "node:path"; -import type { Logger, PersistedSessionState, StateStore } from "./types.ts"; -import { ensureDir, isPlainObject } from "./utils.ts"; - -const STATE_VERSION = 1; -const RETENTION_MS = 30 * 24 * 60 * 60 * 1000; -const DEFAULT_PERSIST_DEBOUNCE_MS = 50; - -interface StoreData { - version: number; - sessions: Record; -} - -interface StoreBackend { - stateFile: string; - state: StoreData; - dirty: boolean; - scheduledPersistTimer?: ReturnType; - pendingPersist: Promise; - lastPersistedSnapshot?: string; - logger?: Logger; -} - -const storeBackends = new Map(); - -function normalizeSessions(value: unknown): Record { - if (!isPlainObject(value)) return {}; - - const sessions: Record = {}; - for (const [key, session] of Object.entries(value)) { - if ( - isPlainObject(session) && - typeof session.rootSpanId === "string" && - typeof session.startedAt === "number" - ) { - sessions[key] = { - rootSpanId: session.rootSpanId, - rootSpanRecordId: - typeof session.rootSpanRecordId === "string" ? session.rootSpanRecordId : undefined, - traceRootSpanId: - typeof session.traceRootSpanId === "string" ? session.traceRootSpanId : undefined, - parentSpanId: typeof session.parentSpanId === "string" ? session.parentSpanId : undefined, - traceUrl: typeof session.traceUrl === "string" ? session.traceUrl : undefined, - startedAt: session.startedAt, - totalTurns: typeof session.totalTurns === "number" ? session.totalTurns : undefined, - totalToolCalls: - typeof session.totalToolCalls === "number" ? session.totalToolCalls : undefined, - lastSeenAt: typeof session.lastSeenAt === "number" ? session.lastSeenAt : undefined, - sessionFile: typeof session.sessionFile === "string" ? session.sessionFile : undefined, - }; - } - } - - return sessions; -} - -function serializedState(backend: StoreBackend): string { - return `${JSON.stringify(backend.state, null, 2)}\n`; -} - -function markDirty(backend: StoreBackend): void { - backend.dirty = true; -} - -function pruneExpired(backend: StoreBackend): boolean { - const cutoff = Date.now() - RETENTION_MS; - let changed = false; - - for (const [key, value] of Object.entries(backend.state.sessions)) { - if ((value.lastSeenAt ?? value.startedAt ?? 0) < cutoff) { - delete backend.state.sessions[key]; - changed = true; - } - } - - if (changed) markDirty(backend); - return changed; -} - -async function persistDirty(backend: StoreBackend): Promise { - if (!backend.dirty) { - await backend.pendingPersist.catch(() => {}); - return; - } - - const snapshot = serializedState(backend); - backend.dirty = false; - - backend.pendingPersist = backend.pendingPersist - .catch(() => {}) - .then(async () => { - if (snapshot === backend.lastPersistedSnapshot) return; - - const tempFile = `${backend.stateFile}.tmp`; - try { - await writeFile(tempFile, snapshot, "utf8"); - await rename(tempFile, backend.stateFile); - backend.lastPersistedSnapshot = snapshot; - } catch (error) { - markDirty(backend); - backend.logger?.warn("failed to persist state store", { error: String(error) }); - } - }); - - await backend.pendingPersist.catch(() => {}); - if (backend.dirty) await persistDirty(backend); -} - -function schedulePersist(backend: StoreBackend, delayMs = DEFAULT_PERSIST_DEBOUNCE_MS): void { - if (backend.scheduledPersistTimer) clearTimeout(backend.scheduledPersistTimer); - backend.scheduledPersistTimer = setTimeout(() => { - backend.scheduledPersistTimer = undefined; - void persistDirty(backend); - }, delayMs); - backend.scheduledPersistTimer.unref?.(); -} - -function createBackend(stateFile: string, logger?: Logger): StoreBackend { - let state: StoreData = { - version: STATE_VERSION, - sessions: {}, - }; - - if (existsSync(stateFile)) { - try { - const parsed = JSON.parse(readFileSync(stateFile, "utf8")) as unknown; - if (isPlainObject(parsed)) { - state = { - version: STATE_VERSION, - sessions: normalizeSessions(parsed.sessions), - }; - } - } catch (error) { - logger?.warn("failed to load state store", { error: String(error) }); - } - } - - const backend: StoreBackend = { - stateFile, - state, - dirty: false, - pendingPersist: Promise.resolve(), - logger, - }; - - backend.lastPersistedSnapshot = serializedState(backend); - if (pruneExpired(backend)) schedulePersist(backend, 0); - return backend; -} - -export function createStateStore(stateDir: string, logger?: Logger): StateStore { - ensureDir(stateDir); - const stateFile = join(stateDir, "sessions.json"); - - let backend = storeBackends.get(stateFile); - if (!backend) { - backend = createBackend(stateFile, logger); - storeBackends.set(stateFile, backend); - } else if (!backend.logger) { - backend.logger = logger; - } - - return { - get(sessionKey) { - return backend.state.sessions[sessionKey]; - }, - set(sessionKey, value) { - backend.state.sessions[sessionKey] = value; - markDirty(backend); - return backend.state.sessions[sessionKey]; - }, - patch(sessionKey, patch) { - backend.state.sessions[sessionKey] = { - ...backend.state.sessions[sessionKey], - ...patch, - } as PersistedSessionState; - markDirty(backend); - return backend.state.sessions[sessionKey]; - }, - delete(sessionKey) { - delete backend.state.sessions[sessionKey]; - markDirty(backend); - }, - schedulePersist(delayMs) { - schedulePersist(backend, delayMs); - }, - async flush() { - if (backend.scheduledPersistTimer) { - clearTimeout(backend.scheduledPersistTimer); - backend.scheduledPersistTimer = undefined; - } - await persistDirty(backend); - }, - }; -} diff --git a/src/plugins/pi/content/src/types.ts b/src/plugins/pi/content/src/types.ts deleted file mode 100644 index d5034f7..0000000 --- a/src/plugins/pi/content/src/types.ts +++ /dev/null @@ -1,194 +0,0 @@ -export type JsonPrimitive = string | number | boolean | null; -export type JsonValue = JsonPrimitive | JsonObject | JsonValue[]; - -export interface JsonObject { - [key: string]: JsonValue | undefined; -} - -export type LogLevel = "debug" | "info" | "warn" | "error"; - -export interface ConfigIssue { - path: string; - message: string; - severity: "error" | "warning"; -} - -export interface TraceConfig { - enabled: boolean; - apiKey: string; - apiUrl?: string; - appUrl: string; - profile?: string; - orgName?: string; - projectName: string; - debug: boolean; - logFile?: string; - stateDir: string; - additionalMetadata: JsonObject; - parentSpanId?: string; - rootSpanId?: string; - showUi: boolean; - showTraceLink: boolean; - configIssues: ConfigIssue[]; -} - -export interface Logger { - filePath: string; - debug(message: string, data?: unknown): void; - info(message: string, data?: unknown): void; - warn(message: string, data?: unknown): void; - error(message: string, data?: unknown): void; - flush(): Promise; -} - -export interface PersistedSessionState { - rootSpanId: string; - rootSpanRecordId?: string; - traceRootSpanId?: string; - parentSpanId?: string; - traceUrl?: string; - startedAt: number; - totalTurns?: number; - totalToolCalls?: number; - lastSeenAt?: number; - sessionFile?: string; -} - -export interface StateStore { - get(sessionKey: string): PersistedSessionState | undefined; - set(sessionKey: string, value: PersistedSessionState): PersistedSessionState; - patch(sessionKey: string, patch: Partial): PersistedSessionState; - delete(sessionKey: string): void; - schedulePersist(delayMs?: number): void; - flush(): Promise; -} - -export interface TextContentLike { - type: "text"; - text?: string; -} - -export interface ThinkingContentLike { - type: "thinking"; - thinking?: string; - thinkingSignature?: string; - redacted?: boolean; -} - -export interface ToolCallContentLike { - type: "toolCall"; - id: string; - name?: string; - arguments?: Record; -} - -export interface ImageContentLike { - type: "image"; - mimeType?: string; -} - -export type ContentPartLike = - | TextContentLike - | ThinkingContentLike - | ToolCallContentLike - | ImageContentLike - | { - type?: string; - [key: string]: unknown; - }; - -export interface ImageLike { - mimeType?: string; - source?: { - mediaType?: string; - }; -} - -export interface UsageLike { - input?: number; - output?: number; - cacheRead?: number; - cacheWrite?: number; - cacheWrite1h?: number; - reasoning?: number; - totalTokens?: number; - cost?: { - input?: number; - output?: number; - cacheRead?: number; - cacheWrite?: number; - total?: number; - }; -} - -export interface UserMessageLike { - role: "user"; - content?: unknown; -} - -export interface AssistantMessageLike { - role: "assistant"; - content?: ContentPartLike[]; - api?: string; - provider?: string; - model?: string; - responseModel?: string; - responseId?: string; - usage?: UsageLike; - stopReason?: string; - errorMessage?: string; - timestamp?: number; -} - -export interface ToolResultMessageLike { - role: "toolResult"; - toolCallId?: string; - toolName?: string; - content?: unknown; - details?: unknown; - isError?: boolean; -} - -export type AgentMessageLike = - | UserMessageLike - | AssistantMessageLike - | ToolResultMessageLike - | { - role?: string; - content?: unknown; - }; - -export interface NormalizedUserMessage { - role: "user"; - content: string; -} - -export interface NormalizedAssistantMessage { - role: "assistant"; - content: string; - tool_calls?: Array<{ - id?: string; - type: "function"; - function: { - name?: string; - arguments: string; - }; - }>; - reasoning?: Array<{ - id: string; - content: string; - }>; -} - -export interface NormalizedToolMessage { - role: "tool"; - content: string; - tool_call_id?: string; - name?: string; - is_error: boolean; -} - -export type NormalizedAgentMessage = - | NormalizedUserMessage - | NormalizedAssistantMessage - | NormalizedToolMessage; diff --git a/src/plugins/pi/content/src/utils.test.ts b/src/plugins/pi/content/src/utils.test.ts deleted file mode 100644 index b4b26f3..0000000 --- a/src/plugins/pi/content/src/utils.test.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { - buildTurnInput, - extractErrorText, - formatToolSpanName, - gitMetadataForCwd, - normalizeAssistantMessage, - normalizeContextMessages, - normalizeToolResult, - repoSlugForCwd, - redactGitRemoteUrl, - rootSpanName, - sessionKeyFor, -} from "./utils.ts"; - -const tempDirs: string[] = []; - -afterEach(() => { - vi.restoreAllMocks(); - while (tempDirs.length > 0) { - rmSync(tempDirs.pop()!, { recursive: true, force: true }); - } -}); - -function makeTempDir(prefix: string): string { - const dir = mkdtempSync(join(tmpdir(), prefix)); - tempDirs.push(dir); - return dir; -} - -function makeGitRepo(): { dir: string; commit: string } { - const dir = makeTempDir("pi-extension-git-metadata-"); - execFileSync("git", ["init"], { cwd: dir, stdio: "ignore" }); - execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: dir }); - execFileSync("git", ["config", "user.name", "Test User"], { cwd: dir }); - writeFileSync(join(dir, "README.md"), "hello\n"); - execFileSync("git", ["add", "README.md"], { cwd: dir }); - execFileSync("git", ["commit", "-m", "init"], { cwd: dir, stdio: "ignore" }); - execFileSync("git", ["branch", "-M", "main"], { cwd: dir }); - execFileSync("git", ["remote", "add", "origin", "https://token@github.com/acme/app.git"], { - cwd: dir, - }); - const commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: dir, encoding: "utf8" }).trim(); - return { dir, commit }; -} - -describe("utils", () => { - it("normalizes assistant messages with text, reasoning, and tool calls", () => { - const normalized = normalizeAssistantMessage({ - role: "assistant", - content: [ - { type: "text", text: "First line" }, - { type: "thinking", thinking: "Plan the next step" }, - { - type: "toolCall", - id: "call-1", - name: "read", - arguments: { path: "package.json" }, - }, - ], - }); - - expect(normalized).toEqual({ - role: "assistant", - content: "First line", - reasoning: [{ id: "thinking", content: "Plan the next step" }], - tool_calls: [ - { - id: "call-1", - type: "function", - function: { - name: "read", - arguments: '{"path":"package.json"}', - }, - }, - ], - }); - }); - - it("omits empty signed thinking content without exposing its signature", () => { - const normalized = normalizeAssistantMessage({ - role: "assistant", - content: [ - { type: "thinking", thinking: "", thinkingSignature: "opaque-signature" }, - { type: "text", text: "Visible answer" }, - ], - }); - - expect(normalized).toEqual({ - role: "assistant", - content: "Visible answer", - }); - expect(JSON.stringify(normalized)).not.toContain("opaque-signature"); - }); - - it("normalizes mixed context messages into Braintrust-friendly shapes", () => { - const messages = normalizeContextMessages([ - { role: "user", content: [{ type: "text", text: "Use the docs" }] }, - { - role: "assistant", - content: [ - { type: "text", text: "I will inspect the config" }, - { type: "thinking", redacted: true }, - ], - }, - { - role: "toolResult", - toolCallId: "tool-1", - toolName: "read", - isError: false, - content: [{ type: "text", text: "README contents" }], - }, - { role: "system", content: "ignored" }, - ]); - - expect(messages).toEqual([ - { role: "user", content: "Use the docs" }, - { - role: "assistant", - content: "I will inspect the config", - reasoning: [{ id: "thinking", content: "[thinking redacted]" }], - }, - { - role: "tool", - content: "README contents", - tool_call_id: "tool-1", - name: "read", - is_error: false, - }, - ]); - }); - - it("normalizes structured tool results and extracts readable error text", () => { - const result = normalizeToolResult({ - content: [ - { type: "text", text: "command failed" }, - { type: "thinking", redacted: true }, - ], - details: { exitCode: 1, stderr: "boom" }, - isError: true, - }); - - expect(result).toEqual({ - content: "command failed\n[thinking redacted]", - details: { exitCode: 1, stderr: "boom" }, - isError: true, - }); - - expect( - extractErrorText( - { - content: [{ type: "text", text: "tool exploded" }], - }, - "fallback message", - ), - ).toBe("tool exploded"); - }); - - it("formats tool span names and builds turn inputs", () => { - expect(formatToolSpanName("read", { path: "/tmp/project/package.json" })).toBe( - "read: package.json", - ); - expect(formatToolSpanName("bash", { command: "npm test -- --runInBand" })).toBe( - "bash: npm test -- --runInBand", - ); - - expect( - buildTurnInput("Summarize these screenshots", [ - { source: { mediaType: "image/png" } }, - { mimeType: "image/jpeg" }, - ]), - ).toBe("Summarize these screenshots\n[image/png]\n[image/jpeg]"); - }); - - it("scopes ephemeral session keys by project cwd", () => { - const projectA = makeTempDir("pi-extension-project-a-"); - const projectB = makeTempDir("pi-extension-project-b-"); - - expect(sessionKeyFor(undefined, "automation-session", projectA)).toBe( - sessionKeyFor(undefined, "automation-session", projectA), - ); - expect(sessionKeyFor(undefined, "automation-session", projectA)).not.toBe( - sessionKeyFor(undefined, "automation-session", projectB), - ); - expect(sessionKeyFor("/tmp/session.json", "automation-session", projectA)).toBe( - "file:/tmp/session.json", - ); - }); - - it("prefers owner/repo from git origin for the root span name", () => { - const repoDir = makeTempDir("pi-extension-git-"); - execFileSync("git", ["init"], { cwd: repoDir, stdio: "ignore" }); - execFileSync( - "git", - ["remote", "add", "origin", "git@github.com:braintrustdata/braintrust-pi-extension.git"], - { cwd: repoDir, stdio: "ignore" }, - ); - - expect(repoSlugForCwd(repoDir)).toBe("braintrustdata/braintrust-pi-extension"); - expect(rootSpanName(repoDir)).toBe("pi: braintrustdata/braintrust-pi-extension"); - }); - - it("redacts URL-style git remote credentials", () => { - expect(redactGitRemoteUrl("https://token:secret@github.com/acme/app.git")).toBe( - "https://github.com/acme/app.git", - ); - expect(redactGitRemoteUrl("git@github.com:acme/app.git")).toBe("git@github.com:acme/app.git"); - }); - - it("captures minimal git attribution metadata", () => { - const repo = makeGitRepo(); - - expect(gitMetadataForCwd(repo.dir)).toEqual({ - git_origin_url: "https://github.com/acme/app.git", - git_branch: "main", - git_commit_sha: repo.commit, - }); - }); - - it("omits git_branch when HEAD is detached", () => { - const repo = makeGitRepo(); - execFileSync("git", ["checkout", "--detach", "HEAD"], { cwd: repo.dir, stdio: "ignore" }); - - expect(gitMetadataForCwd(repo.dir)).toEqual({ - git_origin_url: "https://github.com/acme/app.git", - git_commit_sha: repo.commit, - }); - }); - - it("falls back to the cwd basename when no git origin is available", () => { - const dir = makeTempDir("pi-extension-no-git-"); - - expect(repoSlugForCwd(dir)).toBeUndefined(); - expect(rootSpanName(dir)).toBe(`pi: ${dir.split("/").at(-1)}`); - }); -}); diff --git a/src/plugins/pi/content/src/utils.ts b/src/plugins/pi/content/src/utils.ts deleted file mode 100644 index bc8d836..0000000 --- a/src/plugins/pi/content/src/utils.ts +++ /dev/null @@ -1,425 +0,0 @@ -import * as childProcess from "node:child_process"; -import { createHash, randomUUID } from "node:crypto"; -import { mkdirSync } from "node:fs"; -import { appendFile, mkdir } from "node:fs/promises"; -import { basename, dirname, resolve } from "node:path"; -import type { - AgentMessageLike, - AssistantMessageLike, - ContentPartLike, - ImageLike, - NormalizedAgentMessage, - NormalizedAssistantMessage, - ToolResultMessageLike, -} from "./types.ts"; - -export function isPlainObject(value: unknown): value is Record { - return Boolean(value) && typeof value === "object" && !Array.isArray(value); -} - -export function ensureDir(path: string | undefined): void { - if (!path) return; - mkdirSync(path, { recursive: true }); -} - -export function toUnixSeconds(timestampMs: number): number { - return timestampMs / 1000; -} - -export function generateUuid(): string { - return randomUUID(); -} - -export function sessionKeyFor( - sessionFile: string | undefined, - sessionId: string | undefined, - cwd = process.cwd(), -): string { - if (sessionFile) return `file:${sessionFile}`; - const projectKey = shortHash(resolve(cwd)); - return `ephemeral:${projectKey}:${sessionId ?? generateUuid()}`; -} - -export function coerceToString(value: unknown): string | undefined { - if (typeof value === "string") return value; - if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") { - return String(value); - } - return undefined; -} - -export function shortHash(value: unknown): string { - return createHash("sha256").update(safeStringify(value)).digest("hex").slice(0, 12); -} - -export function parseBoolean(value: unknown, fallback = false): boolean { - if (value === undefined || value === null || value === "") return fallback; - if (typeof value === "boolean") return value; - const normalized = coerceToString(value)?.trim().toLowerCase(); - if (!normalized) return fallback; - if (["true", "1", "yes", "y", "on"].includes(normalized)) return true; - if (["false", "0", "no", "n", "off"].includes(normalized)) return false; - return fallback; -} - -export function safeJsonParse(value: string, fallback: T): T { - try { - return JSON.parse(value) as T; - } catch { - return fallback; - } -} - -export function safeStringify(value: unknown): string { - const primitive = coerceToString(value); - if (primitive !== undefined) return primitive; - try { - return JSON.stringify(value) ?? ""; - } catch { - return ""; - } -} - -export function truncateString(value: unknown, maxLength = 10_000): string { - const text = typeof value === "string" ? value : safeStringify(value); - if (text.length <= maxLength) return text; - return `${text.slice(0, maxLength)}… [truncated ${text.length - maxLength} chars]`; -} - -export function truncateValue(value: unknown, maxLength = 10_000): unknown { - if (value === null || value === undefined) return value; - if (typeof value === "string") return truncateString(value, maxLength); - if (typeof value === "number" || typeof value === "boolean") return value; - if (Array.isArray(value)) return value.map((item) => truncateValue(item, maxLength)); - if (isPlainObject(value)) { - const result: Record = {}; - for (const [key, item] of Object.entries(value)) { - result[key] = truncateValue(item, maxLength); - } - return result; - } - return truncateString(safeStringify(value), maxLength); -} - -function contentPartToText(item: ContentPartLike): string { - if (item.type === "text") { - return typeof item.text === "string" ? item.text : ""; - } - - if (item.type === "thinking") { - return item.redacted - ? "[thinking redacted]" - : typeof item.thinking === "string" - ? item.thinking - : ""; - } - - if (item.type === "image") { - return `[image:${typeof item.mimeType === "string" ? item.mimeType : "unknown"}]`; - } - - if (item.type === "toolCall") { - const toolName = typeof item.name === "string" ? item.name : "unknown"; - return `[toolCall:${toolName}] ${safeStringify(item.arguments ?? {})}`; - } - - return `[${typeof item.type === "string" ? item.type : "content"}] ${safeStringify(item)}`; -} - -export function contentToText(content: unknown): string { - if (content === null || content === undefined) return ""; - if (typeof content === "string") return truncateString(content); - if (!Array.isArray(content)) return truncateString(safeStringify(content)); - - const lines: string[] = []; - for (const item of content) { - if (!isPlainObject(item)) { - lines.push(safeStringify(item)); - continue; - } - - lines.push(contentPartToText(item as ContentPartLike)); - } - - return truncateString(lines.filter(Boolean).join("\n")); -} - -export function normalizeUserContent(content: unknown): string { - return contentToText(content); -} - -export function normalizeAssistantMessage( - message: AssistantMessageLike, -): NormalizedAssistantMessage { - const textParts: string[] = []; - const thinkingParts: string[] = []; - const toolCalls: NonNullable = []; - - for (const part of message.content ?? []) { - if (!isPlainObject(part)) continue; - - if (part.type === "text") { - textParts.push(typeof part.text === "string" ? part.text : ""); - continue; - } - - if (part.type === "thinking") { - if (part.redacted) { - thinkingParts.push("[thinking redacted]"); - } else if (typeof part.thinking === "string" && part.thinking.length > 0) { - thinkingParts.push(part.thinking); - } - continue; - } - - if (part.type === "toolCall") { - toolCalls.push({ - id: typeof part.id === "string" ? part.id : undefined, - type: "function", - function: { - name: typeof part.name === "string" ? part.name : undefined, - arguments: truncateString(safeStringify(part.arguments ?? {})), - }, - }); - } - } - - const normalized: NormalizedAssistantMessage = { - role: "assistant", - content: truncateString(textParts.join("\n")), - }; - - if (toolCalls.length > 0) normalized.tool_calls = toolCalls; - if (thinkingParts.length > 0) { - normalized.reasoning = [{ id: "thinking", content: truncateString(thinkingParts.join("\n")) }]; - } - - return normalized; -} - -export function normalizeAgentMessage( - message: AgentMessageLike, -): NormalizedAgentMessage | undefined { - if (!message || typeof message !== "object" || !("role" in message)) return undefined; - - if (message.role === "user") { - return { - role: "user", - content: normalizeUserContent(message.content), - }; - } - - if (message.role === "assistant") { - return normalizeAssistantMessage(message as AssistantMessageLike); - } - - if (message.role === "toolResult") { - const toolMessage = message as ToolResultMessageLike; - return { - role: "tool", - content: contentToText(toolMessage.content), - tool_call_id: toolMessage.toolCallId, - name: toolMessage.toolName, - is_error: Boolean(toolMessage.isError), - }; - } - - return undefined; -} - -export function normalizeContextMessages( - messages: readonly AgentMessageLike[] | undefined, -): NormalizedAgentMessage[] { - return (messages ?? []) - .map(normalizeAgentMessage) - .filter((message): message is NormalizedAgentMessage => Boolean(message)); -} - -export function normalizeToolResult(result: unknown): unknown { - if (result === null || result === undefined) return undefined; - if (typeof result === "string") return truncateString(result); - - if (isPlainObject(result)) { - const normalized: Record = {}; - - if ("content" in result && result.content !== undefined) { - normalized.content = contentToText(result.content); - } - - if ("details" in result && result.details !== undefined) { - normalized.details = truncateValue(result.details); - } - - if ("isError" in result) { - normalized.isError = Boolean(result.isError); - } - - if (Object.keys(normalized).length > 0) return normalized; - } - - return truncateValue(result); -} - -export function extractErrorText(value: unknown, fallback: string | undefined): string | undefined { - if (!value) return fallback; - if (typeof value === "string") return truncateString(value); - if (isPlainObject(value)) { - if (typeof value.errorMessage === "string") return truncateString(value.errorMessage); - if (typeof value.message === "string") return truncateString(value.message); - if (Array.isArray(value.content)) { - const text = contentToText(value.content); - if (text) return text; - } - } - return fallback; -} - -export function formatToolSpanName(toolName: string, args: unknown = {}): string { - const objectArgs = isPlainObject(args) ? args : {}; - const pathLike = - objectArgs.path ?? - objectArgs.file ?? - objectArgs.filePath ?? - objectArgs.target ?? - objectArgs.sessionDir; - if (typeof pathLike === "string") { - return `${toolName}: ${basename(pathLike)}`; - } - - if (toolName === "bash" && typeof objectArgs.command === "string") { - const command = objectArgs.command.replace(/\s+/g, " ").trim(); - return `bash: ${truncateString(command, 60)}`; - } - - return toolName; -} - -export function buildTurnInput( - prompt: string, - images: readonly ImageLike[] | undefined = [], -): string { - const input = [prompt ?? ""]; - for (const image of images ?? []) { - const type = image?.source?.mediaType ?? image?.mimeType ?? "image"; - input.push(`[${type}]`); - } - return truncateString(input.filter(Boolean).join("\n")); -} - -const gitRemoteRepoCache = new Map(); - -export interface GitMetadata { - git_origin_url?: string; - git_branch?: string; - git_commit_sha?: string; -} - -export function redactGitRemoteUrl(remoteUrl: string): string { - const trimmed = remoteUrl.trim(); - if (!trimmed) return trimmed; - - try { - const parsed = new URL(trimmed); - if (parsed.username || parsed.password) { - parsed.username = ""; - parsed.password = ""; - } - return parsed.toString(); - } catch { - return trimmed; - } -} - -function runGit(cwd: string, args: string[]): string | undefined { - try { - const result = childProcess.spawnSync("git", ["-C", cwd, ...args], { - encoding: "utf8", - env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" }, - timeout: 500, - windowsHide: true, - }); - - if (result.status === 0 && typeof result.stdout === "string") { - const value = result.stdout.trim(); - return value.length > 0 ? value : undefined; - } - } catch { - return undefined; - } - - return undefined; -} - -function parseGitRemoteRepo(remoteUrl: string): string | undefined { - const trimmed = remoteUrl.trim(); - if (!trimmed) return undefined; - - const scpLikeMatch = trimmed.match(/^[^@\s]+@[^:\s]+:(.+)$/); - const path = scpLikeMatch - ? scpLikeMatch[1] - : (() => { - try { - return new URL(trimmed).pathname; - } catch { - return undefined; - } - })(); - - if (!path) return undefined; - - const segments = path - .replace(/^\/+/, "") - .replace(/\.git$/i, "") - .split("/") - .filter(Boolean); - - if (segments.length < 2) return undefined; - return `${segments.at(-2)}/${segments.at(-1)}`; -} - -export function gitMetadataForCwd(cwd: string | undefined): GitMetadata { - if (!cwd) return {}; - - const origin = runGit(cwd, ["remote", "get-url", "origin"]); - const branch = runGit(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"]); - const commit = runGit(cwd, ["rev-parse", "HEAD"]); - - return { - ...(origin ? { git_origin_url: redactGitRemoteUrl(origin) } : {}), - ...(branch ? { git_branch: branch } : {}), - ...(commit ? { git_commit_sha: commit } : {}), - }; -} - -export function repoSlugForCwd(cwd: string): string | undefined { - const resolvedCwd = cwd || process.cwd(); - if (gitRemoteRepoCache.has(resolvedCwd)) { - return gitRemoteRepoCache.get(resolvedCwd); - } - - let repo: string | undefined; - - const origin = runGit(resolvedCwd, ["remote", "get-url", "origin"]); - if (origin) repo = parseGitRemoteRepo(origin); - - gitRemoteRepoCache.set(resolvedCwd, repo); - return repo; -} - -export function rootSpanName(cwd: string): string { - return `pi: ${repoSlugForCwd(cwd) ?? basename(cwd || process.cwd())}`; -} - -export async function writeJsonLog( - filePath: string, - level: string, - message: string, - data?: unknown, -): Promise { - await mkdir(dirname(filePath), { recursive: true }); - await appendFile( - filePath, - `${JSON.stringify({ timestamp: new Date().toISOString(), level, message, data: truncateValue(data) })}\n`, - "utf8", - ); -} diff --git a/src/plugins/pi/validate.sh b/src/plugins/pi/validate.sh index e830620..e74677b 100755 --- a/src/plugins/pi/validate.sh +++ b/src/plugins/pi/validate.sh @@ -19,10 +19,22 @@ for required in package/dist/index.mjs package/dist/index.d.mts package/README.m grep -q "^${required}$" <<<"$contents" || fail "tarball omits $required" done if grep -q '^package/src/' <<<"$contents"; then fail "tarball contains local tracing source"; fi -node -e 'const p=require(process.argv[1]); if (p.dependencies?.braintrust) process.exit(1)' "$TARGET_DIR/package.json" \ - || fail "Braintrust SDK remains a Pi runtime dependency" -if grep -n -E "from [\"']braintrust[\"']|from [\"']\.\/(client|state)[\"']" "$TARGET_DIR/src/index.ts"; then - fail "Pi daemon adapter imports the Braintrust SDK or local span persistence" +node -e ' + const p = require(process.argv[1]); + for (const field of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) { + if (p[field]?.braintrust) process.exit(1); + } +' "$TARGET_DIR/package.json" || fail "Braintrust SDK remains in Pi package dependencies" +for removed in client.ts legacy-processor.ts state.ts types.ts utils.ts; do + [[ ! -e "$TARGET_DIR/src/$removed" ]] || fail "legacy tracing source remains: src/$removed" +done +if grep -RInE \ + "from [\"']braintrust[\"']|initLogger|startSpan|updateSpan|BRAINTRUST_API_KEY|api_key|state_dir|parent_span_id|root_span_id" \ + "$TARGET_DIR/src"; then + fail "Pi source still contains local tracing, credential, or span-management logic" +fi +if grep -Eq '(^|[ /])braintrust@|^[[:space:]]+braintrust:' "$TARGET_DIR/pnpm-lock.yaml"; then + fail "Braintrust SDK remains in the Pi lockfile" fi echo "validate: Pi npm package OK ($TARGET_DIR)"