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