use std::sync::Arc;
use objectiveai_sdk::HttpClient;
use tokio::sync::OnceCell;
use crate::db;
use crate::filesystem;
use crate::plugin_path::PluginPath;
use crate::run::Config;
use crate::viewer_client::ViewerClient;
#[derive(Clone)]
pub struct Context {
pub config: Config,
pub filesystem: filesystem::Client,
pub plugin: Option<PluginPath>,
api: Arc<OnceCell<HttpClient>>,
viewer: Arc<OnceCell<ViewerClient>>,
db: Arc<OnceCell<db::DbHandle>>,
python: Arc<OnceCell<crate::python::Python>>,
pub no_objectiveai: bool,
}
impl Context {
pub fn new(config: Config) -> Self {
let filesystem = filesystem::Client::new(
config.objectiveai_dir.clone(),
config.objectiveai_state.clone(),
config.commit_author_name.clone(),
config.commit_author_email.clone(),
);
let plugin = PluginPath::from_parts(
config.plugin_owner.clone(),
config.plugin_repository.clone(),
config.plugin_version.clone(),
);
Self {
config,
filesystem,
plugin,
api: Arc::new(OnceCell::new()),
viewer: Arc::new(OnceCell::new()),
db: Arc::new(OnceCell::new()),
python: Arc::new(OnceCell::new()),
no_objectiveai: false,
}
}
pub fn with_no_objectiveai(&self, no_objectiveai: bool) -> Self {
let mut clone = self.clone();
clone.no_objectiveai = no_objectiveai;
clone
}
pub async fn python(&self) -> Result<&crate::python::Python, crate::error::Error> {
self.python
.get_or_try_init(|| crate::python::Python::initialize(self.filesystem.bin_dir()))
.await
}
pub async fn api_client(&self) -> Result<&HttpClient, crate::error::Error> {
self.api
.get_or_try_init(|| async {
let mut config = self
.filesystem
.read_config_view(objectiveai_sdk::cli::command::GetScope::Final)
.await?;
let address = match config.api().get_address() {
Some(a) => ensure_scheme(a),
None => crate::command::api::spawn::spawn(self).await?,
};
Ok(build_http_client(&self.config, &mut config, address))
})
.await
}
pub async fn github_http_client(
&self,
) -> Result<HttpClient, crate::error::Error> {
let mut config = self
.filesystem
.read_config_view(objectiveai_sdk::cli::command::GetScope::Final)
.await?;
let address = "https://api.invalid.objectiveai-github-client-only/".to_string();
Ok(build_http_client(&self.config, &mut config, address))
}
pub async fn resolve_mcp_timeout_ms(&self) -> Result<u64, crate::error::Error> {
Ok(self.resolve_mcp_timeout_ms_opt().await?.unwrap_or(60000))
}
pub async fn resolve_backoff_max_elapsed_time_ms(&self) -> Result<u64, crate::error::Error> {
Ok(self.resolve_backoff_max_elapsed_time_ms_opt().await?.unwrap_or(60000))
}
pub async fn resolve_backoff_max_elapsed_time_ms_opt(
&self,
) -> Result<Option<u64>, crate::error::Error> {
let mut config = self
.filesystem
.read_config_view(objectiveai_sdk::cli::command::GetScope::Final)
.await?;
Ok(config.api().get_backoff_max_elapsed_time_ms())
}
pub async fn resolve_mcp_timeout_ms_opt(
&self,
) -> Result<Option<u64>, crate::error::Error> {
let mut config = self
.filesystem
.read_config_view(objectiveai_sdk::cli::command::GetScope::Final)
.await?;
Ok(config.api().get_mcp_timeout_ms())
}
pub async fn viewer_client(&self) -> Result<&ViewerClient, crate::error::Error> {
self.viewer
.get_or_try_init(|| async {
let mut config = self
.filesystem
.read_config_view(objectiveai_sdk::cli::command::GetScope::Final)
.await?;
let address = match config.viewer().get_address() {
Some(a) => ensure_scheme(a),
None => crate::command::viewer::spawn::spawn(self).await?,
};
let signature = env("VIEWER_SIGNATURE")
.or_else(|| config.viewer().get_signature().map(String::from));
Ok(ViewerClient::new(address, signature))
})
.await
}
pub async fn db_client(&self) -> Result<&db::Pool, crate::error::Error> {
Ok(&self.db_handle().await?.pool)
}
pub async fn db_handle(&self) -> Result<&db::DbHandle, crate::error::Error> {
self.db
.get_or_try_init(|| async {
let mut config = self
.filesystem
.read_config_view(objectiveai_sdk::cli::command::GetScope::Final)
.await?;
let address = config.db().get_address().map(String::from);
let (url, address, admin_user, admin_password) = match address {
Some(address) => {
let db = config.db();
let user = db
.get_user()
.unwrap_or(crate::filesystem::config::DB_DEFAULT_USER)
.to_string();
let password = db
.get_password()
.unwrap_or(crate::filesystem::config::DB_DEFAULT_PASSWORD)
.to_string();
let url = db::config_url(&address, &user, &password);
(url, address, user, password)
}
None => {
let url = crate::command::db::spawn::spawn(self).await?;
let (address, user, password) =
parse_spawn_db_url(&url).ok_or_else(|| {
crate::error::Error::Instance(format!(
"db spawn published an unparseable URL: {url}"
))
})?;
(url, address, user, password)
}
};
let database = config
.db()
.get_database()
.unwrap_or(crate::filesystem::config::DB_DEFAULT_DATABASE)
.to_string();
let pool = db::init(&url, &database).await?;
Ok(db::DbHandle {
pool,
address,
admin_user,
admin_password,
database,
})
})
.await
}
pub fn reset_api_client(&mut self) {
self.api = Arc::new(OnceCell::new());
}
}
fn build_http_client(
cli_config: &Config,
config: &mut crate::filesystem::config::Config,
address: String,
) -> HttpClient {
let authorization =
config.api().get_objectiveai_authorization().map(String::from);
let user_agent = config.api().get_user_agent().map(String::from);
let x_title = config.api().get_x_title().map(String::from);
let http_referer = config.api().get_http_referer().map(String::from);
let x_github_authorization =
config.api().get_github_authorization().map(String::from);
let x_openrouter_authorization =
config.api().get_openrouter_authorization().map(String::from);
let x_mcp_authorization: Option<std::collections::HashMap<String, String>> =
config
.api()
.get_mcp_authorization()
.map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
let agent_instance_hierarchy = Some(cli_config.agent_instance_hierarchy.clone());
let mcp_session_id = cli_config.mcp_session_id.clone();
HttpClient::new(
reqwest::Client::new(),
Some(address),
authorization,
user_agent,
x_title,
http_referer,
x_github_authorization,
x_openrouter_authorization,
x_mcp_authorization,
None::<String>,
None::<String>,
agent_instance_hierarchy,
mcp_session_id,
)
}
pub(crate) fn ensure_scheme(a: &str) -> String {
if a.starts_with("http://") || a.starts_with("https://") {
a.to_string()
} else {
format!("http://{a}")
}
}
fn parse_spawn_db_url(url: &str) -> Option<(String, String, String)> {
let rest = url
.strip_prefix("postgresql://")
.or_else(|| url.strip_prefix("postgres://"))?;
let (credentials, address) = rest.rsplit_once('@')?;
let (user, encoded_password) = credentials.split_once(':')?;
let password = percent_encoding::percent_decode_str(encoded_password)
.decode_utf8()
.ok()?
.into_owned();
Some((address.to_string(), user.to_string(), password))
}
fn env(name: &str) -> Option<String> {
std::env::var(name).ok()
}
#[cfg(test)]
mod tests {
use super::ensure_scheme;
#[test]
fn ensure_scheme_cases() {
assert_eq!(ensure_scheme("127.0.0.1"), "http://127.0.0.1");
assert_eq!(ensure_scheme("127.0.0.1:8080"), "http://127.0.0.1:8080");
assert_eq!(ensure_scheme("::1"), "http://::1");
assert_eq!(ensure_scheme("api.x"), "http://api.x");
assert_eq!(ensure_scheme("http://api.x"), "http://api.x");
assert_eq!(ensure_scheme("https://api.x"), "https://api.x");
}
}