meshcode 2.10.55__tar.gz → 2.10.56__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.55 → meshcode-2.10.56}/PKG-INFO +1 -1
- meshcode-2.10.56/meshcode/__init__.py +67 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/comms_v4.py +338 -19
- meshcode-2.10.56/meshcode/compat.py +174 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/meshcode_mcp/backend.py +52 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/meshcode_mcp/realtime.py +31 -7
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/meshcode_mcp/server.py +85 -13
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode.egg-info/SOURCES.txt +6 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/pyproject.toml +1 -1
- meshcode-2.10.56/tests/test_core.py +216 -0
- meshcode-2.10.56/tests/test_cross_agent_messaging.py +366 -0
- meshcode-2.10.56/tests/test_esc_deaf_state.py +361 -0
- meshcode-2.10.56/tests/test_realtime_event_freshness.py +236 -0
- meshcode-2.10.56/tests/test_rpc_migrations.py +377 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/tests/test_status_enum_coverage.py +32 -0
- meshcode-2.10.55/meshcode/__init__.py +0 -2
- {meshcode-2.10.55 → meshcode-2.10.56}/README.md +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/cli.py +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/invites.py +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/launcher.py +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/preferences.py +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/run_agent.py +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/secrets.py +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/self_update.py +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.55 → meshcode-2.10.56}/setup.cfg +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
+
__version__ = "2.10.56"
|
|
3
|
+
|
|
4
|
+
# Public API — lazy imports to avoid heavy deps at import time
|
|
5
|
+
def __getattr__(name):
|
|
6
|
+
if name == "backend":
|
|
7
|
+
from meshcode.meshcode_mcp import backend
|
|
8
|
+
return backend
|
|
9
|
+
if name in _BACKEND_EXPORTS:
|
|
10
|
+
from meshcode.meshcode_mcp import backend
|
|
11
|
+
return getattr(backend, name)
|
|
12
|
+
if name in _SECRETS_EXPORTS:
|
|
13
|
+
from meshcode import secrets
|
|
14
|
+
return getattr(secrets, name)
|
|
15
|
+
raise AttributeError(f"module 'meshcode' has no attribute {name!r}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Backend: core messaging & agent management
|
|
19
|
+
_BACKEND_EXPORTS = {
|
|
20
|
+
"send_message",
|
|
21
|
+
"read_inbox",
|
|
22
|
+
"count_pending",
|
|
23
|
+
"get_board",
|
|
24
|
+
"heartbeat",
|
|
25
|
+
"set_status",
|
|
26
|
+
"register_agent",
|
|
27
|
+
"get_project_id",
|
|
28
|
+
"sb_rpc",
|
|
29
|
+
"task_create",
|
|
30
|
+
"task_list",
|
|
31
|
+
"encrypt_payload",
|
|
32
|
+
"decrypt_payload",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Secrets: credential management
|
|
36
|
+
_SECRETS_EXPORTS = {
|
|
37
|
+
"get_api_key",
|
|
38
|
+
"set_api_key",
|
|
39
|
+
"list_profiles",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"__version__",
|
|
44
|
+
"backend",
|
|
45
|
+
# Messaging
|
|
46
|
+
"send_message",
|
|
47
|
+
"read_inbox",
|
|
48
|
+
"count_pending",
|
|
49
|
+
# Agent management
|
|
50
|
+
"register_agent",
|
|
51
|
+
"get_project_id",
|
|
52
|
+
"get_board",
|
|
53
|
+
"heartbeat",
|
|
54
|
+
"set_status",
|
|
55
|
+
# Tasks
|
|
56
|
+
"task_create",
|
|
57
|
+
"task_list",
|
|
58
|
+
# Low-level
|
|
59
|
+
"sb_rpc",
|
|
60
|
+
# Encryption
|
|
61
|
+
"encrypt_payload",
|
|
62
|
+
"decrypt_payload",
|
|
63
|
+
# Credentials
|
|
64
|
+
"get_api_key",
|
|
65
|
+
"set_api_key",
|
|
66
|
+
"list_profiles",
|
|
67
|
+
]
|
|
@@ -166,7 +166,13 @@ def sb_delete(table, filters):
|
|
|
166
166
|
|
|
167
167
|
|
|
168
168
|
def sb_rpc(fn_name, params):
|
|
169
|
-
"""Call a Supabase RPC function.
|
|
169
|
+
"""Call a Supabase RPC function.
|
|
170
|
+
|
|
171
|
+
Returns the parsed JSON response on success, or None on network/HTTP error.
|
|
172
|
+
HTTP errors are logged and printed to stderr for observability.
|
|
173
|
+
RPC-level errors (e.g. {"error": "..."}) are returned as-is — callers
|
|
174
|
+
must check for result.get("error") themselves.
|
|
175
|
+
"""
|
|
170
176
|
url = f"{SUPABASE_URL}/rest/v1/rpc/{fn_name}"
|
|
171
177
|
body = json.dumps(params).encode()
|
|
172
178
|
req = Request(url, data=body, method="POST", headers=_headers(content_profile=False))
|
|
@@ -174,8 +180,20 @@ def sb_rpc(fn_name, params):
|
|
|
174
180
|
with urlopen(req, timeout=10) as resp:
|
|
175
181
|
raw = resp.read().decode()
|
|
176
182
|
return json.loads(raw) if raw.strip() else None
|
|
177
|
-
except
|
|
178
|
-
|
|
183
|
+
except HTTPError as e:
|
|
184
|
+
err_body = ""
|
|
185
|
+
try:
|
|
186
|
+
err_body = e.read().decode()
|
|
187
|
+
err_obj = json.loads(err_body)
|
|
188
|
+
msg = err_obj.get("message", err_body[:200])
|
|
189
|
+
except Exception:
|
|
190
|
+
msg = err_body[:200] if err_body else str(e)
|
|
191
|
+
print(f"[ERROR] rpc:{fn_name}: {e.code} {msg}", file=sys.stderr)
|
|
192
|
+
log_error(f"rpc:{fn_name}", f"{e.code} {msg}", json.dumps(params, default=str)[:200])
|
|
193
|
+
return None
|
|
194
|
+
except URLError as e:
|
|
195
|
+
print(f"[ERROR] rpc:{fn_name}: network error: {e.reason}", file=sys.stderr)
|
|
196
|
+
log_error(f"rpc:{fn_name}", f"network: {e.reason}", json.dumps(params, default=str)[:200])
|
|
179
197
|
return None
|
|
180
198
|
|
|
181
199
|
|
|
@@ -1376,7 +1394,7 @@ def watch(project, name, interval=10, timeout=0):
|
|
|
1376
1394
|
print(f"\n*** [{project.upper()}] MENSAJE RECIBIDO ({elapsed}s en standby) ***")
|
|
1377
1395
|
read_messages(project, name, silent=False, send_acks=False)
|
|
1378
1396
|
|
|
1379
|
-
_update_agent_status({"status": "working", "task": "
|
|
1397
|
+
_update_agent_status({"status": "working", "task": "Processing received message", "last_heartbeat": now_iso()})
|
|
1380
1398
|
|
|
1381
1399
|
# Reset to standby and keep polling (don't exit the loop)
|
|
1382
1400
|
time.sleep(2)
|
|
@@ -1430,7 +1448,7 @@ def show_board(project):
|
|
|
1430
1448
|
project_id = get_project_id(project)
|
|
1431
1449
|
if not project_id:
|
|
1432
1450
|
print(f"[{project}] Project not found")
|
|
1433
|
-
|
|
1451
|
+
sys.exit(1)
|
|
1434
1452
|
|
|
1435
1453
|
_ak = _load_api_key_for_cli()
|
|
1436
1454
|
agents = None
|
|
@@ -1442,7 +1460,7 @@ def show_board(project):
|
|
|
1442
1460
|
if agents is None:
|
|
1443
1461
|
agents = sb_select("mc_agents", f"project_id=eq.{project_id}", order="registered_at.asc")
|
|
1444
1462
|
if not agents:
|
|
1445
|
-
print(f"[{project}]
|
|
1463
|
+
print(f"[{project}] No agents found")
|
|
1446
1464
|
return
|
|
1447
1465
|
|
|
1448
1466
|
print(f"\n{'='*60}")
|
|
@@ -1502,8 +1520,8 @@ def show_status(project=None):
|
|
|
1502
1520
|
def show_history(project, last_n=20, between=None):
|
|
1503
1521
|
project_id = get_project_id(project)
|
|
1504
1522
|
if not project_id:
|
|
1505
|
-
print(f"[{project}]
|
|
1506
|
-
|
|
1523
|
+
print(f"[{project}] No history")
|
|
1524
|
+
sys.exit(1)
|
|
1507
1525
|
|
|
1508
1526
|
filters = f"project_id=eq.{project_id}&type=neq.ack"
|
|
1509
1527
|
if between:
|
|
@@ -1515,7 +1533,7 @@ def show_history(project, last_n=20, between=None):
|
|
|
1515
1533
|
|
|
1516
1534
|
messages = sb_select("mc_messages", filters, order="created_at.desc", limit=last_n)
|
|
1517
1535
|
if not messages:
|
|
1518
|
-
print(f"[{project}]
|
|
1536
|
+
print(f"[{project}] No history")
|
|
1519
1537
|
return
|
|
1520
1538
|
|
|
1521
1539
|
messages.reverse()
|
|
@@ -1523,7 +1541,7 @@ def show_history(project, last_n=20, between=None):
|
|
|
1523
1541
|
print(f"\n{'='*60}")
|
|
1524
1542
|
print(f" {project.upper()} — History ({len(messages)} messages)")
|
|
1525
1543
|
if between:
|
|
1526
|
-
print(f"
|
|
1544
|
+
print(f" Filter: {between}")
|
|
1527
1545
|
print(f"{'='*60}\n")
|
|
1528
1546
|
|
|
1529
1547
|
for msg in messages:
|
|
@@ -1917,6 +1935,222 @@ def connect(project, name, hook_target="claude", role=""):
|
|
|
1917
1935
|
print()
|
|
1918
1936
|
|
|
1919
1937
|
|
|
1938
|
+
def cmd_doctor(flags, pos):
|
|
1939
|
+
"""Diagnose common meshcode setup issues."""
|
|
1940
|
+
import subprocess as _sp
|
|
1941
|
+
import shutil
|
|
1942
|
+
|
|
1943
|
+
auto_fix = "--fix" in sys.argv or flags.get("fix")
|
|
1944
|
+
results = [] # (status, label, detail) status: ok/warn/fail
|
|
1945
|
+
|
|
1946
|
+
def ok(label, detail=""):
|
|
1947
|
+
results.append(("ok", label, detail))
|
|
1948
|
+
|
|
1949
|
+
def warn(label, detail=""):
|
|
1950
|
+
results.append(("warn", label, detail))
|
|
1951
|
+
|
|
1952
|
+
def fail(label, detail=""):
|
|
1953
|
+
results.append(("fail", label, detail))
|
|
1954
|
+
|
|
1955
|
+
# 1. Python version
|
|
1956
|
+
py_ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
1957
|
+
if sys.version_info >= (3, 9):
|
|
1958
|
+
ok("Python version", py_ver)
|
|
1959
|
+
elif sys.version_info >= (3, 7):
|
|
1960
|
+
warn("Python version", f"{py_ver} (3.9+ recommended)")
|
|
1961
|
+
else:
|
|
1962
|
+
fail("Python version", f"{py_ver} (3.9+ required)")
|
|
1963
|
+
|
|
1964
|
+
# 2. meshcode version vs latest PyPI
|
|
1965
|
+
try:
|
|
1966
|
+
from meshcode import __version__ as mc_ver
|
|
1967
|
+
except Exception:
|
|
1968
|
+
mc_ver = "unknown"
|
|
1969
|
+
latest = None
|
|
1970
|
+
try:
|
|
1971
|
+
req = Request("https://pypi.org/pypi/meshcode/json", method="GET")
|
|
1972
|
+
with urlopen(req, timeout=5) as resp:
|
|
1973
|
+
data = json.loads(resp.read().decode())
|
|
1974
|
+
latest = data.get("info", {}).get("version")
|
|
1975
|
+
except Exception:
|
|
1976
|
+
pass
|
|
1977
|
+
if latest and mc_ver != "unknown":
|
|
1978
|
+
if mc_ver == latest:
|
|
1979
|
+
ok("meshcode version", f"{mc_ver} (latest)")
|
|
1980
|
+
else:
|
|
1981
|
+
warn("meshcode version", f"{mc_ver} (latest: {latest}). Run: pip install --upgrade meshcode")
|
|
1982
|
+
elif mc_ver != "unknown":
|
|
1983
|
+
ok("meshcode version", f"{mc_ver} (could not check PyPI)")
|
|
1984
|
+
else:
|
|
1985
|
+
fail("meshcode version", "could not determine version")
|
|
1986
|
+
|
|
1987
|
+
# 3. Config directory
|
|
1988
|
+
config_dir = Path.home() / ".meshcode"
|
|
1989
|
+
if config_dir.exists():
|
|
1990
|
+
api_key_file = config_dir / "api_key"
|
|
1991
|
+
profile_meta = config_dir / "profile_meta.json"
|
|
1992
|
+
has_key = bool(_load_api_key_for_cli())
|
|
1993
|
+
if has_key:
|
|
1994
|
+
ok("Authentication", "API key found in keychain")
|
|
1995
|
+
elif api_key_file.exists():
|
|
1996
|
+
warn("Authentication", "Legacy api_key file found. Run: meshcode login <key>")
|
|
1997
|
+
else:
|
|
1998
|
+
fail("Authentication", "Not logged in. Run: meshcode login <api_key>")
|
|
1999
|
+
if profile_meta.exists():
|
|
2000
|
+
try:
|
|
2001
|
+
meta = json.loads(profile_meta.read_text(encoding="utf-8"))
|
|
2002
|
+
email = meta.get("email", "unknown")
|
|
2003
|
+
ok("Profile", f"logged in as {email}")
|
|
2004
|
+
except Exception:
|
|
2005
|
+
warn("Profile", "profile_meta.json exists but unreadable")
|
|
2006
|
+
else:
|
|
2007
|
+
warn("Profile", "no profile_meta.json (run meshcode login)")
|
|
2008
|
+
else:
|
|
2009
|
+
fail("Config directory", "~/.meshcode/ does not exist. Run: meshcode login <api_key>")
|
|
2010
|
+
|
|
2011
|
+
# 4. Supabase connectivity (use a lightweight RPC that always exists)
|
|
2012
|
+
try:
|
|
2013
|
+
# Ping via a simple select on a known table with limit=0
|
|
2014
|
+
test_url = f"{SUPABASE_URL}/rest/v1/mc_projects?limit=0"
|
|
2015
|
+
req = Request(test_url, method="GET", headers=_headers())
|
|
2016
|
+
with urlopen(req, timeout=8) as resp:
|
|
2017
|
+
ok("Supabase API", "reachable")
|
|
2018
|
+
except HTTPError as e:
|
|
2019
|
+
if e.code in (401, 403):
|
|
2020
|
+
# Auth error means the API IS reachable, just RLS blocks it
|
|
2021
|
+
ok("Supabase API", "reachable (auth required for data)")
|
|
2022
|
+
else:
|
|
2023
|
+
fail("Supabase API", f"HTTP {e.code}")
|
|
2024
|
+
except Exception as e:
|
|
2025
|
+
fail("Supabase API", f"unreachable: {e}")
|
|
2026
|
+
|
|
2027
|
+
# 5. Stale agents check (requires auth)
|
|
2028
|
+
api_key = _load_api_key_for_cli()
|
|
2029
|
+
if api_key:
|
|
2030
|
+
try:
|
|
2031
|
+
r = sb_rpc("mc_resolve_project", {"p_api_key": api_key, "p_project_name": "*"})
|
|
2032
|
+
# Try listing all projects for this user
|
|
2033
|
+
projects_data = sb_rpc("mc_list_user_projects", {"p_api_key": api_key})
|
|
2034
|
+
if isinstance(projects_data, list):
|
|
2035
|
+
stale_agents = [] # (project_id, agent_name)
|
|
2036
|
+
for proj in projects_data[:5]:
|
|
2037
|
+
pid = proj.get("id") or proj.get("project_id")
|
|
2038
|
+
if not pid:
|
|
2039
|
+
continue
|
|
2040
|
+
agents = sb_select("mc_agents", f"project_id=eq.{pid}")
|
|
2041
|
+
if isinstance(agents, list):
|
|
2042
|
+
for a in agents:
|
|
2043
|
+
hb = a.get("last_heartbeat", "")
|
|
2044
|
+
status = a.get("status", "offline")
|
|
2045
|
+
if status not in ("offline", "needs_setup") and hb:
|
|
2046
|
+
try:
|
|
2047
|
+
from datetime import datetime, timezone
|
|
2048
|
+
hb_dt = datetime.fromisoformat(hb.replace("Z", "+00:00"))
|
|
2049
|
+
age = (datetime.now(timezone.utc) - hb_dt).total_seconds()
|
|
2050
|
+
if age > 180:
|
|
2051
|
+
stale_agents.append((pid, a.get("name", "?")))
|
|
2052
|
+
except Exception:
|
|
2053
|
+
pass
|
|
2054
|
+
if stale_agents:
|
|
2055
|
+
names = ", ".join(n for _, n in stale_agents[:5])
|
|
2056
|
+
warn("Stale agents", f"{len(stale_agents)} stale: {names}")
|
|
2057
|
+
if auto_fix:
|
|
2058
|
+
fixed = 0
|
|
2059
|
+
for pid, aname in stale_agents:
|
|
2060
|
+
try:
|
|
2061
|
+
sb_rpc("mc_disconnect_agent", {
|
|
2062
|
+
"p_project_id": pid, "p_agent_name": aname,
|
|
2063
|
+
})
|
|
2064
|
+
fixed += 1
|
|
2065
|
+
except Exception:
|
|
2066
|
+
pass
|
|
2067
|
+
if fixed:
|
|
2068
|
+
ok("Stale agent fix", f"forced {fixed} agent(s) offline")
|
|
2069
|
+
else:
|
|
2070
|
+
ok("Stale agents", "none detected")
|
|
2071
|
+
else:
|
|
2072
|
+
warn("Stale agents", "could not list projects (RPC not available)")
|
|
2073
|
+
except Exception as e:
|
|
2074
|
+
warn("Stale agents", f"check failed: {e}")
|
|
2075
|
+
else:
|
|
2076
|
+
warn("Stale agents", "skipped (not logged in)")
|
|
2077
|
+
|
|
2078
|
+
# 6. MCP server processes
|
|
2079
|
+
try:
|
|
2080
|
+
out = _sp.run(["ps", "-eo", "pid,ppid,comm"], capture_output=True, text=True, timeout=5)
|
|
2081
|
+
mcp_procs = [l for l in out.stdout.splitlines() if "meshcode_mcp" in l]
|
|
2082
|
+
orphans = [l for l in mcp_procs if l.split()[1].strip() == "1"]
|
|
2083
|
+
if not mcp_procs:
|
|
2084
|
+
ok("MCP processes", "none running (normal if no agents active)")
|
|
2085
|
+
elif len(orphans) > 3:
|
|
2086
|
+
warn("MCP processes", f"{len(mcp_procs)} running, {len(orphans)} orphaned (PPID=1)")
|
|
2087
|
+
if auto_fix:
|
|
2088
|
+
for line in orphans:
|
|
2089
|
+
pid = line.split()[0].strip()
|
|
2090
|
+
try:
|
|
2091
|
+
os.kill(int(pid), 15) # SIGTERM
|
|
2092
|
+
except Exception:
|
|
2093
|
+
pass
|
|
2094
|
+
ok("MCP cleanup", f"sent SIGTERM to {len(orphans)} orphan processes")
|
|
2095
|
+
else:
|
|
2096
|
+
ok("MCP processes", f"{len(mcp_procs)} running")
|
|
2097
|
+
except Exception:
|
|
2098
|
+
warn("MCP processes", "could not check (ps failed)")
|
|
2099
|
+
|
|
2100
|
+
# 7. Claude Code version
|
|
2101
|
+
claude_bin = shutil.which("claude")
|
|
2102
|
+
if claude_bin:
|
|
2103
|
+
try:
|
|
2104
|
+
out = _sp.run([claude_bin, "--version"], capture_output=True, text=True, timeout=5)
|
|
2105
|
+
cc_ver = out.stdout.strip()
|
|
2106
|
+
# Check for known-bad versions
|
|
2107
|
+
blocked = ["2.1.111", "2.1.112", "2.1.113", "2.1.114", "2.1.115",
|
|
2108
|
+
"2.1.116", "2.1.117", "2.1.118", "2.1.119"]
|
|
2109
|
+
ver_num = cc_ver.split()[-1].strip("()") if cc_ver else ""
|
|
2110
|
+
if any(b in cc_ver for b in blocked):
|
|
2111
|
+
fail("Claude Code", f"{cc_ver} (KNOWN MCP REGRESSION). Pin to 2.1.104: npm i -g @anthropic-ai/claude-code@2.1.104")
|
|
2112
|
+
else:
|
|
2113
|
+
ok("Claude Code", cc_ver)
|
|
2114
|
+
# Check for dual install
|
|
2115
|
+
try:
|
|
2116
|
+
out2 = _sp.run(["type", "-a", "claude"], capture_output=True, text=True,
|
|
2117
|
+
timeout=5, shell=True)
|
|
2118
|
+
installs = [l for l in out2.stdout.strip().splitlines() if "claude" in l]
|
|
2119
|
+
if len(installs) > 1:
|
|
2120
|
+
warn("Claude Code installs", f"DUAL INSTALL: {'; '.join(installs)}. Active: {claude_bin}")
|
|
2121
|
+
except Exception:
|
|
2122
|
+
pass
|
|
2123
|
+
except Exception as e:
|
|
2124
|
+
warn("Claude Code", f"found at {claude_bin} but version check failed: {e}")
|
|
2125
|
+
else:
|
|
2126
|
+
warn("Claude Code", "not found in PATH")
|
|
2127
|
+
|
|
2128
|
+
# Print results
|
|
2129
|
+
print()
|
|
2130
|
+
print(" meshcode doctor")
|
|
2131
|
+
print(" " + "=" * 40)
|
|
2132
|
+
ok_count = sum(1 for s, _, _ in results if s == "ok")
|
|
2133
|
+
warn_count = sum(1 for s, _, _ in results if s == "warn")
|
|
2134
|
+
fail_count = sum(1 for s, _, _ in results if s == "fail")
|
|
2135
|
+
for status, label, detail in results:
|
|
2136
|
+
icon = {"ok": "+", "warn": "!", "fail": "x"}[status]
|
|
2137
|
+
detail_str = f" -- {detail}" if detail else ""
|
|
2138
|
+
print(f" [{icon}] {label}{detail_str}")
|
|
2139
|
+
print()
|
|
2140
|
+
summary_parts = []
|
|
2141
|
+
if ok_count:
|
|
2142
|
+
summary_parts.append(f"{ok_count} passed")
|
|
2143
|
+
if warn_count:
|
|
2144
|
+
summary_parts.append(f"{warn_count} warnings")
|
|
2145
|
+
if fail_count:
|
|
2146
|
+
summary_parts.append(f"{fail_count} failed")
|
|
2147
|
+
print(f" {', '.join(summary_parts)}")
|
|
2148
|
+
if warn_count or fail_count:
|
|
2149
|
+
print(" Run with --fix to auto-repair where possible.")
|
|
2150
|
+
print()
|
|
2151
|
+
sys.exit(1 if fail_count else 0)
|
|
2152
|
+
|
|
2153
|
+
|
|
1920
2154
|
def login(api_key):
|
|
1921
2155
|
"""Authenticate with API key and save it to the OS keychain.
|
|
1922
2156
|
|
|
@@ -1983,6 +2217,7 @@ def show_help():
|
|
|
1983
2217
|
╚═══════════════════════════════════════════════════════════╝
|
|
1984
2218
|
|
|
1985
2219
|
QUICK START:
|
|
2220
|
+
init Guided setup (opens browser + login)
|
|
1986
2221
|
login <api_key> Authenticate (get key at meshcode.io)
|
|
1987
2222
|
go <agent> [--project <name>] Connect agent in one command
|
|
1988
2223
|
|
|
@@ -2022,6 +2257,10 @@ AGENT CONTROL:
|
|
|
2022
2257
|
profile [agent] Show/set agent profile
|
|
2023
2258
|
connect <proj> <name> Connect existing agent
|
|
2024
2259
|
|
|
2260
|
+
DIAGNOSTICS:
|
|
2261
|
+
doctor [--fix] Diagnose setup issues
|
|
2262
|
+
compat Claude Code version compatibility
|
|
2263
|
+
|
|
2025
2264
|
ADMIN:
|
|
2026
2265
|
clear <proj> <name> Clear inbox
|
|
2027
2266
|
unregister <proj> <name> Leave project
|
|
@@ -2043,6 +2282,26 @@ Run `meshcode <command> --help` for help on a specific command.
|
|
|
2043
2282
|
|
|
2044
2283
|
# Per-subcommand help texts
|
|
2045
2284
|
SUBCOMMAND_HELP = {
|
|
2285
|
+
"init": """meshcode init
|
|
2286
|
+
|
|
2287
|
+
Guided first-time setup. Opens meshcode.io in your browser to create
|
|
2288
|
+
an account, then prompts you to paste your API key.
|
|
2289
|
+
|
|
2290
|
+
After init, run:
|
|
2291
|
+
meshcode go <agent-name> --project <meshwork-name>
|
|
2292
|
+
""",
|
|
2293
|
+
"doctor": """meshcode doctor [--fix]
|
|
2294
|
+
|
|
2295
|
+
Diagnose common setup issues: Python version, meshcode version, auth,
|
|
2296
|
+
Supabase connectivity, stale agents, MCP processes, Claude Code version.
|
|
2297
|
+
|
|
2298
|
+
OPTIONS:
|
|
2299
|
+
--fix Auto-repair where possible (kill orphan MCP processes, etc.)
|
|
2300
|
+
|
|
2301
|
+
EXAMPLES:
|
|
2302
|
+
meshcode doctor # diagnose only
|
|
2303
|
+
meshcode doctor --fix # diagnose + auto-fix
|
|
2304
|
+
""",
|
|
2046
2305
|
"register": """meshcode register <project> <name> [role]
|
|
2047
2306
|
|
|
2048
2307
|
Join a meshwork as a new agent. Tier limits enforced (free=3 agents).
|
|
@@ -2148,8 +2407,8 @@ EXAMPLES:
|
|
|
2148
2407
|
"login": """meshcode login <api_key>
|
|
2149
2408
|
|
|
2150
2409
|
Authenticate with a MeshCode API key. Saves credentials to
|
|
2151
|
-
|
|
2152
|
-
|
|
2410
|
+
your OS keychain (macOS Keychain, Linux libsecret, Windows Credential
|
|
2411
|
+
Manager). Get a key from meshcode.io/settings or during signup.
|
|
2153
2412
|
""",
|
|
2154
2413
|
"clear": """meshcode clear <project> <name>
|
|
2155
2414
|
|
|
@@ -2329,6 +2588,53 @@ if __name__ == "__main__":
|
|
|
2329
2588
|
except Exception:
|
|
2330
2589
|
pass
|
|
2331
2590
|
|
|
2591
|
+
if cmd == "init":
|
|
2592
|
+
# Guided onboarding: open signup page + prompt for API key
|
|
2593
|
+
import webbrowser
|
|
2594
|
+
print()
|
|
2595
|
+
print(" Welcome to MeshCode!")
|
|
2596
|
+
print(" " + "=" * 40)
|
|
2597
|
+
print()
|
|
2598
|
+
print(" Step 1: Create your account")
|
|
2599
|
+
print(" Opening meshcode.io in your browser...")
|
|
2600
|
+
print()
|
|
2601
|
+
try:
|
|
2602
|
+
webbrowser.open("https://meshcode.io")
|
|
2603
|
+
except Exception:
|
|
2604
|
+
print(" Could not open browser. Visit: https://meshcode.io")
|
|
2605
|
+
print(" Step 2: Copy your API key from Settings")
|
|
2606
|
+
print()
|
|
2607
|
+
try:
|
|
2608
|
+
api_key = input(" Paste your API key (mc_...): ").strip()
|
|
2609
|
+
except (EOFError, KeyboardInterrupt):
|
|
2610
|
+
api_key = ""
|
|
2611
|
+
print()
|
|
2612
|
+
if api_key:
|
|
2613
|
+
login(api_key)
|
|
2614
|
+
print()
|
|
2615
|
+
print(" Step 3: Create your first meshwork")
|
|
2616
|
+
print(" Run: meshcode go <agent-name> --project <meshwork-name>")
|
|
2617
|
+
print()
|
|
2618
|
+
print(" Example:")
|
|
2619
|
+
print(" meshcode go backend --project my-app")
|
|
2620
|
+
print()
|
|
2621
|
+
else:
|
|
2622
|
+
print(" [meshcode] No API key provided. Run: meshcode login <api_key>")
|
|
2623
|
+
sys.exit(1)
|
|
2624
|
+
sys.exit(0)
|
|
2625
|
+
|
|
2626
|
+
# Auth guard: commands that talk to Supabase need a valid API key.
|
|
2627
|
+
# doctor, help, version, login, prefs, launcher don't need auth.
|
|
2628
|
+
_NO_AUTH_CMDS = {"doctor", "compat", "help", "--help", "-h", "login", "init",
|
|
2629
|
+
"prefs", "launcher", "--version", "-V", "version", "whoami",
|
|
2630
|
+
"profiles", "scan"}
|
|
2631
|
+
if cmd not in _NO_AUTH_CMDS:
|
|
2632
|
+
_cli_key = _load_api_key_for_cli()
|
|
2633
|
+
if not _cli_key:
|
|
2634
|
+
print("[meshcode] Not logged in. Run: meshcode login <api_key>")
|
|
2635
|
+
print("[meshcode] Get your API key at: https://meshcode.io/settings")
|
|
2636
|
+
sys.exit(1)
|
|
2637
|
+
|
|
2332
2638
|
if cmd == "register":
|
|
2333
2639
|
# Backwards-compat alias for `connect`. Mesh-only model: bind THIS
|
|
2334
2640
|
# terminal to (project, agent) — never spawns claude.
|
|
@@ -2447,7 +2753,7 @@ if __name__ == "__main__":
|
|
|
2447
2753
|
elif cmd == "validate-sessions":
|
|
2448
2754
|
proj = pos[0] if len(pos) > 0 else (sys.argv[2] if len(sys.argv) > 2 else "")
|
|
2449
2755
|
if not proj:
|
|
2450
|
-
print("[
|
|
2756
|
+
print("[ERROR] Usage: meshcode validate-sessions <project>")
|
|
2451
2757
|
sys.exit(1)
|
|
2452
2758
|
ensure_sessions()
|
|
2453
2759
|
prefix = f"{proj}_"
|
|
@@ -2473,7 +2779,7 @@ if __name__ == "__main__":
|
|
|
2473
2779
|
proj = pos[0] if len(pos) > 0 else ""
|
|
2474
2780
|
name = pos[1] if len(pos) > 1 else ""
|
|
2475
2781
|
if not proj or not name:
|
|
2476
|
-
print("[
|
|
2782
|
+
print("[ERROR] Usage: meshcode wake-headless <project> <agent>")
|
|
2477
2783
|
sys.exit(1)
|
|
2478
2784
|
project_id = get_project_id(proj)
|
|
2479
2785
|
if not project_id:
|
|
@@ -2489,7 +2795,7 @@ if __name__ == "__main__":
|
|
|
2489
2795
|
proj = pos[0] if len(pos) > 0 else "default"
|
|
2490
2796
|
name = pos[1] if len(pos) > 1 else ""
|
|
2491
2797
|
if not name:
|
|
2492
|
-
print(f"[
|
|
2798
|
+
print(f"[ERROR] Usage: meshcode {cmd} <project> <agent>")
|
|
2493
2799
|
sys.exit(1)
|
|
2494
2800
|
project_id = get_project_id(proj)
|
|
2495
2801
|
if not project_id:
|
|
@@ -2508,7 +2814,7 @@ if __name__ == "__main__":
|
|
|
2508
2814
|
proj = pos[1] if len(pos) > 1 else "default"
|
|
2509
2815
|
name = pos[2] if len(pos) > 2 else ""
|
|
2510
2816
|
if not name:
|
|
2511
|
-
print("[
|
|
2817
|
+
print("[ERROR] Usage: meshcode profile get|set <project> <agent> [flags]")
|
|
2512
2818
|
sys.exit(1)
|
|
2513
2819
|
project_id = get_project_id(proj)
|
|
2514
2820
|
if not project_id:
|
|
@@ -2533,7 +2839,7 @@ if __name__ == "__main__":
|
|
|
2533
2839
|
sys.exit(1)
|
|
2534
2840
|
print(f"[{proj}] profile updated for {name}")
|
|
2535
2841
|
else:
|
|
2536
|
-
print(f"[
|
|
2842
|
+
print(f"[ERROR] Unknown subcommand: {sub}. Use 'get' or 'set'.")
|
|
2537
2843
|
sys.exit(1)
|
|
2538
2844
|
|
|
2539
2845
|
elif cmd == "connect":
|
|
@@ -2721,7 +3027,7 @@ if __name__ == "__main__":
|
|
|
2721
3027
|
elif cmd == "login":
|
|
2722
3028
|
key = sys.argv[2] if len(sys.argv) > 2 else ""
|
|
2723
3029
|
if not key:
|
|
2724
|
-
print("[
|
|
3030
|
+
print("[ERROR] Usage: meshcode login <api_key>")
|
|
2725
3031
|
sys.exit(1)
|
|
2726
3032
|
login(key)
|
|
2727
3033
|
|
|
@@ -2886,6 +3192,19 @@ if __name__ == "__main__":
|
|
|
2886
3192
|
print(" meshcode prefs reset")
|
|
2887
3193
|
sys.exit(1)
|
|
2888
3194
|
|
|
3195
|
+
elif cmd == "compat":
|
|
3196
|
+
from meshcode.compat import check as cc_check, format_report, RECOMMENDED_VERSION
|
|
3197
|
+
version, status, entry = cc_check()
|
|
3198
|
+
print()
|
|
3199
|
+
print(" meshcode compat")
|
|
3200
|
+
print(" " + "=" * 40)
|
|
3201
|
+
print(format_report(version, entry))
|
|
3202
|
+
print()
|
|
3203
|
+
sys.exit(0 if status in ("safe", "unknown") else 1)
|
|
3204
|
+
|
|
3205
|
+
elif cmd == "doctor":
|
|
3206
|
+
cmd_doctor(flags, pos)
|
|
3207
|
+
|
|
2889
3208
|
elif cmd == "launcher":
|
|
2890
3209
|
# meshcode launcher {install|uninstall|start|stop|restart|status|logs|test}
|
|
2891
3210
|
try:
|
|
@@ -2906,7 +3225,7 @@ if __name__ == "__main__":
|
|
|
2906
3225
|
"history", "clear", "unregister", "connect", "disconnect",
|
|
2907
3226
|
"setup", "run", "go", "invite", "join", "invites", "members",
|
|
2908
3227
|
"revoke-invite", "revoke-member", "login", "prefs", "launcher",
|
|
2909
|
-
"help", "profile", "validate-sessions", "wake-headless",
|
|
3228
|
+
"help", "init", "doctor", "compat", "profile", "validate-sessions", "wake-headless",
|
|
2910
3229
|
]
|
|
2911
3230
|
# Simple fuzzy: prefix match + Levenshtein-like best match
|
|
2912
3231
|
suggestions = [c for c in known_cmds if c.startswith(cmd)]
|