Skip to main content

zeroclaw/
lib.rs

1#![warn(clippy::all, clippy::pedantic)]
2#![allow(
3    clippy::assigning_clones,
4    clippy::bool_to_int_with_if,
5    clippy::case_sensitive_file_extension_comparisons,
6    clippy::cast_possible_wrap,
7    clippy::doc_markdown,
8    clippy::field_reassign_with_default,
9    clippy::float_cmp,
10    clippy::implicit_clone,
11    clippy::items_after_statements,
12    clippy::map_unwrap_or,
13    clippy::manual_let_else,
14    clippy::missing_errors_doc,
15    clippy::missing_panics_doc,
16    clippy::module_name_repetitions,
17    clippy::must_use_candidate,
18    clippy::new_without_default,
19    clippy::needless_pass_by_value,
20    clippy::needless_raw_string_hashes,
21    clippy::redundant_closure_for_method_calls,
22    clippy::return_self_not_must_use,
23    clippy::similar_names,
24    clippy::single_match_else,
25    clippy::struct_field_names,
26    clippy::too_many_lines,
27    clippy::uninlined_format_args,
28    clippy::unnecessary_cast,
29    clippy::unnecessary_lazy_evaluations,
30    clippy::unnecessary_literal_bound,
31    clippy::unnecessary_map_or,
32    clippy::unused_self,
33    clippy::cast_precision_loss,
34    clippy::unnecessary_wraps
35)]
36
37use clap::Subcommand;
38use serde::{Deserialize, Serialize};
39
40#[cfg(feature = "agent-runtime")]
41pub mod agent;
42#[cfg(feature = "agent-runtime")]
43pub(crate) mod approval;
44#[cfg(feature = "agent-runtime")]
45pub(crate) mod auth;
46#[cfg(feature = "agent-runtime")]
47pub mod channels;
48pub mod commands;
49pub mod config;
50#[cfg(feature = "agent-runtime")]
51pub(crate) mod cost;
52#[cfg(feature = "agent-runtime")]
53pub mod cron;
54#[cfg(feature = "agent-runtime")]
55pub(crate) mod daemon;
56#[cfg(feature = "agent-runtime")]
57pub(crate) mod doctor;
58#[cfg(feature = "gateway")]
59pub mod gateway;
60#[cfg(feature = "agent-runtime")]
61pub(crate) mod hardware;
62#[cfg(feature = "agent-runtime")]
63pub(crate) mod health;
64#[cfg(feature = "agent-runtime")]
65pub(crate) mod heartbeat;
66#[cfg(feature = "agent-runtime")]
67pub mod hooks;
68#[cfg(feature = "agent-runtime")]
69pub(crate) mod integrations;
70pub mod memory;
71#[cfg(feature = "agent-runtime")]
72pub(crate) mod multimodal;
73#[cfg(feature = "agent-runtime")]
74pub mod nodes;
75#[cfg(feature = "agent-runtime")]
76pub mod observability;
77#[cfg(feature = "agent-runtime")]
78pub mod peripherals;
79#[cfg(feature = "agent-runtime")]
80pub mod platform;
81pub mod providers;
82#[cfg(feature = "agent-runtime")]
83pub mod rag;
84#[cfg(feature = "agent-runtime")]
85pub mod routines;
86#[cfg(feature = "agent-runtime")]
87pub(crate) mod security;
88#[cfg(feature = "agent-runtime")]
89pub(crate) mod service;
90#[cfg(feature = "agent-runtime")]
91pub(crate) mod skills;
92#[cfg(feature = "agent-runtime")]
93pub mod sop;
94#[cfg(feature = "agent-runtime")]
95pub mod tools;
96#[cfg(feature = "agent-runtime")]
97pub(crate) mod trust;
98#[cfg(feature = "agent-runtime")]
99pub(crate) mod tunnel;
100#[cfg(feature = "agent-runtime")]
101pub mod verifiable_intent;
102
103#[cfg(feature = "plugins-wasm")]
104pub mod plugins;
105
106pub use config::Config;
107
108/// Gateway management subcommands
109#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110pub enum GatewayCommands {
111    /// Start the gateway server (default if no subcommand specified)
112    #[command(long_about = "\
113Start the gateway server (webhooks, websockets).
114
115Runs the HTTP/WebSocket gateway that accepts incoming webhook events \
116and WebSocket connections. Bind address defaults to the values in \
117your config file (gateway.host / gateway.port).
118
119Examples:
120  zeroclaw gateway start              # use config defaults
121  zeroclaw gateway start -p 8080      # listen on port 8080
122  zeroclaw gateway start --host 0.0.0.0   # requires [gateway].allow_public_bind=true or a tunnel
123  zeroclaw gateway start -p 0         # random available port")]
124    Start {
125        /// Port to listen on (use 0 for random available port); defaults to config gateway.port
126        #[arg(short, long)]
127        port: Option<u16>,
128
129        /// Host to bind to; defaults to config gateway.host
130        /// Note: Binding to 0.0.0.0 requires `gateway.allow_public_bind = true` in config
131        #[arg(long)]
132        host: Option<String>,
133    },
134    /// Restart the gateway server
135    #[command(long_about = "\
136Restart the gateway server.
137
138Stops the running gateway if present, then starts a new instance \
139with the current configuration.
140
141Examples:
142  zeroclaw gateway restart            # restart with config defaults
143  zeroclaw gateway restart -p 8080    # restart on port 8080")]
144    Restart {
145        /// Port to listen on (use 0 for random available port); defaults to config gateway.port
146        #[arg(short, long)]
147        port: Option<u16>,
148
149        /// Host to bind to; defaults to config gateway.host
150        /// Note: Binding to 0.0.0.0 requires `gateway.allow_public_bind = true` in config
151        #[arg(long)]
152        host: Option<String>,
153    },
154    /// Show or generate the pairing code without restarting
155    #[command(long_about = "\
156Show or generate the gateway pairing code.
157
158Displays the pairing code for connecting new clients without \
159restarting the gateway. Requires the gateway to be running.
160
161With --new, generates a fresh pairing code even if the gateway \
162was previously paired (useful for adding additional clients).
163
164Examples:
165  zeroclaw gateway get-paircode       # show current pairing code
166  zeroclaw gateway get-paircode --new # generate a new pairing code
167  zeroclaw gateway get-paircode --new --port 3001 # target alternate-port gateway")]
168    GetPaircode {
169        /// Generate a new pairing code (even if already paired)
170        #[arg(long)]
171        new: bool,
172
173        /// Port of the running gateway to query; defaults to config gateway.port
174        #[arg(short, long)]
175        port: Option<u16>,
176
177        /// Host of the running gateway to query; defaults to config gateway.host
178        #[arg(long)]
179        host: Option<String>,
180    },
181}
182
183/// Service management subcommands
184#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
185pub enum ServiceCommands {
186    /// Install daemon service unit for auto-start and restart
187    Install,
188    /// Start daemon service
189    Start,
190    /// Stop daemon service
191    Stop,
192    /// Restart daemon service to apply latest config
193    Restart,
194    /// Check daemon service status
195    Status,
196    /// Uninstall daemon service unit
197    Uninstall,
198    /// Tail daemon service logs
199    Logs {
200        /// Number of lines to show (default: 50)
201        #[arg(short = 'n', long, default_value = "50")]
202        lines: usize,
203        /// Follow log output (like tail -f)
204        #[arg(short, long)]
205        follow: bool,
206    },
207}
208
209/// Channel management subcommands
210#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
211pub enum ChannelCommands {
212    /// List all configured channels
213    List,
214    /// Start all configured channels (handled in main.rs for async)
215    Start,
216    /// Run health checks for configured channels (handled in main.rs for async)
217    Doctor,
218    /// Add a new channel configuration
219    #[command(long_about = "\
220Add a new channel configuration.
221
222Provide the channel type and a JSON object with the required \
223configuration keys for that channel type.
224
225Supported types: telegram, discord, slack, whatsapp, matrix, imessage, email.
226
227Examples:
228  zeroclaw channel add telegram '{\"bot_token\":\"...\",\"name\":\"my-bot\"}'
229  zeroclaw channel add discord '{\"bot_token\":\"...\",\"name\":\"my-discord\"}'")]
230    Add {
231        /// Channel type (telegram, discord, slack, whatsapp, matrix, imessage, email)
232        channel_type: String,
233        /// Optional configuration as JSON
234        config: String,
235    },
236    /// Remove a channel configuration
237    Remove {
238        /// Channel name to remove
239        name: String,
240    },
241    /// Bind a Telegram identity (username or numeric user ID) into allowlist
242    #[command(long_about = "\
243Bind a Telegram identity into the allowlist.
244
245Adds a Telegram username (without the '@' prefix) or numeric user \
246ID to the channel allowlist so the agent will respond to messages \
247from that identity.
248
249Examples:
250  zeroclaw channel bind-telegram zeroclaw_user
251  zeroclaw channel bind-telegram 123456789")]
252    BindTelegram {
253        /// Telegram identity to allow (username without '@' or numeric user ID)
254        identity: String,
255    },
256    /// Send a message to a configured channel
257    #[command(long_about = "\
258Send a one-off message to a configured channel.
259
260Sends a text message through the specified channel without starting \
261the full agent loop. Useful for scripted notifications, hardware \
262sensor alerts, and automation pipelines.
263
264The --channel-id selects the channel by its config section name \
265(e.g. 'telegram', 'discord', 'slack'). The --recipient is the \
266platform-specific destination (e.g. a Telegram chat ID).
267
268Examples:
269  zeroclaw channel send 'Someone is near your device.' --channel-id telegram --recipient 123456789
270  zeroclaw channel send 'Build succeeded!' --channel-id discord --recipient 987654321")]
271    Send {
272        /// Message text to send
273        message: String,
274        /// Channel config name (e.g. telegram, discord, slack)
275        #[arg(long)]
276        channel_id: String,
277        /// Recipient identifier (platform-specific, e.g. Telegram chat ID)
278        #[arg(long)]
279        recipient: String,
280    },
281}
282
283/// Skills management subcommands
284#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
285pub enum SkillCommands {
286    /// List all installed skills
287    List,
288    /// Scaffold a new skill from scratch (canonical SKILL.md + optional subdirs)
289    #[command(long_about = "\
290Scaffold a new skill under a skill bundle. Writes <bundle.directory>/<name>/SKILL.md \
291plus the canonical optional subdirs (scripts/, references/, assets/). \
292Name must be lowercase + hyphens; description is required (prompted on TTY if omitted).
293
294Examples:
295  zeroclaw skills add code-review --bundle official --description \"Review PRs.\"
296  zeroclaw skills add ops-runbook --description \"Triage prod incidents.\" --edit")]
297    Add {
298        /// Skill name (lowercase + hyphens only)
299        name: String,
300        /// Target bundle alias. Optional when exactly one bundle is configured.
301        #[arg(long)]
302        bundle: Option<String>,
303        /// What the skill does and when to use it (frontmatter `description`).
304        /// Required; prompted on TTY when missing.
305        #[arg(long)]
306        description: Option<String>,
307        /// SPDX license identifier (e.g. MIT).
308        #[arg(long)]
309        license: Option<String>,
310        /// Skill author handle.
311        #[arg(long)]
312        author: Option<String>,
313        /// SemVer version (defaults to 0.1.0).
314        #[arg(long)]
315        version: Option<String>,
316        /// Skill category for registry grouping.
317        #[arg(long)]
318        category: Option<String>,
319        /// Skip scaffolding scripts/, references/, assets/.
320        #[arg(long)]
321        no_scaffold: bool,
322        /// Open SKILL.md in $EDITOR after scaffold.
323        #[arg(long)]
324        edit: bool,
325    },
326    /// Open a skill's SKILL.md (or a sibling file) in $EDITOR
327    Edit {
328        /// Skill name
329        name: String,
330        /// Target bundle alias. Optional when name is unique across bundles.
331        #[arg(long)]
332        bundle: Option<String>,
333        /// Edit a sibling file instead of SKILL.md (e.g. scripts/runner.sh).
334        #[arg(long)]
335        file: Option<String>,
336    },
337    /// Manage skill bundles (the named directories skills live in)
338    Bundle {
339        #[command(subcommand)]
340        bundle_command: SkillBundleCommands,
341    },
342    /// Audit a skill source directory or installed skill name
343    Audit {
344        /// Skill path or installed skill name
345        source: String,
346    },
347    /// Install a new skill from a URL or local path
348    Install {
349        /// Source URL or local path
350        source: String,
351        /// Suppress only the install-time tier banner; other install
352        /// progress output (resolving, installed, audited) is unaffected.
353        #[arg(long)]
354        no_tier_banner: bool,
355    },
356    /// Remove an installed skill
357    Remove {
358        /// Skill name to remove
359        name: String,
360    },
361    /// Run TEST.sh validation for a skill (or all skills)
362    Test {
363        /// Skill name to test; omit for all skills
364        name: Option<String>,
365        /// Show verbose output
366        #[arg(long)]
367        verbose: bool,
368    },
369}
370
371/// Skill bundle subcommands (`zeroclaw skills bundle <op>`)
372#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
373pub enum SkillBundleCommands {
374    /// List configured skill bundles and their resolved directories
375    List,
376    /// Add a new skill bundle. Directory defaults to shared/skills/<alias>/.
377    Add {
378        /// Bundle alias (lowercase + hyphens; same convention as agents/channels)
379        alias: String,
380        /// Override directory (relative to install root or absolute).
381        /// Must resolve inside `<install>/shared/`.
382        #[arg(long)]
383        directory: Option<String>,
384    },
385    /// Remove a configured skill bundle
386    Remove {
387        /// Bundle alias
388        alias: String,
389    },
390    /// Show metadata + skill list for a bundle
391    Show {
392        /// Bundle alias
393        alias: String,
394    },
395}
396
397/// Migration subcommands
398#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
399pub enum MigrateCommands {
400    /// Import memory from an `OpenClaw` workspace into this `ZeroClaw` workspace
401    Openclaw {
402        /// Optional path to `OpenClaw` workspace (defaults to ~/.openclaw/workspace)
403        #[arg(long)]
404        source: Option<std::path::PathBuf>,
405
406        /// Validate and preview migration without writing any data
407        #[arg(long)]
408        dry_run: bool,
409    },
410}
411
412/// Cron subcommands
413#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
414pub enum CronCommands {
415    /// List all scheduled tasks
416    List,
417    /// Add a new scheduled task
418    #[command(long_about = "\
419Add a new recurring scheduled task.
420
421Uses standard 5-field cron syntax: 'min hour day month weekday'. \
422When --tz is omitted, cron schedules use the runtime local timezone. \
423For user-facing schedules, pass --tz with an explicit IANA timezone.
424
425Examples:
426  zeroclaw cron add '0 9 * * 1-5' 'Good morning' --tz America/New_York --agent
427  zeroclaw cron add '*/30 * * * *' 'Check system health' --agent
428  zeroclaw cron add '*/5 * * * *' 'echo ok'")]
429    Add {
430        /// Cron expression
431        expression: String,
432        /// Configured agent alias the cron job runs as. Required —
433        /// there is no default agent.
434        #[arg(short = 'a', long = "agent")]
435        agent_alias: String,
436        /// Optional IANA timezone (e.g. America/Los_Angeles)
437        #[arg(long)]
438        tz: Option<String>,
439        /// Treat the argument as an agent prompt instead of a shell command.
440        #[arg(long)]
441        prompt: bool,
442        /// Restrict agent cron jobs to the specified tool names (repeatable, prompt-only).
443        #[arg(long = "allowed-tool")]
444        allowed_tools: Vec<String>,
445        /// Command (shell) or prompt (when --prompt) to run
446        command: String,
447    },
448    /// Add a one-shot scheduled task at an RFC3339 timestamp with explicit Z or offset
449    #[command(long_about = "\
450Add a one-shot task that fires at a specific RFC3339 timestamp with explicit Z or offset.
451
452The timestamp must include an explicit Z or numeric offset \
453(e.g. 2025-01-15T14:00:00Z or 2025-01-15T09:00:00-05:00).
454
455Examples:
456  zeroclaw cron add-at --agent morning-shift 2025-01-15T14:00:00Z 'Send reminder'
457  zeroclaw cron add-at --agent morning-shift --prompt 2025-12-31T23:59:00Z 'Happy New Year!'")]
458    AddAt {
459        /// One-shot RFC3339 timestamp with explicit Z or offset
460        at: String,
461        /// Configured agent alias the cron job runs as.
462        #[arg(short = 'a', long = "agent")]
463        agent_alias: String,
464        /// Treat the argument as an agent prompt instead of a shell command.
465        #[arg(long)]
466        prompt: bool,
467        /// Restrict agent cron jobs to the specified tool names (repeatable, prompt-only).
468        #[arg(long = "allowed-tool")]
469        allowed_tools: Vec<String>,
470        /// Command (shell) or prompt (when --prompt) to run
471        command: String,
472    },
473    /// Add a fixed-interval scheduled task
474    #[command(long_about = "\
475Add a task that repeats at a fixed interval.
476
477Interval is specified in milliseconds. For example, 60000 = 1 minute.
478
479Examples:
480  zeroclaw cron add-every --agent triage 60000 'Ping heartbeat'
481  zeroclaw cron add-every --agent triage 3600000 'Hourly report'")]
482    AddEvery {
483        /// Interval in milliseconds
484        every_ms: u64,
485        /// Configured agent alias the cron job runs as.
486        #[arg(short = 'a', long = "agent")]
487        agent_alias: String,
488        /// Treat the argument as an agent prompt instead of a shell command.
489        #[arg(long)]
490        prompt: bool,
491        /// Restrict agent cron jobs to the specified tool names (repeatable, prompt-only).
492        #[arg(long = "allowed-tool")]
493        allowed_tools: Vec<String>,
494        /// Command (shell) or prompt (when --prompt) to run
495        command: String,
496    },
497    /// Add a one-shot delayed task (e.g. "30m", "2h", "1d")
498    #[command(long_about = "\
499Add a one-shot task that fires after a delay from now.
500
501Accepts human-readable durations: s (seconds), m (minutes), \
502h (hours), d (days).
503
504Examples:
505  zeroclaw cron once --agent ops-bot 30m 'Run backup in 30 minutes'
506  zeroclaw cron once --agent researcher --prompt 2h 'Follow up on deployment'")]
507    Once {
508        /// Delay duration
509        delay: String,
510        /// Configured agent alias the cron job runs as.
511        #[arg(short = 'a', long = "agent")]
512        agent_alias: String,
513        /// Treat the argument as an agent prompt instead of a shell command.
514        #[arg(long)]
515        prompt: bool,
516        /// Restrict agent cron jobs to the specified tool names (repeatable, prompt-only).
517        #[arg(long = "allowed-tool")]
518        allowed_tools: Vec<String>,
519        /// Command (shell) or prompt (when --prompt) to run
520        command: String,
521    },
522    /// Remove a scheduled task
523    Remove {
524        /// Task ID
525        id: String,
526    },
527    /// Update a scheduled task
528    #[command(long_about = "\
529Update one or more fields of an existing scheduled task.
530
531Only the fields you specify are changed; others remain unchanged.
532
533Examples:
534  zeroclaw cron update TASK_ID --expression '0 8 * * *'
535  zeroclaw cron update TASK_ID --tz Europe/London --name 'Morning check'
536  zeroclaw cron update TASK_ID --command 'Updated message'")]
537    Update {
538        /// Task ID
539        id: String,
540        /// Configured agent alias whose risk profile gates the new
541        /// shell command (when --command is provided). Required.
542        #[arg(short = 'a', long = "agent")]
543        agent_alias: String,
544        /// New cron expression
545        #[arg(long)]
546        expression: Option<String>,
547        /// New IANA timezone
548        #[arg(long)]
549        tz: Option<String>,
550        /// New command to run
551        #[arg(long)]
552        command: Option<String>,
553        /// New job name
554        #[arg(long)]
555        name: Option<String>,
556        /// Replace the agent job allowlist with the specified tool names (repeatable)
557        #[arg(long = "allowed-tool")]
558        allowed_tools: Vec<String>,
559    },
560    /// Pause a scheduled task
561    Pause {
562        /// Task ID
563        id: String,
564    },
565    /// Resume a paused task
566    Resume {
567        /// Task ID
568        id: String,
569    },
570}
571
572/// Memory management subcommands
573#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
574pub enum MemoryCommands {
575    /// List memory entries with optional filters
576    List {
577        /// Filter by category (core, daily, conversation, or custom name)
578        #[arg(long)]
579        category: Option<String>,
580        /// Filter by session ID
581        #[arg(long)]
582        session: Option<String>,
583        /// Maximum number of entries to display
584        #[arg(long, default_value = "50")]
585        limit: usize,
586        /// Number of entries to skip (for pagination)
587        #[arg(long, default_value = "0")]
588        offset: usize,
589    },
590    /// Get a specific memory entry by key
591    Get {
592        /// Memory key to look up
593        key: String,
594    },
595    /// Show memory backend statistics and health
596    Stats,
597    /// Clear memories by category, by key, or clear all
598    Clear {
599        /// Delete a single entry by key (supports prefix match)
600        #[arg(long)]
601        key: Option<String>,
602        /// Only clear entries in this category
603        #[arg(long)]
604        category: Option<String>,
605        /// Skip confirmation prompt
606        #[arg(long)]
607        yes: bool,
608    },
609    /// Rebuild backend indexes: FTS tables + any missing embedding vectors.
610    ///
611    /// Run after `zeroclaw migrate openclaw` or other bulk writes that
612    /// land rows with `embedding = NULL`. Safe to re-run; only touches
613    /// entries whose vector is missing. No-op for backends without a
614    /// vector index.
615    Reindex,
616}
617
618/// Integration subcommands
619#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
620pub enum IntegrationCommands {
621    /// Show details about a specific integration
622    Info {
623        /// Integration name
624        name: String,
625    },
626}
627
628/// Hardware discovery subcommands
629#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
630pub enum HardwareCommands {
631    /// Enumerate USB devices (VID/PID) and show known boards
632    #[command(long_about = "\
633Enumerate USB devices and show known boards.
634
635Scans connected USB devices by VID/PID and matches them against \
636known development boards (STM32 Nucleo, Arduino, ESP32).
637
638Examples:
639  zeroclaw hardware discover")]
640    Discover,
641    /// Introspect a device by path (e.g. /dev/ttyACM0)
642    #[command(long_about = "\
643Introspect a device by its serial or device path.
644
645Opens the specified device path and queries for board information, \
646firmware version, and supported capabilities.
647
648Examples:
649  zeroclaw hardware introspect /dev/ttyACM0
650  zeroclaw hardware introspect COM3")]
651    Introspect {
652        /// Serial or device path
653        path: String,
654    },
655    /// Get chip info via USB (probe-rs over ST-Link). No firmware needed on target.
656    #[command(long_about = "\
657Get chip info via USB using probe-rs over ST-Link.
658
659Queries the target MCU directly through the debug probe without \
660requiring any firmware on the target board.
661
662Examples:
663  zeroclaw hardware info
664  zeroclaw hardware info --chip STM32F401RETx")]
665    Info {
666        /// Chip name (e.g. STM32F401RETx). Default: STM32F401RETx for Nucleo-F401RE
667        #[arg(long, default_value = "STM32F401RETx")]
668        chip: String,
669    },
670}
671
672/// Peripheral (hardware) management subcommands
673#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
674pub enum PeripheralCommands {
675    /// List configured peripherals
676    List,
677    /// Add a peripheral (board path, e.g. nucleo-f401re /dev/ttyACM0)
678    #[command(long_about = "\
679Add a peripheral by board type and transport path.
680
681Registers a hardware board so the agent can use its tools (GPIO, \
682sensors, actuators). Use 'native' as path for local GPIO on \
683single-board computers like Raspberry Pi.
684
685Supported boards: nucleo-f401re, rpi-gpio, esp32, arduino-uno.
686
687Examples:
688  zeroclaw peripheral add nucleo-f401re /dev/ttyACM0
689  zeroclaw peripheral add rpi-gpio native
690  zeroclaw peripheral add esp32 /dev/ttyUSB0")]
691    Add {
692        /// Board type (nucleo-f401re, rpi-gpio, esp32)
693        board: String,
694        /// Path for serial transport (/dev/ttyACM0) or "native" for local GPIO
695        path: String,
696    },
697    /// Flash ZeroClaw firmware to Arduino (creates .ino, installs arduino-cli if needed, uploads)
698    #[command(long_about = "\
699Flash ZeroClaw firmware to an Arduino board.
700
701Generates the .ino sketch, installs arduino-cli if it is not \
702already available, compiles, and uploads the firmware.
703
704Examples:
705  zeroclaw peripheral flash
706  zeroclaw peripheral flash --port /dev/cu.usbmodem12345
707  zeroclaw peripheral flash -p COM3")]
708    Flash {
709        /// Serial port (e.g. /dev/cu.usbmodem12345). If omitted, uses first arduino-uno from config.
710        #[arg(short, long)]
711        port: Option<String>,
712    },
713    /// Setup Arduino Uno Q Bridge app (deploy GPIO bridge for agent control)
714    SetupUnoQ {
715        /// Uno Q IP (e.g. 192.168.0.48). If omitted, assumes running ON the Uno Q.
716        #[arg(long)]
717        host: Option<String>,
718    },
719    /// Flash ZeroClaw firmware to Nucleo-F401RE (builds + probe-rs run)
720    FlashNucleo,
721}
722
723/// SOP management subcommands
724#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
725pub enum SopCommands {
726    /// List loaded SOPs
727    List,
728    /// Validate SOP definitions
729    Validate {
730        /// SOP name to validate (all if omitted)
731        name: Option<String>,
732    },
733    /// Show details of an SOP
734    Show {
735        /// Name of the SOP to show
736        name: String,
737    },
738}