diff --git a/Cargo.toml b/Cargo.toml index f89cafe..f60965c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "continue-transcripts" -version = "0.10.0" +version = "0.11.0" edition = "2021" description = "Convert continue.dev session files to readable HTML transcripts" license = "MIT" diff --git a/README.md b/README.md index 10c42fd..d75360a 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Install globally so the `continue-transcripts` command is always available: ```sh uv tool install continue-transcripts \ --no-index \ - --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.10.0 + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.11.0 ``` The `continue-transcripts` command is then available on your `PATH`. @@ -83,7 +83,7 @@ To upgrade later (update the version in the URL): ```sh uv tool install --upgrade continue-transcripts \ --no-index \ - --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.10.0 + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.11.0 ``` To uninstall: @@ -100,7 +100,7 @@ Run without installing: uvx \ --no-index \ --from continue-transcripts \ - --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.10.0 \ + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.11.0 \ continue-transcripts ./sessions ``` @@ -115,7 +115,7 @@ uv venv .venv source .venv/bin/activate uv pip install continue-transcripts \ --no-index \ - --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.10.0 + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.11.0 ``` ### Building from source diff --git a/pyproject.toml b/pyproject.toml index 68ce820..c3f2ecd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "continue-transcripts" -version = "0.10.0" +version = "0.11.0" description = "Convert continue.dev session files to readable HTML transcripts" readme = "README.md" license = { text = "MIT" } diff --git a/python/continue_transcripts/__init__.py b/python/continue_transcripts/__init__.py index e059e4e..5afbda9 100644 --- a/python/continue_transcripts/__init__.py +++ b/python/continue_transcripts/__init__.py @@ -1,3 +1,3 @@ """continue-transcripts - Convert continue.dev session files to readable HTML transcripts.""" -__version__ = "0.10.0" +__version__ = "0.11.0" diff --git a/src/main.rs b/src/main.rs index 26c2202..fb9f1bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use html_escape::encode_text; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser as MdParser, Tag, TagEnd}; use rayon::prelude::*; use serde::Deserialize; +use std::collections::HashMap; use std::fs; use std::io::{BufWriter, Write}; use std::path::{Path, PathBuf}; @@ -2809,6 +2810,10 @@ fn discover_session_files(path: &Path) -> Vec { // Recursively find .json files let pattern = format!("{}/**/*.json", path.display()); for p in glob::glob(&pattern).expect("Failed to read glob pattern").flatten() { + // Skip sessions.json — it contains session metadata, not a session transcript + if p.file_name().map_or(false, |n| n == "sessions.json") { + continue; + } files.push(p); } } @@ -2816,6 +2821,49 @@ fn discover_session_files(path: &Path) -> Vec { files } +/// Metadata entry from sessions.json. +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct SessionMeta { + #[serde(default)] + session_id: String, + /// dateCreated in sessions.json is a Unix timestamp (seconds since epoch). + #[serde(default)] + date_created: Option, +} + +/// Load sessions.json from a directory and return a map of session_id → ISO 8601 date string. +/// Returns an empty map if the file doesn't exist or can't be parsed. +fn load_sessions_metadata(dir: &Path) -> HashMap { + let meta_path = dir.join("sessions.json"); + let raw = match fs::read_to_string(&meta_path) { + Ok(s) => s, + Err(_) => return HashMap::new(), + }; + + let entries: Vec = match serde_json::from_str(&raw) { + Ok(v) => v, + Err(_) => return HashMap::new(), + }; + + let mut map = HashMap::new(); + for entry in entries { + if entry.session_id.is_empty() { + continue; + } + if let Some(ts) = entry.date_created { + let secs = ts as i64; + if let Some(dt) = chrono::DateTime::from_timestamp(secs, 0) { + map.insert( + entry.session_id, + dt.format("%Y-%m-%dT%H:%M:%SZ").to_string(), + ); + } + } + } + map +} + /// Parse a date string (YYYY-MM-DD) into a chrono NaiveDate. fn parse_date_filter(s: &str) -> Option { chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").ok() @@ -3088,6 +3136,13 @@ fn main() { let files_discovered = files.len(); eprintln!("Discovered {} JSON file(s)", files_discovered); + // Load sessions.json metadata for date fallback (only when input is a directory) + let sessions_meta = if input.is_dir() { + load_sessions_metadata(input) + } else { + HashMap::new() + }; + fs::create_dir_all(output_dir).expect("Failed to create output directory"); // Process files in parallel: read, parse, filter, and render HTML concurrently @@ -3171,6 +3226,7 @@ fn main() { let date = session .date_created .clone() + .or_else(|| sessions_meta.get(&session.session_id).cloned()) .or_else(|| file_modified_date(file.as_path())) .unwrap_or_default(); @@ -3258,6 +3314,10 @@ fn main() { } } + // Sort index entries by date descending (newest first). + // Dates are ISO 8601 strings, so lexicographic sort works correctly. + index_entries.sort_by(|a, b| b.2.cmp(&a.2)); + // Generate index.html (also skip if unchanged) let mut index_written = false; if !index_entries.is_empty() {