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 clippy::cast_possible_truncation,
40 clippy::cast_sign_loss,
41 clippy::collapsible_if,
42 clippy::default_trait_access,
43 clippy::doc_overindented_list_items,
44 clippy::double_ended_iterator_last,
45 clippy::double_must_use,
46 clippy::empty_line_after_doc_comments,
47 clippy::format_push_string,
48 clippy::if_not_else,
49 clippy::implicit_hasher,
50 clippy::ip_constant,
51 clippy::len_without_is_empty,
52 clippy::manual_clamp,
53 clippy::manual_contains,
54 clippy::manual_string_new,
55 clippy::match_same_arms,
56 clippy::needless_borrow,
57 clippy::needless_continue,
58 clippy::needless_option_as_deref,
59 clippy::redundant_closure,
60 clippy::ref_option,
61 clippy::result_large_err,
62 clippy::struct_excessive_bools,
63 clippy::trivially_copy_pass_by_ref,
64 clippy::type_complexity,
65 clippy::unchecked_time_subtraction,
66 clippy::unnested_or_patterns,
67 clippy::unused_async,
68 clippy::unwrap_or_default,
69 clippy::used_underscore_binding,
70 clippy::vec_init_then_push,
71 dead_code
72)]
73
74use clap::Subcommand;
75use serde::{Deserialize, Serialize};
76
77pub mod agent;
78pub(crate) mod approval;
79pub(crate) mod auth;
80pub mod channels;
81pub(crate) mod cli_input;
82pub mod commands;
83pub mod config;
84pub(crate) mod cost;
85pub mod cron;
86pub(crate) mod daemon;
87pub(crate) mod doctor;
88pub mod gateway;
89pub(crate) mod hardware;
90pub(crate) mod health;
91pub(crate) mod heartbeat;
92pub mod hooks;
93pub mod i18n;
94pub(crate) mod identity;
95pub(crate) mod integrations;
96pub mod mcp_server;
97pub mod memory;
98pub(crate) mod migration;
99pub(crate) mod multimodal;
100pub mod nodes;
101pub mod observability;
102pub(crate) mod onboard;
103pub mod peripherals;
104pub mod providers;
105pub mod rag;
106pub mod runtime;
107pub(crate) mod security;
108pub(crate) mod service;
109pub mod sidecars;
110pub(crate) mod skills;
111pub mod sop;
112pub mod tools;
113pub(crate) mod trust;
114pub(crate) mod tunnel;
115pub(crate) mod util;
116pub mod verifiable_intent;
117
118#[cfg(feature = "plugins-wasm")]
119pub mod plugins;
120
121pub use config::Config;
122
123#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
125pub enum GatewayCommands {
126 #[command(long_about = "\
128Start the gateway server (webhooks, websockets).
129
130Runs the HTTP/WebSocket gateway that accepts incoming webhook events \
131and WebSocket connections. Bind address defaults to the values in \
132your config file (gateway.host / gateway.port).
133
134Examples:
135 construct gateway start # use config defaults
136 construct gateway start -p 8080 # listen on port 8080
137 construct gateway start --host 0.0.0.0 # requires [gateway].allow_public_bind=true or a tunnel
138 construct gateway start -p 0 # random available port")]
139 Start {
140 #[arg(short, long)]
142 port: Option<u16>,
143
144 #[arg(long)]
147 host: Option<String>,
148 },
149 #[command(long_about = "\
151Restart the gateway server.
152
153Stops the running gateway if present, then starts a new instance \
154with the current configuration.
155
156Examples:
157 construct gateway restart # restart with config defaults
158 construct gateway restart -p 8080 # restart on port 8080")]
159 Restart {
160 #[arg(short, long)]
162 port: Option<u16>,
163
164 #[arg(long)]
167 host: Option<String>,
168 },
169 #[command(long_about = "\
171Show or generate the gateway pairing code.
172
173Displays the pairing code for connecting new clients without \
174restarting the gateway. Requires the gateway to be running.
175
176With --new, generates a fresh pairing code even if the gateway \
177was previously paired (useful for adding additional clients).
178
179Examples:
180 construct gateway get-paircode # show current pairing code
181 construct gateway get-paircode --new # generate a new pairing code")]
182 GetPaircode {
183 #[arg(long)]
185 new: bool,
186 },
187}
188
189#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
191pub enum ServiceCommands {
192 Install,
194 Start,
196 Stop,
198 Restart,
200 Status,
202 Uninstall,
204 Logs {
206 #[arg(short = 'n', long, default_value = "50")]
208 lines: usize,
209 #[arg(short, long)]
211 follow: bool,
212 },
213}
214
215#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
217pub enum ChannelCommands {
218 List,
220 Start,
222 Doctor,
224 #[command(long_about = "\
226Add a new channel configuration.
227
228Provide the channel type and a JSON object with the required \
229configuration keys for that channel type.
230
231Supported types: telegram, discord, slack, whatsapp, matrix, imessage, email.
232
233Examples:
234 construct channel add telegram '{\"bot_token\":\"...\",\"name\":\"my-bot\"}'
235 construct channel add discord '{\"bot_token\":\"...\",\"name\":\"my-discord\"}'")]
236 Add {
237 channel_type: String,
239 config: String,
241 },
242 Remove {
244 name: String,
246 },
247 #[command(long_about = "\
249Bind a Telegram identity into the allowlist.
250
251Adds a Telegram username (without the '@' prefix) or numeric user \
252ID to the channel allowlist so the agent will respond to messages \
253from that identity.
254
255Examples:
256 construct channel bind-telegram construct_user
257 construct channel bind-telegram 123456789")]
258 BindTelegram {
259 identity: String,
261 },
262 #[command(long_about = "\
264Send a one-off message to a configured channel.
265
266Sends a text message through the specified channel without starting \
267the full agent loop. Useful for scripted notifications, hardware \
268sensor alerts, and automation pipelines.
269
270The --channel-id selects the channel by its config section name \
271(e.g. 'telegram', 'discord', 'slack'). The --recipient is the \
272platform-specific destination (e.g. a Telegram chat ID).
273
274Examples:
275 construct channel send 'Someone is near your device.' --channel-id telegram --recipient 123456789
276 construct channel send 'Build succeeded!' --channel-id discord --recipient 987654321")]
277 Send {
278 message: String,
280 #[arg(long)]
282 channel_id: String,
283 #[arg(long)]
285 recipient: String,
286 },
287}
288
289#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
291pub enum SkillCommands {
292 List,
294 Audit {
296 source: String,
298 },
299 Install {
301 source: String,
303 },
304 Remove {
306 name: String,
308 },
309 Test {
311 name: Option<String>,
313 #[arg(long)]
315 verbose: bool,
316 },
317}
318
319#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
321pub enum WorkflowCommands {
322 List,
324 #[command(long_about = "\
326Copy the workflow YAMLs bundled with this binary into the active \
327workspace at `operator_mcp/workflow/builtins/`. Existing files are left \
328alone unless --force is passed.")]
329 Sync {
330 #[arg(long)]
332 force: bool,
333 },
334}
335
336#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
338pub enum MigrateCommands {
339 Openclaw {
341 #[arg(long)]
343 source: Option<std::path::PathBuf>,
344
345 #[arg(long)]
347 dry_run: bool,
348 },
349}
350
351#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
353pub enum CronCommands {
354 List,
356 #[command(long_about = "\
358Add a new recurring scheduled task.
359
360Uses standard 5-field cron syntax: 'min hour day month weekday'. \
361Times are evaluated in UTC by default; use --tz with an IANA \
362timezone name to override.
363
364Examples:
365 construct cron add '0 9 * * 1-5' 'Good morning' --tz America/New_York --agent
366 construct cron add '*/30 * * * *' 'Check system health' --agent
367 construct cron add '*/5 * * * *' 'echo ok'")]
368 Add {
369 expression: String,
371 #[arg(long)]
373 tz: Option<String>,
374 #[arg(long)]
376 agent: bool,
377 #[arg(long = "allowed-tool")]
379 allowed_tools: Vec<String>,
380 command: String,
382 },
383 #[command(long_about = "\
385Add a one-shot task that fires at a specific UTC timestamp.
386
387The timestamp must be in RFC 3339 format (e.g. 2025-01-15T14:00:00Z).
388
389Examples:
390 construct cron add-at 2025-01-15T14:00:00Z 'Send reminder'
391 construct cron add-at 2025-12-31T23:59:00Z 'Happy New Year!'")]
392 AddAt {
393 at: String,
395 #[arg(long)]
397 agent: bool,
398 #[arg(long = "allowed-tool")]
400 allowed_tools: Vec<String>,
401 command: String,
403 },
404 #[command(long_about = "\
406Add a task that repeats at a fixed interval.
407
408Interval is specified in milliseconds. For example, 60000 = 1 minute.
409
410Examples:
411 construct cron add-every 60000 'Ping heartbeat' # every minute
412 construct cron add-every 3600000 'Hourly report' # every hour")]
413 AddEvery {
414 every_ms: u64,
416 #[arg(long)]
418 agent: bool,
419 #[arg(long = "allowed-tool")]
421 allowed_tools: Vec<String>,
422 command: String,
424 },
425 #[command(long_about = "\
427Add a one-shot task that fires after a delay from now.
428
429Accepts human-readable durations: s (seconds), m (minutes), \
430h (hours), d (days).
431
432Examples:
433 construct cron once 30m 'Run backup in 30 minutes'
434 construct cron once 2h 'Follow up on deployment'
435 construct cron once 1d 'Daily check'")]
436 Once {
437 delay: String,
439 #[arg(long)]
441 agent: bool,
442 #[arg(long = "allowed-tool")]
444 allowed_tools: Vec<String>,
445 command: String,
447 },
448 Remove {
450 id: String,
452 },
453 #[command(long_about = "\
455Update one or more fields of an existing scheduled task.
456
457Only the fields you specify are changed; others remain unchanged.
458
459Examples:
460 construct cron update <task-id> --expression '0 8 * * *'
461 construct cron update <task-id> --tz Europe/London --name 'Morning check'
462 construct cron update <task-id> --command 'Updated message'")]
463 Update {
464 id: String,
466 #[arg(long)]
468 expression: Option<String>,
469 #[arg(long)]
471 tz: Option<String>,
472 #[arg(long)]
474 command: Option<String>,
475 #[arg(long)]
477 name: Option<String>,
478 #[arg(long = "allowed-tool")]
480 allowed_tools: Vec<String>,
481 },
482 Pause {
484 id: String,
486 },
487 Resume {
489 id: String,
491 },
492}
493
494#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
496pub enum MemoryCommands {
497 List {
499 #[arg(long)]
501 category: Option<String>,
502 #[arg(long)]
504 session: Option<String>,
505 #[arg(long, default_value = "50")]
507 limit: usize,
508 #[arg(long, default_value = "0")]
510 offset: usize,
511 },
512 Get {
514 key: String,
516 },
517 Stats,
519 Clear {
521 #[arg(long)]
523 key: Option<String>,
524 #[arg(long)]
526 category: Option<String>,
527 #[arg(long)]
529 yes: bool,
530 },
531}
532
533#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
535pub enum IntegrationCommands {
536 Info {
538 name: String,
540 },
541}
542
543#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
545pub enum HardwareCommands {
546 #[command(long_about = "\
548Enumerate USB devices and show known boards.
549
550Scans connected USB devices by VID/PID and matches them against \
551known development boards (STM32 Nucleo, Arduino, ESP32).
552
553Examples:
554 construct hardware discover")]
555 Discover,
556 #[command(long_about = "\
558Introspect a device by its serial or device path.
559
560Opens the specified device path and queries for board information, \
561firmware version, and supported capabilities.
562
563Examples:
564 construct hardware introspect /dev/ttyACM0
565 construct hardware introspect COM3")]
566 Introspect {
567 path: String,
569 },
570 #[command(long_about = "\
572Get chip info via USB using probe-rs over ST-Link.
573
574Queries the target MCU directly through the debug probe without \
575requiring any firmware on the target board.
576
577Examples:
578 construct hardware info
579 construct hardware info --chip STM32F401RETx")]
580 Info {
581 #[arg(long, default_value = "STM32F401RETx")]
583 chip: String,
584 },
585}
586
587#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
589pub enum PeripheralCommands {
590 List,
592 #[command(long_about = "\
594Add a peripheral by board type and transport path.
595
596Registers a hardware board so the agent can use its tools (GPIO, \
597sensors, actuators). Use 'native' as path for local GPIO on \
598single-board computers like Raspberry Pi.
599
600Supported boards: nucleo-f401re, rpi-gpio, esp32, arduino-uno.
601
602Examples:
603 construct peripheral add nucleo-f401re /dev/ttyACM0
604 construct peripheral add rpi-gpio native
605 construct peripheral add esp32 /dev/ttyUSB0")]
606 Add {
607 board: String,
609 path: String,
611 },
612 #[command(long_about = "\
614Flash Construct firmware to an Arduino board.
615
616Generates the .ino sketch, installs arduino-cli if it is not \
617already available, compiles, and uploads the firmware.
618
619Examples:
620 construct peripheral flash
621 construct peripheral flash --port /dev/cu.usbmodem12345
622 construct peripheral flash -p COM3")]
623 Flash {
624 #[arg(short, long)]
626 port: Option<String>,
627 },
628 SetupUnoQ {
630 #[arg(long)]
632 host: Option<String>,
633 },
634 FlashNucleo,
636}
637
638#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
640pub enum SopCommands {
641 List,
643 Validate {
645 name: Option<String>,
647 },
648 Show {
650 name: String,
652 },
653}