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
188
src/daemon.rs
188
src/daemon.rs
|
|
@ -540,7 +540,12 @@ pub fn show_status() -> Result<(), String> {
|
||||||
|
|
||||||
if n_running > 0 {
|
if n_running > 0 {
|
||||||
for t in tasks.iter().filter(|t| matches!(t.status, TaskStatus::Running)) {
|
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();
|
let mut parts = Vec::new();
|
||||||
|
|
@ -566,9 +571,13 @@ pub fn show_status() -> Result<(), String> {
|
||||||
eprintln!(" {}", group_label);
|
eprintln!(" {}", group_label);
|
||||||
for t in &tasks {
|
for t in &tasks {
|
||||||
let sym = status_symbol(t);
|
let sym = status_symbol(t);
|
||||||
let duration = t.result.as_ref()
|
let duration = if matches!(t.status, TaskStatus::Running) && !t.elapsed.is_zero() {
|
||||||
.map(|r| format_duration_human(r.duration.as_millis()))
|
format_duration_human(t.elapsed.as_millis())
|
||||||
.unwrap_or_default();
|
} else {
|
||||||
|
t.result.as_ref()
|
||||||
|
.map(|r| format_duration_human(r.duration.as_millis()))
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
let retry = if t.max_retries > 0 && t.retry_count > 0 {
|
let retry = if t.max_retries > 0 && t.retry_count > 0 {
|
||||||
format!(" retry {}/{}", t.retry_count, t.max_retries)
|
format!(" retry {}/{}", t.retry_count, t.max_retries)
|
||||||
|
|
@ -645,26 +654,81 @@ WantedBy=default.target
|
||||||
|
|
||||||
eprintln!("Service enabled and started");
|
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()?;
|
install_hook()?;
|
||||||
|
|
||||||
Ok(())
|
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.
|
/// 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> {
|
pub fn install_hook() -> Result<(), String> {
|
||||||
let home = std::env::var("HOME").map_err(|e| format!("HOME: {}", e))?;
|
let home = std::env::var("HOME").map_err(|e| format!("HOME: {}", e))?;
|
||||||
let exe = std::env::current_exe()
|
let exe = std::env::current_exe()
|
||||||
.map_err(|e| format!("current_exe: {}", e))?;
|
.map_err(|e| format!("current_exe: {}", e))?;
|
||||||
let settings_path = PathBuf::from(&home).join(".claude/settings.json");
|
let settings_path = PathBuf::from(&home).join(".claude/settings.json");
|
||||||
let hook_binary = exe.with_file_name("memory-search");
|
|
||||||
|
|
||||||
if !hook_binary.exists() {
|
let memory_search = exe.with_file_name("memory-search");
|
||||||
eprintln!("Warning: {} not found — hook not installed", hook_binary.display());
|
let poc_hook = exe.with_file_name("poc-hook");
|
||||||
eprintln!(" Build with: cargo install --path .");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut settings: serde_json::Value = if settings_path.exists() {
|
let mut settings: serde_json::Value = if settings_path.exists() {
|
||||||
let content = fs::read_to_string(&settings_path)
|
let content = fs::read_to_string(&settings_path)
|
||||||
|
|
@ -675,61 +739,81 @@ pub fn install_hook() -> Result<(), String> {
|
||||||
serde_json::json!({})
|
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 obj = settings.as_object_mut().ok_or("settings not an object")?;
|
||||||
let hooks_obj = obj.entry("hooks")
|
let hooks_obj = obj.entry("hooks")
|
||||||
.or_insert_with(|| serde_json::json!({}))
|
.or_insert_with(|| serde_json::json!({}))
|
||||||
.as_object_mut().ok_or("hooks not an object")?;
|
.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() {
|
let mut changed = false;
|
||||||
ups_array.push(serde_json::json!({"hooks": []}));
|
|
||||||
}
|
|
||||||
let inner_hooks = ups_array[0]
|
|
||||||
.as_object_mut().ok_or("first element not an object")?
|
|
||||||
.entry("hooks")
|
|
||||||
.or_insert_with(|| serde_json::json!([]))
|
|
||||||
.as_array_mut().ok_or("inner hooks not an array")?;
|
|
||||||
|
|
||||||
// Remove load-memory.sh if present (replaced by memory-search)
|
// Helper: ensure a hook binary is present in an event's hook list
|
||||||
let before_len = inner_hooks.len();
|
let ensure_hook = |hooks_obj: &mut serde_json::Map<String, serde_json::Value>,
|
||||||
inner_hooks.retain(|h| {
|
event: &str,
|
||||||
let cmd = h.get("command").and_then(|c| c.as_str()).unwrap_or("");
|
binary: &Path,
|
||||||
!cmd.contains("load-memory")
|
timeout: u32,
|
||||||
});
|
changed: &mut bool| {
|
||||||
if inner_hooks.len() < before_len {
|
if !binary.exists() {
|
||||||
eprintln!("Removed load-memory.sh hook (replaced by memory-search)");
|
eprintln!("Warning: {} not found — skipping", binary.display());
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
let cmd = binary.to_string_lossy().to_string();
|
||||||
|
let name = binary.file_name().unwrap().to_string_lossy().to_string();
|
||||||
|
|
||||||
// Check if memory-search hook already exists
|
let event_array = hooks_obj.entry(event)
|
||||||
let already_installed = inner_hooks.iter().any(|h| {
|
.or_insert_with(|| serde_json::json!([{"hooks": []}]))
|
||||||
h.get("command").and_then(|c| c.as_str())
|
.as_array_mut().unwrap();
|
||||||
.is_some_and(|c| c.contains("memory-search"))
|
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().unwrap();
|
||||||
|
|
||||||
let mut changed = inner_hooks.len() < before_len;
|
// 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.len() < before {
|
||||||
|
eprintln!("Removed load-memory.sh from {event}");
|
||||||
|
*changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
if already_installed {
|
let already = inner.iter().any(|h| {
|
||||||
eprintln!("Hook already installed in {}", settings_path.display());
|
h.get("command").and_then(|c| c.as_str())
|
||||||
} else {
|
.is_some_and(|c| c.contains(&name))
|
||||||
inner_hooks.push(serde_json::json!({
|
});
|
||||||
"type": "command",
|
|
||||||
"command": hook_command,
|
if !already {
|
||||||
"timeout": 10
|
inner.push(serde_json::json!({
|
||||||
}));
|
"type": "command",
|
||||||
changed = true;
|
"command": cmd,
|
||||||
eprintln!("Hook installed: {}", hook_command);
|
"timeout": timeout
|
||||||
}
|
}));
|
||||||
|
*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 {
|
if changed {
|
||||||
let json = serde_json::to_string_pretty(&settings)
|
let json = serde_json::to_string_pretty(&settings)
|
||||||
.map_err(|e| format!("serialize settings: {}", e))?;
|
.map_err(|e| format!("serialize settings: {}", e))?;
|
||||||
fs::write(&settings_path, json)
|
fs::write(&settings_path, json)
|
||||||
.map_err(|e| format!("write settings: {}", e))?;
|
.map_err(|e| format!("write settings: {}", e))?;
|
||||||
|
eprintln!("Updated {}", settings_path.display());
|
||||||
|
} else {
|
||||||
|
eprintln!("All hooks already installed in {}", settings_path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue