use serde::Serialize;
use std::process::ExitCode;
pub struct Output {
format: agent_first_data::OutputFormat,
}
fn built_event(
result: Result<agent_first_data::Event, agent_first_data::BuildError>,
) -> agent_first_data::Event {
result.unwrap_or_else(|err| agent_first_data::build_cli_error(&err.to_string(), None))
}
impl Output {
pub fn new(format: agent_first_data::OutputFormat) -> Self {
Self { format }
}
fn emit(&self, event: agent_first_data::Event) {
let stdout = std::io::stdout();
let mut emitter = agent_first_data::CliEmitter::new(stdout.lock(), self.format);
let _ = emitter.emit(event);
}
fn finish(&self, event: agent_first_data::Event, success_code: u8) -> ExitCode {
let stdout = std::io::stdout();
let mut emitter = agent_first_data::CliEmitter::new(stdout.lock(), self.format);
ExitCode::from(emitter.finish(event, success_code))
}
pub fn ok<T: Serialize>(&self, result: T) -> ExitCode {
let result_value = serde_json::to_value(&result).unwrap_or_default();
self.finish(agent_first_data::json_result(result_value).build(), 0)
}
pub fn ok_trace<T: Serialize>(&self, result: T, trace: impl Serialize) -> ExitCode {
let result_value = serde_json::to_value(&result).unwrap_or_default();
let trace_value = serde_json::to_value(&trace).unwrap_or_default();
self.finish(
agent_first_data::json_result(result_value)
.trace(trace_value)
.build(),
0,
)
}
pub fn ok_with_code<T: Serialize>(&self, result: T, code: u8) -> ExitCode {
let result_value = serde_json::to_value(&result).unwrap_or_default();
self.finish(agent_first_data::json_result(result_value).build(), code)
}
pub fn error(&self, error_code: &str, message: &str) -> ExitCode {
self.error_hint(error_code, message, None)
}
pub fn error_hint(&self, error_code: &str, message: &str, hint: Option<&str>) -> ExitCode {
self.finish(
built_event(
agent_first_data::json_error(error_code, message)
.hint(actionable_hint(error_code, hint))
.trace(serde_json::json!({"duration_ms": 0}))
.build(),
),
1,
)
}
pub fn error_from(&self, error_code: &str, err: &anyhow::Error) -> ExitCode {
self.error(error_code, &err.to_string())
}
pub fn error_hypha(&self, err: &crate::HyphaError) -> ExitCode {
self.error_hint(&err.code, &err.message, err.hint.as_deref())
}
pub fn progress(&self, step: u32, total: u32, message: &str, data: serde_json::Value) {
let mut fields = match data {
serde_json::Value::Object(map) => map,
_ => serde_json::Map::new(),
};
fields.insert("current".into(), step.into());
fields.insert("total".into(), total.into());
fields.insert("message".into(), message.into());
self.emit(agent_first_data::json_progress(serde_json::Value::Object(fields)).build());
}
pub fn download_progress(&self, downloaded_bytes: u64, total_bytes: Option<u64>) {
self.emit(
agent_first_data::json_progress(serde_json::json!({
"event": "download_progress",
"downloaded_bytes": downloaded_bytes,
"total_bytes": total_bytes,
}))
.build(),
);
}
pub fn warn(&self, code: &str, message: &str) {
self.emit(
agent_first_data::json_log(serde_json::json!({
"event": code,
"message": message,
}))
.build(),
);
}
pub fn startup(&self, args: serde_json::Value) {
let (config, config_error) = match crate::config::HyphaConfig::load() {
Ok(cfg) => (serde_json::to_value(&cfg).unwrap_or_default(), None),
Err(err) => (
serde_json::Value::Null,
Some(serde_json::json!({
"code": err.code,
"message": err.message,
"hint": err.hint,
})),
),
};
let env = serde_json::json!({
"CMN_HOME": std::env::var("CMN_HOME").ok(),
"SYNAPSE_TOKEN_SECRET": std::env::var("SYNAPSE_TOKEN_SECRET").ok(),
});
self.emit(
agent_first_data::json_log(serde_json::json!({
"category": "startup",
"event": "startup",
"hypha_version": env!("CARGO_PKG_VERSION"),
"config": config,
"config_error": config_error,
"args": args,
"env": env
}))
.build(),
);
}
}
fn actionable_hint<'a>(error_code: &str, hint: Option<&'a str>) -> &'a str {
if let Some(hint) = hint.filter(|h| !h.trim().is_empty()) {
return hint;
}
match error_code {
"invalid_args" | "invalid_value" | "unknown_key" | "missing_domain" => {
"run hypha --help or the relevant subcommand --help, then retry with valid inputs"
}
"invalid_uri" | "uri_error" | "cmn_invalid" => {
"use a CMN URI in the form cmn://domain or cmn://domain/b3.hash"
}
"synapse_error" | "network_error" | "NETWORK_ERR" => {
"check the synapse URL, network connectivity, and any configured auth token"
}
"not_found" | "missing_spore" | "not_cached" | "spore_not_found" => {
"verify the domain/hash and run hypha sense or hypha cache list before retrying"
}
"spore_security_rejected" => {
"received content targets protected control paths; inspect the spore and only relax cache.spore_reject_path_components if you accept that risk"
}
"skill_error" => "run hypha skill --help and retry with the suggested options",
_ => "read the error field, check hypha --help for the expected input, and retry",
}
}
pub struct OutSink<'a>(pub &'a Output);
impl crate::EventSink for OutSink<'_> {
fn emit(&self, event: crate::HyphaEvent) {
match event {
crate::HyphaEvent::Progress {
current,
total,
message,
} => {
self.0
.progress(current, total, &message, serde_json::Value::Null);
}
crate::HyphaEvent::DownloadProgress {
downloaded_bytes,
total_bytes,
} => {
self.0.download_progress(downloaded_bytes, total_bytes);
}
crate::HyphaEvent::Log { message } => {
self.0.warn("log", &message);
}
crate::HyphaEvent::Warn { message } => {
self.0.warn("warn", &message);
}
}
}
}