ironclaw 0.24.0

Secure personal AI assistant that protects your data and expands its capabilities on the fly
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
//! CLI command handling.
//!
//! Provides subcommands for:
//! - Running the agent (`run`)
//! - Interactive onboarding wizard (`onboard`)
//! - Managing configuration (`config list`, `config get`, `config set`)
//! - Managing WASM tools (`tool install`, `tool list`, `tool remove`)
//! - Managing MCP servers (`mcp add`, `mcp auth`, `mcp list`, `mcp test`)
//! - Querying workspace memory (`memory search`, `memory read`, `memory write`)
//! - Managing routines (`routines list`, `routines create`, `routines edit`, ...)
//! - Managing OS service (`service install`, `service start`, `service stop`)
//! - Listing configured channels (`channels list`)
//! - Active health diagnostics (`doctor`)
//! - Viewing gateway logs (`logs`)
//! - Checking system health (`status`)

mod channels;
mod completion;
mod config;
mod doctor;
pub mod fmt;
mod hooks;
#[cfg(feature = "import")]
pub mod import;
mod logs;
mod mcp;
pub mod memory;
mod models;
pub mod oauth_defaults;
mod pairing;
mod registry;
mod routines;
mod service;
mod skills;
pub mod status;
mod tool;

pub use channels::{ChannelsCommand, run_channels_command};
pub use completion::Completion;
pub use config::{ConfigCommand, run_config_command};
pub use doctor::run_doctor_command;
pub use hooks::{HooksCommand, run_hooks_command};
#[cfg(feature = "import")]
pub use import::{ImportCommand, run_import_command};
pub use logs::{LogsCommand, run_logs_command};
pub use mcp::{McpCommand, run_mcp_command};
pub use memory::MemoryCommand;
pub use memory::run_memory_command_with_db;
pub use models::{ModelsCommand, run_models_command};
pub use pairing::{PairingCommand, run_pairing_command, run_pairing_command_with_store};
pub use registry::{RegistryCommand, run_registry_command};
pub use routines::{RoutinesCommand, run_routines_command};
pub use service::{ServiceCommand, run_service_command};
pub use skills::{SkillsCommand, run_skills_command};
pub use status::run_status_command;
pub use tool::{ToolCommand, run_tool_command};

use std::sync::Arc;

use clap::{ColorChoice, Parser, Subcommand};

#[derive(Parser, Debug)]
#[command(name = "ironclaw")]
#[command(
    about = "Secure personal AI assistant that protects your data and expands its capabilities"
)]
#[command(
    long_about = "IronClaw is a secure AI assistant. Use 'ironclaw <subcommand> --help' for details.\nExamples:\n  ironclaw run  # Start the agent\n  ironclaw config list  # List configs"
)]
#[command(version)]
#[command(color = ColorChoice::Auto)] // Enable auto-color for help (if the terminal supports it)
pub struct Cli {
    #[command(subcommand)]
    pub command: Option<Command>,

    /// Run in interactive CLI mode only (disable other channels)
    #[arg(long, global = true)]
    pub cli_only: bool,

    /// Skip database connection (for testing)
    #[arg(long, global = true)]
    pub no_db: bool,

    /// Single message mode - send one message and exit
    #[arg(short, long, global = true)]
    pub message: Option<String>,

    /// Configuration file path (optional, uses env vars by default)
    #[arg(short, long, global = true)]
    pub config: Option<std::path::PathBuf>,

    /// Skip first-run onboarding check
    #[arg(long, global = true)]
    pub no_onboard: bool,
}

#[derive(Subcommand, Debug)]
pub enum Command {
    /// Run the agent (default if no subcommand given)
    #[command(
        about = "Run the AI agent",
        long_about = "Starts the IronClaw agent in default mode.\nExample: ironclaw run"
    )]
    Run,

    /// Interactive onboarding wizard
    #[command(
        about = "Run interactive setup wizard",
        long_about = "Guides through initial configuration.\nExamples:\n  ironclaw onboard --skip-auth  # Skip auth step\n  ironclaw onboard --channels-only  # Reconfigure channels\n  ironclaw onboard --provider-only  # Change LLM provider and model"
    )]
    Onboard {
        /// Skip authentication (use existing session)
        #[arg(long)]
        skip_auth: bool,

        /// Reconfigure channels only
        #[arg(long, conflicts_with_all = ["provider_only", "quick", "step"], help = "Deprecated: use --step channels")]
        channels_only: bool,

        /// Reconfigure LLM provider and model only
        #[arg(long, conflicts_with_all = ["channels_only", "quick", "step"], help = "Deprecated: use --step provider")]
        provider_only: bool,

        /// Quick setup: auto-defaults everything except LLM provider and model
        #[arg(long, conflicts_with_all = ["channels_only", "provider_only", "step"])]
        quick: bool,

        /// Run only specific setup steps (comma-separated: provider, channels, model, database, security)
        #[arg(long, value_delimiter = ',', conflicts_with_all = ["channels_only", "provider_only", "quick"])]
        step: Vec<String>,
    },

    /// Manage configuration settings
    #[command(
        subcommand,
        about = "Manage app configs",
        long_about = "Commands for listing, getting, and setting configurations.\nExample: ironclaw config list"
    )]
    Config(ConfigCommand),

    /// Manage WASM tools
    #[command(
        subcommand,
        about = "Manage WASM tools",
        long_about = "Install, list, or remove WASM-based tools.\nExample: ironclaw tool install mytool.wasm"
    )]
    Tool(ToolCommand),

    /// Browse and install extensions from the registry
    #[command(
        subcommand,
        about = "Browse/install extensions",
        long_about = "Interact with extension registry.\nExample: ironclaw registry list"
    )]
    Registry(RegistryCommand),

    /// List and inspect messaging channels
    #[command(
        subcommand,
        about = "Manage channels",
        long_about = "List configured messaging channels.\nExamples:\n  ironclaw channels list\n  ironclaw channels list --verbose\n  ironclaw channels list --json"
    )]
    Channels(ChannelsCommand),

    /// Manage routines (scheduled, event-driven, webhook, manual)
    #[command(
        subcommand,
        alias = "cron",
        about = "Manage routines",
        long_about = "List, create, edit, enable/disable, delete, and view history of routines.\nExamples:\n  ironclaw routines list\n  ironclaw routines create --name daily-digest --schedule '0 0 9 * * *' --prompt 'Summarize today'"
    )]
    Routines(RoutinesCommand),

    /// Manage MCP servers (hosted tool providers)
    #[command(
        subcommand,
        about = "Manage MCP servers",
        long_about = "Add, auth, list, or test MCP servers.\nExample: ironclaw mcp add notion https://mcp.notion.com"
    )]
    Mcp(Box<McpCommand>),

    /// Query and manage workspace memory
    #[command(
        subcommand,
        about = "Manage workspace memory",
        long_about = "Search, read, or write to memory.\nExample: ironclaw memory search 'query'"
    )]
    Memory(MemoryCommand),

    /// DM pairing (approve inbound requests from unknown senders)
    #[command(
        subcommand,
        about = "Manage DM pairing",
        long_about = "Approve or manage pairing requests.\nExamples:\n  ironclaw pairing list telegram\n  ironclaw pairing approve telegram ABC12345"
    )]
    Pairing(PairingCommand),

    /// Manage OS service (launchd / systemd)
    #[command(
        subcommand,
        about = "Manage OS service",
        long_about = "Install, start, or stop service.\nExample: ironclaw service install"
    )]
    Service(ServiceCommand),

    /// Manage SKILL.md-based skills
    #[command(
        subcommand,
        about = "Manage skills",
        long_about = "List, search, and inspect SKILL.md-based skills.\nExamples:\n  ironclaw skills list\n  ironclaw skills search 'writing'\n  ironclaw skills info my-skill"
    )]
    Skills(SkillsCommand),

    /// Manage lifecycle hooks
    #[command(
        subcommand,
        about = "Manage lifecycle hooks",
        long_about = "List and inspect lifecycle hooks (bundled, plugin, workspace).\nExamples:\n  ironclaw hooks list\n  ironclaw hooks list --verbose\n  ironclaw hooks list --json"
    )]
    Hooks(HooksCommand),

    /// Manage LLM providers and models
    #[command(
        subcommand,
        about = "Manage LLM providers and models",
        long_about = "List providers, view current configuration, and set active provider/model.\nExamples:\n  ironclaw models list\n  ironclaw models list openai --verbose\n  ironclaw models status\n  ironclaw models set gpt-4o\n  ironclaw models set-provider anthropic --model claude-sonnet-4-6-20250514"
    )]
    Models(ModelsCommand),

    /// Probe external dependencies and validate configuration
    #[command(
        about = "Run diagnostics",
        long_about = "Checks dependencies and config validity.\nExample: ironclaw doctor"
    )]
    Doctor,

    /// View and manage gateway logs
    #[command(
        about = "View and manage gateway logs",
        long_about = "Tail gateway logs, stream live output, or adjust log level.\nExamples:\n  ironclaw logs                 # Show last 200 lines from gateway.log\n  ironclaw logs --follow        # Stream live logs via SSE\n  ironclaw logs --level         # Show current log level\n  ironclaw logs --level debug   # Set log level to debug"
    )]
    Logs(LogsCommand),

    /// Show system health and diagnostics
    #[command(
        about = "Show system status",
        long_about = "Displays health and diagnostics info.\nExample: ironclaw status"
    )]
    Status,

    /// Generate shell completion scripts
    #[command(
        about = "Generate completions",
        long_about = "Generates shell completion scripts.\nExample: ironclaw completion --shell bash > ironclaw.bash"
    )]
    Completion(Completion),

    /// Import data from other AI systems
    #[cfg(feature = "import")]
    #[command(
        subcommand,
        about = "Import from other AI systems",
        long_about = "Migrate data from other AI assistants like OpenClaw.\nExample: ironclaw import openclaw"
    )]
    Import(ImportCommand),

    /// Authenticate with a provider (re-login)
    #[command(
        about = "Authenticate with a provider",
        long_about = "Re-authenticate with an LLM provider.\nExample: ironclaw login --openai-codex"
    )]
    Login {
        /// Authenticate with OpenAI Codex (ChatGPT subscription)
        #[arg(long)]
        openai_codex: bool,
    },

    /// Run as a sandboxed worker inside a Docker container (internal use).
    /// This is invoked automatically by the orchestrator, not by users directly.
    #[command(hide = true)]
    Worker {
        /// Job ID to execute.
        #[arg(long)]
        job_id: uuid::Uuid,

        /// URL of the orchestrator's internal API.
        #[arg(long, default_value = "http://host.docker.internal:50051")]
        orchestrator_url: String,

        /// Maximum iterations before stopping.
        #[arg(long, default_value = "50")]
        max_iterations: u32,
    },

    /// Run as a Claude Code bridge inside a Docker container (internal use).
    /// Spawns the `claude` CLI and streams output back to the orchestrator.
    #[command(hide = true)]
    ClaudeBridge {
        /// Job ID to execute.
        #[arg(long)]
        job_id: uuid::Uuid,

        /// URL of the orchestrator's internal API.
        #[arg(long, default_value = "http://host.docker.internal:50051")]
        orchestrator_url: String,

        /// Maximum agentic turns for Claude Code.
        #[arg(long, default_value = "50")]
        max_turns: u32,

        /// Claude model to use (e.g. "sonnet", "opus").
        #[arg(long, default_value = "sonnet")]
        model: String,
    },
}

impl Cli {
    /// Check if we should run the agent (default behavior or explicit `run` command).
    pub fn should_run_agent(&self) -> bool {
        matches!(self.command, None | Some(Command::Run))
    }
}

/// Initialize a secrets store from environment config.
///
/// Shared helper for CLI subcommands (`mcp auth`, `tool auth`, etc.) that need
/// access to encrypted secrets without spinning up the full AppBuilder.
pub async fn init_secrets_store()
-> anyhow::Result<Arc<dyn crate::secrets::SecretsStore + Send + Sync>> {
    let config = crate::config::Config::from_env().await?;
    let master_key = config.secrets.master_key().ok_or_else(|| {
        anyhow::anyhow!(
            "SECRETS_MASTER_KEY not set. Run 'ironclaw onboard' first or set it in .env"
        )
    })?;

    let crypto = Arc::new(crate::secrets::SecretsCrypto::new(master_key.clone())?);

    Ok(crate::db::create_secrets_store(&config.database, crypto).await?)
}

/// Run the Routines CLI subcommand.
pub async fn run_routines_cli(
    routines_cmd: &RoutinesCommand,
    config_path: Option<&std::path::Path>,
) -> anyhow::Result<()> {
    let config = crate::config::Config::from_env_with_toml(config_path)
        .await
        .map_err(|e| anyhow::anyhow!("{e:#}"))?;

    let db: Arc<dyn crate::db::Database> = crate::db::connect_from_config(&config.database)
        .await
        .map_err(|e| anyhow::anyhow!("{e:#}"))?;

    let user_id = std::env::var("IRONCLAW_OWNER_ID").unwrap_or_else(|_| "default".to_string());
    run_routines_command(routines_cmd.clone(), db, &user_id).await
}

/// Run the Memory CLI subcommand.
pub async fn run_memory_command(mem_cmd: &MemoryCommand) -> anyhow::Result<()> {
    let config = crate::config::Config::from_env()
        .await
        .map_err(|e| anyhow::anyhow!("{}", e))?;

    let session = crate::llm::create_session_manager(config.llm.session.clone()).await;

    let embeddings = config
        .embeddings
        .create_provider(&config.llm.nearai.base_url, session);

    let db: Arc<dyn crate::db::Database> = crate::db::connect_from_config(&config.database)
        .await
        .map_err(|e| anyhow::anyhow!("{}", e))?;

    let cache_config = crate::workspace::EmbeddingCacheConfig {
        max_entries: config.embeddings.cache_size,
    };
    run_memory_command_with_db(mem_cmd.clone(), db, embeddings, cache_config).await
}

#[cfg(test)]
mod tests {
    use super::*;
    use clap::CommandFactory;
    use insta::assert_snapshot;

    #[test]
    fn test_version() {
        let cmd = Cli::command();
        assert_eq!(
            cmd.get_version().unwrap_or("unknown"),
            env!("CARGO_PKG_VERSION")
        );
    }

    #[test]
    #[cfg(feature = "import")]
    fn test_help_output() {
        let mut cmd = Cli::command();
        let help = cmd.render_help().to_string();
        assert_snapshot!(help);
    }

    #[test]
    #[cfg(not(feature = "import"))]
    fn test_help_output_without_import() {
        let mut cmd = Cli::command();
        let help = cmd.render_help().to_string();
        assert_snapshot!(help);
    }

    #[test]
    #[cfg(feature = "import")]
    fn test_long_help_output() {
        let mut cmd = Cli::command();
        let help = cmd.render_long_help().to_string();
        assert_snapshot!(help);
    }

    #[test]
    #[cfg(not(feature = "import"))]
    fn test_long_help_output_without_import() {
        let mut cmd = Cli::command();
        let help = cmd.render_long_help().to_string();
        assert_snapshot!(help);
    }
}