use std::path::PathBuf;
use std::process::Command;
use recall_hooks::{claude, client::Client, declared_env, project, scope, ClientConfig, Context};
pub fn root() -> PathBuf {
if let Some(root) = git(&["rev-parse", "--show-toplevel"]) {
return PathBuf::from(root);
}
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
pub fn remote() -> String {
git(&["remote", "get-url", "origin"]).unwrap_or_default()
}
pub fn git_root() -> Option<PathBuf> {
git(&["rev-parse", "--show-toplevel"]).map(PathBuf::from)
}
pub struct Resolved {
pub root: PathBuf,
pub env: declared_env::Environment,
}
pub fn resolve() -> Resolved {
resolve_at(root())
}
pub fn resolve_at(root: PathBuf) -> Resolved {
let env = declared_env::Environment::discover(&root);
Resolved { root, env }
}
impl Resolved {
pub fn memory_dir(&self) -> PathBuf {
claude::Env::from_lookup(self.env.lookup()).memory_dir(&self.root.to_string_lossy())
}
pub fn config(&self) -> ClientConfig {
ClientConfig::from_lookup(self.env.lookup())
}
pub fn project_key(&self, cfg: &ClientConfig, remote: &str) -> String {
project::key_with_override(
cfg.project_key.as_deref(),
remote,
&self.root.to_string_lossy(),
)
}
pub fn hook_context(&self) -> anyhow::Result<Context> {
let cfg = self.config();
cfg.require()?;
let root_str = self.root.to_string_lossy().to_string();
Ok(Context {
memory_dir: cfg.claude.memory_dir(&root_str),
state_file: cfg.claude.state_file(&root_str),
scopes: scope::scopes(self.project_key(&cfg, &remote()), cfg.global_key.clone()),
source_env: cfg.source_env.clone(),
client: Client::new(&cfg.url, &cfg.token)?,
})
}
}
fn git(args: &[&str]) -> Option<String> {
let out = Command::new("git").args(args).output().ok()?;
if !out.status.success() {
return None;
}
let value = String::from_utf8_lossy(&out.stdout).trim().to_string();
(!value.is_empty()).then_some(value)
}