meshcode 2.10.46__tar.gz → 2.10.48__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {meshcode-2.10.46 → meshcode-2.10.48}/PKG-INFO +1 -1
  2. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/__init__.py +1 -1
  3. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/comms_v4.py +155 -41
  4. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/meshcode_mcp/backend.py +86 -15
  5. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/meshcode_mcp/realtime.py +1 -1
  6. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/meshcode_mcp/server.py +288 -7
  7. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/run_agent.py +47 -15
  8. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode.egg-info/PKG-INFO +1 -1
  9. {meshcode-2.10.46 → meshcode-2.10.48}/pyproject.toml +1 -1
  10. {meshcode-2.10.46 → meshcode-2.10.48}/README.md +0 -0
  11. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/ascii_art.py +0 -0
  12. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/cli.py +0 -0
  13. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/invites.py +0 -0
  14. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/launcher.py +0 -0
  15. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/launcher_install.py +0 -0
  16. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/meshcode_mcp/__init__.py +0 -0
  17. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/meshcode_mcp/__main__.py +0 -0
  18. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/meshcode_mcp/test_backend.py +0 -0
  19. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  20. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  21. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/preferences.py +0 -0
  22. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/protocol_v2.py +0 -0
  23. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/secrets.py +0 -0
  24. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/self_update.py +0 -0
  25. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode/setup_clients.py +0 -0
  26. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode.egg-info/SOURCES.txt +0 -0
  27. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode.egg-info/requires.txt +0 -0
  30. {meshcode-2.10.46 → meshcode-2.10.48}/meshcode.egg-info/top_level.txt +0 -0
  31. {meshcode-2.10.46 → meshcode-2.10.48}/setup.cfg +0 -0
  32. {meshcode-2.10.46 → meshcode-2.10.48}/tests/test_status_enum_coverage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.46
3
+ Version: 2.10.48
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,2 +1,2 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.10.46"
2
+ __version__ = "2.10.48"
@@ -640,11 +640,19 @@ def spawn_headless_agent(project, name, project_id, message_body, from_agent):
640
640
  # Flip status to 'working' so the dashboard reflects real-time activity.
641
641
  # We'll flip back to 'sleeping' (or 'online' if any reply went out) at the end.
642
642
  try:
643
- sb_update("mc_agents",
644
- f"project_id=eq.{project_id}&name=eq.{quote(name)}",
645
- {"status": "working", "task": f"headless: msg from {from_agent}",
643
+ _ak = _load_api_key_for_cli()
644
+ _fields = {"status": "working", "task": f"headless: msg from {from_agent}",
646
645
  "last_active_at": now_iso(), "last_heartbeat": now_iso(),
647
- "last_woken_at": now_iso()})
646
+ "last_woken_at": now_iso()}
647
+ if _ak:
648
+ _r = sb_rpc("mc_update_agent", {"p_api_key": _ak, "p_project_id": project_id,
649
+ "p_agent_name": name, "p_fields": _fields})
650
+ if not (_r and _r.get("ok")):
651
+ sb_update("mc_agents",
652
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}", _fields)
653
+ else:
654
+ sb_update("mc_agents",
655
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}", _fields)
648
656
  except Exception:
649
657
  pass
650
658
  try:
@@ -692,10 +700,18 @@ def spawn_headless_agent(project, name, project_id, message_body, from_agent):
692
700
  try:
693
701
  # Flip status back to 'sleeping' after the headless turn ends.
694
702
  # The agent is identity-only at rest; next message wakes it again.
695
- sb_update("mc_agents",
696
- f"project_id=eq.{project_id}&name=eq.{quote(name)}",
697
- {"status": "sleeping", "task": "Waiting for messages...",
698
- "last_active_at": now_iso(), "last_heartbeat": now_iso()})
703
+ _ak = _load_api_key_for_cli()
704
+ _fields = {"status": "sleeping", "task": "Waiting for messages...",
705
+ "last_active_at": now_iso(), "last_heartbeat": now_iso()}
706
+ if _ak:
707
+ _r = sb_rpc("mc_update_agent", {"p_api_key": _ak, "p_project_id": project_id,
708
+ "p_agent_name": name, "p_fields": _fields})
709
+ if not (_r and _r.get("ok")):
710
+ sb_update("mc_agents",
711
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}", _fields)
712
+ else:
713
+ sb_update("mc_agents",
714
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}", _fields)
699
715
  except Exception:
700
716
  pass
701
717
 
@@ -995,16 +1011,24 @@ def register(project, name, role=""):
995
1011
  return
996
1012
 
997
1013
  # Update tty/pid (RPC doesn't take these — patch separately)
998
- sb_update("mc_agents",
999
- f"project_id=eq.{project_id}&name=eq.{quote(name)}",
1000
- {"tty": tty, "pid": ppid, "task": role, "last_heartbeat": now_iso()})
1014
+ _fields_reg = {"tty": tty, "pid": ppid, "task": role, "last_heartbeat": now_iso()}
1015
+ _r_upd = sb_rpc("mc_update_agent", {"p_api_key": _api_key, "p_project_id": project_id,
1016
+ "p_agent_name": name, "p_fields": _fields_reg})
1017
+ if not (_r_upd and _r_upd.get("ok")):
1018
+ sb_update("mc_agents",
1019
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}", _fields_reg)
1001
1020
 
1002
1021
  # Save local session file (for nudge TTY detection)
1003
1022
  session_data = {"project": project, "agent": name, "pid": ppid, "tty": tty, "registered_at": now()}
1004
1023
  (SESSIONS_DIR / f"{project}_{name}").write_text(json.dumps(session_data), encoding="utf-8")
1005
1024
 
1006
1025
  # Get all agents in project
1007
- agents = sb_select("mc_agents", f"project_id=eq.{project_id}", order="registered_at.asc")
1026
+ _r_agents = sb_rpc("mc_get_agents", {"p_api_key": _api_key, "p_project_id": project_id,
1027
+ "p_agent_name": None, "p_select": None, "p_limit": None})
1028
+ if _r_agents and _r_agents.get("ok"):
1029
+ agents = _r_agents.get("agents", [])
1030
+ else:
1031
+ agents = sb_select("mc_agents", f"project_id=eq.{project_id}", order="registered_at.asc")
1008
1032
  agent_names = [a["name"] for a in agents]
1009
1033
 
1010
1034
  # Count pending messages
@@ -1044,7 +1068,15 @@ def _resolve_agent_name(project_id, name):
1044
1068
  """Resolve partial agent name to full registered name."""
1045
1069
  if not name or name == "*":
1046
1070
  return name
1047
- agents = sb_select("mc_agents", f"project_id=eq.{project_id}", limit=50)
1071
+ _ak = _load_api_key_for_cli()
1072
+ agents = None
1073
+ if _ak:
1074
+ _r = sb_rpc("mc_get_agents", {"p_api_key": _ak, "p_project_id": project_id,
1075
+ "p_agent_name": None, "p_select": ["name"], "p_limit": 50})
1076
+ if _r and _r.get("ok"):
1077
+ agents = _r.get("agents", [])
1078
+ if agents is None:
1079
+ agents = sb_select("mc_agents", f"project_id=eq.{project_id}", limit=50)
1048
1080
  if not agents:
1049
1081
  return name
1050
1082
  all_names = [a["name"] for a in agents]
@@ -1107,8 +1139,17 @@ def send_msg(project, from_agent, to_agent, content, msg_type="msg", compact=Fal
1107
1139
  # and forth and burn tokens forever.
1108
1140
  if msg_type not in ("ack", "warning") and from_agent != "system":
1109
1141
  if not _is_pure_ack_payload(payload):
1110
- agents = sb_select("mc_agents",
1111
- f"project_id=eq.{project_id}&name=eq.{quote(to_agent)}")
1142
+ _ak = _load_api_key_for_cli()
1143
+ agents = None
1144
+ if _ak:
1145
+ _r = sb_rpc("mc_get_agents", {"p_api_key": _ak, "p_project_id": project_id,
1146
+ "p_agent_name": to_agent, "p_select": ["name", "status"],
1147
+ "p_limit": 1})
1148
+ if _r and _r.get("ok"):
1149
+ agents = _r.get("agents", [])
1150
+ if agents is None:
1151
+ agents = sb_select("mc_agents",
1152
+ f"project_id=eq.{project_id}&name=eq.{quote(to_agent)}")
1112
1153
  if agents and agents[0].get("status") not in ("offline", "killed"):
1113
1154
  nudge_agent(project, to_agent, from_agent)
1114
1155
  else:
@@ -1119,7 +1160,15 @@ def broadcast(project, from_agent, content, msg_type="broadcast"):
1119
1160
  project_id = get_project_id(project)
1120
1161
  if not project_id:
1121
1162
  return
1122
- agents = sb_select("mc_agents", f"project_id=eq.{project_id}")
1163
+ _ak = _load_api_key_for_cli()
1164
+ agents = None
1165
+ if _ak:
1166
+ _r = sb_rpc("mc_get_agents", {"p_api_key": _ak, "p_project_id": project_id,
1167
+ "p_agent_name": None, "p_select": ["name"], "p_limit": None})
1168
+ if _r and _r.get("ok"):
1169
+ agents = _r.get("agents", [])
1170
+ if agents is None:
1171
+ agents = sb_select("mc_agents", f"project_id=eq.{project_id}")
1123
1172
  count = 0
1124
1173
  for agent in agents:
1125
1174
  if agent["name"] != from_agent:
@@ -1191,8 +1240,17 @@ def inbox(project, name):
1191
1240
  return
1192
1241
 
1193
1242
  # Agent status
1194
- agent_rows = sb_select("mc_agents",
1195
- f"project_id=eq.{project_id}&name=eq.{quote(name)}", limit=1)
1243
+ _ak = _load_api_key_for_cli()
1244
+ agent_rows = None
1245
+ if _ak:
1246
+ _r = sb_rpc("mc_get_agents", {"p_api_key": _ak, "p_project_id": project_id,
1247
+ "p_agent_name": name, "p_select": ["name", "status", "task"],
1248
+ "p_limit": 1})
1249
+ if _r and _r.get("ok"):
1250
+ agent_rows = _r.get("agents", [])
1251
+ if agent_rows is None:
1252
+ agent_rows = sb_select("mc_agents",
1253
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}", limit=1)
1196
1254
  if agent_rows:
1197
1255
  a = agent_rows[0]
1198
1256
  print(f"[{project}] {name} | status: {a.get('status', '?')} | task: {a.get('task', '-')}")
@@ -1242,9 +1300,16 @@ def hook_check():
1242
1300
  return
1243
1301
 
1244
1302
  # Always heartbeat — keeps agent alive on dashboard
1245
- sb_update("mc_agents",
1246
- f"project_id=eq.{project_id}&name=eq.{quote(agent)}",
1247
- {"status": "working", "last_heartbeat": now_iso()})
1303
+ _ak = _load_api_key_for_cli()
1304
+ _hb_fields = {"status": "working", "last_heartbeat": now_iso()}
1305
+ _hb_ok = False
1306
+ if _ak:
1307
+ _r = sb_rpc("mc_update_agent", {"p_api_key": _ak, "p_project_id": project_id,
1308
+ "p_agent_name": agent, "p_fields": _hb_fields})
1309
+ _hb_ok = bool(_r and _r.get("ok"))
1310
+ if not _hb_ok:
1311
+ sb_update("mc_agents",
1312
+ f"project_id=eq.{project_id}&name=eq.{quote(agent)}", _hb_fields)
1248
1313
 
1249
1314
  # Check for unread non-ack messages
1250
1315
  pending = sb_select("mc_messages",
@@ -1268,14 +1333,24 @@ def watch(project, name, interval=10, timeout=0):
1268
1333
  if not project_id:
1269
1334
  return
1270
1335
 
1336
+ _ak = _load_api_key_for_cli()
1337
+
1338
+ def _update_agent_status(fields):
1339
+ """Try RPC first, fall back to direct PostgREST."""
1340
+ if _ak:
1341
+ _r = sb_rpc("mc_update_agent", {"p_api_key": _ak, "p_project_id": project_id,
1342
+ "p_agent_name": name, "p_fields": fields})
1343
+ if _r and _r.get("ok"):
1344
+ return
1345
+ sb_update("mc_agents",
1346
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}", fields)
1347
+
1271
1348
  import signal
1272
1349
  _interrupted = [False]
1273
1350
  def handle_interrupt(signum, frame):
1274
1351
  _interrupted[0] = True
1275
1352
  print(f"\n[{project}] {name}: Watch interrumpido por el usuario.")
1276
- sb_update("mc_agents",
1277
- f"project_id=eq.{project_id}&name=eq.{quote(name)}",
1278
- {"status": "online", "task": "Listening to user", "last_heartbeat": now_iso()})
1353
+ _update_agent_status({"status": "online", "task": "Listening to user", "last_heartbeat": now_iso()})
1279
1354
  sys.exit(0)
1280
1355
  try:
1281
1356
  signal.signal(signal.SIGINT, handle_interrupt)
@@ -1283,9 +1358,7 @@ def watch(project, name, interval=10, timeout=0):
1283
1358
  pass # Non-main thread (Windows restriction)
1284
1359
 
1285
1360
  # Update status to standby
1286
- sb_update("mc_agents",
1287
- f"project_id=eq.{project_id}&name=eq.{quote(name)}",
1288
- {"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
1361
+ _update_agent_status({"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
1289
1362
 
1290
1363
  cycle = timeout if timeout > 0 else 600
1291
1364
  print(f"[{project}] {name} en watch — poll cada {interval}s, ciclo {cycle}s (auto-loop)")
@@ -1303,15 +1376,11 @@ def watch(project, name, interval=10, timeout=0):
1303
1376
  print(f"\n*** [{project.upper()}] MENSAJE RECIBIDO ({elapsed}s en standby) ***")
1304
1377
  read_messages(project, name, silent=False, send_acks=False)
1305
1378
 
1306
- sb_update("mc_agents",
1307
- f"project_id=eq.{project_id}&name=eq.{quote(name)}",
1308
- {"status": "working", "task": "Procesando mensaje recibido", "last_heartbeat": now_iso()})
1379
+ _update_agent_status({"status": "working", "task": "Procesando mensaje recibido", "last_heartbeat": now_iso()})
1309
1380
 
1310
1381
  # Reset to standby and keep polling (don't exit the loop)
1311
1382
  time.sleep(2)
1312
- sb_update("mc_agents",
1313
- f"project_id=eq.{project_id}&name=eq.{quote(name)}",
1314
- {"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
1383
+ _update_agent_status({"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
1315
1384
  continue
1316
1385
 
1317
1386
  # Heartbeat
@@ -1320,9 +1389,7 @@ def watch(project, name, interval=10, timeout=0):
1320
1389
 
1321
1390
  # Cycle ended without messages — check user context, then AUTO-RESTART watch
1322
1391
  print(f"[{project}] No messages this cycle. Watch active, still running. Standby.")
1323
- sb_update("mc_agents",
1324
- f"project_id=eq.{project_id}&name=eq.{quote(name)}",
1325
- {"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
1392
+ _update_agent_status({"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
1326
1393
 
1327
1394
 
1328
1395
  def update_board(project, name, status, task=""):
@@ -1347,8 +1414,15 @@ def update_board(project, name, status, task=""):
1347
1414
  if task:
1348
1415
  updates["task"] = task
1349
1416
 
1350
- sb_update("mc_agents",
1351
- f"project_id=eq.{project_id}&name=eq.{quote(name)}", updates)
1417
+ _ak = _load_api_key_for_cli()
1418
+ _ub_ok = False
1419
+ if _ak:
1420
+ _r = sb_rpc("mc_update_agent", {"p_api_key": _ak, "p_project_id": project_id,
1421
+ "p_agent_name": name, "p_fields": updates})
1422
+ _ub_ok = bool(_r and _r.get("ok"))
1423
+ if not _ub_ok:
1424
+ sb_update("mc_agents",
1425
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}", updates)
1352
1426
  print(f"[{project}] {name}: {status} — {task}")
1353
1427
 
1354
1428
 
@@ -1358,7 +1432,15 @@ def show_board(project):
1358
1432
  print(f"[{project}] Project not found")
1359
1433
  return
1360
1434
 
1361
- agents = sb_select("mc_agents", f"project_id=eq.{project_id}", order="registered_at.asc")
1435
+ _ak = _load_api_key_for_cli()
1436
+ agents = None
1437
+ if _ak:
1438
+ _r = sb_rpc("mc_get_agents", {"p_api_key": _ak, "p_project_id": project_id,
1439
+ "p_agent_name": None, "p_select": None, "p_limit": None})
1440
+ if _r and _r.get("ok"):
1441
+ agents = _r.get("agents", [])
1442
+ if agents is None:
1443
+ agents = sb_select("mc_agents", f"project_id=eq.{project_id}", order="registered_at.asc")
1362
1444
  if not agents:
1363
1445
  print(f"[{project}] Sin agentes")
1364
1446
  return
@@ -1395,7 +1477,14 @@ def show_status(project=None):
1395
1477
  print(f"{'='*60}")
1396
1478
 
1397
1479
  for proj in projects_data:
1398
- agents = sb_select("mc_agents", f"project_id=eq.{proj['id']}", order="name.asc")
1480
+ agents = None
1481
+ if api_key:
1482
+ _r = sb_rpc("mc_get_agents", {"p_api_key": api_key, "p_project_id": proj['id'],
1483
+ "p_agent_name": None, "p_select": None, "p_limit": None})
1484
+ if _r and _r.get("ok"):
1485
+ agents = _r.get("agents", [])
1486
+ if agents is None:
1487
+ agents = sb_select("mc_agents", f"project_id=eq.{proj['id']}", order="name.asc")
1399
1488
  if not agents:
1400
1489
  continue
1401
1490
 
@@ -1494,7 +1583,14 @@ def unregister(project, name):
1494
1583
  project_id = get_project_id(project)
1495
1584
  if not project_id:
1496
1585
  return
1497
- sb_delete("mc_agents", f"project_id=eq.{project_id}&name=eq.{quote(name)}")
1586
+ _ak = _load_api_key_for_cli()
1587
+ _del_ok = False
1588
+ if _ak:
1589
+ _r = sb_rpc("mc_delete_agent", {"p_api_key": _ak, "p_project_id": project_id,
1590
+ "p_agent_name": name})
1591
+ _del_ok = bool(_r and _r.get("ok"))
1592
+ if not _del_ok:
1593
+ sb_delete("mc_agents", f"project_id=eq.{project_id}&name=eq.{quote(name)}")
1498
1594
 
1499
1595
  # Clean local session
1500
1596
  session_file = SESSIONS_DIR / f"{project}_{name}"
@@ -1570,17 +1666,30 @@ def _start_heartbeat_daemon(project, name):
1570
1666
  except Exception:
1571
1667
  pass
1572
1668
 
1669
+ _ak = _load_api_key_for_cli()
1573
1670
  code = (
1574
1671
  "import os,sys,time,json,urllib.request\n"
1575
1672
  f"project={project!r}; name={name!r}\n"
1576
1673
  f"url={SUPABASE_URL!r}; key={SUPABASE_KEY!r}\n"
1674
+ f"api_key={_ak!r}\n"
1577
1675
  "import urllib.parse\n"
1578
1676
  "def post(path, body):\n"
1579
1677
  " req=urllib.request.Request(url+path, data=json.dumps(body).encode(),\n"
1580
1678
  " headers={'apikey':key,'Authorization':'Bearer '+key,'Content-Type':'application/json','Accept-Profile':'meshcode','Content-Profile':'meshcode'}, method='POST')\n"
1581
1679
  " try: urllib.request.urlopen(req,timeout=10).read()\n"
1582
1680
  " except Exception: pass\n"
1681
+ "def rpc(fn, params):\n"
1682
+ " req=urllib.request.Request(url+'/rest/v1/rpc/'+fn, data=json.dumps(params).encode(),\n"
1683
+ " headers={'apikey':key,'Authorization':'Bearer '+key,'Content-Type':'application/json'}, method='POST')\n"
1684
+ " try:\n"
1685
+ " d=json.loads(urllib.request.urlopen(req,timeout=10).read())\n"
1686
+ " return d\n"
1687
+ " except Exception: return None\n"
1583
1688
  "def get_pid_for_project():\n"
1689
+ " if api_key:\n"
1690
+ " r=rpc('mc_resolve_project',{'p_api_key':api_key,'p_project_name':project})\n"
1691
+ " if isinstance(r,dict) and r.get('project_id'): return r['project_id']\n"
1692
+ " if isinstance(r,list) and r and r[0].get('project_id'): return r[0]['project_id']\n"
1584
1693
  " req=urllib.request.Request(url+'/rest/v1/mc_projects?select=id&name=eq.'+urllib.parse.quote(project), headers={'apikey':key,'Authorization':'Bearer '+key,'Accept-Profile':'meshcode'})\n"
1585
1694
  " try:\n"
1586
1695
  " d=json.loads(urllib.request.urlopen(req,timeout=10).read())\n"
@@ -1588,6 +1697,11 @@ def _start_heartbeat_daemon(project, name):
1588
1697
  " except Exception: return None\n"
1589
1698
  "def check_still_leased(pid):\n"
1590
1699
  " \"\"\"Return False if force-disconnected (instance_id cleared).\"\"\"\n"
1700
+ " if api_key:\n"
1701
+ " r=rpc('mc_get_agents',{'p_api_key':api_key,'p_project_id':pid,'p_agent_name':name,'p_select':['instance_id'],'p_limit':1})\n"
1702
+ " if r and isinstance(r,dict) and r.get('ok'):\n"
1703
+ " agents=r.get('agents',[])\n"
1704
+ " return bool(agents and agents[0].get('instance_id'))\n"
1591
1705
  " try:\n"
1592
1706
  " req=urllib.request.Request(url+'/rest/v1/mc_agents?select=instance_id&project_id=eq.'+pid+'&name=eq.'+urllib.parse.quote(name),\n"
1593
1707
  " headers={'apikey':key,'Authorization':'Bearer '+key,'Accept-Profile':'meshcode'})\n"
@@ -334,7 +334,13 @@ _recording_session_id = ""
334
334
 
335
335
  # RPCs that should NOT be recorded (internal/heartbeat/recording itself)
336
336
  _SKIP_RECORDING = {"mc_heartbeat", "mc_record_event", "mc_agent_set_status_by_api_key",
337
- "mc_acquire_agent_lease", "mc_release_agent_lease"}
337
+ "mc_acquire_agent_lease", "mc_release_agent_lease",
338
+ "mc_update_agent", "mc_get_agents"}
339
+
340
+
341
+ def _get_api_key() -> Optional[str]:
342
+ """Return the current agent's API key (set at boot via enable_recording)."""
343
+ return _recording_api_key or os.environ.get("MESHCODE_API_KEY") or None
338
344
 
339
345
 
340
346
  def enable_recording(api_key: str, project_id: str, agent_name: str, session_id: str):
@@ -462,9 +468,23 @@ def register_agent(project: str, name: str, role: str = "", api_key: Optional[st
462
468
  if not result or (isinstance(result, dict) and result.get("error")):
463
469
  return result or {"error": "Failed to register agent"}
464
470
 
465
- sb_update("mc_agents",
466
- f"project_id=eq.{project_id}&name=eq.{quote(name)}",
467
- {"task": role, "last_heartbeat": _now_iso()})
471
+ # Prefer SECURITY DEFINER RPC for mc_agents update
472
+ _ak = api_key or _get_api_key()
473
+ _agent_updated = False
474
+ if _ak:
475
+ _upd = sb_rpc("mc_update_agent", {
476
+ "p_api_key": _ak,
477
+ "p_project_id": project_id,
478
+ "p_agent_name": name,
479
+ "p_fields": {"task": role, "last_heartbeat": _now_iso()},
480
+ })
481
+ if isinstance(_upd, dict) and _upd.get("ok"):
482
+ _agent_updated = True
483
+ if not _agent_updated:
484
+ # Fallback: direct PATCH (gradual rollout)
485
+ sb_update("mc_agents",
486
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}",
487
+ {"task": role, "last_heartbeat": _now_iso()})
468
488
 
469
489
  return {
470
490
  "registered": True,
@@ -522,15 +542,42 @@ def resolve_agent_name(project_id: str, name: str) -> str:
522
542
  cached = _agent_name_cache_get(project_id, name)
523
543
  if cached is not None:
524
544
  return cached
525
- # Exact match — fast path
526
- exact = sb_select("mc_agents",
527
- f"project_id=eq.{project_id}&name=eq.{quote(name)}",
528
- limit=1)
545
+ # Exact match — fast path (prefer SECURITY DEFINER RPC)
546
+ api_key = _get_api_key()
547
+ exact = None
548
+ if api_key:
549
+ _res = sb_rpc("mc_get_agents", {
550
+ "p_api_key": api_key,
551
+ "p_project_id": project_id,
552
+ "p_agent_name": name,
553
+ "p_select": None,
554
+ "p_limit": 1,
555
+ })
556
+ if isinstance(_res, dict) and _res.get("ok"):
557
+ exact = _res.get("agents", [])
558
+ if exact is None:
559
+ # Fallback: direct SELECT (gradual rollout)
560
+ exact = sb_select("mc_agents",
561
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}",
562
+ limit=1)
529
563
  if exact:
530
564
  _agent_name_cache_set(project_id, name, name)
531
565
  return name
532
566
  # Fuzzy: prefix or substring match among registered agents
533
- agents = sb_select("mc_agents", f"project_id=eq.{project_id}&select=name")
567
+ agents = None
568
+ if api_key:
569
+ _res2 = sb_rpc("mc_get_agents", {
570
+ "p_api_key": api_key,
571
+ "p_project_id": project_id,
572
+ "p_agent_name": None,
573
+ "p_select": ["name"],
574
+ "p_limit": None,
575
+ })
576
+ if isinstance(_res2, dict) and _res2.get("ok"):
577
+ agents = _res2.get("agents", [])
578
+ if agents is None:
579
+ # Fallback: direct SELECT (gradual rollout)
580
+ agents = sb_select("mc_agents", f"project_id=eq.{project_id}&select=name")
534
581
  if not agents:
535
582
  return name
536
583
  all_names = [a["name"] for a in agents]
@@ -845,7 +892,20 @@ def decrypt_payload(encrypted_b64: str, hex_key: str, aad: Optional[str] = None)
845
892
  return None
846
893
 
847
894
 
848
- def get_board(project_id: str) -> List[Dict]:
895
+ def get_board(project_id: str, api_key: Optional[str] = None) -> List[Dict]:
896
+ # Prefer SECURITY DEFINER RPC
897
+ _ak = api_key or _get_api_key()
898
+ if _ak:
899
+ _res = sb_rpc("mc_get_agents", {
900
+ "p_api_key": _ak,
901
+ "p_project_id": project_id,
902
+ "p_agent_name": None,
903
+ "p_select": ["name", "role", "status", "task", "last_heartbeat", "registered_at"],
904
+ "p_limit": None,
905
+ })
906
+ if isinstance(_res, dict) and _res.get("ok"):
907
+ return _res.get("agents", [])
908
+ # Fallback: direct SELECT (gradual rollout)
849
909
  return sb_select(
850
910
  "mc_agents",
851
911
  f"project_id=eq.{project_id}&select=name,role,status,task,last_heartbeat,registered_at",
@@ -875,13 +935,24 @@ def set_status(project_id: str, agent: str, status: str, task: str = "", api_key
875
935
  return {"ok": True, "status": status}
876
936
  # Fall through to direct PATCH if RPC doesn't exist yet
877
937
 
878
- # Fallback: direct PATCH (may silently fail with anon key if RLS blocks)
879
- updates = {"status": status, "last_heartbeat": _now_iso()}
938
+ # Fallback: try mc_update_agent RPC, then direct PATCH
939
+ _ak = api_key or _get_api_key()
940
+ fields: Dict[str, Any] = {"status": status, "last_heartbeat": _now_iso()}
880
941
  if task:
881
- updates["task"] = task
942
+ fields["task"] = task
882
943
  if editor:
883
- updates["editor"] = editor
884
- result = sb_update("mc_agents", f"project_id=eq.{project_id}&name=eq.{quote(agent)}", updates)
944
+ fields["editor"] = editor
945
+ if _ak:
946
+ _upd = sb_rpc("mc_update_agent", {
947
+ "p_api_key": _ak,
948
+ "p_project_id": project_id,
949
+ "p_agent_name": agent,
950
+ "p_fields": fields,
951
+ })
952
+ if isinstance(_upd, dict) and _upd.get("ok"):
953
+ return {"ok": True, "status": status}
954
+ # Final fallback: direct PATCH (may silently fail with anon key if RLS blocks)
955
+ result = sb_update("mc_agents", f"project_id=eq.{project_id}&name=eq.{quote(agent)}", fields)
885
956
  if isinstance(result, dict) and result.get("_error"):
886
957
  return {"error": result["_error"]}
887
958
  return {"ok": True, "status": status}
@@ -143,7 +143,7 @@ class RealtimeListener:
143
143
  # can hang indefinitely behind a corporate firewall / DNS stall, which
144
144
  # blocks the MCP lifespan startup -> Claude Code times out the spawn
145
145
  # -> "Failed to reconnect".
146
- connect_kwargs = {"ping_interval": 20, "ping_timeout": 10}
146
+ connect_kwargs = {"ping_interval": 20, "ping_timeout": 15}
147
147
  if _SSL_CTX is not None and self.ws_url.startswith("wss://"):
148
148
  connect_kwargs["ssl"] = _SSL_CTX
149
149
  ws = await asyncio.wait_for(
@@ -118,6 +118,72 @@ _IN_WAIT = False # True while meshcode_wait is blocking
118
118
  # the MCP server side (useful if comms_v4 nudge doesn't reach the agent).
119
119
  _AUTO_WAKE = os.environ.get("MESHCODE_AUTO_WAKE", "0").lower() in ("1", "true", "yes")
120
120
 
121
+ # ============================================================
122
+ # PID lockfile: prevent zombie MCP processes from accumulating.
123
+ # When Claude Code respawns the MCP server, the old process may
124
+ # still be alive. We write our PID to a lockfile on boot and
125
+ # kill any stale process found there first.
126
+ # ============================================================
127
+ import signal as _signal
128
+ import tempfile as _tempfile
129
+
130
+
131
+ def _pid_lockfile_path() -> str:
132
+ """Return path to the PID lockfile for this agent."""
133
+ agent = os.environ.get("MESHCODE_AGENT", "unknown")
134
+ project = os.environ.get("MESHCODE_PROJECT", "unknown")
135
+ safe_name = f"meshcode_mcp_{project}_{agent}.pid".replace("/", "_").replace(" ", "_")
136
+ return os.path.join(_tempfile.gettempdir(), safe_name)
137
+
138
+
139
+ def _kill_stale_mcp_process() -> None:
140
+ """Kill any stale MCP process for this agent found in the lockfile."""
141
+ lockfile = _pid_lockfile_path()
142
+ if not os.path.exists(lockfile):
143
+ return
144
+ try:
145
+ with open(lockfile, "r") as f:
146
+ old_pid = int(f.read().strip())
147
+ if old_pid == os.getpid():
148
+ return # It's us
149
+ # Check if process is alive
150
+ os.kill(old_pid, 0) # Signal 0 = existence check, no actual signal
151
+ # Process is alive — kill it gracefully, then forcefully
152
+ _mc_log(f"Killing stale MCP process (PID {old_pid})")
153
+ os.kill(old_pid, _signal.SIGTERM)
154
+ import time
155
+ time.sleep(1)
156
+ try:
157
+ os.kill(old_pid, 0) # Still alive?
158
+ os.kill(old_pid, _signal.SIGKILL)
159
+ _mc_log(f"Force-killed stale MCP process (PID {old_pid})")
160
+ except OSError:
161
+ pass # Already dead after SIGTERM
162
+ except (ValueError, FileNotFoundError):
163
+ pass # Corrupt or missing lockfile
164
+ except OSError:
165
+ pass # Process not found (already dead)
166
+
167
+
168
+ def _write_pid_lockfile() -> None:
169
+ """Write current PID to lockfile."""
170
+ lockfile = _pid_lockfile_path()
171
+ try:
172
+ with open(lockfile, "w") as f:
173
+ f.write(str(os.getpid()))
174
+ except Exception as e:
175
+ _mc_log(f"Warning: couldn't write PID lockfile: {e}", level="warn")
176
+
177
+
178
+ def _remove_pid_lockfile() -> None:
179
+ """Remove PID lockfile on shutdown."""
180
+ lockfile = _pid_lockfile_path()
181
+ try:
182
+ if os.path.exists(lockfile):
183
+ os.remove(lockfile)
184
+ except Exception:
185
+ pass
186
+
121
187
 
122
188
  def _try_auto_wake(from_agent: str, preview: str) -> None:
123
189
  """Inject a nudge into the terminal ONLY when agent is truly sleeping.
@@ -1362,6 +1428,10 @@ async def lifespan(_app):
1362
1428
  global _REALTIME, _MAIN_LOOP
1363
1429
  _MAIN_LOOP = asyncio.get_running_loop()
1364
1430
 
1431
+ # Kill any stale MCP process for this agent before acquiring lease
1432
+ _kill_stale_mcp_process()
1433
+ _write_pid_lockfile()
1434
+
1365
1435
  import platform as _pl
1366
1436
  if _pl.system() == "Windows":
1367
1437
  log.info(
@@ -1431,6 +1501,10 @@ async def lifespan(_app):
1431
1501
  _heartbeat_stop.set()
1432
1502
  except Exception:
1433
1503
  pass
1504
+ try:
1505
+ _remove_pid_lockfile()
1506
+ except Exception:
1507
+ pass
1434
1508
  try:
1435
1509
  _record_event_bg("shutdown", {"agent": AGENT_NAME, "session_id": _SESSION_ID})
1436
1510
  except Exception:
@@ -2068,9 +2142,14 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
2068
2142
  woke = await asyncio.shield(
2069
2143
  _REALTIME.wait_for_message(timeout=float(actual_timeout))
2070
2144
  )
2071
- except (asyncio.CancelledError, Exception) as _cancel_exc:
2072
- log.debug(f"[meshcode] wait_for_message cancelled/failed: {type(_cancel_exc).__name__}")
2145
+ except asyncio.CancelledError:
2146
+ log.debug("[meshcode] wait_for_message cancelled by ESC")
2073
2147
  return {"timed_out": True, "reason": "cancelled_by_client"}
2148
+ except Exception as _wait_exc:
2149
+ # Non-cancellation errors (e.g. connection dropped) — log and
2150
+ # fall through to DB fallback instead of masking the error.
2151
+ log.warning(f"[meshcode] wait_for_message error: {type(_wait_exc).__name__}: {_wait_exc}")
2152
+ woke = False
2074
2153
  if woke:
2075
2154
  buffered = _REALTIME.drain()
2076
2155
  if buffered:
@@ -2174,13 +2253,16 @@ def meshcode_done(reason: str) -> Dict[str, Any]:
2174
2253
 
2175
2254
  @mcp.tool()
2176
2255
  @with_working_status
2177
- def meshcode_check(include_acks: bool = False, since: Optional[str] = None) -> Dict[str, Any]:
2256
+ def meshcode_check(include_acks: bool = False, since: Optional[str] = None, mark_read: bool = False) -> Dict[str, Any]:
2178
2257
  """Peek at inbox (non-destructive). Returns pending count + new messages.
2179
2258
 
2180
2259
  Args:
2181
2260
  include_acks: Include ack messages in response.
2182
2261
  since: ISO-8601 timestamp. Only return messages newer than this.
2183
2262
  Use meshcode_remember("last_seen", ts) to persist across sessions.
2263
+ mark_read: When True, consume messages (mark as read in DB) instead of
2264
+ just peeking. Useful during boot when meshcode_wait() refuses
2265
+ to run because of open tasks.
2184
2266
  """
2185
2267
  global _LAST_SEEN_TS
2186
2268
  pending = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=_get_api_key())
@@ -2190,11 +2272,9 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None) -> D
2190
2272
  deduped = [m for m in realtime_buffered if _seen_key(m) not in _SEEN_MSG_IDS]
2191
2273
 
2192
2274
  # Fallback: if realtime buffer is empty but DB has pending messages,
2193
- # fetch them from the DB. Use mark_read=False so meshcode_check is a
2194
- # non-destructive peek — messages stay pending until meshcode_wait or
2195
- # meshcode_read consumes them.
2275
+ # fetch them from the DB. mark_read controls whether we consume or peek.
2196
2276
  if not deduped and pending > 0:
2197
- raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=False, api_key=_get_api_key())
2277
+ raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=mark_read, api_key=_get_api_key())
2198
2278
  deduped = [
2199
2279
  {
2200
2280
  "from": m["from_agent"],
@@ -2213,6 +2293,14 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None) -> D
2213
2293
  if effective_since:
2214
2294
  deduped = [m for m in deduped if m.get("ts") and str(m["ts"]) > effective_since]
2215
2295
 
2296
+ # When mark_read=True, update tracking state so messages aren't re-processed
2297
+ if mark_read and deduped:
2298
+ for m in deduped:
2299
+ _SEEN_MSG_IDS.add(_seen_key(m))
2300
+ latest_ts = max((str(m.get("ts", "")) for m in deduped), default=None)
2301
+ if latest_ts and (not _LAST_SEEN_TS or latest_ts > _LAST_SEEN_TS):
2302
+ _LAST_SEEN_TS = latest_ts
2303
+
2216
2304
  split = _split_messages(deduped)
2217
2305
  if not include_acks:
2218
2306
  split["acks"] = []
@@ -2489,6 +2577,199 @@ def meshcode_task_reassign(task_id: str, new_assignee: str) -> Dict[str, Any]:
2489
2577
  })
2490
2578
 
2491
2579
 
2580
+ # ----------------- CROSS-MESH TASKS -----------------
2581
+
2582
+ @mcp.tool()
2583
+ @with_working_status
2584
+ def meshcode_create_cross_mesh_task(target_mesh: str, title: str,
2585
+ description: str = "",
2586
+ assignee: Optional[str] = None,
2587
+ priority: str = "normal") -> Dict[str, Any]:
2588
+ """Create a task in a linked meshwork (requires active mesh link).
2589
+
2590
+ Args:
2591
+ target_mesh: Name of the target meshwork.
2592
+ title: Task title.
2593
+ description: Task description.
2594
+ assignee: Agent name in target mesh to assign to.
2595
+ priority: low / normal / high / urgent.
2596
+ """
2597
+ return be.sb_rpc("mc_create_cross_mesh_task", {
2598
+ "p_api_key": _get_api_key(),
2599
+ "p_target_mesh": target_mesh,
2600
+ "p_title": title,
2601
+ "p_description": description,
2602
+ "p_assignee": assignee,
2603
+ "p_priority": priority,
2604
+ "p_agent_name": AGENT_NAME,
2605
+ })
2606
+
2607
+
2608
+ # ----------------- GOALS -----------------
2609
+
2610
+ @mcp.tool()
2611
+ @with_working_status
2612
+ def meshcode_set_goal(title: str, description: str = "",
2613
+ priority: str = "normal", assignee: Optional[str] = None,
2614
+ target_date: Optional[str] = None,
2615
+ parent_id: Optional[str] = None) -> Dict[str, Any]:
2616
+ """Create a new goal for the meshwork.
2617
+
2618
+ Args:
2619
+ title: Goal title.
2620
+ description: Optional detailed description.
2621
+ priority: low / normal / high / urgent.
2622
+ assignee: Agent name to assign the goal to.
2623
+ target_date: Target date (YYYY-MM-DD format).
2624
+ parent_id: UUID of parent goal for sub-goals.
2625
+ """
2626
+ return be.sb_rpc("mc_create_goal", {
2627
+ "p_api_key": _get_api_key(),
2628
+ "p_title": title,
2629
+ "p_description": description,
2630
+ "p_priority": priority,
2631
+ "p_assignee": assignee,
2632
+ "p_target_date": target_date,
2633
+ "p_parent_id": parent_id,
2634
+ "p_agent_name": AGENT_NAME,
2635
+ })
2636
+
2637
+
2638
+ @mcp.tool()
2639
+ @with_working_status
2640
+ def meshcode_goals() -> Dict[str, Any]:
2641
+ """List all goals for this meshwork."""
2642
+ return be.sb_rpc("mc_get_goals", {
2643
+ "p_api_key": _get_api_key(),
2644
+ })
2645
+
2646
+
2647
+ @mcp.tool()
2648
+ @with_working_status
2649
+ def meshcode_update_goal(goal_id: str, status: Optional[str] = None,
2650
+ title: Optional[str] = None,
2651
+ description: Optional[str] = None,
2652
+ priority: Optional[str] = None,
2653
+ assignee: Optional[str] = None) -> Dict[str, Any]:
2654
+ """Update a goal's status or details.
2655
+
2656
+ Args:
2657
+ goal_id: UUID of the goal to update.
2658
+ status: active / in_progress / achieved / abandoned.
2659
+ title: New title (optional).
2660
+ description: New description (optional).
2661
+ priority: New priority (optional).
2662
+ assignee: New assignee (optional).
2663
+ """
2664
+ return be.sb_rpc("mc_update_goal", {
2665
+ "p_api_key": _get_api_key(),
2666
+ "p_goal_id": goal_id,
2667
+ "p_status": status,
2668
+ "p_title": title,
2669
+ "p_description": description,
2670
+ "p_priority": priority,
2671
+ "p_assignee": assignee,
2672
+ })
2673
+
2674
+
2675
+ # ----------------- MARKETPLACE -----------------
2676
+
2677
+ @mcp.tool()
2678
+ @with_working_status
2679
+ def meshcode_marketplace_search(query: Optional[str] = None,
2680
+ category: Optional[str] = None) -> Dict[str, Any]:
2681
+ """Search the MarketMesh marketplace for agents and meshes.
2682
+
2683
+ Args:
2684
+ query: Search term (matches name, description, tags).
2685
+ category: Filter by category (Development, Content, Marketing, etc.).
2686
+ """
2687
+ return be.sb_rpc("mc_marketplace_list", {
2688
+ "p_category": category,
2689
+ "p_search": query,
2690
+ "p_status": "available",
2691
+ })
2692
+
2693
+
2694
+ @mcp.tool()
2695
+ @with_working_status
2696
+ def meshcode_marketplace_install(template_slug: str,
2697
+ meshwork_name: str) -> Dict[str, Any]:
2698
+ """Install a marketplace item — creates a new meshwork with pre-configured agents.
2699
+
2700
+ Args:
2701
+ template_slug: The slug of the marketplace item to install.
2702
+ meshwork_name: Name for the new meshwork (must be unique).
2703
+ """
2704
+ return be.sb_rpc("mc_marketplace_install_by_key", {
2705
+ "p_api_key": _get_api_key(),
2706
+ "p_template_slug": template_slug,
2707
+ "p_meshwork_name": meshwork_name,
2708
+ })
2709
+
2710
+
2711
+ @mcp.tool()
2712
+ @with_working_status
2713
+ def meshcode_marketplace_list_installed() -> Dict[str, Any]:
2714
+ """List marketplace items you have installed."""
2715
+ return be.sb_rpc("mc_marketplace_installed_by_key", {
2716
+ "p_api_key": _get_api_key(),
2717
+ })
2718
+
2719
+
2720
+ # ----------------- WEBHOOKS -----------------
2721
+
2722
+ @mcp.tool()
2723
+ @with_working_status
2724
+ def meshcode_webhook_register(url: str,
2725
+ events: Optional[list] = None,
2726
+ description: str = "") -> Dict[str, Any]:
2727
+ """Register a webhook URL to receive POST notifications for mesh events.
2728
+
2729
+ Args:
2730
+ url: The URL to POST events to.
2731
+ events: List of event types to subscribe to.
2732
+ Options: agent_online, agent_offline, task_created, task_completed,
2733
+ message_cross_mesh, goal_updated.
2734
+ Default: all events (agent_online, agent_offline, task_created,
2735
+ task_completed, message_cross_mesh, goal_created, goal_updated).
2736
+ description: Human-readable description of this webhook.
2737
+
2738
+ Returns the webhook_id and signing secret (save the secret — shown only once).
2739
+ """
2740
+ return be.sb_rpc("mc_webhook_register", {
2741
+ "p_api_key": _get_api_key(),
2742
+ "p_project_id": _PROJECT_ID,
2743
+ "p_url": url,
2744
+ "p_events": events,
2745
+ "p_description": description,
2746
+ })
2747
+
2748
+
2749
+ @mcp.tool()
2750
+ @with_working_status
2751
+ def meshcode_webhook_list() -> Dict[str, Any]:
2752
+ """List all registered webhooks for this meshwork."""
2753
+ return be.sb_rpc("mc_webhook_list", {
2754
+ "p_api_key": _get_api_key(),
2755
+ "p_project_id": _PROJECT_ID,
2756
+ })
2757
+
2758
+
2759
+ @mcp.tool()
2760
+ @with_working_status
2761
+ def meshcode_webhook_delete(webhook_id: str) -> Dict[str, Any]:
2762
+ """Delete a webhook by ID.
2763
+
2764
+ Args:
2765
+ webhook_id: UUID of the webhook to delete.
2766
+ """
2767
+ return be.sb_rpc("mc_webhook_delete", {
2768
+ "p_api_key": _get_api_key(),
2769
+ "p_webhook_id": webhook_id,
2770
+ })
2771
+
2772
+
2492
2773
  # ----------------- SCHEDULED TASKS -----------------
2493
2774
 
2494
2775
  @mcp.tool()
@@ -71,17 +71,49 @@ def _fetch_or_generate_art(agent: str, project: str) -> tuple:
71
71
  if not proj_data or not proj_data.get("project_id"):
72
72
  return generate_art(agent), agent, None
73
73
  project_id = proj_data["project_id"]
74
- # Step 2: fetch existing art + role
75
- req = Request(
76
- f"{sb['SUPABASE_URL']}/rest/v1/mc_agents?select=ascii_art,role,id&name=eq.{urllib.parse.quote(agent)}&project_id=eq.{project_id}",
77
- headers={
78
- "apikey": sb["SUPABASE_KEY"],
79
- "Authorization": f"Bearer {sb['SUPABASE_KEY']}",
80
- "Accept-Profile": "meshcode",
81
- },
82
- )
83
- with urlopen(req, timeout=5) as resp:
84
- data = json.loads(resp.read().decode())
74
+ # Step 2: fetch existing art + role via mc_get_agents RPC
75
+ data = None
76
+ try:
77
+ rpc_body = json.dumps({
78
+ "p_api_key": api_key,
79
+ "p_project_id": project_id,
80
+ "p_agent_name": agent,
81
+ "p_select": ["ascii_art", "role", "id"],
82
+ })
83
+ rpc_req = Request(
84
+ f"{sb['SUPABASE_URL']}/rest/v1/rpc/mc_get_agents",
85
+ data=rpc_body.encode(),
86
+ method="POST",
87
+ headers={
88
+ "apikey": sb["SUPABASE_KEY"],
89
+ "Authorization": f"Bearer {sb['SUPABASE_KEY']}",
90
+ "Content-Type": "application/json",
91
+ "Content-Profile": "meshcode",
92
+ },
93
+ )
94
+ with urlopen(rpc_req, timeout=5) as resp:
95
+ rpc_result = json.loads(resp.read().decode())
96
+ if isinstance(rpc_result, dict) and rpc_result.get("ok"):
97
+ data = rpc_result.get("agents") or []
98
+ except Exception:
99
+ pass # fall through to legacy direct REST call
100
+
101
+ # Legacy fallback: direct REST call to mc_agents table
102
+ if data is None:
103
+ try:
104
+ req = Request(
105
+ f"{sb['SUPABASE_URL']}/rest/v1/mc_agents?select=ascii_art,role,id&name=eq.{urllib.parse.quote(agent)}&project_id=eq.{project_id}",
106
+ headers={
107
+ "apikey": sb["SUPABASE_KEY"],
108
+ "Authorization": f"Bearer {sb['SUPABASE_KEY']}",
109
+ "Accept-Profile": "meshcode",
110
+ },
111
+ )
112
+ with urlopen(req, timeout=5) as resp:
113
+ data = json.loads(resp.read().decode())
114
+ except Exception:
115
+ data = []
116
+
85
117
  if data:
86
118
  agent_id = data[0].get("id")
87
119
  # Step 3: fetch profile color
@@ -393,8 +425,8 @@ def _fetch_global_claude_version() -> Optional[str]:
393
425
  from urllib.request import Request, urlopen
394
426
  import json as _json
395
427
  req = Request(
396
- f"{url}/rest/v1/rpc/mc_get_global_config_by_key",
397
- data=_json.dumps({"p_api_key": api_key, "p_config_key": "claude_version"}).encode(),
428
+ f"{url}/rest/v1/rpc/mc_get_global_config",
429
+ data=_json.dumps({"p_key": "claude_code_version"}).encode(),
398
430
  headers={
399
431
  "Content-Type": "application/json",
400
432
  "apikey": anon_key,
@@ -719,7 +751,7 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
719
751
  if not editor:
720
752
  print("[meshcode] ERROR: 'claude' not found in PATH", file=sys.stderr)
721
753
  print(f"[meshcode] Workspace is ready at: {ws}", file=sys.stderr)
722
- print("[meshcode] Install Claude Code: npm install -g @anthropic-ai/claude-code", file=sys.stderr)
754
+ print("[meshcode] Install Claude Code: npm install -g @anthropic-ai/claude-code@2.1.104", file=sys.stderr)
723
755
  if sys.platform == "win32":
724
756
  print("[meshcode] Then verify: where claude", file=sys.stderr)
725
757
  print("[meshcode] If 'where' finds it, open a NEW terminal and try again.", file=sys.stderr)
@@ -815,7 +847,7 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
815
847
  else:
816
848
  print(f"[meshcode] Permission mode: bypass requested but --dangerously-skip-permissions")
817
849
  print(f"[meshcode] not supported by this Claude Code version. Agent will prompt for tools.")
818
- print(f"[meshcode] Upgrade Claude Code: npm install -g @anthropic-ai/claude-code@latest")
850
+ print(f"[meshcode] Upgrade Claude Code: npm install -g @anthropic-ai/claude-code@2.1.104")
819
851
  else:
820
852
  print(f"[meshcode] Permission mode: safe (Claude will prompt for every tool call)")
821
853
  print(f"[meshcode] Tip: change with `meshcode prefs permission-mode bypass`")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.46
3
+ Version: 2.10.48
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.10.46"
7
+ version = "2.10.48"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes