meshcode 2.10.43__tar.gz → 2.10.46__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {meshcode-2.10.43 → meshcode-2.10.46}/PKG-INFO +1 -1
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/__init__.py +1 -1
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/comms_v4.py +37 -9
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/meshcode_mcp/server.py +47 -12
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/run_agent.py +144 -9
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.43 → meshcode-2.10.46}/pyproject.toml +1 -1
- {meshcode-2.10.43 → meshcode-2.10.46}/README.md +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/cli.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/invites.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/launcher.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/preferences.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/secrets.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/self_update.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/setup.cfg +0 -0
- {meshcode-2.10.43 → meshcode-2.10.46}/tests/test_status_enum_coverage.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "2.10.
|
|
2
|
+
__version__ = "2.10.46"
|
|
@@ -315,10 +315,14 @@ def mark_nudged(project, name):
|
|
|
315
315
|
|
|
316
316
|
|
|
317
317
|
def _sanitize_notification_text(text: str) -> str:
|
|
318
|
-
"""
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
318
|
+
"""Whitelist-sanitize text for safe use in shell notification commands.
|
|
319
|
+
|
|
320
|
+
Only allows alphanumeric, spaces, and safe punctuation. This is a
|
|
321
|
+
whitelist (not blocklist) to prevent injection in osascript, PowerShell,
|
|
322
|
+
and notify-send even if new shell metacharacters are discovered.
|
|
323
|
+
"""
|
|
324
|
+
import re
|
|
325
|
+
return re.sub(r'[^a-zA-Z0-9 _\-.,!?:@#/\u00c0-\u024f\u2190-\u21ff]', '', text)[:200]
|
|
322
326
|
|
|
323
327
|
|
|
324
328
|
def send_notification(project, name, from_agent, pending=1):
|
|
@@ -332,19 +336,20 @@ def send_notification(project, name, from_agent, pending=1):
|
|
|
332
336
|
f'display notification "{body}" with title "{title}" sound name "Ping"'],
|
|
333
337
|
capture_output=True, timeout=3)
|
|
334
338
|
elif sys.platform == "win32":
|
|
335
|
-
# PowerShell:
|
|
336
|
-
#
|
|
339
|
+
# PowerShell: read title/body from environment variables instead of
|
|
340
|
+
# interpolating into the script string — prevents PS injection.
|
|
337
341
|
ps_script = (
|
|
338
342
|
'[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null; '
|
|
339
343
|
'$xml = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent(0); '
|
|
340
344
|
'$text = $xml.GetElementsByTagName("text"); '
|
|
341
|
-
|
|
342
|
-
|
|
345
|
+
'$text[0].AppendChild($xml.CreateTextNode($env:MC_NOTIFY_TITLE)); '
|
|
346
|
+
'$text[1].AppendChild($xml.CreateTextNode($env:MC_NOTIFY_BODY)); '
|
|
343
347
|
'$toast = [Windows.UI.Notifications.ToastNotification]::new($xml); '
|
|
344
348
|
'[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("MeshCode").Show($toast)'
|
|
345
349
|
)
|
|
350
|
+
env = {**os.environ, "MC_NOTIFY_TITLE": title, "MC_NOTIFY_BODY": body}
|
|
346
351
|
subprocess.run(['powershell', '-NoProfile', '-Command', ps_script],
|
|
347
|
-
capture_output=True, timeout=5)
|
|
352
|
+
capture_output=True, timeout=5, env=env)
|
|
348
353
|
else:
|
|
349
354
|
# Linux: notify-send takes title and body as separate args (no shell)
|
|
350
355
|
subprocess.run(['notify-send', title, body], capture_output=True, timeout=3)
|
|
@@ -2735,12 +2740,35 @@ if __name__ == "__main__":
|
|
|
2735
2740
|
cur = get_permission_mode()
|
|
2736
2741
|
print(f"permission_mode: {cur or '(unset — will prompt on next run)'}")
|
|
2737
2742
|
sys.exit(0)
|
|
2743
|
+
elif sub == "claude-version":
|
|
2744
|
+
from meshcode.preferences import load_prefs, save_prefs
|
|
2745
|
+
if len(sys.argv) > 3:
|
|
2746
|
+
ver = sys.argv[3].strip()
|
|
2747
|
+
if ver in ("reset", "unpin", "latest"):
|
|
2748
|
+
prefs = load_prefs()
|
|
2749
|
+
prefs.pop("claude_version", None)
|
|
2750
|
+
save_prefs(prefs)
|
|
2751
|
+
print("[meshcode] Claude Code version pin removed — will use installed version")
|
|
2752
|
+
else:
|
|
2753
|
+
prefs = load_prefs()
|
|
2754
|
+
prefs["claude_version"] = ver
|
|
2755
|
+
save_prefs(prefs)
|
|
2756
|
+
print(f"[meshcode] Claude Code version pinned to: {ver}")
|
|
2757
|
+
print(f"[meshcode] meshcode run will use npx @anthropic-ai/claude-code@{ver}")
|
|
2758
|
+
sys.exit(0)
|
|
2759
|
+
else:
|
|
2760
|
+
from meshcode.preferences import load_prefs
|
|
2761
|
+
cur = load_prefs().get("claude_version")
|
|
2762
|
+
print(f"claude_version: {cur or '(not pinned — using installed version)'}")
|
|
2763
|
+
sys.exit(0)
|
|
2738
2764
|
elif sub == "reset":
|
|
2739
2765
|
reset_permission_mode()
|
|
2740
2766
|
print("[meshcode] preferences reset")
|
|
2741
2767
|
sys.exit(0)
|
|
2742
2768
|
else:
|
|
2743
2769
|
print("Usage: meshcode prefs permission-mode [bypass|safe|ask]")
|
|
2770
|
+
print(" meshcode prefs claude-version [version|reset]")
|
|
2771
|
+
print(" meshcode prefs auto-update [on|off|reset]")
|
|
2744
2772
|
print(" meshcode prefs reset")
|
|
2745
2773
|
sys.exit(1)
|
|
2746
2774
|
|
|
@@ -174,10 +174,12 @@ def _try_auto_wake(from_agent: str, preview: str) -> None:
|
|
|
174
174
|
system = platform.system()
|
|
175
175
|
try:
|
|
176
176
|
if system == "Darwin":
|
|
177
|
-
#
|
|
177
|
+
# Sanitize app_name — TERM_PROGRAM is an env var, not DB-sourced,
|
|
178
|
+
# but defense-in-depth: only allow known terminal app names.
|
|
178
179
|
parent_app = os.environ.get("TERM_PROGRAM", "Terminal")
|
|
179
180
|
app_name = "iTerm" if "iTerm" in parent_app else "Terminal"
|
|
180
|
-
# Escape for AppleScript string: replace backslash and double-quote
|
|
181
|
+
# Escape for AppleScript string: replace backslash and double-quote.
|
|
182
|
+
# nudge is already whitelist-sanitized (alphanum + _-.,!? space only).
|
|
181
183
|
as_safe = nudge.replace("\\", "\\\\").replace('"', '\\"')
|
|
182
184
|
script = f'''
|
|
183
185
|
tell application "{app_name}"
|
|
@@ -191,16 +193,16 @@ def _try_auto_wake(from_agent: str, preview: str) -> None:
|
|
|
191
193
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
192
194
|
log.info(f"auto-wake: injected nudge via {app_name} AppleScript")
|
|
193
195
|
elif system == "Windows":
|
|
194
|
-
#
|
|
195
|
-
|
|
196
|
-
ps_script =
|
|
196
|
+
# Pass nudge text via environment variable instead of interpolating
|
|
197
|
+
# into a PowerShell string — prevents any PS injection.
|
|
198
|
+
ps_script = '''
|
|
197
199
|
Add-Type -AssemblyName System.Windows.Forms
|
|
198
|
-
[System.Windows.Forms.SendKeys]::SendWait("{
|
|
200
|
+
[System.Windows.Forms.SendKeys]::SendWait($env:MC_NUDGE_TEXT + "{ENTER}")
|
|
199
201
|
'''
|
|
200
|
-
|
|
202
|
+
env = {**os.environ, "MC_NUDGE_TEXT": nudge}
|
|
201
203
|
subprocess.run(["powershell", "-NoProfile", "-Command", ps_script],
|
|
202
204
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
203
|
-
timeout=10)
|
|
205
|
+
timeout=10, env=env)
|
|
204
206
|
log.info("auto-wake: injected nudge via PowerShell SendKeys")
|
|
205
207
|
else:
|
|
206
208
|
# Linux — xdotool takes args directly, not shell-interpolated (safe)
|
|
@@ -633,6 +635,12 @@ def with_working_status(func):
|
|
|
633
635
|
_record_event_bg("tool_call", {"tool": name, "args_keys": list(kwargs.keys()), "estimated_tokens": _est_tokens})
|
|
634
636
|
try:
|
|
635
637
|
return await func(*args, **kwargs)
|
|
638
|
+
except asyncio.CancelledError:
|
|
639
|
+
# ESC in Claude Code cancels the in-flight task. Catch here
|
|
640
|
+
# to prevent cascade through FastMCP into the event loop.
|
|
641
|
+
# Return an error dict instead of propagating BaseException.
|
|
642
|
+
log.debug(f"[meshcode] tool {name} cancelled by client (ESC)")
|
|
643
|
+
return {"error": "cancelled_by_client", "tool": name}
|
|
636
644
|
except Exception as e:
|
|
637
645
|
if not skip:
|
|
638
646
|
_auto_learn_error(name, e, list(kwargs.keys()))
|
|
@@ -1663,7 +1671,11 @@ async def meshcode_call(to: str, function: str, args: Any = None, timeout_second
|
|
|
1663
1671
|
poll_interval = 1.0
|
|
1664
1672
|
elapsed = 0.0
|
|
1665
1673
|
while elapsed < timeout_seconds:
|
|
1666
|
-
|
|
1674
|
+
try:
|
|
1675
|
+
await _asyncio.sleep(poll_interval)
|
|
1676
|
+
except _asyncio.CancelledError:
|
|
1677
|
+
log.debug("[meshcode] meshcode_call poll cancelled by ESC")
|
|
1678
|
+
return {"error": "cancelled_by_client", "call_id": call_id, "ok": False}
|
|
1667
1679
|
elapsed += poll_interval
|
|
1668
1680
|
|
|
1669
1681
|
# Check Realtime buffer first
|
|
@@ -1896,7 +1908,13 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
|
|
|
1896
1908
|
# Keep polling until something actionable arrives.
|
|
1897
1909
|
# The agent (LLM) is NOT called between iterations — zero token cost.
|
|
1898
1910
|
while True:
|
|
1899
|
-
|
|
1911
|
+
try:
|
|
1912
|
+
result = await _meshcode_wait_inner(actual_timeout=capped_timeout, include_acks=include_acks)
|
|
1913
|
+
except asyncio.CancelledError:
|
|
1914
|
+
# Safety net: if CancelledError escapes _meshcode_wait_inner
|
|
1915
|
+
# despite the inner shield, catch it here to prevent cascade.
|
|
1916
|
+
log.debug("[meshcode] meshcode_wait outer loop caught CancelledError")
|
|
1917
|
+
result = {"timed_out": True, "reason": "cancelled_by_client"}
|
|
1900
1918
|
|
|
1901
1919
|
if result.get("got_message"):
|
|
1902
1920
|
# Real message arrived — return to agent for processing
|
|
@@ -2039,7 +2057,20 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
2039
2057
|
|
|
2040
2058
|
if _rt_live:
|
|
2041
2059
|
# 2a) Real async wait — zero CPU, zero Supabase calls.
|
|
2042
|
-
|
|
2060
|
+
# Shield from CancelledError: when Claude Code presses ESC, it cancels
|
|
2061
|
+
# the in-flight asyncio task. CancelledError is a BaseException that
|
|
2062
|
+
# bypasses most try/except, cascades through FastMCP's tool handler,
|
|
2063
|
+
# and unwinds the event loop — killing the MCP server. By shielding
|
|
2064
|
+
# the await and catching CancelledError, we return a clean timeout
|
|
2065
|
+
# result instead. The mcp.run() retry loop restarts the event loop
|
|
2066
|
+
# without the lifespan cascade.
|
|
2067
|
+
try:
|
|
2068
|
+
woke = await asyncio.shield(
|
|
2069
|
+
_REALTIME.wait_for_message(timeout=float(actual_timeout))
|
|
2070
|
+
)
|
|
2071
|
+
except (asyncio.CancelledError, Exception) as _cancel_exc:
|
|
2072
|
+
log.debug(f"[meshcode] wait_for_message cancelled/failed: {type(_cancel_exc).__name__}")
|
|
2073
|
+
return {"timed_out": True, "reason": "cancelled_by_client"}
|
|
2043
2074
|
if woke:
|
|
2044
2075
|
buffered = _REALTIME.drain()
|
|
2045
2076
|
if buffered:
|
|
@@ -2052,7 +2083,11 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
2052
2083
|
_poll_interval = 5
|
|
2053
2084
|
_elapsed = 0
|
|
2054
2085
|
while _elapsed < actual_timeout:
|
|
2055
|
-
|
|
2086
|
+
try:
|
|
2087
|
+
await asyncio.sleep(min(_poll_interval, actual_timeout - _elapsed))
|
|
2088
|
+
except asyncio.CancelledError:
|
|
2089
|
+
log.debug("[meshcode] DB poll sleep cancelled by ESC")
|
|
2090
|
+
return {"timed_out": True, "reason": "cancelled_by_client"}
|
|
2056
2091
|
_elapsed += _poll_interval
|
|
2057
2092
|
try:
|
|
2058
2093
|
api_key = _get_api_key()
|
|
@@ -351,6 +351,118 @@ def _list_local_projects_for_agent(agent: str) -> list:
|
|
|
351
351
|
return out
|
|
352
352
|
|
|
353
353
|
|
|
354
|
+
STABLE_CLAUDE_VERSION = "2.1.104" # Known-good version: ESC does NOT kill MCP
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _get_claude_version(editor_cmd: str) -> Optional[str]:
|
|
358
|
+
"""Get the installed Claude Code version. Returns None on failure."""
|
|
359
|
+
try:
|
|
360
|
+
use_shell = sys.platform == "win32" and editor_cmd.lower().endswith((".cmd", ".bat"))
|
|
361
|
+
r = subprocess.run(
|
|
362
|
+
[editor_cmd, "--version"],
|
|
363
|
+
capture_output=True, text=True, timeout=10,
|
|
364
|
+
shell=use_shell,
|
|
365
|
+
)
|
|
366
|
+
# Output is typically just the version string, e.g. "2.1.104"
|
|
367
|
+
version = r.stdout.strip().split('\n')[0].strip()
|
|
368
|
+
if version:
|
|
369
|
+
return version
|
|
370
|
+
except Exception:
|
|
371
|
+
pass
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _fetch_global_claude_version() -> Optional[str]:
|
|
376
|
+
"""Fetch the admin-pinned Claude Code version from mc_global_config.
|
|
377
|
+
|
|
378
|
+
Uses the user's API key to authenticate the RPC call. Returns None
|
|
379
|
+
on any failure (no network, no key, etc.) — caller falls back to
|
|
380
|
+
local preferences or installed version.
|
|
381
|
+
"""
|
|
382
|
+
try:
|
|
383
|
+
from .secrets import get_api_key
|
|
384
|
+
api_key = get_api_key()
|
|
385
|
+
if not api_key:
|
|
386
|
+
return None
|
|
387
|
+
from .setup_clients import _load_supabase_env
|
|
388
|
+
env = _load_supabase_env()
|
|
389
|
+
url = env.get("SUPABASE_URL", "")
|
|
390
|
+
anon_key = env.get("SUPABASE_KEY", "")
|
|
391
|
+
if not url or not anon_key:
|
|
392
|
+
return None
|
|
393
|
+
from urllib.request import Request, urlopen
|
|
394
|
+
import json as _json
|
|
395
|
+
req = Request(
|
|
396
|
+
f"{url}/rest/v1/rpc/mc_get_global_config_by_key",
|
|
397
|
+
data=_json.dumps({"p_api_key": api_key, "p_config_key": "claude_version"}).encode(),
|
|
398
|
+
headers={
|
|
399
|
+
"Content-Type": "application/json",
|
|
400
|
+
"apikey": anon_key,
|
|
401
|
+
"Authorization": f"Bearer {anon_key}",
|
|
402
|
+
},
|
|
403
|
+
)
|
|
404
|
+
with urlopen(req, timeout=5) as resp:
|
|
405
|
+
raw = _json.loads(resp.read())
|
|
406
|
+
# RPC returns the JSONB value directly — could be a string or object
|
|
407
|
+
if isinstance(raw, str) and raw not in ("latest", ""):
|
|
408
|
+
return raw
|
|
409
|
+
except Exception:
|
|
410
|
+
pass
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _resolve_pinned_claude(editor_cmd: str) -> str:
|
|
415
|
+
"""Check if a specific Claude Code version is pinned and resolve the command.
|
|
416
|
+
|
|
417
|
+
Version pin sources (first wins):
|
|
418
|
+
1. MESHCODE_CLAUDE_VERSION env var (per-launch override)
|
|
419
|
+
2. mc_global_config 'claude_version' from DB (admin-controlled)
|
|
420
|
+
3. claude_version in ~/.meshcode/preferences.json (local fallback)
|
|
421
|
+
4. No pin — use whatever is installed
|
|
422
|
+
|
|
423
|
+
If a pin is set and the installed version doesn't match, uses npx to
|
|
424
|
+
run the exact pinned version.
|
|
425
|
+
"""
|
|
426
|
+
from .preferences import load_prefs
|
|
427
|
+
|
|
428
|
+
pinned = os.environ.get("MESHCODE_CLAUDE_VERSION", "").strip()
|
|
429
|
+
if not pinned:
|
|
430
|
+
# Check global admin config from DB
|
|
431
|
+
global_ver = _fetch_global_claude_version()
|
|
432
|
+
if global_ver:
|
|
433
|
+
pinned = global_ver
|
|
434
|
+
print(f"[meshcode] Global admin pin: Claude Code v{pinned}", file=sys.stderr)
|
|
435
|
+
if not pinned:
|
|
436
|
+
pinned = load_prefs().get("claude_version", "")
|
|
437
|
+
|
|
438
|
+
# "latest" or "none" means skip pinning — use whatever is installed
|
|
439
|
+
if pinned and pinned.lower() in ("latest", "none", "skip"):
|
|
440
|
+
print(f"[meshcode] Version pin disabled ({pinned}) — using installed version", file=sys.stderr)
|
|
441
|
+
return editor_cmd
|
|
442
|
+
|
|
443
|
+
if not pinned:
|
|
444
|
+
# Default pin: v2.1.104 is the last Claude Code version where ESC
|
|
445
|
+
# doesn't kill the MCP server. Newer versions send CancelledError
|
|
446
|
+
# that cascades through the event loop. This default is overridden
|
|
447
|
+
# by any of the 3 pin sources above (set to "latest" to disable).
|
|
448
|
+
# Remove this default once Anthropic fixes the ESC bug upstream.
|
|
449
|
+
pinned = "2.1.104"
|
|
450
|
+
print(f"[meshcode] Default ESC-safe pin: Claude Code v{pinned}", file=sys.stderr)
|
|
451
|
+
|
|
452
|
+
current = _get_claude_version(editor_cmd)
|
|
453
|
+
if current == pinned:
|
|
454
|
+
print(f"[meshcode] Claude Code version {current} matches pin ✓", file=sys.stderr)
|
|
455
|
+
return editor_cmd
|
|
456
|
+
|
|
457
|
+
print(f"[meshcode] Version pin: {pinned} (installed: {current or 'unknown'})", file=sys.stderr)
|
|
458
|
+
print(f"[meshcode] Using npx to run pinned version...", file=sys.stderr)
|
|
459
|
+
|
|
460
|
+
# Use npx to run the exact pinned version. npx will download it if needed.
|
|
461
|
+
# We set an env var so the launch path knows to prepend npx args.
|
|
462
|
+
os.environ["_MESHCODE_NPX_CLAUDE_VERSION"] = pinned
|
|
463
|
+
return editor_cmd # Still return the editor — launch path handles npx
|
|
464
|
+
|
|
465
|
+
|
|
354
466
|
def _claude_supports_bypass(editor_cmd: str) -> bool:
|
|
355
467
|
"""Check if the Claude Code binary supports --dangerously-skip-permissions.
|
|
356
468
|
|
|
@@ -663,10 +775,14 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
663
775
|
print(f"[meshcode] Closing the editor will flip this agent offline.")
|
|
664
776
|
print()
|
|
665
777
|
|
|
778
|
+
# On Windows, editor is the fully-resolved path (e.g. 'c:\...\claude.cmd')
|
|
779
|
+
# so we compare against the stem (filename without extension).
|
|
780
|
+
editor_stem = Path(editor).stem.lower() if sys.platform == "win32" else editor
|
|
781
|
+
|
|
666
782
|
# Set editor type so MCP server can report it to the dashboard
|
|
667
|
-
os.environ["MESHCODE_EDITOR_TYPE"] =
|
|
783
|
+
os.environ["MESHCODE_EDITOR_TYPE"] = editor_stem
|
|
668
784
|
|
|
669
|
-
if
|
|
785
|
+
if editor_stem == "claude":
|
|
670
786
|
# Claude Code: run from inside the workspace and let Claude Code
|
|
671
787
|
# auto-discover .mcp.json as a PROJECT-scoped MCP. We used to pass
|
|
672
788
|
# --mcp-config + --strict-mcp-config here, but that categorises the
|
|
@@ -676,12 +792,24 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
676
792
|
# scoped MCPs (loaded from ./.mcp.json at cwd) live in the "Local
|
|
677
793
|
# MCP" bucket and survive ESC. Verified with a minimal FastMCP
|
|
678
794
|
# repro side-by-side: Built-in dies, Local survives, same SDK code.
|
|
795
|
+
# Check for version pin before building launch command
|
|
796
|
+
effective_editor = _resolve_pinned_claude(editor)
|
|
797
|
+
pinned_version = os.environ.get("_MESHCODE_NPX_CLAUDE_VERSION", "")
|
|
798
|
+
|
|
679
799
|
mode = resolve_permission_mode(permission_override)
|
|
680
800
|
print(f"[meshcode] Editor resolved: {editor}", file=sys.stderr)
|
|
681
801
|
print(f"[meshcode] Permission mode resolved: {mode}", file=sys.stderr)
|
|
682
|
-
|
|
802
|
+
|
|
803
|
+
if pinned_version:
|
|
804
|
+
# Use npx to run the exact pinned version
|
|
805
|
+
cmd = ["npx", f"@anthropic-ai/claude-code@{pinned_version}"]
|
|
806
|
+
print(f"[meshcode] Using pinned version via npx: {pinned_version}")
|
|
807
|
+
else:
|
|
808
|
+
cmd = [effective_editor]
|
|
809
|
+
|
|
683
810
|
if mode == "bypass":
|
|
684
|
-
if
|
|
811
|
+
bypass_check_cmd = effective_editor if not pinned_version else "claude"
|
|
812
|
+
if pinned_version or _claude_supports_bypass(bypass_check_cmd):
|
|
685
813
|
cmd.append("--dangerously-skip-permissions")
|
|
686
814
|
print(f"[meshcode] Permission mode: bypass (autonomous loop, no prompts)")
|
|
687
815
|
else:
|
|
@@ -702,17 +830,17 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
702
830
|
os.chdir(ws)
|
|
703
831
|
except Exception as e:
|
|
704
832
|
print(f"[meshcode] WARNING: chdir to workspace failed: {e}", file=sys.stderr)
|
|
705
|
-
elif
|
|
833
|
+
elif editor_stem == "cursor":
|
|
706
834
|
# Cursor reads .cursor/mcp.json from the workspace cwd.
|
|
707
835
|
cmd = [editor, str(ws)]
|
|
708
|
-
elif
|
|
836
|
+
elif editor_stem == "code":
|
|
709
837
|
# VS Code (Cline reads .vscode/mcp.json from workspace cwd).
|
|
710
838
|
cmd = [editor, str(ws)]
|
|
711
|
-
elif
|
|
839
|
+
elif editor_stem == "windsurf":
|
|
712
840
|
# Windsurf (Codeium fork of VS Code) — reads workspace cwd the same
|
|
713
841
|
# way VS Code does; global MCP config at ~/.codeium/windsurf/mcp_config.json.
|
|
714
842
|
cmd = [editor, str(ws)]
|
|
715
|
-
elif
|
|
843
|
+
elif editor_stem == "codex":
|
|
716
844
|
# Codex CLI reads ~/.codex/config.toml globally. There's no per-
|
|
717
845
|
# workspace flag, so launching is just `codex` with cwd set to the
|
|
718
846
|
# workspace dir so any file ops the agent does land there.
|
|
@@ -769,7 +897,14 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
769
897
|
# workspace even if os.chdir() didn't stick (shell=True can
|
|
770
898
|
# reset cwd via cmd.exe).
|
|
771
899
|
import subprocess as _sp
|
|
772
|
-
|
|
900
|
+
# .cmd/.bat files need shell=True. Also check if the resolved
|
|
901
|
+
# path of cmd[0] is a .cmd (e.g., "npx" resolves to "npx.cmd").
|
|
902
|
+
cmd0 = cmd[0].lower()
|
|
903
|
+
use_shell = cmd0.endswith((".cmd", ".bat"))
|
|
904
|
+
if not use_shell:
|
|
905
|
+
resolved_cmd0 = shutil.which(cmd[0])
|
|
906
|
+
if resolved_cmd0 and resolved_cmd0.lower().endswith((".cmd", ".bat")):
|
|
907
|
+
use_shell = True
|
|
773
908
|
if use_shell:
|
|
774
909
|
print(f"[meshcode] (Windows: launching via shell for .cmd wrapper)")
|
|
775
910
|
launch_cwd = str(ws) if os.path.isdir(ws) else None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|