systemprompt-cli 0.1.18

systemprompt.io OS - CLI for agent orchestration, AI operations, and system management
Documentation
mod helpers;

use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use systemprompt_cloud::{SessionKey, SessionStore};
use systemprompt_loader::ProfileLoader;
use systemprompt_logging::CliService;
use systemprompt_models::profile_bootstrap::ProfileBootstrap;
use systemprompt_models::{Profile, SecretsBootstrap};

use super::context::CliSessionContext;
use crate::CliConfig;
use crate::paths::ResolvedPaths;
use helpers::{
    create_new_session, extract_profile_name, initialize_profile_bootstraps,
    resolve_profile_path_from_session, resolve_profile_path_without_session, try_session_from_env,
    try_validate_context,
};

pub(super) struct ProfileContext<'a> {
    pub name: &'a str,
    pub path: PathBuf,
}

async fn get_session_for_profile(
    profile_input: &str,
    config: &CliConfig,
) -> Result<CliSessionContext> {
    let (profile_path, profile) = crate::shared::resolve_profile_with_data(profile_input)
        .map_err(|e| anyhow::anyhow!("{}", e))?;

    if !ProfileBootstrap::is_initialized() {
        ProfileBootstrap::init_from_path(&profile_path)
            .with_context(|| format!("Failed to initialize profile '{}'", profile_input))?;
    }

    if !SecretsBootstrap::is_initialized() {
        SecretsBootstrap::try_init().with_context(|| {
            "Failed to initialize secrets. Check your profile's secrets configuration."
        })?;
    }

    get_session_for_loaded_profile(&profile, &profile_path, config).await
}

async fn get_session_for_loaded_profile(
    profile: &Profile,
    profile_path: &Path,
    config: &CliConfig,
) -> Result<CliSessionContext> {
    if let Some(ctx) = try_session_from_env(profile) {
        return Ok(ctx);
    }

    let profile_name = extract_profile_name(profile_path)?;
    let tenant_id = profile.cloud.as_ref().and_then(|c| c.tenant_id.as_deref());
    let session_key = SessionKey::from_tenant_id(tenant_id);
    let sessions_dir = ResolvedPaths::discover().sessions_dir()?;
    let mut store = SessionStore::load_or_create(&sessions_dir)?;

    if let Some(mut session) = store.get_valid_session(&session_key).cloned() {
        session.touch();

        if let Some(refreshed) = try_validate_context(&mut session, &profile_name).await {
            session = refreshed;
        }

        store.upsert_session(&session_key, session.clone());
        store.save(&sessions_dir)?;
        return Ok(CliSessionContext {
            session,
            profile: profile.clone(),
        });
    }

    let session_email_hint = store
        .get_session(&session_key)
        .map(|s| s.user_email.to_string());

    let profile_ctx = ProfileContext {
        name: &profile_name,
        path: profile_path.to_path_buf(),
    };

    let session = create_new_session(
        profile,
        &profile_ctx,
        &session_key,
        config,
        session_email_hint.as_deref(),
    )
    .await?;

    store.upsert_session(&session_key, session.clone());
    store.set_active_with_profile(&session_key, &profile_name);
    store.save(&sessions_dir)?;

    if session.session_token.as_str().is_empty() {
        anyhow::bail!("Session token is empty. Session creation failed.");
    }

    Ok(CliSessionContext {
        session,
        profile: profile.clone(),
    })
}

async fn try_session_from_active_key(config: &CliConfig) -> Result<Option<CliSessionContext>> {
    let paths = ResolvedPaths::discover();
    let sessions_dir = paths.sessions_dir()?;
    let store = SessionStore::load_or_create(&sessions_dir)?;

    let Some(ref active_key_str) = store.active_key else {
        return Ok(None);
    };

    let active_key = store
        .active_session_key()
        .ok_or_else(|| anyhow::anyhow!("Invalid active session key: {}", active_key_str))?;

    let active_profile = store.active_profile_name.as_deref();

    let profile_path = if let Some(session) = store.active_session() {
        match resolve_profile_path_from_session(session, active_profile)? {
            Some(path) => path,
            None => return Ok(None),
        }
    } else {
        resolve_profile_path_without_session(&paths, &store, &active_key, active_profile)?
    };

    let profile = ProfileLoader::load_from_path(&profile_path).with_context(|| {
        format!(
            "Failed to load profile from stored path: {}",
            profile_path.display()
        )
    })?;

    initialize_profile_bootstraps(&profile_path)?;

    let ctx = get_session_for_loaded_profile(&profile, &profile_path, config).await?;
    Ok(Some(ctx))
}

pub async fn get_or_create_session(config: &CliConfig) -> Result<CliSessionContext> {
    let ctx = resolve_session(config).await?;

    if config.is_interactive() {
        let tenant = ctx
            .session
            .tenant_key
            .as_ref()
            .map_or("local", systemprompt_identifiers::TenantId::as_str);
        CliService::session_context_with_url(
            ctx.session.profile_name.as_str(),
            &ctx.session.session_id,
            Some(tenant),
            Some(&ctx.profile.server.api_external_url),
        );
    }

    Ok(ctx)
}

async fn resolve_session(config: &CliConfig) -> Result<CliSessionContext> {
    if let Some(ref profile_name) = config.profile_override {
        return get_session_for_profile(profile_name, config).await;
    }

    let env_profile_set = std::env::var("SYSTEMPROMPT_PROFILE").is_ok();

    if !env_profile_set {
        if let Some(ctx) = try_session_from_active_key(config).await? {
            return Ok(ctx);
        }
    }

    let profile = ProfileBootstrap::get()
        .map_err(|_| {
            anyhow::anyhow!(
                "Profile required.\n\nSet SYSTEMPROMPT_PROFILE environment variable to your \
                 profile.yaml path, or use --profile <name>."
            )
        })?
        .clone();

    let profile_path_str = ProfileBootstrap::get_path().map_err(|_| {
        anyhow::anyhow!(
            "Profile path required.\n\nSet SYSTEMPROMPT_PROFILE environment variable or use \
             --profile <name>."
        )
    })?;

    let profile_path = Path::new(profile_path_str);
    get_session_for_loaded_profile(&profile, profile_path, config).await
}