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.
Files changed (32) hide show
  1. {meshcode-2.10.43 → meshcode-2.10.46}/PKG-INFO +1 -1
  2. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/__init__.py +1 -1
  3. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/comms_v4.py +37 -9
  4. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/meshcode_mcp/server.py +47 -12
  5. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/run_agent.py +144 -9
  6. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode.egg-info/PKG-INFO +1 -1
  7. {meshcode-2.10.43 → meshcode-2.10.46}/pyproject.toml +1 -1
  8. {meshcode-2.10.43 → meshcode-2.10.46}/README.md +0 -0
  9. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/ascii_art.py +0 -0
  10. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/cli.py +0 -0
  11. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/invites.py +0 -0
  12. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/launcher.py +0 -0
  13. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/launcher_install.py +0 -0
  14. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/meshcode_mcp/__init__.py +0 -0
  15. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/meshcode_mcp/__main__.py +0 -0
  16. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/meshcode_mcp/backend.py +0 -0
  17. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/meshcode_mcp/realtime.py +0 -0
  18. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/meshcode_mcp/test_backend.py +0 -0
  19. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  20. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  21. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/preferences.py +0 -0
  22. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/protocol_v2.py +0 -0
  23. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/secrets.py +0 -0
  24. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/self_update.py +0 -0
  25. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode/setup_clients.py +0 -0
  26. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode.egg-info/SOURCES.txt +0 -0
  27. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode.egg-info/requires.txt +0 -0
  30. {meshcode-2.10.43 → meshcode-2.10.46}/meshcode.egg-info/top_level.txt +0 -0
  31. {meshcode-2.10.43 → meshcode-2.10.46}/setup.cfg +0 -0
  32. {meshcode-2.10.43 → meshcode-2.10.46}/tests/test_status_enum_coverage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.43
3
+ Version: 2.10.46
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,2 +1,2 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.10.43"
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
- """Strip characters that could escape shell/PS quoting contexts."""
319
- # Remove quotes, backticks, dollar signs, semicolons, pipes, and backslashes
320
- # to prevent injection in osascript, PowerShell, and notify-send.
321
- return "".join(c for c in text if c not in '"\'`$;|\\<>(){}')
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: use single-quoted strings (no variable expansion)
336
- # and pass title/body via -replace to avoid any interpolation
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
- f"$text[0].AppendChild($xml.CreateTextNode('{title}')); "
342
- f"$text[1].AppendChild($xml.CreateTextNode('{body}')); "
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
- # Use xdotool-style approach: pass nudge as argument, not interpolated script
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
- # Sanitize for SendKeys: strip braces and special SendKeys metacharacters
195
- sk_safe = nudge.replace("{", "").replace("}", "").replace("+", "").replace("^", "").replace("%", "").replace("~", "")
196
- ps_script = f'''
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("{sk_safe}{{ENTER}}")
200
+ [System.Windows.Forms.SendKeys]::SendWait($env:MC_NUDGE_TEXT + "{ENTER}")
199
201
  '''
200
- # Use subprocess.run with timeout to prevent orphaned PowerShell processes
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
- await _asyncio.sleep(poll_interval)
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
- result = await _meshcode_wait_inner(actual_timeout=capped_timeout, include_acks=include_acks)
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
- woke = await _REALTIME.wait_for_message(timeout=float(actual_timeout))
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
- await asyncio.sleep(min(_poll_interval, actual_timeout - _elapsed))
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"] = editor
783
+ os.environ["MESHCODE_EDITOR_TYPE"] = editor_stem
668
784
 
669
- if editor == "claude":
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
- cmd = [editor]
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 _claude_supports_bypass(editor):
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 editor == "cursor":
833
+ elif editor_stem == "cursor":
706
834
  # Cursor reads .cursor/mcp.json from the workspace cwd.
707
835
  cmd = [editor, str(ws)]
708
- elif editor == "code":
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 editor == "windsurf":
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 editor == "codex":
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
- use_shell = cmd[0].lower().endswith((".cmd", ".bat"))
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.43
3
+ Version: 2.10.46
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.10.43"
7
+ version = "2.10.46"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes