meshcode 2.11.149__tar.gz → 2.11.152__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 (107) hide show
  1. {meshcode-2.11.149 → meshcode-2.11.152}/PKG-INFO +11 -2
  2. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/comms_v4.py +41 -3
  4. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/doctor.py +101 -1
  5. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/hostd.py +169 -1
  6. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/run_agent.py +24 -1
  7. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode.egg-info/PKG-INFO +11 -2
  8. {meshcode-2.11.149 → meshcode-2.11.152}/pyproject.toml +1 -1
  9. {meshcode-2.11.149 → meshcode-2.11.152}/README.md +0 -0
  10. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/__main__.py +0 -0
  11. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/_session_handoff_template.py +0 -0
  12. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/_stop_hook_template.py +0 -0
  13. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/ascii_art.py +0 -0
  14. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/atomic_push.py +0 -0
  15. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/claude_update.py +0 -0
  16. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/cli.py +0 -0
  17. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/compat.py +0 -0
  18. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/daemon.py +0 -0
  19. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/date_parse.py +0 -0
  20. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/error_hints.py +0 -0
  21. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/exceptions.py +0 -0
  22. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/helper_visuals.py +0 -0
  23. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/hooks/__init__.py +0 -0
  24. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/hooks/repo_path_lock.py +0 -0
  25. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/invites.py +0 -0
  26. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/launcher.py +0 -0
  27. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/launcher_install.py +0 -0
  28. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/__init__.py +0 -0
  29. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/__main__.py +0 -0
  30. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/backend.py +0 -0
  31. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/realtime.py +0 -0
  32. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/server.py +0 -0
  33. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  34. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/swarm.py +0 -0
  35. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/test_backend.py +0 -0
  36. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  37. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  38. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  39. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  40. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  41. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/test_swarm.py +0 -0
  42. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/preferences.py +0 -0
  43. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/protocol_handler.py +0 -0
  44. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/protocol_v2.py +0 -0
  45. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/quickstart.py +0 -0
  46. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/rpc_allowlist.py +0 -0
  47. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/scripts/check_secrets.py +0 -0
  48. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/scripts/race_rate_harness.py +0 -0
  49. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/secrets.py +0 -0
  50. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/self_update.py +0 -0
  51. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/setup_clients.py +0 -0
  52. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/supervisor.py +0 -0
  53. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/up.py +0 -0
  54. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/upload.py +0 -0
  55. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode.egg-info/SOURCES.txt +0 -0
  56. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode.egg-info/dependency_links.txt +0 -0
  57. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode.egg-info/entry_points.txt +0 -0
  58. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode.egg-info/requires.txt +0 -0
  59. {meshcode-2.11.149 → meshcode-2.11.152}/meshcode.egg-info/top_level.txt +0 -0
  60. {meshcode-2.11.149 → meshcode-2.11.152}/setup.cfg +0 -0
  61. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_auto_update_hardening.py +0 -0
  62. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_autonomous_closegap_1.py +0 -0
  63. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_autonomous_closegap_2.py +0 -0
  64. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_autonomous_closegap_3.py +0 -0
  65. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_autonomous_prompt_inject.py +0 -0
  66. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_boot_bug_regression.py +0 -0
  67. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_color_truecolor.py +0 -0
  68. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_core.py +0 -0
  69. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_cross_agent_messaging.py +0 -0
  70. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_date_parse.py +0 -0
  71. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_doctor.py +0 -0
  72. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_epistemic_v1_python_sdk.py +0 -0
  73. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  74. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_esc_deaf_state.py +0 -0
  75. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_exceptions.py +0 -0
  76. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_file_upload.py +0 -0
  77. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_helper_visuals.py +0 -0
  78. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_hostd_launch_pinned_env.py +0 -0
  79. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_hostd_serve_discovery_split.py +0 -0
  80. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_hostd_zombie_sessions.py +0 -0
  81. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_init_device_code.py +0 -0
  82. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_install_guard.py +0 -0
  83. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_lease_sigterm_release.py +0 -0
  84. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_live_mesh_guard.py +0 -0
  85. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_mark_read_batch.py +0 -0
  86. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_marketplace_ratings.py +0 -0
  87. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_migration_integrity.py +0 -0
  88. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_pretrust_claude.py +0 -0
  89. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_realtime_event_freshness.py +0 -0
  90. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_rls_cross_tenant.py +0 -0
  91. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_rpc_grants.py +0 -0
  92. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_rpc_migrations.py +0 -0
  93. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_run_agent_dry_run.py +0 -0
  94. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_run_agent_no_server_import.py +0 -0
  95. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_security_regressions.py +0 -0
  96. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_self_update_user_site.py +0 -0
  97. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_sentinel.py +0 -0
  98. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_session_replay_gate.py +0 -0
  99. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_setup_path.py +0 -0
  100. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_sleep_signals.py +0 -0
  101. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_status_enum_coverage.py +0 -0
  102. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_stay_on_loop_hook.py +0 -0
  103. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_stop_ghost_terminal.py +0 -0
  104. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_swarm_events.py +0 -0
  105. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_task_progress.py +0 -0
  106. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_terminal_lifecycle.py +0 -0
  107. {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_wait_open_tasks_contradiction.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.149
3
+ Version: 2.11.152
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -18,8 +18,17 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Operating System :: OS Independent
19
19
  Requires-Python: >=3.9
20
20
  Description-Content-Type: text/markdown
21
+ Requires-Dist: mcp[cli]>=1.0.0
22
+ Requires-Dist: websockets>=12.0
23
+ Requires-Dist: realtime>=2.0.0
24
+ Requires-Dist: keyring>=24.0
25
+ Requires-Dist: cryptography>=41.0
21
26
  Provides-Extra: test
27
+ Requires-Dist: pytest>=8; extra == "test"
22
28
  Provides-Extra: dev
29
+ Requires-Dist: build>=1.0; extra == "dev"
30
+ Requires-Dist: twine>=4; extra == "dev"
31
+ Requires-Dist: pytest>=8; extra == "dev"
23
32
 
24
33
  # MeshCode
25
34
 
@@ -1,5 +1,5 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.11.149"
2
+ __version__ = "2.11.152"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -319,8 +319,28 @@ def get_project_id(project_name):
319
319
  if rows:
320
320
  return rows[0]["id"]
321
321
 
322
- # Last resort: try to create. Will fail under RLS for unauthenticated
323
- # contexts, but kept for legacy callers + admin tooling.
322
+ # ZERO-FRICTION onboarding (R3): the project doesn't exist yet create it
323
+ # for the authenticated user via the same RPC the MCP tool uses, then
324
+ # re-resolve to get its id. Makes `meshcode go <agent> --project <new>`
325
+ # just work right after `meshcode init` (no dashboard round-trip).
326
+ if api_key:
327
+ try:
328
+ cr = sb_rpc("mc_create_meshwork_by_api_key", {
329
+ "p_api_key": api_key, "p_name": project_name,
330
+ })
331
+ if isinstance(cr, dict) and (cr.get("ok") or cr.get("project_id")):
332
+ r2 = sb_rpc("mc_resolve_project", {
333
+ "p_api_key": api_key, "p_project_name": project_name,
334
+ })
335
+ if isinstance(r2, dict) and r2.get("project_id"):
336
+ return r2["project_id"]
337
+ if isinstance(cr.get("project_id"), str):
338
+ return cr["project_id"]
339
+ except Exception:
340
+ pass
341
+
342
+ # Last resort: legacy anon insert (fails under RLS for owned projects;
343
+ # kept for very old test scripts / admin tooling).
324
344
  result = sb_insert("mc_projects", {"name": project_name})
325
345
  if result and len(result) > 0:
326
346
  return result[0]["id"]
@@ -2426,6 +2446,14 @@ def login(api_key):
2426
2446
  except Exception:
2427
2447
  pass
2428
2448
 
2449
+ # ZERO-FRICTION onboarding (R1): auto-install Claude Code so `pip install
2450
+ # meshcode` + `meshcode login` is enough — no separate manual editor install.
2451
+ try:
2452
+ from meshcode.doctor import ensure_claude_installed
2453
+ ensure_claude_installed(verbose=True)
2454
+ except Exception:
2455
+ pass
2456
+
2429
2457
  return True
2430
2458
 
2431
2459
 
@@ -2822,7 +2850,8 @@ if __name__ == "__main__":
2822
2850
  if not _has_creds:
2823
2851
  print()
2824
2852
  print(" Welcome to MeshCode!")
2825
- print(" First time? Run: meshcode quickstart")
2853
+ print(" First time? Run: meshcode init")
2854
+ print(" ↳ opens your browser to approve — no API key to copy/paste")
2826
2855
  print(" Already have an API key? Run: meshcode login <api_key>")
2827
2856
  print(" Need help? Run: meshcode doctor")
2828
2857
  print()
@@ -2914,6 +2943,15 @@ if __name__ == "__main__":
2914
2943
  print(" " + "=" * 40)
2915
2944
  print()
2916
2945
 
2946
+ # ZERO-FRICTION onboarding (R1): auto-install Claude Code BEFORE the
2947
+ # pre-flight so a brand-new machine (no claude) self-heals and PASSES
2948
+ # doctor instead of hard-blocking onboarding.
2949
+ try:
2950
+ from meshcode.doctor import ensure_claude_installed
2951
+ ensure_claude_installed(verbose=True)
2952
+ except Exception:
2953
+ pass
2954
+
2917
2955
  # B2 pre-flight: refuse to onboard if env is broken
2918
2956
  from meshcode.doctor import run_doctor
2919
2957
  if run_doctor(skip_api_key_check=True) != 0:
@@ -17,7 +17,7 @@ import shutil
17
17
  import subprocess
18
18
  import urllib.request
19
19
  import urllib.error
20
- from typing import List, Tuple
20
+ from typing import List, Optional, Tuple
21
21
 
22
22
  Status = Tuple[str, str, str]
23
23
 
@@ -97,6 +97,106 @@ def _check_claude() -> Status:
97
97
  )
98
98
 
99
99
 
100
+ def _resolve_claude() -> Optional[str]:
101
+ """Return a path to the `claude` binary — via PATH, or common global-bin
102
+ locations a fresh `npm i -g` / claude.ai install lands in but that aren't
103
+ on this process's PATH yet (qa cold-start finding: macOS/Linux had no
104
+ fallback, only Windows did). None if not found."""
105
+ p = shutil.which("claude")
106
+ if p:
107
+ return p
108
+ if sys.platform == "win32":
109
+ appdata = os.environ.get("APPDATA", "")
110
+ local = os.environ.get("LOCALAPPDATA", "")
111
+ cands = [
112
+ os.path.join(appdata, "npm", "claude.cmd"),
113
+ os.path.join(appdata, "npm", "claude"),
114
+ os.path.join(local, "Programs", "claude", "claude.exe"),
115
+ ]
116
+ else:
117
+ home = os.path.expanduser("~")
118
+ cands = [
119
+ os.path.join(home, ".local", "bin", "claude"),
120
+ "/opt/homebrew/bin/claude",
121
+ "/usr/local/bin/claude",
122
+ os.path.join(home, ".npm-global", "bin", "claude"),
123
+ ]
124
+ for c in cands:
125
+ if c and os.path.isfile(c):
126
+ return c
127
+ # Ask npm where global packages live and check its bin dir.
128
+ try:
129
+ if sys.platform == "win32":
130
+ base = os.path.dirname(subprocess.check_output(
131
+ ["npm", "root", "-g"], text=True, timeout=5).strip())
132
+ names = ("claude.cmd", "claude.exe", "claude")
133
+ else:
134
+ base = os.path.join(subprocess.check_output(
135
+ ["npm", "prefix", "-g"], text=True, timeout=5).strip(), "bin")
136
+ names = ("claude",)
137
+ for n in names:
138
+ cp = os.path.join(base, n)
139
+ if os.path.isfile(cp):
140
+ return cp
141
+ except Exception:
142
+ pass
143
+ return None
144
+
145
+
146
+ def ensure_claude_installed(verbose: bool = False) -> bool:
147
+ """ZERO-FRICTION onboarding (R1): best-effort auto-install of the Claude
148
+ Code CLI so getting started is just `pip install meshcode` + `meshcode
149
+ init` / `meshcode login` — no separate manual editor install. Mirrors the
150
+ hostd._hostd_bootstrap pattern (best-effort, idempotent, NEVER raises —
151
+ auth/onboarding must not fail on a hiccup). Cross-platform: macOS / Linux /
152
+ Windows (qa finding — cover mac/linux so a cold machine doesn't break).
153
+
154
+ Returns True if `claude` is present (already, or after a successful
155
+ install), False if it's still missing (caller falls back to the manual
156
+ hint / doctor still reports it).
157
+ """
158
+ if _resolve_claude():
159
+ return True
160
+ npm = shutil.which("npm")
161
+ if npm:
162
+ if verbose:
163
+ print("[meshcode] Claude Code not found — installing "
164
+ "(npm i -g @anthropic-ai/claude-code)...")
165
+ try:
166
+ r = subprocess.run(
167
+ [npm, "install", "-g", "@anthropic-ai/claude-code"],
168
+ capture_output=True, text=True, timeout=300,
169
+ )
170
+ found = _resolve_claude()
171
+ if found:
172
+ # Make the freshly-installed claude visible to the REST of this
173
+ # process (the login/init flow) even if npm's global bin isn't
174
+ # on PATH yet. The later `meshcode run` is a separate process and
175
+ # resolves independently via _detect_editor's own fallback.
176
+ try:
177
+ bdir = os.path.dirname(found)
178
+ parts = os.environ.get("PATH", "").split(os.pathsep)
179
+ if bdir and bdir not in parts:
180
+ os.environ["PATH"] = bdir + os.pathsep + os.environ.get("PATH", "")
181
+ except Exception:
182
+ pass
183
+ if verbose:
184
+ print("[meshcode] Claude Code installed.")
185
+ return True
186
+ if verbose:
187
+ tail = (r.stderr or r.stdout or "").strip().splitlines()
188
+ print("[meshcode] Claude Code install did not complete"
189
+ + (f": {tail[-1]}" if tail else "."))
190
+ except Exception as e:
191
+ if verbose:
192
+ print(f"[meshcode] Claude Code auto-install skipped: {e}")
193
+ if verbose:
194
+ print("[meshcode] Claude Code is required. Install it with one of:")
195
+ print("[meshcode] npm i -g @anthropic-ai/claude-code (needs Node.js)")
196
+ print("[meshcode] or visit https://claude.ai/install")
197
+ return False
198
+
199
+
100
200
  def _check_node() -> Status:
101
201
  needs_node = False
102
202
  try:
@@ -2822,7 +2822,12 @@ def _hostd_install_windows() -> int:
2822
2822
  'set "MESHCODE_NO_UPDATE=1"\r\n' # var honored pre-2.11.74 (belt-and-suspenders)
2823
2823
  'set "MESHCODE_NO_AUTO_UPDATE=1"\r\n' # unified var (honored 2.11.74+)
2824
2824
  'set "MESHCODE_HOSTD_POLL_SEC=10"\r\n' # faster click->spawn (default 10s, floor 3)
2825
- f'"{mc}" hostd run\r\n',
2825
+ # F1a (task 48c3f294): launch the SUPERVISOR, not `hostd run` directly.
2826
+ # The supervisor owns + revives hostd on CRASH — the recovery the HKCU\Run
2827
+ # tier (login-only) lacks (that was the dead launch button on Samuel's box).
2828
+ # It self-singletons, so the Task-Scheduler periodic watchdog tier above
2829
+ # can also point here without ever double-starting.
2830
+ f'"{mc}" hostd supervise\r\n',
2826
2831
  encoding="utf-8",
2827
2832
  )
2828
2833
  # Register-ScheduledTask (PowerShell) — ROBUST KEEP-ALIVE (task 843f282c CRITICAL: hostd died ~15:04
@@ -3135,6 +3140,163 @@ def _acquire_hostd_singleton():
3135
3140
  return (None, "error")
3136
3141
 
3137
3142
 
3143
+ # ------------------------------------------------------------------
3144
+ # hostd SUPERVISOR (task 48c3f294 / F1a — Windows crash+reboot revive). The only
3145
+ # non-admin Windows persistence that works on locked-down boxes (Samuel's) is
3146
+ # HKCU\Run, which fires ONLY at login: a hostd that CRASHES mid-session is never
3147
+ # revived = the dead launch button. This supervisor is a SEPARATE, minimal
3148
+ # process that OWNS a `hostd run` child and restarts it on exit — the crash
3149
+ # recovery HKCU\Run lacks. Separate process, NOT hostd self-forking: an early
3150
+ # hostd crash (import error / startup fault) before any in-process fork would
3151
+ # otherwise never be revived. COMPOSES with hostd's in-process wedge watchdog
3152
+ # (process-STUCK self-restart via execv); this is the process-GONE half.
3153
+ # Anti-storm: the restart loop mirrors the absolute respawn cap (5618ac17) — a
3154
+ # hostd that crash-loops WITHOUT ever staying up is STOPPED after a ceiling and
3155
+ # its status written to hostd-supervisor.state, so the supervisor can never
3156
+ # become the storm it guards against. macOS (launchd KeepAlive) + Linux (systemd
3157
+ # Restart=on-failure) already get crash-revive, so this is win32-only in practice.
3158
+ # ------------------------------------------------------------------
3159
+ _SUP_LOCK_FH = None
3160
+ _SUP_STATE_PATH = STATE_DIR / "hostd-supervisor.state"
3161
+ _SUP_CONFIRMED_BOOT_S = _env_int("MESHCODE_SUP_CONFIRMED_BOOT_SEC", 120, 30) # child up >= this = confirmed boot -> reset cap
3162
+ _SUP_ABS_CAP = _env_int("MESHCODE_SUP_ABS_CAP", 5, 2) # crash-restarts w/o confirmed boot before STOP
3163
+ _SUP_ABS_WINDOW_S = _env_int("MESHCODE_SUP_ABS_WINDOW_SEC", 3600, 300) # rolling window
3164
+ _SUP_BACKOFF_BASE_S = _env_int("MESHCODE_SUP_BACKOFF_BASE_SEC", 5, 1) # exp backoff base between crash restarts
3165
+ _SUP_BACKOFF_MAX_S = _env_int("MESHCODE_SUP_BACKOFF_MAX_SEC", 300, 10) # backoff ceiling
3166
+ _SUP_POLL_S = _env_int("MESHCODE_SUP_POLL_SEC", 5, 1) # poll cadence while a hostd is already alive
3167
+
3168
+
3169
+ def _flock_probe_held(lock_path: Path) -> bool:
3170
+ """True if a LIVE process holds an exclusive flock/msvcrt lock on lock_path.
3171
+ Non-destructive try-acquire-release — robust vs the informational pid inside
3172
+ the lock file (which can be stale after a hard kill). Used to detect a live
3173
+ hostd before the supervisor spawns one (never double-start)."""
3174
+ try:
3175
+ fh = open(lock_path, "a+")
3176
+ except Exception:
3177
+ return False # can't evaluate -> assume free (supervisor errs toward having a hostd)
3178
+ try:
3179
+ if sys.platform == "win32":
3180
+ import msvcrt
3181
+ fh.seek(0)
3182
+ try:
3183
+ msvcrt.locking(fh.fileno(), msvcrt.LK_NBLCK, 1)
3184
+ except OSError:
3185
+ return True # held by a live process
3186
+ try:
3187
+ msvcrt.locking(fh.fileno(), msvcrt.LK_UNLCK, 1)
3188
+ except OSError:
3189
+ pass
3190
+ return False
3191
+ import fcntl
3192
+ try:
3193
+ fcntl.flock(fh.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
3194
+ except OSError:
3195
+ return True
3196
+ fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
3197
+ return False
3198
+ finally:
3199
+ fh.close()
3200
+
3201
+
3202
+ def _acquire_supervisor_singleton():
3203
+ """One supervisor per machine — flock/msvcrt on hostd-supervisor.lock, held
3204
+ for the process lifetime, OS-released on ANY death (no stale-lock problem;
3205
+ same field-tested mechanism as _acquire_hostd_singleton — addresses the
3206
+ SPOC's 'O_EXCL leaves a stale lock after a hard kill' note). Returns the fh
3207
+ on success, None if another live supervisor already holds it."""
3208
+ try:
3209
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
3210
+ fh = open(STATE_DIR / "hostd-supervisor.lock", "a+")
3211
+ except Exception:
3212
+ return None
3213
+ try:
3214
+ if sys.platform == "win32":
3215
+ import msvcrt
3216
+ fh.seek(0)
3217
+ msvcrt.locking(fh.fileno(), msvcrt.LK_NBLCK, 1)
3218
+ else:
3219
+ import fcntl
3220
+ fcntl.flock(fh.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
3221
+ except OSError:
3222
+ fh.close()
3223
+ return None
3224
+ try:
3225
+ fh.seek(0)
3226
+ fh.truncate()
3227
+ fh.write(str(os.getpid()))
3228
+ fh.flush()
3229
+ except Exception:
3230
+ pass
3231
+ return fh
3232
+
3233
+
3234
+ def _sup_write_state(state: str, reason: str = "", restart_count: int = 0) -> None:
3235
+ """Persist supervisor status to hostd-supervisor.state — the data source for
3236
+ the F1b dashboard-liveness UX (FE reads it to surface 'hostd stopped/looping'
3237
+ instead of a silently dead daemon). Best-effort."""
3238
+ try:
3239
+ _SUP_STATE_PATH.write_text(json.dumps({
3240
+ "state": state, "reason": reason, "restart_count": restart_count,
3241
+ "pid": os.getpid(), "ts": int(time.time()),
3242
+ }), encoding="utf-8")
3243
+ except Exception:
3244
+ pass
3245
+
3246
+
3247
+ def _hostd_supervise() -> int:
3248
+ """Supervisor loop: own a `hostd run` child and revive it on crash, with an
3249
+ absolute no-confirmed-boot cap so a crash-loop can't storm. See the block
3250
+ comment above for the full rationale."""
3251
+ fh = _acquire_supervisor_singleton()
3252
+ if fh is None:
3253
+ _log("hostd supervisor already running (hostd-supervisor.lock held) — exiting (singleton)")
3254
+ return 0
3255
+ global _SUP_LOCK_FH
3256
+ _SUP_LOCK_FH = fh
3257
+ _log("hostd supervisor started — owning + reviving `hostd run` (F1a)")
3258
+ argv = _hostd_run_argv()
3259
+ restarts: list = [] # spawn timestamps that did NOT reach a confirmed boot
3260
+ while True:
3261
+ # NEVER double-start: if a live hostd already holds hostd.lock (a manual
3262
+ # `hostd run`, or an orphan that outlived a prior supervisor), don't spawn
3263
+ # — wait for it to free. Without this, hostd's own singleton would make
3264
+ # our child exit 0 instantly and we'd read that as a crash and loop.
3265
+ if _flock_probe_held(STATE_DIR / "hostd.lock"):
3266
+ _sup_write_state("running", "hostd_already_live", len(restarts))
3267
+ time.sleep(_SUP_POLL_S)
3268
+ continue
3269
+ now = time.time()
3270
+ restarts = [t for t in restarts if now - t < _SUP_ABS_WINDOW_S]
3271
+ if len(restarts) >= _SUP_ABS_CAP:
3272
+ _sup_write_state("stopped", "abs_cap_crash_loop", len(restarts))
3273
+ _log(f"SUPERVISOR ABS-CAP: hostd crash-looped {len(restarts)}x in <= {_SUP_ABS_WINDOW_S}s "
3274
+ f"without staying up >= {_SUP_CONFIRMED_BOOT_S}s — STOPPING (a fresh `meshcode hostd "
3275
+ f"install` re-arms). Wrote {_SUP_STATE_PATH.name} for the dashboard.")
3276
+ return 0
3277
+ start = time.time()
3278
+ restarts.append(start)
3279
+ _sup_write_state("running", "spawning_hostd", len(restarts))
3280
+ try:
3281
+ proc = subprocess.Popen(argv, stdin=subprocess.DEVNULL)
3282
+ except Exception as e:
3283
+ backoff = min(_SUP_BACKOFF_MAX_S, _SUP_BACKOFF_BASE_S * (2 ** (len(restarts) - 1)))
3284
+ _log(f"supervisor: failed to spawn hostd ({e}) — retry in {backoff}s")
3285
+ time.sleep(backoff)
3286
+ continue
3287
+ rc = proc.wait() # authoritative: the hostd process is GONE
3288
+ uptime = time.time() - start
3289
+ if uptime >= _SUP_CONFIRMED_BOOT_S:
3290
+ restarts = [] # stayed up long enough = confirmed boot -> reset the cap
3291
+ backoff = _SUP_BACKOFF_BASE_S
3292
+ _log(f"hostd exited rc={rc} after {int(uptime)}s (confirmed boot) — restart in {backoff}s")
3293
+ else:
3294
+ backoff = min(_SUP_BACKOFF_MAX_S, _SUP_BACKOFF_BASE_S * (2 ** (len(restarts) - 1)))
3295
+ _log(f"hostd exited rc={rc} after {int(uptime)}s (no confirmed boot, "
3296
+ f"{len(restarts)}/{_SUP_ABS_CAP}) — restart in {backoff}s")
3297
+ time.sleep(backoff)
3298
+
3299
+
3138
3300
  def cmd_hostd(args: list) -> int:
3139
3301
  """Entry point for `meshcode hostd ...`."""
3140
3302
  if not args or args[0] in ("-h", "--help"):
@@ -3159,6 +3321,12 @@ def cmd_hostd(args: list) -> int:
3159
3321
  if sub == "uninstall":
3160
3322
  return _hostd_uninstall()
3161
3323
 
3324
+ if sub == "supervise":
3325
+ # F1a (task 48c3f294): own + revive `hostd run`. The Windows non-admin
3326
+ # autostart (HKCU\Run) points here so a mid-session hostd CRASH is revived,
3327
+ # not just a login. Not for macOS/Linux (launchd/systemd already revive).
3328
+ return _hostd_supervise()
3329
+
3162
3330
  if sub == "run":
3163
3331
  if not api_key:
3164
3332
  _log("FATAL: no api key — run `meshcode login` (key is read from the keychain)")
@@ -34,7 +34,7 @@ REGISTRY_PATH = WORKSPACES_ROOT / ".registry.json"
34
34
 
35
35
  # Samuel directive 2026-06-17: all agents boot with the best model by default.
36
36
  # Overrides (settings.json "model" or mc_agent_model_pref RPC) still respected.
37
- PLATFORM_DEFAULT_MODEL = "opus"
37
+ PLATFORM_DEFAULT_MODEL = "claude-opus-4-8"
38
38
 
39
39
 
40
40
  def _pretrust_claude_workspace(*paths) -> None:
@@ -769,6 +769,29 @@ def _detect_editor() -> Optional[str]:
769
769
  except Exception:
770
770
  pass
771
771
 
772
+ # macOS/Linux fallback: a freshly `npm i -g` claude (or claude.ai installer)
773
+ # may not be on this process's PATH yet (GUI launch / stale shell) — qa
774
+ # cold-start finding: posix had no fallback, only Windows did. Check common
775
+ # global-bin locations + ask npm for its global prefix.
776
+ if sys.platform != "win32":
777
+ home = os.path.expanduser("~")
778
+ posix_candidates = [
779
+ os.path.join(home, ".local", "bin", "claude"),
780
+ "/opt/homebrew/bin/claude",
781
+ "/usr/local/bin/claude",
782
+ os.path.join(home, ".npm-global", "bin", "claude"),
783
+ ]
784
+ for c in posix_candidates:
785
+ if c and os.path.isfile(c):
786
+ return c
787
+ try:
788
+ prefix = subprocess.check_output(["npm", "prefix", "-g"], text=True, timeout=5).strip()
789
+ p = os.path.join(prefix, "bin", "claude")
790
+ if os.path.isfile(p):
791
+ return p
792
+ except Exception:
793
+ pass
794
+
772
795
  return None
773
796
 
774
797
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.149
3
+ Version: 2.11.152
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -18,8 +18,17 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Operating System :: OS Independent
19
19
  Requires-Python: >=3.9
20
20
  Description-Content-Type: text/markdown
21
+ Requires-Dist: mcp[cli]>=1.0.0
22
+ Requires-Dist: websockets>=12.0
23
+ Requires-Dist: realtime>=2.0.0
24
+ Requires-Dist: keyring>=24.0
25
+ Requires-Dist: cryptography>=41.0
21
26
  Provides-Extra: test
27
+ Requires-Dist: pytest>=8; extra == "test"
22
28
  Provides-Extra: dev
29
+ Requires-Dist: build>=1.0; extra == "dev"
30
+ Requires-Dist: twine>=4; extra == "dev"
31
+ Requires-Dist: pytest>=8; extra == "dev"
23
32
 
24
33
  # MeshCode
25
34
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.11.149"
7
+ version = "2.11.152"
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
File without changes