use clap::{Parser, Subcommand, ValueEnum};
use heartbeat_rs::hook;
use heartbeat_rs::recover::{self, OrphanPolicy};
use std::path::PathBuf;
#[derive(Debug, Parser)]
#[command(
name = "heartbeat-stop",
about = "Claude Code stop hook for autonomous agent loops",
long_about = "Reads from a JSONL inbox at a byte offset and outputs a block/approve \
decision. Used as a Stop hook in .claude/settings.json to keep a \
Claude Code session alive while messages are queued.\n\n\
Also provides `recover` subcommand for launcher-side orphan recovery."
)]
struct Args {
#[command(subcommand)]
command: Option<Command>,
#[arg(long, global = true)]
inbox: Option<PathBuf>,
#[arg(long, default_value = "drain")]
mode: CliMode,
}
#[derive(Debug, Subcommand)]
enum Command {
Recover {
#[arg(long, default_value = "deadletter")]
on_orphan: CliOrphanPolicy,
},
}
#[derive(Debug, Clone, ValueEnum)]
enum CliMode {
Drain,
Persist,
}
#[derive(Debug, Clone, ValueEnum)]
enum CliOrphanPolicy {
Retry,
Deadletter,
Drop,
}
impl From<CliMode> for hook::Mode {
fn from(m: CliMode) -> Self {
match m {
CliMode::Drain => hook::Mode::Drain,
CliMode::Persist => hook::Mode::Persist,
}
}
}
impl From<CliOrphanPolicy> for OrphanPolicy {
fn from(p: CliOrphanPolicy) -> Self {
match p {
CliOrphanPolicy::Retry => OrphanPolicy::Retry,
CliOrphanPolicy::Deadletter => OrphanPolicy::DeadLetter,
CliOrphanPolicy::Drop => OrphanPolicy::Drop,
}
}
}
fn main() {
let args = Args::parse();
match args.command {
Some(Command::Recover { on_orphan }) => {
let inbox = match args.inbox {
Some(p) => p,
None => {
eprintln!("heartbeat-stop recover: --inbox is required");
std::process::exit(1);
}
};
let policy = OrphanPolicy::from(on_orphan);
match recover::recover(&inbox, policy) {
Ok(outcome) => {
eprintln!("heartbeat-stop recover: {:?}", outcome);
}
Err(e) => {
eprintln!("heartbeat-stop recover: error: {}", e);
std::process::exit(1);
}
}
std::process::exit(0);
}
None => {
let inbox = match args.inbox {
Some(p) => p,
None => {
eprintln!("heartbeat-stop: --inbox is required");
std::process::exit(1);
}
};
let mode = hook::Mode::from(args.mode);
let decision = match hook::run(&inbox, &mode) {
Ok(d) => d,
Err(e) => {
eprintln!("heartbeat-stop: error reading inbox: {}", e);
hook::Decision::Approve
}
};
let output = hook::serialize(&decision);
if !output.is_empty() {
print!("{}", output);
}
std::process::exit(0);
}
}
}