use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::common::{Message, Meta};
use crate::error::Result;
pub struct Transcript<H: Harness = Common> {
pub meta: Meta,
pub body: H::Body,
}
impl<H: Harness> Transcript<H> {
pub fn new(meta: Meta, body: H::Body) -> Self {
Self { meta, body }
}
}
impl<H: Harness> Clone for Transcript<H>
where
H::Body: Clone,
{
fn clone(&self) -> Self {
Self {
meta: self.meta.clone(),
body: self.body.clone(),
}
}
}
impl<H: Harness> fmt::Debug for Transcript<H>
where
H::Body: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Transcript")
.field("harness", &H::NAME)
.field("meta", &self.meta)
.field("body", &self.body)
.finish()
}
}
impl<H: Harness> PartialEq for Transcript<H>
where
H::Body: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
self.meta == other.meta && self.body == other.body
}
}
pub trait Harness {
const NAME: &'static str;
type Body;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Common;
impl Harness for Common {
const NAME: &'static str = "common";
type Body = Vec<Message>;
}
pub trait Codec: Harness + Sized {
fn to_common(transcript: &Transcript<Self>) -> Result<Transcript<Common>>;
fn from_common(transcript: &Transcript<Common>) -> Result<Transcript<Self>>;
}
impl Codec for Common {
fn to_common(transcript: &Transcript<Common>) -> Result<Transcript<Common>> {
Ok(transcript.clone())
}
fn from_common(transcript: &Transcript<Common>) -> Result<Transcript<Common>> {
Ok(transcript.clone())
}
}
pub fn convert<A, B>(transcript: &Transcript<A>) -> Result<Transcript<B>>
where
A: Codec,
B: Codec,
{
B::from_common(&A::to_common(transcript)?)
}
pub trait TextCodec: Harness + Sized {
fn from_text(text: &str) -> Result<Transcript<Self>>;
fn to_text(transcript: &Transcript<Self>) -> Result<String>;
}
pub trait Store {
type H: Harness;
type Ref;
fn discover(&self) -> Result<Vec<Discovered<Self::Ref>>>;
fn load(&self, reference: &Self::Ref) -> Result<Transcript<Self::H>>;
fn save(&self, transcript: &Transcript<Self::H>) -> Result<Saved<Self::Ref>>;
fn delete(&self, reference: &Self::Ref) -> Result<()>;
fn fingerprints(&self, _refs: &[Self::Ref]) -> Result<HashMap<String, String>> {
Ok(HashMap::new())
}
}
#[derive(Debug, Clone)]
pub struct Discovered<R> {
pub meta: Meta,
pub reference: R,
}
#[derive(Debug, Clone)]
pub struct Saved<R> {
pub id: String,
pub reference: R,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HarnessId {
ClaudeCode,
Codex,
OpenCode,
Pi,
Campfire,
Cursor,
Grok,
}
impl HarnessId {
pub const ALL: [HarnessId; 7] = [
HarnessId::ClaudeCode,
HarnessId::Codex,
HarnessId::OpenCode,
HarnessId::Pi,
HarnessId::Campfire,
HarnessId::Cursor,
HarnessId::Grok,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
HarnessId::ClaudeCode => "claude_code",
HarnessId::Codex => "codex",
HarnessId::OpenCode => "opencode",
HarnessId::Pi => "pi",
HarnessId::Campfire => "campfire",
HarnessId::Cursor => "cursor",
HarnessId::Grok => "grok",
}
}
}
impl fmt::Display for HarnessId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for HarnessId {
type Err = crate::error::Error;
fn from_str(s: &str) -> Result<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"claude" | "claude_code" | "claude-code" | "claudecode" => Ok(HarnessId::ClaudeCode),
"codex" => Ok(HarnessId::Codex),
"opencode" | "open_code" | "open-code" => Ok(HarnessId::OpenCode),
"pi" => Ok(HarnessId::Pi),
"campfire" => Ok(HarnessId::Campfire),
"cursor" | "cursor_cli" | "cursor-cli" | "cursorcli" => Ok(HarnessId::Cursor),
"grok" | "grok_cli" | "grok-cli" | "grokcli" | "grok_build" | "grok-build" => {
Ok(HarnessId::Grok)
}
other => Err(crate::error::Error::UnknownHarness(other.to_string())),
}
}
}