fix: detect nvm ai agent binaries

This commit is contained in:
lucaronin
2026-04-28 23:27:29 +02:00
parent f34fdb0289
commit 40544fc551
4 changed files with 64 additions and 6 deletions

View File

@@ -705,7 +705,7 @@ Tolaria tracks managed vault-level AI guidance separately from normal note conte
`useAiAgentsOnboarding(enabled)` adds a separate first-launch agent step:
- Reads a local dismissal flag for the AI agents prompt (with a legacy fallback to the older Claude-only key)
- Only shows after vault onboarding has already resolved to a ready state
- Uses `get_ai_agents_status`, whose backend treats the app process path, login-shell path, and supported local/toolchain/app install locations, including Windows `.exe` and npm/pnpm/Scoop shim paths, as valid CLI-agent sources
- Uses `get_ai_agents_status`, whose backend treats the app process path, login-shell path, and supported local/toolchain/app install locations, including nvm-managed Node installs plus Windows `.exe` and npm/pnpm/Scoop shim paths, as valid CLI-agent sources
- Persists dismissal locally once the user continues
### Remote Git Operations

View File

@@ -229,7 +229,7 @@ Full agent mode — spawns the selected local CLI agent as a subprocess with too
3. **Agent adapters** — Claude Code still uses `claude_cli.rs` with `acceptEdits`, strict Tolaria MCP config, a file/search-only built-in tool list, hidden Windows subprocess launches, and closed stdin for print-mode subprocesses so Windows launches receive EOF; Codex runs through `codex --sandbox workspace-write --ask-for-approval never exec --json`; OpenCode runs through `opencode run --format json`; Pi runs through `pi --mode json --no-session` with `npm:pi-mcp-adapter`. OpenCode and Pi both launch from the active vault cwd with closed stdin and transient MCP config. All app-launched paths use hidden Windows launches and avoid dangerous permission-bypass flags.
4. **MCP Integration** — Claude receives the generated MCP config file path, Codex receives the same Tolaria MCP server via transient `-c mcp_servers.tolaria.*` config overrides, OpenCode receives it through `OPENCODE_CONFIG_CONTENT`, and Pi receives it through a temporary `PI_CODING_AGENT_DIR/mcp.json` consumed by `pi-mcp-adapter`
CLI-agent availability intentionally does not depend only on the desktop app's inherited `PATH`. The detectors check the current process path, the user's login shell, and supported local/toolchain install locations such as native `~/.local/bin`, local `~/.claude/local`, Mise/asdf shims, npm-global, Homebrew, Windows `%APPDATA%\npm`/pnpm/Scoop shims, Windows `.exe` launchers, and the macOS Codex app resource path so first-run onboarding works on fresh macOS and Windows installs.
CLI-agent availability intentionally does not depend only on the desktop app's inherited `PATH`. The detectors check the current process path, the user's login shell, and supported local/toolchain install locations such as native `~/.local/bin`, local `~/.claude/local`, Mise/asdf shims, nvm-managed Node installs, npm-global, Homebrew, Windows `%APPDATA%\npm`/pnpm/Scoop shims, Windows `.exe` launchers, and the macOS Codex app resource path so first-run onboarding works on fresh macOS and Windows installs.
#### Agent Event Flow

View File

@@ -229,7 +229,7 @@ fn codex_binary_candidates() -> Vec<PathBuf> {
}
fn codex_binary_candidates_for_home(home: &Path) -> Vec<PathBuf> {
vec![
let mut candidates = vec![
home.join(".local/bin/codex"),
home.join(".codex/bin/codex"),
home.join(".local/share/mise/shims/codex"),
@@ -240,7 +240,24 @@ fn codex_binary_candidates_for_home(home: &Path) -> Vec<PathBuf> {
PathBuf::from("/usr/local/bin/codex"),
PathBuf::from("/opt/homebrew/bin/codex"),
PathBuf::from("/Applications/Codex.app/Contents/Resources/codex"),
]
];
candidates.extend(nvm_node_binary_candidates_for_home(home, "codex"));
candidates
}
fn nvm_node_binary_candidates_for_home(home: &Path, binary_name: &str) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(home.join(".nvm/versions/node")) else {
return Vec::new();
};
let mut candidates = entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| path.is_dir())
.map(|path| path.join("bin").join(binary_name))
.collect::<Vec<_>>();
candidates.sort();
candidates
}
fn find_existing_binary(candidates: Vec<PathBuf>) -> Option<PathBuf> {
@@ -585,6 +602,18 @@ mod tests {
}
}
#[test]
fn codex_binary_candidates_include_nvm_managed_node_installs() {
let home = tempfile::tempdir().unwrap();
let codex = home.path().join(".nvm/versions/node/v22.12.0/bin/codex");
std::fs::create_dir_all(codex.parent().unwrap()).unwrap();
std::fs::write(&codex, "#!/bin/sh\n").unwrap();
let candidates = codex_binary_candidates_for_home(home.path());
assert!(candidates.contains(&codex), "missing {}", codex.display());
}
#[test]
fn first_existing_path_skips_empty_and_missing_lines() {
let dir = tempfile::tempdir().unwrap();

View File

@@ -148,7 +148,7 @@ fn claude_binary_candidates() -> Vec<PathBuf> {
}
fn claude_binary_candidates_for_home(home: &Path) -> Vec<PathBuf> {
vec![
let mut candidates = vec![
home.join(".local/bin/claude"),
home.join(".local/bin/claude.exe"),
home.join(".claude/local/claude"),
@@ -170,7 +170,24 @@ fn claude_binary_candidates_for_home(home: &Path) -> Vec<PathBuf> {
home.join("scoop/shims/claude.exe"),
PathBuf::from("/opt/homebrew/bin/claude"),
PathBuf::from("/usr/local/bin/claude"),
]
];
candidates.extend(nvm_node_binary_candidates_for_home(home, "claude"));
candidates
}
fn nvm_node_binary_candidates_for_home(home: &Path, binary_name: &str) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(home.join(".nvm/versions/node")) else {
return Vec::new();
};
let mut candidates = entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| path.is_dir())
.map(|path| path.join("bin").join(binary_name))
.collect::<Vec<_>>();
candidates.sort();
candidates
}
fn find_existing_binary(candidates: Vec<PathBuf>) -> Option<PathBuf> {
@@ -658,6 +675,18 @@ mod tests {
}
}
#[test]
fn claude_binary_candidates_include_nvm_managed_node_installs() {
let home = tempfile::tempdir().unwrap();
let claude = home.path().join(".nvm/versions/node/v22.12.0/bin/claude");
std::fs::create_dir_all(claude.parent().unwrap()).unwrap();
std::fs::write(&claude, "#!/bin/sh\n").unwrap();
let candidates = claude_binary_candidates_for_home(home.path());
assert!(candidates.contains(&claude), "missing {}", claude.display());
}
#[test]
fn build_mcp_config_is_valid_json() {
if let Ok(config_str) = build_mcp_config("/tmp/test-vault") {