1#![allow(clippy::print_stdout)]
2use serde::Serialize;
6use std::process::ExitCode;
7
8pub 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_with_options(
24 value,
25 self.format,
26 &agent_first_data::OutputOptions::default(),
27 )
28 }
29
30 pub fn value(&self, mut value: serde_json::Value) -> ExitCode {
32 agent_first_data::redact_secrets_in_place(&mut value);
33 println!("{}", self.format(&value));
34 ExitCode::SUCCESS
35 }
36
37 pub fn ok<T: Serialize>(&self, result: T) -> ExitCode {
39 let result_value = serde_json::to_value(&result).unwrap_or_default();
40 let mut resp = agent_first_data::build_json_ok(result_value, None);
41 agent_first_data::redact_secrets_in_place(&mut resp);
42 println!("{}", self.format(&resp));
43 ExitCode::SUCCESS
44 }
45
46 pub fn ok_trace<T: Serialize>(&self, result: T, trace: impl Serialize) -> ExitCode {
48 let result_value = serde_json::to_value(&result).unwrap_or_default();
49 let trace_value = serde_json::to_value(&trace).unwrap_or_default();
50 let mut resp = agent_first_data::build_json_ok(result_value, Some(trace_value));
51 agent_first_data::redact_secrets_in_place(&mut resp);
52 println!("{}", self.format(&resp));
53 ExitCode::SUCCESS
54 }
55
56 pub fn error(&self, error_code: &str, message: &str) -> ExitCode {
58 self.error_hint(error_code, message, None)
59 }
60
61 pub fn error_hint(&self, error_code: &str, message: &str, hint: Option<&str>) -> ExitCode {
63 let mut fields = serde_json::Map::new();
64 fields.insert(
65 "error".into(),
66 serde_json::Value::String(message.to_string()),
67 );
68 fields.insert(
69 "hint".into(),
70 serde_json::Value::String(actionable_hint(error_code, hint).to_string()),
71 );
72 let mut resp = agent_first_data::build_json(
73 error_code,
74 serde_json::Value::Object(fields),
75 Some(serde_json::json!({"duration_ms": 0})),
76 );
77 agent_first_data::redact_secrets_in_place(&mut resp);
78 println!("{}", self.format(&resp));
79 ExitCode::FAILURE
80 }
81
82 pub fn error_from(&self, error_code: &str, err: &anyhow::Error) -> ExitCode {
84 self.error(error_code, &err.to_string())
85 }
86
87 pub fn error_hypha(&self, err: &crate::HyphaError) -> ExitCode {
89 self.error_hint(&err.code, &err.message, err.hint.as_deref())
90 }
91
92 pub fn progress(&self, step: u32, total: u32, message: &str, data: serde_json::Value) {
95 let mut fields = match data {
96 serde_json::Value::Object(map) => map,
97 _ => serde_json::Map::new(),
98 };
99 fields.insert("current".into(), step.into());
100 fields.insert("total".into(), total.into());
101 fields.insert("message".into(), message.into());
102 let mut resp =
103 agent_first_data::build_json("progress", serde_json::Value::Object(fields), None);
104 agent_first_data::redact_secrets_in_place(&mut resp);
105 println!("{}", self.format(&resp));
106 }
107
108 pub fn download_progress(&self, downloaded_bytes: u64, total_bytes: Option<u64>) {
111 let mut resp = agent_first_data::build_json(
112 "download_progress",
113 serde_json::json!({
114 "downloaded_bytes": downloaded_bytes,
115 "total_bytes": total_bytes,
116 }),
117 None,
118 );
119 agent_first_data::redact_secrets_in_place(&mut resp);
120 println!("{}", self.format(&resp));
121 }
122
123 pub fn warn(&self, code: &str, message: &str) {
125 let mut resp =
126 agent_first_data::build_json(code, serde_json::json!({"message": message}), None);
127 agent_first_data::redact_secrets_in_place(&mut resp);
128 println!("{}", self.format(&resp));
129 }
130
131 pub fn startup(&self, args: serde_json::Value) {
133 let (config, config_error) = match crate::config::HyphaConfig::load() {
134 Ok(cfg) => (serde_json::to_value(&cfg).unwrap_or_default(), None),
135 Err(err) => (
136 serde_json::Value::Null,
137 Some(serde_json::json!({
138 "code": err.code,
139 "message": err.message,
140 "hint": err.hint,
141 })),
142 ),
143 };
144
145 let env = serde_json::json!({
146 "CMN_HOME": std::env::var("CMN_HOME").ok(),
147 "SYNAPSE_TOKEN_SECRET": std::env::var("SYNAPSE_TOKEN_SECRET").ok(),
148 });
149 let mut resp = agent_first_data::build_json(
150 "log",
151 serde_json::json!({
152 "category": "startup",
153 "event": "startup",
154 "hypha_version": env!("CARGO_PKG_VERSION"),
155 "config": config,
156 "config_error": config_error,
157 "args": args,
158 "env": env
159 }),
160 None,
161 );
162 agent_first_data::redact_secrets_in_place(&mut resp);
163 println!("{}", self.format(&resp));
164 }
165}
166
167fn actionable_hint<'a>(error_code: &str, hint: Option<&'a str>) -> &'a str {
168 if let Some(hint) = hint.filter(|h| !h.trim().is_empty()) {
169 return hint;
170 }
171 match error_code {
172 "invalid_args" | "invalid_value" | "unknown_key" | "missing_domain" => {
173 "run hypha --help or the relevant subcommand --help, then retry with valid inputs"
174 }
175 "invalid_uri" | "uri_error" | "cmn_invalid" => {
176 "use a CMN URI in the form cmn://domain or cmn://domain/b3.hash"
177 }
178 "synapse_error" | "network_error" | "NETWORK_ERR" => {
179 "check the synapse URL, network connectivity, and any configured auth token"
180 }
181 "not_found" | "missing_spore" | "not_cached" | "spore_not_found" => {
182 "verify the domain/hash and run hypha sense or hypha cache list before retrying"
183 }
184 "spore_security_rejected" => {
185 "received content targets protected control paths; inspect the spore and only relax cache.spore_reject_path_components if you accept that risk"
186 }
187 "skill_error" => "run hypha skill --help and retry with the suggested options",
188 _ => "read the error field, check hypha --help for the expected input, and retry",
189 }
190}
191
192pub struct OutSink<'a>(pub &'a Output);
197
198impl crate::EventSink for OutSink<'_> {
199 fn emit(&self, event: crate::HyphaEvent) {
200 match event {
201 crate::HyphaEvent::Progress {
202 current,
203 total,
204 message,
205 } => {
206 self.0
207 .progress(current, total, &message, serde_json::Value::Null);
208 }
209 crate::HyphaEvent::DownloadProgress {
210 downloaded_bytes,
211 total_bytes,
212 } => {
213 self.0.download_progress(downloaded_bytes, total_bytes);
214 }
215 crate::HyphaEvent::Log { message } => {
216 self.0.warn("log", &message);
217 }
218 crate::HyphaEvent::Warn { message } => {
219 self.0.warn("warn", &message);
220 }
221 }
222 }
223}