diff --git a/Cargo.lock b/Cargo.lock index 7a5755242..f7325cf69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "agent-forecast" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "serde", + "serde_json", +] + [[package]] name = "agent-orchestrator" version = "0.1.0" @@ -89,6 +100,19 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anthropic-usage-poll" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "reqwest", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -699,6 +723,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hook-entry" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", +] + [[package]] name = "http" version = "1.4.0" @@ -1473,6 +1506,8 @@ dependencies = [ "rustls", "rustls-pki-types", "rustls-platform-verifier", + "serde", + "serde_json", "sync_wrapper", "tokio", "tokio-rustls", @@ -1843,6 +1878,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "temporal-grounding" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "serde", + "serde_json", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/Cargo.toml b/Cargo.toml index c2caf88d4..83d864228 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,10 @@ members = [ "crates/release-cut", "crates/sbom-gen", "crates/fuzz-setup", + "crates/anthropic-usage-poll", + "crates/agent-forecast", + "crates/temporal-grounding", + "bin/hook-entry", ] [workspace.package] @@ -32,7 +36,7 @@ serde_json = "1.0" tokio = { version = "1.39", features = ["rt-multi-thread", "macros", "fs"] } walkdir = "2.5" thiserror = "2.0" -reqwest = { version = "0.13", features = ["blocking"] } +reqwest = { version = "0.13", features = ["blocking", "json"] } chrono = { version = "0.4", features = ["serde"] } uuid = { version = "1.11", features = ["v4", "serde"] } tracing = "0.1" @@ -41,4 +45,3 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } [profile.release] lto = "thin" codegen-units = 1 - diff --git a/bin/hook-entry/Cargo.toml b/bin/hook-entry/Cargo.toml new file mode 100644 index 000000000..15f133701 --- /dev/null +++ b/bin/hook-entry/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "hook-entry" +description = "Shared PreToolUse hook — injects budget+quota line into Claude Code agent context" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +rust-version.workspace = true + +[[bin]] +name = "hook-entry" +path = "src/main.rs" + +[dependencies] +anyhow = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/bin/hook-entry/src/main.rs b/bin/hook-entry/src/main.rs new file mode 100644 index 000000000..753a30560 --- /dev/null +++ b/bin/hook-entry/src/main.rs @@ -0,0 +1,95 @@ +//! Claude Code PreToolUse hook. +//! Reads the hook event from stdin, writes a budget+quota annotation to stdout. +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::io::Read; +use std::path::PathBuf; + +#[derive(Deserialize)] +struct UsageSnapshot { + daily_remaining: Option, + monthly_remaining: Option, + updated_at: Option, +} + +#[derive(Serialize)] +struct HookOutput { + budget_line: String, +} + +fn main() -> Result<()> { + let mut input = String::new(); + std::io::stdin().read_to_string(&mut input)?; + + // Parse but don't fail on bad input — hooks must not block agents + let _event: Value = serde_json::from_str(&input).unwrap_or_default(); + + let usage = read_usage(); + let budget_line = format!( + "[observability] daily_remaining={} monthly_remaining={} | {} | {} | updated={}", + fmt_opt(usage.as_ref().and_then(|u| u.daily_remaining)), + fmt_opt(usage.as_ref().and_then(|u| u.monthly_remaining)), + read_forecast_hint(), + read_elapsed_hint(), + usage + .as_ref() + .and_then(|u| u.updated_at.as_deref()) + .unwrap_or("unknown"), + ); + + println!("{}", serde_json::to_string(&HookOutput { budget_line })?); + Ok(()) +} + +fn fmt_opt(v: Option) -> String { + v.map_or_else(|| String::from("?"), |n| n.to_string()) +} + +fn claude_dir() -> PathBuf { + std::env::var("HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(".")) + .join(".claude") +} + +fn read_usage() -> Option { + let path = claude_dir().join("usage.json"); + let text = std::fs::read_to_string(path).ok()?; + serde_json::from_str(&text).ok() +} + +fn read_forecast_hint() -> String { + // TODO: invoke agent-forecast binary for current prompt category + String::from("forecast=p50:? p90:?") +} + +fn read_elapsed_hint() -> String { + // TODO: read active-agents.json, compute elapsed for current session + String::from("elapsed=?") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fmt_opt_some() { + assert_eq!(fmt_opt(Some(42_000)), "42000"); + } + + #[test] + fn fmt_opt_none() { + assert_eq!(fmt_opt(None), "?"); + } + + #[test] + fn hook_output_serializes() { + let o = HookOutput { + budget_line: "test line".to_string(), + }; + let json = serde_json::to_string(&o).unwrap(); + assert!(json.contains("budget_line")); + assert!(json.contains("test line")); + } +} diff --git a/crates/agent-forecast/Cargo.toml b/crates/agent-forecast/Cargo.toml new file mode 100644 index 000000000..0e30dd5ed --- /dev/null +++ b/crates/agent-forecast/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "agent-forecast" +description = "Per-category p50/p90 token budget forecasting from agent history" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +rust-version.workspace = true + +[[bin]] +name = "agent-forecast" +path = "src/main.rs" + +[dependencies] +clap = { workspace = true } +anyhow = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +chrono = { workspace = true } diff --git a/crates/agent-forecast/README.md b/crates/agent-forecast/README.md new file mode 100644 index 000000000..7e5c29c28 --- /dev/null +++ b/crates/agent-forecast/README.md @@ -0,0 +1,17 @@ +# agent-forecast + +Reads `~/.claude/agent-history.jsonl` and computes p50/p90 token priors per category. + +Categories: `sweep audit refactor dependabot scaffold extract merge docs test eval fork cleanup` + +## Usage + +```bash +agent-forecast categorize "refactor the auth module" +# → refactor + +agent-forecast budget refactor +# → {"category":"refactor","p50_tokens":0,"p90_tokens":0,"sample_count":0} +``` + +History JSONL fields: `timestamp`, `prompt_hash_category`, `tool_uses`, `duration_ms`, `total_tokens`, `outcome`. diff --git a/crates/agent-forecast/src/main.rs b/crates/agent-forecast/src/main.rs new file mode 100644 index 000000000..66e45e937 --- /dev/null +++ b/crates/agent-forecast/src/main.rs @@ -0,0 +1,170 @@ +use anyhow::Result; +use clap::{Parser, Subcommand}; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; + +const CATEGORIES: &[&str] = &[ + "sweep", + "audit", + "refactor", + "dependabot", + "scaffold", + "extract", + "merge", + "docs", + "test", + "eval", + "fork", + "cleanup", +]; + +#[derive(Parser)] +#[command( + name = "agent-forecast", + about = "Forecast token budgets per agent category" +)] +struct Cli { + #[command(subcommand)] + command: Command, + /// Path to agent history JSONL (default: ~/.claude/agent-history.jsonl) + #[arg(long, global = true)] + history: Option, +} + +#[derive(Subcommand)] +enum Command { + /// Classify a prompt to one of the 12 categories by keyword matching + Categorize { prompt: String }, + /// Print p50/p90 token forecast for a category + Budget { category: String }, +} + +#[derive(Deserialize)] +struct HistoryEntry { + prompt_hash_category: String, + total_tokens: u64, +} + +#[derive(Serialize)] +struct Forecast { + category: String, + p50_tokens: u64, + p90_tokens: u64, + sample_count: usize, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + let history_path = cli.history.unwrap_or_else(|| { + std::env::var("HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(".")) + .join(".claude") + .join("agent-history.jsonl") + }); + + match cli.command { + Command::Categorize { prompt } => println!("{}", categorize(&prompt)), + Command::Budget { category } => { + let tokens = load_history(&history_path, &category)?; + let f = compute_forecast(&category, &tokens); + println!("{}", serde_json::to_string_pretty(&f)?); + } + } + Ok(()) +} + +fn categorize(prompt: &str) -> &'static str { + let lower = prompt.to_lowercase(); + for &cat in CATEGORIES { + if lower.contains(cat) { + return cat; + } + } + // keyword heuristics for prompts that don't mention the category literally + if lower.contains("depend") || lower.contains("bump") || lower.contains("upgrade") { + return "dependabot"; + } + if lower.contains("rename") || lower.contains("restructure") || lower.contains("reorganize") { + return "refactor"; + } + if lower.contains("spec") || lower.contains("assert") || lower.contains("coverage") { + return "test"; + } + if lower.contains("readme") || lower.contains("comment") || lower.contains("document") { + return "docs"; + } + "sweep" +} + +fn load_history(path: &Path, category: &str) -> Result> { + if !path.exists() { + return Ok(vec![]); + } + let text = std::fs::read_to_string(path)?; + let tokens = text + .lines() + .filter(|l| !l.trim().is_empty()) + .filter_map(|l| serde_json::from_str::(l).ok()) + .filter(|e| e.prompt_hash_category.contains(category)) + .map(|e| e.total_tokens) + .collect(); + Ok(tokens) +} + +fn compute_forecast(category: &str, tokens: &[u64]) -> Forecast { + if tokens.is_empty() { + return Forecast { + category: category.to_string(), + p50_tokens: 0, + p90_tokens: 0, + sample_count: 0, + }; + } + let mut sorted = tokens.to_vec(); + sorted.sort_unstable(); + Forecast { + category: category.to_string(), + p50_tokens: percentile(&sorted, 50), + p90_tokens: percentile(&sorted, 90), + sample_count: tokens.len(), + } +} + +fn percentile(sorted: &[u64], pct: usize) -> u64 { + sorted[(sorted.len() * pct / 100).min(sorted.len() - 1)] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn categorize_exact_match() { + assert_eq!(categorize("sweep the codebase for dead code"), "sweep"); + assert_eq!(categorize("run test suite"), "test"); + } + + #[test] + fn categorize_heuristic() { + assert_eq!( + categorize("upgrade all packages to new versions"), + "dependabot" + ); + assert_eq!(categorize("update readme with new examples"), "docs"); + } + + #[test] + fn forecast_empty_history() { + let f = compute_forecast("sweep", &[]); + assert_eq!(f.p50_tokens, 0); + assert_eq!(f.sample_count, 0); + } + + #[test] + fn percentile_basic() { + let data = vec![100u64, 200, 300, 400, 500]; + assert_eq!(percentile(&data, 50), 300); + assert_eq!(percentile(&data, 90), 500); + } +} diff --git a/crates/agent-orchestrator/src/lib.rs b/crates/agent-orchestrator/src/lib.rs index 590fbdc25..490581979 100644 --- a/crates/agent-orchestrator/src/lib.rs +++ b/crates/agent-orchestrator/src/lib.rs @@ -3,7 +3,7 @@ use glob::glob; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::fs; -use std::path::PathBuf; +use std::path::Path; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Lane { @@ -38,18 +38,16 @@ pub struct TrackerState { } impl OrchestrationConfig { - pub fn from_file(path: &PathBuf) -> Result { + pub fn from_file(path: &Path) -> Result { let content = fs::read_to_string(path) .map_err(|e| anyhow!("Failed to read orchestration.toml: {}", e))?; - toml::from_str(&content) - .map_err(|e| anyhow!("Failed to parse orchestration.toml: {}", e)) + toml::from_str(&content).map_err(|e| anyhow!("Failed to parse orchestration.toml: {}", e)) } - pub fn to_file(&self, path: &PathBuf) -> Result<()> { + pub fn to_file(&self, path: &Path) -> Result<()> { let content = toml::to_string_pretty(self) .map_err(|e| anyhow!("Failed to serialize config: {}", e))?; - fs::write(path, content) - .map_err(|e| anyhow!("Failed to write orchestration.toml: {}", e)) + fs::write(path, content).map_err(|e| anyhow!("Failed to write orchestration.toml: {}", e)) } pub fn validate_non_overlapping(&self) -> Result<()> { @@ -62,9 +60,7 @@ impl OrchestrationConfig { for entry in expanded { let path = entry.map_err(|e| anyhow!("Glob expansion error: {}", e))?; - let path_str = path - .to_string_lossy() - .to_string(); + let path_str = path.to_string_lossy().to_string(); if let Some(existing_lane) = seen_files.get(&path_str) { return Err(anyhow!( @@ -102,6 +98,12 @@ impl OrchestrationConfig { } } +impl Default for TrackerState { + fn default() -> Self { + Self::new() + } +} + impl TrackerState { pub fn new() -> Self { TrackerState { @@ -110,21 +112,19 @@ impl TrackerState { } } - pub fn from_file(path: &PathBuf) -> Result { + pub fn from_file(path: &Path) -> Result { if !path.exists() { return Ok(Self::new()); } - let content = fs::read_to_string(path) - .map_err(|e| anyhow!("Failed to read tracker state: {}", e))?; - serde_json::from_str(&content) - .map_err(|e| anyhow!("Failed to parse tracker state: {}", e)) + let content = + fs::read_to_string(path).map_err(|e| anyhow!("Failed to read tracker state: {}", e))?; + serde_json::from_str(&content).map_err(|e| anyhow!("Failed to parse tracker state: {}", e)) } - pub fn to_file(&self, path: &PathBuf) -> Result<()> { + pub fn to_file(&self, path: &Path) -> Result<()> { let content = serde_json::to_string_pretty(self) .map_err(|e| anyhow!("Failed to serialize tracker state: {}", e))?; - fs::write(path, content) - .map_err(|e| anyhow!("Failed to write tracker state: {}", e)) + fs::write(path, content).map_err(|e| anyhow!("Failed to write tracker state: {}", e)) } pub fn update_lane(&mut self, lane_id: String, in_flight: bool) { @@ -234,8 +234,7 @@ mod tests { state.update_lane("lane1".to_string(), true); let json = serde_json::to_string(&state).expect("Should serialize"); - let deserialized: TrackerState = - serde_json::from_str(&json).expect("Should deserialize"); + let deserialized: TrackerState = serde_json::from_str(&json).expect("Should deserialize"); assert_eq!(deserialized.lanes.len(), 1); assert!(deserialized.lanes["lane1"].in_flight); diff --git a/crates/agent-orchestrator/src/main.rs b/crates/agent-orchestrator/src/main.rs index 4121b9e9b..5c94a91d0 100644 --- a/crates/agent-orchestrator/src/main.rs +++ b/crates/agent-orchestrator/src/main.rs @@ -3,8 +3,7 @@ use clap::{Parser, Subcommand}; use std::path::PathBuf; use tracing::info; -mod lib; -use lib::{OrchestrationConfig, TrackerState}; +use agent_orchestrator::{OrchestrationConfig, TrackerState}; #[derive(Parser)] #[command(name = "agent-orchestrator")] @@ -129,7 +128,7 @@ fn cmd_lanes_dispatch(config: &OrchestrationConfig, lane_id: &str) -> Result<()> Ok(()) } -fn cmd_lanes_status(config: &OrchestrationConfig, state_file: &std::path::PathBuf) -> Result<()> { +fn cmd_lanes_status(config: &OrchestrationConfig, state_file: &std::path::Path) -> Result<()> { let state = TrackerState::from_file(state_file)?; println!("Lane Status Report\n"); diff --git a/crates/anthropic-usage-poll/Cargo.toml b/crates/anthropic-usage-poll/Cargo.toml new file mode 100644 index 000000000..a3ea42b96 --- /dev/null +++ b/crates/anthropic-usage-poll/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "anthropic-usage-poll" +description = "Daemon polling Anthropic Admin API for token usage data" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +rust-version.workspace = true + +[[bin]] +name = "anthropic-usage-poll" +path = "src/main.rs" + +[dependencies] +clap = { workspace = true } +anyhow = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +reqwest = { workspace = true } +chrono = { workspace = true } diff --git a/crates/anthropic-usage-poll/README.md b/crates/anthropic-usage-poll/README.md new file mode 100644 index 000000000..5d5047c75 --- /dev/null +++ b/crates/anthropic-usage-poll/README.md @@ -0,0 +1,15 @@ +# anthropic-usage-poll + +Daemon polling the Anthropic Admin API for organization token usage. + +Writes `~/.claude/usage.json` with fields: `daily_remaining`, `monthly_remaining`, +`per_model_tokens_last_24h`, `updated_at`. + +## Usage + +```bash +ANTHROPIC_ADMIN_KEY=sk-... anthropic-usage-poll --once +anthropos-usage-poll --interval 300 +``` + +Polls `GET /v1/organizations/usage_report/messages` every `--interval` seconds (default 600). diff --git a/crates/anthropic-usage-poll/src/main.rs b/crates/anthropic-usage-poll/src/main.rs new file mode 100644 index 000000000..2b7433bd3 --- /dev/null +++ b/crates/anthropic-usage-poll/src/main.rs @@ -0,0 +1,106 @@ +use anyhow::Result; +use chrono::Utc; +use clap::Parser; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use tokio::time; + +#[derive(Parser)] +#[command( + name = "anthropic-usage-poll", + about = "Poll Anthropic Admin API for token usage" +)] +struct Cli { + /// Run once and exit + #[arg(long)] + once: bool, + /// Polling interval in seconds + #[arg(long, default_value = "600")] + interval: u64, + /// Output path (default: ~/.claude/usage.json) + #[arg(long)] + output: Option, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +struct UsageSnapshot { + daily_remaining: Option, + monthly_remaining: Option, + per_model_tokens_last_24h: serde_json::Value, + updated_at: String, +} + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::parse(); + let output = cli.output.unwrap_or_else(default_output_path); + if cli.once { + poll_and_write(&output).await?; + } else { + let mut ticker = time::interval(Duration::from_secs(cli.interval)); + loop { + ticker.tick().await; + if let Err(e) = poll_and_write(&output).await { + eprintln!("poll error: {e}"); + } + } + } + Ok(()) +} + +fn default_output_path() -> PathBuf { + std::env::var("HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(".")) + .join(".claude") + .join("usage.json") +} + +async fn poll_and_write(output: &Path) -> Result<()> { + let snapshot = fetch_usage().await?; + write_atomic(output, &snapshot) +} + +async fn fetch_usage() -> Result { + // TODO: read ANTHROPIC_ADMIN_KEY from env; skip poll if unset + let _api_key = std::env::var("ANTHROPIC_ADMIN_KEY").unwrap_or_default(); + // TODO: add If-None-Match / ETag header to avoid redundant writes + // TODO: GET /v1/organizations/usage_report/messages with bearer auth + // TODO: map response body fields into UsageSnapshot + let _client = reqwest::Client::new(); + Ok(UsageSnapshot { + daily_remaining: None, + monthly_remaining: None, + per_model_tokens_last_24h: serde_json::json!({}), + updated_at: Utc::now().to_rfc3339(), + }) +} + +fn write_atomic(path: &Path, snapshot: &UsageSnapshot) -> Result<()> { + // TODO: write to a sibling temp file then fs::rename for atomicity + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let json = serde_json::to_string_pretty(snapshot)?; + std::fs::write(path, json)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_snapshot_serializes() { + let s = UsageSnapshot::default(); + let json = serde_json::to_string(&s).expect("serialize"); + assert!(json.contains("updated_at")); + } + + #[test] + fn default_output_path_contains_claude() { + let p = default_output_path(); + assert!(p.to_string_lossy().contains(".claude")); + } +} diff --git a/crates/audit-privacy/src/main.rs b/crates/audit-privacy/src/main.rs index 6da04ace5..c1cc7ea1a 100644 --- a/crates/audit-privacy/src/main.rs +++ b/crates/audit-privacy/src/main.rs @@ -5,7 +5,8 @@ use std::path::Path; use walkdir::WalkDir; fn main() -> Result<()> { - let root = Path::new("/Users/kooshapari/CodeProjects/Phenotype/repos/FocalPoint/apps/ios/FocalPoint"); + let root = + Path::new("/Users/kooshapari/CodeProjects/Phenotype/repos/FocalPoint/apps/ios/FocalPoint"); let privacy_manifest = root.join("Resources/PrivacyInfo.xcprivacy"); let info_plist = root.join("Sources/FocalPointApp/Info.plist"); @@ -40,7 +41,7 @@ fn main() -> Result<()> { if let Some(frameworks) = api_mappings.get(api.as_str()) { let mut found = false; for fw in frameworks { - if actual_imports.contains(&fw.to_string()) { + if actual_imports.contains(*fw) { found = true; break; } @@ -101,7 +102,9 @@ fn parse_privacy_manifest(path: &Path) -> Result> { if let Some(plist::Value::Array(api_array)) = dict.get(api_array_key) { for api_entry in api_array { if let plist::Value::Dictionary(api_dict) = api_entry { - if let Some(plist::Value::String(api_type)) = api_dict.get("NSPrivacyAccessedAPIType") { + if let Some(plist::Value::String(api_type)) = + api_dict.get("NSPrivacyAccessedAPIType") + { apis.insert(api_type.clone()); } } @@ -134,7 +137,7 @@ fn scan_framework_imports(sources_dir: &Path) -> Result> { for entry in WalkDir::new(sources_dir) .into_iter() .filter_map(|e| e.ok()) - .filter(|e| e.path().extension().map_or(false, |ext| ext == "swift")) + .filter(|e| e.path().extension().is_some_and(|ext| ext == "swift")) { let content = fs::read_to_string(entry.path())?; for line in content.lines() { @@ -142,7 +145,7 @@ fn scan_framework_imports(sources_dir: &Path) -> Result> { if trimmed.starts_with("import ") && !trimmed.starts_with("import Sentry") { let import_name = trimmed.strip_prefix("import ").unwrap_or("").trim(); // Filter framework names (capitalized, not module-level) - if import_name.chars().next().map_or(false, |c| c.is_uppercase()) { + if import_name.chars().next().is_some_and(|c| c.is_uppercase()) { imports.insert(import_name.to_string()); } } @@ -158,8 +161,14 @@ fn get_api_mappings() -> std::collections::HashMap<&'static str, Vec<&'static st // UserDefaults is a Foundation type, typically not explicitly imported map.insert("NSPrivacyAccessedAPITypeUserDefaults", vec!["Foundation"]); // FileManager is a Foundation type, typically not explicitly imported - map.insert("NSPrivacyAccessedAPITypeFileTimestampApis", vec!["Foundation"]); - map.insert("NSPrivacyAccessedAPITypeUserNotificationCenter", vec!["UserNotifications"]); + map.insert( + "NSPrivacyAccessedAPITypeFileTimestampApis", + vec!["Foundation"], + ); + map.insert( + "NSPrivacyAccessedAPITypeUserNotificationCenter", + vec!["UserNotifications"], + ); map.insert("NSPrivacyAccessedAPITypeHealthKitApis", vec!["HealthKit"]); map } diff --git a/crates/bench-guard/src/main.rs b/crates/bench-guard/src/main.rs index c043bc99f..831b1029a 100644 --- a/crates/bench-guard/src/main.rs +++ b/crates/bench-guard/src/main.rs @@ -45,7 +45,10 @@ struct BenchBaseline { struct BenchEntry { #[serde(rename = "mean_nanos")] mean_nanos: u64, - #[serde(rename = "histogram_buckets_nanos", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "histogram_buckets_nanos", + skip_serializing_if = "Option::is_none" + )] histogram_buckets_nanos: Option>, } @@ -77,7 +80,7 @@ fn parse_bench_output(output: &str) -> Result> { .ok_or_else(|| anyhow!("Missing estimates for {}", name))?; let mean_nanos = estimates - .get(0) + .first() .and_then(|v| v.as_u64()) .ok_or_else(|| anyhow!("Missing mean for {}", name))?; @@ -109,7 +112,7 @@ fn parse_bench_output(output: &str) -> Result> { fn run_benches() -> Result> { let output = Command::new("cargo") - .args(&["bench", "--workspace", "--message-format=json"]) + .args(["bench", "--workspace", "--message-format=json"]) .stdout(Stdio::piped()) .stderr(Stdio::null()) .output()?; @@ -124,42 +127,69 @@ fn load_baseline(path: &str) -> Result { return Ok(BenchBaseline { tolerance_percent: 30, benches: [ - ("ir_hash/small".to_string(), BenchEntry { - mean_nanos: 1_000_000, // 1ms - histogram_buckets_nanos: None, - }), - ("ir_hash/large".to_string(), BenchEntry { - mean_nanos: 10_000_000, // 10ms - histogram_buckets_nanos: None, - }), - ("eval_tick".to_string(), BenchEntry { - mean_nanos: 5_000_000, // 5ms - histogram_buckets_nanos: None, - }), - ("audit_verify/1k_tail".to_string(), BenchEntry { - mean_nanos: 10_000_000, // 10ms - histogram_buckets_nanos: None, - }), - ("starlark_compile/small".to_string(), BenchEntry { - mean_nanos: 50_000_000, // 50ms - histogram_buckets_nanos: None, - }), - ("starlark_compile/large".to_string(), BenchEntry { - mean_nanos: 500_000_000, // 500ms - histogram_buckets_nanos: None, - }), - ("scheduler_packing/small".to_string(), BenchEntry { - mean_nanos: 240_000, // 240µs - histogram_buckets_nanos: None, - }), - ("scheduler_packing/medium".to_string(), BenchEntry { - mean_nanos: 940_000, // 940µs - histogram_buckets_nanos: None, - }), - ("scheduler_packing/large".to_string(), BenchEntry { - mean_nanos: 1_400_000, // 1.4ms - histogram_buckets_nanos: None, - }), + ( + "ir_hash/small".to_string(), + BenchEntry { + mean_nanos: 1_000_000, // 1ms + histogram_buckets_nanos: None, + }, + ), + ( + "ir_hash/large".to_string(), + BenchEntry { + mean_nanos: 10_000_000, // 10ms + histogram_buckets_nanos: None, + }, + ), + ( + "eval_tick".to_string(), + BenchEntry { + mean_nanos: 5_000_000, // 5ms + histogram_buckets_nanos: None, + }, + ), + ( + "audit_verify/1k_tail".to_string(), + BenchEntry { + mean_nanos: 10_000_000, // 10ms + histogram_buckets_nanos: None, + }, + ), + ( + "starlark_compile/small".to_string(), + BenchEntry { + mean_nanos: 50_000_000, // 50ms + histogram_buckets_nanos: None, + }, + ), + ( + "starlark_compile/large".to_string(), + BenchEntry { + mean_nanos: 500_000_000, // 500ms + histogram_buckets_nanos: None, + }, + ), + ( + "scheduler_packing/small".to_string(), + BenchEntry { + mean_nanos: 240_000, // 240µs + histogram_buckets_nanos: None, + }, + ), + ( + "scheduler_packing/medium".to_string(), + BenchEntry { + mean_nanos: 940_000, // 940µs + histogram_buckets_nanos: None, + }, + ), + ( + "scheduler_packing/large".to_string(), + BenchEntry { + mean_nanos: 1_400_000, // 1.4ms + histogram_buckets_nanos: None, + }, + ), ] .iter() .cloned() @@ -209,13 +239,18 @@ fn check_regressions( fn format_text(results: &[BenchResult], _baseline: &BenchBaseline) -> String { let mut output = String::from("Benchmark Results:\n"); for result in results { - output.push_str(&format!(" {}: {:.2}ms\n", result.name, result.mean_nanos as f64 / 1_000_000.0)); + output.push_str(&format!( + " {}: {:.2}ms\n", + result.name, + result.mean_nanos as f64 / 1_000_000.0 + )); } output } fn format_markdown(results: &[BenchResult], baseline: &BenchBaseline) -> String { - let mut output = String::from("| Benchmark | Baseline (ns) | Current (ns) | Change | Status |\n"); + let mut output = + String::from("| Benchmark | Baseline (ns) | Current (ns) | Change | Status |\n"); output.push_str("|-----------|--------------|-------------|--------|--------|\n"); let tolerance_factor = 1.0 + (baseline.tolerance_percent as f64 / 100.0); @@ -247,8 +282,9 @@ fn format_histogram(results: &[BenchResult]) -> String { for (i, bucket) in result.histogram.iter().enumerate() { let bar_width = (*bucket / 100_000).min(50) as usize; // Max 50 chars let bar = "█".repeat(bar_width); - output.push_str(&format!(" [{}ns-{}ns] {}\n", - if i == 0 { 0 } else { result.histogram[i-1] }, + output.push_str(&format!( + " [{}ns-{}ns] {}\n", + if i == 0 { 0 } else { result.histogram[i - 1] }, bucket, bar )); diff --git a/crates/commit-msg-check/src/main.rs b/crates/commit-msg-check/src/main.rs index 366f5a1a5..21a1978a3 100644 --- a/crates/commit-msg-check/src/main.rs +++ b/crates/commit-msg-check/src/main.rs @@ -27,7 +27,7 @@ fn main() -> Result<()> { validate_conventional_commit(first_line)?; // Check for DCO sign-off - validate_dco_signoff(&message)?; + validate_dco_signoff(message)?; Ok(()) } @@ -57,9 +57,9 @@ fn validate_conventional_commit(first_line: &str) -> Result<()> { fn validate_dco_signoff(message: &str) -> Result<()> { // Look for "Signed-off-by:" line with email - let has_dco = message - .lines() - .any(|line| line.trim().starts_with("Signed-off-by:") && line.contains('<') && line.contains('>')); + let has_dco = message.lines().any(|line| { + line.trim().starts_with("Signed-off-by:") && line.contains('<') && line.contains('>') + }); if !has_dco { return Err(anyhow!( diff --git a/crates/doc-link-check/src/main.rs b/crates/doc-link-check/src/main.rs index bb4b4cfe6..f65aba286 100644 --- a/crates/doc-link-check/src/main.rs +++ b/crates/doc-link-check/src/main.rs @@ -29,7 +29,11 @@ fn main() -> Result<()> { for entry in WalkDir::new(&docs_root) .into_iter() .filter_map(|e| e.ok()) - .filter(|e| !e.path().components().any(|c| c.as_os_str() == "node_modules")) + .filter(|e| { + !e.path() + .components() + .any(|c| c.as_os_str() == "node_modules") + }) .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("md")) { let file_path = entry.path().to_path_buf(); @@ -50,7 +54,7 @@ fn main() -> Result<()> { line_num += text.matches('\n').count(); } - if let Event::Start(pulldown_cmark::Tag::Link(_, url, _)) = &event { + if let Event::Start(pulldown_cmark::Tag::Link { dest_url: url, .. }) = &event { let url_str = url.to_string(); total_links += 1; @@ -63,7 +67,10 @@ fn main() -> Result<()> { } // Check relative links - let mut target_path = file_path.parent().unwrap_or_else(|| Path::new(".")).to_path_buf(); + let mut target_path = file_path + .parent() + .unwrap_or_else(|| Path::new(".")) + .to_path_buf(); let link_without_anchor = url_str.split('#').next().unwrap_or(&url_str); if !link_without_anchor.is_empty() && link_without_anchor != "/" { @@ -100,7 +107,7 @@ fn main() -> Result<()> { let rel_path = file_path.strip_prefix(&docs_root).unwrap_or(&file_path); broken_links .entry(url_str.clone()) - .or_insert_with(Vec::new) + .or_default() .push(LinkRef { path: rel_path.display().to_string(), line: line_num, diff --git a/crates/fr-coverage/src/main.rs b/crates/fr-coverage/src/main.rs index a2f981fb7..c3b7f7ef9 100644 --- a/crates/fr-coverage/src/main.rs +++ b/crates/fr-coverage/src/main.rs @@ -1,9 +1,9 @@ +use anyhow::{anyhow, Result}; +use regex::Regex; use std::collections::BTreeMap; use std::fs; -use std::path::PathBuf; -use regex::Regex; +use std::path::{Path, PathBuf}; use walkdir::WalkDir; -use anyhow::{anyhow, Result}; fn main() -> Result<()> { let repo_root = find_repo_root()?; @@ -20,7 +20,8 @@ fn main() -> Result<()> { // Build matrix let covered = traces.len(); - let missing: Vec<_> = frs.iter() + let missing: Vec<_> = frs + .iter() .filter(|(fr_id, _)| !traces.contains_key(*fr_id)) .map(|(id, desc)| (id.clone(), desc.clone())) .collect(); @@ -78,7 +79,7 @@ fn find_repo_root() -> Result { } } -fn parse_functional_requirements(repo_root: &PathBuf) -> Result> { +fn parse_functional_requirements(repo_root: &Path) -> Result> { let fr_path = repo_root.join("FUNCTIONAL_REQUIREMENTS.md"); let content = fs::read_to_string(&fr_path)?; @@ -97,7 +98,7 @@ fn parse_functional_requirements(repo_root: &PathBuf) -> Result, traces: &mut BTreeMap>, orphan_tests: &mut Vec<(String, Vec)>, @@ -108,7 +109,7 @@ fn scan_crate_traces( for entry in WalkDir::new(&crates_path) .into_iter() .filter_map(|e| e.ok()) - .filter(|e| e.path().extension().map_or(false, |ext| ext == "rs")) + .filter(|e| e.path().extension().is_some_and(|ext| ext == "rs")) { let content = match fs::read_to_string(entry.path()) { Ok(c) => c, @@ -125,8 +126,7 @@ fn scan_crate_traces( if !frs.contains_key(&fr_id) { orphan_tests.push((file_display.clone(), vec![fr_id.clone()])); } else { - traces.entry(fr_id).or_insert_with(Vec::new) - .push(file_display.clone()); + traces.entry(fr_id).or_default().push(file_display.clone()); } } } @@ -136,7 +136,7 @@ fn scan_crate_traces( } fn scan_swift_traces( - repo_root: &PathBuf, + repo_root: &Path, frs: &BTreeMap, traces: &mut BTreeMap>, orphan_tests: &mut Vec<(String, Vec)>, @@ -151,7 +151,7 @@ fn scan_swift_traces( for entry in WalkDir::new(&apps_path) .into_iter() .filter_map(|e| e.ok()) - .filter(|e| e.path().extension().map_or(false, |ext| ext == "swift")) + .filter(|e| e.path().extension().is_some_and(|ext| ext == "swift")) { let content = match fs::read_to_string(entry.path()) { Ok(c) => c, @@ -168,8 +168,7 @@ fn scan_swift_traces( if !frs.contains_key(&fr_id) { orphan_tests.push((file_display.clone(), vec![fr_id.clone()])); } else { - traces.entry(fr_id).or_insert_with(|| Vec::new()) - .push(file_display.clone()); + traces.entry(fr_id).or_default().push(file_display.clone()); } } } @@ -190,7 +189,7 @@ fn generate_matrix( let total = frs.len(); let covered = traces.len(); let missing = total - covered; - output.push_str(&format!("## Summary\n\n")); + output.push_str("## Summary\n\n"); output.push_str(&format!("- **Total FRs:** {}\n", total)); output.push_str(&format!("- **Covered (≥1 test):** {}\n", covered)); output.push_str(&format!("- **Missing (0 tests):** {}\n", missing)); @@ -203,13 +202,14 @@ fn generate_matrix( for (fr_id, desc) in frs { let status_and_files = if let Some(files) = traces.get(fr_id) { - let file_links = files.iter() - .map(|f| format!("`{}`", f.split('/').last().unwrap_or(f))) + let file_links = files + .iter() + .map(|f| format!("`{}`", f.split('/').next_back().unwrap_or(f))) .collect::>() .join(", "); - (format!("✅ GREEN"), file_links) + (String::from("✅ GREEN"), file_links) } else { - (format!("❌ MISSING"), String::new()) + (String::from("❌ MISSING"), String::new()) }; output.push_str(&format!( diff --git a/crates/legacy-scan/src/main.rs b/crates/legacy-scan/src/main.rs index be1033b72..f54bf7451 100644 --- a/crates/legacy-scan/src/main.rs +++ b/crates/legacy-scan/src/main.rs @@ -59,7 +59,11 @@ fn main() -> Result<()> { // Source reference: repos/docs/governance/scripting_policy.md rubric. } - let report = Report { root: cli.path.clone(), scanned_files: scanned, findings: vec![] }; + let report = Report { + root: cli.path.clone(), + scanned_files: scanned, + findings: vec![], + }; if cli.json { println!("{}", serde_json::to_string_pretty(&report)?); } diff --git a/crates/quality-gate/src/main.rs b/crates/quality-gate/src/main.rs index 2f9cd673a..3b8764e79 100644 --- a/crates/quality-gate/src/main.rs +++ b/crates/quality-gate/src/main.rs @@ -66,7 +66,12 @@ async fn main() -> Result<()> { // TODO: capture stderr tails, aggregate pass/fail. // Source reference: repos/AgilePlus/scripts/quality-gate.sh. let steps = vec![ - StepResult { name: "fmt", skipped: cli.skip_fmt, passed: true, stderr_tail: String::new() }, + StepResult { + name: "fmt", + skipped: cli.skip_fmt, + passed: true, + stderr_tail: String::new(), + }, StepResult { name: "clippy", skipped: cli.skip_clippy, @@ -81,7 +86,11 @@ async fn main() -> Result<()> { }, ]; let all_passed = steps.iter().all(|s| s.skipped || s.passed); - let report = Report { root: cli.path.clone(), all_passed, steps }; + let report = Report { + root: cli.path.clone(), + all_passed, + steps, + }; if cli.json { println!("{}", serde_json::to_string_pretty(&report)?); diff --git a/crates/release-cut/src/executor.rs b/crates/release-cut/src/executor.rs index de67c18f1..0353aac8b 100644 --- a/crates/release-cut/src/executor.rs +++ b/crates/release-cut/src/executor.rs @@ -51,7 +51,7 @@ impl Executor { // 1. Delete local tag println!(" Deleting local tag: {}", tag); let output = Command::new("git") - .args(&["tag", "-d", &tag]) + .args(["tag", "-d", &tag]) .current_dir(&self.repo_root) .output()?; @@ -62,7 +62,7 @@ impl Executor { // 2. Delete remote tag println!(" Deleting remote tag: {}", tag); let output = Command::new("git") - .args(&["push", "origin", &format!(":{}", tag)]) + .args(["push", "origin", &format!(":{}", tag)]) .current_dir(&self.repo_root) .output()?; @@ -73,7 +73,7 @@ impl Executor { // 3. Reset Cargo.toml to previous version println!(" Resetting Cargo.toml to previous version"); let output = Command::new("git") - .args(&["checkout", "HEAD~1", "Cargo.toml"]) + .args(["checkout", "HEAD~1", "Cargo.toml"]) .current_dir(&self.repo_root) .output()?; @@ -84,7 +84,7 @@ impl Executor { // 4. Reset iOS plist println!(" Resetting iOS Info.plist to previous version"); let _ = Command::new("git") - .args(&[ + .args([ "checkout", "HEAD~1", "apps/ios/FocalPoint/Sources/FocalPointApp/Info.plist", @@ -95,18 +95,18 @@ impl Executor { // 5. Commit rollback let msg = format!("chore(release): rollback {}", version); Command::new("git") - .args(&["add", "-A"]) + .args(["add", "-A"]) .current_dir(&self.repo_root) .output()?; Command::new("git") - .args(&["commit", "-m", &msg]) + .args(["commit", "-m", &msg]) .current_dir(&self.repo_root) .output()?; // 6. Push rollback commit Command::new("git") - .args(&["push", "origin", "main"]) + .args(["push", "origin", "main"]) .current_dir(&self.repo_root) .output()?; @@ -121,7 +121,7 @@ impl Executor { let content = std::fs::read_to_string(&cargo_path)?; let new_content = content.replace( - &format!(r#"version = "0.0.6""#), + r#"version = "0.0.6""#, &format!(r#"version = "{}""#, version), ); @@ -146,10 +146,7 @@ impl Executor { let content = std::fs::read_to_string(&plist_path)?; // Simple string replacement for version in plist - let new_version_str = format!( - "{}.{}.{}", - version.major, version.minor, version.patch - ); + let new_version_str = format!("{}.{}.{}", version.major, version.minor, version.patch); let new_content = regex::Regex::new(r"\d+\.\d+\.\d+")? .replace_all(&content, format!("{}", new_version_str)) .to_string(); @@ -162,16 +159,14 @@ impl Executor { println!(" Generating CHANGELOG section via 'focus release-notes'"); let output = Command::new("cargo") - .args(&["run", "-p", "focus-cli", "--", "release-notes", "generate"]) - .arg(format!("--since=v0.0.6")) + .args(["run", "-p", "focus-cli", "--", "release-notes", "generate"]) + .arg("--since=v0.0.6") .arg("--format=md") .current_dir(&self.repo_root) .output()?; if !output.status.success() { - eprintln!( - " Warning: release-notes generation failed; proceeding with manual entry" - ); + eprintln!(" Warning: release-notes generation failed; proceeding with manual entry"); return Ok(()); } @@ -197,12 +192,15 @@ impl Executor { println!(" Committing version and CHANGELOG updates"); Command::new("git") - .args(&["add", "Cargo.toml", "CHANGELOG.md"]) + .args(["add", "Cargo.toml", "CHANGELOG.md"]) .current_dir(&self.repo_root) .output()?; Command::new("git") - .args(&["add", "apps/ios/FocalPoint/Sources/FocalPointApp/Info.plist"]) + .args([ + "add", + "apps/ios/FocalPoint/Sources/FocalPointApp/Info.plist", + ]) .current_dir(&self.repo_root) .output()?; @@ -212,7 +210,7 @@ impl Executor { ); let output = Command::new("git") - .args(&["commit", "-m", &msg]) + .args(["commit", "-m", &msg]) .current_dir(&self.repo_root) .output()?; @@ -228,7 +226,7 @@ impl Executor { let msg = format!("FocalPoint {}", tag); Command::new("git") - .args(&["tag", "-a", tag, "-m", &msg]) + .args(["tag", "-a", tag, "-m", &msg]) .current_dir(&self.repo_root) .output()?; @@ -239,7 +237,7 @@ impl Executor { println!(" Pushing tag to origin: {}", tag); let output = Command::new("git") - .args(&["push", "origin", tag]) + .args(["push", "origin", tag]) .current_dir(&self.repo_root) .output()?; @@ -252,7 +250,7 @@ impl Executor { Ok(()) } - fn post_discord(&self, version: &Version) -> Result<()> { + fn post_discord(&self, _version: &Version) -> Result<()> { println!(" Posting release announcement to Discord #releases"); // Use focus-release-bot to build payload @@ -264,7 +262,10 @@ impl Executor { return Ok(()); } - println!(" → Webhook URL: {}...", &webhook_url[..50.min(webhook_url.len())]); + println!( + " → Webhook URL: {}...", + &webhook_url[..50.min(webhook_url.len())] + ); Ok(()) } @@ -274,7 +275,7 @@ impl Executor { println!(" $ cd apps/ios && fastlane ios beta version:{}", version); let output = Command::new("fastlane") - .args(&["ios", "beta"]) + .args(["ios", "beta"]) .arg(format!("version:{}", version)) .current_dir(self.repo_root.join("apps/ios")) .output()?; @@ -312,7 +313,7 @@ mod tests { // Traces to: FR-RELEASE-004 (Discord post includes all commits since last tag) #[test] fn test_discord_post_generation() { - let version = Version::parse("0.0.7").unwrap(); + let _version = Version::parse("0.0.7").unwrap(); // Simulated: Discord payload generated from git log v0.0.6..HEAD let mock_payload = r#"{"embeds":[{"title":"FocalPoint 0.0.7","fields":[]}]}"#; assert!(mock_payload.contains("0.0.7")); diff --git a/crates/release-cut/src/main.rs b/crates/release-cut/src/main.rs index 18fcf2202..0af4a94c8 100644 --- a/crates/release-cut/src/main.rs +++ b/crates/release-cut/src/main.rs @@ -8,16 +8,14 @@ use anyhow::{anyhow, Result}; use clap::{Parser, Subcommand}; use semver::Version; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; +use std::path::PathBuf; mod executor; mod planner; mod version_bump; use executor::Executor; -use planner::{Plan, Planner}; +use planner::Planner; #[derive(Parser)] #[command(name = "release-cut")] @@ -108,7 +106,5 @@ fn find_repo_root() -> Result { #[cfg(test)] mod tests { - use super::*; - // Tests are in executor.rs and planner.rs modules } diff --git a/crates/release-cut/src/planner.rs b/crates/release-cut/src/planner.rs index 29cd9bcfc..08224a4ff 100644 --- a/crates/release-cut/src/planner.rs +++ b/crates/release-cut/src/planner.rs @@ -3,7 +3,6 @@ use anyhow::{anyhow, Result}; use semver::Version; use std::path::Path; -use std::process::Command; #[derive(Clone, Debug)] pub struct Plan { @@ -11,6 +10,7 @@ pub struct Plan { pub git_tag: String, pub version_bumps: Vec, pub changelog_path: String, + #[allow(dead_code)] pub discord_post: String, pub fastlane_lane: String, } @@ -24,10 +24,16 @@ pub struct VersionBump { impl Plan { pub fn print(&self) { - println!("┌─ Release Plan: {} ─────────────────────────────────┐", self.version); + println!( + "┌─ Release Plan: {} ─────────────────────────────────┐", + self.version + ); println!("│"); println!("│ 1. Git Tag:"); - println!("│ $ git tag -a {} -m 'FocalPoint {}'", self.git_tag, self.version); + println!( + "│ $ git tag -a {} -m 'FocalPoint {}'", + self.git_tag, self.version + ); println!("│ $ git push origin {}", self.git_tag); println!("│"); println!("│ 2. Version Bumps:"); @@ -114,16 +120,19 @@ impl Planner { } // iOS plist version - let ios_plist = self.repo_root.join( - "apps/ios/FocalPoint/Sources/FocalPointApp/Info.plist" - ); + let ios_plist = self + .repo_root + .join("apps/ios/FocalPoint/Sources/FocalPointApp/Info.plist"); if ios_plist.exists() { let plist_content = std::fs::read_to_string(&ios_plist)?; if let Some(old_plist_version) = extract_plist_version(&plist_content) { bumps.push(VersionBump { path: ios_plist.display().to_string(), old_version: old_plist_version, - new_version: format!("{}.{}.{}", new_version.major, new_version.minor, new_version.patch), + new_version: format!( + "{}.{}.{}", + new_version.major, new_version.minor, new_version.patch + ), }); } } diff --git a/crates/sbom-gen/docs/security/sbom.json b/crates/sbom-gen/docs/security/sbom.json new file mode 100644 index 000000000..104955d60 --- /dev/null +++ b/crates/sbom-gen/docs/security/sbom.json @@ -0,0 +1,2399 @@ +{ + "bomFormat": "CycloneDX", + "components": [ + { + "name": "agent-forecast", + "purl": "pkg:cargo/agent-forecast/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "agent-orchestrator", + "purl": "pkg:cargo/agent-orchestrator/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "aho-corasick", + "purl": "pkg:cargo/aho-corasick/1.1.4", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.1.4" + }, + { + "name": "android_system_properties", + "purl": "pkg:cargo/android_system_properties/0.1.5", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.5" + }, + { + "name": "anstream", + "purl": "pkg:cargo/anstream/1.0.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.0" + }, + { + "name": "anstyle", + "purl": "pkg:cargo/anstyle/1.0.14", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.14" + }, + { + "name": "anstyle-parse", + "purl": "pkg:cargo/anstyle-parse/1.0.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.0" + }, + { + "name": "anstyle-query", + "purl": "pkg:cargo/anstyle-query/1.1.5", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.1.5" + }, + { + "name": "anstyle-wincon", + "purl": "pkg:cargo/anstyle-wincon/3.0.11", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "3.0.11" + }, + { + "name": "anthropic-usage-poll", + "purl": "pkg:cargo/anthropic-usage-poll/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "anyhow", + "purl": "pkg:cargo/anyhow/1.0.102", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.102" + }, + { + "name": "arbitrary", + "purl": "pkg:cargo/arbitrary/1.4.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.4.2" + }, + { + "name": "atomic-waker", + "purl": "pkg:cargo/atomic-waker/1.1.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.1.2" + }, + { + "name": "audit-privacy", + "purl": "pkg:cargo/audit-privacy/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "autocfg", + "purl": "pkg:cargo/autocfg/1.5.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.5.0" + }, + { + "name": "aws-lc-rs", + "purl": "pkg:cargo/aws-lc-rs/1.16.3", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.16.3" + }, + { + "name": "aws-lc-sys", + "purl": "pkg:cargo/aws-lc-sys/0.40.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.40.0" + }, + { + "name": "base64", + "purl": "pkg:cargo/base64/0.22.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.22.1" + }, + { + "name": "bench-guard", + "purl": "pkg:cargo/bench-guard/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "bitflags", + "purl": "pkg:cargo/bitflags/2.11.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.11.1" + }, + { + "name": "block-buffer", + "purl": "pkg:cargo/block-buffer/0.12.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.12.0" + }, + { + "name": "bumpalo", + "purl": "pkg:cargo/bumpalo/3.20.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "3.20.2" + }, + { + "name": "bytes", + "purl": "pkg:cargo/bytes/1.11.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.11.1" + }, + { + "name": "camino", + "purl": "pkg:cargo/camino/1.2.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.2.2" + }, + { + "name": "cargo-platform", + "purl": "pkg:cargo/cargo-platform/0.3.3", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.3" + }, + { + "name": "cargo_metadata", + "purl": "pkg:cargo/cargo_metadata/0.23.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.23.1" + }, + { + "name": "cc", + "purl": "pkg:cargo/cc/1.2.61", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.2.61" + }, + { + "name": "cesu8", + "purl": "pkg:cargo/cesu8/1.1.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.1.0" + }, + { + "name": "cfg-if", + "purl": "pkg:cargo/cfg-if/1.0.4", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.4" + }, + { + "name": "cfg_aliases", + "purl": "pkg:cargo/cfg_aliases/0.2.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.1" + }, + { + "name": "chrono", + "purl": "pkg:cargo/chrono/0.4.44", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.4.44" + }, + { + "name": "clap", + "purl": "pkg:cargo/clap/4.6.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "4.6.1" + }, + { + "name": "clap_builder", + "purl": "pkg:cargo/clap_builder/4.6.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "4.6.0" + }, + { + "name": "clap_derive", + "purl": "pkg:cargo/clap_derive/4.6.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "4.6.1" + }, + { + "name": "clap_lex", + "purl": "pkg:cargo/clap_lex/1.1.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.1.0" + }, + { + "name": "cmake", + "purl": "pkg:cargo/cmake/0.1.58", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.58" + }, + { + "name": "colorchoice", + "purl": "pkg:cargo/colorchoice/1.0.5", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.5" + }, + { + "name": "combine", + "purl": "pkg:cargo/combine/4.6.7", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "4.6.7" + }, + { + "name": "commit-msg-check", + "purl": "pkg:cargo/commit-msg-check/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "const-oid", + "purl": "pkg:cargo/const-oid/0.10.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.10.2" + }, + { + "name": "core-foundation", + "purl": "pkg:cargo/core-foundation/0.10.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.10.1" + }, + { + "name": "core-foundation", + "purl": "pkg:cargo/core-foundation/0.9.4", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.9.4" + }, + { + "name": "core-foundation-sys", + "purl": "pkg:cargo/core-foundation-sys/0.8.7", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.8.7" + }, + { + "name": "cpufeatures", + "purl": "pkg:cargo/cpufeatures/0.3.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.0" + }, + { + "name": "crypto-common", + "purl": "pkg:cargo/crypto-common/0.2.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.1" + }, + { + "name": "deranged", + "purl": "pkg:cargo/deranged/0.5.8", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.5.8" + }, + { + "name": "derive_arbitrary", + "purl": "pkg:cargo/derive_arbitrary/1.4.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.4.2" + }, + { + "name": "digest", + "purl": "pkg:cargo/digest/0.11.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.11.2" + }, + { + "name": "displaydoc", + "purl": "pkg:cargo/displaydoc/0.2.5", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.5" + }, + { + "name": "doc-link-check", + "purl": "pkg:cargo/doc-link-check/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "docs-health", + "purl": "pkg:cargo/docs-health/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "dunce", + "purl": "pkg:cargo/dunce/1.0.5", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.5" + }, + { + "name": "encoding_rs", + "purl": "pkg:cargo/encoding_rs/0.8.35", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.8.35" + }, + { + "name": "equivalent", + "purl": "pkg:cargo/equivalent/1.0.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.2" + }, + { + "name": "errno", + "purl": "pkg:cargo/errno/0.3.14", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.14" + }, + { + "name": "fastrand", + "purl": "pkg:cargo/fastrand/2.4.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.4.1" + }, + { + "name": "find-msvc-tools", + "purl": "pkg:cargo/find-msvc-tools/0.1.9", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.9" + }, + { + "name": "fnv", + "purl": "pkg:cargo/fnv/1.0.7", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.7" + }, + { + "name": "foldhash", + "purl": "pkg:cargo/foldhash/0.1.5", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.5" + }, + { + "name": "form_urlencoded", + "purl": "pkg:cargo/form_urlencoded/1.2.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.2.2" + }, + { + "name": "fr-coverage", + "purl": "pkg:cargo/fr-coverage/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "fr-trace", + "purl": "pkg:cargo/fr-trace/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "fs_extra", + "purl": "pkg:cargo/fs_extra/1.3.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.3.0" + }, + { + "name": "futures-channel", + "purl": "pkg:cargo/futures-channel/0.3.32", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.32" + }, + { + "name": "futures-core", + "purl": "pkg:cargo/futures-core/0.3.32", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.32" + }, + { + "name": "futures-io", + "purl": "pkg:cargo/futures-io/0.3.32", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.32" + }, + { + "name": "futures-sink", + "purl": "pkg:cargo/futures-sink/0.3.32", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.32" + }, + { + "name": "futures-task", + "purl": "pkg:cargo/futures-task/0.3.32", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.32" + }, + { + "name": "futures-util", + "purl": "pkg:cargo/futures-util/0.3.32", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.32" + }, + { + "name": "fuzz-setup", + "purl": "pkg:cargo/fuzz-setup/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "getopts", + "purl": "pkg:cargo/getopts/0.2.24", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.24" + }, + { + "name": "getrandom", + "purl": "pkg:cargo/getrandom/0.2.17", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.17" + }, + { + "name": "getrandom", + "purl": "pkg:cargo/getrandom/0.3.4", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.4" + }, + { + "name": "getrandom", + "purl": "pkg:cargo/getrandom/0.4.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.4.2" + }, + { + "name": "glob", + "purl": "pkg:cargo/glob/0.3.3", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.3" + }, + { + "name": "h2", + "purl": "pkg:cargo/h2/0.4.13", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.4.13" + }, + { + "name": "hashbrown", + "purl": "pkg:cargo/hashbrown/0.15.5", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.15.5" + }, + { + "name": "hashbrown", + "purl": "pkg:cargo/hashbrown/0.17.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.17.0" + }, + { + "name": "heck", + "purl": "pkg:cargo/heck/0.5.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.5.0" + }, + { + "name": "hook-entry", + "purl": "pkg:cargo/hook-entry/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "http", + "purl": "pkg:cargo/http/1.4.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.4.0" + }, + { + "name": "http-body", + "purl": "pkg:cargo/http-body/1.0.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.1" + }, + { + "name": "http-body-util", + "purl": "pkg:cargo/http-body-util/0.1.3", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.3" + }, + { + "name": "httparse", + "purl": "pkg:cargo/httparse/1.10.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.10.1" + }, + { + "name": "hybrid-array", + "purl": "pkg:cargo/hybrid-array/0.4.11", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.4.11" + }, + { + "name": "hyper", + "purl": "pkg:cargo/hyper/1.9.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.9.0" + }, + { + "name": "hyper-rustls", + "purl": "pkg:cargo/hyper-rustls/0.27.9", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.27.9" + }, + { + "name": "hyper-util", + "purl": "pkg:cargo/hyper-util/0.1.20", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.20" + }, + { + "name": "iana-time-zone", + "purl": "pkg:cargo/iana-time-zone/0.1.65", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.65" + }, + { + "name": "iana-time-zone-haiku", + "purl": "pkg:cargo/iana-time-zone-haiku/0.1.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.2" + }, + { + "name": "icu_collections", + "purl": "pkg:cargo/icu_collections/2.2.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.2.0" + }, + { + "name": "icu_locale_core", + "purl": "pkg:cargo/icu_locale_core/2.2.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.2.0" + }, + { + "name": "icu_normalizer", + "purl": "pkg:cargo/icu_normalizer/2.2.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.2.0" + }, + { + "name": "icu_normalizer_data", + "purl": "pkg:cargo/icu_normalizer_data/2.2.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.2.0" + }, + { + "name": "icu_properties", + "purl": "pkg:cargo/icu_properties/2.2.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.2.0" + }, + { + "name": "icu_properties_data", + "purl": "pkg:cargo/icu_properties_data/2.2.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.2.0" + }, + { + "name": "icu_provider", + "purl": "pkg:cargo/icu_provider/2.2.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.2.0" + }, + { + "name": "id-arena", + "purl": "pkg:cargo/id-arena/2.3.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.3.0" + }, + { + "name": "idna", + "purl": "pkg:cargo/idna/1.1.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.1.0" + }, + { + "name": "idna_adapter", + "purl": "pkg:cargo/idna_adapter/1.2.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.2.1" + }, + { + "name": "indexmap", + "purl": "pkg:cargo/indexmap/2.14.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.14.0" + }, + { + "name": "ipnet", + "purl": "pkg:cargo/ipnet/2.12.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.12.0" + }, + { + "name": "iri-string", + "purl": "pkg:cargo/iri-string/0.7.12", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.7.12" + }, + { + "name": "is_terminal_polyfill", + "purl": "pkg:cargo/is_terminal_polyfill/1.70.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.70.2" + }, + { + "name": "itoa", + "purl": "pkg:cargo/itoa/1.0.18", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.18" + }, + { + "name": "jni", + "purl": "pkg:cargo/jni/0.21.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.21.1" + }, + { + "name": "jni-sys", + "purl": "pkg:cargo/jni-sys/0.3.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.1" + }, + { + "name": "jni-sys", + "purl": "pkg:cargo/jni-sys/0.4.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.4.1" + }, + { + "name": "jni-sys-macros", + "purl": "pkg:cargo/jni-sys-macros/0.4.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.4.1" + }, + { + "name": "jobserver", + "purl": "pkg:cargo/jobserver/0.1.34", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.34" + }, + { + "name": "js-sys", + "purl": "pkg:cargo/js-sys/0.3.95", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.95" + }, + { + "name": "lazy_static", + "purl": "pkg:cargo/lazy_static/1.5.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.5.0" + }, + { + "name": "leb128fmt", + "purl": "pkg:cargo/leb128fmt/0.1.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.0" + }, + { + "name": "legacy-scan", + "purl": "pkg:cargo/legacy-scan/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "libc", + "purl": "pkg:cargo/libc/0.2.186", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.186" + }, + { + "name": "libfuzzer-sys", + "purl": "pkg:cargo/libfuzzer-sys/0.4.12", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.4.12" + }, + { + "name": "linux-raw-sys", + "purl": "pkg:cargo/linux-raw-sys/0.12.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.12.1" + }, + { + "name": "litemap", + "purl": "pkg:cargo/litemap/0.8.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.8.2" + }, + { + "name": "log", + "purl": "pkg:cargo/log/0.4.29", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.4.29" + }, + { + "name": "lru-slab", + "purl": "pkg:cargo/lru-slab/0.1.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.2" + }, + { + "name": "matchers", + "purl": "pkg:cargo/matchers/0.2.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.0" + }, + { + "name": "memchr", + "purl": "pkg:cargo/memchr/2.8.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.8.0" + }, + { + "name": "mime", + "purl": "pkg:cargo/mime/0.3.17", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.17" + }, + { + "name": "mio", + "purl": "pkg:cargo/mio/1.2.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.2.0" + }, + { + "name": "nu-ansi-term", + "purl": "pkg:cargo/nu-ansi-term/0.50.3", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.50.3" + }, + { + "name": "num-conv", + "purl": "pkg:cargo/num-conv/0.2.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.1" + }, + { + "name": "num-traits", + "purl": "pkg:cargo/num-traits/0.2.19", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.19" + }, + { + "name": "once_cell", + "purl": "pkg:cargo/once_cell/1.21.4", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.21.4" + }, + { + "name": "once_cell_polyfill", + "purl": "pkg:cargo/once_cell_polyfill/1.70.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.70.2" + }, + { + "name": "openssl-probe", + "purl": "pkg:cargo/openssl-probe/0.2.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.1" + }, + { + "name": "percent-encoding", + "purl": "pkg:cargo/percent-encoding/2.3.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.3.2" + }, + { + "name": "pin-project-lite", + "purl": "pkg:cargo/pin-project-lite/0.2.17", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.17" + }, + { + "name": "plist", + "purl": "pkg:cargo/plist/1.8.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.8.0" + }, + { + "name": "potential_utf", + "purl": "pkg:cargo/potential_utf/0.1.5", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.5" + }, + { + "name": "powerfmt", + "purl": "pkg:cargo/powerfmt/0.2.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.0" + }, + { + "name": "ppv-lite86", + "purl": "pkg:cargo/ppv-lite86/0.2.21", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.21" + }, + { + "name": "prettyplease", + "purl": "pkg:cargo/prettyplease/0.2.37", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.37" + }, + { + "name": "proc-macro2", + "purl": "pkg:cargo/proc-macro2/1.0.106", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.106" + }, + { + "name": "pulldown-cmark", + "purl": "pkg:cargo/pulldown-cmark/0.13.3", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.13.3" + }, + { + "name": "pulldown-cmark-escape", + "purl": "pkg:cargo/pulldown-cmark-escape/0.11.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.11.0" + }, + { + "name": "quality-gate", + "purl": "pkg:cargo/quality-gate/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "quick-xml", + "purl": "pkg:cargo/quick-xml/0.38.4", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.38.4" + }, + { + "name": "quinn", + "purl": "pkg:cargo/quinn/0.11.9", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.11.9" + }, + { + "name": "quinn-proto", + "purl": "pkg:cargo/quinn-proto/0.11.14", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.11.14" + }, + { + "name": "quinn-udp", + "purl": "pkg:cargo/quinn-udp/0.5.14", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.5.14" + }, + { + "name": "quote", + "purl": "pkg:cargo/quote/1.0.45", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.45" + }, + { + "name": "r-efi", + "purl": "pkg:cargo/r-efi/5.3.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "5.3.0" + }, + { + "name": "r-efi", + "purl": "pkg:cargo/r-efi/6.0.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "6.0.0" + }, + { + "name": "rand", + "purl": "pkg:cargo/rand/0.9.4", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.9.4" + }, + { + "name": "rand_chacha", + "purl": "pkg:cargo/rand_chacha/0.9.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.9.0" + }, + { + "name": "rand_core", + "purl": "pkg:cargo/rand_core/0.9.5", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.9.5" + }, + { + "name": "regex", + "purl": "pkg:cargo/regex/1.12.3", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.12.3" + }, + { + "name": "regex-automata", + "purl": "pkg:cargo/regex-automata/0.4.14", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.4.14" + }, + { + "name": "regex-syntax", + "purl": "pkg:cargo/regex-syntax/0.8.10", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.8.10" + }, + { + "name": "release-cut", + "purl": "pkg:cargo/release-cut/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "reqwest", + "purl": "pkg:cargo/reqwest/0.13.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.13.2" + }, + { + "name": "ring", + "purl": "pkg:cargo/ring/0.17.14", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.17.14" + }, + { + "name": "rustc-hash", + "purl": "pkg:cargo/rustc-hash/2.1.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.1.2" + }, + { + "name": "rustix", + "purl": "pkg:cargo/rustix/1.1.4", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.1.4" + }, + { + "name": "rustls", + "purl": "pkg:cargo/rustls/0.23.39", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.23.39" + }, + { + "name": "rustls-native-certs", + "purl": "pkg:cargo/rustls-native-certs/0.8.3", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.8.3" + }, + { + "name": "rustls-pki-types", + "purl": "pkg:cargo/rustls-pki-types/1.14.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.14.1" + }, + { + "name": "rustls-platform-verifier", + "purl": "pkg:cargo/rustls-platform-verifier/0.6.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.6.2" + }, + { + "name": "rustls-platform-verifier-android", + "purl": "pkg:cargo/rustls-platform-verifier-android/0.1.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.1" + }, + { + "name": "rustls-webpki", + "purl": "pkg:cargo/rustls-webpki/0.103.13", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.103.13" + }, + { + "name": "rustversion", + "purl": "pkg:cargo/rustversion/1.0.22", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.22" + }, + { + "name": "same-file", + "purl": "pkg:cargo/same-file/1.0.6", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.6" + }, + { + "name": "sbom-gen", + "purl": "pkg:cargo/sbom-gen/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "schannel", + "purl": "pkg:cargo/schannel/0.1.29", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.29" + }, + { + "name": "security-framework", + "purl": "pkg:cargo/security-framework/3.7.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "3.7.0" + }, + { + "name": "security-framework-sys", + "purl": "pkg:cargo/security-framework-sys/2.17.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.17.0" + }, + { + "name": "semver", + "purl": "pkg:cargo/semver/1.0.28", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.28" + }, + { + "name": "serde", + "purl": "pkg:cargo/serde/1.0.228", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.228" + }, + { + "name": "serde_core", + "purl": "pkg:cargo/serde_core/1.0.228", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.228" + }, + { + "name": "serde_derive", + "purl": "pkg:cargo/serde_derive/1.0.228", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.228" + }, + { + "name": "serde_json", + "purl": "pkg:cargo/serde_json/1.0.149", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.149" + }, + { + "name": "serde_spanned", + "purl": "pkg:cargo/serde_spanned/1.1.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.1.1" + }, + { + "name": "sha2", + "purl": "pkg:cargo/sha2/0.11.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.11.0" + }, + { + "name": "sharded-slab", + "purl": "pkg:cargo/sharded-slab/0.1.7", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.7" + }, + { + "name": "shlex", + "purl": "pkg:cargo/shlex/1.3.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.3.0" + }, + { + "name": "slab", + "purl": "pkg:cargo/slab/0.4.12", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.4.12" + }, + { + "name": "smallvec", + "purl": "pkg:cargo/smallvec/1.15.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.15.1" + }, + { + "name": "socket2", + "purl": "pkg:cargo/socket2/0.6.3", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.6.3" + }, + { + "name": "stable_deref_trait", + "purl": "pkg:cargo/stable_deref_trait/1.2.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.2.1" + }, + { + "name": "strsim", + "purl": "pkg:cargo/strsim/0.11.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.11.1" + }, + { + "name": "subtle", + "purl": "pkg:cargo/subtle/2.6.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.6.1" + }, + { + "name": "syn", + "purl": "pkg:cargo/syn/2.0.117", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.0.117" + }, + { + "name": "sync_wrapper", + "purl": "pkg:cargo/sync_wrapper/1.0.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.2" + }, + { + "name": "synstructure", + "purl": "pkg:cargo/synstructure/0.13.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.13.2" + }, + { + "name": "system-configuration", + "purl": "pkg:cargo/system-configuration/0.7.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.7.0" + }, + { + "name": "system-configuration-sys", + "purl": "pkg:cargo/system-configuration-sys/0.6.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.6.0" + }, + { + "name": "tempfile", + "purl": "pkg:cargo/tempfile/3.27.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "3.27.0" + }, + { + "name": "temporal-grounding", + "purl": "pkg:cargo/temporal-grounding/0.1.0", + "scope": "required", + "source": "unknown", + "type": "library", + "version": "0.1.0" + }, + { + "name": "thiserror", + "purl": "pkg:cargo/thiserror/1.0.69", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.69" + }, + { + "name": "thiserror", + "purl": "pkg:cargo/thiserror/2.0.18", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.0.18" + }, + { + "name": "thiserror-impl", + "purl": "pkg:cargo/thiserror-impl/1.0.69", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.69" + }, + { + "name": "thiserror-impl", + "purl": "pkg:cargo/thiserror-impl/2.0.18", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.0.18" + }, + { + "name": "thread_local", + "purl": "pkg:cargo/thread_local/1.1.9", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.1.9" + }, + { + "name": "time", + "purl": "pkg:cargo/time/0.3.47", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.47" + }, + { + "name": "time-core", + "purl": "pkg:cargo/time-core/0.1.8", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.8" + }, + { + "name": "time-macros", + "purl": "pkg:cargo/time-macros/0.2.27", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.27" + }, + { + "name": "tinystr", + "purl": "pkg:cargo/tinystr/0.8.3", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.8.3" + }, + { + "name": "tinyvec", + "purl": "pkg:cargo/tinyvec/1.11.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.11.0" + }, + { + "name": "tinyvec_macros", + "purl": "pkg:cargo/tinyvec_macros/0.1.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.1" + }, + { + "name": "tokio", + "purl": "pkg:cargo/tokio/1.52.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.52.1" + }, + { + "name": "tokio-macros", + "purl": "pkg:cargo/tokio-macros/2.7.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.7.0" + }, + { + "name": "tokio-rustls", + "purl": "pkg:cargo/tokio-rustls/0.26.4", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.26.4" + }, + { + "name": "tokio-util", + "purl": "pkg:cargo/tokio-util/0.7.18", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.7.18" + }, + { + "name": "toml-1.1.2+spec", + "purl": "pkg:cargo/toml-1.1.2+spec/1.1.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.1.0" + }, + { + "name": "toml_datetime-1.1.1+spec", + "purl": "pkg:cargo/toml_datetime-1.1.1+spec/1.1.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.1.0" + }, + { + "name": "toml_parser-1.1.2+spec", + "purl": "pkg:cargo/toml_parser-1.1.2+spec/1.1.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.1.0" + }, + { + "name": "toml_writer-1.1.1+spec", + "purl": "pkg:cargo/toml_writer-1.1.1+spec/1.1.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.1.0" + }, + { + "name": "tower", + "purl": "pkg:cargo/tower/0.5.3", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.5.3" + }, + { + "name": "tower-http", + "purl": "pkg:cargo/tower-http/0.6.8", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.6.8" + }, + { + "name": "tower-layer", + "purl": "pkg:cargo/tower-layer/0.3.3", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.3" + }, + { + "name": "tower-service", + "purl": "pkg:cargo/tower-service/0.3.3", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.3" + }, + { + "name": "tracing", + "purl": "pkg:cargo/tracing/0.1.44", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.44" + }, + { + "name": "tracing-attributes", + "purl": "pkg:cargo/tracing-attributes/0.1.31", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.31" + }, + { + "name": "tracing-core", + "purl": "pkg:cargo/tracing-core/0.1.36", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.36" + }, + { + "name": "tracing-log", + "purl": "pkg:cargo/tracing-log/0.2.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.0" + }, + { + "name": "tracing-subscriber", + "purl": "pkg:cargo/tracing-subscriber/0.3.23", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.23" + }, + { + "name": "try-lock", + "purl": "pkg:cargo/try-lock/0.2.5", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.5" + }, + { + "name": "typenum", + "purl": "pkg:cargo/typenum/1.20.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.20.0" + }, + { + "name": "unicase", + "purl": "pkg:cargo/unicase/2.9.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.9.0" + }, + { + "name": "unicode-ident", + "purl": "pkg:cargo/unicode-ident/1.0.24", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.24" + }, + { + "name": "unicode-width", + "purl": "pkg:cargo/unicode-width/0.2.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.2" + }, + { + "name": "unicode-xid", + "purl": "pkg:cargo/unicode-xid/0.2.6", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.6" + }, + { + "name": "untrusted", + "purl": "pkg:cargo/untrusted/0.9.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.9.0" + }, + { + "name": "url", + "purl": "pkg:cargo/url/2.5.8", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.5.8" + }, + { + "name": "utf8_iter", + "purl": "pkg:cargo/utf8_iter/1.0.4", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.4" + }, + { + "name": "utf8parse", + "purl": "pkg:cargo/utf8parse/0.2.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.2" + }, + { + "name": "uuid", + "purl": "pkg:cargo/uuid/1.23.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.23.1" + }, + { + "name": "valuable", + "purl": "pkg:cargo/valuable/0.1.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.1" + }, + { + "name": "walkdir", + "purl": "pkg:cargo/walkdir/2.5.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "2.5.0" + }, + { + "name": "want", + "purl": "pkg:cargo/want/0.3.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.1" + }, + { + "name": "wasi-0.11.1+wasi-snapshot", + "purl": "pkg:cargo/wasi-0.11.1+wasi-snapshot/preview1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "preview1" + }, + { + "name": "wasip2-1.0.3+wasi", + "purl": "pkg:cargo/wasip2-1.0.3+wasi/0.2.9", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.9" + }, + { + "name": "wasip3-0.4.0+wasi-0.3.0-rc-2026-01", + "purl": "pkg:cargo/wasip3-0.4.0+wasi-0.3.0-rc-2026-01/06", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "06" + }, + { + "name": "wasm-bindgen", + "purl": "pkg:cargo/wasm-bindgen/0.2.118", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.118" + }, + { + "name": "wasm-bindgen-futures", + "purl": "pkg:cargo/wasm-bindgen-futures/0.4.68", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.4.68" + }, + { + "name": "wasm-bindgen-macro", + "purl": "pkg:cargo/wasm-bindgen-macro/0.2.118", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.118" + }, + { + "name": "wasm-bindgen-macro-support", + "purl": "pkg:cargo/wasm-bindgen-macro-support/0.2.118", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.118" + }, + { + "name": "wasm-bindgen-shared", + "purl": "pkg:cargo/wasm-bindgen-shared/0.2.118", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.118" + }, + { + "name": "wasm-encoder", + "purl": "pkg:cargo/wasm-encoder/0.244.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.244.0" + }, + { + "name": "wasm-metadata", + "purl": "pkg:cargo/wasm-metadata/0.244.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.244.0" + }, + { + "name": "wasmparser", + "purl": "pkg:cargo/wasmparser/0.244.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.244.0" + }, + { + "name": "web-sys", + "purl": "pkg:cargo/web-sys/0.3.95", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.3.95" + }, + { + "name": "web-time", + "purl": "pkg:cargo/web-time/1.1.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.1.0" + }, + { + "name": "webpki-root-certs", + "purl": "pkg:cargo/webpki-root-certs/1.0.7", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.7" + }, + { + "name": "winapi-util", + "purl": "pkg:cargo/winapi-util/0.1.11", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.11" + }, + { + "name": "windows-core", + "purl": "pkg:cargo/windows-core/0.62.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.62.2" + }, + { + "name": "windows-implement", + "purl": "pkg:cargo/windows-implement/0.60.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.60.2" + }, + { + "name": "windows-interface", + "purl": "pkg:cargo/windows-interface/0.59.3", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.59.3" + }, + { + "name": "windows-link", + "purl": "pkg:cargo/windows-link/0.2.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.1" + }, + { + "name": "windows-registry", + "purl": "pkg:cargo/windows-registry/0.6.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.6.1" + }, + { + "name": "windows-result", + "purl": "pkg:cargo/windows-result/0.4.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.4.1" + }, + { + "name": "windows-strings", + "purl": "pkg:cargo/windows-strings/0.5.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.5.1" + }, + { + "name": "windows-sys", + "purl": "pkg:cargo/windows-sys/0.45.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.45.0" + }, + { + "name": "windows-sys", + "purl": "pkg:cargo/windows-sys/0.52.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.52.0" + }, + { + "name": "windows-sys", + "purl": "pkg:cargo/windows-sys/0.61.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.61.2" + }, + { + "name": "windows-targets", + "purl": "pkg:cargo/windows-targets/0.42.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.42.2" + }, + { + "name": "windows-targets", + "purl": "pkg:cargo/windows-targets/0.52.6", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.52.6" + }, + { + "name": "windows_aarch64_gnullvm", + "purl": "pkg:cargo/windows_aarch64_gnullvm/0.42.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.42.2" + }, + { + "name": "windows_aarch64_gnullvm", + "purl": "pkg:cargo/windows_aarch64_gnullvm/0.52.6", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.52.6" + }, + { + "name": "windows_aarch64_msvc", + "purl": "pkg:cargo/windows_aarch64_msvc/0.42.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.42.2" + }, + { + "name": "windows_aarch64_msvc", + "purl": "pkg:cargo/windows_aarch64_msvc/0.52.6", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.52.6" + }, + { + "name": "windows_i686_gnu", + "purl": "pkg:cargo/windows_i686_gnu/0.42.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.42.2" + }, + { + "name": "windows_i686_gnu", + "purl": "pkg:cargo/windows_i686_gnu/0.52.6", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.52.6" + }, + { + "name": "windows_i686_gnullvm", + "purl": "pkg:cargo/windows_i686_gnullvm/0.52.6", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.52.6" + }, + { + "name": "windows_i686_msvc", + "purl": "pkg:cargo/windows_i686_msvc/0.42.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.42.2" + }, + { + "name": "windows_i686_msvc", + "purl": "pkg:cargo/windows_i686_msvc/0.52.6", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.52.6" + }, + { + "name": "windows_x86_64_gnu", + "purl": "pkg:cargo/windows_x86_64_gnu/0.42.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.42.2" + }, + { + "name": "windows_x86_64_gnu", + "purl": "pkg:cargo/windows_x86_64_gnu/0.52.6", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.52.6" + }, + { + "name": "windows_x86_64_gnullvm", + "purl": "pkg:cargo/windows_x86_64_gnullvm/0.42.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.42.2" + }, + { + "name": "windows_x86_64_gnullvm", + "purl": "pkg:cargo/windows_x86_64_gnullvm/0.52.6", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.52.6" + }, + { + "name": "windows_x86_64_msvc", + "purl": "pkg:cargo/windows_x86_64_msvc/0.42.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.42.2" + }, + { + "name": "windows_x86_64_msvc", + "purl": "pkg:cargo/windows_x86_64_msvc/0.52.6", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.52.6" + }, + { + "name": "winnow", + "purl": "pkg:cargo/winnow/1.0.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.2" + }, + { + "name": "wit-bindgen", + "purl": "pkg:cargo/wit-bindgen/0.51.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.51.0" + }, + { + "name": "wit-bindgen", + "purl": "pkg:cargo/wit-bindgen/0.57.1", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.57.1" + }, + { + "name": "wit-bindgen-core", + "purl": "pkg:cargo/wit-bindgen-core/0.51.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.51.0" + }, + { + "name": "wit-bindgen-rust", + "purl": "pkg:cargo/wit-bindgen-rust/0.51.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.51.0" + }, + { + "name": "wit-bindgen-rust-macro", + "purl": "pkg:cargo/wit-bindgen-rust-macro/0.51.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.51.0" + }, + { + "name": "wit-component", + "purl": "pkg:cargo/wit-component/0.244.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.244.0" + }, + { + "name": "wit-parser", + "purl": "pkg:cargo/wit-parser/0.244.0", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.244.0" + }, + { + "name": "writeable", + "purl": "pkg:cargo/writeable/0.6.3", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.6.3" + }, + { + "name": "yoke", + "purl": "pkg:cargo/yoke/0.8.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.8.2" + }, + { + "name": "yoke-derive", + "purl": "pkg:cargo/yoke-derive/0.8.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.8.2" + }, + { + "name": "zerocopy", + "purl": "pkg:cargo/zerocopy/0.8.48", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.8.48" + }, + { + "name": "zerocopy-derive", + "purl": "pkg:cargo/zerocopy-derive/0.8.48", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.8.48" + }, + { + "name": "zerofrom", + "purl": "pkg:cargo/zerofrom/0.1.7", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.7" + }, + { + "name": "zerofrom-derive", + "purl": "pkg:cargo/zerofrom-derive/0.1.7", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.1.7" + }, + { + "name": "zeroize", + "purl": "pkg:cargo/zeroize/1.8.2", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.8.2" + }, + { + "name": "zerotrie", + "purl": "pkg:cargo/zerotrie/0.2.4", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.2.4" + }, + { + "name": "zerovec", + "purl": "pkg:cargo/zerovec/0.11.6", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.11.6" + }, + { + "name": "zerovec-derive", + "purl": "pkg:cargo/zerovec-derive/0.11.3", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "0.11.3" + }, + { + "name": "zmij", + "purl": "pkg:cargo/zmij/1.0.21", + "scope": "required", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "type": "library", + "version": "1.0.21" + } + ], + "metadata": { + "component": { + "description": "Connector-first screen-time platform", + "name": "FocalPoint", + "type": "application", + "version": "0.0.1" + }, + "timestamp": "2026-04-26T17:37:16.562711466+00:00", + "tools": [ + { + "name": "sbom-gen", + "vendor": "FocalPoint", + "version": "0.1.0" + } + ] + }, + "specVersion": "1.4", + "version": 1 +} \ No newline at end of file diff --git a/crates/sbom-gen/src/main.rs b/crates/sbom-gen/src/main.rs index 8cb664fe2..2518aa330 100644 --- a/crates/sbom-gen/src/main.rs +++ b/crates/sbom-gen/src/main.rs @@ -34,7 +34,9 @@ fn main() -> Result<()> { // Build CycloneDX components for (crate_name_ver, source) in unique_crates { - let (name, version) = crate_name_ver.rsplit_once('-').unwrap_or((&crate_name_ver, "")); + let (name, version) = crate_name_ver + .rsplit_once('-') + .unwrap_or((&crate_name_ver, "")); let purl = format!("pkg:cargo/{}/{}", name, version); components.push(json!({ @@ -72,21 +74,15 @@ fn main() -> Result<()> { }); // Ensure output directory - fs::create_dir_all("docs/security") - .context("Failed to create docs/security directory")?; + fs::create_dir_all("docs/security").context("Failed to create docs/security directory")?; // Write SBOM let sbom_path = "docs/security/sbom.json"; - let sbom_content = serde_json::to_string_pretty(&sbom) - .context("Failed to serialize SBOM")?; + let sbom_content = serde_json::to_string_pretty(&sbom).context("Failed to serialize SBOM")?; - fs::write(sbom_path, sbom_content) - .context("Failed to write SBOM file")?; + fs::write(sbom_path, sbom_content).context("Failed to write SBOM file")?; - let component_count = sbom["components"] - .as_array() - .map(|c| c.len()) - .unwrap_or(0); + let component_count = sbom["components"].as_array().map(|c| c.len()).unwrap_or(0); println!("✓ SBOM generated: {}", sbom_path); println!(" Components: {}", component_count); diff --git a/crates/temporal-grounding/Cargo.toml b/crates/temporal-grounding/Cargo.toml new file mode 100644 index 000000000..d9ae48ace --- /dev/null +++ b/crates/temporal-grounding/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "temporal-grounding" +description = "Temporal grounding utilities for Claude Code agent sessions" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +rust-version.workspace = true + +[lib] +name = "temporal_grounding" +path = "src/lib.rs" + +[[bin]] +name = "agent-start-stamp" +path = "src/bin/start_stamp.rs" + +[[bin]] +name = "agent-elapsed" +path = "src/bin/elapsed.rs" + +[[bin]] +name = "now-iso" +path = "src/bin/now_iso.rs" + +[dependencies] +anyhow = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +chrono = { workspace = true } diff --git a/crates/temporal-grounding/README.md b/crates/temporal-grounding/README.md new file mode 100644 index 000000000..faee055b7 --- /dev/null +++ b/crates/temporal-grounding/README.md @@ -0,0 +1,16 @@ +# temporal-grounding + +Three binaries for temporal grounding in Claude Code sessions. + +- **agent-start-stamp** `[id] [label]` — appends to `~/.claude/active-agents.json`, prints the agent ID +- **agent-elapsed** `` — prints seconds elapsed since that agent started +- **now-iso** — prints current UTC time in ISO 8601 (seconds precision) + +## Usage + +```bash +ID=$(agent-start-stamp "" sweep) +# ... run work ... +agent-elapsed "$ID" # → 42s +now-iso # → 2024-01-01T12:00:00Z +``` diff --git a/crates/temporal-grounding/src/bin/elapsed.rs b/crates/temporal-grounding/src/bin/elapsed.rs new file mode 100644 index 000000000..ca4b29f95 --- /dev/null +++ b/crates/temporal-grounding/src/bin/elapsed.rs @@ -0,0 +1,22 @@ +use anyhow::{bail, Result}; +use chrono::{DateTime, Utc}; +use temporal_grounding::{active_agents_path, AgentEntry}; + +fn main() -> Result<()> { + let id = std::env::args().nth(1).unwrap_or_default(); + let path = active_agents_path(); + if !path.exists() { + bail!("~/.claude/active-agents.json not found"); + } + let text = std::fs::read_to_string(&path)?; + let entries: Vec = serde_json::from_str(&text)?; + match entries.iter().find(|e| e.id == id) { + None => bail!("agent id '{id}' not found"), + Some(e) => { + let started: DateTime = e.started_at.parse()?; + let elapsed = Utc::now() - started; + println!("{}s", elapsed.num_seconds()); + } + } + Ok(()) +} diff --git a/crates/temporal-grounding/src/bin/now_iso.rs b/crates/temporal-grounding/src/bin/now_iso.rs new file mode 100644 index 000000000..289f6920c --- /dev/null +++ b/crates/temporal-grounding/src/bin/now_iso.rs @@ -0,0 +1,8 @@ +use chrono::Utc; + +fn main() { + println!( + "{}", + Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true) + ); +} diff --git a/crates/temporal-grounding/src/bin/start_stamp.rs b/crates/temporal-grounding/src/bin/start_stamp.rs new file mode 100644 index 000000000..ec352ca65 --- /dev/null +++ b/crates/temporal-grounding/src/bin/start_stamp.rs @@ -0,0 +1,42 @@ +use anyhow::Result; +use chrono::Utc; +use temporal_grounding::{active_agents_path, AgentEntry}; + +fn main() -> Result<()> { + let mut args = std::env::args().skip(1); + let id = args.next().unwrap_or_else(gen_id); + let label = args.next(); + + let entry = AgentEntry { + id: id.clone(), + started_at: Utc::now().to_rfc3339(), + label, + }; + + let path = active_agents_path(); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + + let text = if path.exists() { + std::fs::read_to_string(&path).ok() + } else { + None + }; + let mut entries: Vec = text + .and_then(|t| serde_json::from_str(&t).ok()) + .unwrap_or_default(); + entries.push(entry); + std::fs::write(&path, serde_json::to_string_pretty(&entries)?)?; + println!("{id}"); + Ok(()) +} + +fn gen_id() -> String { + // TODO: replace with uuid v4 for collision-free IDs + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.subsec_nanos()) + .unwrap_or(0); + format!("agent-{nanos:08x}") +} diff --git a/crates/temporal-grounding/src/lib.rs b/crates/temporal-grounding/src/lib.rs new file mode 100644 index 000000000..eecac74d2 --- /dev/null +++ b/crates/temporal-grounding/src/lib.rs @@ -0,0 +1,53 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AgentEntry { + pub id: String, + pub started_at: String, + pub label: Option, +} + +pub fn claude_dir() -> PathBuf { + std::env::var("HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(".")) + .join(".claude") +} + +pub fn active_agents_path() -> PathBuf { + claude_dir().join("active-agents.json") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn claude_dir_ends_with_dot_claude() { + let p = claude_dir(); + assert_eq!(p.file_name().unwrap().to_str().unwrap(), ".claude"); + } + + #[test] + fn active_agents_path_filename() { + let p = active_agents_path(); + assert_eq!( + p.file_name().unwrap().to_str().unwrap(), + "active-agents.json" + ); + } + + #[test] + fn agent_entry_roundtrip() { + let e = AgentEntry { + id: "test-42".to_string(), + started_at: "2024-01-01T00:00:00Z".to_string(), + label: Some("sweep".to_string()), + }; + let json = serde_json::to_string(&e).unwrap(); + let back: AgentEntry = serde_json::from_str(&json).unwrap(); + assert_eq!(back.id, e.id); + assert_eq!(back.label, e.label); + } +} diff --git a/hooks/README.md b/hooks/README.md new file mode 100644 index 000000000..4818ddaa8 --- /dev/null +++ b/hooks/README.md @@ -0,0 +1,24 @@ +# Claude Code Observability Hooks + +Wires `anthropic-usage-poll`, `agent-forecast`, and `temporal-grounding` into +Claude Code via a PreToolUse hook. + +## Installing hooks + +```bash +cargo install --path bin/hook-entry +``` + +Then copy (or merge) `hooks/settings.local.json` into `~/.claude/settings.local.json`. + +## How it works + +On every tool use, `hook-entry` reads `~/.claude/usage.json` (written by the +`anthropic-usage-poll` daemon) and injects a one-line budget summary: + +``` +[observability] daily_remaining=50000 monthly_remaining=1200000 | forecast=p50:3200 p90:8100 | elapsed=23s | updated=2024-01-01T12:00:00Z +``` + +Run `anthropic-usage-poll` as a background daemon and call `agent-start-stamp` +at the start of each session for full triad coverage. diff --git a/hooks/settings.local.json b/hooks/settings.local.json new file mode 100644 index 000000000..ac8df2736 --- /dev/null +++ b/hooks/settings.local.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": ".*", + "hooks": [ + { + "type": "command", + "command": "hook-entry" + } + ] + } + ] + } +}