Skip to main content

objectiveai_sdk/cli/command/
command.rs

1//! Root-level clap `Command` plus the matching typed `Request` /
2//! `ResponseItem` aggregators and conversions, mirroring the same
3//! pattern every tier `mod.rs` follows for its own subcommands.
4
5#[derive(clap::Subcommand)]
6pub enum Subcommand {
7    Agents {
8        #[command(subcommand)]
9        command: super::agents::Command,
10    },
11    Api {
12        #[command(subcommand)]
13        command: super::api::Command,
14    },
15    Daemon {
16        #[command(subcommand)]
17        command: super::daemon::Command,
18    },
19    Db {
20        #[command(subcommand)]
21        command: super::db::Command,
22    },
23    Functions {
24        #[command(subcommand)]
25        command: super::functions::Command,
26    },
27    /// Kill every process holding a lock anywhere under the configured
28    /// `OBJECTIVEAI_DIR` — the blunt whole-tree sweep.
29    KillAll(super::kill_all::Command),
30    Laboratories {
31        #[command(subcommand)]
32        command: super::laboratories::Command,
33    },
34    Mcp {
35        #[command(subcommand)]
36        command: super::mcp::Command,
37    },
38    Plugins {
39        #[command(subcommand)]
40        command: super::plugins::Command,
41    },
42    /// Run a Python snippet and return its output as JSON.
43    Python(super::python::Command),
44    Swarms {
45        #[command(subcommand)]
46        command: super::swarms::Command,
47    },
48    Tools {
49        #[command(subcommand)]
50        command: super::tools::Command,
51    },
52    Update(super::update::Command),
53    Viewer {
54        #[command(subcommand)]
55        command: super::viewer::Command,
56    },
57}
58
59#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
60#[serde(untagged)]
61#[schemars(rename = "cli.command.Request")]
62pub enum Request {
63    #[schemars(title = "Agents")]
64    Agents(super::agents::Request),
65    #[schemars(title = "Api")]
66    Api(super::api::Request),
67    #[schemars(title = "Daemon")]
68    Daemon(super::daemon::Request),
69    #[schemars(title = "Db")]
70    Db(super::db::Request),
71    #[schemars(title = "Functions")]
72    Functions(super::functions::Request),
73    #[schemars(title = "KillAll")]
74    KillAll(super::kill_all::Request),
75    #[schemars(title = "KillAllRequestSchema")]
76    KillAllRequestSchema(super::kill_all::request_schema::Request),
77    #[schemars(title = "KillAllResponseSchema")]
78    KillAllResponseSchema(super::kill_all::response_schema::Request),
79    #[schemars(title = "Laboratories")]
80    Laboratories(super::laboratories::Request),
81    #[schemars(title = "Mcp")]
82    Mcp(super::mcp::Request),
83    #[schemars(title = "Plugins")]
84    Plugins(super::plugins::Request),
85    #[schemars(title = "Python")]
86    Python(super::python::Request),
87    #[schemars(title = "PythonRequestSchema")]
88    PythonRequestSchema(super::python::request_schema::Request),
89    #[schemars(title = "PythonResponseSchema")]
90    PythonResponseSchema(super::python::response_schema::Request),
91    #[schemars(title = "Swarms")]
92    Swarms(super::swarms::Request),
93    #[schemars(title = "Tools")]
94    Tools(super::tools::Request),
95    #[schemars(title = "Update")]
96    Update(super::update::Request),
97    #[schemars(title = "UpdateRequestSchema")]
98    UpdateRequestSchema(super::update::request_schema::Request),
99    #[schemars(title = "UpdateResponseSchema")]
100    UpdateResponseSchema(super::update::response_schema::Request),
101    #[schemars(title = "Viewer")]
102    Viewer(super::viewer::Request),
103}
104
105// Exempt from json-schema coverage: the aggregate's transitive
106// expansion spans the whole command tree, which downstream
107// generated TypeScript cannot emit declarations for (TS7056), and
108// no consumer uses the aggregate schema — leaf schemas cover the
109// wire.
110#[objectiveai_sdk_macros::json_schema_ignore]
111#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
112#[schemars(rename = "cli.command.ResponseItem")]
113#[serde(untagged)]
114pub enum ResponseItem {
115    #[schemars(title = "Agents")]
116    Agents(super::agents::ResponseItem),
117    #[schemars(title = "Api")]
118    Api(super::api::Response),
119    #[schemars(title = "Daemon")]
120    Daemon(super::daemon::ResponseItem),
121    #[schemars(title = "Db")]
122    Db(super::db::ResponseItem),
123    #[schemars(title = "Functions")]
124    Functions(super::functions::ResponseItem),
125    #[schemars(title = "KillAll")]
126    KillAll(super::kill_all::Response),
127    #[schemars(title = "KillAllRequestSchema")]
128    KillAllRequestSchema(super::kill_all::request_schema::Response),
129    #[schemars(title = "KillAllResponseSchema")]
130    KillAllResponseSchema(super::kill_all::response_schema::Response),
131    #[schemars(title = "Laboratories")]
132    Laboratories(super::laboratories::ResponseItem),
133    #[schemars(title = "Mcp")]
134    Mcp(super::mcp::Response),
135    #[schemars(title = "Plugins")]
136    Plugins(super::plugins::ResponseItem),
137    #[schemars(title = "Python")]
138    Python(serde_json::Value),
139    #[schemars(title = "PythonRequestSchema")]
140    PythonRequestSchema(super::python::request_schema::Response),
141    #[schemars(title = "PythonResponseSchema")]
142    PythonResponseSchema(super::python::response_schema::Response),
143    #[schemars(title = "Swarms")]
144    Swarms(super::swarms::ResponseItem),
145    #[schemars(title = "Tools")]
146    Tools(super::tools::ResponseItem),
147    #[schemars(title = "Update")]
148    Update(super::update::ResponseItem),
149    #[schemars(title = "UpdateRequestSchema")]
150    UpdateRequestSchema(super::update::request_schema::Response),
151    #[schemars(title = "UpdateResponseSchema")]
152    UpdateResponseSchema(super::update::response_schema::Response),
153    #[schemars(title = "Viewer")]
154    Viewer(super::viewer::Response),
155}
156
157#[cfg(feature = "mcp")]
158impl super::CommandResponse for ResponseItem {
159    fn into_mcp(self) -> super::McpResponseItem {
160        match self {
161            ResponseItem::Agents(v) => v.into_mcp(),
162            ResponseItem::Api(v) => v.into_mcp(),
163            ResponseItem::Daemon(v) => v.into_mcp(),
164            ResponseItem::Db(v) => v.into_mcp(),
165            ResponseItem::Functions(v) => v.into_mcp(),
166            ResponseItem::KillAll(v) => v.into_mcp(),
167            ResponseItem::KillAllRequestSchema(v) => v.into_mcp(),
168            ResponseItem::KillAllResponseSchema(v) => v.into_mcp(),
169            ResponseItem::Laboratories(v) => v.into_mcp(),
170            ResponseItem::Mcp(v) => v.into_mcp(),
171            ResponseItem::Plugins(v) => v.into_mcp(),
172            ResponseItem::Python(v) => v.into_mcp(),
173            ResponseItem::PythonRequestSchema(v) => v.into_mcp(),
174            ResponseItem::PythonResponseSchema(v) => v.into_mcp(),
175            ResponseItem::Swarms(v) => v.into_mcp(),
176            ResponseItem::Tools(v) => v.into_mcp(),
177            ResponseItem::Update(v) => v.into_mcp(),
178            ResponseItem::UpdateRequestSchema(v) => v.into_mcp(),
179            ResponseItem::UpdateResponseSchema(v) => v.into_mcp(),
180            ResponseItem::Viewer(v) => v.into_mcp(),
181        }
182    }
183}
184
185impl TryFrom<Subcommand> for Request {
186    type Error = super::FromArgsError;
187    fn try_from(command: Subcommand) -> Result<Self, Self::Error> {
188        match command {
189            Subcommand::Agents { command } =>
190                Ok(Request::Agents(super::agents::Request::try_from(command)?)),
191            Subcommand::Api { command } =>
192                Ok(Request::Api(super::api::Request::try_from(command)?)),
193            Subcommand::Daemon { command } =>
194                Ok(Request::Daemon(super::daemon::Request::try_from(command)?)),
195            Subcommand::Db { command } =>
196                Ok(Request::Db(super::db::Request::try_from(command)?)),
197            Subcommand::Functions { command } =>
198                Ok(Request::Functions(super::functions::Request::try_from(command)?)),
199            Subcommand::KillAll(cmd) => match cmd.schema {
200                None => Ok(Request::KillAll(super::kill_all::Request::try_from(cmd.args)?)),
201                Some(super::kill_all::Schema::RequestSchema(args)) =>
202                    Ok(Request::KillAllRequestSchema(super::kill_all::request_schema::Request::try_from(args)?)),
203                Some(super::kill_all::Schema::ResponseSchema(args)) =>
204                    Ok(Request::KillAllResponseSchema(super::kill_all::response_schema::Request::try_from(args)?)),
205            },
206            Subcommand::Laboratories { command } =>
207                Ok(Request::Laboratories(super::laboratories::Request::try_from(command)?)),
208            Subcommand::Mcp { command } =>
209                Ok(Request::Mcp(super::mcp::Request::try_from(command)?)),
210            Subcommand::Plugins { command } =>
211                Ok(Request::Plugins(super::plugins::Request::try_from(command)?)),
212            Subcommand::Python(cmd) => match cmd.schema {
213                None => Ok(Request::Python(super::python::Request::try_from(cmd.args)?)),
214                Some(super::python::Schema::RequestSchema(args)) =>
215                    Ok(Request::PythonRequestSchema(super::python::request_schema::Request::try_from(args)?)),
216                Some(super::python::Schema::ResponseSchema(args)) =>
217                    Ok(Request::PythonResponseSchema(super::python::response_schema::Request::try_from(args)?)),
218            },
219            Subcommand::Swarms { command } =>
220                Ok(Request::Swarms(super::swarms::Request::try_from(command)?)),
221            Subcommand::Tools { command } =>
222                Ok(Request::Tools(super::tools::Request::try_from(command)?)),
223            Subcommand::Update(cmd) => match cmd.schema {
224                None => Ok(Request::Update(super::update::Request::try_from(cmd.args)?)),
225                Some(super::update::Schema::RequestSchema(args)) =>
226                    Ok(Request::UpdateRequestSchema(super::update::request_schema::Request::try_from(args)?)),
227                Some(super::update::Schema::ResponseSchema(args)) =>
228                    Ok(Request::UpdateResponseSchema(super::update::response_schema::Request::try_from(args)?)),
229            },
230            Subcommand::Viewer { command } =>
231                Ok(Request::Viewer(super::viewer::Request::try_from(command)?)),
232        }
233    }
234}
235
236impl TryFrom<Command> for Request {
237    type Error = super::FromArgsError;
238    fn try_from(command: Command) -> Result<Self, Self::Error> {
239        match (command.request, command.command) {
240            // `--request <json>`: deserialize straight into the aggregate
241            // Request (serde-tagged on `path_type`) — the same wire shape the
242            // SDKs already serialize. Mutually exclusive with a subcommand
243            // (clap enforces it), so the `_` arm can't carry a command.
244            (Some(json), _) => {
245                let de = &mut serde_json::Deserializer::from_str(&json);
246                serde_path_to_error::deserialize(de)
247                    .map_err(|e| super::FromArgsError::json("request", e))
248            }
249            (None, Some(subcommand)) => Request::try_from(subcommand),
250            // Unreachable in practice: `arg_required_else_help` turns a bare
251            // invocation into a clap help error before conversion.
252            (None, None) => Err(super::FromArgsError::path_parse(
253                "command",
254                "a command path or --request is required".to_string(),
255            )),
256        }
257    }
258}
259
260impl super::CommandRequest for Request {
261    fn request_base(&self) -> &crate::cli::command::RequestBase {
262        match self {
263            Request::Agents(inner) => inner.request_base(),
264            Request::Api(inner) => inner.request_base(),
265            Request::Daemon(inner) => inner.request_base(),
266            Request::Db(inner) => inner.request_base(),
267            Request::Functions(inner) => inner.request_base(),
268            Request::KillAll(inner) => inner.request_base(),
269            Request::KillAllRequestSchema(inner) => inner.request_base(),
270            Request::KillAllResponseSchema(inner) => inner.request_base(),
271            Request::Laboratories(inner) => inner.request_base(),
272            Request::Mcp(inner) => inner.request_base(),
273            Request::Plugins(inner) => inner.request_base(),
274            Request::Python(inner) => inner.request_base(),
275            Request::PythonRequestSchema(inner) => inner.request_base(),
276            Request::PythonResponseSchema(inner) => inner.request_base(),
277            Request::Swarms(inner) => inner.request_base(),
278            Request::Tools(inner) => inner.request_base(),
279            Request::Update(inner) => inner.request_base(),
280            Request::UpdateRequestSchema(inner) => inner.request_base(),
281            Request::UpdateResponseSchema(inner) => inner.request_base(),
282            Request::Viewer(inner) => inner.request_base(),
283        }
284    }
285
286    fn request_base_mut(&mut self) -> Option<&mut crate::cli::command::RequestBase> {
287        match self {
288            Request::Agents(inner) => inner.request_base_mut(),
289            Request::Api(inner) => inner.request_base_mut(),
290            Request::Daemon(inner) => inner.request_base_mut(),
291            Request::Db(inner) => inner.request_base_mut(),
292            Request::Functions(inner) => inner.request_base_mut(),
293            Request::KillAll(inner) => inner.request_base_mut(),
294            Request::KillAllRequestSchema(inner) => inner.request_base_mut(),
295            Request::KillAllResponseSchema(inner) => inner.request_base_mut(),
296            Request::Laboratories(inner) => inner.request_base_mut(),
297            Request::Mcp(inner) => inner.request_base_mut(),
298            Request::Plugins(inner) => inner.request_base_mut(),
299            Request::Python(inner) => inner.request_base_mut(),
300            Request::PythonRequestSchema(inner) => inner.request_base_mut(),
301            Request::PythonResponseSchema(inner) => inner.request_base_mut(),
302            Request::Swarms(inner) => inner.request_base_mut(),
303            Request::Tools(inner) => inner.request_base_mut(),
304            Request::Update(inner) => inner.request_base_mut(),
305            Request::UpdateRequestSchema(inner) => inner.request_base_mut(),
306            Request::UpdateResponseSchema(inner) => inner.request_base_mut(),
307            Request::Viewer(inner) => inner.request_base_mut(),
308        }
309    }
310}
311
312#[cfg(feature = "cli-executor")]
313pub async fn execute<E: super::CommandExecutor>(
314    executor: &E,
315    request: Request,
316
317        agent_arguments: Option<&crate::cli::command::AgentArguments>,
318    ) -> Result<
319    std::pin::Pin<Box<dyn futures::Stream<Item = Result<ResponseItem, E::Error>> + Send>>,
320    E::Error,
321> {
322    use futures::StreamExt;
323    let stream: std::pin::Pin<Box<dyn futures::Stream<Item = Result<ResponseItem, E::Error>> + Send>> =
324        match request {
325            Request::Agents(req) => {
326                let inner = super::agents::execute(executor, req, agent_arguments).await?;
327                Box::pin(inner.map(|r| r.map(ResponseItem::Agents)))
328            }
329            Request::Api(req) => {
330                let inner = super::api::execute(executor, req, agent_arguments).await?;
331                Box::pin(inner.map(|r| r.map(ResponseItem::Api)))
332            }
333            Request::Daemon(req) => {
334                let inner = super::daemon::execute(executor, req, agent_arguments).await?;
335                Box::pin(inner.map(|r| r.map(ResponseItem::Daemon)))
336            }
337            Request::Db(req) => {
338                let inner = super::db::execute(executor, req, agent_arguments).await?;
339                Box::pin(inner.map(|r| r.map(ResponseItem::Db)))
340            }
341            Request::Functions(req) => {
342                let inner = super::functions::execute(executor, req, agent_arguments).await?;
343                Box::pin(inner.map(|r| r.map(ResponseItem::Functions)))
344            }
345            Request::KillAll(req) => {
346                let value = super::kill_all::execute(executor, req, agent_arguments).await?;
347                Box::pin(super::StreamOnce::new(Ok(ResponseItem::KillAll(value))))
348            }
349            Request::KillAllRequestSchema(req) => {
350                let value = super::kill_all::request_schema::execute(executor, req, agent_arguments).await?;
351                Box::pin(super::StreamOnce::new(Ok(ResponseItem::KillAllRequestSchema(value))))
352            }
353            Request::KillAllResponseSchema(req) => {
354                let value = super::kill_all::response_schema::execute(executor, req, agent_arguments).await?;
355                Box::pin(super::StreamOnce::new(Ok(ResponseItem::KillAllResponseSchema(value))))
356            }
357            Request::Laboratories(req) => {
358                let inner = super::laboratories::execute(executor, req, agent_arguments).await?;
359                Box::pin(inner.map(|r| r.map(ResponseItem::Laboratories)))
360            }
361            Request::Mcp(req) => {
362                let inner = super::mcp::execute(executor, req, agent_arguments).await?;
363                Box::pin(inner.map(|r| r.map(ResponseItem::Mcp)))
364            }
365            Request::Plugins(req) => {
366                let inner = super::plugins::execute(executor, req, agent_arguments).await?;
367                Box::pin(inner.map(|r| r.map(ResponseItem::Plugins)))
368            }
369            Request::Python(req) => {
370                let value = super::python::execute(executor, req, agent_arguments).await?;
371                Box::pin(super::StreamOnce::new(Ok(ResponseItem::Python(value))))
372            }
373            Request::PythonRequestSchema(req) => {
374                let value = super::python::request_schema::execute(executor, req, agent_arguments).await?;
375                Box::pin(super::StreamOnce::new(Ok(ResponseItem::PythonRequestSchema(value))))
376            }
377            Request::PythonResponseSchema(req) => {
378                let value = super::python::response_schema::execute(executor, req, agent_arguments).await?;
379                Box::pin(super::StreamOnce::new(Ok(ResponseItem::PythonResponseSchema(value))))
380            }
381            Request::Swarms(req) => {
382                let inner = super::swarms::execute(executor, req, agent_arguments).await?;
383                Box::pin(inner.map(|r| r.map(ResponseItem::Swarms)))
384            }
385            Request::Tools(req) => {
386                let inner = super::tools::execute(executor, req, agent_arguments).await?;
387                Box::pin(inner.map(|r| r.map(ResponseItem::Tools)))
388            }
389            Request::Update(req) => {
390                let inner = super::update::execute(executor, req, agent_arguments).await?;
391                Box::pin(inner.map(|r| r.map(ResponseItem::Update)))
392            }
393            Request::UpdateRequestSchema(req) => {
394                let value = super::update::request_schema::execute(executor, req, agent_arguments).await?;
395                Box::pin(super::StreamOnce::new(Ok(ResponseItem::UpdateRequestSchema(value))))
396            }
397            Request::UpdateResponseSchema(req) => {
398                let value = super::update::response_schema::execute(executor, req, agent_arguments).await?;
399                Box::pin(super::StreamOnce::new(Ok(ResponseItem::UpdateResponseSchema(value))))
400            }
401            Request::Viewer(req) => {
402                let inner = super::viewer::execute(executor, req, agent_arguments).await?;
403                Box::pin(inner.map(|r| r.map(ResponseItem::Viewer)))
404            }
405        };
406    Ok(stream)
407}
408
409#[cfg(feature = "cli-executor")]
410pub async fn execute_transform<E: super::CommandExecutor>(
411    executor: &E,
412    request: Request,
413    transform: crate::cli::command::Transform,
414
415        agent_arguments: Option<&crate::cli::command::AgentArguments>,
416    ) -> Result<
417    std::pin::Pin<Box<dyn futures::Stream<Item = Result<serde_json::Value, E::Error>> + Send>>,
418    E::Error,
419> {
420    let stream: std::pin::Pin<Box<dyn futures::Stream<Item = Result<serde_json::Value, E::Error>> + Send>> =
421        match request {
422            Request::Agents(req) => {
423                let inner = super::agents::execute_transform(executor, req, transform, agent_arguments).await?;
424                Box::pin(inner)
425            }
426            Request::Api(req) => {
427                let inner = super::api::execute_transform(executor, req, transform, agent_arguments).await?;
428                Box::pin(inner)
429            }
430            Request::Daemon(req) => {
431                let inner = super::daemon::execute_transform(executor, req, transform, agent_arguments).await?;
432                Box::pin(inner)
433            }
434            Request::Db(req) => {
435                let inner = super::db::execute_transform(executor, req, transform, agent_arguments).await?;
436                Box::pin(inner)
437            }
438            Request::Functions(req) => {
439                let inner = super::functions::execute_transform(executor, req, transform, agent_arguments).await?;
440                Box::pin(inner)
441            }
442            Request::KillAll(req) => {
443                let value = super::kill_all::execute_transform(executor, req, transform, agent_arguments).await?;
444                Box::pin(super::StreamOnce::new(Ok(value)))
445            }
446            Request::KillAllRequestSchema(req) => {
447                let value = super::kill_all::request_schema::execute_transform(executor, req, transform, agent_arguments).await?;
448                Box::pin(super::StreamOnce::new(Ok(value)))
449            }
450            Request::KillAllResponseSchema(req) => {
451                let value = super::kill_all::response_schema::execute_transform(executor, req, transform, agent_arguments).await?;
452                Box::pin(super::StreamOnce::new(Ok(value)))
453            }
454            Request::Laboratories(req) => {
455                let inner = super::laboratories::execute_transform(executor, req, transform, agent_arguments).await?;
456                Box::pin(inner)
457            }
458            Request::Mcp(req) => {
459                let inner = super::mcp::execute_transform(executor, req, transform, agent_arguments).await?;
460                Box::pin(inner)
461            }
462            Request::Plugins(req) => {
463                let inner = super::plugins::execute_transform(executor, req, transform, agent_arguments).await?;
464                Box::pin(inner)
465            }
466            Request::Python(req) => {
467                let value = super::python::execute_transform(executor, req, transform, agent_arguments).await?;
468                Box::pin(super::StreamOnce::new(Ok(value)))
469            }
470            Request::PythonRequestSchema(req) => {
471                let value = super::python::request_schema::execute_transform(executor, req, transform, agent_arguments).await?;
472                Box::pin(super::StreamOnce::new(Ok(value)))
473            }
474            Request::PythonResponseSchema(req) => {
475                let value = super::python::response_schema::execute_transform(executor, req, transform, agent_arguments).await?;
476                Box::pin(super::StreamOnce::new(Ok(value)))
477            }
478            Request::Swarms(req) => {
479                let inner = super::swarms::execute_transform(executor, req, transform, agent_arguments).await?;
480                Box::pin(inner)
481            }
482            Request::Tools(req) => {
483                let inner = super::tools::execute_transform(executor, req, transform, agent_arguments).await?;
484                Box::pin(inner)
485            }
486            Request::Update(req) => {
487                let inner = super::update::execute_transform(executor, req, transform, agent_arguments).await?;
488                Box::pin(inner)
489            }
490            Request::UpdateRequestSchema(req) => {
491                let value = super::update::request_schema::execute_transform(executor, req, transform, agent_arguments).await?;
492                Box::pin(super::StreamOnce::new(Ok(value)))
493            }
494            Request::UpdateResponseSchema(req) => {
495                let value = super::update::response_schema::execute_transform(executor, req, transform, agent_arguments).await?;
496                Box::pin(super::StreamOnce::new(Ok(value)))
497            }
498            Request::Viewer(req) => {
499                let inner = super::viewer::execute_transform(executor, req, transform, agent_arguments).await?;
500                Box::pin(inner)
501            }
502        };
503    Ok(stream)
504}
505
506/// Parse an argv slice into a typed [`Request`]. Accepts either
507/// shape:
508///
509/// - With a program-name prefix (`["objectiveai", "agents", "list"]`)
510///   — matches `std::env::args()` and binary entry-point usage.
511/// - Without one (`["agents", "list"]`) — matches
512///   [`super::CommandRequest::into_command`]'s output shape and the
513///   "command-only" shape MCP callers send.
514///
515/// If `args[0]` is `"objectiveai"` we pass it straight through;
516/// otherwise we prepend so clap (which always treats argv[0] as the
517/// program name) sees a well-formed argv. Hides clap behind the SDK
518/// boundary so downstream crates can dispatch arbitrary argv without
519/// taking a clap dep themselves.
520pub fn parse_request(args: &[String]) -> Result<Request, ParseError> {
521    let command = if args.first().map(String::as_str) == Some("objectiveai") {
522        <Command as clap::Parser>::try_parse_from(args)?
523    } else {
524        let argv = std::iter::once("objectiveai".to_string())
525            .chain(args.iter().cloned());
526        <Command as clap::Parser>::try_parse_from(argv)?
527    };
528    Ok(Request::try_from(command)?)
529}
530
531/// Top-level CLI parser: a command path, OR `--request <json>` to execute a
532/// JSON `CliCommandRequest` directly. The two are mutually exclusive
533/// (`args_conflicts_with_subcommands`, enforced natively by clap); a bare
534/// invocation prints help (`arg_required_else_help`). `--request` is a
535/// legitimate but unadvertised programmatic entry point.
536#[derive(clap::Parser)]
537#[command(
538    name = "objectiveai",
539    args_conflicts_with_subcommands = true,
540    arg_required_else_help = true
541)]
542pub struct Command {
543    /// Execute this JSON `CliCommandRequest` instead of a command path.
544    /// Mutually exclusive with any command.
545    #[arg(long, value_name = "JSON")]
546    pub request: Option<String>,
547    #[command(subcommand)]
548    pub command: Option<Subcommand>,
549}
550
551/// Error from [`parse_request`]. Either clap rejected the argv
552/// ([`ParseError::Clap`] — `--help`, unknown subcommand, missing
553/// required arg, etc.) or the typed `Command` couldn't be lowered
554/// into a `Request` ([`ParseError::FromArgs`] — inline body JSON
555/// failed to parse, etc.).
556#[derive(Debug)]
557pub enum ParseError {
558    Clap(clap::Error),
559    FromArgs(super::FromArgsError),
560}
561
562impl std::fmt::Display for ParseError {
563    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
564        match self {
565            ParseError::Clap(e) => write!(f, "{e}"),
566            ParseError::FromArgs(e) => write!(f, "{e}"),
567        }
568    }
569}
570
571impl std::error::Error for ParseError {}
572
573impl From<clap::Error> for ParseError {
574    fn from(e: clap::Error) -> Self {
575        ParseError::Clap(e)
576    }
577}
578
579impl From<super::FromArgsError> for ParseError {
580    fn from(e: super::FromArgsError) -> Self {
581        ParseError::FromArgs(e)
582    }
583}