supercode_harness/
hermes_import.rs1use std::path::{Path, PathBuf};
14use std::process::Command;
15
16use serde::Serialize;
17use supercode_interchange::{Session, SessionFormat};
18
19use crate::Result;
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
23pub struct HermesImported {
24 pub session_id: String,
26 pub home: PathBuf,
28 pub rollout: PathBuf,
30 pub via: &'static str,
32}
33
34pub fn hermes_home() -> PathBuf {
36 std::env::var_os("HERMES_HOME")
37 .map(PathBuf::from)
38 .unwrap_or_else(|| {
39 let home = supercode_interchange::user_home()
40 .map(std::path::PathBuf::into_os_string)
41 .map(PathBuf::from)
42 .unwrap_or_default();
43 home.join(".hermes")
44 })
45}
46
47pub fn import_into_hermes(session: &Session, home: Option<&Path>) -> Result<HermesImported> {
50 let home = home.map(Path::to_path_buf).unwrap_or_else(hermes_home);
51 let rollout_dir = home.join("imports").join("supercode");
52 std::fs::create_dir_all(&rollout_dir)?;
53 let stamp = std::time::SystemTime::now()
54 .duration_since(std::time::UNIX_EPOCH)
55 .map(|d| d.as_millis())
56 .unwrap_or(0);
57 let stem = session
58 .meta
59 .session_id
60 .clone()
61 .unwrap_or_else(|| "session".into());
62 let rollout = rollout_dir.join(format!("rollout-{stamp}-{stem}.jsonl"));
63 std::fs::write(&rollout, session.to_jsonl(SessionFormat::Codex)?)?;
64 let output = Command::new("hermes")
65 .args(["sessions", "import", "--from", "codex"])
66 .arg(&rollout)
67 .env("HERMES_HOME", &home)
68 .env("HERMES_NO_ONBOARDING", "1")
69 .env("HERMES_NONINTERACTIVE", "1")
70 .output()
71 .map_err(|e| {
72 crate::Error::Other(format!("`hermes sessions import` could not start: {e}"))
73 })?;
74 let stdout = String::from_utf8_lossy(&output.stdout);
75 let session_id = stdout
76 .lines()
77 .find_map(|line| line.split(" session as ").nth(1))
78 .map(|rest| rest.trim().to_string())
79 .filter(|s| !s.is_empty());
80 match (output.status.success(), session_id) {
81 (true, Some(session_id)) => Ok(HermesImported {
82 session_id,
83 home,
84 rollout,
85 via: "hermes sessions import --from codex",
86 }),
87 (false, _) if String::from_utf8_lossy(&output.stderr).contains("database is locked") => {
91 Err(crate::Error::Other(format!(
92 "Hermes's session database is locked by another Hermes process; Hermes waited, gave \
93 up, and imported nothing. The converted rollout is kept at {}: run `hermes sessions \
94 import --from codex {}` once Hermes is idle.",
95 rollout.display(),
96 rollout.display()
97 )))
98 }
99 _ => Err(crate::Error::Other(format!(
100 "`hermes sessions import --from codex {}` did not report a session id (exit {}): {}{}",
101 rollout.display(),
102 output.status,
103 stdout.trim(),
104 String::from_utf8_lossy(&output.stderr).trim()
105 ))),
106 }
107}