use crate::desk::{self, Claim};
use crate::store::{self, Session};
use crate::{adb, install};
use serde_json::{json, Value};
use std::io::{Read, Write};
pub fn run(event: Option<&str>) -> i32 {
let mut input = String::new();
let _ = std::io::stdin().read_to_string(&mut input);
let v: Value = serde_json::from_str(&input).unwrap_or(Value::Null);
let Some(id) = v["session_id"].as_str().filter(|s| !s.is_empty()) else {
return 0;
};
match event {
Some("session-start") => session_start(id, &v),
Some("pre-tool-use") => pre_tool_use(id, &v),
Some("session-end") => session_end(id, &v),
_ => {}
}
0
}
fn session_start(id: &str, v: &Value) {
let agent = store::anchor();
let session = Session {
pid: agent.pid,
start: agent.start,
name: agent.name,
cwd: v["cwd"].as_str().unwrap_or_default().to_string(),
};
let _ = store::write_session(id, &session);
let _ = store::adopt(&store::me_for_session(id));
let _ = install::ensure_shim();
if let Some(path) = std::env::var_os("CLAUDE_ENV_FILE").filter(|p| !p.is_empty()) {
let lines = install::env_lines(Some(id));
let current = std::fs::read_to_string(&path).unwrap_or_default();
let named = current
.lines()
.rfind(|l| l.starts_with("export ADBRENT_SESSION="));
if named != lines.lines().next() {
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
{
let _ = writeln!(f, "{lines}");
}
}
}
let context = "Android devices on this machine are shared between agents through adbrent. \
Use `adb` as usual: your first device command rents a free device for this session, and plain `adb` \
(no -s) keeps going to it. A device rented by another agent is refused with a message naming the free \
ones; never work around a refusal. `adbrent status` lists devices, models and renters (name one with \
`adb -s <serial>` when the task needs a particular device), `adbrent rent --wait` \
queues when all are taken, and `adbrent release` hands yours back once you no longer need it.";
println!(
"{}",
json!({"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": context}})
);
}
fn device_args(v: &Value, key: &str, out: &mut Vec<String>) {
match v {
Value::String(s) => {
let k = key.to_ascii_lowercase();
if ["udid", "serial", "device"].iter().any(|n| k.contains(n)) {
out.push(s.clone());
}
}
Value::Array(items) => items.iter().for_each(|i| device_args(i, key, out)),
Value::Object(map) => map.iter().for_each(|(k, val)| device_args(val, k, out)),
_ => {}
}
}
fn pre_tool_use(id: &str, v: &Value) {
if !v["tool_name"]
.as_str()
.is_some_and(|n| n.starts_with("mcp__"))
{
return;
}
let mut named = Vec::new();
device_args(&v["tool_input"], "", &mut named);
if named.is_empty() {
return;
}
let Some(real) = adb::real_adb() else { return };
let devs = adb::devices(&real, &[]);
let me = store::me_for_session(id);
for d in devs.iter().filter(|d| named.contains(&d.serial)) {
if let Claim::Refused(reason) = desk::claim(&real, &d.serial, &me, &devs, false) {
println!(
"{}",
json!({"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": reason,
}})
);
return;
}
}
}
fn session_end(id: &str, v: &Value) {
if v["reason"].as_str() != Some("clear") {
let owner = format!("s:{id}");
let _ = store::release(|l| l.owner == owner);
}
store::remove_session(id);
}