Skip to content

fix(clink): resolve codex CLI from nvm/npm global paths - #458

Open
syf2211 wants to merge 3 commits into
BeehiveInnovations:mainfrom
syf2211:fix/442-codex-nvm-path
Open

fix(clink): resolve codex CLI from nvm/npm global paths#458
syf2211 wants to merge 3 commits into
BeehiveInnovations:mainfrom
syf2211:fix/442-codex-nvm-path

Conversation

@syf2211

@syf2211 syf2211 commented Jun 27, 2026

Copy link
Copy Markdown

Summary

Fix clink failing with codex: command not found when Codex CLI is installed globally via nvm + npm and PAL MCP is launched through uvx, which does not inherit nvm-injected PATH entries.

Motivation

Fixes #442. The MCP subprocess inherits a minimal PATH from uvx, so shutil.which('codex') cannot find binaries installed under $NVM_DIR/versions/node/*/bin.

Changes

  • Add clink/path_resolution.py with resolve_cli_executable() and augment_path() to search common user-local install directories (nvm node bins, ~/.local/bin, ~/.npm-global/bin, etc.)
  • Update clink/agents/base.py to resolve CLI executables via the augmented lookup and pass an augmented PATH to subprocesses
  • Add unit and integration tests for nvm-style layouts
  • Update existing agent test fake_which stubs to accept path= (required by shutil.which)

Tests

pytest tests/test_clink_path_resolution.py tests/test_clink_codex_path_resolution.py tests/test_clink_codex_agent.py tests/test_clink_claude_agent.py tests/test_clink_gemini_agent.py -q
# 12 passed

Notes

  • Resolution still prefers the inherited PATH; augmented directories are only used as fallback.
  • Mirrors the native CLI discovery pattern already used in run-server.sh for Claude/Codex integration.
  • Does not cover fnm/volta/asdf layouts; can be extended if needed.

syf2211 added 2 commits June 27, 2026 01:34
When PAL MCP runs via uvx, PATH may omit nvm-injected directories so
clink cannot find globally installed codex binaries. Augment PATH with
common user-local install locations before resolving CLI executables.

Fixes BeehiveInnovations#442

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new path resolution utility (clink/path_resolution.py) to locate CLI executables in common user-local installation directories (such as NVM, NPM, and Cargo) when they are missing from the host PATH. The base agent is updated to leverage this utility, and corresponding unit and integration tests are added. Feedback on these changes highlights a potential bug in lexicographical version sorting for Node directories, an opportunity to simplify path deduplication using dict.fromkeys(), and a cross-platform issue in tests where mocking the HOME environment variable should be replaced by directly mocking Path.home.

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.

Comment thread clink/path_resolution.py
Comment on lines +17 to +19
if versions_dir.is_dir():
version_bins = sorted(versions_dir.glob("*/bin"), reverse=True)
candidates.extend(str(path) for path in version_bins)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Sorting version directories lexicographically with reverse=True can lead to incorrect version preference. For example, "v9.0.0" > "v18.0.0" evaluates to True in Python because '9' > '1'. This means an older single-digit major version of Node will be preferred over a newer double-digit major version.

To fix this, we should use a semver-aware sorting key that splits the version string into integers.

    if versions_dir.is_dir():
        def version_key(p: Path) -> tuple[int, ...]:
            parts = p.parent.name.lstrip("v").split("-")[0].split(".")
            return tuple(int(x) for x in parts if x.isdigit())

        version_bins = sorted(versions_dir.glob("*/bin"), key=version_key, reverse=True)
        candidates.extend(str(path) for path in version_bins)

Comment thread clink/path_resolution.py
Comment on lines +30 to +36
seen: set[str] = set()
unique: list[str] = []
for candidate in candidates:
if candidate not in seen:
seen.add(candidate)
unique.append(candidate)
return unique

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In Python 3.7+, dictionaries maintain insertion order. We can simplify the deduplication of candidate paths using dict.fromkeys(), which is more concise and idiomatic.

Suggested change
seen: set[str] = set()
unique: list[str] = []
for candidate in candidates:
if candidate not in seen:
seen.add(candidate)
unique.append(candidate)
return unique
return list(dict.fromkeys(candidates))

def test_augment_path_prepends_candidate_dirs(tmp_path, monkeypatch):
local_bin = tmp_path / ".local" / "bin"
local_bin.mkdir(parents=True)
monkeypatch.setenv("HOME", str(tmp_path))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using monkeypatch.setenv("HOME", ...) to mock the home directory is not fully cross-platform because Path.home() on Windows resolves using the USERPROFILE environment variable (or other Windows-specific APIs) rather than HOME.

To ensure the tests are robust and cross-platform, we should mock Path.home directly using monkeypatch.setattr.

Suggested change
monkeypatch.setenv("HOME", str(tmp_path))
import pathlib
monkeypatch.setattr(pathlib.Path, "home", lambda: tmp_path)


monkeypatch.setenv("NVM_DIR", str(tmp_path / ".nvm"))
monkeypatch.setenv("PATH", "/usr/bin:/bin")
monkeypatch.setenv("HOME", str(tmp_path))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using monkeypatch.setenv("HOME", ...) to mock the home directory is not fully cross-platform because Path.home() on Windows resolves using the USERPROFILE environment variable (or other Windows-specific APIs) rather than HOME.

To ensure the tests are robust and cross-platform, we should mock Path.home directly using monkeypatch.setattr.

Suggested change
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setattr(Path, "home", lambda: tmp_path)

@syf2211
syf2211 force-pushed the fix/442-codex-nvm-path branch from ba5b732 to 8728994 Compare June 27, 2026 07:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] clink "codex: command not found" when Codex CLI is installed globally via nvm + npm

1 participant