meshcode 2.5.8__tar.gz → 2.6.1__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.5.8 → meshcode-2.6.1}/PKG-INFO +1 -1
  2. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/__init__.py +1 -1
  3. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/comms_v4.py +4 -3
  4. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/meshcode_mcp/backend.py +4 -2
  5. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/meshcode_mcp/server.py +118 -16
  6. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode.egg-info/PKG-INFO +1 -1
  7. {meshcode-2.5.8 → meshcode-2.6.1}/pyproject.toml +1 -1
  8. {meshcode-2.5.8 → meshcode-2.6.1}/README.md +0 -0
  9. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/cli.py +0 -0
  10. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/invites.py +0 -0
  11. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/launcher.py +0 -0
  12. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/launcher_install.py +0 -0
  13. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/meshcode_mcp/__init__.py +0 -0
  14. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/meshcode_mcp/__main__.py +0 -0
  15. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/meshcode_mcp/realtime.py +0 -0
  16. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/meshcode_mcp/test_backend.py +0 -0
  17. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  18. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  19. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/preferences.py +0 -0
  20. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/protocol_v2.py +0 -0
  21. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/run_agent.py +0 -0
  22. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/secrets.py +0 -0
  23. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/self_update.py +0 -0
  24. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/setup_clients.py +0 -0
  25. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode.egg-info/SOURCES.txt +0 -0
  26. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode.egg-info/dependency_links.txt +0 -0
  27. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode.egg-info/entry_points.txt +0 -0
  28. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode.egg-info/requires.txt +0 -0
  29. {meshcode-2.5.8 → meshcode-2.6.1}/meshcode.egg-info/top_level.txt +0 -0
  30. {meshcode-2.5.8 → meshcode-2.6.1}/setup.cfg +0 -0
  31. {meshcode-2.5.8 → meshcode-2.6.1}/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.5.8
3
+ Version: 2.6.1
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.5.8"
2
+ __version__ = "2.6.1"
@@ -470,9 +470,10 @@ def _throttle_spawn_ok(project, name, max_per_5min=5):
470
470
 
471
471
 
472
472
  def _headless_spawn_allowed():
473
- """MeshCode is mesh infrastructure only by default it never spawns claude.
474
- Power users can opt back in by exporting MESHCODE_ALLOW_HEADLESS_SPAWN=1."""
475
- return os.environ.get("MESHCODE_ALLOW_HEADLESS_SPAWN", "").strip() in ("1", "true", "yes", "on")
473
+ """Headless spawn is ON by default for mesh agents sleeping agents auto-wake
474
+ when a message/task arrives. Opt OUT with MESHCODE_ALLOW_HEADLESS_SPAWN=0."""
475
+ val = os.environ.get("MESHCODE_ALLOW_HEADLESS_SPAWN", "1").strip().lower()
476
+ return val not in ("0", "false", "no", "off")
476
477
 
477
478
 
478
479
  def spawn_headless_agent(project, name, project_id, message_body, from_agent):
@@ -321,7 +321,8 @@ def read_inbox(project_id: str, agent: str, mark_read: bool = True, api_key: Opt
321
321
  if mesh_key is None:
322
322
  mesh_key = get_mesh_key(api_key, project_id)
323
323
  if mesh_key:
324
- decrypted = decrypt_payload(p["_encrypted"], mesh_key)
324
+ msg_aad = p.get("_aad", project_id)
325
+ decrypted = decrypt_payload(p["_encrypted"], mesh_key, aad=msg_aad)
325
326
  if decrypted is not None:
326
327
  m["payload"] = decrypted
327
328
  m["_was_encrypted"] = True
@@ -365,7 +366,8 @@ def read_inbox(project_id: str, agent: str, mark_read: bool = True, api_key: Opt
365
366
  if mesh_key is None:
366
367
  mesh_key = get_mesh_key(api_key, project_id)
367
368
  if mesh_key:
368
- decrypted = decrypt_payload(p["_encrypted"], mesh_key)
369
+ msg_aad = p.get("_aad", project_id)
370
+ decrypted = decrypt_payload(p["_encrypted"], mesh_key, aad=msg_aad)
369
371
  if decrypted is not None:
370
372
  m["payload"] = decrypted
371
373
  m["_was_encrypted"] = True
@@ -1124,15 +1124,24 @@ except Exception:
1124
1124
  @mcp.tool()
1125
1125
  @with_working_status
1126
1126
  def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1127
- sensitive: bool = False, encrypted: bool = False) -> Dict[str, Any]:
1128
- """Send message. Use "agent@meshwork" for cross-mesh. sensitive=True hides from exports. encrypted=True encrypts payload with per-meshwork AES-256 key."""
1127
+ sensitive: bool = False, encrypted: bool = True) -> Dict[str, Any]:
1128
+ """Send message. Use "agent@meshwork" for cross-mesh. sensitive=True hides from exports. All messages encrypted by default (AES-256-GCM). Pass encrypted=False to send plaintext."""
1129
1129
  if isinstance(message, str):
1130
+ # Auto-wrap strings but warn if too long — prefer structured JSON
1131
+ if len(message) > 400:
1132
+ 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."}
1130
1133
  payload: Dict[str, Any] = {"text": message}
1131
1134
  elif isinstance(message, dict):
1132
1135
  payload = message
1133
1136
  else:
1134
1137
  payload = {"text": str(message)}
1135
1138
 
1139
+ # Enforce message size limit — long content belongs in task descriptions
1140
+ import json as _json
1141
+ _payload_len = len(_json.dumps(payload, default=str))
1142
+ if _payload_len > 2000:
1143
+ return {"error": f"message too large ({_payload_len} chars). Use meshcode_task_create for long content. Messages must be structured JSON <2000 chars."}
1144
+
1136
1145
  # Cross-mesh routing: if 'to' contains '@', parse as agent@meshwork
1137
1146
  if "@" in to:
1138
1147
  target_agent, target_meshwork = to.split("@", 1)
@@ -1154,8 +1163,9 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1154
1163
  err = key_result.get("error", "unknown") if isinstance(key_result, dict) else "RPC failed"
1155
1164
  return {"error": f"cross-mesh encryption failed: {err}"}
1156
1165
  tgt_key = key_result["key"]
1157
- encrypted_data = be.encrypt_payload(payload, tgt_key)
1158
- payload = {"_encrypted": encrypted_data}
1166
+ tgt_project_id = key_result.get("target_project_id", "")
1167
+ encrypted_data = be.encrypt_payload(payload, tgt_key, aad=tgt_project_id)
1168
+ payload = {"_encrypted": encrypted_data, "_aad": tgt_project_id}
1159
1169
 
1160
1170
  result = be.sb_rpc("mc_send_cross_mesh", {
1161
1171
  "p_api_key": api_key,
@@ -1279,6 +1289,28 @@ def _detect_global_done(messages: List[Dict[str, Any]]) -> Optional[Dict[str, An
1279
1289
  # Auto-sleep: track consecutive idle timeouts to auto-sleep after threshold
1280
1290
  _CONSECUTIVE_IDLE_SECONDS = 0
1281
1291
  _AUTO_SLEEP_THRESHOLD = int(os.environ.get("MESHCODE_AUTO_SLEEP_SECONDS", "600")) # default 10min
1292
+ _LAST_SEEN_TS: Optional[str] = None # auto-persisted for message dedup
1293
+
1294
+
1295
+ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
1296
+ """Fetch open/in_progress tasks assigned to this agent. Returns compact list or None."""
1297
+ try:
1298
+ api_key = _get_api_key()
1299
+ if not api_key:
1300
+ return None
1301
+ result = be.task_list(api_key, _PROJECT_ID, AGENT_NAME, status_filter=None)
1302
+ if not isinstance(result, dict) or not result.get("ok"):
1303
+ return None
1304
+ tasks = result.get("tasks", [])
1305
+ pending = [
1306
+ {"id": t["id"][:8], "title": t["title"][:80], "priority": t.get("priority", "normal"), "status": t["status"]}
1307
+ for t in tasks
1308
+ if t.get("status") in ("open", "in_progress")
1309
+ and (t.get("assigned_to") in (AGENT_NAME, "*", None) or t.get("claimed_by") == AGENT_NAME)
1310
+ ]
1311
+ return pending if pending else None
1312
+ except Exception:
1313
+ return None
1282
1314
 
1283
1315
 
1284
1316
  @mcp.tool()
@@ -1290,6 +1322,41 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
1290
1322
  timeout_seconds: Max wait time in seconds (default 120, hard cap 120).
1291
1323
  """
1292
1324
  global _IN_WAIT, _CONSECUTIVE_IDLE_SECONDS
1325
+
1326
+ # PRODUCT RULE 1: If agent has open tasks, refuse to wait. Work first.
1327
+ pending_tasks = _get_pending_tasks_summary()
1328
+ if pending_tasks:
1329
+ return {
1330
+ "refused": True,
1331
+ "reason": "You have open tasks. Work them before entering wait.",
1332
+ "pending_tasks": pending_tasks,
1333
+ "count": len(pending_tasks),
1334
+ }
1335
+
1336
+ # PRODUCT RULE 2: If agent has unread messages in DB, refuse to wait.
1337
+ # The in-memory dedupe (_SEEN_MSG_IDS) can mark messages as "seen" via
1338
+ # realtime without the agent actually processing them. Always check DB.
1339
+ try:
1340
+ db_pending = be.count_pending(_PROJECT_ID, AGENT_NAME)
1341
+ if db_pending and db_pending > 0:
1342
+ # Fetch and return the messages instead of waiting
1343
+ raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=False, api_key=_get_api_key())
1344
+ msgs = [
1345
+ {"from": m["from_agent"], "type": m.get("type", "msg"),
1346
+ "ts": m.get("created_at"), "payload": m.get("payload", {}),
1347
+ "id": m.get("id"), "parent_id": m.get("parent_msg_id")}
1348
+ for m in raw
1349
+ ]
1350
+ split = _split_messages(msgs)
1351
+ return {
1352
+ "refused": True,
1353
+ "reason": f"You have {db_pending} unread messages. Process them before waiting.",
1354
+ "got_message": True,
1355
+ **split,
1356
+ }
1357
+ except Exception:
1358
+ pass
1359
+
1293
1360
  _IN_WAIT = True
1294
1361
  _set_state("waiting", "listening for messages")
1295
1362
  # Universal hard cap: even if a caller passes a larger value (e.g. 1800),
@@ -1324,6 +1391,18 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
1324
1391
  "threshold": _AUTO_SLEEP_THRESHOLD,
1325
1392
  "instruction": "You have been idle too long. Status set to sleeping. Exit the wait loop to save resources. You will be woken by auto_wake if a new message arrives.",
1326
1393
  }
1394
+ # Auto-inject pending tasks so agents don't forget to check
1395
+ pending_tasks = _get_pending_tasks_summary()
1396
+ if pending_tasks:
1397
+ result["pending_tasks"] = pending_tasks
1398
+ # Track last seen timestamp for message dedup
1399
+ global _LAST_SEEN_TS
1400
+ if result.get("got_message"):
1401
+ msgs = result.get("messages", [])
1402
+ if msgs:
1403
+ latest_ts = max((m.get("ts", "") for m in msgs), default="")
1404
+ if latest_ts:
1405
+ _LAST_SEEN_TS = latest_ts
1327
1406
  return result
1328
1407
  finally:
1329
1408
  _IN_WAIT = False
@@ -1388,7 +1467,15 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
1388
1467
  # Realtime unavailable — plain sleep fallback so we still honor timeout.
1389
1468
  await asyncio.sleep(actual_timeout)
1390
1469
 
1391
- return {"timed_out": True}
1470
+ # Check if there's any pending work before returning timeout
1471
+ pending_tasks = _get_pending_tasks_summary()
1472
+ out: Dict[str, Any] = {"timed_out": True}
1473
+ if pending_tasks:
1474
+ out["pending_tasks"] = pending_tasks
1475
+ else:
1476
+ out["no_work"] = True
1477
+ out["hint"] = "No messages or tasks. Safe to sleep — launcher daemon will wake you on new messages."
1478
+ return out
1392
1479
 
1393
1480
 
1394
1481
  @mcp.tool()
@@ -1452,20 +1539,26 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None) -> D
1452
1539
  if _seen_key({"id": m.get("id"), "from": m.get("from_agent"), "payload": m.get("payload", {}), "ts": m.get("created_at")}) not in _SEEN_MSG_IDS
1453
1540
  ]
1454
1541
 
1455
- # Filter by `since` timestamp if provided
1456
- if since:
1457
- deduped = [m for m in deduped if m.get("ts") and str(m["ts"]) > since]
1542
+ # Auto-apply last_seen_ts if no explicit since provided
1543
+ effective_since = since or _LAST_SEEN_TS
1544
+ if effective_since:
1545
+ deduped = [m for m in deduped if m.get("ts") and str(m["ts"]) > effective_since]
1458
1546
 
1459
1547
  split = _split_messages(deduped)
1460
1548
  if not include_acks:
1461
1549
  split["acks"] = []
1462
- return {
1463
- "pending": pending if not since else len(split.get("messages", [])),
1550
+ result = {
1551
+ "pending": pending if not effective_since else len(split.get("messages", [])),
1464
1552
  "agent": AGENT_NAME,
1465
1553
  "project": PROJECT_NAME,
1466
1554
  "realtime_connected": _REALTIME.is_connected if _REALTIME else False,
1467
1555
  **split,
1468
1556
  }
1557
+ # Auto-inject pending tasks
1558
+ pending_tasks = _get_pending_tasks_summary()
1559
+ if pending_tasks:
1560
+ result["pending_tasks"] = pending_tasks
1561
+ return result
1469
1562
 
1470
1563
 
1471
1564
  @mcp.tool()
@@ -1500,9 +1593,18 @@ def meshcode_set_status(status: str, task: str = "") -> Dict[str, Any]:
1500
1593
  """Update your status in the board.
1501
1594
 
1502
1595
  Args:
1503
- status: One of: working, idle, standby, blocked, done, online.
1596
+ status: One of: working, idle, standby, blocked, done, online, sleeping.
1504
1597
  task: Optional human-readable task description.
1505
1598
  """
1599
+ # PRODUCT RULE: Cannot sleep/idle/standby with open tasks. Work first.
1600
+ if status in ("sleeping", "idle", "standby"):
1601
+ pending_tasks = _get_pending_tasks_summary()
1602
+ if pending_tasks:
1603
+ return {
1604
+ "refused": True,
1605
+ "reason": f"Cannot set status '{status}' — you have {len(pending_tasks)} open tasks. Work them first.",
1606
+ "pending_tasks": pending_tasks,
1607
+ }
1506
1608
  return be.set_status(_PROJECT_ID, AGENT_NAME, status, task)
1507
1609
 
1508
1610
 
@@ -2080,13 +2182,13 @@ def history_resource() -> str:
2080
2182
  # ============================================================
2081
2183
 
2082
2184
  def _auto_update() -> None:
2083
- """Upgrade meshcode from PyPI in background opt-in via MESHCODE_AUTO_UPDATE=1.
2185
+ """Silently upgrade meshcode from PyPI in background on every launch.
2084
2186
 
2085
- Disabled by default to mitigate supply chain risk: if the PyPI account
2086
- is compromised, every agent would silently install malicious code.
2187
+ Ensures all agents always run the latest version. Disable with
2188
+ MESHCODE_AUTO_UPDATE=0 if needed.
2087
2189
  """
2088
- if os.environ.get("MESHCODE_AUTO_UPDATE", "0") != "1":
2089
- log.debug("[meshcode] Auto-update disabled (set MESHCODE_AUTO_UPDATE=1 to enable)")
2190
+ if os.environ.get("MESHCODE_AUTO_UPDATE", "1").lower() in ("0", "false", "no"):
2191
+ log.debug("[meshcode] Auto-update disabled (MESHCODE_AUTO_UPDATE=0)")
2090
2192
  return
2091
2193
 
2092
2194
  import threading
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.5.8
3
+ Version: 2.6.1
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.5.8"
7
+ version = "2.6.1"
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