1use serde::Serialize;
4use std::process::ExitCode;
5
6pub struct Output {
10 format: agent_first_data::OutputFormat,
11}
12
13fn built_event(
14 result: Result<agent_first_data::Event, agent_first_data::BuildError>,
15) -> agent_first_data::Event {
16 result.unwrap_or_else(|err| agent_first_data::build_cli_error(&err.to_string(), None))
17}
18
19impl Output {
20 pub fn new(format: agent_first_data::OutputFormat) -> Self {
21 Self { format }
22 }
23
24 fn emit(&self, event: agent_first_data::Event) {
28 let stdout = std::io::stdout();
29 let mut emitter = agent_first_data::CliEmitter::new(stdout.lock(), self.format);
30 let _ = emitter.emit(event);
31 }
32
33 fn finish(&self, event: agent_first_data::Event, success_code: u8) -> ExitCode {
40 let stdout = std::io::stdout();
41 let mut emitter = agent_first_data::CliEmitter::new(stdout.lock(), self.format);
42 ExitCode::from(emitter.finish(event, success_code))
43 }
44
45 pub fn ok<T: Serialize>(&self, result: T) -> ExitCode {
47 let result_value = serde_json::to_value(&result).unwrap_or_default();
48 self.finish(agent_first_data::json_result(result_value).build(), 0)
49 }
50
51 pub fn ok_trace<T: Serialize>(&self, result: T, trace: impl Serialize) -> ExitCode {
53 let result_value = serde_json::to_value(&result).unwrap_or_default();
54 let trace_value = serde_json::to_value(&trace).unwrap_or_default();
55 self.finish(
56 agent_first_data::json_result(result_value)
57 .trace(trace_value)
58 .build(),
59 0,
60 )
61 }
62
63 pub fn ok_with_code<T: Serialize>(&self, result: T, code: u8) -> ExitCode {
69 let result_value = serde_json::to_value(&result).unwrap_or_default();
70 self.finish(agent_first_data::json_result(result_value).build(), code)
71 }
72
73 pub fn error(&self, error_code: &str, message: &str) -> ExitCode {
75 self.error_hint(error_code, message, None)
76 }
77
78 pub fn error_hint(&self, error_code: &str, message: &str, hint: Option<&str>) -> ExitCode {
80 self.finish(
81 built_event(
82 agent_first_data::json_error(error_code, message)
83 .hint(actionable_hint(error_code, hint))
84 .trace(serde_json::json!({"duration_ms": 0}))
85 .build(),
86 ),
87 1,
88 )
89 }
90
91 pub fn error_from(&self, error_code: &str, err: &anyhow::Error) -> ExitCode {
93 self.error(error_code, &err.to_string())
94 }
95
96 pub fn error_hypha(&self, err: &crate::HyphaError) -> ExitCode {
98 self.error_hint(&err.code, &err.message, err.hint.as_deref())
99 }
100
101 pub fn progress(&self, step: u32, total: u32, message: &str, data: serde_json::Value) {
104 let mut fields = match data {
105 serde_json::Value::Object(map) => map,
106 _ => serde_json::Map::new(),
107 };
108 fields.insert("current".into(), step.into());
109 fields.insert("total".into(), total.into());
110 fields.insert("message".into(), message.into());
111 self.emit(agent_first_data::json_progress(serde_json::Value::Object(fields)).build());
112 }
113
114 pub fn download_progress(&self, downloaded_bytes: u64, total_bytes: Option<u64>) {
117 self.emit(
118 agent_first_data::json_progress(serde_json::json!({
119 "event": "download_progress",
120 "downloaded_bytes": downloaded_bytes,
121 "total_bytes": total_bytes,
122 }))
123 .build(),
124 );
125 }
126
127 pub fn warn(&self, code: &str, message: &str) {
129 self.emit(
130 agent_first_data::json_log(serde_json::json!({
131 "event": code,
132 "message": message,
133 }))
134 .build(),
135 );
136 }
137
138 pub fn startup(&self, args: serde_json::Value) {
140 let (config, config_error) = match crate::config::HyphaConfig::load() {
141 Ok(cfg) => (serde_json::to_value(&cfg).unwrap_or_default(), None),
142 Err(err) => (
143 serde_json::Value::Null,
144 Some(serde_json::json!({
145 "code": err.code,
146 "message": err.message,
147 "hint": err.hint,
148 })),
149 ),
150 };
151
152 let env = serde_json::json!({
153 "CMN_HOME": std::env::var("CMN_HOME").ok(),
154 "SYNAPSE_TOKEN_SECRET": std::env::var("SYNAPSE_TOKEN_SECRET").ok(),
155 });
156 self.emit(
157 agent_first_data::json_log(serde_json::json!({
158 "category": "startup",
159 "event": "startup",
160 "hypha_version": env!("CARGO_PKG_VERSION"),
161 "config": config,
162 "config_error": config_error,
163 "args": args,
164 "env": env
165 }))
166 .build(),
167 );
168 }
169}
170
171fn actionable_hint<'a>(error_code: &str, hint: Option<&'a str>) -> &'a str {
172 if let Some(hint) = hint.filter(|h| !h.trim().is_empty()) {
173 return hint;
174 }
175 match error_code {
176 "invalid_args" | "invalid_value" | "unknown_key" | "missing_domain" => {
177 "run hypha --help or the relevant subcommand --help, then retry with valid inputs"
178 }
179 "invalid_uri" | "uri_error" | "cmn_invalid" => {
180 "use a CMN URI in the form cmn://domain or cmn://domain/b3.hash"
181 }
182 "synapse_error" | "network_error" | "NETWORK_ERR" => {
183 "check the synapse URL, network connectivity, and any configured auth token"
184 }
185 "not_found" | "missing_spore" | "not_cached" | "spore_not_found" => {
186 "verify the domain/hash and run hypha sense or hypha cache list before retrying"
187 }
188 "spore_security_rejected" => {
189 "received content targets protected control paths; inspect the spore and only relax cache.spore_reject_path_components if you accept that risk"
190 }
191 "skill_error" => "run hypha skill --help and retry with the suggested options",
192 _ => "read the error field, check hypha --help for the expected input, and retry",
193 }
194}
195
196pub struct OutSink<'a>(pub &'a Output);
201
202impl crate::EventSink for OutSink<'_> {
203 fn emit(&self, event: crate::HyphaEvent) {
204 match event {
205 crate::HyphaEvent::Progress {
206 current,
207 total,
208 message,
209 } => {
210 self.0
211 .progress(current, total, &message, serde_json::Value::Null);
212 }
213 crate::HyphaEvent::DownloadProgress {
214 downloaded_bytes,
215 total_bytes,
216 } => {
217 self.0.download_progress(downloaded_bytes, total_bytes);
218 }
219 crate::HyphaEvent::Log { message } => {
220 self.0.warn("log", &message);
221 }
222 crate::HyphaEvent::Warn { message } => {
223 self.0.warn("warn", &message);
224 }
225 }
226 }
227}