diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce2d196..60ad19be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to the Toolpath workspace are documented here. +## Preamble steps get real timestamps; validate enforces schema formats — 2026-07-06 + +Claude Code writes several headerless session-log lines with no +`timestamp` field (`last-prompt`, `ai-title`, `custom-title`; a headless +`claude -p` run produces them every time). `toolpath-claude` stamped the +steps derived from those lines with `"timestamp": ""`, which the schema +forbids (`$defs/timestamp` is `format: date-time`) but `path p validate` +silently accepted, because JSON Schema draft 2020-12 treats `format` as +annotation-only unless the validator opts in. Both ends are fixed: the +derivation now resolves a real timestamp, and validation now asserts the +formats the schema declares. + +- **`toolpath-claude`** (0.12.1): a step derived from a timestamp-less + preamble line resolves its timestamp from the nearest real source — the + entry the line references (`leafUuid` / `messageId`), else the previous + preamble line, else the session's first timestamped entry. Only the + derived step's identity changes; the source line still rides verbatim + inside the step's structural change (`extra["raw"]`), so projecting back + to Claude JSONL is unaffected. +- **`path-cli`** (0.15.0): `path p validate` opts into format assertion, + so `format: date-time` on step timestamps and `format: uri` on + `meta.kind` / path-ref `$ref` / signature `href` are enforced instead of + ignored. Documents imported with an older `toolpath-claude` may now fail + validation on their empty preamble timestamps; re-importing the session + fixes them. (`toolpath-cli` 0.15.0 tracks the bump.) + ## Derive: resolve duplicate step ids — 2026-07-01 - **`toolpath-convo`** (0.11.1): `derive_path` now guarantees the derived diff --git a/CLAUDE.md b/CLAUDE.md index 01c70e9f..40a224e5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -179,14 +179,14 @@ Tests live alongside the code (`#[cfg(test)] mod tests`), plus `path-cli` has in - `toolpath-convo`: 118 unit + 4 doc tests (types, enrichment, display, ConversationView -> Path derivation, message-group usage accounting, breakdowns) - `toolpath-git`: 33 unit + 3 doc tests (derive, branch detection, diffstat) - `toolpath-github`: 32 unit + 3 doc tests (mapping, DAG construction, fixtures) -- `toolpath-claude`: 229 unit + 18 integration + 6 doc tests (path resolution, conversation reading, query, chaining, watcher, derive, metadata first-user-message, group_id grouping + once-per-message usage totals) +- `toolpath-claude`: 232 unit + 19 integration + 6 doc tests (path resolution, conversation reading, query, chaining, watcher, derive, metadata first-user-message, group_id grouping + once-per-message usage totals, preamble timestamp fallback) - `toolpath-gemini`: 161 unit + 29 integration + 5 doc tests (path resolution, chat-file parsing, query, watcher, derive, provider, round-trip fidelity, thoughts-folded-into-output + reasoning breakdown round-trip) - `toolpath-codex`: 80 unit + 51 integration + 2 doc tests (rollout parsing, provider assembly, patch-fidelity derive, real-session fixture, source→path fidelity invariants, JSON wire-level round-trip, per-turn token deltas from cumulative counters, reasoning breakdown) - `toolpath-opencode`: 52 unit + 19 integration + 1 doc test (SQLite reader, JSON payload serde, provider assembly, snapshot-based derive, tool-input fallback for gitignored paths, reasoning breakdown) - `toolpath-cursor`: 78 unit + 8 integration round-trip + 1 real-DB sanity + 1 doc test (state.vscdb SQLite reader, bubble store + composer header parsing, content-addressed blob lookup, projector with full TOOL_TABLE coverage, JSONL transcript ingest in `examples/dump_fixture.rs`) - `toolpath-pi`: 133 unit + 26 integration + 5 doc tests (types, paths, error, reader, io, provider) - `toolpath-dot`: 30 unit + 2 doc tests (render, visual conventions, escaping) -- `path-cli`: 294 unit + 65 integration tests (import/export/cache, track sessions, merge, validate, roundtrip, render-md snapshots, deprecation aliases, pathbase HTTP mock-server tests, fzf-friendly TSV output, `path resume` orchestration with injectable `ExecStrategy`). For an end-to-end check against a real Pathbase deployment, run `scripts/test-pathbase-live.sh ` — it does an anon round-trip in a sandboxed config dir and, if you're logged into that URL, an authed pathstash round-trip too. +- `path-cli`: 295 unit + 66 integration tests (import/export/cache, track sessions, merge, validate — including JSON Schema format assertion, roundtrip, render-md snapshots, deprecation aliases, pathbase HTTP mock-server tests, fzf-friendly TSV output, `path resume` orchestration with injectable `ExecStrategy`). For an end-to-end check against a real Pathbase deployment, run `scripts/test-pathbase-live.sh ` — it does an anon round-trip in a sandboxed config dir and, if you're logged into that URL, an authed pathstash round-trip too. - `toolpath-cli`: 0 tests (it's a one-line `path_cli::run()` shim crate that exists only so `cargo install toolpath-cli` keeps installing the `path` binary) Validate example documents: `for f in examples/*.json; do cargo run -p path-cli -- p validate --input "$f"; done` diff --git a/Cargo.lock b/Cargo.lock index a799d3c7..06fa054d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2308,7 +2308,7 @@ dependencies = [ [[package]] name = "path-cli" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "assert_cmd", @@ -3893,7 +3893,7 @@ dependencies = [ [[package]] name = "toolpath-claude" -version = "0.12.0" +version = "0.12.1" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index cf3d6237..b124569e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ license = "Apache-2.0" toolpath = { version = "0.7.0", 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.0", path = "crates/toolpath-claude", default-features = false } +toolpath-claude = { version = "0.12.1", path = "crates/toolpath-claude", default-features = false } toolpath-gemini = { version = "0.6.0", path = "crates/toolpath-gemini", default-features = false } toolpath-codex = { version = "0.6.0", path = "crates/toolpath-codex" } toolpath-opencode = { version = "0.5.0", path = "crates/toolpath-opencode" } @@ -35,7 +35,7 @@ toolpath-github = { version = "0.6.0", path = "crates/toolpath-github" } toolpath-dot = { version = "0.5.0", path = "crates/toolpath-dot" } toolpath-md = { version = "0.7.0", path = "crates/toolpath-md" } toolpath-pi = { version = "0.6.0", path = "crates/toolpath-pi" } -path-cli = { version = "0.14.0", path = "crates/path-cli" } +path-cli = { version = "0.15.0", path = "crates/path-cli" } pathbase-client = { version = "0.2.0", path = "crates/pathbase-client" } reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "rustls"] } diff --git a/crates/path-cli/Cargo.toml b/crates/path-cli/Cargo.toml index 01eea174..7a6fa876 100644 --- a/crates/path-cli/Cargo.toml +++ b/crates/path-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "path-cli" -version = "0.14.0" +version = "0.15.0" edition.workspace = true license.workspace = true repository = "https://github.com/empathic/toolpath" diff --git a/crates/path-cli/src/schema.rs b/crates/path-cli/src/schema.rs index 381ef7dd..ce754098 100644 --- a/crates/path-cli/src/schema.rs +++ b/crates/path-cli/src/schema.rs @@ -40,7 +40,14 @@ fn validator() -> &'static Validator { VALIDATOR.get_or_init(|| { let schema: serde_json::Value = serde_json::from_str(SCHEMA_SOURCE) .expect("toolpath.schema.json embedded in binary parses as JSON"); - jsonschema::validator_for(&schema) + // Draft 2020-12 treats `format` as annotation-only by default, but + // the schema means what it says: `$defs/timestamp` is + // `format: date-time` and derived documents must satisfy it (a + // `""` step timestamp used to sail through here and then fail on + // format-asserting consumers). Opt in to format assertion. + jsonschema::options() + .should_validate_formats(true) + .build(&schema) .expect("toolpath.schema.json embedded in binary is itself a valid JSON Schema") }) } @@ -54,9 +61,12 @@ fn kind_validators() -> &'static HashMap<&'static str, Validator> { .map(|(uri, source)| { let schema: serde_json::Value = serde_json::from_str(source) .unwrap_or_else(|e| panic!("bundled kind schema {uri} is not valid JSON: {e}")); - let v = jsonschema::validator_for(&schema).unwrap_or_else(|e| { - panic!("bundled kind schema {uri} is not a valid JSON Schema: {e}") - }); + let v = jsonschema::options() + .should_validate_formats(true) + .build(&schema) + .unwrap_or_else(|e| { + panic!("bundled kind schema {uri} is not a valid JSON Schema: {e}") + }); (*uri, v) }) .collect() @@ -207,6 +217,34 @@ mod tests { validate(&doc).expect("base may carry both ref and branch"); } + /// `$defs/timestamp` is `format: date-time`; format assertion is + /// opted in, so an empty step timestamp must fail — surfaced as a + /// oneOf violation at the containing path (`$defs/pathOrRef`). + /// Regression: `toolpath-claude` used to stamp `""` on steps derived + /// from timestamp-less preamble lines (`last-prompt`, `ai-title`). + #[test] + fn empty_step_timestamp_is_rejected() { + let doc = json!({ + "graph": {"id": "g1"}, + "paths": [{ + "path": {"id": "p1", "head": "s1"}, + "steps": [{ + "step": { + "id": "s1", + "actor": "human:alex", + "timestamp": "" + }, + "change": {"src/main.rs": {"raw": "@@ -1 +1 @@\n-a\n+b"}} + }] + }] + }); + let err = validate(&doc).expect_err("empty timestamp violates format: date-time"); + assert!( + err.to_string().contains("/paths/0"), + "error should point at the offending path: {err}" + ); + } + /// `path.base` is optional: a Graph that wraps a single step (the shape /// of the new `step-NN.json` example fixtures) has no base, and that's /// fine. diff --git a/crates/path-cli/tests/integration.rs b/crates/path-cli/tests/integration.rs index 27c53d8a..75a60f21 100644 --- a/crates/path-cli/tests/integration.rs +++ b/crates/path-cli/tests/integration.rs @@ -256,6 +256,62 @@ fn derive_git_validate_roundtrip() { let _ = std::fs::remove_file(&tmp_file); } +/// Importing a session whose JSONL carries headerless preamble lines +/// without timestamps (`last-prompt`, `ai-title` — the shape a headless +/// `claude -p` run writes) must produce a schema-valid document. The +/// derived claude-preamble-* steps used to carry `"timestamp": ""`, +/// which `p validate` rejects (`$defs/timestamp` is `format: date-time`). +#[test] +fn import_claude_with_untimestamped_preamble_validates() { + let temp = tempfile::tempdir().unwrap(); + let project = temp.path().join("proj"); + std::fs::create_dir_all(&project).unwrap(); + let claude_dir = temp.path().join(".claude"); + let project_slug = project + .to_string_lossy() + .replace([std::path::MAIN_SEPARATOR, '_', '.'], "-"); + let project_dir = claude_dir.join("projects").join(&project_slug); + std::fs::create_dir_all(&project_dir).unwrap(); + std::fs::write( + project_dir.join("session-pre.jsonl"), + format!( + r#"{{"type":"ai-title","aiTitle":"Say hi","sessionId":"session-pre"}} +{{"type":"queue-operation","operation":"enqueue","content":"queued","timestamp":"2024-01-01T00:00:03Z","sessionId":"session-pre"}} +{{"type":"last-prompt","lastPrompt":"hi","leafUuid":"u-1","sessionId":"session-pre"}} +{{"type":"user","uuid":"u-1","timestamp":"2024-01-01T00:00:00Z","cwd":"{cwd}","message":{{"role":"user","content":"hi"}}}} +{{"type":"assistant","uuid":"a-1","parentUuid":"u-1","timestamp":"2024-01-01T00:00:01Z","message":{{"role":"assistant","content":"hello"}}}} +"#, + cwd = project.display() + ), + ) + .unwrap(); + + let import_output = cmd() + .env("HOME", temp.path()) + .args(["p", "import", "claude", "--session", "session-pre", "--no-cache", "--project"]) + .arg(&project) + .output() + .unwrap(); + assert!( + import_output.status.success(), + "import failed: {}", + String::from_utf8_lossy(&import_output.stderr) + ); + + let tmp_file = std::env::temp_dir().join("toolpath-integration-claude-preamble.json"); + std::fs::write(&tmp_file, &import_output.stdout).unwrap(); + + cmd() + .args(["p", "validate"]) + .arg("--input") + .arg(&tmp_file) + .assert() + .success() + .stdout(predicate::str::contains("Valid")); + + let _ = std::fs::remove_file(&tmp_file); +} + // ── Render ─────────────────────────────────────────────────────────── #[test] diff --git a/crates/toolpath-claude/Cargo.toml b/crates/toolpath-claude/Cargo.toml index 5f463412..de527899 100644 --- a/crates/toolpath-claude/Cargo.toml +++ b/crates/toolpath-claude/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toolpath-claude" -version = "0.12.0" +version = "0.12.1" edition.workspace = true license.workspace = true repository = "https://github.com/empathic/toolpath" @@ -25,5 +25,6 @@ notify = { workspace = true, optional = true } tokio = { workspace = true, features = ["sync", "time", "fs"], optional = true } [dev-dependencies] +chrono = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/crates/toolpath-claude/src/derive.rs b/crates/toolpath-claude/src/derive.rs index 8d9cb61d..13a92669 100644 --- a/crates/toolpath-claude/src/derive.rs +++ b/crates/toolpath-claude/src/derive.rs @@ -171,4 +171,48 @@ mod tests { let anc = toolpath::v1::query::ancestors(&pp.steps, &pp.path.head); assert_eq!(anc.len(), pp.steps.len(), "all steps on head ancestry"); } + + #[test] + fn derive_path_preamble_steps_carry_valid_timestamps() { + // A headless (`claude -p`) session caches its prompt as a headerless + // `last-prompt` line with no `timestamp` on the wire. The derived + // step must still satisfy the schema's `format: date-time` — + // regression test for `"timestamp": ""` on claude-preamble-* steps + // failing `path p validate`. + let mut convo = make_convo(); + convo.preamble = vec![ + serde_json::json!({ + "type": "queue-operation", + "operation": "enqueue", + "content": "queued while busy", + "timestamp": "2026-01-01T00:00:02Z", + "sessionId": "sess-1abc", + }), + serde_json::json!({ + "type": "last-prompt", + "lastPrompt": "Fix bug", + "leafUuid": "u1", + "sessionId": "sess-1abc", + }), + ]; + + let path = derive_path(&convo, &DeriveConfig::default()); + + let last_prompt = path + .steps + .iter() + .find(|s| s.step.id == "claude-preamble-1") + .expect("last-prompt preamble step"); + // leafUuid resolves to the referenced user entry's timestamp. + assert_eq!(last_prompt.step.timestamp, "2026-01-01T00:00:00Z"); + + for step in &path.steps { + assert!( + chrono::DateTime::parse_from_rfc3339(&step.step.timestamp).is_ok(), + "step {} timestamp {:?} is not RFC 3339", + step.step.id, + step.step.timestamp + ); + } + } } diff --git a/crates/toolpath-claude/src/provider.rs b/crates/toolpath-claude/src/provider.rs index 1896d1e3..b4af3ed8 100644 --- a/crates/toolpath-claude/src/provider.rs +++ b/crates/toolpath-claude/src/provider.rs @@ -326,9 +326,45 @@ fn conversation_to_view(convo: &Conversation) -> ConversationView { // Headerless preamble lines (ai-title, last-prompt, queue-operation, // permission-mode, file-history-snapshot, etc.) become events so they - // round-trip back to JSONL. + // round-trip back to JSONL. Several of these (`ai-title`, `last-prompt`, + // `summary`) carry no `timestamp`, but each event becomes a Step whose + // schema requires a valid RFC 3339 instant — resolve one: the entry the + // line references (`leafUuid` / `messageId`), else the previous preamble + // line's time, else the session's first timestamped entry. Only + // `event.timestamp` is patched; the line itself rides verbatim in + // `data["raw"]`, so projecting back to JSONL is unaffected. + let first_entry_ts = convo + .entries + .iter() + .map(|e| e.timestamp.as_str()) + .find(|ts| !ts.is_empty()) + .map(str::to_string); + let mut prev_preamble_ts: Option = None; for (idx, raw) in convo.preamble.iter().enumerate() { - events.push(preamble_to_event(idx, raw)); + let mut event = preamble_to_event(idx, raw); + if event.timestamp.is_empty() { + let referenced_ts = ["leafUuid", "messageId"] + .into_iter() + .filter_map(|key| raw.get(key).and_then(|v| v.as_str())) + .find_map(|uuid| { + convo + .entries + .iter() + .find(|e| e.uuid == uuid) + .map(|e| e.timestamp.clone()) + .filter(|ts| !ts.is_empty()) + }); + if let Some(ts) = referenced_ts + .or_else(|| prev_preamble_ts.clone()) + .or_else(|| first_entry_ts.clone()) + { + event.timestamp = ts; + } + } + if !event.timestamp.is_empty() { + prev_preamble_ts = Some(event.timestamp.clone()); + } + events.push(event); } // Map from "absorbed-or-skipped entry UUID" → "the previous @@ -461,7 +497,9 @@ fn conversation_to_view(convo: &Conversation) -> ConversationView { /// dumps it straight back onto `convo.preamble`. We don't model the shape — /// a headerless line is identified by the presence of `data["raw"]`, not by /// an enumerated `type` list. `event_type` carries the line's `type`, purely -/// informational. +/// informational. Lines without a `timestamp` produce an empty one here; +/// [`conversation_to_view`] resolves a fallback so the derived step still +/// carries a valid instant. fn preamble_to_event(idx: usize, raw: &serde_json::Value) -> toolpath_convo::ConversationEvent { let event_type = raw .get("type") @@ -1924,4 +1962,95 @@ mod tests { assert!(meta_b.predecessor.is_none()); assert!(meta_b.successor.is_none()); } + + // ── Preamble timestamp resolution ──────────────────────────────── + + /// A session whose preamble mixes timestamped and timestamp-less + /// headerless lines — the shape a headless (`claude -p`) run writes: + /// `ai-title` and `last-prompt` carry no `timestamp` on the wire. + fn setup_preamble_provider() -> (TempDir, ClaudeConvo) { + let temp = TempDir::new().unwrap(); + let claude_dir = temp.path().join(".claude"); + let project_dir = claude_dir.join("projects/-test-project"); + fs::create_dir_all(&project_dir).unwrap(); + + let lines = [ + r#"{"type":"ai-title","aiTitle":"Fix the bug","sessionId":"session-pre"}"#, + r#"{"type":"queue-operation","operation":"enqueue","content":"queued","timestamp":"2024-01-01T00:00:03Z","sessionId":"session-pre"}"#, + r#"{"type":"last-prompt","lastPrompt":"Fix the bug","leafUuid":"uuid-2","sessionId":"session-pre"}"#, + r#"{"type":"last-prompt","lastPrompt":"Fix the bug","sessionId":"session-pre"}"#, + r#"{"uuid":"uuid-1","type":"user","timestamp":"2024-01-01T00:00:00Z","message":{"role":"user","content":"Fix the bug"}}"#, + r#"{"uuid":"uuid-2","type":"assistant","parentUuid":"uuid-1","timestamp":"2024-01-01T00:00:01Z","message":{"role":"assistant","content":"Done."}}"#, + ]; + fs::write(project_dir.join("session-pre.jsonl"), lines.join("\n")).unwrap(); + + let resolver = PathResolver::new().with_claude_dir(&claude_dir); + (temp, ClaudeConvo::with_resolver(resolver)) + } + + #[test] + fn test_preamble_events_resolve_missing_timestamps() { + let (_temp, provider) = setup_preamble_provider(); + let view = + ConversationProvider::load_conversation(&provider, "/test/project", "session-pre") + .unwrap(); + + let ts_of = |id: &str| -> String { + view.events + .iter() + .find(|e| e.id == id) + .unwrap_or_else(|| panic!("missing event {id}")) + .timestamp + .clone() + }; + + // ai-title has no timestamp, no reference, and no previous preamble + // line — falls back to the session's first timestamped entry. + assert_eq!(ts_of("claude-preamble-0"), "2024-01-01T00:00:00Z"); + // queue-operation keeps its own wire timestamp. + assert_eq!(ts_of("claude-preamble-1"), "2024-01-01T00:00:03Z"); + // last-prompt with a leafUuid resolves the referenced entry's + // timestamp (beats the previous preamble line's). + assert_eq!(ts_of("claude-preamble-2"), "2024-01-01T00:00:01Z"); + // last-prompt without a reference inherits the previous preamble + // line's resolved timestamp. + assert_eq!(ts_of("claude-preamble-3"), "2024-01-01T00:00:01Z"); + + for event in &view.events { + assert!( + chrono::DateTime::parse_from_rfc3339(&event.timestamp).is_ok(), + "event {} timestamp {:?} is not RFC 3339", + event.id, + event.timestamp + ); + } + } + + #[test] + fn test_preamble_timestamp_fill_does_not_leak_into_projection() { + use toolpath_convo::ConversationProjector; + + let (_temp, provider) = setup_preamble_provider(); + let view = + ConversationProvider::load_conversation(&provider, "/test/project", "session-pre") + .unwrap(); + let projected = crate::ClaudeProjector.project(&view).unwrap(); + + // The raw lines ride in data["raw"] untouched: projecting back must + // not stamp the resolved timestamp onto lines that had none. + assert_eq!(projected.preamble.len(), 4); + for raw in &projected.preamble { + let ty = raw.get("type").and_then(|v| v.as_str()).unwrap(); + match ty { + "queue-operation" => assert_eq!( + raw.get("timestamp").and_then(|v| v.as_str()), + Some("2024-01-01T00:00:03Z") + ), + _ => assert!( + raw.get("timestamp").is_none(), + "{ty} line gained a timestamp on roundtrip: {raw}" + ), + } + } + } } diff --git a/crates/toolpath-claude/tests/real_fixture_roundtrip.rs b/crates/toolpath-claude/tests/real_fixture_roundtrip.rs index db9a55db..fb5a6527 100644 --- a/crates/toolpath-claude/tests/real_fixture_roundtrip.rs +++ b/crates/toolpath-claude/tests/real_fixture_roundtrip.rs @@ -239,6 +239,33 @@ fn roundtrip_preserves_total_token_usage_when_present() { } } +/// Every derived step must carry a schema-valid RFC 3339 timestamp. +/// +/// The real capture includes headerless `ai-title` / `last-prompt` lines +/// with no `timestamp` on the wire; the provider resolves one from an +/// adjacent entry during derivation. Without that, the preamble steps +/// carry `"timestamp": ""` and `path p validate` rejects the whole +/// imported document (`$defs/timestamp` requires `format: date-time`). +#[test] +fn derived_steps_all_carry_rfc3339_timestamps() { + let view = load_fixture_view(); + let path = derive_path(&view, &DeriveConfig::default()); + assert!( + path.steps + .iter() + .any(|s| s.step.id.starts_with("claude-preamble-")), + "fixture should derive preamble steps — refresh capture" + ); + for step in &path.steps { + assert!( + chrono::DateTime::parse_from_rfc3339(&step.step.timestamp).is_ok(), + "step {} timestamp {:?} is not RFC 3339", + step.step.id, + step.step.timestamp + ); + } +} + /// Reading + re-projecting a real fixture must preserve every JSONL line. /// /// This is the bluntest possible UX-loss check: count source lines, count diff --git a/crates/toolpath-cli/Cargo.toml b/crates/toolpath-cli/Cargo.toml index da3b7092..dd3ddc7f 100644 --- a/crates/toolpath-cli/Cargo.toml +++ b/crates/toolpath-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toolpath-cli" -version = "0.14.0" +version = "0.15.0" edition = "2024" license = "Apache-2.0" repository = "https://github.com/empathic/toolpath" @@ -14,7 +14,7 @@ name = "path" path = "src/main.rs" [dependencies] -path-cli = { path = "../path-cli", version = "0.14.0" } +path-cli = { path = "../path-cli", version = "0.15.0" } anyhow = "1.0" [workspace] diff --git a/site/_data/crates.json b/site/_data/crates.json index 6b730be1..db82852f 100644 --- a/site/_data/crates.json +++ b/site/_data/crates.json @@ -33,7 +33,7 @@ }, { "name": "toolpath-claude", - "version": "0.12.0", + "version": "0.12.1", "description": "Derive from Claude conversation logs", "docs": "https://docs.rs/toolpath-claude", "crate": "https://crates.io/crates/toolpath-claude", @@ -105,7 +105,7 @@ }, { "name": "path-cli", - "version": "0.14.0", + "version": "0.15.0", "description": "Unified CLI (binary: path)", "docs": "https://docs.rs/path-cli", "crate": "https://crates.io/crates/path-cli", @@ -113,7 +113,7 @@ }, { "name": "toolpath-cli", - "version": "0.14.0", + "version": "0.15.0", "description": "Deprecated alias for path-cli", "docs": "https://docs.rs/toolpath-cli", "crate": "https://crates.io/crates/toolpath-cli",