aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src/main.rs
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-10-19 16:02:57 +0300
committerIgor Soarez <igor@soarez.org>2026-10-19 16:02:57 +0300
commitab33633ceb9e4bea3e292b2371241797d1587d02 (patch)
tree7b03faf43e5714782e43d582a00626a6f0d852f1 /crates/dinggerd/src/main.rs
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.
Diffstat (limited to 'crates/dinggerd/src/main.rs')
-rw-r--r--crates/dinggerd/src/main.rs100
1 files changed, 100 insertions, 0 deletions
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(())
+}