main.rsannotatedmain.rssource62 lines · 1.8 KB · raw
1//! `jevhooks serve` is the daemon, one for the machine. `jevhooks mcp` is the
2//! MCP server a Claude Code session starts, which makes sure the daemon runs.
3
4mod client;
5mod headroom;
6mod judge;
7mod log;
8mod mcp;
9mod paths;
10mod serve;
11
12use clap::{Parser, Subcommand};
13use hyper::Method;
14
15#[derive(Parser)]
16#[command(version, about = "Claude Code hook events answered by Jev")]
17struct Cli {
18    #[command(subcommand)]
19    command: Command,
20}
21
22#[derive(Subcommand)]
23enum Command {
24    /// Run the daemon: one for the machine, on a Unix socket.
25    Serve,
26    /// Run the MCP server a Claude Code session starts (stdio).
27    Mcp,
28    /// Print the running daemon's status.
29    Status,
30    /// Print the most recent decisions, oldest first.
31    Recent {
32        #[arg(default_value_t = 20)]
33        count: u32,
34    },
35    /// Stop the running daemon.
36    Stop,
37}
38
39/// One request to the daemon, printed.
40async fn print(method: Method, path: &str) -> Result<(), Box<dyn std::error::Error>> {
41    // A reader that closes early (`| head`) ends this quietly. Only for the
42    // commands that print: a daemon must survive a peer closing its socket.
43    unsafe { libc::signal(libc::SIGPIPE, libc::SIG_DFL) };
44    let (_, value) = client::call(method, path, None).await?;
45    println!("{}", serde_json::to_string_pretty(&value)?);
46    Ok(())
47}
48
49#[tokio::main(flavor = "current_thread")]
50async fn main() {
51    let done = match Cli::parse().command {
52        Command::Serve => serve::serve().await,
53        Command::Mcp => mcp::serve().await,
54        Command::Status => print(Method::GET, "/status").await,
55        Command::Recent { count } => print(Method::GET, &format!("/recent?count={count}")).await,
56        Command::Stop => print(Method::POST, "/shutdown").await,
57    };
58    if let Err(why) = done {
59        eprintln!("jevhooks: {why}");
60        std::process::exit(1);
61    }
62}