lifeloop-cli 0.2.0

Provider-neutral lifecycle abstraction and normalizer for AI harnesses
Documentation
//! `lifeloop telemetry snapshot --host <id>`.
//!
//! Reads the per-host telemetry source via the existing
//! `lifeloop::telemetry::*::current()` entry points and emits the
//! resulting `PressureObservation` as JSON. When the source is
//! unavailable (no thread id, no log file, etc.) the command exits
//! validation-class with a structured message rather than printing
//! a misleading partial observation.

use std::path::PathBuf;

use lifeloop::telemetry::PressureObservation;

use super::{CliError, print_json};

pub fn run<I: Iterator<Item = String>>(mut args: I) -> Result<(), CliError> {
    let action = args
        .next()
        .ok_or_else(|| CliError::Usage("telemetry requires a subcommand: snapshot".to_string()))?;
    match action.as_str() {
        "snapshot" => run_snapshot(args),
        other => Err(CliError::Usage(format!(
            "telemetry: unknown subcommand `{other}` (expected: snapshot)"
        ))),
    }
}

fn run_snapshot<I: Iterator<Item = String>>(mut args: I) -> Result<(), CliError> {
    let mut host_id: Option<String> = None;
    let mut repo_root: Option<PathBuf> = None;
    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--host" => {
                host_id = Some(require_value(&arg, args.next())?);
            }
            "--repo-root" => {
                repo_root = Some(PathBuf::from(require_value(&arg, args.next())?));
            }
            other => {
                return Err(CliError::Usage(format!(
                    "telemetry snapshot: unknown flag `{other}`"
                )));
            }
        }
    }
    let host_id = host_id
        .ok_or_else(|| CliError::Usage("telemetry snapshot: --host <id> is required".into()))?;
    let repo_root = repo_root.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());

    let observation: Option<PressureObservation> = match host_id.as_str() {
        "claude" => lifeloop::telemetry::claude::current(&repo_root)
            .map_err(|e| CliError::Validation(format!("telemetry source error: {e}")))?,
        "codex" => lifeloop::telemetry::codex::current()
            .map_err(|e| CliError::Validation(format!("telemetry source error: {e}")))?,
        "gemini" => lifeloop::telemetry::gemini::current(&repo_root)
            .map_err(|e| CliError::Validation(format!("telemetry source error: {e}")))?,
        other => {
            return Err(CliError::Validation(format!(
                "telemetry snapshot: no telemetry reader for host `{other}` (have: claude, codex, gemini)"
            )));
        }
    };

    let observation = observation.ok_or_else(|| {
        CliError::Validation(format!(
            "telemetry snapshot: no observation available for host `{host_id}` (source missing or empty)"
        ))
    })?;
    print_json(&observation)
}

fn require_value(flag: &str, value: Option<String>) -> Result<String, CliError> {
    value.ok_or_else(|| CliError::Usage(format!("flag `{flag}` requires a value")))
}