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 CHANGED
@@ -1,2 +1,2 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.4.0"
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
- with open(candidate) as f:
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
- rpc_result = sb_rpc("mc_register_agent", {
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
- proc = subprocess.Popen(
1393
- [sys.executable, "-c", code],
1394
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
1395
- start_new_session=True,
1396
- )
1397
- pid_file.write_text(str(proc.pid))
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
- sb_rpc("mc_register_agent", {
1431
- "p_project_id": project_id,
1432
- "p_name": name,
1433
- "p_role": role,
1434
- "p_status": "needs_setup",
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
- agent = sys.argv[2]
2061
- proj_override = flags.get("project")
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
- return secrets_mod.get_api_key(profile="default")
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
- project = redeem.get("meshwork")
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
- print("[meshcode] ERROR: redeem succeeded but response was incomplete", file=sys.stderr)
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
 
@@ -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: Dict[str, Any]) -> Dict[str, Any]:
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
- api_key = secrets_mod.get_api_key(profile="default")
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 auto-generated by meshcode setup"]
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.4.0
3
+ Version: 2.4.2
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,25 +1,25 @@
1
- meshcode/__init__.py,sha256=P1Cz8G5O-3maP25kkOKpt2HysDxZRijr-GrPlKdMjKU,84
2
- meshcode/cli.py,sha256=0_QKzkNr4VPwRyfEGQ7R_rb9--qxEGeZ8XNXqA4R9Tg,922
3
- meshcode/comms_v4.py,sha256=8vZ6Z6xE-jcLn12dvjs0F5VR78TfdAC5GKnOR_vdXEI,88041
4
- meshcode/invites.py,sha256=eltXeL1ycplsZXk8FRl4dAIwTvRq8wgowwIxHRehMWc,13530
5
- meshcode/launcher.py,sha256=tCMD8ExwtN2ogPqSJ7Xp2Dt6w-EpD9pg7zS0kM8eLLw,12095
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=f-yWVjyTfV2H3jZTDb2Mp7hD0A296P-idLDlbSk9OMg,11304
9
+ meshcode/run_agent.py,sha256=s4nccjkzOMYF-b0eT0RgNRRvFHbv7rgFPIViY01M3VM,11395
10
10
  meshcode/secrets.py,sha256=IaYL2nP8jZzryjB36oM4JKc6lFMyUZYf5qhJwOzihEY,12663
11
- meshcode/self_update.py,sha256=Exvzgv5WK1ygGkM6bAdzwP7_VPEl5RzhBaJmDKus9Ag,11156
12
- meshcode/setup_clients.py,sha256=McFscPisuS_t5bS31C1rV5_wRh7VmwAcMDuo1KJPl80,27950
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=JbP9Os5_57Na6KPm0Z0872MW0BUsxMBZQP2nqyMPx4A,61070
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.0.dist-info/METADATA,sha256=qBbRTwZRAU4OyEPbhNRxr_3jFjjXg__iy_8W27QxvQU,15353
22
- meshcode-2.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
23
- meshcode-2.4.0.dist-info/entry_points.txt,sha256=-sX1FVOjMA-96Rt4GDCFCyFRGjAuYJSgXkBpWnDfShA,98
24
- meshcode-2.4.0.dist-info/top_level.txt,sha256=M_cv_gd_8ojgYoHk9qOCaDmmX47ypozEOYKtQvm03H8,9
25
- meshcode-2.4.0.dist-info/RECORD,,
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,,