fix(clink): resolve npm-managed CLIs when nvm PATH is missing - #455
fix(clink): resolve npm-managed CLIs when nvm PATH is missing#455syf2211 wants to merge 2 commits into
Conversation
When PAL MCP runs via uvx, the subprocess often lacks nvm/fnm node bin directories in PATH, causing clink to fail with 'codex: command not found' even though the CLI is installed globally via npm. Augment PATH with common version-manager bin directories before resolving executables and pass the augmented PATH to CLI subprocesses. Fixes BeehiveInnovations#442
There was a problem hiding this comment.
Code Review
This pull request introduces PATH augmentation helpers in clink/path_utils.py to resolve npm-managed CLI executables (such as those managed by nvm, fnm, or volta) and integrates them into the base CLI agent. Feedback highlights a bug in sorting Node.js version directories lexicographically rather than semantically, which can lead to older versions being preferred. Additionally, there is a redundant call to augment_path when resolving executables in clink/agents/base.py because the environment's PATH is already augmented; using shutil.which directly would prevent duplicate filesystem operations and simplify imports.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| for node_bin in sorted(versions_dir.glob("*/bin"), reverse=True): | ||
| candidates.append(str(node_bin)) |
There was a problem hiding this comment.
The current implementation sorts the Node.js version directories lexicographically using sorted(..., reverse=True). Lexicographical sorting of version strings does not align with semantic versioning (e.g., "v9.0.0" is lexicographically greater than "v22.1.0" because '9' > '2'). This can cause older Node.js versions to be incorrectly preferred over newer ones when no default alias is set.
We should use a custom sorting key that parses the version components as integers to ensure correct semantic version ordering.
| for node_bin in sorted(versions_dir.glob("*/bin"), reverse=True): | |
| candidates.append(str(node_bin)) | |
| import re | |
| def version_key(p: Path) -> list[int]: | |
| return [int(x) for x in re.findall(r"\d+", p.parent.name)] | |
| for node_bin in sorted(versions_dir.glob("*/bin"), key=version_key, reverse=True): | |
| candidates.append(str(node_bin)) |
| # Resolve executable path for cross-platform compatibility (especially Windows) | ||
| executable_name = command[0] | ||
| resolved_executable = shutil.which(executable_name) | ||
| resolved_executable = resolve_executable(executable_name, path=env.get("PATH")) |
There was a problem hiding this comment.
In _build_environment(), env["PATH"] is already augmented with augment_path(env.get("PATH")). Calling resolve_executable(executable_name, path=env.get("PATH")) here results in a redundant call to augment_path(), which performs duplicate filesystem I/O (scanning directories, reading files, etc.).
Since env["PATH"] is already fully augmented, we can directly use shutil.which() to resolve the executable path efficiently.
import shutil
resolved_executable = shutil.which(executable_name, path=env.get("PATH"))| from clink.constants import DEFAULT_STREAM_LIMIT | ||
| from clink.models import ResolvedCLIClient, ResolvedCLIRole | ||
| from clink.parsers import BaseParser, ParsedCLIResponse, ParserError, get_parser | ||
| from clink.path_utils import augment_path, resolve_executable |
There was a problem hiding this comment.
- Sort nvm version directories by semantic version, not lexicographically - Use shutil.which on already-augmented PATH to avoid redundant I/O - Add test for semantic version ordering
|
Thanks for the review! Addressed both points in aaa867d:
All clink path_utils tests pass locally. |
Summary
Fixes clink failing with
codex: command not foundwhen Codex CLI is installed globally via nvm + npm, but the PAL MCP subprocess (e.g. launched byuvx) does not inherit the nvm-injected PATH.Motivation
Issue #442 reports that
clink with codexfails on Linux when Codex is installed withnpm install -g @openai/codexunder nvm. The MCP server process only sees a minimal PATH, soshutil.which("codex")returnsNoneeven though the binary exists under~/.nvm/versions/node/<version>/bin.Changes
clink/path_utils.pywith helpers to collect common npm/node version-manager bin directories (nvm, fnm, volta, asdf, mise,~/.npm-global/bin)resolve_executable()instead of bareshutil.which()inBaseCLIAgent.run()PATHin_build_environment()so node-based CLIs can find their runtimeresolve_executableTests
python3 -m pytest tests/test_clink_path_utils.py tests/test_clink_codex_agent.py tests/test_clink_claude_agent.py tests/test_clink_gemini_agent.py -q # 12 passedNotes
$NVM_DIR/versions/node/*/bin); nvm-windows uses different paths and is out of scope for this issue.Fixes #442