Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to the Toolpath workspace are documented here.

## toolpath-claude 0.12.3 (2026-08-21)

- `ConversationWriter::write_conversation<W: Write>(&conv, w)` writes a conversation in Claude Code session-file layout: preamble lines, then entries, one JSON value per line, newline-terminated. It is the inverse of `ConversationReader::read_conversation`. `p export claude` writes through it.

## toolpath 0.7.1 — 2026-08-14

Adds an optional `description` field to `StepMeta`, `PathMeta`, and
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ license = "Apache-2.0"
toolpath = { version = "0.7.1", path = "crates/toolpath" }
toolpath-convo = { version = "0.11.1", path = "crates/toolpath-convo" }
toolpath-git = { version = "0.6.0", path = "crates/toolpath-git" }
toolpath-claude = { version = "0.12.2", path = "crates/toolpath-claude", default-features = false }
toolpath-claude = { version = "0.12.3", path = "crates/toolpath-claude", default-features = false }
toolpath-gemini = { version = "0.6.1", path = "crates/toolpath-gemini", default-features = false }
toolpath-codex = { version = "0.6.1", path = "crates/toolpath-codex" }
toolpath-copilot = { version = "0.1.0", path = "crates/toolpath-copilot" }
Expand Down
15 changes: 3 additions & 12 deletions crates/path-cli/src/cmd_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -706,18 +706,9 @@ fn build_claude_conversation(path: &toolpath::v1::Path) -> Result<toolpath_claud

#[cfg(not(target_os = "emscripten"))]
fn serialize_jsonl(conv: &toolpath_claude::Conversation) -> Result<String> {
let mut lines = Vec::with_capacity(conv.preamble.len() + conv.entries.len());
for raw in &conv.preamble {
lines.push(serde_json::to_string(raw)?);
}
for entry in &conv.entries {
lines.push(serde_json::to_string(entry)?);
}
// Trailing newline matters: Claude Code appends to this file on resume,
// and without it the first appended entry lands on the last line.
let mut out = lines.join("\n");
out.push('\n');
Ok(out)
let mut buf = Vec::new();
toolpath_claude::ConversationWriter::write_conversation(conv, &mut buf)?;
Ok(String::from_utf8(buf).expect("serde_json emits UTF-8"))
}

#[cfg(not(target_os = "emscripten"))]
Expand Down
2 changes: 1 addition & 1 deletion crates/toolpath-claude/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "toolpath-claude"
version = "0.12.2"
version = "0.12.3"
edition.workspace = true
license.workspace = true
repository = "https://github.com/empathic/toolpath"
Expand Down
2 changes: 2 additions & 0 deletions crates/toolpath-claude/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod reader;
pub mod types;
#[cfg(feature = "watcher")]
pub mod watcher;
pub mod writer;

#[cfg(feature = "watcher")]
pub use async_watcher::{AsyncConversationWatcher, WatcherConfig, WatcherHandle};
Expand All @@ -30,6 +31,7 @@ pub use types::{
};
#[cfg(feature = "watcher")]
pub use watcher::ConversationWatcher;
pub use writer::ConversationWriter;

/// High-level interface for reading Claude conversations.
///
Expand Down
77 changes: 77 additions & 0 deletions crates/toolpath-claude/src/writer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use crate::error::Result;
use crate::types::Conversation;
use std::io::Write;

pub struct ConversationWriter;

impl ConversationWriter {
/// Writes `conv` as Claude Code session-file JSONL: preamble
/// lines, then entries, one JSON value per line, each line
/// newline-terminated.
pub fn write_conversation<W: Write>(conv: &Conversation, mut w: W) -> Result<()> {
// Trailing newline matters: Claude Code appends to this file on resume,
// and without it the first appended entry lands on the last line.
for raw in &conv.preamble {
serde_json::to_writer(&mut w, raw)?;
w.write_all(b"\n")?;
}
for entry in &conv.entries {
serde_json::to_writer(&mut w, entry)?;
w.write_all(b"\n")?;
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::reader::ConversationReader;
use crate::types::ConversationEntry;
use serde_json::Value;

fn conversation() -> Conversation {
let mut convo = Conversation::new("test-session".to_string());
let entries = [
r#"{"uuid":"uuid-1","type":"user","timestamp":"2024-01-01T00:00:00Z","message":{"role":"user","content":"Hello"}}"#,
r#"{"uuid":"uuid-2","type":"assistant","timestamp":"2024-01-01T00:00:01Z","message":{"role":"assistant","content":"Hi"}}"#,
];
for entry_json in entries {
let entry: ConversationEntry = serde_json::from_str(entry_json).unwrap();
convo.add_entry(entry);
}
convo
}

#[test]
fn one_record_per_line_newline_terminated() {
let mut convo = conversation();
convo
.preamble
.push(serde_json::json!({"type": "summary", "summary": "s"}));

let mut buf = Vec::new();
ConversationWriter::write_conversation(&convo, &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();

assert!(out.ends_with('\n'));
assert!(!out.ends_with("\n\n"));
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 3);
assert!(lines[0].contains("\"summary\""));
for line in &lines {
serde_json::from_str::<Value>(line).unwrap();
}
}

#[test]
fn round_trips_through_the_reader() {
let convo = conversation();
let file = tempfile::NamedTempFile::new().unwrap();
ConversationWriter::write_conversation(&convo, file.as_file()).unwrap();

let back = ConversationReader::read_conversation(file.path()).unwrap();
let ids = |c: &Conversation| c.entries.iter().map(|e| e.uuid.clone()).collect::<Vec<_>>();
assert_eq!(ids(&back), ids(&convo));
}
}
2 changes: 1 addition & 1 deletion site/_data/crates.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
},
{
"name": "toolpath-claude",
"version": "0.12.2",
"version": "0.12.3",
"description": "Derive from Claude conversation logs",
"docs": "https://docs.rs/toolpath-claude",
"crate": "https://crates.io/crates/toolpath-claude",
Expand Down
Loading