use crate::client::{spawn_daemon_detached, Client};
use autofork_core::config::Paths;
use autofork_core::project::project_root;
use autofork_core::protocol::{Event, EventKind, RequestBody, ResponseBody};
use serde::Deserialize;
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum HookKind {
SessionStart,
UserPromptSubmit,
StopWait,
SessionEnd,
}
#[derive(Debug, Deserialize)]
struct HookInput {
session_id: String,
#[serde(default)]
transcript_path: Option<PathBuf>,
#[serde(default)]
cwd: Option<PathBuf>,
#[serde(default)]
source: Option<String>,
#[serde(default)]
reason: Option<String>,
#[serde(default)]
model: Option<String>,
#[serde(default)]
prompt: Option<String>,
}
pub fn run_hook(kind: HookKind) {
let _ = run_hook_inner(kind);
}
fn run_hook_inner(kind: HookKind) -> Option<()> {
if std::env::var_os("AUTOFORK_FORK").is_some()
|| std::env::var_os("AUTOFORK_SESSION_ID").is_some()
{
return Some(());
}
let mut raw = String::new();
use std::io::Read;
std::io::stdin().read_to_string(&mut raw).ok()?;
let input: HookInput = serde_json::from_str(&raw).ok()?;
let paths = Paths::from_env()?;
let cwd = input.cwd.clone().or_else(|| std::env::current_dir().ok())?;
let root = project_root(&cwd);
let enable_tags = tags_from_env("AUTOFORK_ENABLE_TAGS");
let disable_tags = tags_from_env("AUTOFORK_DISABLE_TAGS");
let event = |ev: EventKind| Event {
event: ev,
session_id: input.session_id.clone(),
transcript_path: input.transcript_path.clone(),
cwd: cwd.clone(),
project_root: root.clone(),
source: input.source.clone(),
reason: input.reason.clone(),
model: input.model.clone(),
enable_tags: enable_tags.clone(),
disable_tags: disable_tags.clone(),
waking: None,
notif_tool_use_id: None,
notif_task_id: None,
notif_status: None,
notif_continue: None,
context_tokens: None,
context_window: None,
client: None,
busy: None,
};
match kind {
HookKind::SessionStart => {
let client = Client::connect_or_spawn(&paths, Duration::from_secs(5)).ok()?;
let mut client = client.ensure_current_version(&paths).ok()?;
let _ = client.request(RequestBody::Event(event(EventKind::SessionStart)));
}
HookKind::UserPromptSubmit => {
let Ok(mut client) = Client::connect(&paths, Duration::from_millis(1500)) else {
spawn_daemon_detached(&paths);
return Some(());
};
if let Ok(ResponseBody::Reports { blocks }) = client.request(RequestBody::TakeReports {
session_id: input.session_id.clone(),
}) {
if !blocks.is_empty() {
print_additional_context(&blocks);
}
}
let mut ev = event(EventKind::PromptSubmit);
if let Some(p) = input.prompt.as_deref() {
if p.contains(autofork_core::wake::WAKE_MARKER) {
ev.waking = Some(false);
} else if let Some(n) = autofork_core::notification::parse_task_notification(p) {
ev.waking = Some(false);
ev.notif_tool_use_id = n.tool_use_id;
ev.notif_task_id = n.task_id;
ev.notif_status = n.status;
ev.notif_continue = Some(n.continue_requested);
} else {
ev.waking = Some(true);
}
}
let _ = client.request(RequestBody::Event(ev));
}
HookKind::StopWait => {
let client = Client::connect_or_spawn(&paths, Duration::from_secs(10)).ok()?;
let mut client = client.ensure_current_version(&paths).ok()?;
let headless = {
let (cfg, _warnings) =
autofork_core::config::load_config_at(Some(&root), &paths.user_config());
cfg.fork_runner == autofork_core::config::ForkRunner::Headless
};
if headless {
let resume_target = input
.transcript_path
.as_deref()
.and_then(|p| p.file_stem())
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| input.session_id.clone());
let mut reports = std::collections::HashMap::new();
while let Ok(ResponseBody::Wake { forks, .. }) =
client.stop_wait(event(EventKind::Stop))
{
crate::runner::execute_wake(
&paths,
&input.session_id,
&resume_target,
&cwd,
forks.unwrap_or_default(),
&mut reports,
);
std::thread::sleep(Duration::from_secs(1));
let c = Client::connect_or_spawn(&paths, Duration::from_secs(10)).ok()?;
client = c.ensure_current_version(&paths).ok()?;
}
} else {
if let Ok(ResponseBody::Wake { payload, .. }) =
client.stop_wait(event(EventKind::Stop))
{
eprintln!("{payload}");
std::process::exit(2);
}
}
}
HookKind::SessionEnd => {
let mut client = Client::connect_or_spawn(&paths, Duration::from_secs(5)).ok()?;
let _ = client.request(RequestBody::Event(event(EventKind::SessionEnd)));
}
}
Some(())
}
fn print_additional_context(blocks: &[String]) {
const CAP: usize = 9_800;
let mut text = blocks.join("\n\n");
if text.len() > CAP {
let mut cut = CAP;
while !text.is_char_boundary(cut) {
cut -= 1;
}
text.truncate(cut);
text.push_str("\n[…report truncated to fit the context budget]");
}
let out = serde_json::json!({
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": text,
}
});
println!("{out}");
}
pub(crate) fn tags_from_env(var: &str) -> Option<Vec<String>> {
let raw = std::env::var(var).ok()?;
let mut out: Vec<String> = Vec::new();
for piece in raw.split(',') {
let t = piece.trim();
if !t.is_empty() && !out.iter().any(|e| e == t) {
out.push(t.to_string());
}
}
if out.is_empty() {
None
} else {
Some(out)
}
}