xagent-pi 0.2.8

Self-contained local brain (chat UI + API + SSE) for the Pi agent, tunneled into xagent-service.
//! REST: create/load a session via `POST /cli/sessions`.

use anyhow::{bail, Context, Result};
use serde_json::{json, Value};

pub struct CreatedSession {
    pub id: String,
    pub metadata_version: u64,
}

pub async fn create_session(
    hub_url: &str,
    token: &str,
    machine_id: &str,
    session_id: Option<&str>,
    session_name: &str,
    cwd: &str,
    hostname: &str,
    app_version: &str,
) -> Result<CreatedSession> {
    let client = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(30))
        .build()?;

    // XAGENT get-or-create keys on `tag` (a fresh UUID per session by default,
    // matching the TS CLI which does `tag = randomUUID()`). `id` is omitted so
    // the hub mints one; pass `--session-id` only to resume a specific session.
    let tag = session_id
        .map(|s| s.to_string())
        .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());

    let mut body = json!({
        "tag": tag,
        "metadata": {
            "path": cwd,
            "host": hostname,
            "machineId": machine_id,
            "flavor": "pi",
            "version": app_version,
            "os": std::env::consts::OS,
            "name": session_name,
            "capabilities": { "terminal": false },
            "startedBy": "terminal",
            "lifecycleState": "running"
        },
        "machine": {
            "id": machine_id,
            "metadata": {
                "host": hostname,
                "os": std::env::consts::OS,
                "version": app_version,
                "machineId": machine_id
            }
        }
    });
    if let Some(sid) = session_id {
        body["id"] = json!(sid);
    }

    let url = format!("{}/cli/sessions", hub_url.trim_end_matches('/'));
    log::debug!("POST {url} body={body}");

    let resp = client
        .post(&url)
        .header("Authorization", format!("Bearer {token}"))
        .header("Content-Type", "application/json")
        .header("X-Xagent-Protocol-Version", "1")
        .json(&body)
        .send()
        .await
        .context("POST /cli/sessions")?;

    let status = resp.status();
    let text = resp.text().await.unwrap_or_default();
    if !status.is_success() {
        bail!("POST /cli/sessions failed {status}: {}", truncate(&text, 500));
    }
    let v: Value = serde_json::from_str(&text)
        .with_context(|| format!("parsing /cli/sessions response: {}", truncate(&text, 500)))?;
    let session = v
        .get("session")
        .ok_or_else(|| anyhow::anyhow!("missing session in response: {}", truncate(&text, 300)))?;
    let returned_id = session
        .get("id")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
        .ok_or_else(|| anyhow::anyhow!("missing session.id"))?;
    let metadata_version = session
        .get("metadataVersion")
        .and_then(|v| v.as_u64())
        .unwrap_or(0);

    Ok(CreatedSession {
        id: returned_id,
        metadata_version,
    })
}

fn truncate(s: &str, n: usize) -> String {
    if s.len() <= n {
        s.to_string()
    } else {
        format!("{}", &s[..n])
    }
}