opencrabs 0.3.45

The autonomous, self-improving AI agent. Single Rust binary. Every channel. Install with: cargo install opencrabs
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
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
//! CLI argument types and entry point.

use anyhow::Result;
use clap::{Parser, Subcommand};

use super::{commands, cron, migrate, ui};
use crate::config::Config;

/// OpenCrabs - High-Performance Terminal AI Orchestration Agent
#[derive(Parser, Debug)]
#[command(name = "opencrabs")]
#[command(version, about, long_about = None)]
pub struct Cli {
    /// Enable debug mode (creates log files in .opencrabs/logs/)
    #[arg(short, long, global = true)]
    pub debug: bool,

    /// Configuration file path
    #[arg(short, long, global = true)]
    pub config: Option<String>,

    /// Profile to use (default: "default", or OPENCRABS_PROFILE env)
    #[arg(short, long, global = true)]
    pub profile: Option<String>,

    /// Subcommand to execute
    #[command(subcommand)]
    pub command: Option<Commands>,
}

#[derive(Subcommand, Debug)]
pub enum Commands {
    /// Start interactive TUI mode (default)
    Chat {
        /// Session ID to resume
        #[arg(short, long)]
        session: Option<String>,

        /// Force onboarding wizard before chat
        #[arg(long)]
        onboard: bool,
    },

    /// Run the onboarding setup wizard
    Onboard,

    /// Run a single command non-interactively
    Run {
        /// The prompt to execute
        prompt: String,

        /// Auto-approve all tool executions (dangerous!)
        #[arg(long, alias = "yolo")]
        auto_approve: bool,

        /// Output format
        #[arg(short, long, default_value = "text")]
        format: OutputFormat,
    },

    /// Show system status: version, provider, channels, database, brain
    Status,

    /// Run diagnostics: check config, provider connectivity, channel health, tools, brain
    Doctor,

    /// Initialize configuration
    Init {
        /// Force overwrite existing configuration
        #[arg(short, long)]
        force: bool,
    },

    /// Show configuration
    Config {
        /// Show full configuration including secrets
        #[arg(short, long)]
        show_secrets: bool,
    },

    /// Database operations
    Db {
        #[command(subcommand)]
        operation: DbCommands,
    },

    /// Log management operations
    Logs {
        #[command(subcommand)]
        operation: LogCommands,
    },

    /// Interactive CLI agent (no TUI) — multi-turn conversation in your terminal
    Agent {
        /// Single message mode (non-interactive)
        #[arg(short, long)]
        message: Option<String>,

        /// Session ID to resume
        #[arg(short, long)]
        session: Option<String>,

        /// Auto-approve all tool executions
        #[arg(long, alias = "yolo")]
        auto_approve: bool,

        /// Output format (only for single-message mode)
        #[arg(short, long, default_value = "text")]
        format: OutputFormat,
    },

    /// Channel operations
    Channel {
        #[command(subcommand)]
        operation: ChannelCommands,
    },

    /// Memory operations
    Memory {
        #[command(subcommand)]
        operation: MemoryCommands,
    },

    /// Session management
    Session {
        #[command(subcommand)]
        operation: SessionCommands,
    },

    /// OS service management (launchd/systemd)
    Service {
        #[command(subcommand)]
        operation: ServiceCommands,
    },

    /// Run in headless daemon mode — no TUI, channel bots only (Telegram, Discord, Slack, WhatsApp)
    /// Used by the systemd/LaunchAgent service installed during onboarding
    Daemon,

    /// Manage profiles — isolated OpenCrabs instances with their own config, DB, and memory
    Profile {
        #[command(subcommand)]
        operation: ProfileCommands,
    },

    /// Manage scheduled cron jobs
    Cron {
        #[command(subcommand)]
        operation: CronCommands,
    },

    /// Generate shell completions
    Completions {
        /// Shell to generate completions for
        #[arg(value_enum)]
        shell: clap_complete::Shell,
    },

    /// Print version and exit
    Version,

    /// Print the database schema migration count (used by evolve for compatibility checks)
    PrintMigrationCount,

    /// Check for and install the latest OpenCrabs release
    Evolve {
        /// Only check for updates without installing
        #[arg(long)]
        check_only: bool,
    },

    /// Migrate config and brain files from another AI agent tool
    ///
    /// Scans the system for instances of the source tool, shows an interactive
    /// picker if multiple are found, then spawns an agent to handle the migration.
    Migrate {
        /// Source tool to migrate from (openclaw, hermes)
        source: MigrationSource,

        /// Preview what would be migrated without making changes
        #[arg(long)]
        dry_run: bool,
    },
}

#[derive(Subcommand, Debug)]
pub enum LogCommands {
    /// Show log file location and status
    Status,
    /// View recent log entries (requires debug mode)
    View {
        /// Number of lines to show (default: 50)
        #[arg(short, long, default_value = "50")]
        lines: usize,
    },
    /// Clean up old log files
    Clean {
        /// Maximum age in days (default: 7)
        #[arg(short = 'a', long, default_value = "7")]
        days: u64,
    },
    /// Open log directory in file manager
    Open,
}

#[derive(Subcommand, Debug)]
pub enum DbCommands {
    /// Initialize database
    Init,
    /// Show database statistics
    Stats,
    /// Clear all sessions and messages from database
    Clear {
        /// Skip confirmation prompt (use with caution)
        #[arg(short, long)]
        force: bool,
    },
}

#[derive(Subcommand, Debug)]
pub enum CronCommands {
    /// Add a new cron job
    Add {
        /// Job name
        #[arg(long)]
        name: String,

        /// Cron expression (5-field: min hour dom mon dow)
        #[arg(long)]
        cron: String,

        /// Timezone (default: UTC)
        #[arg(long, default_value = "UTC")]
        tz: String,

        /// Prompt / instructions for the agent
        #[arg(long, alias = "message")]
        prompt: String,

        /// Override provider (e.g. anthropic, openai)
        #[arg(long)]
        provider: Option<String>,

        /// Override model (e.g. claude-sonnet-4-20250514)
        #[arg(long)]
        model: Option<String>,

        /// Thinking mode: off, on, budget
        #[arg(long, default_value = "off")]
        thinking: String,

        /// Auto-approve tool executions
        #[arg(long, default_value = "true")]
        auto_approve: bool,

        /// Channel to deliver results (e.g. telegram:123456)
        #[arg(long, alias = "deliver")]
        deliver_to: Option<String>,
    },

    /// List all cron jobs
    List,

    /// Remove a cron job by ID or name
    Remove {
        /// Job ID or name
        id: String,
    },

    /// Enable a cron job
    Enable {
        /// Job ID or name
        id: String,
    },

    /// Disable a cron job (pause without deleting)
    Disable {
        /// Job ID or name
        id: String,
    },

    /// Trigger a cron job immediately (runs on next scheduler tick)
    Test {
        /// Job ID or name
        id: String,
    },
}

#[derive(Subcommand, Debug)]
pub enum ChannelCommands {
    /// List configured channels and their status
    List,
    /// Run health checks on all enabled channels
    Doctor,
}

#[derive(Subcommand, Debug)]
pub enum MemoryCommands {
    /// List memory files in the brain directory
    List,
    /// Show a specific memory file
    Get {
        /// Memory file name (e.g. "MEMORY.md" or just "MEMORY")
        name: String,
    },
    /// Show memory statistics
    Stats,
}

#[derive(Subcommand, Debug)]
pub enum SessionCommands {
    /// List all sessions
    List {
        /// Include archived sessions
        #[arg(short, long)]
        all: bool,
    },
    /// Show session details
    Get {
        /// Session ID
        id: String,
    },
}

#[derive(Subcommand, Debug)]
pub enum ProfileCommands {
    /// Create a new profile
    Create {
        /// Profile name (alphanumeric, hyphens, underscores)
        name: String,

        /// Optional description
        #[arg(short, long)]
        description: Option<String>,
    },
    /// List all profiles
    List,
    /// Delete a profile and all its data
    Delete {
        /// Profile name to delete
        name: String,

        /// Skip confirmation prompt
        #[arg(short, long)]
        force: bool,
    },
    /// Export a profile as a tar.gz archive
    Export {
        /// Profile name to export
        name: String,

        /// Output file path (default: <name>.tar.gz)
        #[arg(short, long)]
        output: Option<String>,
    },
    /// Import a profile from a tar.gz archive
    Import {
        /// Path to the archive file
        path: String,
    },
    /// Migrate config and brain files from one profile to another (no DB/sessions)
    Migrate {
        /// Source profile name
        from: String,

        /// Destination profile name
        to: String,

        /// Overwrite existing files in the destination
        #[arg(short, long)]
        force: bool,
    },
}

#[derive(Subcommand, Debug)]
pub enum ServiceCommands {
    /// Install as OS service (launchd on macOS, systemd on Linux)
    Install,
    /// Start the service
    Start,
    /// Stop the service
    Stop,
    /// Restart the service
    Restart,
    /// Show service status
    Status,
    /// Uninstall the service
    Uninstall,
}

#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum OutputFormat {
    Text,
    Json,
    Markdown,
}

/// Source tool for migration
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum MigrationSource {
    /// Migrate from OpenClaw (TypeScript AI agent, ~/.openclaw/)
    Openclaw,
    /// Migrate from Hermes (NousResearch Python AI agent, ~/.hermes/)
    Hermes,
}

/// Main CLI entry point
pub async fn run() -> Result<()> {
    let cli = Cli::parse();

    // Set active profile BEFORE anything touches opencrabs_home()
    crate::config::profile::set_active_profile(cli.profile.clone())
        .unwrap_or_else(|e| tracing::warn!("Profile already set: {}", e));

    // Track profile usage
    if let Some(ref name) = cli.profile
        && let Ok(mut registry) = crate::config::profile::ProfileRegistry::load()
    {
        registry.touch(name);
        let _ = registry.save();
    }

    // Set up logging level based on debug flag
    if cli.debug {
        tracing::info!("Debug mode enabled");
    }

    // Load configuration
    let config = commands::load_config(cli.config.as_deref()).await?;
    // Seed the in-memory mirror so Config::current() readers never touch disk.
    Config::set_current(config.clone());

    // Auto-generate config.toml if API keys exist in env but no config file yet.
    // This prevents the onboarding wizard from triggering when .env is already set up.
    let config_path = Config::system_config_path();
    if let Some(ref path) = config_path
        && !path.exists()
        && config.has_any_api_key()
    {
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).ok();
        }
        if let Err(e) = config.save(path) {
            tracing::warn!("Failed to auto-generate config.toml: {}", e);
        } else {
            tracing::info!("Auto-generated config.toml from environment");
        }
    }

    match cli.command {
        None | Some(Commands::Chat { .. }) => {
            // Default: Interactive TUI mode
            let (session, force_onboard) = match &cli.command {
                Some(Commands::Chat { session, onboard }) => (session.clone(), *onboard),
                _ => (None, false),
            };
            ui::cmd_chat(&config, session, force_onboard).await
        }
        Some(Commands::Onboard) => {
            // Launch TUI with onboarding wizard (skip splash)
            ui::cmd_chat(&config, None, true).await
        }
        Some(Commands::Status) => commands::cmd_status(&config).await,
        Some(Commands::Doctor) => commands::cmd_doctor(&config).await,
        Some(Commands::Init { force }) => commands::cmd_init(&config, force).await,
        Some(Commands::Config { show_secrets }) => {
            commands::cmd_config(&config, show_secrets).await
        }
        Some(Commands::Db { operation }) => commands::cmd_db(&config, operation).await,
        Some(Commands::Logs { operation }) => commands::cmd_logs(operation).await,
        Some(Commands::Run {
            prompt,
            auto_approve,
            format,
        }) => commands::cmd_run(&config, prompt, auto_approve, format).await,
        Some(Commands::Agent {
            message,
            session: _,
            auto_approve,
            format,
        }) => {
            if let Some(msg) = message {
                // Single message mode — same as `run`
                commands::cmd_run(&config, msg, auto_approve, format).await
            } else {
                // Interactive CLI agent (no TUI)
                commands::cmd_agent_interactive(&config, auto_approve).await
            }
        }
        Some(Commands::Channel { operation }) => commands::cmd_channel(&config, operation).await,
        Some(Commands::Memory { operation }) => commands::cmd_memory(operation).await,
        Some(Commands::Session { operation }) => commands::cmd_session(&config, operation).await,
        Some(Commands::Service { operation }) => commands::cmd_service(operation).await,
        Some(Commands::Daemon) => ui::cmd_daemon(&config).await,
        Some(Commands::Profile { operation }) => commands::cmd_profile(operation).await,
        Some(Commands::Cron { operation }) => cron::cmd_cron(&config, operation).await,
        Some(Commands::Completions { shell }) => {
            use clap::CommandFactory;
            clap_complete::generate(
                shell,
                &mut Cli::command(),
                "opencrabs",
                &mut std::io::stdout(),
            );
            Ok(())
        }
        Some(Commands::Version) => {
            println!("opencrabs {}", env!("CARGO_PKG_VERSION"));
            Ok(())
        }
        Some(Commands::PrintMigrationCount) => {
            println!("{}", crate::db::Database::MIGRATION_COUNT);
            Ok(())
        }
        Some(Commands::Evolve { check_only }) => commands::cmd_evolve(&config, check_only).await,
        Some(Commands::Migrate { source, dry_run }) => migrate::cmd_migrate(source, dry_run).await,
    }
}