installer: add poc-daemon systemd service, multi-event hook setup
- Install poc-daemon.service alongside poc-memory.service - Rewrite install_hook() to install hooks across multiple events: UserPromptSubmit: memory-search (10s) + poc-hook (5s) PostToolUse: poc-hook (5s) Stop: poc-hook (5s) - Show elapsed time for running tasks in status display - Deduplicate hook installation (idempotent ensure_hook helper) - Still removes legacy load-memory.sh hooks Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
This commit is contained in:
parent
d919bc3e51
commit
8662759d53
1 changed files with 136 additions and 52 deletions
168
src/daemon.rs
168
src/daemon.rs
|
|
@ -540,7 +540,12 @@ pub fn show_status() -> Result<(), String> {
|
|||
|
||||
if n_running > 0 {
|
||||
for t in tasks.iter().filter(|t| matches!(t.status, TaskStatus::Running)) {
|
||||
eprintln!(" {} {}", status_symbol(t), t.name);
|
||||
let elapsed = if !t.elapsed.is_zero() {
|
||||
format!(" ({})", format_duration_human(t.elapsed.as_millis()))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
eprintln!(" {} {}{}", status_symbol(t), t.name, elapsed);
|
||||
}
|
||||
}
|
||||
let mut parts = Vec::new();
|
||||
|
|
@ -566,9 +571,13 @@ pub fn show_status() -> Result<(), String> {
|
|||
eprintln!(" {}", group_label);
|
||||
for t in &tasks {
|
||||
let sym = status_symbol(t);
|
||||
let duration = t.result.as_ref()
|
||||
let duration = if matches!(t.status, TaskStatus::Running) && !t.elapsed.is_zero() {
|
||||
format_duration_human(t.elapsed.as_millis())
|
||||
} else {
|
||||
t.result.as_ref()
|
||||
.map(|r| format_duration_human(r.duration.as_millis()))
|
||||
.unwrap_or_default();
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let retry = if t.max_retries > 0 && t.retry_count > 0 {
|
||||
format!(" retry {}/{}", t.retry_count, t.max_retries)
|
||||
|
|
@ -645,26 +654,81 @@ WantedBy=default.target
|
|||
|
||||
eprintln!("Service enabled and started");
|
||||
|
||||
// Install memory-search hook into Claude settings
|
||||
// Install poc-daemon service
|
||||
install_notify_daemon(&unit_dir, &home)?;
|
||||
|
||||
// Install memory-search + poc-hook into Claude settings
|
||||
install_hook()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install the memory-search hook into Claude Code settings.json.
|
||||
/// Install the poc-daemon (notification/idle) systemd user service.
|
||||
fn install_notify_daemon(unit_dir: &Path, home: &str) -> Result<(), String> {
|
||||
let poc_daemon = PathBuf::from(home).join(".cargo/bin/poc-daemon");
|
||||
if !poc_daemon.exists() {
|
||||
eprintln!("Warning: poc-daemon not found at {} — skipping service install", poc_daemon.display());
|
||||
eprintln!(" Build with: cargo install --path .");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let unit = format!(
|
||||
r#"[Unit]
|
||||
Description=poc-daemon — notification routing and idle management
|
||||
After=default.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart={exe}
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
Environment=HOME={home}
|
||||
Environment=PATH={home}/.cargo/bin:{home}/.local/bin:{home}/bin:/usr/local/bin:/usr/bin:/bin
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
"#, exe = poc_daemon.display(), home = home);
|
||||
|
||||
let unit_path = unit_dir.join("poc-daemon.service");
|
||||
fs::write(&unit_path, &unit)
|
||||
.map_err(|e| format!("write {}: {}", unit_path.display(), e))?;
|
||||
eprintln!("Wrote {}", unit_path.display());
|
||||
|
||||
let status = std::process::Command::new("systemctl")
|
||||
.args(["--user", "daemon-reload"])
|
||||
.status()
|
||||
.map_err(|e| format!("systemctl daemon-reload: {}", e))?;
|
||||
if !status.success() {
|
||||
return Err("systemctl daemon-reload failed".into());
|
||||
}
|
||||
|
||||
let status = std::process::Command::new("systemctl")
|
||||
.args(["--user", "enable", "--now", "poc-daemon"])
|
||||
.status()
|
||||
.map_err(|e| format!("systemctl enable: {}", e))?;
|
||||
if !status.success() {
|
||||
return Err("systemctl enable --now poc-daemon failed".into());
|
||||
}
|
||||
|
||||
eprintln!("poc-daemon service enabled and started");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install memory-search and poc-hook into Claude Code settings.json.
|
||||
/// Public so `poc-memory init` can call it too.
|
||||
///
|
||||
/// Hook layout:
|
||||
/// UserPromptSubmit: memory-search (10s), poc-hook (5s)
|
||||
/// PostToolUse: poc-hook (5s)
|
||||
/// Stop: poc-hook (5s)
|
||||
pub fn install_hook() -> Result<(), String> {
|
||||
let home = std::env::var("HOME").map_err(|e| format!("HOME: {}", e))?;
|
||||
let exe = std::env::current_exe()
|
||||
.map_err(|e| format!("current_exe: {}", e))?;
|
||||
let settings_path = PathBuf::from(&home).join(".claude/settings.json");
|
||||
let hook_binary = exe.with_file_name("memory-search");
|
||||
|
||||
if !hook_binary.exists() {
|
||||
eprintln!("Warning: {} not found — hook not installed", hook_binary.display());
|
||||
eprintln!(" Build with: cargo install --path .");
|
||||
return Ok(());
|
||||
}
|
||||
let memory_search = exe.with_file_name("memory-search");
|
||||
let poc_hook = exe.with_file_name("poc-hook");
|
||||
|
||||
let mut settings: serde_json::Value = if settings_path.exists() {
|
||||
let content = fs::read_to_string(&settings_path)
|
||||
|
|
@ -675,61 +739,81 @@ pub fn install_hook() -> Result<(), String> {
|
|||
serde_json::json!({})
|
||||
};
|
||||
|
||||
let hook_command = hook_binary.to_string_lossy().to_string();
|
||||
|
||||
// Navigate the nested structure: hooks.UserPromptSubmit[0].hooks[]
|
||||
let obj = settings.as_object_mut().ok_or("settings not an object")?;
|
||||
let hooks_obj = obj.entry("hooks")
|
||||
.or_insert_with(|| serde_json::json!({}))
|
||||
.as_object_mut().ok_or("hooks not an object")?;
|
||||
let ups_array = hooks_obj.entry("UserPromptSubmit")
|
||||
.or_insert_with(|| serde_json::json!([{"hooks": []}]))
|
||||
.as_array_mut().ok_or("UserPromptSubmit not an array")?;
|
||||
|
||||
if ups_array.is_empty() {
|
||||
ups_array.push(serde_json::json!({"hooks": []}));
|
||||
let mut changed = false;
|
||||
|
||||
// Helper: ensure a hook binary is present in an event's hook list
|
||||
let ensure_hook = |hooks_obj: &mut serde_json::Map<String, serde_json::Value>,
|
||||
event: &str,
|
||||
binary: &Path,
|
||||
timeout: u32,
|
||||
changed: &mut bool| {
|
||||
if !binary.exists() {
|
||||
eprintln!("Warning: {} not found — skipping", binary.display());
|
||||
return;
|
||||
}
|
||||
let inner_hooks = ups_array[0]
|
||||
.as_object_mut().ok_or("first element not an object")?
|
||||
let cmd = binary.to_string_lossy().to_string();
|
||||
let name = binary.file_name().unwrap().to_string_lossy().to_string();
|
||||
|
||||
let event_array = hooks_obj.entry(event)
|
||||
.or_insert_with(|| serde_json::json!([{"hooks": []}]))
|
||||
.as_array_mut().unwrap();
|
||||
if event_array.is_empty() {
|
||||
event_array.push(serde_json::json!({"hooks": []}));
|
||||
}
|
||||
let inner = event_array[0]
|
||||
.as_object_mut().unwrap()
|
||||
.entry("hooks")
|
||||
.or_insert_with(|| serde_json::json!([]))
|
||||
.as_array_mut().ok_or("inner hooks not an array")?;
|
||||
.as_array_mut().unwrap();
|
||||
|
||||
// Remove load-memory.sh if present (replaced by memory-search)
|
||||
let before_len = inner_hooks.len();
|
||||
inner_hooks.retain(|h| {
|
||||
let cmd = h.get("command").and_then(|c| c.as_str()).unwrap_or("");
|
||||
!cmd.contains("load-memory")
|
||||
// Remove legacy load-memory.sh
|
||||
let before = inner.len();
|
||||
inner.retain(|h| {
|
||||
let c = h.get("command").and_then(|c| c.as_str()).unwrap_or("");
|
||||
!c.contains("load-memory")
|
||||
});
|
||||
if inner_hooks.len() < before_len {
|
||||
eprintln!("Removed load-memory.sh hook (replaced by memory-search)");
|
||||
if inner.len() < before {
|
||||
eprintln!("Removed load-memory.sh from {event}");
|
||||
*changed = true;
|
||||
}
|
||||
|
||||
// Check if memory-search hook already exists
|
||||
let already_installed = inner_hooks.iter().any(|h| {
|
||||
let already = inner.iter().any(|h| {
|
||||
h.get("command").and_then(|c| c.as_str())
|
||||
.is_some_and(|c| c.contains("memory-search"))
|
||||
.is_some_and(|c| c.contains(&name))
|
||||
});
|
||||
|
||||
let mut changed = inner_hooks.len() < before_len;
|
||||
|
||||
if already_installed {
|
||||
eprintln!("Hook already installed in {}", settings_path.display());
|
||||
} else {
|
||||
inner_hooks.push(serde_json::json!({
|
||||
if !already {
|
||||
inner.push(serde_json::json!({
|
||||
"type": "command",
|
||||
"command": hook_command,
|
||||
"timeout": 10
|
||||
"command": cmd,
|
||||
"timeout": timeout
|
||||
}));
|
||||
changed = true;
|
||||
eprintln!("Hook installed: {}", hook_command);
|
||||
*changed = true;
|
||||
eprintln!("Installed {name} in {event}");
|
||||
}
|
||||
};
|
||||
|
||||
// UserPromptSubmit: memory-search + poc-hook
|
||||
ensure_hook(hooks_obj, "UserPromptSubmit", &memory_search, 10, &mut changed);
|
||||
ensure_hook(hooks_obj, "UserPromptSubmit", &poc_hook, 5, &mut changed);
|
||||
|
||||
// PostToolUse + Stop: poc-hook only
|
||||
ensure_hook(hooks_obj, "PostToolUse", &poc_hook, 5, &mut changed);
|
||||
ensure_hook(hooks_obj, "Stop", &poc_hook, 5, &mut changed);
|
||||
|
||||
if changed {
|
||||
let json = serde_json::to_string_pretty(&settings)
|
||||
.map_err(|e| format!("serialize settings: {}", e))?;
|
||||
fs::write(&settings_path, json)
|
||||
.map_err(|e| format!("write settings: {}", e))?;
|
||||
eprintln!("Updated {}", settings_path.display());
|
||||
} else {
|
||||
eprintln!("All hooks already installed in {}", settings_path.display());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue