diff --git a/bt-daemon/src/agents/antigravity.rs b/bt-daemon/src/agents/antigravity.rs new file mode 100644 index 0000000..2e39511 --- /dev/null +++ b/bt-daemon/src/agents/antigravity.rs @@ -0,0 +1,23 @@ +use super::{Agent, Identity, Register, Registrar}; +use std::sync::Arc; + +pub(crate) struct Antigravity; + +pub(crate) const IDENTITY: Identity = Identity { + id: "antigravity", + source: "antigravity", + aliases: &["agy"], + display_name: "Google Antigravity", +}; + +impl Agent for Antigravity { + fn identity(&self) -> &'static Identity { + &IDENTITY + } +} + +impl Register for Antigravity { + fn register(self: Arc, registrar: &mut Registrar) { + registrar.setup.insert(self); + } +} diff --git a/bt-daemon/src/agents/claude.rs b/bt-daemon/src/agents/claude.rs new file mode 100644 index 0000000..47034b6 --- /dev/null +++ b/bt-daemon/src/agents/claude.rs @@ -0,0 +1,23 @@ +use super::{Agent, Identity, Register, Registrar}; +use std::sync::Arc; + +pub(crate) struct Claude; + +pub(crate) const IDENTITY: Identity = Identity { + id: "claude", + source: "claude-code", + aliases: &[], + display_name: "Claude Code", +}; + +impl Agent for Claude { + fn identity(&self) -> &'static Identity { + &IDENTITY + } +} + +impl Register for Claude { + fn register(self: Arc, registrar: &mut Registrar) { + registrar.setup.insert(self); + } +} diff --git a/bt-daemon/src/agents/codex.rs b/bt-daemon/src/agents/codex.rs new file mode 100644 index 0000000..8278a28 --- /dev/null +++ b/bt-daemon/src/agents/codex.rs @@ -0,0 +1,23 @@ +use super::{Agent, Identity, Register, Registrar}; +use std::sync::Arc; + +pub(crate) struct Codex; + +pub(crate) const IDENTITY: Identity = Identity { + id: "codex", + source: "codex", + aliases: &[], + display_name: "Codex", +}; + +impl Agent for Codex { + fn identity(&self) -> &'static Identity { + &IDENTITY + } +} + +impl Register for Codex { + fn register(self: Arc, registrar: &mut Registrar) { + registrar.setup.insert(self); + } +} diff --git a/bt-daemon/src/agents/cursor.rs b/bt-daemon/src/agents/cursor.rs new file mode 100644 index 0000000..bf96fc0 --- /dev/null +++ b/bt-daemon/src/agents/cursor.rs @@ -0,0 +1,23 @@ +use super::{Agent, Identity, Register, Registrar}; +use std::sync::Arc; + +pub(crate) struct Cursor; + +pub(crate) const IDENTITY: Identity = Identity { + id: "cursor", + source: "cursor", + aliases: &[], + display_name: "Cursor", +}; + +impl Agent for Cursor { + fn identity(&self) -> &'static Identity { + &IDENTITY + } +} + +impl Register for Cursor { + fn register(self: Arc, registrar: &mut Registrar) { + registrar.setup.insert(self); + } +} diff --git a/bt-daemon/src/agents/grok.rs b/bt-daemon/src/agents/grok.rs new file mode 100644 index 0000000..7a4f717 --- /dev/null +++ b/bt-daemon/src/agents/grok.rs @@ -0,0 +1,23 @@ +use super::{Agent, Identity, Register, Registrar}; +use std::sync::Arc; + +pub(crate) struct Grok; + +pub(crate) const IDENTITY: Identity = Identity { + id: "grok", + source: "grok", + aliases: &[], + display_name: "Grok", +}; + +impl Agent for Grok { + fn identity(&self) -> &'static Identity { + &IDENTITY + } +} + +impl Register for Grok { + fn register(self: Arc, registrar: &mut Registrar) { + registrar.setup.insert(self); + } +} diff --git a/bt-daemon/src/agents/mod.rs b/bt-daemon/src/agents/mod.rs new file mode 100644 index 0000000..6c2b6f0 --- /dev/null +++ b/bt-daemon/src/agents/mod.rs @@ -0,0 +1,100 @@ +//! One type per supported coding agent. +//! +//! Each agent implements the capability traits it supports, such as +//! [`Setup`], and registers itself with the matching registries in +//! [`Register::register`]. A command looks up agents in its own registry, so +//! an agent without a capability simply does not appear there. + +mod antigravity; +mod claude; +mod codex; +mod cursor; +mod grok; +mod opencode; +mod pi; + +pub(crate) use antigravity::Antigravity; +pub(crate) use claude::Claude; +pub(crate) use codex::Codex; +pub(crate) use cursor::Cursor; +pub(crate) use grok::Grok; +pub(crate) use opencode::OpenCode; +pub(crate) use pi::Pi; + +use crate::setup::Setup; +use std::sync::{Arc, OnceLock}; + +/// The names an agent is known by. +#[derive(Debug)] +pub(crate) struct Identity { + /// The CLI name, which also names the agent's settings file. + pub id: &'static str, + /// The `source` its hooks report and its translator is registered under. + pub source: &'static str, + /// Other accepted spellings. + pub aliases: &'static [&'static str], + pub display_name: &'static str, +} + +impl Identity { + pub fn matches(&self, name: &str) -> bool { + self.id == name || self.source == name || self.aliases.contains(&name) + } +} + +/// Implemented by every agent type and required by every capability trait, +/// so any registry can find an agent by name. +pub(crate) trait Agent: Send + Sync + 'static { + fn identity(&self) -> &'static Identity; +} + +/// The agents that support one capability. +pub(crate) struct Agents { + agents: Vec>, +} + +impl Default for Agents { + fn default() -> Self { + Self { agents: Vec::new() } + } +} + +impl Agents { + pub fn insert(&mut self, agent: Arc) { + self.agents.push(agent); + } + + /// Find an agent by its id, source, or an alias. + pub fn get(&self, name: &str) -> Option<&T> { + self.agents + .iter() + .find(|agent| agent.identity().matches(name)) + .map(|agent| &**agent) + } +} + +/// Every capability registry. Agents add themselves in [`Register::register`]. +#[derive(Default)] +pub(crate) struct Registrar { + pub setup: Agents, +} + +pub(crate) trait Register { + fn register(self: Arc, registrar: &mut Registrar); +} + +/// The registries for all supported agents. +pub(crate) fn registrar() -> &'static Registrar { + static REGISTRAR: OnceLock = OnceLock::new(); + REGISTRAR.get_or_init(|| { + let mut registrar = Registrar::default(); + Arc::new(Antigravity).register(&mut registrar); + Arc::new(Claude).register(&mut registrar); + Arc::new(Codex).register(&mut registrar); + Arc::new(Cursor).register(&mut registrar); + Arc::new(Grok).register(&mut registrar); + Arc::new(OpenCode).register(&mut registrar); + Arc::new(Pi).register(&mut registrar); + registrar + }) +} diff --git a/bt-daemon/src/agents/opencode.rs b/bt-daemon/src/agents/opencode.rs new file mode 100644 index 0000000..7d61790 --- /dev/null +++ b/bt-daemon/src/agents/opencode.rs @@ -0,0 +1,23 @@ +use super::{Agent, Identity, Register, Registrar}; +use std::sync::Arc; + +pub(crate) struct OpenCode; + +pub(crate) const IDENTITY: Identity = Identity { + id: "opencode", + source: "opencode", + aliases: &["open-code"], + display_name: "OpenCode", +}; + +impl Agent for OpenCode { + fn identity(&self) -> &'static Identity { + &IDENTITY + } +} + +impl Register for OpenCode { + fn register(self: Arc, registrar: &mut Registrar) { + registrar.setup.insert(self); + } +} diff --git a/bt-daemon/src/agents/pi.rs b/bt-daemon/src/agents/pi.rs new file mode 100644 index 0000000..deede7e --- /dev/null +++ b/bt-daemon/src/agents/pi.rs @@ -0,0 +1,23 @@ +use super::{Agent, Identity, Register, Registrar}; +use std::sync::Arc; + +pub(crate) struct Pi; + +pub(crate) const IDENTITY: Identity = Identity { + id: "pi", + source: "pi", + aliases: &[], + display_name: "Pi", +}; + +impl Agent for Pi { + fn identity(&self) -> &'static Identity { + &IDENTITY + } +} + +impl Register for Pi { + fn register(self: Arc, registrar: &mut Registrar) { + registrar.setup.insert(self); + } +} diff --git a/bt-daemon/src/args.rs b/bt-daemon/src/args.rs index 16e5d0e..90ca7b9 100644 --- a/bt-daemon/src/args.rs +++ b/bt-daemon/src/args.rs @@ -6,6 +6,7 @@ use clap::{Args, ValueEnum}; use std::ffi::OsString; use std::path::PathBuf; +use crate::agents::{self, Agent, Identity}; use crate::wire; /// Arguments for `serve`. @@ -194,6 +195,17 @@ pub enum ParentObjectType { PlaygroundLogs, } +impl ImportSource { + pub(crate) fn identity(self) -> &'static Identity { + match self { + Self::Codex => agents::Codex.identity(), + Self::Cursor => agents::Cursor.identity(), + Self::Claude => agents::Claude.identity(), + Self::Antigravity => agents::Antigravity.identity(), + } + } +} + impl From for SpanObjectType { fn from(value: ParentObjectType) -> Self { match value { diff --git a/bt-daemon/src/lib.rs b/bt-daemon/src/lib.rs index d31d355..66ba37d 100644 --- a/bt-daemon/src/lib.rs +++ b/bt-daemon/src/lib.rs @@ -12,6 +12,7 @@ pub mod paths; +mod agents; mod args; mod client; mod command_output; diff --git a/bt-daemon/src/setup/antigravity.rs b/bt-daemon/src/setup/antigravity.rs index 35ef130..361751c 100644 --- a/bt-daemon/src/setup/antigravity.rs +++ b/bt-daemon/src/setup/antigravity.rs @@ -1,7 +1,9 @@ //! Google Antigravity: the tracing plugin installed with the `agy` CLI. use super::common::{edit_object, CommandRunner, FileAccess}; +use crate::agents::Antigravity; use crate::paths; +use crate::setup::Setup; use anyhow::bail; use serde_json::Value; use std::path::Path; @@ -51,20 +53,20 @@ fn remove_legacy_registration(config_dir: &Path) -> anyhow::Result<()> { ) } -fn enable_at(runner: &mut impl CommandRunner, config_dir: &Path) -> anyhow::Result<()> { +fn enable_at(runner: &mut dyn CommandRunner, config_dir: &Path) -> anyhow::Result<()> { let home = home(config_dir)?; runner.run_in_home("agy", &["plugin", "install", PLUGIN_SOURCE], home)?; runner.run_in_home("agy", &["plugin", "enable", PLUGIN], home)?; remove_legacy_registration(config_dir) } -fn disable_at(runner: &mut impl CommandRunner, config_dir: &Path) -> anyhow::Result<()> { +fn disable_at(runner: &mut dyn CommandRunner, config_dir: &Path) -> anyhow::Result<()> { // Antigravity's uninstall command is idempotent when the plugin is absent. runner.run_in_home("agy", &["plugin", "uninstall", PLUGIN], home(config_dir)?)?; remove_legacy_registration(config_dir) } -fn update_at(runner: &mut impl CommandRunner, config_dir: &Path) -> anyhow::Result<()> { +fn update_at(runner: &mut dyn CommandRunner, config_dir: &Path) -> anyhow::Result<()> { let home = home(config_dir)?; let plugins = runner.json_in_home("agy", &["plugin", "list"], home)?; let installed = plugins @@ -83,16 +85,18 @@ fn update_at(runner: &mut impl CommandRunner, config_dir: &Path) -> anyhow::Resu runner.run_in_home("agy", &["plugin", "install", PLUGIN_SOURCE], home) } -pub(super) fn enable(runner: &mut impl CommandRunner) -> anyhow::Result<()> { - enable_at(runner, &paths::antigravity_config_dir()) -} +impl Setup for Antigravity { + fn enable(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + enable_at(runner, &paths::antigravity_config_dir()) + } -pub(super) fn disable(runner: &mut impl CommandRunner) -> anyhow::Result<()> { - disable_at(runner, &paths::antigravity_config_dir()) -} + fn disable(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + disable_at(runner, &paths::antigravity_config_dir()) + } -pub(super) fn update(runner: &mut impl CommandRunner) -> anyhow::Result<()> { - update_at(runner, &paths::antigravity_config_dir()) + fn update(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + update_at(runner, &paths::antigravity_config_dir()) + } } #[cfg(test)] diff --git a/bt-daemon/src/setup/claude.rs b/bt-daemon/src/setup/claude.rs index 0f29cb5..0968e39 100644 --- a/bt-daemon/src/setup/claude.rs +++ b/bt-daemon/src/setup/claude.rs @@ -3,7 +3,9 @@ use super::common::{ github_repo_matches, installed_json_version_is_older, plugin_source, CommandRunner, Marketplace, }; +use crate::agents::Claude; use crate::paths; +use crate::setup::Setup; use anyhow::{bail, Context}; use serde_json::Value; use std::path::Path; @@ -42,17 +44,47 @@ fn installed_plugin(value: &Value) -> Option<&Value> { .find(|item| item.get("id").and_then(Value::as_str) == Some(PLUGIN)) } -fn list_plugins(runner: &mut impl CommandRunner) -> anyhow::Result { +fn list_plugins(runner: &mut dyn CommandRunner) -> anyhow::Result { runner.json("claude", &["plugin", "list", "--json"]) } -pub(super) fn enable(runner: &mut impl CommandRunner) -> anyhow::Result<()> { - install(runner)?; - warn_legacy_tracing_env(); - Ok(()) +impl Setup for Claude { + fn enable(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + install(runner)?; + warn_legacy_tracing_env(); + Ok(()) + } + + fn disable(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + if installed_plugin(&list_plugins(runner)?).is_some() { + runner.run("claude", &["plugin", "uninstall", PLUGIN])?; + } + Ok(()) + } + + fn update(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + if installed_plugin(&list_plugins(runner)?).is_none() { + bail!("Claude Code tracing plugin is not installed; run `bt trace enable claude`"); + } + MARKETPLACE.refresh_published(runner, "Claude Code", "claude")?; + runner.run("claude", &["plugin", "update", PLUGIN]) + } + + fn stale(&self) -> bool { + installed_json_version_is_older( + "claude", + &["plugin", "list", "--json"], + |value| { + installed_plugin(value) + .and_then(|plugin| plugin.get("version")) + .and_then(Value::as_str) + }, + PLUGIN_MANIFEST, + ) + } } -fn install(runner: &mut impl CommandRunner) -> anyhow::Result<()> { +fn install(runner: &mut dyn CommandRunner) -> anyhow::Result<()> { // Claude removes a marketplace's installed plugins when that marketplace // is removed, so replacing a stale source requires a fresh installation. if MARKETPLACE.reconcile(runner)? { @@ -71,34 +103,6 @@ fn install(runner: &mut impl CommandRunner) -> anyhow::Result<()> { Ok(()) } -pub(super) fn disable(runner: &mut impl CommandRunner) -> anyhow::Result<()> { - if installed_plugin(&list_plugins(runner)?).is_some() { - runner.run("claude", &["plugin", "uninstall", PLUGIN])?; - } - Ok(()) -} - -pub(super) fn update(runner: &mut impl CommandRunner) -> anyhow::Result<()> { - if installed_plugin(&list_plugins(runner)?).is_none() { - bail!("Claude Code tracing plugin is not installed; run `bt trace enable claude`"); - } - MARKETPLACE.refresh_published(runner, "Claude Code", "claude")?; - runner.run("claude", &["plugin", "update", PLUGIN]) -} - -pub(super) fn stale() -> bool { - installed_json_version_is_older( - "claude", - &["plugin", "list", "--json"], - |value| { - installed_plugin(value) - .and_then(|plugin| plugin.get("version")) - .and_then(Value::as_str) - }, - PLUGIN_MANIFEST, - ) -} - fn legacy_tracing_env_keys(path: &Path) -> anyhow::Result> { let raw = match std::fs::read(path) { Ok(raw) => raw, @@ -232,7 +236,7 @@ mod tests { }]), ]); - update(&mut runner).unwrap(); + Claude.update(&mut runner).unwrap(); assert_eq!( runner.calls, @@ -252,7 +256,7 @@ mod tests { serde_json::json!([{"name": MARKETPLACE_NAME, "source": "directory"}]), ]); - let error = update(&mut runner).unwrap_err().to_string(); + let error = Claude.update(&mut runner).unwrap_err().to_string(); assert_eq!( error, @@ -264,7 +268,7 @@ mod tests { #[test] fn disable_uses_the_uninstall_command() { let mut runner = FakeRunner::new([serde_json::json!([{"id": PLUGIN}])]); - disable(&mut runner).unwrap(); + Claude.disable(&mut runner).unwrap(); assert!(runner.called("claude plugin uninstall trace-claude-code@braintrust-claude-plugin")); } diff --git a/bt-daemon/src/setup/codex.rs b/bt-daemon/src/setup/codex.rs index bfcfdb0..1dcfe1d 100644 --- a/bt-daemon/src/setup/codex.rs +++ b/bt-daemon/src/setup/codex.rs @@ -3,6 +3,8 @@ use super::common::{ github_repo_matches, installed_json_version_is_older, plugin_source, CommandRunner, Marketplace, }; +use crate::agents::Codex; +use crate::setup::Setup; use anyhow::bail; use serde_json::Value; @@ -40,43 +42,45 @@ fn installed_plugin(value: &Value) -> Option<&Value> { .find(|item| item.get("pluginId").and_then(Value::as_str) == Some(PLUGIN)) } -fn list_plugins(runner: &mut impl CommandRunner) -> anyhow::Result { +fn list_plugins(runner: &mut dyn CommandRunner) -> anyhow::Result { runner.json("codex", &["plugin", "list", "--json"]) } -pub(super) fn enable(runner: &mut impl CommandRunner) -> anyhow::Result<()> { - MARKETPLACE.reconcile(runner)?; - // Adding is idempotent and reconciles the installed cache to the refreshed - // marketplace snapshot. - runner.run("codex", &["plugin", "add", PLUGIN]) -} +impl Setup for Codex { + fn enable(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + MARKETPLACE.reconcile(runner)?; + // Adding is idempotent and reconciles the installed cache to the refreshed + // marketplace snapshot. + runner.run("codex", &["plugin", "add", PLUGIN]) + } -pub(super) fn disable(runner: &mut impl CommandRunner) -> anyhow::Result<()> { - if installed_plugin(&list_plugins(runner)?).is_some() { - runner.run("codex", &["plugin", "remove", PLUGIN, "--json"])?; + fn disable(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + if installed_plugin(&list_plugins(runner)?).is_some() { + runner.run("codex", &["plugin", "remove", PLUGIN, "--json"])?; + } + Ok(()) } - Ok(()) -} -pub(super) fn update(runner: &mut impl CommandRunner) -> anyhow::Result<()> { - if installed_plugin(&list_plugins(runner)?).is_none() { - bail!("Codex tracing plugin is not installed; run `bt trace enable codex`"); + fn update(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + if installed_plugin(&list_plugins(runner)?).is_none() { + bail!("Codex tracing plugin is not installed; run `bt trace enable codex`"); + } + MARKETPLACE.refresh_published(runner, "Codex", "codex")?; + runner.run("codex", &["plugin", "add", PLUGIN]) } - MARKETPLACE.refresh_published(runner, "Codex", "codex")?; - runner.run("codex", &["plugin", "add", PLUGIN]) -} -pub(super) fn stale() -> bool { - installed_json_version_is_older( - "codex", - &["plugin", "list", "--json"], - |value| { - installed_plugin(value) - .and_then(|plugin| plugin.get("version")) - .and_then(Value::as_str) - }, - PLUGIN_MANIFEST, - ) + fn stale(&self) -> bool { + installed_json_version_is_older( + "codex", + &["plugin", "list", "--json"], + |value| { + installed_plugin(value) + .and_then(|plugin| plugin.get("version")) + .and_then(Value::as_str) + }, + PLUGIN_MANIFEST, + ) + } } #[cfg(test)] @@ -88,7 +92,7 @@ mod tests { fn installs_from_the_published_marketplace_when_missing() { let mut runner = FakeRunner::new([serde_json::json!({"marketplaces": []})]); - enable(&mut runner).unwrap(); + Codex.enable(&mut runner).unwrap(); assert!( runner.called("codex plugin marketplace add braintrustdata/braintrust-codex-plugin") @@ -108,7 +112,7 @@ mod tests { }] })]); - enable(&mut runner).unwrap(); + Codex.enable(&mut runner).unwrap(); assert!(runner.called("codex plugin marketplace upgrade braintrust-codex-plugins")); assert!(runner.called("codex plugin add trace-codex@braintrust-codex-plugins")); @@ -124,7 +128,7 @@ mod tests { }] })]); - enable(&mut runner).unwrap(); + Codex.enable(&mut runner).unwrap(); assert!(runner.called("codex plugin marketplace remove braintrust-codex-plugins")); assert!( @@ -137,7 +141,7 @@ mod tests { fn update_refuses_to_install_a_missing_plugin() { let mut runner = FakeRunner::new([serde_json::json!({"installed": []})]); - assert!(update(&mut runner).is_err()); + assert!(Codex.update(&mut runner).is_err()); assert!(!runner .calls .iter() @@ -156,7 +160,7 @@ mod tests { }), ]); - update(&mut runner).unwrap(); + Codex.update(&mut runner).unwrap(); assert_eq!( runner.calls, @@ -174,7 +178,7 @@ mod tests { let mut runner = FakeRunner::new([serde_json::json!({ "installed": [{"pluginId": PLUGIN}] })]); - disable(&mut runner).unwrap(); + Codex.disable(&mut runner).unwrap(); assert!(runner.called("codex plugin remove trace-codex@braintrust-codex-plugins --json")); } } diff --git a/bt-daemon/src/setup/common.rs b/bt-daemon/src/setup/common.rs index 8b33bf9..b50f046 100644 --- a/bt-daemon/src/setup/common.rs +++ b/bt-daemon/src/setup/common.rs @@ -20,7 +20,7 @@ macro_rules! plugin_source { pub(super) use plugin_source; /// Runs agent CLIs. Tests substitute a recording fake. -pub(super) trait CommandRunner { +pub(crate) trait CommandRunner { fn json(&mut self, program: &str, args: &[&str]) -> anyhow::Result; fn json_in_home(&mut self, program: &str, args: &[&str], home: &Path) -> anyhow::Result; fn run(&mut self, program: &str, args: &[&str]) -> anyhow::Result<()>; @@ -98,14 +98,14 @@ pub(super) struct Marketplace { } impl Marketplace { - fn list(&self, runner: &mut impl CommandRunner) -> anyhow::Result { + fn list(&self, runner: &mut dyn CommandRunner) -> anyhow::Result { runner.json(self.program, &["plugin", "marketplace", "list", "--json"]) } /// Configure the published marketplace, refreshing it when it is already /// present. A same-name marketplace from another source is replaced; /// returns whether that happened. - pub fn reconcile(&self, runner: &mut impl CommandRunner) -> anyhow::Result { + pub fn reconcile(&self, runner: &mut dyn CommandRunner) -> anyhow::Result { let marketplaces = self.list(runner)?; let replaced = match (self.find)(&marketplaces) { Some(marketplace) if (self.is_published)(marketplace) => { @@ -136,7 +136,7 @@ impl Marketplace { /// run `bt trace enable ` instead. pub fn refresh_published( &self, - runner: &mut impl CommandRunner, + runner: &mut dyn CommandRunner, display_name: &str, agent: &str, ) -> anyhow::Result<()> { diff --git a/bt-daemon/src/setup/cursor/mod.rs b/bt-daemon/src/setup/cursor/mod.rs index 999a17d..8c39998 100644 --- a/bt-daemon/src/setup/cursor/mod.rs +++ b/bt-daemon/src/setup/cursor/mod.rs @@ -5,8 +5,12 @@ mod hooks; pub(crate) use hooks::{discovery_hooks_are_installed_at, CursorManagedHooks}; +use crate::agents::Cursor; use crate::paths; -use crate::setup::common::{github_repo_matches, package_version, plugin_source, version_is_older}; +use crate::setup::common::{ + github_repo_matches, package_version, plugin_source, version_is_older, CommandRunner, +}; +use crate::setup::Setup; use anyhow::{bail, Context}; use serde_json::Value; use std::path::Path; @@ -105,18 +109,40 @@ fn plugin_is_installed_for_platform_at(plugin: &Path, windows: bool) -> bool { true } -pub(super) fn stale() -> bool { - let Some(installed_manifest) = installed_manifest_at(&paths::cursor_plugin_dir()) else { - return false; - }; - if !is_ours(&installed_manifest) { - return false; +impl Setup for Cursor { + fn enable(&self, _runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + enable_at(&paths::cursor_plugin_dir(), &paths::cursor_config_dir()) + } + + fn disable(&self, _runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + disable_at(&paths::cursor_plugin_dir(), &paths::cursor_config_dir()) + } + + fn update(&self, _runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + update_at(&paths::cursor_plugin_dir(), &paths::cursor_config_dir()) + } + + fn stale(&self) -> bool { + let Some(installed_manifest) = installed_manifest_at(&paths::cursor_plugin_dir()) else { + return false; + }; + if !is_ours(&installed_manifest) { + return false; + } + let expected = package_version(PLUGIN_MANIFEST).ok(); + let installed = installed_manifest.get("version").and_then(Value::as_str); + installed + .zip(expected.as_deref()) + .is_some_and(|(installed, expected)| version_is_older(installed, expected)) + } + + fn activation_warning(&self) -> Option<&'static str> { + (!plugin_is_installed_at(&paths::cursor_plugin_dir()) + || !discovery_hooks_are_installed_at(&paths::cursor_config_dir())) + .then_some( + "Cursor tracing plugin or lifecycle hooks are missing; run `bt trace enable cursor`", + ) } - let expected = package_version(PLUGIN_MANIFEST).ok(); - let installed = installed_manifest.get("version").and_then(Value::as_str); - installed - .zip(expected.as_deref()) - .is_some_and(|(installed, expected)| version_is_older(installed, expected)) } /// The plugin's hook manifest. On Windows each hook runs the PowerShell @@ -301,18 +327,6 @@ fn update_at(plugin_dir: &Path, config_dir: &Path) -> anyhow::Result<()> { with_discovery_hooks(plugin_dir, config_dir, true, update_plugin_at) } -pub(super) fn enable() -> anyhow::Result<()> { - enable_at(&paths::cursor_plugin_dir(), &paths::cursor_config_dir()) -} - -pub(super) fn disable() -> anyhow::Result<()> { - disable_at(&paths::cursor_plugin_dir(), &paths::cursor_config_dir()) -} - -pub(super) fn update() -> anyhow::Result<()> { - update_at(&paths::cursor_plugin_dir(), &paths::cursor_config_dir()) -} - #[cfg(test)] mod tests { use super::*; diff --git a/bt-daemon/src/setup/grok.rs b/bt-daemon/src/setup/grok.rs index e135e5a..1d1272f 100644 --- a/bt-daemon/src/setup/grok.rs +++ b/bt-daemon/src/setup/grok.rs @@ -1,6 +1,8 @@ //! Grok: the trace-grok plugin installed from its GitHub repository. use super::common::{github_repo_matches, CommandRunner}; +use crate::agents::Grok; +use crate::setup::Setup; use anyhow::bail; use serde_json::Value; @@ -20,45 +22,53 @@ fn is_published(item: &Value) -> bool { .is_some_and(|source| github_repo_matches(source, PLUGIN_SOURCE)) } -fn list_plugins(runner: &mut impl CommandRunner) -> anyhow::Result { +fn list_plugins(runner: &mut dyn CommandRunner) -> anyhow::Result { runner.json("grok", &["plugin", "list", "--json"]) } -pub(super) fn enable(runner: &mut impl CommandRunner) -> anyhow::Result<()> { - let plugins = list_plugins(runner)?; - // `bt trace enable grok` is the user's trust boundary. Grok's `--trust` - // applies to this plugin installation and does not change folder trust. - match installed_plugin(&plugins) { - Some(plugin) if is_published(plugin) => { - runner.run("grok", &["plugin", "update", PLUGIN])?; +impl Setup for Grok { + fn enable(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + let plugins = list_plugins(runner)?; + // `bt trace enable grok` is the user's trust boundary. Grok's `--trust` + // applies to this plugin installation and does not change folder trust. + match installed_plugin(&plugins) { + Some(plugin) if is_published(plugin) => { + runner.run("grok", &["plugin", "update", PLUGIN])?; + } + Some(_) => { + runner.run("grok", &["plugin", "uninstall", PLUGIN, "--confirm"])?; + runner.run("grok", &["plugin", "install", PLUGIN_SOURCE, "--trust"])?; + } + None => runner.run("grok", &["plugin", "install", PLUGIN_SOURCE, "--trust"])?, } - Some(_) => { + runner.run("grok", &["plugin", "enable", PLUGIN]) + } + + fn disable(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + if installed_plugin(&list_plugins(runner)?).is_some_and(is_published) { runner.run("grok", &["plugin", "uninstall", PLUGIN, "--confirm"])?; - runner.run("grok", &["plugin", "install", PLUGIN_SOURCE, "--trust"])?; } - None => runner.run("grok", &["plugin", "install", PLUGIN_SOURCE, "--trust"])?, + Ok(()) } - runner.run("grok", &["plugin", "enable", PLUGIN]) -} -pub(super) fn disable(runner: &mut impl CommandRunner) -> anyhow::Result<()> { - if installed_plugin(&list_plugins(runner)?).is_some_and(is_published) { - runner.run("grok", &["plugin", "uninstall", PLUGIN, "--confirm"])?; + fn update(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + let plugins = list_plugins(runner)?; + let plugin = installed_plugin(&plugins).ok_or_else(|| { + anyhow::anyhow!("Grok tracing plugin is not installed; run `bt trace enable grok`") + })?; + if !is_published(plugin) { + bail!( + "Grok tracing plugin is not the published Braintrust plugin; run `bt trace enable grok`" + ); + } + runner.run("grok", &["plugin", "update", PLUGIN]) } - Ok(()) -} -pub(super) fn update(runner: &mut impl CommandRunner) -> anyhow::Result<()> { - let plugins = list_plugins(runner)?; - let plugin = installed_plugin(&plugins).ok_or_else(|| { - anyhow::anyhow!("Grok tracing plugin is not installed; run `bt trace enable grok`") - })?; - if !is_published(plugin) { - bail!( - "Grok tracing plugin is not the published Braintrust plugin; run `bt trace enable grok`" - ); + fn activation_warning(&self) -> Option<&'static str> { + Some( + "Grok 1.0.13 requires `/reload-plugins` in each active session after plugin installation or update before its hooks become active", + ) } - runner.run("grok", &["plugin", "update", PLUGIN]) } #[cfg(test)] @@ -72,7 +82,7 @@ mod tests { {"name": "other-plugin", "source": "somebody/other-plugin"} ])]); - enable(&mut runner).unwrap(); + Grok.enable(&mut runner).unwrap(); assert_eq!( runner.calls, @@ -95,7 +105,7 @@ mod tests { {"name": "other-plugin", "source": "somebody/other-plugin"} ])]); - enable(&mut runner).unwrap(); + Grok.enable(&mut runner).unwrap(); assert_eq!( runner.calls, @@ -115,7 +125,7 @@ mod tests { "status": "installed" }])]); - update(&mut runner).unwrap(); + Grok.update(&mut runner).unwrap(); assert_eq!( runner.calls, @@ -134,7 +144,7 @@ mod tests { {"name": "other-plugin", "source": "somebody/other-plugin"} ])]); - enable(&mut runner).unwrap(); + Grok.enable(&mut runner).unwrap(); assert_eq!( runner.calls, @@ -158,7 +168,7 @@ mod tests { {"name": "other-plugin", "source": "somebody/other-plugin"} ])]); - disable(&mut runner).unwrap(); + Grok.disable(&mut runner).unwrap(); assert_eq!( runner.calls, @@ -173,7 +183,7 @@ mod tests { "source": "/tmp/local-trace-grok", "status": "installed" }])]); - disable(&mut local).unwrap(); + Grok.disable(&mut local).unwrap(); assert_eq!(local.calls, ["grok plugin list --json"]); } } diff --git a/bt-daemon/src/setup/mod.rs b/bt-daemon/src/setup/mod.rs index 5009501..61aaa3c 100644 --- a/bt-daemon/src/setup/mod.rs +++ b/bt-daemon/src/setup/mod.rs @@ -1,9 +1,8 @@ //! Persistent installation and configuration for coding-agent tracing plugins. //! -//! Each agent's adapter lives in its own module and exposes `enable`, -//! `disable`, and `update`; agents that can detect an outdated installation -//! also expose `stale`. This module dispatches to them and owns the -//! Braintrust settings file every agent shares. +//! Each agent implements [`Setup`] in its own module. This module dispatches +//! to them through the agent registry and owns the Braintrust settings file +//! every agent shares. mod antigravity; mod claude; @@ -14,60 +13,64 @@ mod grok; mod opencode; pub(crate) mod pi; +use crate::agents::{registrar, Agent}; use crate::paths; use crate::trace_command::{EnableArgs, SetupAgent}; use crate::wire::SessionRoute; use crate::TraceCommandOutput; use anyhow::Context; +pub(crate) use common::CommandRunner; use common::{load_object, write_object_atomic_with, FileAccess, SystemCommandRunner}; use serde_json::Value; use std::path::{Path, PathBuf}; -/// The settings source name and display name for an agent. -fn agent_details(agent: SetupAgent) -> (&'static str, &'static str) { - match agent { - SetupAgent::Codex => ("codex", "Codex"), - SetupAgent::Claude => ("claude", "Claude Code"), - SetupAgent::OpenCode => ("opencode", "OpenCode"), - SetupAgent::Pi => ("pi", "Pi"), - SetupAgent::Grok => ("grok", "Grok"), - SetupAgent::Cursor => ("cursor", "Cursor"), - SetupAgent::Antigravity => ("antigravity", "Google Antigravity"), +/// One agent's tracing adapter. Implemented in each agent's setup module and +/// registered in [`crate::agents`]. +pub(crate) trait Setup: Agent { + /// Install or refresh the published adapter. + fn enable(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()>; + /// Remove the adapter if it is the Braintrust one. + fn disable(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()>; + /// Update an installed adapter. Must not install or enable it. + fn update(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()>; + /// Whether the installed adapter is older than the one this build installs. + fn stale(&self) -> bool { + false } + /// A problem that keeps an enabled adapter from tracing, for `doctor`. + fn activation_warning(&self) -> Option<&'static str> { + None + } +} + +fn adapter(agent: SetupAgent) -> &'static dyn Setup { + let name = agent.identity().id; + registrar() + .setup + .get(name) + .unwrap_or_else(|| panic!("no setup adapter is registered for {name}")) } /// A warning when the agent's installed tracing plugin is older than the one /// this build would install. pub(crate) fn update_warning(source: &str) -> Option { - let stale = match source { - "codex" => codex::stale(), - "claude" => claude::stale(), - "opencode" => opencode::stale(), - "pi" => pi::stale(), - "cursor" => cursor::stale(), - _ => false, - }; + let stale = registrar() + .setup + .get(source) + .is_some_and(|agent| agent.stale()); stale.then(|| format!("tracing plugin is out of date; run `bt trace update {source}`")) } /// Install or refresh one agent's published tracing adapter and persist its /// non-secret route selection. pub fn run_enable(args: EnableArgs, route: SessionRoute) -> anyhow::Result { - let mut runner = SystemCommandRunner; - match args.agent { - SetupAgent::Codex => codex::enable(&mut runner)?, - SetupAgent::Claude => claude::enable(&mut runner)?, - SetupAgent::OpenCode => opencode::enable()?, - SetupAgent::Pi => pi::enable(&mut runner)?, - SetupAgent::Grok => grok::enable(&mut runner)?, - SetupAgent::Cursor => cursor::enable()?, - SetupAgent::Antigravity => antigravity::enable(&mut runner)?, - } - let (source, display_name) = agent_details(args.agent); - let settings_path = enable_tracing(source, route)?; + let agent = adapter(args.agent); + agent.enable(&mut SystemCommandRunner)?; + let identity = agent.identity(); + let settings_path = enable_tracing(identity.id, route)?; Ok(TraceCommandOutput::setup( - source, - display_name, + identity.id, + identity.display_name, settings_path, )) } @@ -79,22 +82,14 @@ pub fn run_setup(args: EnableArgs, route: SessionRoute) -> anyhow::Result anyhow::Result { - let mut runner = SystemCommandRunner; - let adapter_result = match agent { - SetupAgent::Codex => codex::disable(&mut runner), - SetupAgent::Claude => claude::disable(&mut runner), - SetupAgent::OpenCode => opencode::disable(), - SetupAgent::Pi => pi::disable(&mut runner), - SetupAgent::Grok => grok::disable(&mut runner), - SetupAgent::Cursor => cursor::disable(), - SetupAgent::Antigravity => antigravity::disable(&mut runner), - }; - let (source, display_name) = agent_details(agent); - let settings_path = paths::agent_settings_path(source, None); + let agent = adapter(agent); + let adapter_result = agent.disable(&mut SystemCommandRunner); + let identity = agent.identity(); + let settings_path = paths::agent_settings_path(identity.id, None); finish_disable(adapter_result, &settings_path)?; Ok(TraceCommandOutput::disable( - source, - display_name, + identity.id, + identity.display_name, settings_path, )) } @@ -102,18 +97,13 @@ pub fn run_disable(agent: SetupAgent) -> anyhow::Result { /// Update an already installed tracing adapter without writing tracing settings, /// enabling a disabled plugin, or creating an agent configuration file. pub fn run_update(agent: SetupAgent) -> anyhow::Result { - let mut runner = SystemCommandRunner; - match agent { - SetupAgent::Codex => codex::update(&mut runner)?, - SetupAgent::Claude => claude::update(&mut runner)?, - SetupAgent::OpenCode => opencode::update()?, - SetupAgent::Pi => pi::update(&mut runner)?, - SetupAgent::Grok => grok::update(&mut runner)?, - SetupAgent::Cursor => cursor::update()?, - SetupAgent::Antigravity => antigravity::update(&mut runner)?, - } - let (source, display_name) = agent_details(agent); - Ok(TraceCommandOutput::update(source, display_name)) + let agent = adapter(agent); + agent.update(&mut SystemCommandRunner)?; + let identity = agent.identity(); + Ok(TraceCommandOutput::update( + identity.id, + identity.display_name, + )) } fn enable_tracing(source: &str, route: SessionRoute) -> anyhow::Result { @@ -197,6 +187,21 @@ mod tests { use super::*; use crate::wire::{AuthSelection, TraceDestination}; + #[test] + fn every_setup_subcommand_has_a_registered_adapter() { + for agent in [ + SetupAgent::Codex, + SetupAgent::Claude, + SetupAgent::OpenCode, + SetupAgent::Pi, + SetupAgent::Grok, + SetupAgent::Cursor, + SetupAgent::Antigravity, + ] { + assert_eq!(adapter(agent).identity().id, agent.identity().id); + } + } + #[test] fn tracing_settings_preserve_unrelated_fields_and_remove_legacy_keys() { let temp = tempfile::tempdir().unwrap(); diff --git a/bt-daemon/src/setup/opencode.rs b/bt-daemon/src/setup/opencode.rs index 5dd48b3..6ff644d 100644 --- a/bt-daemon/src/setup/opencode.rs +++ b/bt-daemon/src/setup/opencode.rs @@ -1,8 +1,10 @@ //! OpenCode: the npm plugin listed in `opencode.json`. OpenCode installs it //! from that entry, so no OpenCode CLI is involved. -use super::common::{edit_object, npm_major_spec, plugin_source, FileAccess}; +use super::common::{edit_object, npm_major_spec, plugin_source, CommandRunner, FileAccess}; +use crate::agents::OpenCode; use crate::paths; +use crate::setup::Setup; use anyhow::bail; use serde_json::{Map, Value}; use std::path::{Path, PathBuf}; @@ -86,33 +88,35 @@ fn disable_at(path: &Path) -> anyhow::Result<()> { }) } -pub(super) fn enable() -> anyhow::Result<()> { - enable_at(&config_path()) -} +impl Setup for OpenCode { + fn enable(&self, _runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + enable_at(&config_path()) + } -pub(super) fn disable() -> anyhow::Result<()> { - disable_at(&config_path()) -} + fn disable(&self, _runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + disable_at(&config_path()) + } -pub(super) fn update() -> anyhow::Result<()> { - update_at(&config_path()) -} + fn update(&self, _runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + update_at(&config_path()) + } -/// Whether `opencode.json` pins a version other than the bundled one. -pub(super) fn stale() -> bool { - let Ok(raw) = std::fs::read(config_path()) else { - return false; - }; - let Ok(config) = serde_json::from_slice::(&raw) else { - return false; - }; - let expected = plugin_spec().ok(); - config - .get("plugin") - .and_then(Value::as_array) - .and_then(|plugins| plugins.iter().find(|plugin| is_ours(plugin))) - .and_then(Value::as_str) - .is_some_and(|plugin| Some(plugin) != expected.as_deref()) + /// Whether `opencode.json` pins a version other than the bundled one. + fn stale(&self) -> bool { + let Ok(raw) = std::fs::read(config_path()) else { + return false; + }; + let Ok(config) = serde_json::from_slice::(&raw) else { + return false; + }; + let expected = plugin_spec().ok(); + config + .get("plugin") + .and_then(Value::as_array) + .and_then(|plugins| plugins.iter().find(|plugin| is_ours(plugin))) + .and_then(Value::as_str) + .is_some_and(|plugin| Some(plugin) != expected.as_deref()) + } } #[cfg(test)] diff --git a/bt-daemon/src/setup/pi.rs b/bt-daemon/src/setup/pi.rs index 6af7ab5..566656a 100644 --- a/bt-daemon/src/setup/pi.rs +++ b/bt-daemon/src/setup/pi.rs @@ -1,6 +1,8 @@ //! Pi: the Braintrust extension installed from npm by Pi's package manager. use super::common::CommandRunner; +use crate::agents::Pi; +use crate::setup::Setup; const PACKAGE: &str = "@braintrust/pi-extension"; @@ -9,33 +11,35 @@ pub(crate) fn plugin_spec() -> String { format!("npm:{PACKAGE}") } -pub(super) fn enable(runner: &mut impl CommandRunner) -> anyhow::Result<()> { - runner.run("pi", &["install", &plugin_spec()]) -} +impl Setup for Pi { + fn enable(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + runner.run("pi", &["install", &plugin_spec()]) + } -pub(super) fn disable(runner: &mut impl CommandRunner) -> anyhow::Result<()> { - runner.run("pi", &["uninstall", &plugin_spec()]) -} + fn disable(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + runner.run("pi", &["uninstall", &plugin_spec()]) + } -pub(super) fn update(runner: &mut impl CommandRunner) -> anyhow::Result<()> { - runner.run("pi", &["update", &plugin_spec()]) -} + fn update(&self, runner: &mut dyn CommandRunner) -> anyhow::Result<()> { + runner.run("pi", &["update", &plugin_spec()]) + } -/// Whether `pi list` reports a pinned version of the extension. -pub(super) fn stale() -> bool { - let output = crate::subprocess::background_command("pi") - .arg("list") - .output(); - let Ok(output) = output else { return false }; - if !output.status.success() { - return false; + /// Whether `pi list` reports a pinned version of the extension. + fn stale(&self) -> bool { + let output = crate::subprocess::background_command("pi") + .arg("list") + .output(); + let Ok(output) = output else { return false }; + if !output.status.success() { + return false; + } + let installed = String::from_utf8_lossy(&output.stdout); + let expected = plugin_spec(); + installed.lines().any(|line| { + let plugin = line.trim(); + plugin.starts_with(&expected) && plugin != expected + }) } - let installed = String::from_utf8_lossy(&output.stdout); - let expected = plugin_spec(); - installed.lines().any(|line| { - let plugin = line.trim(); - plugin.starts_with(&expected) && plugin != expected - }) } #[cfg(test)] @@ -47,7 +51,7 @@ mod tests { fn installs_the_latest_published_extension() { let mut runner = FakeRunner::new([]); - enable(&mut runner).unwrap(); + Pi.enable(&mut runner).unwrap(); assert!(runner.called("pi install npm:@braintrust/pi-extension")); } @@ -56,7 +60,7 @@ mod tests { fn updates_the_npm_extension_without_installing_it() { let mut runner = FakeRunner::new([]); - update(&mut runner).unwrap(); + Pi.update(&mut runner).unwrap(); assert!(runner.called("pi update npm:@braintrust/pi-extension")); assert!(!runner @@ -68,7 +72,7 @@ mod tests { #[test] fn disable_uses_the_uninstall_command() { let mut runner = FakeRunner::new([]); - disable(&mut runner).unwrap(); + Pi.disable(&mut runner).unwrap(); assert!(runner.called("pi uninstall npm:@braintrust/pi-extension")); } } diff --git a/bt-daemon/src/trace_command.rs b/bt-daemon/src/trace_command.rs index d713303..e2d8001 100644 --- a/bt-daemon/src/trace_command.rs +++ b/bt-daemon/src/trace_command.rs @@ -4,6 +4,7 @@ //! integrations they control. Hosts such as `bt` provide global auth flags and //! dispatch these commands without duplicating agent-specific CLI knowledge. +use crate::agents::{self, Agent, Identity}; use crate::{HookArgs, ImportArgs, RunArgs, ServeArgs, StatusArgs}; use clap::{Args, Subcommand, ValueEnum}; use std::path::PathBuf; @@ -74,27 +75,15 @@ pub enum DoctorAgent { } impl DoctorAgent { - pub(crate) fn source(self) -> &'static str { + pub(crate) fn identity(self) -> &'static Identity { match self { - Self::Codex => "codex", - Self::Claude => "claude", - Self::OpenCode => "opencode", - Self::Pi => "pi", - Self::Antigravity => "antigravity", - Self::Grok => "grok", - Self::Cursor => "cursor", - } - } - - pub(crate) fn display_name(self) -> &'static str { - match self { - Self::Codex => "Codex", - Self::Claude => "Claude Code", - Self::OpenCode => "OpenCode", - Self::Pi => "Pi", - Self::Antigravity => "Google Antigravity", - Self::Grok => "Grok", - Self::Cursor => "Cursor", + Self::Codex => agents::Codex.identity(), + Self::Claude => agents::Claude.identity(), + Self::OpenCode => agents::OpenCode.identity(), + Self::Pi => agents::Pi.identity(), + Self::Antigravity => agents::Antigravity.identity(), + Self::Grok => agents::Grok.identity(), + Self::Cursor => agents::Cursor.identity(), } } } @@ -154,6 +143,20 @@ pub enum SetupAgent { Antigravity, } +impl SetupAgent { + pub(crate) fn identity(self) -> &'static Identity { + match self { + Self::Codex => agents::Codex.identity(), + Self::Claude => agents::Claude.identity(), + Self::OpenCode => agents::OpenCode.identity(), + Self::Pi => agents::Pi.identity(), + Self::Grok => agents::Grok.identity(), + Self::Cursor => agents::Cursor.identity(), + Self::Antigravity => agents::Antigravity.identity(), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -323,8 +326,8 @@ mod tests { cli.trace.command, TraceCommand::Doctor(DoctorArgs { agent }) if agent == expected )); - assert_eq!(expected.source(), source); - assert_eq!(expected.display_name(), display_name); + assert_eq!(expected.identity().id, source); + assert_eq!(expected.identity().display_name, display_name); } } diff --git a/bt-daemon/src/trace_runtime.rs b/bt-daemon/src/trace_runtime.rs index 7ac7adb..d69938a 100644 --- a/bt-daemon/src/trace_runtime.rs +++ b/bt-daemon/src/trace_runtime.rs @@ -304,20 +304,10 @@ fn plugin_activation_warning(agent: DoctorAgent, enabled: bool) -> Option<&'stat if !enabled { return None; } - match agent { - DoctorAgent::Grok => Some( - "Grok 1.0.13 requires `/reload-plugins` in each active session after plugin installation or update before its hooks become active", - ), - DoctorAgent::Cursor - if !crate::setup::cursor::plugin_is_installed_at(&crate::paths::cursor_plugin_dir()) - || !crate::setup::cursor::discovery_hooks_are_installed_at( - &crate::paths::cursor_config_dir(), - ) => - { - Some("Cursor tracing plugin or lifecycle hooks are missing; run `bt trace enable cursor`") - } - _ => None, - } + crate::agents::registrar() + .setup + .get(agent.identity().id) + .and_then(|setup| setup.activation_warning()) } /// Bounds `doctor`'s status query to the running daemon. @@ -441,7 +431,7 @@ fn daemon_warnings( daemon: &DaemonDiagnostic, resolves_locally: bool, ) -> Vec { - let (source, display_name) = (agent.source(), agent.display_name()); + let (source, display_name) = (agent.identity().id, agent.identity().display_name); if daemon.status == DaemonStatus::Unreachable { return vec![format!( "could not reach the tracing daemon: {}", @@ -508,7 +498,7 @@ async fn doctor_output_at( args: DoctorArgs, socket: &Path, ) -> DoctorCommandOutput { - let source = args.agent.source(); + let source = args.agent.identity().id; let settings_path = paths::agent_settings_path(source, None); let settings_present = settings_path.exists(); let settings = crate::settings::AgentSettings::load(source); @@ -620,7 +610,7 @@ async fn doctor_output_at( DoctorCommandOutput { source: source.into(), - display_name: args.agent.display_name().into(), + display_name: args.agent.identity().display_name.into(), settings_path, settings_present, enabled, diff --git a/bt-daemon/src/transcript_import/mod.rs b/bt-daemon/src/transcript_import/mod.rs index dcf7494..034e01d 100644 --- a/bt-daemon/src/transcript_import/mod.rs +++ b/bt-daemon/src/transcript_import/mod.rs @@ -193,12 +193,7 @@ fn validate_session_id(session_id: &str) -> anyhow::Result<()> { } fn source_name(source: ImportSource) -> &'static str { - match source { - ImportSource::Codex => "Codex", - ImportSource::Cursor => "Cursor", - ImportSource::Claude => "Claude Code", - ImportSource::Antigravity => "Google Antigravity", - } + source.identity().display_name } #[cfg(test)]