meshcode 2.10.43__tar.gz → 2.10.45__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.45}/PKG-INFO +1 -1
  2. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/__init__.py +1 -1
  3. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/comms_v4.py +37 -9
  4. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/meshcode_mcp/server.py +10 -8
  5. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/run_agent.py +132 -9
  6. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode.egg-info/PKG-INFO +1 -1
  7. {meshcode-2.10.43 → meshcode-2.10.45}/pyproject.toml +1 -1
  8. {meshcode-2.10.43 → meshcode-2.10.45}/README.md +0 -0
  9. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/ascii_art.py +0 -0
  10. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/cli.py +0 -0
  11. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/invites.py +0 -0
  12. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/launcher.py +0 -0
  13. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/launcher_install.py +0 -0
  14. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/meshcode_mcp/__init__.py +0 -0
  15. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/meshcode_mcp/__main__.py +0 -0
  16. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/meshcode_mcp/backend.py +0 -0
  17. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/meshcode_mcp/realtime.py +0 -0
  18. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/meshcode_mcp/test_backend.py +0 -0
  19. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  20. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  21. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/preferences.py +0 -0
  22. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/protocol_v2.py +0 -0
  23. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/secrets.py +0 -0
  24. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/self_update.py +0 -0
  25. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/setup_clients.py +0 -0
  26. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode.egg-info/SOURCES.txt +0 -0
  27. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode.egg-info/requires.txt +0 -0
  30. {meshcode-2.10.43 → meshcode-2.10.45}/meshcode.egg-info/top_level.txt +0 -0
  31. {meshcode-2.10.43 → meshcode-2.10.45}/setup.cfg +0 -0
  32. {meshcode-2.10.43 → meshcode-2.10.45}/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.45
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.45"
@@ -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)
@@ -351,6 +351,106 @@ 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
+ if not pinned:
438
+ return editor_cmd # No pin — use installed version
439
+
440
+ current = _get_claude_version(editor_cmd)
441
+ if current == pinned:
442
+ print(f"[meshcode] Claude Code version {current} matches pin ✓", file=sys.stderr)
443
+ return editor_cmd
444
+
445
+ print(f"[meshcode] Version pin: {pinned} (installed: {current or 'unknown'})", file=sys.stderr)
446
+ print(f"[meshcode] Using npx to run pinned version...", file=sys.stderr)
447
+
448
+ # Use npx to run the exact pinned version. npx will download it if needed.
449
+ # We set an env var so the launch path knows to prepend npx args.
450
+ os.environ["_MESHCODE_NPX_CLAUDE_VERSION"] = pinned
451
+ return editor_cmd # Still return the editor — launch path handles npx
452
+
453
+
354
454
  def _claude_supports_bypass(editor_cmd: str) -> bool:
355
455
  """Check if the Claude Code binary supports --dangerously-skip-permissions.
356
456
 
@@ -663,10 +763,14 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
663
763
  print(f"[meshcode] Closing the editor will flip this agent offline.")
664
764
  print()
665
765
 
766
+ # On Windows, editor is the fully-resolved path (e.g. 'c:\...\claude.cmd')
767
+ # so we compare against the stem (filename without extension).
768
+ editor_stem = Path(editor).stem.lower() if sys.platform == "win32" else editor
769
+
666
770
  # Set editor type so MCP server can report it to the dashboard
667
- os.environ["MESHCODE_EDITOR_TYPE"] = editor
771
+ os.environ["MESHCODE_EDITOR_TYPE"] = editor_stem
668
772
 
669
- if editor == "claude":
773
+ if editor_stem == "claude":
670
774
  # Claude Code: run from inside the workspace and let Claude Code
671
775
  # auto-discover .mcp.json as a PROJECT-scoped MCP. We used to pass
672
776
  # --mcp-config + --strict-mcp-config here, but that categorises the
@@ -676,12 +780,24 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
676
780
  # scoped MCPs (loaded from ./.mcp.json at cwd) live in the "Local
677
781
  # MCP" bucket and survive ESC. Verified with a minimal FastMCP
678
782
  # repro side-by-side: Built-in dies, Local survives, same SDK code.
783
+ # Check for version pin before building launch command
784
+ effective_editor = _resolve_pinned_claude(editor)
785
+ pinned_version = os.environ.get("_MESHCODE_NPX_CLAUDE_VERSION", "")
786
+
679
787
  mode = resolve_permission_mode(permission_override)
680
788
  print(f"[meshcode] Editor resolved: {editor}", file=sys.stderr)
681
789
  print(f"[meshcode] Permission mode resolved: {mode}", file=sys.stderr)
682
- cmd = [editor]
790
+
791
+ if pinned_version:
792
+ # Use npx to run the exact pinned version
793
+ cmd = ["npx", f"@anthropic-ai/claude-code@{pinned_version}"]
794
+ print(f"[meshcode] Using pinned version via npx: {pinned_version}")
795
+ else:
796
+ cmd = [effective_editor]
797
+
683
798
  if mode == "bypass":
684
- if _claude_supports_bypass(editor):
799
+ bypass_check_cmd = effective_editor if not pinned_version else "claude"
800
+ if pinned_version or _claude_supports_bypass(bypass_check_cmd):
685
801
  cmd.append("--dangerously-skip-permissions")
686
802
  print(f"[meshcode] Permission mode: bypass (autonomous loop, no prompts)")
687
803
  else:
@@ -702,17 +818,17 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
702
818
  os.chdir(ws)
703
819
  except Exception as e:
704
820
  print(f"[meshcode] WARNING: chdir to workspace failed: {e}", file=sys.stderr)
705
- elif editor == "cursor":
821
+ elif editor_stem == "cursor":
706
822
  # Cursor reads .cursor/mcp.json from the workspace cwd.
707
823
  cmd = [editor, str(ws)]
708
- elif editor == "code":
824
+ elif editor_stem == "code":
709
825
  # VS Code (Cline reads .vscode/mcp.json from workspace cwd).
710
826
  cmd = [editor, str(ws)]
711
- elif editor == "windsurf":
827
+ elif editor_stem == "windsurf":
712
828
  # Windsurf (Codeium fork of VS Code) — reads workspace cwd the same
713
829
  # way VS Code does; global MCP config at ~/.codeium/windsurf/mcp_config.json.
714
830
  cmd = [editor, str(ws)]
715
- elif editor == "codex":
831
+ elif editor_stem == "codex":
716
832
  # Codex CLI reads ~/.codex/config.toml globally. There's no per-
717
833
  # workspace flag, so launching is just `codex` with cwd set to the
718
834
  # workspace dir so any file ops the agent does land there.
@@ -769,7 +885,14 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
769
885
  # workspace even if os.chdir() didn't stick (shell=True can
770
886
  # reset cwd via cmd.exe).
771
887
  import subprocess as _sp
772
- use_shell = cmd[0].lower().endswith((".cmd", ".bat"))
888
+ # .cmd/.bat files need shell=True. Also check if the resolved
889
+ # path of cmd[0] is a .cmd (e.g., "npx" resolves to "npx.cmd").
890
+ cmd0 = cmd[0].lower()
891
+ use_shell = cmd0.endswith((".cmd", ".bat"))
892
+ if not use_shell:
893
+ resolved_cmd0 = shutil.which(cmd[0])
894
+ if resolved_cmd0 and resolved_cmd0.lower().endswith((".cmd", ".bat")):
895
+ use_shell = True
773
896
  if use_shell:
774
897
  print(f"[meshcode] (Windows: launching via shell for .cmd wrapper)")
775
898
  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.45
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.45"
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