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.
- {meshcode-2.11.149 → meshcode-2.11.152}/PKG-INFO +11 -2
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/__init__.py +1 -1
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/comms_v4.py +41 -3
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/doctor.py +101 -1
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/hostd.py +169 -1
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/run_agent.py +24 -1
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode.egg-info/PKG-INFO +11 -2
- {meshcode-2.11.149 → meshcode-2.11.152}/pyproject.toml +1 -1
- {meshcode-2.11.149 → meshcode-2.11.152}/README.md +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/__main__.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/cli.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/compat.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/daemon.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/helper_visuals.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/hooks/__init__.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/hooks/repo_path_lock.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/invites.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/launcher.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/swarm.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/meshcode_mcp/test_swarm.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/preferences.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/secrets.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/self_update.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/up.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode/upload.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/setup.cfg +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_core.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_doctor.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_helper_visuals.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_hostd_launch_pinned_env.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_hostd_serve_discovery_split.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_hostd_zombie_sessions.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_live_mesh_guard.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_pretrust_claude.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_session_replay_gate.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_stop_ghost_terminal.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_swarm_events.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_task_progress.py +0 -0
- {meshcode-2.11.149 → meshcode-2.11.152}/tests/test_terminal_lifecycle.py +0 -0
- {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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: meshcode
|
|
3
|
-
Version: 2.11.
|
|
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
|
|
|
@@ -319,8 +319,28 @@ def get_project_id(project_name):
|
|
|
319
319
|
if rows:
|
|
320
320
|
return rows[0]["id"]
|
|
321
321
|
|
|
322
|
-
#
|
|
323
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: meshcode
|
|
3
|
-
Version: 2.11.
|
|
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
|
|
|
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
|
|
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
|
|
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
|
|
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
|