Skip to content

Commit 2a4d3d9

Browse files
jgravelleclaude
andcommitted
release: v1.108.84 — install-mechanism-aware upgrade (#357)
pipx/uv venvs ship no pip module, so 'jcodemunch-mcp upgrade' died with 'No module named pip' and skipped the hook refresh. detect_install_mechanism() now routes pip installs through in-process pip-install-U as before, and on pipx/uv prints the exact upgrade command + refreshes hooks in-process (no pip). Startup version-drift warning leads with 'init --hooks' (pip-free). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 883b115 commit 2a4d3d9

6 files changed

Lines changed: 305 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,35 @@
22

33
All notable changes to jcodemunch-mcp are documented here.
44

5+
## [1.108.84] - 2026-06-28 - Install-mechanism-aware `upgrade` (#357)
6+
7+
`jcodemunch-mcp upgrade` shelled out to `python -m pip install -U` blindly.
8+
pipx- and uv-managed venvs ship no `pip` module, so the command died with
9+
`No module named pip` and bailed *before* refreshing hooks (reported by
10+
@zakblacki on a pipx install). Worse, the startup version-drift warning told
11+
the user to run `jcodemunch-mcp upgrade` to clear it — the exact command that
12+
fails on pip-less installs — so the nag never cleared.
13+
14+
### Changed
15+
16+
- **`upgrade` detects the install mechanism instead of guessing-and-running.**
17+
New `detect_install_mechanism()` (path/env heuristic, no subprocess) returns
18+
the mechanism (`pip`/`pipx`/`uv`/`uvx`/`venv`) and the exact upgrade command
19+
for it. When `pip` is importable, the in-process `pip install -U` runs as
20+
before. When it isn't (pipx/uv), the command no longer invokes a foreign
21+
package manager — it prints the correct one (`pipx upgrade jcodemunch-mcp` /
22+
`uv tool upgrade jcodemunch-mcp`) and still refreshes hooks/config
23+
in-process, which needs no pip. A genuine pip *failure* (network/permissions)
24+
now refreshes hooks anyway and preserves the non-zero exit code rather than
25+
skipping the refresh.
26+
- **Startup version-drift warning leads with the pip-free command.** Now
27+
recommends `jcodemunch-mcp init --hooks` (needs no pip) to clear the stale-hook
28+
nag, noting `upgrade` also works on pip installs.
29+
30+
New `cli/upgrade.py` helpers `_pip_available()` / `detect_install_mechanism()` /
31+
`_refresh_hooks()` / `_print_external_upgrade_hint()`. No INDEX_VERSION bump.
32+
New `tests/test_v1_108_84.py` (11).
33+
534
## [1.108.83] - 2026-06-26 - Tame watch-all CPU under WSL (#356)
635

736
`watch-all` pegged 15-30% CPU continuously on WSL (reported by @Blainexi). Root

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# jcodemunch-mcp — Project Brief
22

33
## Current State
4+
- **Version:** 1.108.84 — Install-mechanism-aware `upgrade` (#357, reported by @zakblacki on a pipx install). `jcodemunch-mcp upgrade` ran `sys.executable -m pip install -U` blindly; pipx/uv venvs ship NO `pip` module → `No module named pip`, pip exited non-zero, and the command **bailed before refreshing hooks**. Compounding it: the startup version-drift warning (`server.py`) told the user to run `jcodemunch-mcp upgrade` to clear the stale-hook nag — the exact pip-less-failing command — so it never cleared. Fix (rewrote `cli/upgrade.py`): new `detect_install_mechanism()` (path/env heuristic, no subprocess) → `(mechanism, upgrade_command)` for pip/pipx/uv/uvx/venv. `_pip_available()` gates the in-process `pip install -U`: present → runs as before (a genuine pip *failure* now refreshes hooks anyway + preserves the non-zero rc, was: skip-and-bail); **absent → does NOT shell out to a foreign package manager** (honors jjg's stated "detect then tell, don't guess-and-run" stance from the issue thread), prints the exact command (`pipx upgrade` / `uv tool upgrade`), and refreshes hooks in-process via `init --hooks` (pip-free, stamps `last_init_version.txt` so the nag clears). The nag warning now leads with `init --hooks`. NO INDEX_VERSION bump. New `tests/test_v1_108_84.py` (11). Files: `cli/upgrade.py` (rewrite), `server.py` (warning text). Side issues answered in #357 reply (not bugs): `.pack/nodejs` migration warning = stale starter-pack index; `find_dead_code` `framework_warning` inflated dead-code % on a Node project because its entry-point heuristic looks for `main.py`/`app.py` (pass `entry_point_patterns` or reindex); real-time freshness = `watch-all`. See [[project_jmunch_console]].
45
- **Version:** 1.108.83 — Tame `watch-all` CPU under WSL (#356, reported by @Blainexi). `watch-all` pegged 15-30% CPU non-stop on WSL. NOT a busy loop: **`watchfiles` auto-enables polling whenever it detects WSL** (inotify unreliable across the boundary, confirmed in watchfiles docs), and its default 300ms poll re-stats the whole watched tree several times/sec — multiplied across every indexed repo. `DefaultFilter` drops `node_modules`/`.git` *events* but the polling walk still stats them (structural cost). Fix: the `awatch` call now passes `poll_delay_ms=_watch_poll_delay_ms()` (resolves `JCODEMUNCH_WATCH_POLL_DELAY_MS` → `WATCHFILES_POLL_DELAY_MS` → `DEFAULT_WATCH_POLL_DELAY_MS=1000`; non-positive/garbage → default); ignored when native FS events are used (no-op off WSL). Plus a one-time WSL startup hint (`_is_wsl()`) naming the two levers: raise the poll delay, or set `WATCHFILES_FORCE_POLLING=false` for Linux-filesystem repos to use native inotify (~0 idle CPU); `/mnt/*` repos need polling. NO INDEX_VERSION bump. New `tests/test_v1_108_83.py` (11); `test_watch_all.py`/`test_watcher_lock.py` green. Files: `watcher.py`, `watch_all.py`. See [[project_jmunch_console]].
56
- **Version:** 1.108.82 — Exclude `org_savings.db` from `list_repos`. The team-SKU org-rollup store (`org_savings.db`, written by `org/store.py`) sits in `~/.code-index/` next to per-repo index `.db` files, but `_NON_REPO_DB_FILES` only excluded `telemetry.db` → `list_repos` enumerated it as a phantom `local/org_savings` repo (sym 0, empty source_root, display==repo_id). In the jMunch Console it showed an un-removable card; `delete-index local/org_savings` failed (no matching `local-org_savings.db` index → non-zero exit), which the Console surfaced as the misleading "needs jcodemunch-mcp >= 1.108.50". Fix: `_NON_REPO_DB_FILES = frozenset({"telemetry.db", "org_savings.db"})` (1-line). NO INDEX_VERSION bump. New `tests/test_telemetry_db_skip.py::test_list_repos_skips_org_savings_db`; storage/sqlite_store suites green. Files: `storage/sqlite_store.py`. Console-side companion: the Console's delete-failure message now surfaces jcm's real error instead of the hard-coded version hint (jmunch-console). See [[project_jmunch_console]], [[project_org_rollup_sku]].
67
- **Version:** 1.108.81 — Lazy git identity probe in `get_watch_status` / `list-repos`. `list-repos` (and `get_watch_status`) slowed to 60s+ on many-repo hosts because the per-folder loop resolved each repo's reindex-state key up front via `_reindex_key` → `_local_repo_id` → `resolve_index_identity` — a **git identity probe ≈1 subprocess/repo** (~0.2s each here, worse in worktree-heavy trees; cf. the #303 resolve_repo perf history). But reindex_state is **in-memory only** (populated by a watcher/server process); a cold `list-repos` CLI (the jMunch Console cockpit shells exactly this) has none, so every probe resolved a key only to read an empty default. Fix: new `reindex_state.has_any_reindex_state()` (`bool(_repo_states)` under lock); `get_watch_status` computes `track_reindex = has_any_reindex_state()` ONCE before the loop and skips `_reindex_key`/`get_reindex_status` entirely when False, substituting the default `{index_stale:False, reindex_in_progress:False, stale_since_ms:None}` per repo. **Output shape unchanged** (cold path already returned these defaults — just after paying for N git probes); a watcher/server process WITH state still resolves keys so #353 crash-loop visibility holds. `process_locks.inspect` (OpenProcess, ~0.01s/10) and `service_status` (~0.26s) were measured cheap; the git probe was the sole per-repo scaler. NO INDEX_VERSION bump. New `tests/test_v1_108_81.py` (2: probe skipped + get_reindex_status not consulted when no state; keys resolved when state present); `test_v1_108_78.py`/`test_watch_all.py`/`test_reindex_state.py`/`test_watcher_lock.py` still green. Files: `reindex_state.py`, `tools/get_watch_status.py`. Motivated by the jMunch Console showing fixtures because its 20s CLI timeout lost to this; see [[project_jmunch_console]].

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "jcodemunch-mcp"
3-
version = "1.108.83"
3+
version = "1.108.84"
44
description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing"
55
readme = "README.md"
66
requires-python = ">=3.10"

src/jcodemunch_mcp/cli/upgrade.py

Lines changed: 128 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,78 @@
1-
"""``jcodemunch-mcp upgrade`` — pip install -U + refresh hooks/config.
1+
"""``jcodemunch-mcp upgrade`` — package upgrade + refresh hooks/config.
22
33
Single command for the post-release path that previously required two
44
steps (``pip install -U jcodemunch-mcp`` and ``jcodemunch-mcp init
55
--hooks``). Picked up from issue #273: users on Copilot/parallel-session
66
workflows kept missing the second step and ending up with stale hook
77
templates pointing at older binaries.
8+
9+
Install-mechanism aware (#357): a pipx- or uv-managed venv ships no
10+
``pip`` module, so the old ``python -m pip install -U`` died with
11+
``No module named pip`` and skipped the hook refresh entirely. We do NOT
12+
guess-and-run a foreign package manager; instead we detect the mechanism,
13+
print the exact upgrade command for it, and still refresh hooks/config
14+
in-process (which needs no pip).
815
"""
916

1017
from __future__ import annotations
1118

19+
import importlib.util
20+
import os
1221
import shutil
1322
import subprocess
1423
import sys
1524

1625

17-
def run_upgrade(*, no_pip: bool = False, yes: bool = True) -> int:
18-
"""Run ``pip install -U jcodemunch-mcp`` then refresh hooks/config.
26+
def _pip_available() -> bool:
27+
"""True when ``pip`` is importable in the current interpreter.
1928
20-
Returns exit code (0 on success, non-zero on pip failure).
29+
pipx and uv-tool venvs intentionally omit pip, so ``python -m pip``
30+
fails with ``No module named pip`` rather than upgrading anything.
31+
"""
32+
try:
33+
return importlib.util.find_spec("pip") is not None
34+
except (ImportError, ValueError):
35+
return False
36+
37+
38+
def detect_install_mechanism() -> tuple[str, str | None]:
39+
"""Best-effort detection of how jcodemunch-mcp was installed.
40+
41+
Returns ``(mechanism, upgrade_command)`` where ``mechanism`` is one of
42+
``pip`` / ``pipx`` / ``uv`` / ``uvx`` / ``venv``, and ``upgrade_command``
43+
is the exact shell command to upgrade the package outside this process
44+
(or ``None`` for plain pip, where in-process ``pip install -U`` works).
45+
46+
Detection is path/env heuristic — it never runs a subprocess.
47+
"""
48+
exe = (sys.executable or "").replace("\\", "/").lower()
49+
50+
# pipx: venvs live under .../pipx/venvs/<pkg>/...
51+
if "/pipx/venvs/" in exe or "/pipx/venv/" in exe or os.environ.get("PIPX_HOME"):
52+
return "pipx", "pipx upgrade jcodemunch-mcp"
53+
54+
# uv tool install: .../uv/tools/<pkg>/...
55+
if "/uv/tools/" in exe:
56+
return "uv", "uv tool upgrade jcodemunch-mcp"
57+
58+
# uvx ephemeral run: resolved out of the uv cache; nothing to "upgrade"
59+
# persistently, so point at the @latest refresh form.
60+
if "/uv/" in exe or "/.cache/uv/" in exe or "/uv/cache/" in exe:
61+
return "uvx", "uvx jcodemunch-mcp@latest --version"
62+
63+
# Plain virtualenv without pip (rare) — caller must upgrade it themselves.
64+
if not _pip_available():
65+
return "venv", None
66+
67+
return "pip", None
68+
69+
70+
def _refresh_hooks(*, yes: bool = True) -> int:
71+
"""Refresh hook templates/config via ``init --hooks`` (no pip required).
72+
73+
Prefers the freshly-installed binary if it's on PATH, otherwise falls
74+
back to the in-process module entry so a venv shim isn't bypassed.
2175
"""
22-
if not no_pip:
23-
pip_args = [sys.executable, "-m", "pip", "install", "-U", "jcodemunch-mcp"]
24-
print(f"$ {' '.join(pip_args)}")
25-
try:
26-
r = subprocess.run(pip_args, check=False)
27-
except OSError as e:
28-
print(f" pip invocation failed: {e}", file=sys.stderr)
29-
return 1
30-
if r.returncode != 0:
31-
print(
32-
" pip exited non-zero; skipping hook refresh. "
33-
"Re-run with --no-pip after fixing the install.",
34-
file=sys.stderr,
35-
)
36-
return r.returncode
37-
38-
# Run init in --yes mode to refresh hook templates non-interactively.
39-
# Use the freshly-installed binary if it's on PATH, otherwise fall back
40-
# to the in-process module entry so a venv shim doesn't get bypassed.
4176
exe = shutil.which("jcodemunch-mcp")
4277
if exe:
4378
init_cmd = [exe, "init", "--hooks"]
@@ -53,3 +88,73 @@ def run_upgrade(*, no_pip: bool = False, yes: bool = True) -> int:
5388
print(f" init invocation failed: {e}", file=sys.stderr)
5489
return 1
5590
return r.returncode
91+
92+
93+
def _print_external_upgrade_hint(mechanism: str, command: str | None) -> None:
94+
"""Tell the user the exact command for their install mechanism."""
95+
if command:
96+
print(
97+
f"pip is not available in this environment "
98+
f"({mechanism}-managed install detected)."
99+
)
100+
print("To upgrade the package, run:")
101+
print(f" {command}")
102+
print("Then restart your editor / MCP client so it relaunches the server.")
103+
else:
104+
print(
105+
f"pip is not available in this environment "
106+
f"({mechanism} install detected)."
107+
)
108+
print(
109+
"Upgrade jcodemunch-mcp through whatever installed it, restart your "
110+
"MCP client, then re-run `jcodemunch-mcp upgrade --no-pip` to refresh hooks."
111+
)
112+
113+
114+
def run_upgrade(*, no_pip: bool = False, yes: bool = True) -> int:
115+
"""Upgrade jcodemunch-mcp (when possible) then refresh hooks/config.
116+
117+
Behavior by environment:
118+
119+
- **pip available** (default venv / system pip): runs
120+
``python -m pip install -U jcodemunch-mcp``; on a genuine pip failure
121+
(network, permissions) the non-zero code is preserved but hooks are
122+
still refreshed so a partial state can recover.
123+
- **pip absent** (pipx / uv tool / uvx): does NOT shell out to a foreign
124+
package manager. Prints the exact upgrade command for the detected
125+
mechanism, then refreshes hooks/config in-process and returns that
126+
refresh's exit code.
127+
128+
Returns an exit code (0 on success).
129+
"""
130+
package_upgrade_rc = 0
131+
132+
if not no_pip:
133+
if _pip_available():
134+
pip_args = [sys.executable, "-m", "pip", "install", "-U", "jcodemunch-mcp"]
135+
print(f"$ {' '.join(pip_args)}")
136+
try:
137+
r = subprocess.run(pip_args, check=False)
138+
except OSError as e:
139+
print(f" pip invocation failed: {e}", file=sys.stderr)
140+
package_upgrade_rc = 1
141+
else:
142+
if r.returncode != 0:
143+
print(
144+
" pip exited non-zero; refreshing hooks anyway. "
145+
"Fix the package install, then re-run with --no-pip.",
146+
file=sys.stderr,
147+
)
148+
package_upgrade_rc = r.returncode
149+
else:
150+
mechanism, command = detect_install_mechanism()
151+
_print_external_upgrade_hint(mechanism, command)
152+
153+
print("Refreshing hooks/config in-process (no pip required)...")
154+
refresh_rc = _refresh_hooks(yes=yes)
155+
156+
# A genuine pip failure is a real error the caller should see; a
157+
# pip-absent environment is not a failure (the package upgrade is the
158+
# user's separate keystroke, which we've printed), so the hook-refresh
159+
# result governs the exit code there.
160+
return package_upgrade_rc or refresh_rc

src/jcodemunch_mcp/server.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8416,7 +8416,8 @@ def main(argv: Optional[list[str]] = None):
84168416
logger.warning(
84178417
"jcodemunch-mcp upgraded %s -> %s but `init` has not been "
84188418
"re-run. Hook templates and config may be stale; run "
8419-
"`jcodemunch-mcp upgrade` (or `init --hooks`) to refresh.",
8419+
"`jcodemunch-mcp init --hooks` to refresh (needs no pip; "
8420+
"`jcodemunch-mcp upgrade` also works on pip installs).",
84208421
_stamped,
84218422
_current_version,
84228423
)

0 commit comments

Comments
 (0)