use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{HarnessHomes, HarnessId};
pub const PROFILES_SCHEMA: &str = "supercode.profiles.v1";
pub const PROFILE_HARNESSES: &[&str] = &[
HarnessId::SUPERCODE,
HarnessId::CODEX,
HarnessId::HERMES,
HarnessId::OPENCLAW,
HarnessId::ORCHESTRATOR,
];
pub const HERMES_DEFAULT_PROFILE: &str = "default";
pub const OPENCLAW_DEFAULT_AGENT: &str = "main";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProfileKind {
Preset,
CodexProfile,
HermesProfile,
OpenclawAgent,
OrchestratorProfile,
}
impl ProfileKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::Preset => "preset",
Self::CodexProfile => "codex_profile",
Self::HermesProfile => "hermes_profile",
Self::OpenclawAgent => "openclaw_agent",
Self::OrchestratorProfile => "orchestrator_profile",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProfileRow {
pub name: String,
pub harness: String,
pub kind: ProfileKind,
pub home: Option<PathBuf>,
pub default: bool,
pub routes: Option<u64>,
pub sessions: Option<u64>,
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worker: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ProfileError {
#[error("harness `{harness}` has no profile concept (profiles exist for: {})", PROFILE_HARNESSES.join(", "))]
UnsupportedHarness {
harness: String,
},
#[error("`{harness}` has no profile `{name}`")]
NotFound {
harness: String,
name: String,
},
}
pub fn list_profiles(
homes: &HarnessHomes,
harness: Option<&str>,
) -> Result<Vec<ProfileRow>, ProfileError> {
if let Some(harness) = harness {
if !PROFILE_HARNESSES.contains(&harness) {
return Err(ProfileError::UnsupportedHarness {
harness: harness.to_string(),
});
}
}
let mut rows = Vec::new();
for id in PROFILE_HARNESSES {
if harness.is_some_and(|requested| requested != *id) {
continue;
}
match *id {
HarnessId::SUPERCODE => rows.extend(preset_rows()),
HarnessId::CODEX => rows.extend(codex_rows(&homes.codex)),
HarnessId::HERMES => rows.extend(hermes_rows(&homes.hermes)),
HarnessId::OPENCLAW => rows.extend(openclaw_rows(&homes.openclaw)),
HarnessId::ORCHESTRATOR => rows.extend(orchestrator_rows(&homes.orchestrator)),
_ => {}
}
}
Ok(rows)
}
pub fn get_profile(
homes: &HarnessHomes,
harness: &str,
name: &str,
) -> Result<ProfileRow, ProfileError> {
list_profiles(homes, Some(harness))?
.into_iter()
.find(|row| row.name == name)
.ok_or_else(|| ProfileError::NotFound {
harness: harness.to_string(),
name: name.to_string(),
})
}
fn preset_rows() -> Vec<ProfileRow> {
let mut rows: Vec<ProfileRow> = crate::presets::RESERVED_PRESET_NAMES
.iter()
.map(|name| ProfileRow {
name: (*name).to_string(),
harness: HarnessId::SUPERCODE.to_string(),
kind: ProfileKind::Preset,
home: None,
default: *name == "supercode-default",
routes: None,
sessions: None,
model: crate::presets::lookup(name)
.and_then(|text| toml::from_str::<toml::Value>(text).ok())
.and_then(|doc| {
doc.get("core")
.and_then(|core| core.get("model"))
.and_then(toml::Value::as_str)
.map(str::to_string)
}),
worker: None,
})
.collect();
rows.sort_by(|left, right| left.name.cmp(&right.name));
rows
}
fn codex_rows(sessions_root: &Path) -> Vec<ProfileRow> {
let Some(codex_home) = sessions_root.parent() else {
return Vec::new();
};
let Ok(text) = std::fs::read_to_string(codex_home.join("config.toml")) else {
return Vec::new();
};
let Ok(doc) = toml::from_str::<toml::Value>(&text) else {
return Vec::new();
};
let selected = doc.get("profile").and_then(toml::Value::as_str);
let Some(profiles) = doc.get("profiles").and_then(toml::Value::as_table) else {
return Vec::new();
};
profiles
.iter()
.map(|(name, table)| ProfileRow {
name: name.clone(),
harness: HarnessId::CODEX.to_string(),
kind: ProfileKind::CodexProfile,
home: None,
default: selected == Some(name.as_str()),
routes: None,
sessions: None,
model: table
.get("model")
.and_then(toml::Value::as_str)
.map(str::to_string),
worker: None,
})
.collect()
}
fn hermes_rows(state_db: &Path) -> Vec<ProfileRow> {
let Some(home) = state_db.parent() else {
return Vec::new();
};
let Ok(loaded) = supercode_interchange::orchestration::codec::from_hermes(home) else {
return Vec::new();
};
let routes = loaded
.io
.get(HERMES_DEFAULT_PROFILE)
.is_some_and(|io| io.raw.contains_key("config.yaml"))
.then(|| route_counts(&loaded.orchestration));
let mut names: Vec<&String> = loaded.orchestration.profiles.keys().collect();
names.sort_by_key(|name| (name.as_str() != HERMES_DEFAULT_PROFILE, name.as_str()));
names
.into_iter()
.map(|name| {
let profile = &loaded.orchestration.profiles[name];
let is_default = name == HERMES_DEFAULT_PROFILE;
ProfileRow {
name: name.clone(),
harness: HarnessId::HERMES.to_string(),
kind: ProfileKind::HermesProfile,
home: Some(profile.dir.clone()),
default: is_default,
routes: routes
.as_ref()
.map(|counts| counts.get(name.as_str()).copied().unwrap_or(0)),
sessions: hermes_session_count(state_db, (!is_default).then_some(name.as_str())),
model: hermes_model(profile),
worker: None,
}
})
.collect()
}
fn route_counts(
orchestration: &supercode_interchange::orchestration::Orchestration,
) -> BTreeMap<String, u64> {
let mut counts: BTreeMap<String, u64> = BTreeMap::new();
for profile in orchestration.profiles.values() {
for route in &profile.routes {
*counts.entry(route.profile.clone()).or_default() += 1;
}
}
counts
}
fn orchestrator_rows(root: &Path) -> Vec<ProfileRow> {
use supercode_interchange::orchestration::codec::{load_home, Flavor};
let Ok(loaded) = load_home(root, Flavor::Orchestrator) else {
return Vec::new();
};
let routes = loaded
.io
.values()
.any(|io| io.raw.contains_key("config.yaml"))
.then(|| route_counts(&loaded.orchestration));
let mut names: Vec<&String> = loaded.orchestration.profiles.keys().collect();
names.sort_by_key(|name| (name.as_str() != HERMES_DEFAULT_PROFILE, name.as_str()));
names
.into_iter()
.map(|name| {
let profile = &loaded.orchestration.profiles[name];
ProfileRow {
name: name.clone(),
harness: HarnessId::ORCHESTRATOR.to_string(),
kind: ProfileKind::OrchestratorProfile,
default: name == HERMES_DEFAULT_PROFILE,
routes: routes
.as_ref()
.map(|counts| counts.get(name.as_str()).copied().unwrap_or(0)),
sessions: profile
.dir
.join("state.db")
.is_file()
.then(|| profile.bindings.len() as u64),
model: profile.worker.as_ref().and_then(|w| w.model.clone()),
worker: profile
.worker
.as_ref()
.map(|w| w.harness.as_str().to_string()),
home: Some(profile.dir.clone()),
}
})
.collect()
}
fn hermes_model(profile: &supercode_interchange::orchestration::Profile) -> Option<String> {
match profile.residue.config.get("model")? {
Value::String(pinned) => Some(pinned.clone()),
Value::Object(block) => block
.get("default")
.or_else(|| block.get("model"))
.and_then(Value::as_str)
.map(str::to_string),
_ => None,
}
}
fn hermes_session_count(state_db: &Path, profile: Option<&str>) -> Option<u64> {
let connection = Connection::open_with_flags(
state_db,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.ok()?;
let count: i64 = match profile {
Some(name) => connection
.query_row(
"SELECT COUNT(*) FROM sessions WHERE profile_name = ?1",
[name],
|row| row.get(0),
)
.ok()?,
None => connection
.query_row(
"SELECT COUNT(*) FROM sessions WHERE profile_name IS NULL",
[],
|row| row.get(0),
)
.ok()?,
};
Some(count.max(0) as u64)
}
fn openclaw_rows(home: &Path) -> Vec<ProfileRow> {
let Ok(loaded) = supercode_interchange::orchestration::codec::from_openclaw(home) else {
return Vec::new();
};
let by_agent: BTreeMap<&str, &str> = loaded
.profiles
.iter()
.map(|(name, io)| (io.agent_id.as_str(), name.as_str()))
.collect();
let mut names: Vec<String> = by_agent.keys().map(|id| (*id).to_string()).collect();
if let Ok(dirs) = std::fs::read_dir(home.join("agents")) {
names.extend(
dirs.flatten()
.filter(|entry| entry.path().is_dir())
.filter(|entry| !directory_is_empty(&entry.path()))
.filter_map(|entry| entry.file_name().into_string().ok()),
);
}
names.sort();
names.dedup();
let route_counts = loaded.root.config_present.then(|| {
let mut counts: BTreeMap<String, u64> = BTreeMap::new();
for route in loaded
.orchestration
.profiles
.values()
.flat_map(|profile| profile.routes.iter())
{
if let Some(agent) = route.residue.0.get("agent_id").and_then(Value::as_str) {
*counts.entry(agent.to_string()).or_default() += 1;
}
}
counts
});
names
.into_iter()
.map(|name| {
let agent_home = home.join("agents").join(&name);
let sessions = std::fs::read_dir(agent_home.join("sessions"))
.ok()
.map(|dir| {
dir.flatten()
.filter(|entry| {
let name = entry.file_name();
let name = name.to_string_lossy();
name.ends_with(".jsonl") && !name.ends_with(".trajectory.jsonl")
})
.count() as u64
});
let entry = by_agent
.get(name.as_str())
.and_then(|profile| loaded.orchestration.profiles.get(*profile))
.and_then(|profile| profile.residue.config.get("openclaw_agent"));
ProfileRow {
name: name.clone(),
harness: HarnessId::OPENCLAW.to_string(),
kind: ProfileKind::OpenclawAgent,
home: Some(agent_home),
default: loaded.root.default_agent == name,
routes: route_counts
.as_ref()
.map(|counts| counts.get(name.as_str()).copied().unwrap_or(0)),
sessions,
model: entry.and_then(|entry| match entry.get("model") {
Some(Value::String(id)) => Some(id.clone()),
Some(object) => object
.get("primary")
.and_then(Value::as_str)
.map(str::to_string),
None => None,
}),
worker: None,
}
})
.collect()
}
pub(crate) fn read_json5(path: &Path) -> Value {
std::fs::read_to_string(path)
.ok()
.and_then(|text| serde_json::from_str::<Value>(&strip_json5(&text)).ok())
.unwrap_or(Value::Null)
}
fn directory_is_empty(path: &Path) -> bool {
std::fs::read_dir(path).is_ok_and(|mut entries| entries.next().is_none())
}
fn strip_json5(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
let mut in_string = false;
let mut escaped = false;
while let Some(ch) = chars.next() {
if in_string {
out.push(ch);
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == '"' {
in_string = false;
}
continue;
}
match ch {
'"' => {
in_string = true;
out.push(ch);
}
'/' if chars.peek() == Some(&'/') => {
for next in chars.by_ref() {
if next == '\n' {
out.push('\n');
break;
}
}
}
'/' if chars.peek() == Some(&'*') => {
chars.next();
let mut previous = '\0';
for next in chars.by_ref() {
if previous == '*' && next == '/' {
break;
}
previous = next;
}
out.push(' ');
}
_ => out.push(ch),
}
}
let bytes: Vec<char> = out.chars().collect();
let mut cleaned = String::with_capacity(out.len());
let mut index = 0usize;
let mut in_string = false;
let mut escaped = false;
while index < bytes.len() {
let ch = bytes[index];
if in_string {
cleaned.push(ch);
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == '"' {
in_string = false;
}
index += 1;
continue;
}
if ch == '"' {
in_string = true;
cleaned.push(ch);
index += 1;
continue;
}
if ch == ',' {
let mut lookahead = index + 1;
while lookahead < bytes.len() && bytes[lookahead].is_whitespace() {
lookahead += 1;
}
if lookahead < bytes.len() && (bytes[lookahead] == '}' || bytes[lookahead] == ']') {
index += 1;
continue;
}
}
cleaned.push(ch);
index += 1;
}
cleaned
}