Skip to main content

construct/
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    dead_code
36)]
37
38use clap::Subcommand;
39use serde::{Deserialize, Serialize};
40
41pub mod agent;
42pub(crate) mod approval;
43pub(crate) mod auth;
44pub mod channels;
45pub(crate) mod cli_input;
46pub mod commands;
47pub mod config;
48pub(crate) mod cost;
49pub mod cron;
50pub(crate) mod daemon;
51pub(crate) mod doctor;
52pub mod gateway;
53pub(crate) mod hardware;
54pub(crate) mod health;
55pub(crate) mod heartbeat;
56pub mod hooks;
57pub mod i18n;
58pub(crate) mod identity;
59pub(crate) mod integrations;
60pub mod mcp_server;
61pub mod memory;
62pub(crate) mod migration;
63pub(crate) mod multimodal;
64pub mod nodes;
65pub mod observability;
66pub(crate) mod onboard;
67pub mod peripherals;
68pub mod providers;
69pub mod rag;
70pub mod runtime;
71pub(crate) mod security;
72pub(crate) mod service;
73pub(crate) mod skills;
74pub mod sop;
75pub mod tools;
76pub(crate) mod trust;
77pub(crate) mod tunnel;
78pub(crate) mod util;
79pub mod verifiable_intent;
80
81#[cfg(feature = "plugins-wasm")]
82pub mod plugins;
83
84pub use config::Config;
85
86/// Gateway management subcommands
87#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
88pub enum GatewayCommands {
89    /// Start the gateway server (default if no subcommand specified)
90    #[command(long_about = "\
91Start the gateway server (webhooks, websockets).
92
93Runs the HTTP/WebSocket gateway that accepts incoming webhook events \
94and WebSocket connections. Bind address defaults to the values in \
95your config file (gateway.host / gateway.port).
96
97Examples:
98  construct gateway start              # use config defaults
99  construct gateway start -p 8080      # listen on port 8080
100  construct gateway start --host 0.0.0.0   # requires [gateway].allow_public_bind=true or a tunnel
101  construct gateway start -p 0         # random available port")]
102    Start {
103        /// Port to listen on (use 0 for random available port); defaults to config gateway.port
104        #[arg(short, long)]
105        port: Option<u16>,
106
107        /// Host to bind to; defaults to config gateway.host
108        /// Note: Binding to 0.0.0.0 requires `gateway.allow_public_bind = true` in config
109        #[arg(long)]
110        host: Option<String>,
111    },
112    /// Restart the gateway server
113    #[command(long_about = "\
114Restart the gateway server.
115
116Stops the running gateway if present, then starts a new instance \
117with the current configuration.
118
119Examples:
120  construct gateway restart            # restart with config defaults
121  construct gateway restart -p 8080    # restart on port 8080")]
122    Restart {
123        /// Port to listen on (use 0 for random available port); defaults to config gateway.port
124        #[arg(short, long)]
125        port: Option<u16>,
126
127        /// Host to bind to; defaults to config gateway.host
128        /// Note: Binding to 0.0.0.0 requires `gateway.allow_public_bind = true` in config
129        #[arg(long)]
130        host: Option<String>,
131    },
132    /// Show or generate the pairing code without restarting
133    #[command(long_about = "\
134Show or generate the gateway pairing code.
135
136Displays the pairing code for connecting new clients without \
137restarting the gateway. Requires the gateway to be running.
138
139With --new, generates a fresh pairing code even if the gateway \
140was previously paired (useful for adding additional clients).
141
142Examples:
143  construct gateway get-paircode       # show current pairing code
144  construct gateway get-paircode --new # generate a new pairing code")]
145    GetPaircode {
146        /// Generate a new pairing code (even if already paired)
147        #[arg(long)]
148        new: bool,
149    },
150}
151
152/// Service management subcommands
153#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
154pub enum ServiceCommands {
155    /// Install daemon service unit for auto-start and restart
156    Install,
157    /// Start daemon service
158    Start,
159    /// Stop daemon service
160    Stop,
161    /// Restart daemon service to apply latest config
162    Restart,
163    /// Check daemon service status
164    Status,
165    /// Uninstall daemon service unit
166    Uninstall,
167    /// Tail daemon service logs
168    Logs {
169        /// Number of lines to show (default: 50)
170        #[arg(short = 'n', long, default_value = "50")]
171        lines: usize,
172        /// Follow log output (like tail -f)
173        #[arg(short, long)]
174        follow: bool,
175    },
176}
177
178/// Channel management subcommands
179#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
180pub enum ChannelCommands {
181    /// List all configured channels
182    List,
183    /// Start all configured channels (handled in main.rs for async)
184    Start,
185    /// Run health checks for configured channels (handled in main.rs for async)
186    Doctor,
187    /// Add a new channel configuration
188    #[command(long_about = "\
189Add a new channel configuration.
190
191Provide the channel type and a JSON object with the required \
192configuration keys for that channel type.
193
194Supported types: telegram, discord, slack, whatsapp, matrix, imessage, email.
195
196Examples:
197  construct channel add telegram '{\"bot_token\":\"...\",\"name\":\"my-bot\"}'
198  construct channel add discord '{\"bot_token\":\"...\",\"name\":\"my-discord\"}'")]
199    Add {
200        /// Channel type (telegram, discord, slack, whatsapp, matrix, imessage, email)
201        channel_type: String,
202        /// Optional configuration as JSON
203        config: String,
204    },
205    /// Remove a channel configuration
206    Remove {
207        /// Channel name to remove
208        name: String,
209    },
210    /// Bind a Telegram identity (username or numeric user ID) into allowlist
211    #[command(long_about = "\
212Bind a Telegram identity into the allowlist.
213
214Adds a Telegram username (without the '@' prefix) or numeric user \
215ID to the channel allowlist so the agent will respond to messages \
216from that identity.
217
218Examples:
219  construct channel bind-telegram construct_user
220  construct channel bind-telegram 123456789")]
221    BindTelegram {
222        /// Telegram identity to allow (username without '@' or numeric user ID)
223        identity: String,
224    },
225    /// Send a message to a configured channel
226    #[command(long_about = "\
227Send a one-off message to a configured channel.
228
229Sends a text message through the specified channel without starting \
230the full agent loop. Useful for scripted notifications, hardware \
231sensor alerts, and automation pipelines.
232
233The --channel-id selects the channel by its config section name \
234(e.g. 'telegram', 'discord', 'slack'). The --recipient is the \
235platform-specific destination (e.g. a Telegram chat ID).
236
237Examples:
238  construct channel send 'Someone is near your device.' --channel-id telegram --recipient 123456789
239  construct channel send 'Build succeeded!' --channel-id discord --recipient 987654321")]
240    Send {
241        /// Message text to send
242        message: String,
243        /// Channel config name (e.g. telegram, discord, slack)
244        #[arg(long)]
245        channel_id: String,
246        /// Recipient identifier (platform-specific, e.g. Telegram chat ID)
247        #[arg(long)]
248        recipient: String,
249    },
250}
251
252/// Skills management subcommands
253#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
254pub enum SkillCommands {
255    /// List all installed skills
256    List,
257    /// Audit a skill source directory or installed skill name
258    Audit {
259        /// Skill path or installed skill name
260        source: String,
261    },
262    /// Install a new skill from a URL or local path
263    Install {
264        /// Source URL or local path
265        source: String,
266    },
267    /// Remove an installed skill
268    Remove {
269        /// Skill name to remove
270        name: String,
271    },
272    /// Run TEST.sh validation for a skill (or all skills)
273    Test {
274        /// Skill name to test; omit for all skills
275        name: Option<String>,
276        /// Show verbose output
277        #[arg(long)]
278        verbose: bool,
279    },
280}
281
282/// Workflow management subcommands
283#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
284pub enum WorkflowCommands {
285    /// List built-in workflows bundled with this binary.
286    List,
287    /// Seed (or refresh) built-in workflows into the workspace.
288    #[command(long_about = "\
289Copy the workflow YAMLs bundled with this binary into the active \
290workspace at `operator_mcp/workflow/builtins/`. Existing files are left \
291alone unless --force is passed.")]
292    Sync {
293        /// Overwrite workflow files even when they already exist on disk.
294        #[arg(long)]
295        force: bool,
296    },
297}
298
299/// Migration subcommands
300#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
301pub enum MigrateCommands {
302    /// Import memory from an `OpenClaw` workspace into this `Construct` workspace
303    Openclaw {
304        /// Optional path to `OpenClaw` workspace (defaults to ~/.openclaw/workspace)
305        #[arg(long)]
306        source: Option<std::path::PathBuf>,
307
308        /// Validate and preview migration without writing any data
309        #[arg(long)]
310        dry_run: bool,
311    },
312}
313
314/// Cron subcommands
315#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
316pub enum CronCommands {
317    /// List all scheduled tasks
318    List,
319    /// Add a new scheduled task
320    #[command(long_about = "\
321Add a new recurring scheduled task.
322
323Uses standard 5-field cron syntax: 'min hour day month weekday'. \
324Times are evaluated in UTC by default; use --tz with an IANA \
325timezone name to override.
326
327Examples:
328  construct cron add '0 9 * * 1-5' 'Good morning' --tz America/New_York --agent
329  construct cron add '*/30 * * * *' 'Check system health' --agent
330  construct cron add '*/5 * * * *' 'echo ok'")]
331    Add {
332        /// Cron expression
333        expression: String,
334        /// Optional IANA timezone (e.g. America/Los_Angeles)
335        #[arg(long)]
336        tz: Option<String>,
337        /// Treat the argument as an agent prompt instead of a shell command
338        #[arg(long)]
339        agent: bool,
340        /// Restrict agent cron jobs to the specified tool names (repeatable, agent-only)
341        #[arg(long = "allowed-tool")]
342        allowed_tools: Vec<String>,
343        /// Command (shell) or prompt (agent) to run
344        command: String,
345    },
346    /// Add a one-shot scheduled task at an RFC3339 timestamp
347    #[command(long_about = "\
348Add a one-shot task that fires at a specific UTC timestamp.
349
350The timestamp must be in RFC 3339 format (e.g. 2025-01-15T14:00:00Z).
351
352Examples:
353  construct cron add-at 2025-01-15T14:00:00Z 'Send reminder'
354  construct cron add-at 2025-12-31T23:59:00Z 'Happy New Year!'")]
355    AddAt {
356        /// One-shot timestamp in RFC3339 format
357        at: String,
358        /// Treat the argument as an agent prompt instead of a shell command
359        #[arg(long)]
360        agent: bool,
361        /// Restrict agent cron jobs to the specified tool names (repeatable, agent-only)
362        #[arg(long = "allowed-tool")]
363        allowed_tools: Vec<String>,
364        /// Command (shell) or prompt (agent) to run
365        command: String,
366    },
367    /// Add a fixed-interval scheduled task
368    #[command(long_about = "\
369Add a task that repeats at a fixed interval.
370
371Interval is specified in milliseconds. For example, 60000 = 1 minute.
372
373Examples:
374  construct cron add-every 60000 'Ping heartbeat'     # every minute
375  construct cron add-every 3600000 'Hourly report'    # every hour")]
376    AddEvery {
377        /// Interval in milliseconds
378        every_ms: u64,
379        /// Treat the argument as an agent prompt instead of a shell command
380        #[arg(long)]
381        agent: bool,
382        /// Restrict agent cron jobs to the specified tool names (repeatable, agent-only)
383        #[arg(long = "allowed-tool")]
384        allowed_tools: Vec<String>,
385        /// Command (shell) or prompt (agent) to run
386        command: String,
387    },
388    /// Add a one-shot delayed task (e.g. "30m", "2h", "1d")
389    #[command(long_about = "\
390Add a one-shot task that fires after a delay from now.
391
392Accepts human-readable durations: s (seconds), m (minutes), \
393h (hours), d (days).
394
395Examples:
396  construct cron once 30m 'Run backup in 30 minutes'
397  construct cron once 2h 'Follow up on deployment'
398  construct cron once 1d 'Daily check'")]
399    Once {
400        /// Delay duration
401        delay: String,
402        /// Treat the argument as an agent prompt instead of a shell command
403        #[arg(long)]
404        agent: bool,
405        /// Restrict agent cron jobs to the specified tool names (repeatable, agent-only)
406        #[arg(long = "allowed-tool")]
407        allowed_tools: Vec<String>,
408        /// Command (shell) or prompt (agent) to run
409        command: String,
410    },
411    /// Remove a scheduled task
412    Remove {
413        /// Task ID
414        id: String,
415    },
416    /// Update a scheduled task
417    #[command(long_about = "\
418Update one or more fields of an existing scheduled task.
419
420Only the fields you specify are changed; others remain unchanged.
421
422Examples:
423  construct cron update <task-id> --expression '0 8 * * *'
424  construct cron update <task-id> --tz Europe/London --name 'Morning check'
425  construct cron update <task-id> --command 'Updated message'")]
426    Update {
427        /// Task ID
428        id: String,
429        /// New cron expression
430        #[arg(long)]
431        expression: Option<String>,
432        /// New IANA timezone
433        #[arg(long)]
434        tz: Option<String>,
435        /// New command to run
436        #[arg(long)]
437        command: Option<String>,
438        /// New job name
439        #[arg(long)]
440        name: Option<String>,
441        /// Replace the agent job allowlist with the specified tool names (repeatable)
442        #[arg(long = "allowed-tool")]
443        allowed_tools: Vec<String>,
444    },
445    /// Pause a scheduled task
446    Pause {
447        /// Task ID
448        id: String,
449    },
450    /// Resume a paused task
451    Resume {
452        /// Task ID
453        id: String,
454    },
455}
456
457/// Memory management subcommands
458#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
459pub enum MemoryCommands {
460    /// List memory entries with optional filters
461    List {
462        /// Filter by category (core, daily, conversation, or custom name)
463        #[arg(long)]
464        category: Option<String>,
465        /// Filter by session ID
466        #[arg(long)]
467        session: Option<String>,
468        /// Maximum number of entries to display
469        #[arg(long, default_value = "50")]
470        limit: usize,
471        /// Number of entries to skip (for pagination)
472        #[arg(long, default_value = "0")]
473        offset: usize,
474    },
475    /// Get a specific memory entry by key
476    Get {
477        /// Memory key to look up
478        key: String,
479    },
480    /// Show memory backend statistics and health
481    Stats,
482    /// Clear memories by category, by key, or clear all
483    Clear {
484        /// Delete a single entry by key (supports prefix match)
485        #[arg(long)]
486        key: Option<String>,
487        /// Only clear entries in this category
488        #[arg(long)]
489        category: Option<String>,
490        /// Skip confirmation prompt
491        #[arg(long)]
492        yes: bool,
493    },
494}
495
496/// Integration subcommands
497#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
498pub enum IntegrationCommands {
499    /// Show details about a specific integration
500    Info {
501        /// Integration name
502        name: String,
503    },
504}
505
506/// Hardware discovery subcommands
507#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
508pub enum HardwareCommands {
509    /// Enumerate USB devices (VID/PID) and show known boards
510    #[command(long_about = "\
511Enumerate USB devices and show known boards.
512
513Scans connected USB devices by VID/PID and matches them against \
514known development boards (STM32 Nucleo, Arduino, ESP32).
515
516Examples:
517  construct hardware discover")]
518    Discover,
519    /// Introspect a device by path (e.g. /dev/ttyACM0)
520    #[command(long_about = "\
521Introspect a device by its serial or device path.
522
523Opens the specified device path and queries for board information, \
524firmware version, and supported capabilities.
525
526Examples:
527  construct hardware introspect /dev/ttyACM0
528  construct hardware introspect COM3")]
529    Introspect {
530        /// Serial or device path
531        path: String,
532    },
533    /// Get chip info via USB (probe-rs over ST-Link). No firmware needed on target.
534    #[command(long_about = "\
535Get chip info via USB using probe-rs over ST-Link.
536
537Queries the target MCU directly through the debug probe without \
538requiring any firmware on the target board.
539
540Examples:
541  construct hardware info
542  construct hardware info --chip STM32F401RETx")]
543    Info {
544        /// Chip name (e.g. STM32F401RETx). Default: STM32F401RETx for Nucleo-F401RE
545        #[arg(long, default_value = "STM32F401RETx")]
546        chip: String,
547    },
548}
549
550/// Peripheral (hardware) management subcommands
551#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
552pub enum PeripheralCommands {
553    /// List configured peripherals
554    List,
555    /// Add a peripheral (board path, e.g. nucleo-f401re /dev/ttyACM0)
556    #[command(long_about = "\
557Add a peripheral by board type and transport path.
558
559Registers a hardware board so the agent can use its tools (GPIO, \
560sensors, actuators). Use 'native' as path for local GPIO on \
561single-board computers like Raspberry Pi.
562
563Supported boards: nucleo-f401re, rpi-gpio, esp32, arduino-uno.
564
565Examples:
566  construct peripheral add nucleo-f401re /dev/ttyACM0
567  construct peripheral add rpi-gpio native
568  construct peripheral add esp32 /dev/ttyUSB0")]
569    Add {
570        /// Board type (nucleo-f401re, rpi-gpio, esp32)
571        board: String,
572        /// Path for serial transport (/dev/ttyACM0) or "native" for local GPIO
573        path: String,
574    },
575    /// Flash Construct firmware to Arduino (creates .ino, installs arduino-cli if needed, uploads)
576    #[command(long_about = "\
577Flash Construct firmware to an Arduino board.
578
579Generates the .ino sketch, installs arduino-cli if it is not \
580already available, compiles, and uploads the firmware.
581
582Examples:
583  construct peripheral flash
584  construct peripheral flash --port /dev/cu.usbmodem12345
585  construct peripheral flash -p COM3")]
586    Flash {
587        /// Serial port (e.g. /dev/cu.usbmodem12345). If omitted, uses first arduino-uno from config.
588        #[arg(short, long)]
589        port: Option<String>,
590    },
591    /// Setup Arduino Uno Q Bridge app (deploy GPIO bridge for agent control)
592    SetupUnoQ {
593        /// Uno Q IP (e.g. 192.168.0.48). If omitted, assumes running ON the Uno Q.
594        #[arg(long)]
595        host: Option<String>,
596    },
597    /// Flash Construct firmware to Nucleo-F401RE (builds + probe-rs run)
598    FlashNucleo,
599}
600
601/// SOP management subcommands
602#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
603pub enum SopCommands {
604    /// List loaded SOPs
605    List,
606    /// Validate SOP definitions
607    Validate {
608        /// SOP name to validate (all if omitted)
609        name: Option<String>,
610    },
611    /// Show details of an SOP
612    Show {
613        /// Name of the SOP to show
614        name: String,
615    },
616}