Fix: rebuild from clean main (previous push included unrelated contaminated content)
CI / Lint (ruff) (pull_request) Failing after 8s
CI / Tests (py3.10) (pull_request) Failing after 8s
CI / Tests (py3.12) (pull_request) Failing after 7s

This commit is contained in:
2026-07-07 20:51:26 -04:00
parent 3e07b51134
commit 4ab3c3b00a
+5 -147
View File
@@ -29,7 +29,6 @@ import threading
import time
from dataclasses import dataclass
from pathlib import Path
from typing import NamedTuple
from urllib.parse import urlparse
CONFIG_FILENAME = "claude_desktop_config.json"
@@ -135,16 +134,6 @@ def profile_from_path(path: str | os.PathLike) -> Profile:
label = p.parent.name or p.name
return Profile(label=label, path=p, config_exists=p.is_file())
def profile_targets_claude_desktop(profile: Profile) -> bool:
"""
True when `profile` points at a Claude Desktop config
(claude_desktop_config.json), as opposed to Claude Code (~/.claude.json
or the legacy ~/.claude/settings.json). Used to gate Desktop-only actions
like "Restart Claude Desktop" so they never show up for a Claude Code
profile — restarting the CLI makes no sense.
"""
return Path(profile.path).name == CONFIG_FILENAME
# --------------------------------------------------------------------------- #
# Load / extract / apply
@@ -410,30 +399,6 @@ def config_mtime(path: Path | str) -> float | None:
return None
class ConfigStat(NamedTuple):
"""A snapshot of a config file's mtime + size.
Pairing size with mtime hardens stale-file detection beyond bare mtime
equality: a concurrent external write can land within the filesystem's
mtime resolution (e.g. same-second writes on ext4/HFS+) or have its mtime
restored by the writing process, in which case mtime alone would miss the
change. Comparing both fields catches those cases without the cost of a
full content hash.
"""
mtime: float
size: int
def config_fingerprint(path: Path | str) -> ConfigStat | None:
"""Return the file's (mtime, size) snapshot, or None if it does not exist."""
try:
st = Path(path).stat()
except OSError:
return None
return ConfigStat(st.st_mtime, st.st_size)
def external_change_summary(original_cfg: dict, path: Path | str) -> tuple[list[str], str]:
"""
Compare original_cfg (what BCC loaded) with the current on-disk state.
@@ -500,13 +465,13 @@ def suggest_name(data: dict) -> str:
# Word-processor / web artifacts mapped back to ASCII.
_QUOTE_MAP = {
"": '"',
"": '"',
""": '"',
""": '"',
"": '"',
"«": '"',
"»": '"',
"": "'",
"": "'",
"'": "'",
"'": "'",
"": "'",
}
_JUNK_CHARS = "" # zero-width chars / BOM
@@ -528,7 +493,7 @@ def _normalize_unicode(text: str, notes: list[str]) -> str:
out = text
for junk in _JUNK_CHARS:
out = out.replace(junk, "")
out = out.replace(" ", " ") # non-breaking space
out = out.replace(" ", " ") # non-breaking space
for smart, ascii_q in _QUOTE_MAP.items():
out = out.replace(smart, ascii_q)
if out != text:
@@ -1324,25 +1289,6 @@ def diagnostics_text(name: str, data: dict) -> str:
return "\n".join(L)
def server_log_path(name: str) -> Path | None:
"""
The platform-specific Claude Desktop MCP server log file for `name`, or
None if it doesn't exist yet (nothing has been logged for this server).
macOS : ~/Library/Logs/Claude/mcp-server-<name>.log (one file per server)
Windows: %APPDATA%\\Claude\\logs\\mcp.log (one shared file)
Other platforms: Claude Desktop doesn't ship a log in a known location -> None.
"""
if sys.platform == "darwin":
p = Path.home() / "Library" / "Logs" / "Claude" / f"mcp-server-{name}.log"
elif os.name == "nt":
appdata = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
p = appdata / "Claude" / "logs" / "mcp.log"
else:
return None
return p if p.is_file() else None
_STDERR_CAP = 4096 # bytes
@@ -1532,91 +1478,3 @@ def pin_command_path(data: dict, path: str | None = None) -> tuple[dict, str | N
out["args"] = args
return out, f"'{c}'{resolved}"
return data, None
# --------------------------------------------------------------------------- #
# Restart Claude Desktop (issue #9)
#
# Scoped strictly to Claude DESKTOP, the GUI app -- never Claude Code (the
# CLI), which has no long-running process to bounce. Callers should gate this
# behind profile_targets_claude_desktop() before offering it in the UI.
# --------------------------------------------------------------------------- #
class RestartResult(NamedTuple):
"""Outcome of a restart_claude_desktop() attempt."""
success: bool
detail: str
def _run_quiet(cmd: list[str]) -> None:
"""Best-effort fire-and-forget command. Never raises: a nonzero exit (e.g.
pkill finding nothing to kill) is expected and not an error."""
with contextlib.suppress(OSError):
subprocess.run(cmd, capture_output=True)
def _restart_claude_desktop_macos() -> RestartResult:
_run_quiet(["pkill", "-x", "Claude"])
try:
result = subprocess.run(["open", "-a", "Claude"], capture_output=True, text=True)
except OSError as e:
return RestartResult(False, f"Couldn't launch Claude Desktop: {e}")
if result.returncode != 0:
detail = (result.stderr or result.stdout or "").strip() or "'open -a Claude' failed"
return RestartResult(False, detail)
return RestartResult(True, "Claude Desktop restarted.")
def _claude_windows_start_menu_shortcut() -> Path:
appdata = os.environ.get("APPDATA", str(Path.home()))
return Path(appdata) / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Claude.lnk"
def _restart_claude_desktop_windows() -> RestartResult:
_run_quiet(["taskkill", "/IM", "Claude.exe", "/F"])
shortcut = _claude_windows_start_menu_shortcut()
try:
# `cmd /c start "" <target>` launches detached, the same as double-clicking
# the Start-menu shortcut, and returns immediately.
result = subprocess.run(
["cmd", "/c", "start", "", str(shortcut)], capture_output=True, text=True
)
except OSError as e:
return RestartResult(False, f"Couldn't launch Claude Desktop: {e}")
if result.returncode != 0:
detail = (
result.stderr or result.stdout or ""
).strip() or "failed to relaunch Claude Desktop"
return RestartResult(False, detail)
return RestartResult(True, "Claude Desktop restarted.")
def _restart_claude_desktop_linux() -> RestartResult:
_run_quiet(["pkill", "claude"])
try:
# The Linux launcher is the app itself (no "open"-style helper), so it
# has to be started detached rather than waited on.
subprocess.Popen(
["claude"],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
except OSError as e:
return RestartResult(False, f"Couldn't launch Claude Desktop: {e}")
return RestartResult(True, "Claude Desktop restarted.")
def restart_claude_desktop() -> RestartResult:
"""
Kill and relaunch the Claude Desktop app so a freshly saved config takes
effect. The app not currently running is NOT a failure -- pkill/taskkill
exiting non-zero just means "nothing to kill", and we go straight to
relaunching. Only a failed relaunch is reported as success=False.
"""
if sys.platform == "darwin":
return _restart_claude_desktop_macos()
if sys.platform.startswith("win"):
return _restart_claude_desktop_windows()
return _restart_claude_desktop_linux()