use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result};
use jsonc_parser::ParseOptions;
use serde_json::Value;
use super::number_key;
use crate::telemetry::host::{self as host_telemetry, HostContextSnapshot};
const OPENCODE_CONTEXT_WINDOW_VARS: &[&str] = &[
"CCD_OPENCODE_CONTEXT_WINDOW_TOKENS",
"OPENCODE_CONTEXT_WINDOW_TOKENS",
];
pub(crate) fn current(repo_root: &Path) -> Result<Option<HostContextSnapshot>> {
let database_path = match database_path()? {
Some(path) => path,
None => return Ok(None),
};
let observed_at_epoch_s = match host_telemetry::file_mtime_epoch_s(&database_path)? {
Some(epoch_s) => epoch_s,
None => return Ok(None),
};
if host_telemetry::env_string(&["CCD_OPENCODE_DB_PATH"]).is_none()
&& !host_telemetry::is_recent(observed_at_epoch_s)?
{
return Ok(None);
}
let Some((prompt_tokens, completion_tokens, summary_message_id)) =
latest_session_metrics(&database_path)?
else {
return Ok(None);
};
let context_window = opencode_context_window(repo_root)?;
let model_name = opencode_model_name(repo_root)?
.or_else(|| host_telemetry::env_string(&["CCD_OPENCODE_MODEL", "OPENCODE_MODEL"]))
.or_else(|| host_telemetry::env_string(&["CCD_HOST_MODEL"]));
Ok(Some(HostContextSnapshot {
host: "opencode",
observed_at_epoch_s,
model_name,
context_used_pct: host_telemetry::compute_pct(prompt_tokens, context_window),
total_tokens: Some(prompt_tokens),
model_context_window: context_window,
compacted: summary_message_id
.as_deref()
.filter(|value| !value.is_empty())
.map(|_| true),
cost_usage: host_telemetry::HostCostUsage {
input_tokens: prompt_tokens,
output_tokens: completion_tokens,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
blended_total_tokens: None,
},
}))
}
fn database_path() -> Result<Option<PathBuf>> {
if let Some(path) = host_telemetry::env_string(&["CCD_OPENCODE_DB_PATH"]) {
let path = PathBuf::from(path);
return Ok(path.is_file().then_some(path));
}
if !sqlite3_available() {
return Ok(None);
}
let data_dir = match host_telemetry::env_string(&["CCD_OPENCODE_DATA_DIR", "OPENCODE_DATA_DIR"])
{
Some(path) => PathBuf::from(path),
None => host_telemetry::home_dir()?.join(".local/share/opencode"),
};
if !data_dir.is_dir() {
return Ok(None);
}
let mut candidates = Vec::new();
collect_sqlite_candidates(&data_dir, 4, &mut candidates)?;
let mut newest = None;
for candidate in candidates {
if !looks_like_session_db(&candidate)? {
continue;
}
let mtime = match host_telemetry::file_mtime_epoch_s(&candidate)? {
Some(epoch_s) => epoch_s,
None => continue,
};
match &newest {
Some((best, _)) if *best >= mtime => {}
_ => newest = Some((mtime, candidate)),
}
}
Ok(newest.map(|(_, path)| path))
}
fn collect_sqlite_candidates(
dir: &Path,
depth: usize,
candidates: &mut Vec<PathBuf>,
) -> Result<()> {
if depth == 0 {
return Ok(());
}
for entry in fs::read_dir(dir).with_context(|| format!("failed to read {}", dir.display()))? {
let entry = entry.with_context(|| format!("failed to read entry in {}", dir.display()))?;
let path = entry.path();
if path.is_dir() {
collect_sqlite_candidates(&path, depth - 1, candidates)?;
continue;
}
let matches = path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| matches!(ext, "db" | "sqlite" | "sqlite3"))
.unwrap_or(false);
if matches {
candidates.push(path);
}
}
Ok(())
}
fn looks_like_session_db(path: &Path) -> Result<bool> {
let output = sqlite_query(
path,
"SELECT name FROM sqlite_master WHERE type='table' AND name='sessions' LIMIT 1;",
)?;
Ok(output.trim() == "sessions")
}
fn latest_session_metrics(path: &Path) -> Result<Option<(u64, u64, Option<String>)>> {
let output = sqlite_query(
path,
"SELECT COALESCE(prompt_tokens, 0), COALESCE(completion_tokens, 0), COALESCE(summary_message_id, '') \
FROM sessions ORDER BY COALESCE(updated_at, created_at) DESC LIMIT 1;",
)?;
let mut parts = output.trim().splitn(3, '|');
let prompt_tokens = match parts.next().and_then(|value| value.parse::<u64>().ok()) {
Some(tokens) => tokens,
None => return Ok(None),
};
let completion_tokens = match parts.next().and_then(|value| value.parse::<u64>().ok()) {
Some(tokens) => tokens,
None => return Ok(None),
};
let summary_message_id = parts.next().map(|value| value.to_owned());
Ok(Some((prompt_tokens, completion_tokens, summary_message_id)))
}
fn sqlite_query(path: &Path, sql: &str) -> Result<String> {
let output = Command::new("sqlite3")
.arg("-batch")
.arg("-noheader")
.arg("-separator")
.arg("|")
.arg(path)
.arg(sql)
.output()
.with_context(|| format!("failed to run sqlite3 against {}", path.display()))?;
if !output.status.success() {
return Ok(String::new());
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
}
fn sqlite3_available() -> bool {
Command::new("sqlite3")
.arg("-version")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
fn opencode_context_window(repo_root: &Path) -> Result<Option<u64>> {
if let Some(value) = host_telemetry::env_u64(OPENCODE_CONTEXT_WINDOW_VARS) {
return Ok(Some(value));
}
let home = host_telemetry::home_dir()?;
let candidates = [
repo_root.join("opencode.json"),
repo_root.join("opencode.jsonc"),
home.join(".config/opencode/opencode.json"),
home.join(".config/opencode/opencode.jsonc"),
];
for candidate in candidates {
if !candidate.is_file() {
continue;
}
let contents = fs::read_to_string(&candidate)
.with_context(|| format!("failed to read {}", candidate.display()))?;
let value: Value =
match jsonc_parser::parse_to_serde_value(&contents, &ParseOptions::default()) {
Ok(Some(value)) => value,
_ => continue,
};
if let Some(context) = find_limit_context(&value) {
return Ok(Some(context));
}
}
Ok(host_telemetry::general_context_window())
}
fn opencode_model_name(repo_root: &Path) -> Result<Option<String>> {
let home = host_telemetry::home_dir()?;
let candidates = [
repo_root.join("opencode.json"),
repo_root.join("opencode.jsonc"),
home.join(".config/opencode/opencode.json"),
home.join(".config/opencode/opencode.jsonc"),
];
for candidate in candidates {
if !candidate.is_file() {
continue;
}
let contents = fs::read_to_string(&candidate)
.with_context(|| format!("failed to read {}", candidate.display()))?;
let value: Value =
match jsonc_parser::parse_to_serde_value(&contents, &ParseOptions::default()) {
Ok(Some(value)) => value,
_ => continue,
};
if let Some(model_name) = find_model_name(&value) {
return Ok(Some(model_name));
}
}
Ok(None)
}
fn find_limit_context(value: &Value) -> Option<u64> {
match value {
Value::Object(map) => {
if let Some(limit) = map.get("limit") {
if let Some(context) = number_key(limit, &["context"]) {
return Some(context);
}
}
for child in map.values() {
if let Some(context) = find_limit_context(child) {
return Some(context);
}
}
None
}
Value::Array(items) => items.iter().find_map(find_limit_context),
_ => None,
}
}
fn find_model_name(value: &Value) -> Option<String> {
match value {
Value::Object(map) => {
if let Some(Value::Object(models)) = map.get("models") {
if let Some(model_name) = models.keys().next() {
return Some(model_name.clone());
}
}
for child in map.values() {
if let Some(model_name) = find_model_name(child) {
return Some(model_name);
}
}
None
}
Value::Array(items) => items.iter().find_map(find_model_name),
_ => None,
}
}