From 4aad0cfbaa2c1a02305ce09d71b75453aee03fed Mon Sep 17 00:00:00 2001 From: Zaki Date: Thu, 12 Mar 2026 12:06:30 -0700 Subject: [PATCH] refactor(cli): rename cron subcommand to routines The system manages all routine types (cron, webhook, event, manual), not just cron schedules. Rename the CLI subcommand to reflect this: - `ironclaw cron` -> `ironclaw routines` (with `cron` as hidden alias) - List shows all routines by default, add --trigger filter - Remove cron-trigger-only validation - Simplify require_routine helper (no trigger type check) Co-Authored-By: Claude Opus 4.6 --- src/cli/mod.rs | 23 +- src/cli/{cron.rs => routines.rs} | 203 ++++-------------- .../ironclaw__cli__tests__help_output.snap | 34 --- ...li__tests__help_output_without_import.snap | 2 +- ...ronclaw__cli__tests__long_help_output.snap | 50 ----- ...ests__long_help_output_without_import.snap | 2 +- src/main.rs | 4 +- 7 files changed, 56 insertions(+), 262 deletions(-) rename src/cli/{cron.rs => routines.rs} (77%) delete mode 100644 src/cli/snapshots/ironclaw__cli__tests__help_output.snap delete mode 100644 src/cli/snapshots/ironclaw__cli__tests__long_help_output.snap diff --git a/src/cli/mod.rs b/src/cli/mod.rs index d8b9f11d..44b6d5c8 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -7,7 +7,7 @@ //! - 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 scheduled routines (`cron list`, `cron create`, `cron edit`, ...) +//! - 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`) @@ -16,7 +16,7 @@ mod channels; mod completion; mod config; -mod cron; +mod routines; mod doctor; #[cfg(feature = "import")] pub mod import; @@ -33,7 +33,7 @@ mod tool; pub use channels::{ChannelsCommand, run_channels_command}; pub use completion::Completion; pub use config::{ConfigCommand, run_config_command}; -pub use cron::{CronCommand, run_cron_command}; +pub use routines::{RoutinesCommand, run_routines_command}; pub use doctor::run_doctor_command; #[cfg(feature = "import")] pub use import::{ImportCommand, run_import_command}; @@ -150,13 +150,14 @@ pub enum Command { )] Channels(ChannelsCommand), - /// Manage scheduled routines (cron jobs) + /// Manage routines (scheduled, event-driven, webhook, manual) #[command( subcommand, - about = "Manage cron routines", - long_about = "List, create, edit, enable/disable, delete, and view history of cron routines.\nExamples:\n ironclaw cron list\n ironclaw cron create --name daily-digest --schedule '0 0 9 * * *' --prompt 'Summarize today'" + 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'" )] - Cron(CronCommand), + Routines(RoutinesCommand), /// Manage MCP servers (hosted tool providers) #[command( @@ -292,9 +293,9 @@ pub async fn init_secrets_store() Ok(crate::db::create_secrets_store(&config.database, crypto).await?) } -/// Run the Cron CLI subcommand. -pub async fn run_cron_cli( - cron_cmd: &CronCommand, +/// 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) @@ -306,7 +307,7 @@ pub async fn run_cron_cli( .map_err(|e| anyhow::anyhow!("{e:#}"))?; let user_id = std::env::var("GATEWAY_USER_ID").unwrap_or_else(|_| "default".to_string()); - run_cron_command(cron_cmd.clone(), db, &user_id).await + run_routines_command(routines_cmd.clone(), db, &user_id).await } /// Run the Memory CLI subcommand. diff --git a/src/cli/cron.rs b/src/cli/routines.rs similarity index 77% rename from src/cli/cron.rs rename to src/cli/routines.rs index 759ce26c..305aca63 100644 --- a/src/cli/cron.rs +++ b/src/cli/routines.rs @@ -1,8 +1,7 @@ -//! `ironclaw cron` — manage scheduled routines from the CLI. +//! `ironclaw routines` — manage scheduled routines from the CLI. //! //! Provides subcommands for listing, creating, editing, enabling/disabling, -//! deleting, and viewing run history of cron-triggered routines without -//! starting the full agent. +//! deleting, and viewing run history of routines without starting the full agent. use std::sync::Arc; @@ -15,14 +14,14 @@ use crate::agent::routine::{ }; use crate::db::Database; -/// Cron subcommands. +/// Routines subcommands. #[derive(Subcommand, Debug, Clone)] -pub enum CronCommand { - /// List routines (cron-triggered by default, --all for all types) +pub enum RoutinesCommand { + /// List routines List { - /// Show all trigger types, not just cron + /// Filter by trigger type (e.g. "cron", "webhook", "event") #[arg(long)] - all: bool, + trigger: Option, /// Include disabled routines #[arg(long)] @@ -132,19 +131,19 @@ pub enum CronCommand { }, } -/// Run a cron CLI command against the database. -pub async fn run_cron_command( - cmd: CronCommand, +/// Run a routines CLI command against the database. +pub async fn run_routines_command( + cmd: RoutinesCommand, db: Arc, user_id: &str, ) -> anyhow::Result<()> { match cmd { - CronCommand::List { - all, + RoutinesCommand::List { + trigger, disabled, json, - } => list(&db, user_id, all, disabled, json).await, - CronCommand::Create { + } => list(&db, user_id, trigger.as_deref(), disabled, json).await, + RoutinesCommand::Create { name, schedule, prompt, @@ -166,7 +165,7 @@ pub async fn run_cron_command( ) .await } - CronCommand::Edit { + RoutinesCommand::Edit { name, schedule, prompt, @@ -186,10 +185,10 @@ pub async fn run_cron_command( ) .await } - CronCommand::Enable { name } => set_enabled(&db, user_id, &name, true).await, - CronCommand::Disable { name } => set_enabled(&db, user_id, &name, false).await, - CronCommand::Delete { name, yes } => delete(&db, user_id, &name, yes).await, - CronCommand::History { name, limit, json } => { + RoutinesCommand::Enable { name } => set_enabled(&db, user_id, &name, true).await, + RoutinesCommand::Disable { name } => set_enabled(&db, user_id, &name, false).await, + RoutinesCommand::Delete { name, yes } => delete(&db, user_id, &name, yes).await, + RoutinesCommand::History { name, limit, json } => { history(&db, user_id, &name, limit, json).await } } @@ -200,7 +199,7 @@ pub async fn run_cron_command( async fn list( db: &Arc, user_id: &str, - all_types: bool, + trigger_filter: Option<&str>, show_disabled: bool, json: bool, ) -> anyhow::Result<()> { @@ -208,7 +207,11 @@ async fn list( let filtered: Vec<&Routine> = routines .iter() - .filter(|r| all_types || r.trigger.type_tag() == "cron") + .filter(|r| { + trigger_filter + .map(|t| r.trigger.type_tag() == t) + .unwrap_or(true) + }) .filter(|r| show_disabled || r.enabled) .collect(); @@ -233,10 +236,11 @@ async fn list( } if filtered.is_empty() { - println!( - "No {} routines found.", - if all_types { "" } else { "cron " } - ); + if let Some(t) = trigger_filter { + println!("No {t} routines found."); + } else { + println!("No routines found."); + } return Ok(()); } @@ -375,12 +379,12 @@ async fn edit( timezone: Option<&str>, cooldown: Option, ) -> anyhow::Result<()> { - let mut routine = require_cron_routine(db, user_id, name).await?; + let mut routine = require_routine(db, user_id, name).await?; validate_timezone_arg(timezone)?; let mut changed = false; - // Update schedule if provided. + // Update schedule if provided (only valid for cron routines). if let Some(new_schedule) = schedule { let tz = timezone.or(match &routine.trigger { Trigger::Cron { timezone, .. } => timezone.as_deref(), @@ -452,7 +456,7 @@ async fn set_enabled( name: &str, enabled: bool, ) -> anyhow::Result<()> { - let mut routine = require_cron_routine(db, user_id, name).await?; + let mut routine = require_routine(db, user_id, name).await?; if routine.enabled == enabled { println!( @@ -494,11 +498,12 @@ async fn delete( name: &str, skip_confirm: bool, ) -> anyhow::Result<()> { - let routine = require_cron_routine(db, user_id, name).await?; + let routine = require_routine(db, user_id, name).await?; if !skip_confirm { println!("Routine: {}", routine.name); println!(" ID: {}", routine.id); + println!(" Trigger: {}", routine.trigger.type_tag()); if let Trigger::Cron { ref schedule, .. } = routine.trigger { println!("Schedule: {}", schedule); } @@ -532,7 +537,7 @@ async fn history( limit: i64, json: bool, ) -> anyhow::Result<()> { - let routine = require_cron_routine(db, user_id, name).await?; + let routine = require_routine(db, user_id, name).await?; let limit = limit.clamp(1, 50); let runs = db.list_routine_runs(routine.id, limit).await?; @@ -603,34 +608,15 @@ async fn history( // ── Shared lookup ──────────────────────────────────────────── -/// Look up a routine by name and verify it has a cron trigger. -async fn require_cron_routine( +/// Look up a routine by name. +async fn require_routine( db: &Arc, user_id: &str, name: &str, ) -> anyhow::Result { - let routine = db - .get_routine_by_name(user_id, name) + db.get_routine_by_name(user_id, name) .await? - .ok_or_else(|| anyhow::anyhow!("Routine '{}' not found", name))?; - - validate_cron_trigger(&routine).map_err(|msg| anyhow::anyhow!("{}", msg))?; - - Ok(routine) -} - -/// Validate that a routine has a cron trigger. Returns an error message if not. -fn validate_cron_trigger(routine: &Routine) -> Result<(), String> { - if routine.trigger.type_tag() != "cron" { - return Err(format!( - "Routine '{}' has trigger type '{}', not 'cron'. \ - Use a different command to manage {} routines.", - routine.name, - routine.trigger.type_tag(), - routine.trigger.type_tag(), - )); - } - Ok(()) + .ok_or_else(|| anyhow::anyhow!("Routine '{}' not found", name)) } fn validate_timezone_arg(timezone: Option<&str>) -> anyhow::Result<()> { @@ -741,113 +727,4 @@ mod tests { // Must be valid UTF-8 (would have panicked otherwise). assert!(result.is_char_boundary(result.len())); } - - /// Helper: build a minimal Routine with the given trigger for testing. - fn make_routine(name: &str, trigger: Trigger) -> Routine { - let now = Utc::now(); - Routine { - id: Uuid::new_v4(), - name: name.to_string(), - description: String::new(), - user_id: "test".to_string(), - enabled: true, - trigger, - action: RoutineAction::Lightweight { - prompt: "test".to_string(), - context_paths: Vec::new(), - max_tokens: 4096, - }, - guardrails: RoutineGuardrails { - cooldown: std::time::Duration::from_secs(300), - max_concurrent: 1, - dedup_window: None, - }, - notify: NotifyConfig { - channel: None, - user: "test".to_string(), - on_attention: true, - on_failure: true, - on_success: false, - }, - last_run_at: None, - next_fire_at: None, - run_count: 0, - consecutive_failures: 0, - state: serde_json::json!({}), - created_at: now, - updated_at: now, - } - } - - #[test] - fn validate_cron_trigger_accepts_cron() { - let routine = make_routine( - "daily-digest", - Trigger::Cron { - schedule: "0 0 9 * * *".to_string(), - timezone: None, - }, - ); - assert!(validate_cron_trigger(&routine).is_ok()); - } - - #[test] - fn validate_cron_trigger_rejects_event() { - let routine = make_routine( - "on-push", - Trigger::Event { - channel: Some("github".to_string()), - pattern: "push".to_string(), - }, - ); - let err = validate_cron_trigger(&routine).unwrap_err(); - assert!(err.contains("event"), "expected 'event' in error: {err}"); - assert!( - err.contains("on-push"), - "expected routine name in error: {err}" - ); - } - - #[test] - fn validate_cron_trigger_rejects_system_event() { - let routine = make_routine( - "on-event", - Trigger::SystemEvent { - source: "github".to_string(), - event_type: "issue.opened".to_string(), - filters: std::collections::HashMap::new(), - }, - ); - let err = validate_cron_trigger(&routine).unwrap_err(); - assert!( - err.contains("system_event"), - "expected 'system_event' in error: {err}" - ); - } - - #[test] - fn validate_cron_trigger_rejects_manual() { - let routine = make_routine("run-once", Trigger::Manual); - let err = validate_cron_trigger(&routine).unwrap_err(); - assert!(err.contains("manual"), "expected 'manual' in error: {err}"); - } - - #[test] - fn validate_timezone_arg_accepts_none() { - assert!(validate_timezone_arg(None).is_ok()); - } - - #[test] - fn validate_timezone_arg_accepts_valid_iana_timezone() { - assert!(validate_timezone_arg(Some("America/New_York")).is_ok()); - } - - #[test] - fn validate_timezone_arg_rejects_invalid_timezone() { - let err = validate_timezone_arg(Some("Not/AZone")) - .unwrap_err() - .to_string(); - assert!(err.contains("Invalid timezone"), "got: {err}"); - assert!(err.contains("Not/AZone"), "got: {err}"); - } } diff --git a/src/cli/snapshots/ironclaw__cli__tests__help_output.snap b/src/cli/snapshots/ironclaw__cli__tests__help_output.snap deleted file mode 100644 index b675fd07..00000000 --- a/src/cli/snapshots/ironclaw__cli__tests__help_output.snap +++ /dev/null @@ -1,34 +0,0 @@ ---- -source: src/cli/mod.rs -assertion_line: 302 -expression: help ---- -Secure personal AI assistant that protects your data and expands its capabilities - -Usage: ironclaw [OPTIONS] [COMMAND] - -Commands: - run Run the AI agent - onboard Run interactive setup wizard - config Manage app configs - tool Manage WASM tools - registry Browse/install extensions - cron Manage cron routines - mcp Manage MCP servers - memory Manage workspace memory - pairing Manage DM pairing - service Manage OS service - doctor Run diagnostics - status Show system status - completion Generate completions - import Import from other AI systems - help Print this message or the help of the given subcommand(s) - -Options: - --cli-only Run in interactive CLI mode only (disable other channels) - --no-db Skip database connection (for testing) - -m, --message Single message mode - send one message and exit - -c, --config Configuration file path (optional, uses env vars by default) - --no-onboard Skip first-run onboarding check - -h, --help Print help (see more with '--help') - -V, --version Print version diff --git a/src/cli/snapshots/ironclaw__cli__tests__help_output_without_import.snap b/src/cli/snapshots/ironclaw__cli__tests__help_output_without_import.snap index 2840f8fd..c7d8db13 100644 --- a/src/cli/snapshots/ironclaw__cli__tests__help_output_without_import.snap +++ b/src/cli/snapshots/ironclaw__cli__tests__help_output_without_import.snap @@ -13,7 +13,7 @@ Commands: tool Manage WASM tools registry Browse/install extensions channels Manage channels - cron Manage cron routines + routines Manage routines mcp Manage MCP servers memory Manage workspace memory pairing Manage DM pairing diff --git a/src/cli/snapshots/ironclaw__cli__tests__long_help_output.snap b/src/cli/snapshots/ironclaw__cli__tests__long_help_output.snap deleted file mode 100644 index edb4b313..00000000 --- a/src/cli/snapshots/ironclaw__cli__tests__long_help_output.snap +++ /dev/null @@ -1,50 +0,0 @@ ---- -source: src/cli/mod.rs -assertion_line: 318 -expression: help ---- -IronClaw is a secure AI assistant. Use 'ironclaw --help' for details. -Examples: - ironclaw run # Start the agent - ironclaw config list # List configs - -Usage: ironclaw [OPTIONS] [COMMAND] - -Commands: - run Run the AI agent - onboard Run interactive setup wizard - config Manage app configs - tool Manage WASM tools - registry Browse/install extensions - cron Manage cron routines - mcp Manage MCP servers - memory Manage workspace memory - pairing Manage DM pairing - service Manage OS service - doctor Run diagnostics - status Show system status - completion Generate completions - import Import from other AI systems - help Print this message or the help of the given subcommand(s) - -Options: - --cli-only - Run in interactive CLI mode only (disable other channels) - - --no-db - Skip database connection (for testing) - - -m, --message - Single message mode - send one message and exit - - -c, --config - Configuration file path (optional, uses env vars by default) - - --no-onboard - Skip first-run onboarding check - - -h, --help - Print help (see a summary with '-h') - - -V, --version - Print version diff --git a/src/cli/snapshots/ironclaw__cli__tests__long_help_output_without_import.snap b/src/cli/snapshots/ironclaw__cli__tests__long_help_output_without_import.snap index 3a0617d2..fb4ad231 100644 --- a/src/cli/snapshots/ironclaw__cli__tests__long_help_output_without_import.snap +++ b/src/cli/snapshots/ironclaw__cli__tests__long_help_output_without_import.snap @@ -16,7 +16,7 @@ Commands: tool Manage WASM tools registry Browse/install extensions channels Manage channels - cron Manage cron routines + routines Manage routines mcp Manage MCP servers memory Manage workspace memory pairing Manage DM pairing diff --git a/src/main.rs b/src/main.rs index 3e6e2037..6fd5ccf6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -67,9 +67,9 @@ async fn async_main() -> anyhow::Result<()> { ) .await; } - Some(Command::Cron(cron_cmd)) => { + Some(Command::Routines(routines_cmd)) => { init_cli_tracing(); - return ironclaw::cli::run_cron_cli(cron_cmd, cli.config.as_deref()).await; + return ironclaw::cli::run_routines_cli(routines_cmd, cli.config.as_deref()).await; } Some(Command::Mcp(mcp_cmd)) => { init_cli_tracing();