From ab33633ceb9e4bea3e292b2371241797d1587d02 Mon Sep 17 00:00:00 2001 From: Igor Soarez Date: Mon, 19 Oct 2026 16:02:57 +0300 Subject: initial dingger: tmux agent watcher with herdr's detection engine extract detect-core from herdr's src/detect (engine + 22 manifests, tests unchanged) and add the dingger binary: tmux capture-pane watcher, process-tree agent identification, herdr's notification policy, embedded notification sounds, @agent_state write-back, and catalog-based manifest refresh. viewer screens are non-observations per the engine contract; reviewed by a three-model panel (opus 5.5, gpt 6.1 sol, kimi k3) with all confirmed findings applied as deletions. --- crates/dinggerd/src/main.rs | 100 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 crates/dinggerd/src/main.rs (limited to 'crates/dinggerd/src/main.rs') diff --git a/crates/dinggerd/src/main.rs b/crates/dinggerd/src/main.rs new file mode 100644 index 0000000..f046732 --- /dev/null +++ b/crates/dinggerd/src/main.rs @@ -0,0 +1,100 @@ +//! dingger: agent state watching for tmux panes. + +mod manifest_update; +mod notify; +mod tmux; +mod watcher; + +use clap::{Parser, Subcommand}; +use tracing_subscriber::EnvFilter; + +#[derive(Parser)] +#[command(name = "dingger", about = "Agent state watching for tmux panes")] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Watch all tmux panes and notify on agent state transitions. + Watch { + /// Poll interval in seconds. + #[arg(long, default_value_t = 1)] + interval_secs: u64, + /// Disable notification sounds. + #[arg(long)] + no_sounds: bool, + }, + /// Print the current agent state of every tmux pane. + Status, + /// Refresh detection manifests from herdr's published catalog. + Manifests { + #[command(subcommand)] + command: ManifestCommand, + }, +} + +#[derive(Subcommand)] +enum ManifestCommand { + /// Download newer manifests from herdr's published catalog into the + /// local remote store; bundled manifests are the offline fallback. + /// Restart `dingger watch` to pick them up. + Update, +} + +fn main() { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into())) + .init(); + + match Cli::parse().command { + Command::Watch { + interval_secs, + no_sounds, + } => { + let mut watcher = watcher::Watcher::new(interval_secs, !no_sounds); + if let Err(err) = watcher.run() { + eprintln!("dingger: {err}"); + std::process::exit(1); + } + } + Command::Status => { + if let Err(err) = status() { + eprintln!("dingger: {err}"); + std::process::exit(1); + } + } + Command::Manifests { command } => match command { + ManifestCommand::Update => { + if let Err(err) = manifest_update::update() { + eprintln!("dingger: {err}"); + std::process::exit(1); + } + } + }, + } +} + +fn status() -> Result<(), String> { + let panes = tmux::list_panes()?; + if panes.is_empty() { + println!("no tmux panes"); + return Ok(()); + } + println!("{:<22} {:<10} {:<12} COMMAND", "TARGET", "AGENT", "STATE"); + for pane in panes { + println!( + "{:<22} {:<10} {:<12} {}", + pane.target, + pane.agent.map(detect_core::agent_label).unwrap_or("-"), + if pane.written_state.is_empty() { + "-" + } else { + &pane.written_state + }, + pane.current_command, + ); + } + Ok(()) +} -- cgit v1.2.3