use std::path::PathBuf;
use anyhow::{bail, Result};
use crate::cli::{OutputFormat, WhoamiArgs};
use crate::path;
const SESSION_ID_ENV: &str = "CLAUDE_CODE_SESSION_ID";
const SESSION_ID_ENV_ALIAS: &str = "CODEX_COMPANION_SESSION_ID";
pub const AMBIGUOUS_GUIDANCE: &str = "cannot identify the calling session: \
CLAUDE_CODE_SESSION_ID is not set (old Claude Code build, or running outside Claude \
Code). Do NOT trust most-recent-mtime — many sessions may be live at once. Pass an \
explicit `@<uuid>` target: your id is the basename of your own transcript jsonl, \
or grep a unique recent line you wrote to disambiguate.";
#[must_use]
pub fn detect_session_id() -> Option<String> {
if let Ok(v) = std::env::var(SESSION_ID_ENV) {
if !v.trim().is_empty() {
return Some(v.trim().to_string());
}
}
if let Ok(v) = std::env::var(SESSION_ID_ENV_ALIAS) {
if !v.trim().is_empty() {
return Some(v.trim().to_string());
}
}
None
}
#[derive(Debug, Clone)]
pub struct WhoAmI {
pub session_id: String,
pub path: Option<PathBuf>,
}
pub fn run_whoami(args: &WhoamiArgs) -> Result<()> {
match args.self_target.as_deref() {
None | Some("@main") => run_whoami_env(args),
Some(t) if t.starts_with("@trap:") => {
run_whoami_trap(t.strip_prefix("@trap:").unwrap_or(""), args)
}
Some(other) => bail!(
"whoami accepts no target except `@trap:<marker>` (which SUBAGENT am I?) or `@main` \
(the calling top-level session — the default). Got `{other}`. To inspect a DIFFERENT \
session, use `csift list @<uuid>` / `csift agents @<uuid>`."
),
}
}
fn run_whoami_env(args: &WhoamiArgs) -> Result<()> {
let Some(session_id) = detect_session_id() else {
bail!("{AMBIGUOUS_GUIDANCE}");
};
let path = locate_transcript(&session_id);
let me = WhoAmI { session_id, path };
match args.format {
OutputFormat::Text => render_text(&me),
OutputFormat::Json => render_json(&me)?,
}
Ok(())
}
fn run_whoami_trap(marker: &str, args: &WhoamiArgs) -> Result<()> {
use serde_json::json;
let chain = path::resolve_trap_who(marker)?;
match args.format {
OutputFormat::Text => {
for (i, n) in chain.iter().enumerate() {
let role = if n.is_subagent { "subagent" } else { "session" };
let annot = match (i, n.is_subagent, n.depth) {
(0, true, Some(d)) => format!(" <- you (subagent, depth {d})"),
(0, true, None) => " <- you (subagent)".to_string(),
(0, false, _) => " <- you (top-level session, not a subagent)".to_string(),
(_, true, Some(d)) => format!(" ^ parent subagent (depth {d})"),
(_, true, None) => " ^ parent subagent".to_string(),
(_, false, _) => " ^ top-level root".to_string(),
};
println!("{role:8} {}{annot}", n.session_id);
}
match chain.first().and_then(|n| n.path.as_ref()) {
Some(p) => println!("path {}", p.display()),
None => println!("path <transcript not found under projects root>"),
}
}
OutputFormat::Json => {
println!("{}", crate::text::envelope_header("whoami", json!({})));
for n in &chain {
let obj = json!({
"kind": "identity",
"session_id": n.session_id,
"is_subagent": n.is_subagent,
"parent_session_id": n.parent_session_id,
"depth": n.depth,
"path": n.path.as_ref().map(|p| p.to_string_lossy().into_owned()),
});
println!("{}", serde_json::to_string(&obj)?);
}
println!(
"{}",
crate::text::envelope_summary(json!({"identities": chain.len()}))
);
}
}
Ok(())
}
fn locate_transcript(session_id: &str) -> Option<PathBuf> {
let root = path::projects_root().ok()?;
let filename = format!("{session_id}.jsonl");
if let Ok(cwd) = std::env::current_dir() {
let dir = root.join(path::encode_cwd(&cwd));
let candidate = dir.join(&filename);
if candidate.is_file() {
return Some(candidate);
}
}
let dirs = path::all_project_dirs().ok()?;
for pd in dirs {
let candidate = pd.dir.join(&filename);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
fn render_text(me: &WhoAmI) {
println!("session {}", me.session_id);
match &me.path {
Some(p) => println!("path {}", p.display()),
None => println!("path <not found under projects root for the current cwd>"),
}
}
fn render_json(me: &WhoAmI) -> Result<()> {
use serde_json::json;
println!("{}", crate::text::envelope_header("whoami", json!({})));
let obj = json!({
"kind": "identity",
"session_id": me.session_id,
"is_subagent": false,
"parent_session_id": me.session_id,
"depth": 0,
"path": me.path.as_ref().map(|p| p.to_string_lossy().into_owned()),
});
println!("{}", serde_json::to_string(&obj)?);
println!(
"{}",
crate::text::envelope_summary(json!({"identities": 1}))
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn guidance_mentions_session_target_and_no_mtime() {
assert!(AMBIGUOUS_GUIDANCE.contains("@<uuid>"));
assert!(AMBIGUOUS_GUIDANCE.contains("mtime"));
assert!(AMBIGUOUS_GUIDANCE.contains("CLAUDE_CODE_SESSION_ID"));
}
#[test]
fn exact_env_name_is_canonical_not_a_loose_regex() {
assert_eq!(SESSION_ID_ENV, "CLAUDE_CODE_SESSION_ID");
assert_ne!(SESSION_ID_ENV, "SECURITYSESSIONID");
}
#[test]
fn detect_trims_and_blank_is_none() {
assert!(" ".trim().is_empty());
}
}