use crate::paths::routine_prompt_path;
use super::agents::AgentCommand;
use super::model::Routine;
pub(crate) fn slugify(title: &str) -> String {
let mut out = String::new();
let mut prev_dash = false;
for c in title.chars() {
if c.is_ascii_alphanumeric() {
out.push(c.to_ascii_lowercase());
prev_dash = false;
} else if !prev_dash {
out.push('-');
prev_dash = true;
}
}
let trimmed = out.trim_matches('-').to_string();
if trimmed.is_empty() {
"routine".to_string()
} else {
trimmed
}
}
pub(crate) fn compose_prompt(routine: &Routine) -> String {
let mut s = String::from("# Workbench\n");
s.push_str(
"You are working in an empty directory. These repositories are relevant — clone any you need:\n",
);
for repo in &routine.repositories {
match &repo.branch {
Some(b) => s.push_str(&format!("- {} (branch {})\n", repo.repository, b)),
None => s.push_str(&format!("- {}\n", repo.repository)),
}
}
s.push_str("\n---\n");
s.push_str(&routine.prompt);
s.push('\n');
s
}
pub(crate) fn substitute(s: &str, workbench: &str, prompt_file: &str) -> String {
s.replace("{workbench}", workbench)
.replace("{prompt_file}", prompt_file)
.replace("{prompt}", r#""$(cat prompt.md)""#)
}
fn bin_dir(bin: &str) -> Option<String> {
let path = std::env::var("PATH").ok()?;
path.split(':')
.filter(|d| !d.is_empty())
.find(|d| std::path::Path::new(d).join(bin).is_file())
.map(str::to_string)
}
fn cron_path(agent_command: &str) -> String {
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
let mut dirs: Vec<String> = Vec::new();
for bin in ["tmux", agent_command] {
if let Some(d) = bin_dir(bin) {
dirs.push(d);
}
}
for d in [
format!("{home}/.local/bin"),
"/opt/homebrew/bin".to_string(),
"/usr/local/bin".to_string(),
format!("{home}/.cargo/bin"),
format!("{home}/.bun/bin"),
"/usr/bin".to_string(),
"/bin".to_string(),
"/usr/sbin".to_string(),
"/sbin".to_string(),
] {
dirs.push(d);
}
let mut seen = std::collections::HashSet::new();
dirs.retain(|d| seen.insert(d.clone()));
dirs.join(":")
}
pub(crate) fn shell_quote(s: &str) -> String {
let mut out = String::from("'");
for c in s.chars() {
if c == '\'' {
out.push_str("'\\''");
} else {
out.push(c);
}
}
out.push('\'');
out
}
const MOADIM_SYSTEM_PROMPT: &str = "# Moadim Context\\n\
\\n\
> This section is managed by the moadim daemon. Do not edit it.\\n\
\\n\
You are running inside a moadim-managed agent session. \
Complete the task described in `prompt.md` and exit when done.";
pub(crate) fn system_prompt_stmts(user_prompt_path: &str) -> Vec<String> {
let header = shell_quote(MOADIM_SYSTEM_PROMPT);
let uq = shell_quote(user_prompt_path);
vec![
format!(
r#"printf '%b\n\n**Run date**: %s\n**Timezone**: %s\n' {} "$(date)" "$(date +%Z)" > "$WB/CLAUDE.md""#,
header
),
format!(
r#"[ -f {uq} ] && {{ printf '\n---\n\n'; cat {uq}; printf '\n'; }} >> "$WB/CLAUDE.md" || true"#,
uq = uq
),
]
}
pub(crate) fn build_routine_command(routine: &Routine, agent: &AgentCommand) -> String {
let slug = slugify(&routine.title);
let prompt_path = routine_prompt_path(&slug).to_string_lossy().into_owned();
let prompt_file_ref = "prompt.md";
let workbench_ref = ".";
let mut invocation = vec![agent.command.clone()];
for a in &agent.args {
invocation.push(substitute(a, workbench_ref, prompt_file_ref));
}
let invocation = invocation.join(" ");
let mut stmts = vec![
format!("export PATH={}", shell_quote(&cron_path(&agent.command))),
r#"TS="$(date +%s)""#.to_string(),
format!("SLUG={}", shell_quote(&slug)),
r#"WB="$HOME/.moadim/workbenches/$SLUG-$TS""#.to_string(),
r#"SESS="moadim-$SLUG-$TS""#.to_string(),
r#"mkdir -p "$WB""#.to_string(),
];
stmts.extend(system_prompt_stmts(
&crate::paths::user_prompt_path().to_string_lossy(),
));
stmts.extend([
format!(
r#"cp {src} "$WB/prompt.md" || {{ echo "moadim: missing routine prompt {src}; aborting launch" | tee -a "$WB/agent.log" >&2; exit 1; }}"#,
src = shell_quote(&prompt_path)
),
]);
if let Some(setup) = &agent.setup {
stmts.push(setup.clone());
}
stmts.push(format!(
r#"tmux new-session -d -s "$SESS" -c "$WB" {}"#,
shell_quote(&invocation)
));
stmts.push(r#"tmux pipe-pane -o -t "$SESS" "cat >> \"$WB\"/agent.log""#.to_string());
stmts.join("; ")
}