Skip to main content

objectiveai_sdk/cli/command/plugins/run/
mod.rs

1//! `plugins run` — async handler stub.
2
3use crate::cli::command::CommandRequest;
4
5#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
6#[schemars(rename = "cli.command.plugins.run.Request")]
7pub struct Request {
8    pub path_type: Path,
9    pub owner: String,
10    pub name: String,
11    pub version: String,
12    pub args: Vec<String>,
13    #[serde(flatten)]
14    pub base: crate::cli::command::RequestBase,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
18#[schemars(rename = "cli.command.plugins.run.Path")]
19pub enum Path {
20    #[serde(rename = "plugins/run")]
21    PluginsRun,
22}
23
24impl CommandRequest for Request {
25    fn request_base(&self) -> &crate::cli::command::RequestBase {
26        &self.base
27    }
28
29    fn request_base_mut(&mut self) -> Option<&mut crate::cli::command::RequestBase> {
30        Some(&mut self.base)
31    }
32}
33
34#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
35#[serde(untagged)]
36#[schemars(rename = "cli.command.plugins.run.ResponseItem")]
37pub enum ResponseItem {
38    #[schemars(title = "Mcp")]
39    Mcp(Mcp),
40    // `cli::Error` already carries `type:"error"`. Placement above
41    // `Notification` is load-bearing: serde untagged tries variants
42    // in source order, so a `cli::Error`-shaped JSON must match
43    // `Error` before falling through to the catch-all.
44    #[schemars(title = "Error")]
45    Error(crate::cli::Error),
46    #[schemars(title = "Notification")]
47    Notification(serde_json::Value),
48}
49
50/// Plugin announces a running MCP server URL. The host routes this
51/// through the standard plugin-notification pipeline and dials the
52/// URL the same way it would for an entry in the plugin's manifest
53/// `mcp_servers` — runtime announcements are functionally identical
54/// to manifest-time declarations.
55///
56/// The constant `type:"mcp"` discriminator disambiguates this
57/// variant from the rest of the untagged [`ResponseItem`] /
58/// [`crate::cli::plugins::Output`] catch-all, mirroring the
59/// `type:"error"` discriminator on [`crate::cli::Error`].
60#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
61#[schemars(rename = "cli.command.plugins.run.Mcp")]
62pub struct Mcp {
63    pub r#type: McpType,
64    pub url: String,
65}
66
67/// Single-variant discriminator for [`Mcp`]'s `type` field. Always
68/// `"mcp"` on the wire.
69#[derive(
70    Debug,
71    Clone,
72    Copy,
73    PartialEq,
74    Eq,
75    serde::Serialize,
76    serde::Deserialize,
77    schemars::JsonSchema,
78)]
79#[serde(rename_all = "snake_case")]
80#[schemars(rename = "cli.command.plugins.run.McpType")]
81pub enum McpType {
82    Mcp,
83}
84
85#[derive(clap::Args)]
86#[command(group(clap::ArgGroup::new("owner_required").required(true).args(["owner"])))]
87#[command(group(clap::ArgGroup::new("name_required").required(true).args(["name"])))]
88#[command(group(clap::ArgGroup::new("version_required").required(true).args(["version"])))]
89pub struct Args {
90    /// Plugin owner (GitHub `<owner>` segment). Required.
91    #[arg(long)]
92    pub owner: Option<String>,
93    /// Plugin name (repository segment). Required.
94    #[arg(long)]
95    pub name: Option<String>,
96    /// Plugin version. Required.
97    #[arg(long)]
98    pub version: Option<String>,
99    /// Arguments passed through to the invoked binary, as a JSON array
100    /// of strings (e.g. `--args '["--flag","value"]'`).
101    #[arg(long)]
102    pub args: Option<String>,
103    #[command(flatten)]
104    pub base: crate::cli::command::RequestBaseArgs,
105}
106
107#[derive(clap::Args)]
108#[command(args_conflicts_with_subcommands = true)]
109pub struct Command {
110    #[command(flatten)]
111    pub args: Args,
112    #[command(subcommand)]
113    pub schema: Option<Schema>,
114}
115
116#[derive(clap::Subcommand)]
117pub enum Schema {
118    /// Emit the JSON Schema for this leaf's `Request` type and exit.
119    RequestSchema(request_schema::Args),
120    /// Emit the JSON Schema for this leaf's `Response` type and exit.
121    ResponseSchema(response_schema::Args),
122}
123
124impl TryFrom<Args> for Request {
125    type Error = crate::cli::command::FromArgsError;
126    fn try_from(args: Args) -> Result<Self, Self::Error> {
127        let parsed_args: Vec<String> = match args.args {
128            Some(s) => {
129                let mut de = serde_json::Deserializer::from_str(&s);
130                serde_path_to_error::deserialize(&mut de).map_err(|source| {
131                    crate::cli::command::FromArgsError {
132                        field: "args",
133                        source: source.into(),
134                    }
135                })?
136            }
137            None => Vec::new(),
138        };
139        Ok(Self {
140            path_type: Path::PluginsRun,
141            owner: args.owner.ok_or_else(|| {
142                crate::cli::command::FromArgsError::path_parse(
143                    "owner",
144                    "--owner is required".to_string(),
145                )
146            })?,
147            name: args.name.ok_or_else(|| {
148                crate::cli::command::FromArgsError::path_parse(
149                    "name",
150                    "--name is required".to_string(),
151                )
152            })?,
153            version: args.version.ok_or_else(|| {
154                crate::cli::command::FromArgsError::path_parse(
155                    "version",
156                    "--version is required".to_string(),
157                )
158            })?,
159            args: parsed_args,
160            base: args.base.into(),
161        })
162    }
163}
164
165#[cfg(feature = "cli-executor")]
166pub async fn execute<E: crate::cli::command::CommandExecutor>(
167    executor: &E,
168    mut request: Request,
169
170        agent_arguments: Option<&crate::cli::command::AgentArguments>,
171    ) -> Result<E::Stream<ResponseItem>, E::Error> {
172    request.base.clear_transform();
173    executor.execute(request, agent_arguments).await
174}
175
176#[cfg(feature = "cli-executor")]
177pub async fn execute_transform<E: crate::cli::command::CommandExecutor>(
178    executor: &E,
179    mut request: Request,
180    transform: crate::cli::command::Transform,
181
182        agent_arguments: Option<&crate::cli::command::AgentArguments>,
183    ) -> Result<E::Stream<serde_json::Value>, E::Error> {
184    request.base.set_transform(transform);
185    executor.execute(request, agent_arguments).await
186}
187
188#[cfg(feature = "mcp")]
189impl crate::cli::command::CommandResponse for ResponseItem {
190    fn into_mcp(self) -> crate::cli::command::McpResponseItem {
191        use crate::agent::completions::message::RichContentPart;
192        use crate::cli::command::McpResponseItem;
193        match self {
194            ResponseItem::Mcp(m) => {
195                McpResponseItem::JSONL(serde_json::to_value(m).unwrap())
196            }
197            ResponseItem::Error(e) => e.into_mcp(),
198            ResponseItem::Notification(value) => {
199                // String + data URL → media via RichContentPart::from_blob.
200                // Anything else (and strings that aren't data URLs) →
201                // JSONL passthrough.
202                if let serde_json::Value::String(s) = &value
203                    && let Some((mime, payload)) = crate::data_url::parse_data_url(s)
204                {
205                    let part = RichContentPart::from_blob(
206                        mime,
207                        payload.to_string(),
208                        None,
209                    );
210                    return McpResponseItem::Media(part.into());
211                }
212                McpResponseItem::JSONL(value)
213            }
214        }
215    }
216}
217
218pub mod request_schema;
219
220
221pub mod response_schema;