Skip to main content

byob_plugins/
lib.rs

1//! Plugins: a folder with a `byob-plugin.json` that adds any mix of
2//!
3//! - **skills** (`skills/<name>/SKILL.md`, the Agent Skills format) → [`skills`],
4//!   [`UseSkillTool`], [`skills_prompt`];
5//! - **Python tools** (`tools[].run`) → [`python::PythonTool`];
6//! - **subagents** (`agents/*.md`, Claude Code's format) → [`agents`];
7//! - **slash commands** (`commands/*.md`, Claude Code's format) → [`commands`];
8//! - **connectors** (`mcpServers`, MCP) → [`connectors::ConnectorToolAdapter`] over the
9//!   [`connectors::Connector`] trait, which `byob-mcp`'s client implements.
10//!
11//! Typical use by the agent:
12//! 1. [`discover`] the plugin folders; show `errors` and each plugin's `problems`.
13//! 2. Reserve the built-in tool names in a [`ToolNames`], then add each active plugin's
14//!    [`python_tools`] and, per `mcpServers` entry, [`connector_tools`].
15//! 3. Add [`UseSkillTool`] over [`Discovery::skills`] and put [`skills_prompt`] and
16//!    [`Discovery::prompt`] in the system prompt.
17#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
18
19pub mod agents;
20pub mod authoring;
21pub mod backend;
22pub mod commands;
23pub mod connectors;
24pub mod discover;
25pub mod manifest;
26pub mod materialize;
27pub mod mcp;
28pub mod names;
29pub mod native;
30pub mod pyenv;
31pub mod python;
32pub mod skills;
33
34use std::collections::BTreeMap;
35use std::fmt;
36use std::path::PathBuf;
37
38pub use agents::{AgentDef, load_agents_dir, merge_agents, parse_agent};
39pub use backend::{Backend, BackendTool};
40pub use commands::{Command, load_commands_dir, split_slash};
41pub use connectors::{
42    Connector, ConnectorContent, ConnectorDraft, ConnectorResult, ConnectorTool, ConnectorToolAdapter, base64_decode, connector_tools, connector_tools_from,
43    connector_tools_on, parse_connector,
44};
45pub use discover::{BUNDLED_DIR, Discovery, Embedded, Plugin, PluginSources, discover, discover_embedded, load_embedded, load_plugin};
46pub use manifest::{MANIFEST, Manifest, ManifestError, McpServerConfig, OLD_MANIFEST, parse_manifest};
47pub use names::ToolNames;
48pub use pyenv::{PluginPython, PyEnvs};
49pub use python::{PythonTool, default_python, python_tools, python_tools_in, python_tools_with, run_script_sandboxed, run_script_unsandboxed};
50pub use skills::{SaveSkillTool, Skill, UseSkillTool, load_skills_dir, skills_prompt};
51
52/// Something that couldn't load, for the user (`path`: the plugin or skill folder).
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct LoadError {
55    pub path: PathBuf,
56    pub message: String,
57}
58
59impl fmt::Display for LoadError {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        write!(f, "{}: {}", self.path.display(), self.message)
62    }
63}
64
65impl std::error::Error for LoadError {}
66
67/// What a plugin's tools need to know about it.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct PluginRef {
70    pub id: String,
71    pub name: String,
72    pub dir: PathBuf,
73    /// `permissions.network`.
74    pub network: bool,
75    /// Setting values (defaults filled in), passed as environment variables.
76    pub env: BTreeMap<String, String>,
77}
78
79impl PluginRef {
80    /// `settings`: the user's values by key; declared defaults fill the gaps.
81    pub fn new(plugin: &Plugin, settings: &BTreeMap<String, String>) -> Self {
82        let mut env: BTreeMap<String, String> = plugin.manifest.settings.iter().filter_map(|s| Some((s.key.clone(), s.default.clone()?))).collect();
83        for s in &plugin.manifest.settings {
84            if let Some(v) = settings.get(&s.key) {
85                env.insert(s.key.clone(), v.clone());
86            }
87        }
88        Self { id: plugin.manifest.id.clone(), name: plugin.manifest.name.clone(), dir: plugin.dir.clone(), network: plugin.manifest.permissions.network, env }
89    }
90
91    /// Required settings that have no value yet (labels), e.g. to show "Fill in API key".
92    pub fn missing_settings(plugin: &Plugin, settings: &BTreeMap<String, String>) -> Vec<String> {
93        plugin
94            .manifest
95            .settings
96            .iter()
97            .filter(|s| s.required && settings.get(&s.key).or(s.default.as_ref()).is_none_or(|v| v.trim().is_empty()))
98            .map(|s| s.label.clone())
99            .collect()
100    }
101}