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.
- {meshcode-2.10.43 → meshcode-2.10.45}/PKG-INFO +1 -1
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/__init__.py +1 -1
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/comms_v4.py +37 -9
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/meshcode_mcp/server.py +10 -8
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/run_agent.py +132 -9
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.43 → meshcode-2.10.45}/pyproject.toml +1 -1
- {meshcode-2.10.43 → meshcode-2.10.45}/README.md +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/cli.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/invites.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/launcher.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/preferences.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/secrets.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/self_update.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/setup.cfg +0 -0
- {meshcode-2.10.43 → meshcode-2.10.45}/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.45"
|
|
@@ -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)
|
|
@@ -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"] =
|
|
771
|
+
os.environ["MESHCODE_EDITOR_TYPE"] = editor_stem
|
|
668
772
|
|
|
669
|
-
if
|
|
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
|
-
|
|
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
|
|
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
|
|
821
|
+
elif editor_stem == "cursor":
|
|
706
822
|
# Cursor reads .cursor/mcp.json from the workspace cwd.
|
|
707
823
|
cmd = [editor, str(ws)]
|
|
708
|
-
elif
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|