meshcode 2.7.1__tar.gz → 2.8.0__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 (31) hide show
  1. {meshcode-2.7.1 → meshcode-2.8.0}/PKG-INFO +1 -1
  2. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/__init__.py +1 -1
  3. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/meshcode_mcp/backend.py +44 -3
  4. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/meshcode_mcp/server.py +41 -43
  5. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode.egg-info/PKG-INFO +1 -1
  6. {meshcode-2.7.1 → meshcode-2.8.0}/pyproject.toml +1 -1
  7. {meshcode-2.7.1 → meshcode-2.8.0}/README.md +0 -0
  8. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/cli.py +0 -0
  9. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/comms_v4.py +0 -0
  10. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/invites.py +0 -0
  11. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/launcher.py +0 -0
  12. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/launcher_install.py +0 -0
  13. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/meshcode_mcp/__init__.py +0 -0
  14. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/meshcode_mcp/__main__.py +0 -0
  15. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/meshcode_mcp/realtime.py +0 -0
  16. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/meshcode_mcp/test_backend.py +0 -0
  17. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  18. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  19. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/preferences.py +0 -0
  20. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/protocol_v2.py +0 -0
  21. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/run_agent.py +0 -0
  22. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/secrets.py +0 -0
  23. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/self_update.py +0 -0
  24. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode/setup_clients.py +0 -0
  25. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode.egg-info/SOURCES.txt +0 -0
  26. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode.egg-info/dependency_links.txt +0 -0
  27. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode.egg-info/entry_points.txt +0 -0
  28. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode.egg-info/requires.txt +0 -0
  29. {meshcode-2.7.1 → meshcode-2.8.0}/meshcode.egg-info/top_level.txt +0 -0
  30. {meshcode-2.7.1 → meshcode-2.8.0}/setup.cfg +0 -0
  31. {meshcode-2.7.1 → meshcode-2.8.0}/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.7.1
3
+ Version: 2.8.0
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.7.1"
2
+ __version__ = "2.8.0"
@@ -532,7 +532,21 @@ def heartbeat(project_id: str, agent: str) -> Dict:
532
532
  return result or {}
533
533
 
534
534
 
535
- def set_status(project_id: str, agent: str, status: str, task: str = "") -> Dict:
535
+ def set_status(project_id: str, agent: str, status: str, task: str = "", api_key: Optional[str] = None) -> Dict:
536
+ # Use SECURITY DEFINER RPC when api_key is available (anon PATCH silently fails)
537
+ if api_key:
538
+ rpc_result = sb_rpc("mc_agent_set_status_by_api_key", {
539
+ "p_api_key": api_key,
540
+ "p_project_id": project_id,
541
+ "p_agent_name": agent,
542
+ "p_status": status,
543
+ "p_task": task,
544
+ })
545
+ if isinstance(rpc_result, dict) and rpc_result.get("ok"):
546
+ return {"ok": True, "status": status}
547
+ # Fall through to direct PATCH if RPC doesn't exist yet
548
+
549
+ # Fallback: direct PATCH (may silently fail with anon key if RLS blocks)
536
550
  updates = {"status": status, "last_heartbeat": _now_iso()}
537
551
  if task:
538
552
  updates["task"] = task
@@ -595,7 +609,20 @@ def record_event(api_key, project_id, agent_name, session_id, event_type, payloa
595
609
  })
596
610
 
597
611
 
598
- def get_history(project_id: str, limit: int = 20, agent_filter: str = "") -> List[Dict]:
612
+ def get_history(project_id: str, limit: int = 20, agent_filter: str = "", api_key: Optional[str] = None) -> List[Dict]:
613
+ # Use SECURITY DEFINER RPC when api_key is available (bypasses RLS safely)
614
+ if api_key:
615
+ rpc_result = sb_rpc("mc_get_history", {
616
+ "p_api_key": api_key,
617
+ "p_project_id": project_id,
618
+ "p_limit": limit,
619
+ "p_agent_filter": agent_filter or None,
620
+ })
621
+ if isinstance(rpc_result, dict) and rpc_result.get("ok"):
622
+ return rpc_result.get("messages", [])
623
+ # Fall through to direct query if RPC doesn't exist yet
624
+
625
+ # Fallback: direct SELECT (tests/legacy)
599
626
  filters = f"project_id=eq.{project_id}&type=neq.ack"
600
627
  if agent_filter:
601
628
  filters += f"&or=(from_agent.eq.{quote(agent_filter)},to_agent.eq.{quote(agent_filter)})"
@@ -607,7 +634,21 @@ def get_history(project_id: str, limit: int = 20, agent_filter: str = "") -> Lis
607
634
  )
608
635
 
609
636
 
610
- def get_message_by_id(project_id: str, msg_id: str) -> Optional[Dict]:
637
+ def get_message_by_id(project_id: str, msg_id: str, api_key: Optional[str] = None) -> Optional[Dict]:
638
+ # Use SECURITY DEFINER RPC when api_key is available (bypasses RLS safely)
639
+ if api_key:
640
+ rpc_result = sb_rpc("mc_get_message_by_id", {
641
+ "p_api_key": api_key,
642
+ "p_project_id": project_id,
643
+ "p_msg_id": msg_id,
644
+ })
645
+ if isinstance(rpc_result, dict) and rpc_result.get("ok"):
646
+ return rpc_result.get("message")
647
+ if isinstance(rpc_result, dict) and rpc_result.get("error"):
648
+ return None
649
+ # Fall through to direct query if RPC doesn't exist yet
650
+
651
+ # Fallback: direct SELECT (tests/legacy)
611
652
  results = sb_select(
612
653
  "mc_messages",
613
654
  f"project_id=eq.{project_id}&id=eq.{msg_id}",
@@ -357,31 +357,15 @@ if isinstance(_register_result, dict) and _register_result.get("error"):
357
357
  # bypass RLS — the publishable key has no JWT context and cannot UPDATE
358
358
  # mc_agents directly. The RPC validates ownership via api_key.
359
359
  def _flip_status(status: str, task: str = "") -> bool:
360
- """Write status directly to mc_agents table for instant Realtime propagation.
360
+ """Write status via RPC for reliable updates (anon PATCH silently fails).
361
361
 
362
- Uses direct PATCH (not RPC) so Supabase Realtime fires an UPDATE event
363
- immediately — the dashboard sees the change in <100ms instead of waiting
364
- for the next heartbeat cycle.
362
+ Falls back to direct PATCH only when no api_key is available (tests).
365
363
  """
366
364
  try:
367
- be.set_status(_PROJECT_ID, AGENT_NAME, status, task)
368
- return True
365
+ result = be.set_status(_PROJECT_ID, AGENT_NAME, status, task, api_key=_get_api_key())
366
+ return isinstance(result, dict) and result.get("ok", False)
369
367
  except Exception:
370
- # Fallback to RPC if direct PATCH fails (RLS issue)
371
- api_key = _get_api_key()
372
- if not api_key:
373
- return False
374
- try:
375
- r = be.sb_rpc("mc_agent_set_status_by_api_key", {
376
- "p_api_key": api_key,
377
- "p_project_id": _PROJECT_ID,
378
- "p_agent_name": AGENT_NAME,
379
- "p_status": status,
380
- "p_task": task,
381
- })
382
- return isinstance(r, dict) and r.get("ok", False)
383
- except Exception:
384
- return False
368
+ return False
385
369
 
386
370
  if not _flip_status("idle", ""):
387
371
  print(f"[meshcode-mcp] WARNING: could not flip status to idle", file=sys.stderr)
@@ -578,7 +562,7 @@ def _acquire_lease() -> bool:
578
562
  except Exception:
579
563
  # Force clear via direct update
580
564
  try:
581
- be.set_status(_PROJECT_ID, AGENT_NAME, "offline", "")
565
+ be.set_status(_PROJECT_ID, AGENT_NAME, "offline", "", api_key=_get_api_key())
582
566
  except Exception:
583
567
  pass
584
568
  _time.sleep(1)
@@ -1000,7 +984,7 @@ def _heartbeat_thread_fn():
1000
984
 
1001
985
  # Sync current state to DB (in case realtime missed it)
1002
986
  try:
1003
- be.set_status(_PROJECT_ID, AGENT_NAME, _current_state, _current_tool)
987
+ be.set_status(_PROJECT_ID, AGENT_NAME, _current_state, _current_tool, api_key=_get_api_key())
1004
988
  except Exception:
1005
989
  pass
1006
990
  if _REALTIME and not _REALTIME.is_connected:
@@ -1049,7 +1033,7 @@ async def lifespan(_app):
1049
1033
  for _attempt in range(3):
1050
1034
  try:
1051
1035
  be.sb_rpc("mc_heartbeat", {"p_project_id": _PROJECT_ID, "p_agent_name": AGENT_NAME, "p_version": "2.0.0"})
1052
- be.set_status(_PROJECT_ID, AGENT_NAME, "idle", "MCP session active")
1036
+ be.set_status(_PROJECT_ID, AGENT_NAME, "idle", "MCP session active", api_key=_get_api_key())
1053
1037
  log.info(f"[meshcode] Agent {AGENT_NAME} online — initial heartbeat sent")
1054
1038
  break
1055
1039
  except Exception as e:
@@ -1128,9 +1112,9 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1128
1112
  sensitive: bool = False, encrypted: bool = False) -> Dict[str, Any]:
1129
1113
  """Send message. Use "agent@meshwork" for cross-mesh. sensitive=True hides from exports. Pass encrypted=True for secrets/credentials (AES-256-GCM)."""
1130
1114
  if isinstance(message, str):
1131
- # Auto-wrap strings but warn if too long prefer structured JSON
1132
- if len(message) > 400:
1133
- return {"error": f"plain text message too long ({len(message)} chars). Use a dict payload with structured fields, or use meshcode_task_create for long content. Messages must be structured JSON <2000 chars."}
1115
+ # Auto-wrap strings into dict. Warn if very long but don't reject.
1116
+ if len(message) > 2000:
1117
+ return {"error": f"message too long ({len(message)} chars). Use meshcode_task_create for long content, or split into multiple messages. Max ~2000 chars."}
1134
1118
  payload: Dict[str, Any] = {"text": message}
1135
1119
  elif isinstance(message, dict):
1136
1120
  payload = message
@@ -1237,7 +1221,7 @@ def meshcode_read(include_acks: bool = False) -> Dict[str, Any]:
1237
1221
  @with_working_status
1238
1222
  def meshcode_history(limit: int = 20, agent_filter: Optional[str] = None) -> Dict[str, Any]:
1239
1223
  """View past messages (read+unread). Optional agent_filter."""
1240
- raw = be.get_history(_PROJECT_ID, limit=limit, agent_filter=agent_filter or "")
1224
+ raw = be.get_history(_PROJECT_ID, limit=limit, agent_filter=agent_filter or "", api_key=_get_api_key())
1241
1225
  messages = [
1242
1226
  {
1243
1227
  "from": m.get("from_agent", ""),
@@ -1257,7 +1241,7 @@ def meshcode_history(limit: int = 20, agent_filter: Optional[str] = None) -> Dic
1257
1241
  @with_working_status
1258
1242
  def meshcode_read_message(msg_id: str) -> Dict[str, Any]:
1259
1243
  """Fetch a specific message by its UUID."""
1260
- msg = be.get_message_by_id(_PROJECT_ID, msg_id)
1244
+ msg = be.get_message_by_id(_PROJECT_ID, msg_id, api_key=_get_api_key())
1261
1245
  if not msg:
1262
1246
  return {"error": "message not found", "msg_id": msg_id}
1263
1247
  return {
@@ -1313,7 +1297,12 @@ except Exception as _e:
1313
1297
 
1314
1298
 
1315
1299
  def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
1316
- """Fetch open/in_progress tasks assigned to this agent. Returns compact list or None."""
1300
+ """Fetch tasks that THIS agent should work on. Returns compact list or None.
1301
+
1302
+ Only blocks on tasks directly assigned to this agent or claimed by this agent.
1303
+ Tasks assigned to '*' (wildcard) that are unclaimed are NOT blocking —
1304
+ they're available for any agent but shouldn't prevent wait().
1305
+ """
1317
1306
  try:
1318
1307
  api_key = _get_api_key()
1319
1308
  if not api_key:
@@ -1326,7 +1315,10 @@ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
1326
1315
  {"id": t["id"][:8], "title": t["title"][:80], "priority": t.get("priority", "normal"), "status": t["status"]}
1327
1316
  for t in tasks
1328
1317
  if t.get("status") in ("open", "in_progress")
1329
- and (t.get("assignee") in (AGENT_NAME, "*", None) or t.get("claimed_by") == AGENT_NAME)
1318
+ and (
1319
+ t.get("claimed_by") == AGENT_NAME # I claimed it — must work it
1320
+ or t.get("assignee") == AGENT_NAME # Directly assigned to me
1321
+ )
1330
1322
  ]
1331
1323
  return pending if pending else None
1332
1324
  except Exception:
@@ -1370,13 +1362,19 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
1370
1362
  "id": m.get("id"), "parent_id": m.get("parent_msg_id")}
1371
1363
  for m in raw
1372
1364
  ]
1373
- split = _split_messages(msgs)
1374
- return {
1375
- "refused": True,
1376
- "reason": f"You have {db_pending} unread messages. Process them before waiting.",
1377
- "got_message": True,
1378
- **split,
1379
- }
1365
+ # Dedup against already-seen messages (fixes race where
1366
+ # background mark_read hasn't completed yet)
1367
+ deduped = _filter_and_mark(msgs)
1368
+ if not deduped:
1369
+ pass # All messages already seen — fall through to wait loop
1370
+ else:
1371
+ split = _split_messages(deduped)
1372
+ return {
1373
+ "refused": True,
1374
+ "reason": f"You have {len(deduped)} unread messages. Process them before waiting.",
1375
+ "got_message": True,
1376
+ **split,
1377
+ }
1380
1378
  except Exception:
1381
1379
  pass
1382
1380
 
@@ -1732,7 +1730,7 @@ def meshcode_set_status(status: str, task: str = "") -> Dict[str, Any]:
1732
1730
  "reason": f"Cannot set status '{status}' — you have {len(pending_tasks)} open tasks. Work them first.",
1733
1731
  "pending_tasks": pending_tasks,
1734
1732
  }
1735
- return be.set_status(_PROJECT_ID, AGENT_NAME, status, task)
1733
+ return be.set_status(_PROJECT_ID, AGENT_NAME, status, task, api_key=_get_api_key())
1736
1734
 
1737
1735
 
1738
1736
  @mcp.tool()
@@ -2064,7 +2062,7 @@ def meshcode_scratchpad_set(key: str, value: Any) -> Dict[str, Any]:
2064
2062
  "p_api_key": api_key,
2065
2063
  "p_key": key,
2066
2064
  "p_value": json_value,
2067
- "p_project_name": PROJECT_NAME,
2065
+ "p_tier": "reference",
2068
2066
  })
2069
2067
 
2070
2068
 
@@ -2078,9 +2076,9 @@ def meshcode_scratchpad_get(key: Optional[str] = None) -> Dict[str, Any]:
2078
2076
  """
2079
2077
  api_key = _get_api_key()
2080
2078
  if key:
2081
- return be.sb_rpc("mc_scratchpad_get", {"p_api_key": api_key, "p_key": key, "p_project_name": PROJECT_NAME})
2079
+ return be.sb_rpc("mc_scratchpad_get", {"p_api_key": api_key, "p_key": key})
2082
2080
  else:
2083
- return be.sb_rpc("mc_scratchpad_list", {"p_api_key": api_key, "p_project_name": PROJECT_NAME})
2081
+ return be.sb_rpc("mc_scratchpad_list", {"p_api_key": api_key})
2084
2082
 
2085
2083
 
2086
2084
  # ----------------- OBSIDIAN SYNC HELPER -----------------
@@ -2297,7 +2295,7 @@ def board_resource() -> str:
2297
2295
  @mcp.resource("meshcode://history")
2298
2296
  def history_resource() -> str:
2299
2297
  """Recent message history for this meshwork (last 20 non-ack messages)."""
2300
- history = be.get_history(_PROJECT_ID, limit=20)
2298
+ history = be.get_history(_PROJECT_ID, limit=20, api_key=_get_api_key())
2301
2299
  return json.dumps({
2302
2300
  "project": PROJECT_NAME,
2303
2301
  "history": history,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.7.1
3
+ Version: 2.8.0
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.7.1"
7
+ version = "2.8.0"
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
File without changes
File without changes
File without changes
File without changes
File without changes