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();
};
if !home.is_dir() {
return Vec::new();
}
let config = std::fs::read_to_string(home.join("config.yaml")).ok();
let gateway = yaml_child(config.as_deref().unwrap_or_default(), "gateway");
let profile_routes = yaml_child(&gateway, "profile_routes");
let routes = config
.as_ref()
.map(|_| count_yaml_route_targets(&profile_routes));
let mut names = vec![HERMES_DEFAULT_PROFILE.to_string()];
if let Ok(entries) = std::fs::read_dir(home.join("profiles")) {
let mut found: Vec<String> = entries
.flatten()
.filter(|entry| entry.path().is_dir())
.filter_map(|entry| entry.file_name().into_string().ok())
.collect();
found.sort();
names.extend(found);
}
names
.into_iter()
.map(|name| {
let is_default = name == HERMES_DEFAULT_PROFILE;
let profile_home = if is_default {
home.to_path_buf()
} else {
home.join("profiles").join(&name)
};
let model = if is_default {
config.as_deref().and_then(hermes_model)
} else {
std::fs::read_to_string(profile_home.join("config.yaml"))
.ok()
.as_deref()
.and_then(hermes_model)
};
ProfileRow {
name: name.clone(),
harness: HarnessId::HERMES.to_string(),
kind: ProfileKind::HermesProfile,
home: Some(profile_home),
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,
worker: None,
}
})
.collect()
}
fn orchestrator_rows(root: &Path) -> Vec<ProfileRow> {
let dirs = crate::orchestrator_profile_dirs(root);
let mut routes: Option<BTreeMap<String, u64>> = None;
for (_, dir) in &dirs {
let Ok(config) = std::fs::read_to_string(dir.join("config.yaml")) else {
continue;
};
let block = yaml_child(&yaml_child(&config, "gateway"), "profile_routes");
let counts = routes.get_or_insert_with(BTreeMap::new);
for (target, found) in count_yaml_route_targets(&block) {
*counts.entry(target).or_default() += found;
}
}
dirs.into_iter()
.map(|(name, dir)| {
let config = std::fs::read_to_string(dir.join("config.yaml")).ok();
let worker = config.as_deref().map(|text| yaml_child(text, "worker"));
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: orchestrator_binding_count(&dir.join("state.db")),
model: worker
.as_deref()
.and_then(|block| yaml_scalar(block, "model")),
worker: worker
.as_deref()
.and_then(|block| yaml_scalar(block, "harness")),
home: Some(dir),
}
})
.collect()
}
fn orchestrator_binding_count(state_db: &Path) -> 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 = connection
.query_row("SELECT COUNT(*) FROM bindings", [], |row| row.get(0))
.ok()?;
Some(count.max(0) as u64)
}
fn hermes_model(config: &str) -> Option<String> {
if let Some(pinned) = yaml_scalar(config, "model") {
return Some(pinned);
}
let block = yaml_child(config, "model");
yaml_scalar(&block, "default").or_else(|| yaml_scalar(&block, "model"))
}
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 config_path = home.join("openclaw.json");
let config = read_json5(&config_path);
let entries = config.pointer("/agents/list").map(entry_map);
let bindings = config.pointer("/bindings").and_then(Value::as_array);
let entries = entries.or_else(|| config.pointer("/agents/entries").map(entry_map));
let route_counts = (!config.is_null()).then(|| {
bindings
.map(|list| count_binding_targets(list))
.unwrap_or_default()
});
let declared_ids: Vec<&str> = entries
.iter()
.flatten()
.map(|(id, _)| id.as_str())
.collect();
let mut names: Vec<String> = Vec::new();
if let Ok(dirs) = std::fs::read_dir(home.join("agents")) {
names.extend(
dirs.flatten()
.filter(|entry| entry.path().is_dir())
.filter(|entry| {
declared_ids.contains(&entry.file_name().to_string_lossy().as_ref())
|| !directory_is_empty(&entry.path())
})
.filter_map(|entry| entry.file_name().into_string().ok()),
);
}
if let Some(declared) = &entries {
names.extend(declared.iter().map(|(id, _)| id.clone()));
}
names.sort();
names.dedup();
let declared_default = entries.as_ref().and_then(|entries| {
entries
.iter()
.find(|(_, entry)| entry.get("default").and_then(Value::as_bool) == Some(true))
.map(|(id, _)| id.clone())
.or_else(|| {
entries
.iter()
.find(|(id, _)| id == OPENCLAW_DEFAULT_AGENT)
.map(|(id, _)| id.clone())
})
.or_else(|| entries.first().map(|(id, _)| id.clone()))
});
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 = entries.as_ref().and_then(|entries| {
entries
.iter()
.find(|(id, _)| *id == name)
.map(|(_, entry)| entry)
});
ProfileRow {
name: name.clone(),
harness: HarnessId::OPENCLAW.to_string(),
kind: ProfileKind::OpenclawAgent,
home: Some(agent_home),
default: declared_default.as_deref() == Some(name.as_str()),
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 count_binding_targets(list: &[Value]) -> BTreeMap<String, u64> {
let mut counts: BTreeMap<String, u64> = BTreeMap::new();
for binding in list {
if let Some(agent) = binding.get("agentId").and_then(Value::as_str) {
*counts.entry(agent.to_string()).or_default() += 1;
}
}
counts
}
fn entry_map(value: &Value) -> Vec<(String, Value)> {
match value {
Value::Array(list) => list
.iter()
.filter_map(|entry| {
entry
.get("id")
.or_else(|| entry.get("agentId"))
.and_then(Value::as_str)
.map(|id| (id.to_string(), entry.clone()))
})
.collect(),
Value::Object(map) => map
.iter()
.map(|(name, entry)| (name.clone(), entry.clone()))
.collect(),
_ => Vec::new(),
}
}
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
}
pub(crate) fn yaml_child(text: &str, key: &str) -> String {
let mut out = String::new();
let mut parent_indent: Option<usize> = None;
for line in text.lines() {
let trimmed = line.trim_start();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let indent = line.len() - trimmed.len();
match parent_indent {
None => {
if yaml_key(trimmed).is_some_and(|found| found == key) {
parent_indent = Some(indent);
}
}
Some(parent) => {
if indent <= parent {
break;
}
out.push_str(line);
out.push('\n');
}
}
}
out
}
pub(crate) fn yaml_scalar(text: &str, key: &str) -> Option<String> {
let root = text
.lines()
.filter(|line| !line.trim_start().is_empty() && !line.trim_start().starts_with('#'))
.map(|line| line.len() - line.trim_start().len())
.min()?;
for line in text.lines() {
let trimmed = line.trim_start();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if line.len() - trimmed.len() != root {
continue;
}
if yaml_key(trimmed) != Some(key) {
continue;
}
let value = yaml_value(trimmed)?;
if !value.is_empty() {
return Some(value);
}
}
None
}
fn count_yaml_route_targets(block: &str) -> BTreeMap<String, u64> {
let mut counts: BTreeMap<String, u64> = BTreeMap::new();
let entry_indent = block
.lines()
.filter(|line| !line.trim_start().is_empty() && !line.trim_start().starts_with('#'))
.map(|line| line.len() - line.trim_start().len())
.min();
for line in block.lines() {
let trimmed = line.trim_start();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let indent = line.len() - trimmed.len();
let body = trimmed.strip_prefix("- ").unwrap_or(trimmed);
let (Some(key), Some(value)) = (yaml_key(body), yaml_value(body)) else {
continue;
};
if key == "profile" && !value.is_empty() {
*counts.entry(value).or_default() += 1;
} else if Some(indent) == entry_indent && !trimmed.starts_with("- ") && !value.is_empty() {
*counts.entry(value).or_default() += 1;
}
}
counts
}
pub(crate) fn yaml_key(line: &str) -> Option<&str> {
let (head, _) = line.split_once(':')?;
let head = head.trim();
(!head.is_empty() && !head.contains(char::is_whitespace)).then_some(head)
}
fn yaml_value(line: &str) -> Option<String> {
let (_, tail) = line.split_once(':')?;
let tail = tail.trim();
let tail = tail.split_once(" #").map(|(head, _)| head).unwrap_or(tail);
Some(
tail.trim()
.trim_matches(|ch| ch == '"' || ch == '\'')
.to_string(),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn presets_are_supercodes_profiles_with_the_default_flagged() {
let rows = preset_rows();
assert_eq!(rows.len(), crate::presets::RESERVED_PRESET_NAMES.len());
let default: Vec<&str> = rows
.iter()
.filter(|row| row.default)
.map(|row| row.name.as_str())
.collect();
assert_eq!(default, ["supercode-default"]);
let cc = rows.iter().find(|row| row.name == "cc-parity").unwrap();
assert_eq!(cc.kind, ProfileKind::Preset);
assert_eq!(cc.model.as_deref(), Some("anthropic/claude-opus-4-8"));
assert!(cc.home.is_none());
}
#[test]
fn an_emptied_agent_directory_is_not_an_agent() {
let root = std::env::temp_dir().join(format!(
"supercode-profiles-shell-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(root.join("agents/deleted")).unwrap();
std::fs::create_dir_all(root.join("agents/undeclared/sessions")).unwrap();
std::fs::create_dir_all(root.join("agents/main")).unwrap();
std::fs::write(
root.join("openclaw.json"),
r#"{"agents": {"list": [{"id": "main"}]}}"#,
)
.unwrap();
let names: Vec<String> = openclaw_rows(&root)
.into_iter()
.map(|row| row.name)
.collect();
assert_eq!(names, ["main", "undeclared"], "{names:?}");
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn orchestrator_profiles_are_folders_carrying_their_own_worker() {
let root = std::env::temp_dir().join(format!(
"supercode-profiles-orchestrator-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(root.join("profiles/ops")).unwrap();
std::fs::write(
root.join("config.yaml"),
"worker:\n harness: claude-code\n model: claude-opus-4-8\ngateway:\n profile_routes:\n - platform: slack\n profile: ops\n",
)
.unwrap();
std::fs::write(
root.join("profiles/ops/config.yaml"),
"worker:\n harness: codex\n",
)
.unwrap();
let rows = orchestrator_rows(&root);
let names: Vec<&str> = rows.iter().map(|row| row.name.as_str()).collect();
assert_eq!(names, ["default", "ops"], "{names:?}");
assert!(rows[0].default && !rows[1].default);
assert_eq!(rows[0].kind, ProfileKind::OrchestratorProfile);
assert_eq!(rows[0].worker.as_deref(), Some("claude-code"));
assert_eq!(rows[0].model.as_deref(), Some("claude-opus-4-8"));
assert_eq!(rows[1].worker.as_deref(), Some("codex"));
assert_eq!(rows[1].model, None);
assert_eq!(rows[1].routes, Some(1));
assert_eq!(rows[0].routes, Some(0));
assert_eq!(rows[0].sessions, None);
assert_eq!(rows[0].home.as_deref(), Some(root.as_path()));
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn unsupported_harness_is_refused_not_silently_empty() {
let error = list_profiles(&HarnessHomes::default(), Some(HarnessId::CLAUDE_CODE))
.expect_err("claude-code has no profile concept");
assert_eq!(
error,
ProfileError::UnsupportedHarness {
harness: HarnessId::CLAUDE_CODE.to_string()
}
);
}
#[test]
fn yaml_reader_counts_both_documented_route_shapes() {
let listed = "gateway:\n profile_routes:\n - platform: slack\n chat_id: C1\n profile: coder\n - platform: discord\n profile: coder\n";
let block = yaml_child(&yaml_child(listed, "gateway"), "profile_routes");
assert_eq!(count_yaml_route_targets(&block).get("coder"), Some(&2));
let flat = "gateway:\n profile_routes:\n slack: coder\n discord: main\n";
let block = yaml_child(&yaml_child(flat, "gateway"), "profile_routes");
let counts = count_yaml_route_targets(&block);
assert_eq!(counts.get("coder"), Some(&1));
assert_eq!(counts.get("main"), Some(&1));
let nested = "gateway:\n profile_routes:\n slack:\n profile: coder\n";
let block = yaml_child(&yaml_child(nested, "gateway"), "profile_routes");
assert_eq!(count_yaml_route_targets(&block).get("coder"), Some(&1));
}
#[test]
fn hermes_model_reads_the_block_form_the_real_config_writes() {
let real = "model:\n # Default model to use\n default: \"anthropic/claude-opus-4.6\"\n\n # provider: auto\ntools:\n enabled: true\n";
assert_eq!(
hermes_model(real).as_deref(),
Some("anthropic/claude-opus-4.6")
);
let alias = "model:\n model: anthropic/claude-opus-4.6\n";
assert_eq!(
hermes_model(alias).as_deref(),
Some("anthropic/claude-opus-4.6")
);
let flat = "model: anthropic/claude-opus-4.6\n";
assert_eq!(
hermes_model(flat).as_deref(),
Some("anthropic/claude-opus-4.6")
);
assert_eq!(hermes_model("gateway:\n port: 1\n"), None);
}
#[test]
fn yaml_child_stops_at_the_next_sibling_key() {
let text = "gateway:\n profile_routes:\n - profile: coder\nmodel: sonnet\n";
assert_eq!(yaml_scalar(text, "model").as_deref(), Some("sonnet"));
let block = yaml_child(&yaml_child(text, "gateway"), "profile_routes");
assert!(!block.contains("model"), "{block}");
}
#[test]
fn json5_comments_and_trailing_commas_are_tolerated() {
let text = "{\n // the default agent\n \"agents\": { \"entries\": { \"main\": { \"default\": true, } } },\n /* routes */\n \"bindings\": [ { \"agentId\": \"main\" }, ],\n \"note\": \"https://example.test/x\",\n}\n";
let value: Value = serde_json::from_str(&strip_json5(text)).unwrap();
assert_eq!(value["note"], "https://example.test/x");
assert_eq!(value["bindings"].as_array().unwrap().len(), 1);
assert_eq!(value["agents"]["entries"]["main"]["default"], true);
}
}