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