use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use crate::common;
use crate::harness::{
amp, antigravity, campfire, claude_code, codex, cowork, cursor, cursor_desktop, grok, hermes,
opencode, pi, simple,
};
use crate::transcript::{Codec, Common, HarnessId, TextCodec, Transcript};
#[wasm_bindgen]
pub fn convert(input: &str, from: &str, to: &str) -> Result<String, JsError> {
parse_harness(from)
.and_then(|from| parse_harness(to).map(|to| (from, to)))
.and_then(|(from, to)| {
parse_to_common(from, input)
.and_then(|common| render_from_common(to, &common))
.map_err(js)
})
}
#[wasm_bindgen(js_name = toCommon)]
pub fn to_common(input: &str, from: &str) -> Result<String, JsError> {
parse_harness(from)
.and_then(|from| parse_to_common(from, input).map_err(js))
.and_then(|common| {
serde_json::to_string(&CommonJson {
meta: common.meta,
messages: common.body,
})
.map_err(js)
})
}
#[wasm_bindgen(js_name = fromCommon)]
pub fn from_common(common_json: &str, to: &str) -> Result<String, JsError> {
let to = parse_harness(to)?;
serde_json::from_str(common_json)
.map_err(js)
.and_then(|parsed: CommonJson| {
render_from_common(to, &Transcript::new(parsed.meta, parsed.messages)).map_err(js)
})
}
#[wasm_bindgen]
pub fn harnesses() -> Vec<String> {
HarnessId::ALL
.iter()
.map(|h| h.as_str().to_string())
.collect()
}
#[cfg(feature = "search")]
#[wasm_bindgen(js_name = searchTranscript)]
pub fn search_transcript(input: &str, from: &str, query_json: &str) -> Result<String, JsError> {
let from = parse_harness(from)?;
let common = parse_to_common(from, input).map_err(js)?;
let query: crate::search::Query = serde_json::from_str(query_json).map_err(js)?;
serde_json::to_string(&crate::search::search(&common, &query)).map_err(js)
}
#[cfg(feature = "search")]
#[wasm_bindgen]
pub struct Searcher {
index: crate::search::Index,
}
#[cfg(feature = "search")]
#[wasm_bindgen]
impl Searcher {
#[wasm_bindgen(constructor)]
#[must_use]
pub fn new() -> Searcher {
Searcher {
index: crate::search::Index::new(),
}
}
pub fn insert(&mut self, harness: &str, id: &str, input: &str) -> Result<(), JsError> {
let harness = parse_harness(harness)?;
let common = parse_to_common(harness, input).map_err(js)?;
let key = crate::search::DocKey {
harness,
id: id.to_string(),
source: None,
};
self.index.insert(key, &common);
Ok(())
}
pub fn remove(&mut self, harness: &str, id: &str) -> Result<bool, JsError> {
let key = crate::search::DocKey {
harness: parse_harness(harness)?,
id: id.to_string(),
source: None,
};
Ok(self.index.remove(&key))
}
#[must_use]
pub fn len(&self) -> usize {
self.index.len()
}
pub fn query(&self, query_json: &str) -> Result<String, JsError> {
#[derive(Serialize)]
struct MatchJson<'a> {
key: &'a crate::search::DocKey,
meta: &'a common::Meta,
score: u32,
hits: Vec<crate::search::Hit>,
}
let query: crate::search::Query = serde_json::from_str(query_json).map_err(js)?;
let matches: Vec<MatchJson> = self
.index
.query(&query)
.into_iter()
.map(|m| MatchJson {
key: m.key,
meta: m.meta,
score: m.score,
hits: m.hits,
})
.collect();
serde_json::to_string(&matches).map_err(js)
}
}
#[cfg(feature = "search")]
impl Default for Searcher {
fn default() -> Searcher {
Searcher::new()
}
}
#[derive(Serialize, Deserialize)]
struct CommonJson {
meta: common::Meta,
messages: Vec<common::Message>,
}
fn parse_to_common(harness: HarnessId, text: &str) -> crate::Result<Transcript<Common>> {
fn go<H: TextCodec + Codec>(text: &str) -> crate::Result<Transcript<Common>> {
H::to_common(&H::from_text(text)?)
}
match harness {
HarnessId::ClaudeCode => go::<claude_code::ClaudeCode>(text),
HarnessId::Codex => go::<codex::Codex>(text),
HarnessId::OpenCode => go::<opencode::OpenCode>(text),
HarnessId::Pi => go::<pi::Pi>(text),
HarnessId::Campfire => go::<campfire::Campfire>(text),
HarnessId::Cursor => go::<cursor::Cursor>(text),
HarnessId::CursorDesktop => go::<cursor_desktop::CursorDesktop>(text),
HarnessId::Grok => go::<grok::Grok>(text),
HarnessId::Hermes => go::<hermes::Hermes>(text),
HarnessId::Amp => go::<amp::Amp>(text),
HarnessId::Antigravity => go::<antigravity::Antigravity>(text),
HarnessId::Simple => go::<simple::Simple>(text),
HarnessId::Cowork => go::<cowork::Cowork>(text),
}
}
fn render_from_common(harness: HarnessId, common: &Transcript<Common>) -> crate::Result<String> {
fn go<H: TextCodec + Codec>(common: &Transcript<Common>) -> crate::Result<String> {
H::to_text(&H::from_common(common)?)
}
match harness {
HarnessId::ClaudeCode => go::<claude_code::ClaudeCode>(common),
HarnessId::Codex => go::<codex::Codex>(common),
HarnessId::OpenCode => go::<opencode::OpenCode>(common),
HarnessId::Pi => go::<pi::Pi>(common),
HarnessId::Campfire => go::<campfire::Campfire>(common),
HarnessId::Cursor => go::<cursor::Cursor>(common),
HarnessId::CursorDesktop => go::<cursor_desktop::CursorDesktop>(common),
HarnessId::Grok => go::<grok::Grok>(common),
HarnessId::Hermes => go::<hermes::Hermes>(common),
HarnessId::Amp => go::<amp::Amp>(common),
HarnessId::Antigravity => go::<antigravity::Antigravity>(common),
HarnessId::Simple => go::<simple::Simple>(common),
HarnessId::Cowork => go::<cowork::Cowork>(common),
}
}
fn parse_harness(name: &str) -> Result<HarnessId, JsError> {
name.parse().map_err(js)
}
fn js<E: std::fmt::Display>(e: E) -> JsError {
JsError::new(&e.to_string())
}