#![allow(dead_code)]
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use thiserror::Error;
pub mod claude_code;
pub mod aider;
pub mod opencode;
pub mod goose;
pub mod codex;
pub mod continue_dev;
pub mod generic_openai;
pub mod redact;
pub mod render;
pub mod upload;
pub mod import_session;
pub use import_session::SessionImporter;
#[derive(Error, Debug)]
pub enum ShareError {
#[error("provider `{0}` is not supported (no readable on-disk format)")]
UnsupportedProvider(String),
#[error("session `{0}` not found")]
NotFound(String),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("parse error: {0}")]
Parse(String),
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum MessageRole {
User,
Assistant,
System,
ToolUse,
ToolResult,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionMessage {
pub role: MessageRole,
pub content: String,
pub timestamp: Option<chrono::DateTime<chrono::Utc>>,
#[serde(default)]
pub metadata: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SessionSummary {
pub provider: String,
pub id: String,
pub project_path: Option<PathBuf>,
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
pub message_count: usize,
pub title_hint: Option<String>,
#[serde(default)]
pub imported: bool,
}
pub const IMPORT_PROVENANCE_PREFIX: &str = "[i-self import]";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SharedSession {
pub provider: String,
pub id: String,
pub project_path: Option<PathBuf>,
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
pub messages: Vec<SessionMessage>,
}
pub trait SessionProvider: Send + Sync {
fn name(&self) -> &str;
fn list_sessions(&self) -> Result<Vec<SessionSummary>, ShareError>;
fn load_session(&self, id: &str) -> Result<SharedSession, ShareError>;
}
pub fn registry() -> Vec<Box<dyn SessionProvider>> {
vec![
Box::new(claude_code::ClaudeCodeProvider::default()),
Box::new(aider::AiderProvider::default()),
Box::new(goose::GooseProvider::default()),
Box::new(codex::CodexProvider::default()),
Box::new(continue_dev::ContinueProvider::default()),
Box::new(opencode::OpenCodeProvider::default()),
Box::new(generic_openai::GenericOpenAIProvider::default()),
]
}
pub fn find_session(id: &str) -> Result<SharedSession, ShareError> {
let mut last_err: Option<ShareError> = None;
for provider in registry() {
match provider.load_session(id) {
Ok(s) => return Ok(s),
Err(ShareError::NotFound(_)) => continue,
Err(e) => last_err = Some(e),
}
}
Err(last_err.unwrap_or_else(|| ShareError::NotFound(id.to_string())))
}
pub fn list_all_sessions() -> Vec<SessionSummary> {
let mut out = Vec::new();
for provider in registry() {
match provider.list_sessions() {
Ok(s) => out.extend(s),
Err(e) => tracing::warn!("provider {} list failed: {}", provider.name(), e),
}
}
for s in &mut out {
if let Some(t) = &s.title_hint {
if t.contains(IMPORT_PROVENANCE_PREFIX) {
s.imported = true;
}
}
}
out.sort_by(|a, b| b.started_at.cmp(&a.started_at));
out
}
pub fn parse_duration(s: &str) -> Result<chrono::Duration, ShareError> {
let s = s.trim();
if s.is_empty() {
return Err(ShareError::Parse("empty duration".to_string()));
}
let (num_part, unit) = s.split_at(s.len() - 1);
let n: i64 = num_part
.parse()
.map_err(|_| ShareError::Parse(format!("invalid duration `{}`", s)))?;
match unit {
"m" => Ok(chrono::Duration::minutes(n)),
"h" => Ok(chrono::Duration::hours(n)),
"d" => Ok(chrono::Duration::days(n)),
"w" => Ok(chrono::Duration::weeks(n)),
other => Err(ShareError::Parse(format!(
"unknown duration unit `{}` — use m / h / d / w",
other
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_duration_supports_common_suffixes() {
assert_eq!(parse_duration("7d").unwrap(), chrono::Duration::days(7));
assert_eq!(parse_duration("24h").unwrap(), chrono::Duration::hours(24));
assert_eq!(parse_duration("30m").unwrap(), chrono::Duration::minutes(30));
assert_eq!(parse_duration("2w").unwrap(), chrono::Duration::weeks(2));
}
#[test]
fn parse_duration_rejects_unknown_units() {
assert!(parse_duration("1y").is_err());
assert!(parse_duration("4mo").is_err()); assert!(parse_duration("").is_err());
assert!(parse_duration("d").is_err());
assert!(parse_duration("abc").is_err());
}
}