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
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,38 @@ 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-opencode`: the caller supplies the home directory — 2026-08-14

- **`toolpath-opencode`** (0.6.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>/.local/share/opencode`) and the caller owns "what is home".
`OpencodeConvo::new(home)` and `ConvoIO::new(home)` take the same
argument.

Removed: the `Default` impls on `PathResolver`, `ConvoIO`, and
`OpencodeConvo`; `PathResolver::with_home`; the `NoHomeDirectory`
error variant.

`PathResolver::with_xdg_data_home(xdg)` sets the XDG data root; the
resolver appends `opencode` to it. The data directory resolves in
this order: `with_data_dir`, `with_xdg_data_home`, then
`<home>/.local/share/opencode`.

The home directory is always present, so `home_dir()`, `data_dir()`,
`db_path()`, `snapshot_root()`, `log_dir()`, `snapshot_gitdir()`, and
`ConvoIO::db_path()` return a path instead of a `Result`.

The snapshot git repository needs a resolver. `to_view(session)` and
`derive_path(session, config)` skip it; `to_view_with_resolver` and
`derive_path_with_resolver` open it, as does the
`ConversationProvider` impl on `OpencodeConvo`.
- **`path-cli`** (unreleased): `providers::opencode_resolver` returns
`Option<PathResolver>`. `None` means the configuration carries no home
directory, so opencode is out of reach: the harness bundle omits it,
and a command that targets opencode reports "cannot determine the home
directory". `Config` reads `$XDG_DATA_HOME` and passes it to the
resolver, so the variable keeps its behavior for CLI users.
## `toolpath-copilot`: the caller supplies the home directory — 2026-08-14

- **`toolpath-copilot`** (0.2.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 @@ -31,7 +31,7 @@ toolpath-claude = { version = "0.13.0", path = "crates/toolpath-claude", default
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.2.0", path = "crates/toolpath-copilot" }
toolpath-opencode = { version = "0.5.0", path = "crates/toolpath-opencode" }
toolpath-opencode = { version = "0.6.0", path = "crates/toolpath-opencode" }
toolpath-cursor = { version = "0.2.0", path = "crates/toolpath-cursor" }
toolpath-github = { version = "0.6.0", path = "crates/toolpath-github" }
toolpath-dot = { version = "0.5.0", path = "crates/toolpath-dot" }
Expand Down
20 changes: 3 additions & 17 deletions crates/path-cli/src/cmd_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1479,10 +1479,7 @@ fn write_into_opencode_db(
let project_dir = std::fs::canonicalize(project_dir)
.with_context(|| format!("resolve project path {}", project_dir.display()))?;

let resolver = providers::opencode_resolver(config);
let db_path = resolver
.db_path()
.map_err(|e| anyhow::anyhow!("Cannot resolve opencode db path: {}", e))?;
let db_path = providers::require_opencode_resolver(config)?.db_path();
if !db_path.exists() {
anyhow::bail!(
"opencode database not found at {} — has opencode been run on this machine?",
Expand Down Expand Up @@ -2125,17 +2122,6 @@ mod tests {
}
}

/// A `Config` for the opencode export. The resolver reads
/// `$XDG_DATA_HOME` internally and that read wins against the home,
/// so the data root is injected too.
fn config_with_opencode_home(home: &std::path::Path) -> Config {
Config {
home: Some(home.to_path_buf()),
xdg_data_home: Some(home.join(".local/share")),
..Config::default()
}
}

fn make_path_doc() -> toolpath::v1::Graph {
let artifact_key = "agent://claude/test-session";

Expand Down Expand Up @@ -3112,7 +3098,7 @@ mod tests {
input_path.to_string_lossy().to_string(),
Some(project_dir.clone()),
None,
&config_with_opencode_home(&fake_home),
&config_with_home(&fake_home),
)
.expect("export opencode --project");

Expand Down Expand Up @@ -3382,7 +3368,7 @@ mod tests {
// which adds the `ses_` prefix if not already present.
let path = make_convo_path("opencode://ses_wrapper-test");

let result = project_opencode(&path, &cwd, &config_with_opencode_home(&fake_home));
let result = project_opencode(&path, &cwd, &config_with_home(&fake_home));

let returned_id = result.expect("project_opencode should succeed");
assert_eq!(returned_id, "ses_wrapper-test");
Expand Down
5 changes: 3 additions & 2 deletions crates/path-cli/src/cmd_import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1050,8 +1050,9 @@ fn derive_opencode(

#[cfg(not(target_os = "emscripten"))]
{
let manager =
toolpath_opencode::OpencodeConvo::with_resolver(providers::opencode_resolver(config));
let manager = toolpath_opencode::OpencodeConvo::with_resolver(
providers::require_opencode_resolver(config)?,
);
let derive_one = |sid: &str| derive_opencode_session_with(&manager, sid, no_snapshot_diffs);

let session_ids: Vec<String> = match (session, all) {
Expand Down
4 changes: 3 additions & 1 deletion crates/path-cli/src/cmd_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -795,7 +795,9 @@ fn run_opencode(project: Option<String>, fmt: ListFormat, config: &Config) -> Re

#[cfg(not(target_os = "emscripten"))]
{
let manager = providers::opencode_convo(config);
let manager = toolpath_opencode::OpencodeConvo::with_resolver(
providers::require_opencode_resolver(config)?,
);
let metas = manager
.io()
.list_session_metadata(project.as_deref())
Expand Down
10 changes: 4 additions & 6 deletions crates/path-cli/src/cmd_share.rs
Original file line number Diff line number Diff line change
Expand Up @@ -722,12 +722,10 @@ fn harness_status_opencode(
let Some(mgr) = &bundle.opencode else {
return HarnessStatus::unresolved();
};
match mgr.resolver().db_path() {
Ok(p) => HarnessStatus {
path: crate::config::home_relative(&p, home),
exists: p.exists(),
},
Err(_) => HarnessStatus::unresolved(),
let p = mgr.resolver().db_path();
HarnessStatus {
path: crate::config::home_relative(&p, home),
exists: p.exists(),
}
}

Expand Down
4 changes: 3 additions & 1 deletion crates/path-cli/src/cmd_show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,9 @@ fn derive_one(source: ShowSource, config: &Config) -> Result<toolpath::v1::Path>
session,
project: _,
} => {
let manager = providers::opencode_convo(config);
let manager = toolpath_opencode::OpencodeConvo::with_resolver(
providers::require_opencode_resolver(config)?,
);
let s = manager
.read_session(&session)
.map_err(|e| anyhow::anyhow!("{}", e))?;
Expand Down
4 changes: 3 additions & 1 deletion crates/path-cli/src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,9 @@ pub(crate) fn derive_opencode_session(
no_snapshot_diffs: bool,
) -> Result<DerivedDoc> {
derive_opencode_session_with(
&toolpath_opencode::OpencodeConvo::with_resolver(providers::opencode_resolver(config)),
&toolpath_opencode::OpencodeConvo::with_resolver(providers::require_opencode_resolver(
config,
)?),
session,
no_snapshot_diffs,
)
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 @@ -121,7 +121,6 @@ pub(crate) fn is_not_found_copilot(err: &toolpath_copilot::ConvoError) -> bool {
pub(crate) fn is_not_found_opencode(err: &toolpath_opencode::ConvoError) -> bool {
use toolpath_opencode::ConvoError;
matches!(err, ConvoError::Io(e) if e.kind() == std::io::ErrorKind::NotFound)
|| matches!(err, ConvoError::NoHomeDirectory)
|| matches!(err, ConvoError::OpencodeDirectoryNotFound(_))
|| matches!(err, ConvoError::DatabaseNotFound(_))
}
Expand Down
69 changes: 48 additions & 21 deletions crates/path-cli/src/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@
//! home, so the harness is out of reach. `require_*` turns that into an
//! error for a command that targets one harness.
//!
//! opencode, copilot, and cursor (Windows) get their directory
//! injected, not just the home. `$COPILOT_HOME` replaces the whole
//! Copilot root, so the injected directory wins against the
//! home-derived default. The opencode and cursor resolvers read
//! `$XDG_DATA_HOME` / `$APPDATA` internally, and those reads win
//! against `with_home`; the injected directory wins against both.
//! opencode takes the XDG data root as well as the home:
//! `$XDG_DATA_HOME` comes from [`Config`], and the resolver appends
//! `opencode` to it.
//!
//! copilot gets its directory injected, not just the home:
//! `$COPILOT_HOME` replaces the whole Copilot root, so the injected
//! directory wins against the home-derived default.
//!
//! cursor (Windows) gets its directory injected, not just the home:
//! its resolver reads `$APPDATA` internally, and that read wins
//! against `with_home`. The injected directory wins against both.

use crate::config::Config;
#[cfg(not(target_os = "emscripten"))]
Expand Down Expand Up @@ -90,15 +95,22 @@ pub(crate) fn copilot_strict(config: &Config) -> bool {
}

#[cfg(not(target_os = "emscripten"))]
pub(crate) fn opencode_convo(config: &Config) -> toolpath_opencode::OpencodeConvo {
let mut resolver = toolpath_opencode::PathResolver::new();
if let Some(home) = config.home_dir() {
resolver = resolver.with_home(home);
}
if let Some(xdg) = &config.xdg_data_home {
resolver = resolver.with_data_dir(xdg.join("opencode"));
}
toolpath_opencode::OpencodeConvo::with_resolver(resolver)
pub(crate) fn opencode_resolver(config: &Config) -> Option<toolpath_opencode::PathResolver> {
config.home_dir().map(|home| {
let resolver = toolpath_opencode::PathResolver::new(home);
match &config.xdg_data_home {
Some(xdg) => resolver.with_xdg_data_home(xdg),
None => resolver,
}
})
}

/// [`opencode_resolver`] for a command that targets opencode.
#[cfg(not(target_os = "emscripten"))]
pub(crate) fn require_opencode_resolver(
config: &Config,
) -> Result<toolpath_opencode::PathResolver> {
opencode_resolver(config).ok_or_else(|| missing_home("opencode"))
}

#[cfg(not(target_os = "emscripten"))]
Expand Down Expand Up @@ -147,7 +159,7 @@ pub(crate) fn harness_bundle(config: &Config) -> HarnessBundle {
copilot: copilot_resolver(config).map(|r| {
toolpath_copilot::CopilotConvo::with_resolver(r).with_strict(copilot_strict(config))
}),
opencode: Some(opencode_convo(config)),
opencode: opencode_resolver(config).map(toolpath_opencode::OpencodeConvo::with_resolver),
cursor: Some(cursor_convo(config)),
pi: Some(pi_convo(config, None)),
}
Expand All @@ -160,8 +172,7 @@ mod tests {

// Assertions stay on paths fully determined by injected values;
// resolver defaults that read the ambient environment (home
// fallbacks, `$XDG_DATA_HOME` when no directory is injected) are
// not asserted here.
// fallbacks) are not asserted here.

fn config_with_home() -> Config {
Config {
Expand Down Expand Up @@ -273,19 +284,35 @@ mod tests {
}

#[test]
fn opencode_convo_injects_data_dir() {
fn opencode_resolver_injects_the_xdg_data_root() {
let config = Config {
home: Some(PathBuf::from("/home/jailed")),
xdg_data_home: Some(PathBuf::from("/xdg/data")),
..Config::default()
};
let manager = opencode_convo(&config);
let resolver = opencode_resolver(&config).unwrap();
assert_eq!(
manager.resolver().db_path().unwrap(),
resolver.db_path(),
PathBuf::from("/xdg/data/opencode/opencode.db")
);
}

#[test]
fn opencode_resolver_roots_at_config_home() {
let resolver = opencode_resolver(&config_with_home()).unwrap();
assert_eq!(
resolver.db_path(),
PathBuf::from("/home/jailed/.local/share/opencode/opencode.db")
);
}

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

#[test]
fn cursor_convo_roots_at_config_home() {
let manager = cursor_convo(&config_with_home());
Expand Down
4 changes: 2 additions & 2 deletions crates/path-cli/tests/resume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ fn file_input_explicit_opencode_projects_and_records_exec() {
// Pre-create the opencode db with the canonical schema. (Schema DDL
// copied from cmd_export's existing opencode test until/unless
// toolpath-opencode exposes a public bootstrap helper.)
let resolver = toolpath_opencode::PathResolver::new();
let db_path = resolver.db_path().unwrap();
let resolver = toolpath_opencode::PathResolver::new(home.home_dir());
let db_path = resolver.db_path();
std::fs::create_dir_all(db_path.parent().unwrap()).unwrap();
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
Expand Down
2 changes: 1 addition & 1 deletion crates/toolpath-opencode/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "toolpath-opencode"
version = "0.5.0"
version = "0.6.0"
edition.workspace = true
license.workspace = true
repository = "https://github.com/empathic/toolpath"
Expand Down
2 changes: 1 addition & 1 deletion crates/toolpath-opencode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ git repositories.
```rust,no_run
use toolpath_opencode::{OpencodeConvo, derive::{DeriveConfig, derive_path}};

let manager = OpencodeConvo::new();
let manager = OpencodeConvo::new("/Users/alex");
let session_id = "ses_24ee4deb6ffeWw7ZKWNVoOAgjD";
let convo = manager.read_session(session_id)?;
let path = derive_path(&convo, &DeriveConfig::default());
Expand Down
21 changes: 14 additions & 7 deletions crates/toolpath-opencode/src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ pub struct DeriveConfig {
pub no_snapshot_diffs: bool,
}

/// Derive a [`Path`] from an opencode [`Session`].
/// Derive a [`Path`] from an opencode [`Session`]. Snapshot diffs need
/// a resolver: use [`derive_path_with_resolver`] for them.
pub fn derive_path(session: &Session, config: &DeriveConfig) -> Path {
derive_path_with_resolver(session, config, &PathResolver::new())
derive_from_view(to_view(session), session, config)
}

/// Like [`derive_path`] but with a custom `PathResolver` (useful for
/// tests with a temp data directory).
/// Like [`derive_path`] but with a `PathResolver`, so the snapshot git
/// repository supplies file diffs.
pub fn derive_path_with_resolver(
session: &Session,
config: &DeriveConfig,
Expand All @@ -38,6 +39,14 @@ pub fn derive_path_with_resolver(
} else {
to_view_with_resolver(session, resolver)
};
derive_from_view(view, session, config)
}

fn derive_from_view(
view: toolpath_convo::ConversationView,
session: &Session,
config: &DeriveConfig,
) -> Path {
let base_uri = config.project_path.as_ref().map(|p| {
if p.starts_with('/') {
format!("file://{}", p)
Expand Down Expand Up @@ -108,9 +117,7 @@ mod tests {
))
.unwrap();
drop(conn);
let resolver = PathResolver::new()
.with_home(temp.path())
.with_data_dir(&data_dir);
let resolver = PathResolver::new(temp.path()).with_data_dir(&data_dir);
let mgr = OpencodeConvo::with_resolver(resolver.clone());
(temp, mgr, resolver)
}
Expand Down
3 changes: 0 additions & 3 deletions crates/toolpath-opencode/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ pub enum ConvoError {
#[error("Git error: {0}")]
Git(#[from] git2::Error),

#[error("Home directory not found")]
NoHomeDirectory,

#[error("opencode directory not found at path: {0}")]
OpencodeDirectoryNotFound(PathBuf),

Expand Down
Loading
Loading