Skip to content
Open
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
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,40 @@ cache the same queries run ~4.7× faster (e.g. `length` 966 ms →
drivers so the zero-file rule lives once.
- The emscripten (playground) build keeps the sequential engine —
no threads there.
## `toolpath-claude`: the caller supplies the home directory — 2026-08-13

- **`toolpath-claude`** (0.13.0): breaking. `PathResolver::new(home)`
takes the home directory as a required argument. The crate reads no
environment variable; it keeps the layout knowledge (`<home>/.claude`)
and the caller owns "what is home". `ClaudeConvo::new(home)` and
`ConvoIO::new(home)` take the same argument.

Removed: the `Default` impls on `PathResolver`, `ConvoIO`, and
`ClaudeConvo`; `PathResolver::with_home`; the `NoHomeDirectory` error
variant. `with_claude_dir` stays as the full override.

The home directory is always present, so `home_dir()`,
`claude_dir()`, `projects_dir()`, `history_file()`, `project_dir()`,
`conversation_file()`, `ConvoIO::claude_dir_path()`, and
`ConvoIO::conversation_exists()` return a value instead of a
`Result`. `ClaudeConvo::claude_dir_path()` and
`ClaudeConvo::conversation_exists()` follow.

Verbose parse warnings are a parameter.
`ClaudeConvo::with_verbose_warnings(bool)` and
`ConvoIO::with_verbose_warnings(bool)` set it,
`ConversationReader::read_conversation_with(path, verbose_warnings)`
takes it directly, and `ConversationReader::read_conversation(path)`
warns about the first 5 unparseable lines only. The crate reads no
environment variable for it.
- **`path-cli`** (unreleased): `providers::claude_resolver` returns
`Option<PathResolver>`. `None` means the configuration carries no home
directory, so Claude is out of reach: the harness bundle omits it, and
a command that targets Claude reports "cannot determine the home
directory". `Config` reads `$CLAUDE_CLI_DEBUG` and passes the flag to
every `ClaudeConvo` it builds, so the variable keeps its behavior for
CLI users.

## `toolpath-codex`: the caller supplies the home directory — 2026-08-13

- **`toolpath-codex`** (0.7.0): breaking. `PathResolver::new(home)`
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.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.2", path = "crates/toolpath-claude", default-features = false }
toolpath-claude = { version = "0.13.0", path = "crates/toolpath-claude", default-features = false }
toolpath-gemini = { version = "0.7.0", path = "crates/toolpath-gemini", default-features = false }
toolpath-codex = { version = "0.7.0", path = "crates/toolpath-codex" }
toolpath-copilot = { version = "0.1.0", path = "crates/toolpath-copilot" }
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ Everything the CLI does is a library call. Each source has its own crate
```rust
use toolpath_claude::{ClaudeConvo, derive::{derive_path, DeriveConfig}};

let convo = ClaudeConvo::new();
let convo = ClaudeConvo::new("/Users/alex");
let conversation = convo.read_conversation("/path/to/project", "session-id")?;
let path = derive_path(&conversation, &DeriveConfig {
include_thinking: true,
Expand Down
4 changes: 1 addition & 3 deletions crates/path-cli/src/artifact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,7 @@ pub(crate) fn claude_chain_stamp(
let mut modified: Option<chrono::DateTime<chrono::Utc>> = None;
let mut size: Option<u64> = None;
for segment in &segments {
let Ok(file) = mgr.resolver().conversation_file(project, segment) else {
continue;
};
let file = mgr.resolver().conversation_file(project, segment);
let (m, s) = stat_stamp(&file);
if let Some(m) = m {
modified = Some(modified.map_or(m, |cur| cur.max(m)));
Expand Down
12 changes: 4 additions & 8 deletions crates/path-cli/src/cmd_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,8 @@ fn claude_session_file(
) -> Result<Option<PathBuf>> {
let project_dir = std::fs::canonicalize(project_dir)
.with_context(|| format!("resolve project path {}", project_dir.display()))?;
let resolver = providers::claude_resolver(config);
let claude_project_dir = resolver
.project_dir(&project_dir.to_string_lossy())
.map_err(|e| anyhow::anyhow!("Cannot resolve Claude project dir: {}", e))?;
let resolver = providers::require_claude_resolver(config)?;
let claude_project_dir = resolver.project_dir(&project_dir.to_string_lossy());
let candidate = claude_project_dir.join(format!("{}.jsonl", session_id));
Ok(candidate.exists().then_some(candidate))
}
Expand Down Expand Up @@ -754,10 +752,8 @@ fn write_into_claude_project(
.with_context(|| format!("resolve project path {}", project_dir.display()))?;
let project_path = project_dir.to_string_lossy();

let resolver = providers::claude_resolver(config);
let claude_project_dir = resolver
.project_dir(&project_path)
.map_err(|e| anyhow::anyhow!("Cannot resolve Claude project dir: {}", e))?;
let resolver = providers::require_claude_resolver(config)?;
let claude_project_dir = resolver.project_dir(&project_path);

std::fs::create_dir_all(&claude_project_dir)
.with_context(|| format!("create {}", claude_project_dir.display()))?;
Expand Down
8 changes: 5 additions & 3 deletions crates/path-cli/src/cmd_import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,9 @@ fn derive_claude(
all: bool,
config: &Config,
) -> Result<Vec<DerivedDoc>> {
let manager = toolpath_claude::ClaudeConvo::with_resolver(providers::claude_resolver(config));
let manager =
toolpath_claude::ClaudeConvo::with_resolver(providers::require_claude_resolver(config)?)
.with_verbose_warnings(providers::claude_verbose_warnings(config));
derive_claude_with_manager(&manager, project, session, all)
}

Expand Down Expand Up @@ -1576,7 +1578,7 @@ mod tests {
)
.unwrap();

let resolver = toolpath_claude::PathResolver::new().with_claude_dir(&claude_dir);
let resolver = toolpath_claude::PathResolver::new(temp.path()).with_claude_dir(&claude_dir);
let manager = toolpath_claude::ClaudeConvo::with_resolver(resolver);
(temp, manager)
}
Expand Down Expand Up @@ -1621,7 +1623,7 @@ mod tests {
.unwrap();
}

let resolver = toolpath_claude::PathResolver::new().with_claude_dir(&claude_dir);
let resolver = toolpath_claude::PathResolver::new(temp.path()).with_claude_dir(&claude_dir);
(temp, toolpath_claude::ClaudeConvo::with_resolver(resolver))
}

Expand Down
10 changes: 6 additions & 4 deletions crates/path-cli/src/cmd_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,9 @@ fn run_github(repo: String, fmt: ListFormat) -> Result<()> {
// ── Claude ──────────────────────────────────────────────────────────────────

fn run_claude(project: Option<String>, fmt: ListFormat, config: &Config) -> Result<()> {
let manager = providers::claude_convo(config);
let manager =
toolpath_claude::ClaudeConvo::with_resolver(providers::require_claude_resolver(config)?)
.with_verbose_warnings(providers::claude_verbose_warnings(config));

match (project, fmt) {
// TSV/JSON without --project: emit sessions across every project so
Expand Down Expand Up @@ -1311,7 +1313,7 @@ mod tests {
)
.unwrap();

let resolver = toolpath_claude::PathResolver::new().with_claude_dir(&claude_dir);
let resolver = toolpath_claude::PathResolver::new(temp.path()).with_claude_dir(&claude_dir);
let manager = toolpath_claude::ClaudeConvo::with_resolver(resolver);
(temp, manager)
}
Expand Down Expand Up @@ -1344,7 +1346,7 @@ mod tests {
let projects_dir = claude_dir.join("projects");
std::fs::create_dir_all(&projects_dir).unwrap();

let resolver = toolpath_claude::PathResolver::new().with_claude_dir(&claude_dir);
let resolver = toolpath_claude::PathResolver::new(temp.path()).with_claude_dir(&claude_dir);
let manager = toolpath_claude::ClaudeConvo::with_resolver(resolver);

let result = list_claude_projects(&manager, ListFormat::Pretty);
Expand Down Expand Up @@ -1386,7 +1388,7 @@ mod tests {
let projects_dir = claude_dir.join("projects/-empty-project");
std::fs::create_dir_all(&projects_dir).unwrap();

let resolver = toolpath_claude::PathResolver::new().with_claude_dir(&claude_dir);
let resolver = toolpath_claude::PathResolver::new(temp.path()).with_claude_dir(&claude_dir);
let manager = toolpath_claude::ClaudeConvo::with_resolver(resolver);

let result = list_claude_sessions(&manager, "/empty/project", ListFormat::Pretty);
Expand Down
16 changes: 7 additions & 9 deletions crates/path-cli/src/cmd_share.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,12 +675,10 @@ fn harness_status_claude(bundle: &HarnessBundle, home: Option<&std::path::Path>)
let Some(mgr) = &bundle.claude else {
return HarnessStatus::unresolved();
};
match mgr.resolver().projects_dir() {
Ok(p) => HarnessStatus {
path: crate::config::home_relative(&p, home),
exists: p.exists(),
},
Err(_) => HarnessStatus::unresolved(),
let p = mgr.resolver().projects_dir();
HarnessStatus {
path: crate::config::home_relative(&p, home),
exists: p.exists(),
}
}

Expand Down Expand Up @@ -1042,7 +1040,7 @@ mod tests {
fn claude_only_bundle(home: &Path) -> HarnessBundle {
let claude_dir = home.join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
let resolver = toolpath_claude::PathResolver::new().with_claude_dir(&claude_dir);
let resolver = toolpath_claude::PathResolver::new(home).with_claude_dir(&claude_dir);
HarnessBundle {
claude: Some(toolpath_claude::ClaudeConvo::with_resolver(resolver)),
..Default::default()
Expand Down Expand Up @@ -1364,7 +1362,7 @@ mod tests {
// it as missing rather than going through the `unresolved` branch.
let temp = TempDir::new().unwrap();
let claude_dir = temp.path().join(".claude"); // never created
let resolver = toolpath_claude::PathResolver::new().with_claude_dir(&claude_dir);
let resolver = toolpath_claude::PathResolver::new(temp.path()).with_claude_dir(&claude_dir);
let bundle = HarnessBundle {
claude: Some(toolpath_claude::ClaudeConvo::with_resolver(resolver)),
..Default::default()
Expand All @@ -1383,7 +1381,7 @@ mod tests {
let temp = TempDir::new().unwrap();
let claude_dir = temp.path().join(".claude");
std::fs::create_dir_all(claude_dir.join("projects")).unwrap();
let resolver = toolpath_claude::PathResolver::new().with_claude_dir(&claude_dir);
let resolver = toolpath_claude::PathResolver::new(temp.path()).with_claude_dir(&claude_dir);
let bundle = HarnessBundle {
claude: Some(toolpath_claude::ClaudeConvo::with_resolver(resolver)),
..Default::default()
Expand Down
5 changes: 4 additions & 1 deletion crates/path-cli/src/cmd_show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ pub fn run(source: ShowSource, ansi: bool, config: &Config) -> Result<()> {
fn derive_one(source: ShowSource, config: &Config) -> Result<toolpath::v1::Path> {
match source {
ShowSource::Claude { project, session } => {
let manager = providers::claude_convo(config);
let manager = toolpath_claude::ClaudeConvo::with_resolver(
providers::require_claude_resolver(config)?,
)
.with_verbose_warnings(providers::claude_verbose_warnings(config));
let convo = manager
.read_conversation(&project, &session)
.map_err(|e| anyhow::anyhow!("{}", e))?;
Expand Down
7 changes: 7 additions & 0 deletions crates/path-cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ pub(crate) const DOCUMENTS_DIR_NAME: &str = "documents";
pub struct Config {
/// `$APPDATA`: Windows harness data root.
pub(crate) appdata: Option<PathBuf>,
/// `$CLAUDE_CLI_DEBUG`: the Claude reader warns about every
/// unparseable conversation line, not just the first 5. Presence is
/// the signal; the value is not read.
pub(crate) claude_cli_debug: Option<String>,
/// `$CODEX_ROLLOUT_STRICT`: the Codex reader errors on an
/// unparseable rollout line. Presence is the signal; the value is
/// not read.
Expand Down Expand Up @@ -104,6 +108,7 @@ impl Config {
/// influence a `Config`. Names match case-insensitively.
const ENV_MAP: &'static [(&'static str, &'static str)] = &[
("APPDATA", "appdata"),
("CLAUDE_CLI_DEBUG", "claude_cli_debug"),
("CODEX_ROLLOUT_STRICT", "codex_rollout_strict"),
("COPILOT_HOME", "copilot_home"),
("HOME", "home"),
Expand Down Expand Up @@ -197,6 +202,7 @@ mod tests {
figment::Jail::expect_with(|jail| {
jail.set_env(CONFIG_DIR_ENV, "/tmp/cfg-root");
jail.set_env("HOME", "/home/jailed");
jail.set_env("CLAUDE_CLI_DEBUG", "1");
jail.set_env("CODEX_ROLLOUT_STRICT", "1");
jail.set_env("XDG_DATA_HOME", "/home/jailed/.local/share");
jail.set_env("COPILOT_HOME", "/home/jailed/.copilot");
Expand All @@ -209,6 +215,7 @@ mod tests {
config,
Config {
appdata: Some(PathBuf::from("/home/jailed/appdata")),
claude_cli_debug: Some("1".to_string()),
codex_rollout_strict: Some("1".to_string()),
copilot_home: Some(PathBuf::from("/home/jailed/.copilot")),
home: Some(PathBuf::from("/home/jailed")),
Expand Down
3 changes: 2 additions & 1 deletion crates/path-cli/src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ pub(crate) fn derive_claude_session(
session: &str,
) -> Result<DerivedDoc> {
derive_claude_session_with(
&toolpath_claude::ClaudeConvo::with_resolver(providers::claude_resolver(config)),
&toolpath_claude::ClaudeConvo::with_resolver(providers::require_claude_resolver(config)?)
.with_verbose_warnings(providers::claude_verbose_warnings(config)),
project,
session,
)
Expand Down
1 change: 0 additions & 1 deletion crates/path-cli/src/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ pub(crate) struct HarnessBundle {
pub(crate) fn is_not_found_claude(err: &toolpath_claude::ConvoError) -> bool {
use toolpath_claude::ConvoError;
matches!(err, ConvoError::Io(e) if e.kind() == std::io::ErrorKind::NotFound)
|| matches!(err, ConvoError::NoHomeDirectory)
|| matches!(err, ConvoError::ClaudeDirectoryNotFound(_))
}

Expand Down
55 changes: 41 additions & 14 deletions crates/path-cli/src/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,19 @@ fn missing_home(harness: &str) -> anyhow::Error {
)
}

pub(crate) fn claude_convo(config: &Config) -> toolpath_claude::ClaudeConvo {
let mut resolver = toolpath_claude::PathResolver::new();
if let Some(home) = config.home_dir() {
resolver = resolver.with_home(home);
}
toolpath_claude::ClaudeConvo::with_resolver(resolver)
pub(crate) fn claude_resolver(config: &Config) -> Option<toolpath_claude::PathResolver> {
config.home_dir().map(toolpath_claude::PathResolver::new)
}

/// [`claude_resolver`] for a command that targets Claude.
pub(crate) fn require_claude_resolver(config: &Config) -> Result<toolpath_claude::PathResolver> {
claude_resolver(config).ok_or_else(|| missing_home("Claude"))
}

/// The Claude reader's verbose-warning flag. `$CLAUDE_CLI_DEBUG` is
/// verbose when set, whatever its value.
pub(crate) fn claude_verbose_warnings(config: &Config) -> bool {
config.claude_cli_debug.is_some()
}

pub(crate) fn gemini_resolver(config: &Config) -> Option<toolpath_gemini::PathResolver> {
Expand Down Expand Up @@ -117,7 +124,10 @@ pub(crate) fn pi_convo(config: &Config, base: Option<&Path>) -> toolpath_pi::PiC
#[cfg(not(target_os = "emscripten"))]
pub(crate) fn harness_bundle(config: &Config) -> HarnessBundle {
HarnessBundle {
claude: Some(claude_convo(config)),
claude: claude_resolver(config).map(|r| {
toolpath_claude::ClaudeConvo::with_resolver(r)
.with_verbose_warnings(claude_verbose_warnings(config))
}),
gemini: gemini_resolver(config).map(toolpath_gemini::GeminiConvo::with_resolver),
codex: codex_resolver(config).map(|r| {
toolpath_codex::CodexConvo::with_resolver(r).with_strict(codex_strict(config))
Expand Down Expand Up @@ -147,14 +157,31 @@ mod tests {
}

#[test]
fn claude_convo_roots_at_config_home() {
let manager = claude_convo(&config_with_home());
fn claude_resolver_roots_at_config_home() {
let resolver = claude_resolver(&config_with_home()).unwrap();
assert_eq!(
manager.resolver().projects_dir().unwrap(),
resolver.projects_dir(),
PathBuf::from("/home/jailed/.claude/projects")
);
}

#[test]
fn claude_resolver_is_none_without_a_home() {
assert!(claude_resolver(&Config::default()).is_none());
let err = require_claude_resolver(&Config::default()).unwrap_err();
assert!(err.to_string().contains("home directory"));
}

#[test]
fn claude_verbose_warnings_follows_presence_of_the_variable() {
assert!(!claude_verbose_warnings(&Config::default()));
let config = Config {
claude_cli_debug: Some(String::new()),
..Config::default()
};
assert!(claude_verbose_warnings(&config));
}

#[test]
fn gemini_resolver_roots_at_config_home() {
let resolver = gemini_resolver(&config_with_home()).unwrap();
Expand Down Expand Up @@ -232,14 +259,14 @@ mod tests {
}

#[test]
fn convos_fall_back_to_config_userprofile() {
fn claude_resolver_falls_back_to_config_userprofile() {
let config = Config {
userprofile: Some(PathBuf::from("/users/jailed")),
..Config::default()
};
let manager = claude_convo(&config);
let resolver = claude_resolver(&config).unwrap();
assert_eq!(
manager.resolver().projects_dir().unwrap(),
resolver.projects_dir(),
PathBuf::from("/users/jailed/.claude/projects")
);
}
Expand Down Expand Up @@ -278,7 +305,7 @@ mod tests {
fn harness_bundle_roots_providers_at_config_home() {
let bundle = harness_bundle(&config_with_home());
assert_eq!(
bundle.claude.unwrap().resolver().projects_dir().unwrap(),
bundle.claude.unwrap().resolver().projects_dir(),
PathBuf::from("/home/jailed/.claude/projects")
);
assert_eq!(
Expand Down
3 changes: 2 additions & 1 deletion crates/path-cli/src/sync/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,8 @@ mod tests {
}

fn claude_bundle(home: &Path) -> HarnessBundle {
let resolver = toolpath_claude::PathResolver::new().with_claude_dir(home.join(".claude"));
let resolver =
toolpath_claude::PathResolver::new(home).with_claude_dir(home.join(".claude"));
HarnessBundle {
claude: Some(toolpath_claude::ClaudeConvo::with_resolver(resolver)),
..Default::default()
Expand Down
Loading
Loading