Skip to main content

supercode_harness/
hermes_import.rs

1//! UNI-18: the Hermes write path, through Hermes's own door.
2//!
3//! Hermes 0.21.0 ships `hermes sessions import --from {claude,codex} <file>`:
4//! a per-session import that reads a foreign transcript and writes it into
5//! the Hermes session store with Hermes's OWN writer — its schema, its
6//! migrations, its WAL single-writer lock. supercode never opens a live
7//! Hermes `state.db` for writing: it renders the session as a Codex rollout
8//! (the format Hermes imports most faithfully) and hands the file to that
9//! door. The new session is Hermes's (a fresh Hermes id; `origin_json`
10//! records the rollout path and the foreign id), and `hermes --resume <id>`
11//! continues it.
12
13use std::path::{Path, PathBuf};
14use std::process::Command;
15
16use serde::Serialize;
17use supercode_interchange::{Session, SessionFormat};
18
19use crate::Result;
20
21/// What the door wrote.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
23pub struct HermesImported {
24    /// The Hermes session id the import minted; `hermes --resume <id>`.
25    pub session_id: String,
26    /// The Hermes home whose store now holds it.
27    pub home: PathBuf,
28    /// The Codex rollout Hermes read (kept: Hermes records it as provenance).
29    pub rollout: PathBuf,
30    /// The door.
31    pub via: &'static str,
32}
33
34/// `HERMES_HOME`, else `~/.hermes` — the same resolution Hermes itself uses.
35pub fn hermes_home() -> PathBuf {
36    std::env::var_os("HERMES_HOME")
37        .map(PathBuf::from)
38        .unwrap_or_else(|| {
39            let home = std::env::var_os("HOME")
40                .map(PathBuf::from)
41                .unwrap_or_default();
42            home.join(".hermes")
43        })
44}
45
46/// Write `session` into the Hermes store at `home` (default: [`hermes_home`])
47/// through `hermes sessions import`.
48pub fn import_into_hermes(session: &Session, home: Option<&Path>) -> Result<HermesImported> {
49    let home = home.map(Path::to_path_buf).unwrap_or_else(hermes_home);
50    let rollout_dir = home.join("imports").join("supercode");
51    std::fs::create_dir_all(&rollout_dir)?;
52    let stamp = std::time::SystemTime::now()
53        .duration_since(std::time::UNIX_EPOCH)
54        .map(|d| d.as_millis())
55        .unwrap_or(0);
56    let stem = session
57        .meta
58        .session_id
59        .clone()
60        .unwrap_or_else(|| "session".into());
61    let rollout = rollout_dir.join(format!("rollout-{stamp}-{stem}.jsonl"));
62    std::fs::write(&rollout, session.to_jsonl(SessionFormat::Codex)?)?;
63    let output = Command::new("hermes")
64        .args(["sessions", "import", "--from", "codex"])
65        .arg(&rollout)
66        .env("HERMES_HOME", &home)
67        .env("HERMES_NO_ONBOARDING", "1")
68        .env("HERMES_NONINTERACTIVE", "1")
69        .output()
70        .map_err(|e| {
71            crate::Error::Other(format!("`hermes sessions import` could not start: {e}"))
72        })?;
73    let stdout = String::from_utf8_lossy(&output.stdout);
74    let session_id = stdout
75        .lines()
76        .find_map(|line| line.split(" session as ").nth(1))
77        .map(|rest| rest.trim().to_string())
78        .filter(|s| !s.is_empty());
79    match (output.status.success(), session_id) {
80        (true, Some(session_id)) => Ok(HermesImported {
81            session_id,
82            home,
83            rollout,
84            via: "hermes sessions import --from codex",
85        }),
86        _ => Err(crate::Error::Other(format!(
87            "`hermes sessions import --from codex {}` did not report a session id (exit {}): {}{}",
88            rollout.display(),
89            output.status,
90            stdout.trim(),
91            String::from_utf8_lossy(&output.stderr).trim()
92        ))),
93    }
94}