Skip to main content

hypha/api/
mod.rs

1#![allow(clippy::print_stdout)]
2// Agent-First Data output layer — the ONLY place println! should appear.
3// All other code must use Output methods (ok/error/warn/progress/startup).
4
5use serde::Serialize;
6use std::process::ExitCode;
7
8/// Result type that handles output formatting and exit codes.
9///
10/// Builds Agent-First Data-compliant JSON (code/result/trace/error), redacts secrets,
11/// formats via agent_first_data, and prints protocol/log events to stdout.
12pub struct Output {
13    format: agent_first_data::OutputFormat,
14}
15
16#[allow(clippy::print_stdout)]
17impl Output {
18    pub fn new(format: agent_first_data::OutputFormat) -> Self {
19        Self { format }
20    }
21
22    fn format(&self, value: &serde_json::Value) -> String {
23        agent_first_data::cli_output(value, self.format)
24    }
25
26    /// {code: "ok", result: ...} → stdout
27    pub fn ok<T: Serialize>(&self, result: T) -> ExitCode {
28        let result_value = serde_json::to_value(&result).unwrap_or_default();
29        let mut resp = agent_first_data::build_json_ok(result_value, None);
30        agent_first_data::internal_redact_secrets(&mut resp);
31        println!("{}", self.format(&resp));
32        ExitCode::SUCCESS
33    }
34
35    /// {code: "ok", result: ..., trace: ...} → stdout
36    pub fn ok_trace<T: Serialize>(&self, result: T, trace: impl Serialize) -> ExitCode {
37        let result_value = serde_json::to_value(&result).unwrap_or_default();
38        let trace_value = serde_json::to_value(&trace).unwrap_or_default();
39        let mut resp = agent_first_data::build_json_ok(result_value, Some(trace_value));
40        agent_first_data::internal_redact_secrets(&mut resp);
41        println!("{}", self.format(&resp));
42        ExitCode::SUCCESS
43    }
44
45    /// {code: "<error_code>", error: "msg", hint?: "...", trace: {duration_ms: 0}} → stdout
46    pub fn error(&self, error_code: &str, message: &str) -> ExitCode {
47        self.error_hint(error_code, message, None)
48    }
49
50    /// Like [`error`] but with an actionable hint for remediation.
51    pub fn error_hint(&self, error_code: &str, message: &str, hint: Option<&str>) -> ExitCode {
52        let mut fields = serde_json::Map::new();
53        fields.insert(
54            "error".into(),
55            serde_json::Value::String(message.to_string()),
56        );
57        if let Some(h) = hint {
58            fields.insert("hint".into(), serde_json::Value::String(h.to_string()));
59        }
60        let mut resp = agent_first_data::build_json(
61            error_code,
62            serde_json::Value::Object(fields),
63            Some(serde_json::json!({"duration_ms": 0})),
64        );
65        agent_first_data::internal_redact_secrets(&mut resp);
66        println!("{}", self.format(&resp));
67        ExitCode::FAILURE
68    }
69
70    /// Output error from anyhow::Error
71    pub fn error_from(&self, error_code: &str, err: &anyhow::Error) -> ExitCode {
72        self.error(error_code, &err.to_string())
73    }
74
75    /// Output error from [`crate::HyphaError`] (includes hint when present).
76    pub fn error_hypha(&self, err: &crate::HyphaError) -> ExitCode {
77        self.error_hint(&err.code, &err.message, err.hint.as_deref())
78    }
79
80    /// Agent-First Data progress step → stdout
81    /// {"code": "progress", "current": N, "total": M, "message": "...", ...}
82    pub fn progress(&self, step: u32, total: u32, message: &str, data: serde_json::Value) {
83        let mut fields = match data {
84            serde_json::Value::Object(map) => map,
85            _ => serde_json::Map::new(),
86        };
87        fields.insert("current".into(), step.into());
88        fields.insert("total".into(), total.into());
89        fields.insert("message".into(), message.into());
90        let mut resp =
91            agent_first_data::build_json("progress", serde_json::Value::Object(fields), None);
92        agent_first_data::internal_redact_secrets(&mut resp);
93        println!("{}", self.format(&resp));
94    }
95
96    /// Byte-level download progress → stdout
97    /// {"code": "download_progress", "downloaded_bytes": N, "total_bytes": M}
98    pub fn download_progress(&self, downloaded_bytes: u64, total_bytes: Option<u64>) {
99        let mut resp = agent_first_data::build_json(
100            "download_progress",
101            serde_json::json!({
102                "downloaded_bytes": downloaded_bytes,
103                "total_bytes": total_bytes,
104            }),
105            None,
106        );
107        agent_first_data::internal_redact_secrets(&mut resp);
108        println!("{}", self.format(&resp));
109    }
110
111    /// Non-fatal warning → stdout
112    pub fn warn(&self, code: &str, message: &str) {
113        let mut resp =
114            agent_first_data::build_json(code, serde_json::json!({"message": message}), None);
115        agent_first_data::internal_redact_secrets(&mut resp);
116        println!("{}", self.format(&resp));
117    }
118
119    /// {code: "log", event: "startup", args: ..., config: ..., env: ...} → stdout
120    pub fn startup(&self, args: serde_json::Value) {
121        let cfg = crate::config::HyphaConfig::load();
122        let config = serde_json::to_value(&cfg).unwrap_or_default();
123
124        let env = serde_json::json!({
125            "CMN_HOME": std::env::var("CMN_HOME").ok(),
126            "SYNAPSE_TOKEN_SECRET": std::env::var("SYNAPSE_TOKEN_SECRET").ok(),
127        });
128        let mut resp = agent_first_data::build_json(
129            "log",
130            serde_json::json!({
131                "event": "startup",
132                "hypha_version": env!("CARGO_PKG_VERSION"),
133                "config": config,
134                "args": args,
135                "env": env
136            }),
137            None,
138        );
139        agent_first_data::internal_redact_secrets(&mut resp);
140        println!("{}", self.format(&resp));
141    }
142}
143
144/// Bridges [`crate::EventSink`] to an existing [`Output`].
145///
146/// Used in `handle_*` CLI wrappers so inner lib functions (which now take
147/// `&dyn EventSink`) can still emit warnings through the normal CLI output.
148pub struct OutSink<'a>(pub &'a Output);
149
150impl crate::EventSink for OutSink<'_> {
151    fn emit(&self, event: crate::HyphaEvent) {
152        match event {
153            crate::HyphaEvent::Progress {
154                current,
155                total,
156                message,
157            } => {
158                self.0
159                    .progress(current, total, &message, serde_json::Value::Null);
160            }
161            crate::HyphaEvent::DownloadProgress {
162                downloaded_bytes,
163                total_bytes,
164            } => {
165                self.0.download_progress(downloaded_bytes, total_bytes);
166            }
167            crate::HyphaEvent::Log { message } => {
168                self.0.warn("log", &message);
169            }
170            crate::HyphaEvent::Warn { message } => {
171                self.0.warn("warn", &message);
172            }
173        }
174    }
175}