use std::error::Error;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use clap::{Args, Subcommand, ValueEnum};
use serde_json::Value;
const ERROR_PLACEHOLDER: &str = "{error}";
#[derive(Debug, Args)]
pub struct ContextArgs {
#[command(subcommand)]
action: ContextAction,
}
#[derive(Debug, Subcommand)]
enum ContextAction {
JsonToLino {
#[arg(long, default_value = "-")]
path: PathBuf,
#[arg(short, long, default_value = "-")]
output: PathBuf,
},
Export {
#[arg(long)]
session: String,
#[arg(long, value_enum, default_value_t = ContextSource::Auto)]
source: ContextSource,
#[arg(long)]
db: Option<PathBuf>,
#[arg(long)]
log_dir: Option<PathBuf>,
#[arg(long, value_enum, default_value_t = ContextFormat::Lino)]
format: ContextFormat,
#[arg(short, long, default_value = "-")]
output: PathBuf,
},
}
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
enum ContextSource {
Auto,
Harness,
Server,
Both,
Opencode,
}
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
enum ContextFormat {
Lino,
Json,
}
pub fn run_context(args: ContextArgs) -> Result<(), Box<dyn Error>> {
match args.action {
ContextAction::JsonToLino { path, output } => {
let source = read_input(&path)?;
let value: Value = serde_json::from_str(&source)?;
write_output(&output, &formal_ai::json_lino::json_to_lino(&value))?;
}
ContextAction::Export {
session,
source,
db,
log_dir,
format,
output,
} => {
let text = export_context(&session, source, db.as_deref(), log_dir.as_deref(), format)?;
write_output(&output, &text)?;
}
}
Ok(())
}
fn export_context(
session: &str,
source: ContextSource,
db: Option<&Path>,
log_dir: Option<&Path>,
format: ContextFormat,
) -> Result<String, Box<dyn Error>> {
if source == ContextSource::Opencode {
return opencode_context(session, db, format);
}
let server = load_server_context(session, log_dir);
if matches!(source, ContextSource::Auto) {
if let Ok(context) = server {
return render_server_context(session, &context, format);
}
return opencode_context(session, db, format);
}
if source == ContextSource::Harness {
if let Ok(context) = opencode_context(session, db, format) {
return Ok(context);
}
let mut context = server?;
if let Some(object) = context.as_object_mut() {
object.remove("server_logs");
}
return render_server_context(session, &context, format);
}
let mut context = server?;
if source == ContextSource::Server {
if let Some(object) = context.as_object_mut() {
object.remove("messages");
}
}
render_server_context(session, &context, format)
}
fn load_server_context(session: &str, log_dir: Option<&Path>) -> std::io::Result<Value> {
log_dir.map_or_else(
|| formal_ai::conversation_context::load_conversation_context(session),
|directory| {
formal_ai::conversation_context::load_conversation_context_from(directory, session)
},
)
}
fn render_server_context(
session: &str,
context: &Value,
format: ContextFormat,
) -> Result<String, Box<dyn Error>> {
if format == ContextFormat::Json {
return Ok(format!("{}\n", serde_json::to_string_pretty(context)?));
}
Ok(formal_ai::conversation_context::conversation_context_to_lino(session, context))
}
fn opencode_context(
session: &str,
db: Option<&Path>,
format: ContextFormat,
) -> Result<String, Box<dyn Error>> {
const EXTRACTOR: &str = include_str!("../scripts/opencode-conversation-to-lino.py");
let mut command = Command::new("python3");
command.args(["-c", EXTRACTOR, session, "--format", "json"]);
if let Some(path) = db {
command.arg("--db").arg(path);
}
let result = command.output()?;
if !result.status.success() {
let diagnostic = String::from_utf8_lossy(&result.stderr);
let message =
config("context_opencode_export_failed").replace(ERROR_PLACEHOLDER, diagnostic.trim());
return Err(message.into());
}
let context: Value = serde_json::from_slice(&result.stdout)?;
render_server_context(session, &context, format)
}
fn config(key: &str) -> String {
formal_ai::seed::agent_info()
.remove(key)
.unwrap_or_else(|| key.to_owned())
}
fn read_input(path: &Path) -> Result<String, Box<dyn Error>> {
if path.as_os_str() == "-" {
let mut input = String::new();
std::io::stdin().read_to_string(&mut input)?;
Ok(input)
} else {
Ok(std::fs::read_to_string(path)?)
}
}
fn write_output(path: &Path, text: &str) -> Result<(), Box<dyn Error>> {
if path.as_os_str() == "-" {
std::io::stdout().write_all(text.as_bytes())?;
} else {
std::fs::write(path, text)?;
}
Ok(())
}