kazam 1.13.2

Local infrastructure for coding agents: context, visibility, durable execution. One Rust binary, no cloud.
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;

const GITHUB_REPO: &str = "tdiderich/kazam";
const GITHUB_BRANCH: &str = "main";

const REGISTRY_YAML: &str = r#"
- name: hubspot-icp
  description: "Data-driven ICP from HubSpot deals + Apollo enrichment"
  tags: [gtm, hubspot, apollo, icp]
  path: wishes/hubspot-icp

- name: linear-team-map
  description: "Map people to teams, projects, and ownership from Linear activity"
  tags: [ops, linear, people, ownership]
  path: wishes/linear-team-map

- name: freshness-notifier
  description: "Notify content owners about stale pages via Slack DMs"
  tags: [ops, freshness, slack, notifications]
  path: wishes/freshness-notifier

- name: deal-360
  description: "Per-deal dossier combining CRM data, call intelligence, and Slack activity"
  tags: [gtm, hubspot, attention, slack, deals]
  path: wishes/deal-360
"#;

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct RegistryEntry {
    pub name: String,
    pub description: String,
    pub tags: Vec<String>,
    pub path: String,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct LocalWish {
    pub name: String,
    pub description: String,
    pub tags: Vec<String>,
    #[serde(default)]
    pub env: Vec<String>,
    #[serde(default)]
    pub data_sources: Vec<String>,
}

#[derive(Debug, Deserialize)]
struct GitHubContent {
    pub name: String,
    #[serde(rename = "type")]
    pub content_type: String,
    pub download_url: Option<String>,
}

fn load_registry() -> Result<Vec<RegistryEntry>> {
    serde_yaml::from_str(REGISTRY_YAML).context("failed to parse embedded registry")
}

fn load_local_wishes() -> Result<Vec<LocalWish>> {
    let wishes_dir = PathBuf::from("wishes");
    let mut wishes = Vec::new();

    if !wishes_dir.exists() {
        return Ok(wishes);
    }

    for entry in fs::read_dir(&wishes_dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_dir() {
            let wish_yaml = path.join("wish.yaml");
            if wish_yaml.exists() {
                let contents = fs::read_to_string(&wish_yaml)
                    .with_context(|| format!("failed to read {}", wish_yaml.display()))?;
                match serde_yaml::from_str::<LocalWish>(&contents) {
                    Ok(w) => wishes.push(w),
                    Err(e) => eprintln!("warning: skipping {}: {}", wish_yaml.display(), e),
                }
            }
        }
    }

    Ok(wishes)
}

pub fn list(json: bool) -> Result<()> {
    let local = load_local_wishes()?;
    let registry = load_registry()?;

    let local_names: std::collections::HashSet<&str> =
        local.iter().map(|w| w.name.as_str()).collect();

    let registry_new: Vec<&RegistryEntry> = registry
        .iter()
        .filter(|e| !local_names.contains(e.name.as_str()))
        .collect();

    if json {
        #[derive(Serialize)]
        struct Output<'a> {
            local: &'a Vec<LocalWish>,
            registry: Vec<&'a RegistryEntry>,
        }
        let out = Output {
            local: &local,
            registry: registry_new,
        };
        println!("{}", serde_json::to_string_pretty(&out)?);
        return Ok(());
    }

    if local.is_empty() && registry_new.is_empty() {
        println!("  No wishes available.");
        return Ok(());
    }

    if !local.is_empty() {
        println!("LOCAL");
        for w in &local {
            let tags = w.tags.join(", ");
            println!("  {} — {} [{}]", w.name, w.description, tags);
        }
    }

    if !registry_new.is_empty() {
        println!("REGISTRY");
        for e in registry_new {
            let tags = e.tags.join(", ");
            println!("  {} — {} [{}]", e.name, e.description, tags);
        }
    }

    Ok(())
}

pub fn init(name: &str, dir: Option<PathBuf>, force: bool) -> Result<()> {
    let registry = load_registry()?;

    let entry = registry
        .iter()
        .find(|e| e.name == name)
        .with_context(|| format!("'{}' not found in registry", name))?;

    let dest = match dir {
        Some(d) => d,
        None => PathBuf::from("wishes").join(name),
    };

    if dest.exists() && !force {
        bail!(
            "'{}' already exists at {}. Use --force to overwrite.",
            name,
            dest.display()
        );
    }

    let api_url = format!(
        "https://api.github.com/repos/{}/contents/{}?ref={}",
        GITHUB_REPO, entry.path, GITHUB_BRANCH
    );

    let response = ureq::get(&api_url)
        .set("User-Agent", "kazam")
        .call()
        .with_context(|| format!("failed to fetch registry contents for '{}'", name))?;

    let body = response
        .into_string()
        .context("failed to read GitHub API response body")?;

    let files: Vec<GitHubContent> =
        serde_json::from_str(&body).context("failed to parse GitHub API response")?;

    fs::create_dir_all(&dest)
        .with_context(|| format!("failed to create directory {}", dest.display()))?;

    for file in files {
        if file.content_type != "file" {
            continue;
        }
        let download_url = match file.download_url {
            Some(u) => u,
            None => continue,
        };

        let content = ureq::get(&download_url)
            .set("User-Agent", "kazam")
            .call()
            .with_context(|| format!("failed to download {}", file.name))?
            .into_string()
            .with_context(|| format!("failed to read body for {}", file.name))?;

        let out_path = dest.join(&file.name);
        fs::write(&out_path, &content)
            .with_context(|| format!("failed to write {}", out_path.display()))?;

        println!("  wrote {}", out_path.display());
    }

    println!(
        "\nInstalled '{}' to {}.\nSee {}/README.md to get started.",
        name,
        dest.display(),
        dest.display()
    );

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn registry_parses() {
        let entries = load_registry().unwrap();
        assert!(!entries.is_empty());
        assert!(entries.iter().any(|e| e.name == "hubspot-icp"));
        assert!(entries.iter().any(|e| e.name == "linear-team-map"));
    }

    #[test]
    fn registry_entries_have_required_fields() {
        for entry in load_registry().unwrap() {
            assert!(!entry.name.is_empty());
            assert!(!entry.description.is_empty());
            assert!(!entry.path.is_empty());
            assert!(entry.path.starts_with("wishes/"));
        }
    }

    #[test]
    fn init_rejects_unknown_wish() {
        let result = init("nonexistent-wish", None, false);
        assert!(result.is_err());
        assert!(result
            .unwrap_err()
            .to_string()
            .contains("not found in registry"));
    }
}