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 <[email protected]>
This commit is contained in:
Zaki
2026-03-12 12:06:30 -07:00
co-authored by Claude Opus 4.6
parent 112a4087e7
commit 4aad0cfbaa
7 changed files with 56 additions and 262 deletions
+12 -11
View File
@@ -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.
+40 -163
View File
@@ -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<String>,
/// 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<dyn Database>,
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<dyn Database>,
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<u64>,
) -> 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<dyn Database>,
user_id: &str,
name: &str,
) -> anyhow::Result<Routine> {
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}");
}
}
@@ -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 <MESSAGE> Single message mode - send one message and exit
-c, --config <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
@@ -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
@@ -1,50 +0,0 @@
---
source: src/cli/mod.rs
assertion_line: 318
expression: help
---
IronClaw is a secure AI assistant. Use 'ironclaw <subcommand> --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 <MESSAGE>
Single message mode - send one message and exit
-c, --config <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
@@ -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
+2 -2
View File
@@ -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();