meshcode 2.4.0__py3-none-any.whl → 2.4.2__py3-none-any.whl
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/__init__.py +1 -1
- meshcode/cli.py +13 -1
- meshcode/comms_v4.py +139 -34
- meshcode/invites.py +23 -3
- meshcode/launcher.py +2 -2
- meshcode/meshcode_mcp/server.py +12 -2
- meshcode/run_agent.py +3 -1
- meshcode/self_update.py +3 -3
- meshcode/setup_clients.py +11 -11
- {meshcode-2.4.0.dist-info → meshcode-2.4.2.dist-info}/METADATA +1 -1
- {meshcode-2.4.0.dist-info → meshcode-2.4.2.dist-info}/RECORD +14 -14
- {meshcode-2.4.0.dist-info → meshcode-2.4.2.dist-info}/WHEEL +0 -0
- {meshcode-2.4.0.dist-info → meshcode-2.4.2.dist-info}/entry_points.txt +0 -0
- {meshcode-2.4.0.dist-info → meshcode-2.4.2.dist-info}/top_level.txt +0 -0
meshcode/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "2.4.
|
|
2
|
+
__version__ = "2.4.2"
|
meshcode/cli.py
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
import sys
|
|
4
4
|
import os
|
|
5
5
|
|
|
6
|
+
# Force UTF-8 on stdout/stderr for Windows terminals (cp1252 default crashes
|
|
7
|
+
# on emojis, em-dashes, and any non-ASCII output the CLI prints).
|
|
8
|
+
# Python 3.7+ supports reconfigure(); older versions get a no-op.
|
|
9
|
+
try:
|
|
10
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
11
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
12
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
13
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
14
|
+
except Exception:
|
|
15
|
+
pass
|
|
16
|
+
|
|
6
17
|
|
|
7
18
|
def main():
|
|
8
19
|
"""Entry point that delegates to comms_v4.py within the package."""
|
|
@@ -18,7 +29,8 @@ def main():
|
|
|
18
29
|
sys.argv[0] = candidate
|
|
19
30
|
# Set __name__ and __file__ for the script
|
|
20
31
|
globs = {"__name__": "__main__", "__file__": candidate}
|
|
21
|
-
|
|
32
|
+
# Force UTF-8 on Windows (cp1252 default crashes on non-ASCII)
|
|
33
|
+
with open(candidate, encoding="utf-8") as f:
|
|
22
34
|
exec(compile(f.read(), candidate, "exec"), globs)
|
|
23
35
|
return
|
|
24
36
|
|
meshcode/comms_v4.py
CHANGED
|
@@ -57,7 +57,7 @@ def _load_env_file():
|
|
|
57
57
|
return {}
|
|
58
58
|
out = {}
|
|
59
59
|
try:
|
|
60
|
-
for line in env_path.read_text().splitlines():
|
|
60
|
+
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
61
61
|
line = line.strip()
|
|
62
62
|
if line.startswith("export "):
|
|
63
63
|
line = line[7:]
|
|
@@ -296,7 +296,7 @@ def can_nudge(project, name):
|
|
|
296
296
|
nf = get_nudge_file(project, name)
|
|
297
297
|
if nf and nf.exists():
|
|
298
298
|
try:
|
|
299
|
-
last = float(nf.read_text().strip())
|
|
299
|
+
last = float(nf.read_text(encoding="utf-8").strip())
|
|
300
300
|
if (time.time() - last) < NUDGE_COOLDOWN:
|
|
301
301
|
return False
|
|
302
302
|
except (ValueError, IOError, OSError):
|
|
@@ -307,7 +307,7 @@ def can_nudge(project, name):
|
|
|
307
307
|
def mark_nudged(project, name):
|
|
308
308
|
nf = get_nudge_file(project, name)
|
|
309
309
|
if nf:
|
|
310
|
-
nf.write_text(str(time.time()))
|
|
310
|
+
nf.write_text(str(time.time()), encoding="utf-8")
|
|
311
311
|
|
|
312
312
|
|
|
313
313
|
def send_notification(project, name, from_agent, pending=1):
|
|
@@ -455,7 +455,7 @@ def _throttle_spawn_ok(project, name, max_per_5min=5):
|
|
|
455
455
|
history = []
|
|
456
456
|
if throttle_file.exists():
|
|
457
457
|
try:
|
|
458
|
-
history = [float(x) for x in throttle_file.read_text().split() if x]
|
|
458
|
+
history = [float(x) for x in throttle_file.read_text(encoding="utf-8").split() if x]
|
|
459
459
|
except Exception:
|
|
460
460
|
history = []
|
|
461
461
|
history = [t for t in history if now_ts - t < 300] # last 5 min
|
|
@@ -463,7 +463,7 @@ def _throttle_spawn_ok(project, name, max_per_5min=5):
|
|
|
463
463
|
return False
|
|
464
464
|
history.append(now_ts)
|
|
465
465
|
try:
|
|
466
|
-
throttle_file.write_text(" ".join(f"{t:.0f}" for t in history))
|
|
466
|
+
throttle_file.write_text(" ".join(f"{t:.0f}" for t in history), encoding="utf-8")
|
|
467
467
|
except Exception:
|
|
468
468
|
pass
|
|
469
469
|
return True
|
|
@@ -615,7 +615,7 @@ def spawn_headless_agent(project, name, project_id, message_body, from_agent):
|
|
|
615
615
|
ts_safe = started_iso.replace(":", "-").replace("+", "_")
|
|
616
616
|
(debug_dir / f"{name}_{ts_safe}.log").write_text(
|
|
617
617
|
f"=== SYSTEM PROMPT ===\n{system_prompt}\n\n=== USER MESSAGE ===\n{message_body}\n\n=== STDOUT ===\n{stdout_text}\n\n=== STDERR ===\n{stderr_text}\n"
|
|
618
|
-
)
|
|
618
|
+
, encoding="utf-8")
|
|
619
619
|
except Exception:
|
|
620
620
|
pass
|
|
621
621
|
if exit_code != 0:
|
|
@@ -669,7 +669,7 @@ def nudge_agent(project, name, from_agent=""):
|
|
|
669
669
|
return False
|
|
670
670
|
|
|
671
671
|
try:
|
|
672
|
-
data = json.loads(session_file.read_text())
|
|
672
|
+
data = json.loads(session_file.read_text(encoding="utf-8"))
|
|
673
673
|
except (json.JSONDecodeError, IOError, OSError):
|
|
674
674
|
if _headless_spawn_allowed():
|
|
675
675
|
log_msg(f"[{project}] NUDGE: {name} session file unreadable, spawning headless (opt-in)")
|
|
@@ -818,7 +818,7 @@ def get_session_info():
|
|
|
818
818
|
for f in SESSIONS_DIR.iterdir():
|
|
819
819
|
if f.is_file() and '_' in f.name and not f.name.startswith('.'):
|
|
820
820
|
try:
|
|
821
|
-
data = json.loads(f.read_text())
|
|
821
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
822
822
|
session_tty = data.get("tty", "")
|
|
823
823
|
if session_tty and (session_tty in my_tty or my_tty in session_tty):
|
|
824
824
|
return data.get("project"), data.get("agent")
|
|
@@ -861,8 +861,15 @@ def register(project, name, role=""):
|
|
|
861
861
|
except (subprocess.SubprocessError, OSError, ValueError):
|
|
862
862
|
pass
|
|
863
863
|
|
|
864
|
-
# Register agent via tier-aware RPC (enforces plan limits)
|
|
865
|
-
|
|
864
|
+
# Register agent via tier-aware RPC (enforces plan limits).
|
|
865
|
+
# Use with_api_key variant so scoped guest keys also work and so
|
|
866
|
+
# the RPC validates owner/member access (no anon bypass).
|
|
867
|
+
_api_key = _load_api_key_for_cli()
|
|
868
|
+
if not _api_key:
|
|
869
|
+
print("[ERROR] Not authenticated. Run `meshcode login <api_key>` first.")
|
|
870
|
+
return
|
|
871
|
+
rpc_result = sb_rpc("mc_register_agent_with_api_key", {
|
|
872
|
+
"p_api_key": _api_key,
|
|
866
873
|
"p_project_id": project_id,
|
|
867
874
|
"p_name": name,
|
|
868
875
|
"p_role": role,
|
|
@@ -884,7 +891,7 @@ def register(project, name, role=""):
|
|
|
884
891
|
|
|
885
892
|
# Save local session file (for nudge TTY detection)
|
|
886
893
|
session_data = {"project": project, "agent": name, "pid": ppid, "tty": tty, "registered_at": now()}
|
|
887
|
-
(SESSIONS_DIR / f"{project}_{name}").write_text(json.dumps(session_data))
|
|
894
|
+
(SESSIONS_DIR / f"{project}_{name}").write_text(json.dumps(session_data), encoding="utf-8")
|
|
888
895
|
|
|
889
896
|
# Get all agents in project
|
|
890
897
|
agents = sb_select("mc_agents", f"project_id=eq.{project_id}", order="registered_at.asc")
|
|
@@ -1360,7 +1367,7 @@ def _start_heartbeat_daemon(project, name):
|
|
|
1360
1367
|
# Stop any existing heartbeat
|
|
1361
1368
|
try:
|
|
1362
1369
|
if pid_file.exists():
|
|
1363
|
-
old = int(pid_file.read_text().strip())
|
|
1370
|
+
old = int(pid_file.read_text(encoding="utf-8").strip())
|
|
1364
1371
|
try:
|
|
1365
1372
|
os.kill(old, 15)
|
|
1366
1373
|
except Exception:
|
|
@@ -1389,12 +1396,17 @@ def _start_heartbeat_daemon(project, name):
|
|
|
1389
1396
|
" if pid: post('/rest/v1/rpc/mc_heartbeat', {'p_project_id':pid,'p_agent_name':name})\n"
|
|
1390
1397
|
" time.sleep(30)\n"
|
|
1391
1398
|
)
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
stdout
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1399
|
+
# Windows: start_new_session kwarg doesn't exist. Use creationflags.
|
|
1400
|
+
_popen_kwargs = {
|
|
1401
|
+
"stdout": subprocess.DEVNULL,
|
|
1402
|
+
"stderr": subprocess.DEVNULL,
|
|
1403
|
+
}
|
|
1404
|
+
if sys.platform == "win32":
|
|
1405
|
+
_popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
1406
|
+
else:
|
|
1407
|
+
_popen_kwargs["start_new_session"] = True
|
|
1408
|
+
proc = subprocess.Popen([sys.executable, "-c", code], **_popen_kwargs)
|
|
1409
|
+
pid_file.write_text(str(proc.pid), encoding="utf-8")
|
|
1398
1410
|
return proc.pid
|
|
1399
1411
|
|
|
1400
1412
|
|
|
@@ -1402,7 +1414,7 @@ def _stop_heartbeat_daemon(project, name):
|
|
|
1402
1414
|
pid_file = _heartbeat_loop_pidfile(project, name)
|
|
1403
1415
|
if pid_file.exists():
|
|
1404
1416
|
try:
|
|
1405
|
-
pid = int(pid_file.read_text().strip())
|
|
1417
|
+
pid = int(pid_file.read_text(encoding="utf-8").strip())
|
|
1406
1418
|
os.kill(pid, 15)
|
|
1407
1419
|
except Exception:
|
|
1408
1420
|
pass
|
|
@@ -1426,13 +1438,18 @@ def connect_terminal(project, name, role=""):
|
|
|
1426
1438
|
tty, owning = _capture_tty_for_pid(os.getppid())
|
|
1427
1439
|
ppid = owning or os.getppid()
|
|
1428
1440
|
|
|
1429
|
-
# Make sure agent identity exists (idempotent)
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
"
|
|
1435
|
-
|
|
1441
|
+
# Make sure agent identity exists (idempotent).
|
|
1442
|
+
# Use with_api_key variant so scoped guest keys work and the RPC
|
|
1443
|
+
# validates owner/member access (no anon bypass).
|
|
1444
|
+
_api_key = _load_api_key_for_cli()
|
|
1445
|
+
if _api_key:
|
|
1446
|
+
sb_rpc("mc_register_agent_with_api_key", {
|
|
1447
|
+
"p_api_key": _api_key,
|
|
1448
|
+
"p_project_id": project_id,
|
|
1449
|
+
"p_name": name,
|
|
1450
|
+
"p_role": role,
|
|
1451
|
+
"p_status": "needs_setup",
|
|
1452
|
+
})
|
|
1436
1453
|
|
|
1437
1454
|
rpc_result = sb_rpc("mc_connect_agent", {
|
|
1438
1455
|
"p_project_id": project_id,
|
|
@@ -1445,7 +1462,7 @@ def connect_terminal(project, name, role=""):
|
|
|
1445
1462
|
return
|
|
1446
1463
|
|
|
1447
1464
|
session_data = {"project": project, "agent": name, "pid": ppid, "tty": tty, "registered_at": now()}
|
|
1448
|
-
(SESSIONS_DIR / f"{project}_{name}").write_text(json.dumps(session_data))
|
|
1465
|
+
(SESSIONS_DIR / f"{project}_{name}").write_text(json.dumps(session_data), encoding="utf-8")
|
|
1449
1466
|
|
|
1450
1467
|
hb_pid = _start_heartbeat_daemon(project, name)
|
|
1451
1468
|
|
|
@@ -1503,7 +1520,7 @@ def connect(project, name, hook_target="claude", role=""):
|
|
|
1503
1520
|
meta_path = Path.home() / ".meshcode" / "profile_meta.json"
|
|
1504
1521
|
if meta_path.exists():
|
|
1505
1522
|
try:
|
|
1506
|
-
meta = json.loads(meta_path.read_text())
|
|
1523
|
+
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
1507
1524
|
print(f"[meshcode] Authenticated as {meta.get('display_name', meta.get('email', '?'))}")
|
|
1508
1525
|
except Exception:
|
|
1509
1526
|
pass
|
|
@@ -1534,7 +1551,7 @@ def connect(project, name, hook_target="claude", role=""):
|
|
|
1534
1551
|
"send_command": f"python3 {comms_path} send {project} {name}:{{to}} '{{message}}'",
|
|
1535
1552
|
"platform": os_name
|
|
1536
1553
|
}
|
|
1537
|
-
config_path.write_text(json.dumps(config, indent=2))
|
|
1554
|
+
config_path.write_text(json.dumps(config, indent=2), encoding="utf-8")
|
|
1538
1555
|
print(f"[meshcode] Config saved to {config_path}")
|
|
1539
1556
|
print(f"[meshcode] Codex: use read_command and send_command from the config")
|
|
1540
1557
|
|
|
@@ -1591,7 +1608,7 @@ def login(api_key):
|
|
|
1591
1608
|
config_dir = Path.home() / ".meshcode"
|
|
1592
1609
|
config_dir.mkdir(exist_ok=True)
|
|
1593
1610
|
meta_path = config_dir / "profile_meta.json"
|
|
1594
|
-
meta_path.write_text(json.dumps(meta, indent=2))
|
|
1611
|
+
meta_path.write_text(json.dumps(meta, indent=2), encoding="utf-8")
|
|
1595
1612
|
try:
|
|
1596
1613
|
os.chmod(meta_path, 0o600)
|
|
1597
1614
|
except Exception:
|
|
@@ -1601,7 +1618,7 @@ def login(api_key):
|
|
|
1601
1618
|
flag = config_dir / ".migrated_to_keychain"
|
|
1602
1619
|
if not flag.exists():
|
|
1603
1620
|
try:
|
|
1604
|
-
flag.write_text("login")
|
|
1621
|
+
flag.write_text("login", encoding="utf-8")
|
|
1605
1622
|
except Exception:
|
|
1606
1623
|
pass
|
|
1607
1624
|
|
|
@@ -1656,6 +1673,12 @@ ADMIN:
|
|
|
1656
1673
|
unregister <proj> <name> Leave project
|
|
1657
1674
|
prefs View/set preferences
|
|
1658
1675
|
|
|
1676
|
+
PROFILES (multi-account):
|
|
1677
|
+
profiles List stored keychain profiles
|
|
1678
|
+
--profile <name> Use a specific profile (global flag)
|
|
1679
|
+
MESHCODE_PROFILE=<name> Env var equivalent
|
|
1680
|
+
(Inside a workspace dir, profile is auto-detected from .mcp.json)
|
|
1681
|
+
|
|
1659
1682
|
FEATURES: Auto-status (5 states), Agent Replay, Live Spectator,
|
|
1660
1683
|
Mesh Graph, Session Recording, Hot-reload
|
|
1661
1684
|
|
|
@@ -1843,6 +1866,31 @@ if __name__ == "__main__":
|
|
|
1843
1866
|
for bf in _bare_present:
|
|
1844
1867
|
flags[bf] = True
|
|
1845
1868
|
|
|
1869
|
+
# Global --profile flag: selects which keychain profile api_key comes from.
|
|
1870
|
+
# Priority: --profile <name> > MESHCODE_PROFILE env > "default".
|
|
1871
|
+
# Sets MESHCODE_KEYCHAIN_PROFILE env so every downstream helper picks it up.
|
|
1872
|
+
_profile_flag = flags.pop("profile", None)
|
|
1873
|
+
if _profile_flag:
|
|
1874
|
+
os.environ["MESHCODE_KEYCHAIN_PROFILE"] = _profile_flag
|
|
1875
|
+
elif os.environ.get("MESHCODE_PROFILE") and not os.environ.get("MESHCODE_KEYCHAIN_PROFILE"):
|
|
1876
|
+
os.environ["MESHCODE_KEYCHAIN_PROFILE"] = os.environ["MESHCODE_PROFILE"]
|
|
1877
|
+
else:
|
|
1878
|
+
# Auto-detect: if cwd is a meshcode workspace (~/meshcode/<proj>-<agent>),
|
|
1879
|
+
# read its .mcp.json to find the profile baked in by setup.
|
|
1880
|
+
try:
|
|
1881
|
+
_cwd = Path.cwd()
|
|
1882
|
+
_mcp_json = _cwd / ".mcp.json"
|
|
1883
|
+
if _mcp_json.exists() and str(_cwd).startswith(str(Path.home() / "meshcode")):
|
|
1884
|
+
_doc = json.loads(_mcp_json.read_text(encoding="utf-8"))
|
|
1885
|
+
_servers = _doc.get("mcpServers", {}) or {}
|
|
1886
|
+
for _sid, _sblk in _servers.items():
|
|
1887
|
+
_p = (_sblk.get("env") or {}).get("MESHCODE_KEYCHAIN_PROFILE")
|
|
1888
|
+
if _p and not os.environ.get("MESHCODE_KEYCHAIN_PROFILE"):
|
|
1889
|
+
os.environ["MESHCODE_KEYCHAIN_PROFILE"] = _p
|
|
1890
|
+
break
|
|
1891
|
+
except Exception:
|
|
1892
|
+
pass
|
|
1893
|
+
|
|
1846
1894
|
if cmd == "register":
|
|
1847
1895
|
# Backwards-compat alias for `connect`. Mesh-only model: bind THIS
|
|
1848
1896
|
# terminal to (project, agent) — never spawns claude.
|
|
@@ -1951,7 +1999,7 @@ if __name__ == "__main__":
|
|
|
1951
1999
|
any_found = True
|
|
1952
2000
|
agent = f.name[len(prefix):]
|
|
1953
2001
|
try:
|
|
1954
|
-
d = json.loads(f.read_text())
|
|
2002
|
+
d = json.loads(f.read_text(encoding="utf-8"))
|
|
1955
2003
|
except Exception as e:
|
|
1956
2004
|
print(f" [{agent}] CORRUPT ({e})")
|
|
1957
2005
|
continue
|
|
@@ -2053,12 +2101,28 @@ if __name__ == "__main__":
|
|
|
2053
2101
|
|
|
2054
2102
|
elif cmd in ("run", "go"):
|
|
2055
2103
|
# meshcode go <agent> [--project <name>] [--editor claude|cursor|code]
|
|
2104
|
+
# meshcode go <project>/<agent> — disambiguation syntax when same agent
|
|
2105
|
+
# name exists in multiple meshworks
|
|
2056
2106
|
# meshcode run <agent> (backwards compat, same as go)
|
|
2057
2107
|
if len(sys.argv) < 3:
|
|
2058
2108
|
print(f"Usage: meshcode {cmd} <agent> [--project <name>] [--editor claude|cursor|code]")
|
|
2109
|
+
print(f" or: meshcode {cmd} <project>/<agent> (disambiguation)")
|
|
2059
2110
|
sys.exit(1)
|
|
2060
|
-
|
|
2061
|
-
|
|
2111
|
+
agent_arg = sys.argv[2]
|
|
2112
|
+
# Parse project/agent syntax
|
|
2113
|
+
if "/" in agent_arg and not agent_arg.startswith("/"):
|
|
2114
|
+
parts = agent_arg.split("/", 1)
|
|
2115
|
+
if len(parts) == 2 and parts[0] and parts[1]:
|
|
2116
|
+
proj_override = parts[0]
|
|
2117
|
+
agent = parts[1]
|
|
2118
|
+
else:
|
|
2119
|
+
agent = agent_arg
|
|
2120
|
+
proj_override = flags.get("project")
|
|
2121
|
+
else:
|
|
2122
|
+
agent = agent_arg
|
|
2123
|
+
proj_override = flags.get("project")
|
|
2124
|
+
if flags.get("project"):
|
|
2125
|
+
proj_override = flags.get("project")
|
|
2062
2126
|
editor_override = flags.get("editor")
|
|
2063
2127
|
perm_override = flags.get("permission") # bypass | safe | ask
|
|
2064
2128
|
if "--bypass" in sys.argv:
|
|
@@ -2149,6 +2213,47 @@ if __name__ == "__main__":
|
|
|
2149
2213
|
sys.exit(1)
|
|
2150
2214
|
login(key)
|
|
2151
2215
|
|
|
2216
|
+
elif cmd in ("profiles", "whoami"):
|
|
2217
|
+
# List all stored keychain profiles with metadata
|
|
2218
|
+
try:
|
|
2219
|
+
import importlib as _il
|
|
2220
|
+
_sec = _il.import_module("meshcode.secrets")
|
|
2221
|
+
except Exception as _e:
|
|
2222
|
+
print(f"[meshcode] ERROR: cannot load secrets module: {_e}", file=sys.stderr)
|
|
2223
|
+
sys.exit(1)
|
|
2224
|
+
|
|
2225
|
+
profiles = _sec.list_profiles()
|
|
2226
|
+
active = os.environ.get("MESHCODE_KEYCHAIN_PROFILE") or "default"
|
|
2227
|
+
|
|
2228
|
+
if not profiles:
|
|
2229
|
+
print("[meshcode] No profiles stored yet.")
|
|
2230
|
+
print("[meshcode] Run `meshcode login <api_key>` to create the default profile,")
|
|
2231
|
+
print("[meshcode] or `meshcode join <invite-token>` to create a scoped profile.")
|
|
2232
|
+
sys.exit(0)
|
|
2233
|
+
|
|
2234
|
+
print()
|
|
2235
|
+
print(f"[meshcode] Stored profiles ({len(profiles)}):")
|
|
2236
|
+
print()
|
|
2237
|
+
for name, meta in sorted(profiles.items()):
|
|
2238
|
+
marker = " *ACTIVE*" if name == active else ""
|
|
2239
|
+
kind = meta.get("type", "default" if name == "default" else "scoped")
|
|
2240
|
+
print(f" {name}{marker}")
|
|
2241
|
+
print(f" type: {kind}")
|
|
2242
|
+
if meta.get("meshwork"):
|
|
2243
|
+
print(f" meshwork: {meta['meshwork']}")
|
|
2244
|
+
if meta.get("agent"):
|
|
2245
|
+
print(f" agent: {meta['agent']}")
|
|
2246
|
+
if meta.get("email"):
|
|
2247
|
+
print(f" email: {meta['email']}")
|
|
2248
|
+
if meta.get("expires_at"):
|
|
2249
|
+
print(f" expires: {meta['expires_at']}")
|
|
2250
|
+
if meta.get("backend"):
|
|
2251
|
+
print(f" backend: {meta['backend']}")
|
|
2252
|
+
print()
|
|
2253
|
+
print("[meshcode] Use `--profile <name>` or `MESHCODE_PROFILE=<name>` to switch.")
|
|
2254
|
+
print("[meshcode] In a meshcode workspace dir, the profile is auto-detected from .mcp.json.")
|
|
2255
|
+
sys.exit(0)
|
|
2256
|
+
|
|
2152
2257
|
elif cmd == "prefs":
|
|
2153
2258
|
# meshcode prefs permission-mode [bypass|safe|ask] (no arg = show)
|
|
2154
2259
|
# meshcode prefs auto-update [on|off|reset] (no arg = show)
|
meshcode/invites.py
CHANGED
|
@@ -79,10 +79,17 @@ def _rpc(name: str, body: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
def _get_default_api_key() -> Optional[str]:
|
|
82
|
+
"""Read the api key for the active profile.
|
|
83
|
+
|
|
84
|
+
Profile resolution: MESHCODE_KEYCHAIN_PROFILE env > "default".
|
|
85
|
+
The CLI dispatcher sets MESHCODE_KEYCHAIN_PROFILE from --profile flag,
|
|
86
|
+
MESHCODE_PROFILE env, or workspace auto-detection.
|
|
87
|
+
"""
|
|
82
88
|
try:
|
|
83
89
|
import importlib
|
|
84
90
|
secrets_mod = importlib.import_module("meshcode.secrets")
|
|
85
|
-
|
|
91
|
+
profile = os.environ.get("MESHCODE_KEYCHAIN_PROFILE") or "default"
|
|
92
|
+
return secrets_mod.get_api_key(profile=profile)
|
|
86
93
|
except Exception:
|
|
87
94
|
return None
|
|
88
95
|
|
|
@@ -165,15 +172,28 @@ def cmd_join(token: str, display_name: Optional[str] = None) -> int:
|
|
|
165
172
|
if not isinstance(redeem, dict):
|
|
166
173
|
print("[meshcode] ERROR: could not redeem invite", file=sys.stderr)
|
|
167
174
|
return 1
|
|
175
|
+
# Handle both application errors ({"error": "..."}) and Postgres errors
|
|
176
|
+
# ({"code": "42703", "message": "...", "details": ..., "hint": ...})
|
|
168
177
|
if redeem.get("error"):
|
|
169
178
|
print(f"[meshcode] ERROR: {redeem['error']}", file=sys.stderr)
|
|
170
179
|
return 1
|
|
180
|
+
if redeem.get("code") and redeem.get("message"):
|
|
181
|
+
print(f"[meshcode] ERROR: database error {redeem['code']}: {redeem['message']}", file=sys.stderr)
|
|
182
|
+
if redeem.get("hint"):
|
|
183
|
+
print(f"[meshcode] hint: {redeem['hint']}", file=sys.stderr)
|
|
184
|
+
return 1
|
|
171
185
|
|
|
172
186
|
scoped_key = redeem.get("api_key")
|
|
173
|
-
|
|
187
|
+
# Backend returns both "project_name" (canonical) and "meshwork" (legacy)
|
|
188
|
+
project = redeem.get("project_name") or redeem.get("meshwork")
|
|
174
189
|
agent = redeem.get("agent_name")
|
|
175
190
|
if not scoped_key or not project or not agent:
|
|
176
|
-
|
|
191
|
+
missing = []
|
|
192
|
+
if not scoped_key: missing.append("api_key")
|
|
193
|
+
if not project: missing.append("project_name/meshwork")
|
|
194
|
+
if not agent: missing.append("agent_name")
|
|
195
|
+
print(f"[meshcode] ERROR: redeem succeeded but response missing fields: {', '.join(missing)}", file=sys.stderr)
|
|
196
|
+
print(f"[meshcode] got keys: {sorted(redeem.keys())}", file=sys.stderr)
|
|
177
197
|
return 1
|
|
178
198
|
|
|
179
199
|
# 3. Store scoped key in OS keychain under a per-mesh profile
|
meshcode/launcher.py
CHANGED
|
@@ -67,7 +67,7 @@ def load_env() -> tuple[str, str]:
|
|
|
67
67
|
key = os.environ.get("SUPABASE_KEY", "").strip()
|
|
68
68
|
if not (url and key) and ENV_FILE.exists():
|
|
69
69
|
try:
|
|
70
|
-
for raw in ENV_FILE.read_text().splitlines():
|
|
70
|
+
for raw in ENV_FILE.read_text(encoding="utf-8").splitlines():
|
|
71
71
|
line = raw.strip()
|
|
72
72
|
if not line or line.startswith("#"):
|
|
73
73
|
continue
|
|
@@ -103,7 +103,7 @@ def write_pidfile() -> None:
|
|
|
103
103
|
"uptime_seconds": int(time.time() - _start_time),
|
|
104
104
|
}
|
|
105
105
|
try:
|
|
106
|
-
PID_FILE.write_text(json.dumps(payload, indent=2))
|
|
106
|
+
PID_FILE.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
107
107
|
except Exception as e:
|
|
108
108
|
log(f"pid-file write failed: {e}", err=True)
|
|
109
109
|
|
meshcode/meshcode_mcp/server.py
CHANGED
|
@@ -885,8 +885,18 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
|
|
|
885
885
|
|
|
886
886
|
@mcp.tool()
|
|
887
887
|
@with_working_status
|
|
888
|
-
def meshcode_broadcast(payload:
|
|
889
|
-
"""Broadcast to ALL agents in this meshwork (except yourself).
|
|
888
|
+
def meshcode_broadcast(payload: Any) -> Dict[str, Any]:
|
|
889
|
+
"""Broadcast to ALL agents in this meshwork (except yourself).
|
|
890
|
+
|
|
891
|
+
Accepts payload as either:
|
|
892
|
+
- dict (structured): {"type": "...", "detail": "..."}
|
|
893
|
+
- str (shorthand): wrapped internally as {"text": "..."}
|
|
894
|
+
"""
|
|
895
|
+
if isinstance(payload, str):
|
|
896
|
+
payload = {"text": payload}
|
|
897
|
+
elif not isinstance(payload, dict):
|
|
898
|
+
return {"error": "payload must be a string or object", "got_type": type(payload).__name__}
|
|
899
|
+
|
|
890
900
|
agents = be.get_board(_PROJECT_ID)
|
|
891
901
|
sent = 0
|
|
892
902
|
for a in agents:
|
meshcode/run_agent.py
CHANGED
|
@@ -44,7 +44,9 @@ def _try_auto_setup(agent: str, project: Optional[str] = None) -> Optional[Tuple
|
|
|
44
44
|
except Exception:
|
|
45
45
|
return None
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
import os as _os
|
|
48
|
+
profile = _os.environ.get("MESHCODE_KEYCHAIN_PROFILE") or "default"
|
|
49
|
+
api_key = secrets_mod.get_api_key(profile=profile)
|
|
48
50
|
if not api_key:
|
|
49
51
|
return None
|
|
50
52
|
|
meshcode/self_update.py
CHANGED
|
@@ -144,7 +144,7 @@ def consume_pending_result() -> None:
|
|
|
144
144
|
if not RESULT_PATH.exists():
|
|
145
145
|
return
|
|
146
146
|
try:
|
|
147
|
-
data = json.loads(RESULT_PATH.read_text())
|
|
147
|
+
data = json.loads(RESULT_PATH.read_text(encoding="utf-8"))
|
|
148
148
|
RESULT_PATH.unlink(missing_ok=True)
|
|
149
149
|
except Exception:
|
|
150
150
|
return
|
|
@@ -188,7 +188,7 @@ def _acquire_lock() -> bool:
|
|
|
188
188
|
return False
|
|
189
189
|
LOCK_PATH.unlink(missing_ok=True)
|
|
190
190
|
LOCK_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
191
|
-
LOCK_PATH.write_text(str(os.getpid()))
|
|
191
|
+
LOCK_PATH.write_text(str(os.getpid()), encoding="utf-8")
|
|
192
192
|
return True
|
|
193
193
|
except Exception:
|
|
194
194
|
return False
|
|
@@ -233,7 +233,7 @@ except Exception as e:
|
|
|
233
233
|
finally:
|
|
234
234
|
result["finished_at"] = int(time.time())
|
|
235
235
|
try:
|
|
236
|
-
result_path.write_text(json.dumps(result))
|
|
236
|
+
result_path.write_text(json.dumps(result), encoding="utf-8")
|
|
237
237
|
except Exception:
|
|
238
238
|
pass
|
|
239
239
|
try:
|
meshcode/setup_clients.py
CHANGED
|
@@ -53,7 +53,7 @@ def _load_credentials(profile: str = "default") -> Dict[str, str]:
|
|
|
53
53
|
meta: Dict[str, Any] = {"api_key": api_key}
|
|
54
54
|
if meta_path.exists():
|
|
55
55
|
try:
|
|
56
|
-
extra = json.loads(meta_path.read_text())
|
|
56
|
+
extra = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
57
57
|
if isinstance(extra, dict):
|
|
58
58
|
meta.update({k: v for k, v in extra.items() if v is not None})
|
|
59
59
|
except Exception:
|
|
@@ -64,7 +64,7 @@ def _load_credentials(profile: str = "default") -> Dict[str, str]:
|
|
|
64
64
|
legacy_path = Path.home() / ".meshcode" / "credentials.json"
|
|
65
65
|
if legacy_path.exists() and not meta_path.exists():
|
|
66
66
|
try:
|
|
67
|
-
legacy = json.loads(legacy_path.read_text())
|
|
67
|
+
legacy = json.loads(legacy_path.read_text(encoding="utf-8"))
|
|
68
68
|
if isinstance(legacy, dict):
|
|
69
69
|
for k in ("user_id", "email", "display_name"):
|
|
70
70
|
if legacy.get(k) and not meta.get(k):
|
|
@@ -81,7 +81,7 @@ def _load_supabase_env() -> Dict[str, str]:
|
|
|
81
81
|
if not url or not key:
|
|
82
82
|
env_file = Path.home() / ".meshcode" / "env"
|
|
83
83
|
if env_file.exists():
|
|
84
|
-
for line in env_file.read_text().splitlines():
|
|
84
|
+
for line in env_file.read_text(encoding="utf-8").splitlines():
|
|
85
85
|
line = line.strip()
|
|
86
86
|
if line.startswith("export "):
|
|
87
87
|
line = line[7:]
|
|
@@ -204,7 +204,7 @@ def _build_server_block(project: str, project_id: str, agent: str, role: str,
|
|
|
204
204
|
def _atomic_write_json(path: Path, data: dict) -> None:
|
|
205
205
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
206
206
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
207
|
-
tmp.write_text(json.dumps(data, indent=2))
|
|
207
|
+
tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
208
208
|
tmp.replace(path)
|
|
209
209
|
|
|
210
210
|
|
|
@@ -258,7 +258,7 @@ def _atomic_write_toml_codex(path: Path, server_id: str, server_block: Dict[str,
|
|
|
258
258
|
Atomic via tempfile + replace.
|
|
259
259
|
"""
|
|
260
260
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
261
|
-
text = path.read_text() if path.exists() else ""
|
|
261
|
+
text = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
262
262
|
|
|
263
263
|
quoted = f'"{_toml_escape(server_id)}"'
|
|
264
264
|
header = f"[mcp_servers.{quoted}]"
|
|
@@ -291,7 +291,7 @@ def _atomic_write_toml_codex(path: Path, server_id: str, server_block: Dict[str,
|
|
|
291
291
|
new_text += _render_codex_block(server_id, server_block)
|
|
292
292
|
|
|
293
293
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
294
|
-
tmp.write_text(new_text)
|
|
294
|
+
tmp.write_text(new_text, encoding="utf-8")
|
|
295
295
|
tmp.replace(path)
|
|
296
296
|
|
|
297
297
|
|
|
@@ -306,7 +306,7 @@ def _write_continue_config(ws: Path, server_id: str, server_block: Dict[str, Any
|
|
|
306
306
|
args = server_block.get("args", [])
|
|
307
307
|
|
|
308
308
|
# Build YAML manually (no yaml dependency)
|
|
309
|
-
lines = ["# MeshCode MCP server
|
|
309
|
+
lines = ["# MeshCode MCP server - auto-generated by meshcode setup"]
|
|
310
310
|
lines.append("models: []")
|
|
311
311
|
lines.append("mcpServers:")
|
|
312
312
|
lines.append(f" - name: {server_id}")
|
|
@@ -323,7 +323,7 @@ def _write_continue_config(ws: Path, server_id: str, server_block: Dict[str, Any
|
|
|
323
323
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
324
324
|
config_path = config_dir / "config.yaml"
|
|
325
325
|
try:
|
|
326
|
-
config_path.write_text("\n".join(lines) + "\n")
|
|
326
|
+
config_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
327
327
|
except (IOError, OSError) as e:
|
|
328
328
|
print(f"[meshcode] WARNING: could not write Continue config: {e}", file=sys.stderr)
|
|
329
329
|
|
|
@@ -423,7 +423,7 @@ def setup_workspace(project: str, agent: str, role: str = "",
|
|
|
423
423
|
registry: Dict[str, Any] = {}
|
|
424
424
|
if registry_path.exists():
|
|
425
425
|
try:
|
|
426
|
-
registry = json.loads(registry_path.read_text())
|
|
426
|
+
registry = json.loads(registry_path.read_text(encoding="utf-8"))
|
|
427
427
|
except Exception:
|
|
428
428
|
registry = {}
|
|
429
429
|
registry.setdefault("agents", {})[agent] = {
|
|
@@ -463,7 +463,7 @@ This is your workspace dir — feel free to scaffold any files your task needs h
|
|
|
463
463
|
work in a different repo by `cd`ing elsewhere after launch.
|
|
464
464
|
"""
|
|
465
465
|
try:
|
|
466
|
-
(ws / "README.md").write_text(readme_body)
|
|
466
|
+
(ws / "README.md").write_text(readme_body, encoding="utf-8")
|
|
467
467
|
except Exception as _e:
|
|
468
468
|
print(f"[meshcode] WARNING: could not write README.md: {_e}", file=sys.stderr)
|
|
469
469
|
|
|
@@ -567,7 +567,7 @@ def setup_global(client: str, project: str, agent: str, role: str = "") -> int:
|
|
|
567
567
|
existing: Dict[str, Any] = {}
|
|
568
568
|
if config_path.exists():
|
|
569
569
|
try:
|
|
570
|
-
existing = json.loads(config_path.read_text())
|
|
570
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
571
571
|
except json.JSONDecodeError:
|
|
572
572
|
print(f"[meshcode] WARNING: {config_path} exists but is not valid JSON.", file=sys.stderr)
|
|
573
573
|
return 2
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
meshcode/__init__.py,sha256=
|
|
2
|
-
meshcode/cli.py,sha256=
|
|
3
|
-
meshcode/comms_v4.py,sha256=
|
|
4
|
-
meshcode/invites.py,sha256=
|
|
5
|
-
meshcode/launcher.py,sha256=
|
|
1
|
+
meshcode/__init__.py,sha256=qB8hIuwmMwgqGtGO8uzgyFNbZAvLm4XVrhaj4QjLR1Y,84
|
|
2
|
+
meshcode/cli.py,sha256=zHVYBoj7EIx6J_rxgsALT9bqr1YAIPU9yUVdCRwpP0g,1477
|
|
3
|
+
meshcode/comms_v4.py,sha256=yhsNF8aVFjBI0w_mO4jOyz3W60HoxzcP7eQyd2Kkvos,93426
|
|
4
|
+
meshcode/invites.py,sha256=TWN5dAPrKBhBKzWQ_e4nzwW5QmiBd7mxd1RwjLWr7go,14690
|
|
5
|
+
meshcode/launcher.py,sha256=qoU3yOdvMtqWxHduhqDIJ_iXjT3CeDIScwfVX2osfhM,12129
|
|
6
6
|
meshcode/launcher_install.py,sha256=79MVxHtskmcJYGCRM8aU4wIxjExgEHmTL3Xhmi_e3n0,13143
|
|
7
7
|
meshcode/preferences.py,sha256=loIu-t8zJJLFbjz8smXdZbe1SFFxk_GOowLc3haSavk,8536
|
|
8
8
|
meshcode/protocol_v2.py,sha256=Qi-Lj_fOwCupOEdh-bzQLXE7qlzW-2YCLN3yGEOfiPY,3517
|
|
9
|
-
meshcode/run_agent.py,sha256=
|
|
9
|
+
meshcode/run_agent.py,sha256=s4nccjkzOMYF-b0eT0RgNRRvFHbv7rgFPIViY01M3VM,11395
|
|
10
10
|
meshcode/secrets.py,sha256=IaYL2nP8jZzryjB36oM4JKc6lFMyUZYf5qhJwOzihEY,12663
|
|
11
|
-
meshcode/self_update.py,sha256=
|
|
12
|
-
meshcode/setup_clients.py,sha256=
|
|
11
|
+
meshcode/self_update.py,sha256=Id1O4s4UOuR9AnOD7IIquyizQcKH1cmkgjZuvrvqK00,11208
|
|
12
|
+
meshcode/setup_clients.py,sha256=OEwZz-ydtkVbRRxxyRCYx9nSb1fhCwaG6kvZEMEHllQ,28116
|
|
13
13
|
meshcode/meshcode_mcp/__init__.py,sha256=OdzyVP8YHG4BaSuUvVAOy7FgGp6og2vOVbyV_Iuz3pE,110
|
|
14
14
|
meshcode/meshcode_mcp/__main__.py,sha256=rrnTlA7ciA_C6b5QlbKlQli6O4vPEJRksCoeLIj8k1Q,1533
|
|
15
15
|
meshcode/meshcode_mcp/backend.py,sha256=gglWwJ47wt66LlyFitlmc2Bc6Xc-NYr814g7FBof0nE,14414
|
|
16
16
|
meshcode/meshcode_mcp/realtime.py,sha256=nglDB813psFAhEfUxp6vESH-CUD_T1mQzA9qCCRTPKU,9525
|
|
17
|
-
meshcode/meshcode_mcp/server.py,sha256=
|
|
17
|
+
meshcode/meshcode_mcp/server.py,sha256=wIdaLFPN_bZ01ojet44ye7JtEb4xSO6xSkYGv_ZEYPo,61428
|
|
18
18
|
meshcode/meshcode_mcp/test_backend.py,sha256=B-J__pStnN4vVBzYN4yz2CKRylwDz09ZJbj4RVUtpr0,3101
|
|
19
19
|
meshcode/meshcode_mcp/test_realtime.py,sha256=g2V3QzumdZgus4-b2Q-3bPFwX5Ib7BuzYah_vUrOAOU,3254
|
|
20
20
|
meshcode/meshcode_mcp/test_server_wrapper.py,sha256=ohbKkfN_k-ApElnnOxSx4_y5lWwUBM3urfm8JndOloM,4464
|
|
21
|
-
meshcode-2.4.
|
|
22
|
-
meshcode-2.4.
|
|
23
|
-
meshcode-2.4.
|
|
24
|
-
meshcode-2.4.
|
|
25
|
-
meshcode-2.4.
|
|
21
|
+
meshcode-2.4.2.dist-info/METADATA,sha256=jvhPUKsADXoYVcAmxFKxLsquGwylehA3kmZXNGWL7b8,15353
|
|
22
|
+
meshcode-2.4.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
23
|
+
meshcode-2.4.2.dist-info/entry_points.txt,sha256=-sX1FVOjMA-96Rt4GDCFCyFRGjAuYJSgXkBpWnDfShA,98
|
|
24
|
+
meshcode-2.4.2.dist-info/top_level.txt,sha256=M_cv_gd_8ojgYoHk9qOCaDmmX47ypozEOYKtQvm03H8,9
|
|
25
|
+
meshcode-2.4.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|