meshcode 2.10.34__tar.gz → 2.10.35__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 (33) hide show
  1. {meshcode-2.10.34 → meshcode-2.10.35}/PKG-INFO +34 -2
  2. {meshcode-2.10.34 → meshcode-2.10.35}/README.md +33 -1
  3. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/__init__.py +1 -1
  4. meshcode-2.10.35/meshcode/meshcode_mcp/__init__.py +22 -0
  5. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/meshcode_mcp/__main__.py +29 -6
  6. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/meshcode_mcp/backend.py +40 -4
  7. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/meshcode_mcp/realtime.py +30 -3
  8. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/meshcode_mcp/server.py +98 -21
  9. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/run_agent.py +37 -1
  10. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode.egg-info/PKG-INFO +34 -2
  11. {meshcode-2.10.34 → meshcode-2.10.35}/pyproject.toml +1 -1
  12. meshcode-2.10.34/meshcode/meshcode_mcp/__init__.py +0 -2
  13. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/ascii_art.py +0 -0
  14. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/cli.py +0 -0
  15. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/comms_v4.py +0 -0
  16. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/invites.py +0 -0
  17. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/launcher.py +0 -0
  18. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/launcher_install.py +0 -0
  19. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/meshcode_mcp/test_backend.py +0 -0
  20. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  21. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  22. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/preferences.py +0 -0
  23. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/protocol_v2.py +0 -0
  24. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/secrets.py +0 -0
  25. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/self_update.py +0 -0
  26. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/setup_clients.py +0 -0
  27. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode.egg-info/SOURCES.txt +0 -0
  28. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode.egg-info/dependency_links.txt +0 -0
  29. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode.egg-info/entry_points.txt +0 -0
  30. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode.egg-info/requires.txt +0 -0
  31. {meshcode-2.10.34 → meshcode-2.10.35}/meshcode.egg-info/top_level.txt +0 -0
  32. {meshcode-2.10.34 → meshcode-2.10.35}/setup.cfg +0 -0
  33. {meshcode-2.10.34 → meshcode-2.10.35}/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.34
3
+ Version: 2.10.35
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -26,11 +26,27 @@ Requires-Dist: cryptography>=41.0
26
26
 
27
27
  # MeshCode
28
28
 
29
- **Persistent AI coworkers.** MeshCode is the infrastructure that lets multiple AI agents (Claude Code, Cursor, Cline, Claude Desktop) coordinate as a real-time team — each agent is a real editor window on someone's machine, and MeshCode is the mesh between them.
29
+ **The easiest way to connect multiple AI agents in real time so they collaborate like a team.** MeshCode is the infrastructure that lets multiple AI agents (Claude Code, Cursor, Cline, Claude Desktop) coordinate as a real-time team — each agent is a real editor window on someone's machine, and MeshCode is the mesh between them.
30
30
 
31
31
  - Docs: https://meshcode.io/docs
32
32
  - Dashboard: https://meshcode.io/dashboard
33
33
  - GitHub: https://github.com/meshcode/meshcode
34
+ - **Why MeshCode** (positioning): [docs/WHY_MESHCODE.md](./docs/WHY_MESHCODE.md)
35
+ - **Compare to CrewAI / AutoGen / Swarm / LangGraph**: [docs/COMPARISON.md](./docs/COMPARISON.md)
36
+ - **Use cases** (query → solution): [docs/USE_CASES.md](./docs/USE_CASES.md)
37
+
38
+ ---
39
+
40
+ ## When to use MeshCode
41
+
42
+ - **Connect multiple Claude Code instances** so they coordinate as a team
43
+ - **Run AI agents across editors** (Claude Code + Cursor + Cline) on the same project
44
+ - **Collaborate with a friend's AI agent** across different laptops
45
+ - **Persistent AI coworkers** that remember across sessions, days, weeks
46
+ - **Orchestrate an AI engineering team** (commander + backend + frontend + QA)
47
+ - **Observe + replay** agent sessions in a hosted dashboard
48
+
49
+ Not the right fit if you need a single-agent chatbot, headless batch pipelines, or enterprise SSO/SOC2 paperwork. See [docs/COMPARISON.md](./docs/COMPARISON.md) for honest trade-offs.
34
50
 
35
51
  ---
36
52
 
@@ -59,6 +75,22 @@ That's it. `meshcode go` handles everything: auth check, workspace creation, edi
59
75
 
60
76
  ---
61
77
 
78
+ ## How MeshCode compares
79
+
80
+ | | MeshCode | CrewAI | AutoGen | OpenAI Swarm | Anthropic subagents |
81
+ |---|---|---|---|---|---|
82
+ | Persistent agents across sessions | **yes** | no | no | no | no |
83
+ | Real editor windows (Claude Code, Cursor, Cline) | **yes** | no | no | no | partial |
84
+ | Cross-machine collaboration | **yes** | no | no | no | no |
85
+ | MCP-native | **yes** | no | no | no | yes |
86
+ | Hosted dashboard (status, replay, graph) | **yes** | no | no | no | no |
87
+ | No vendor lock-in | **yes** | yes | partial | **no (OpenAI only)** | **no (Anthropic only)** |
88
+ | One-command setup (<30s) | **yes** | no | no | no | partial |
89
+
90
+ Full comparison with LangGraph, Google A2A, and DIY → [docs/COMPARISON.md](./docs/COMPARISON.md).
91
+
92
+ ---
93
+
62
94
  ## How it works
63
95
 
64
96
  ```
@@ -1,10 +1,26 @@
1
1
  # MeshCode
2
2
 
3
- **Persistent AI coworkers.** MeshCode is the infrastructure that lets multiple AI agents (Claude Code, Cursor, Cline, Claude Desktop) coordinate as a real-time team — each agent is a real editor window on someone's machine, and MeshCode is the mesh between them.
3
+ **The easiest way to connect multiple AI agents in real time so they collaborate like a team.** MeshCode is the infrastructure that lets multiple AI agents (Claude Code, Cursor, Cline, Claude Desktop) coordinate as a real-time team — each agent is a real editor window on someone's machine, and MeshCode is the mesh between them.
4
4
 
5
5
  - Docs: https://meshcode.io/docs
6
6
  - Dashboard: https://meshcode.io/dashboard
7
7
  - GitHub: https://github.com/meshcode/meshcode
8
+ - **Why MeshCode** (positioning): [docs/WHY_MESHCODE.md](./docs/WHY_MESHCODE.md)
9
+ - **Compare to CrewAI / AutoGen / Swarm / LangGraph**: [docs/COMPARISON.md](./docs/COMPARISON.md)
10
+ - **Use cases** (query → solution): [docs/USE_CASES.md](./docs/USE_CASES.md)
11
+
12
+ ---
13
+
14
+ ## When to use MeshCode
15
+
16
+ - **Connect multiple Claude Code instances** so they coordinate as a team
17
+ - **Run AI agents across editors** (Claude Code + Cursor + Cline) on the same project
18
+ - **Collaborate with a friend's AI agent** across different laptops
19
+ - **Persistent AI coworkers** that remember across sessions, days, weeks
20
+ - **Orchestrate an AI engineering team** (commander + backend + frontend + QA)
21
+ - **Observe + replay** agent sessions in a hosted dashboard
22
+
23
+ Not the right fit if you need a single-agent chatbot, headless batch pipelines, or enterprise SSO/SOC2 paperwork. See [docs/COMPARISON.md](./docs/COMPARISON.md) for honest trade-offs.
8
24
 
9
25
  ---
10
26
 
@@ -33,6 +49,22 @@ That's it. `meshcode go` handles everything: auth check, workspace creation, edi
33
49
 
34
50
  ---
35
51
 
52
+ ## How MeshCode compares
53
+
54
+ | | MeshCode | CrewAI | AutoGen | OpenAI Swarm | Anthropic subagents |
55
+ |---|---|---|---|---|---|
56
+ | Persistent agents across sessions | **yes** | no | no | no | no |
57
+ | Real editor windows (Claude Code, Cursor, Cline) | **yes** | no | no | no | partial |
58
+ | Cross-machine collaboration | **yes** | no | no | no | no |
59
+ | MCP-native | **yes** | no | no | no | yes |
60
+ | Hosted dashboard (status, replay, graph) | **yes** | no | no | no | no |
61
+ | No vendor lock-in | **yes** | yes | partial | **no (OpenAI only)** | **no (Anthropic only)** |
62
+ | One-command setup (<30s) | **yes** | no | no | no | partial |
63
+
64
+ Full comparison with LangGraph, Google A2A, and DIY → [docs/COMPARISON.md](./docs/COMPARISON.md).
65
+
66
+ ---
67
+
36
68
  ## How it works
37
69
 
38
70
  ```
@@ -1,2 +1,2 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.10.34"
2
+ __version__ = "2.10.35"
@@ -0,0 +1,22 @@
1
+ """MeshCode MCP server — exposes meshcode tools to MCP clients (Claude Code, etc)."""
2
+ __version__ = "1.0.0"
3
+
4
+ # Convenience re-exports so `from meshcode.meshcode_mcp import MeshCodeMCP`
5
+ # and `from meshcode.meshcode_mcp.backend import MeshCodeBackend` work.
6
+ # These are lazy to avoid import-time side effects (server.py reads env vars
7
+ # and exits if MESHCODE_PROJECT / MESHCODE_AGENT are unset).
8
+
9
+
10
+ def __getattr__(name):
11
+ if name == "MeshCodeMCP":
12
+ from .server import mcp as _mcp, run_server # noqa: F811
13
+ # Expose the FastMCP instance and run_server under the expected name
14
+ class MeshCodeMCP:
15
+ """Convenience wrapper around the FastMCP server instance."""
16
+ server = _mcp
17
+ run = staticmethod(run_server)
18
+ return MeshCodeMCP
19
+ if name == "run_server":
20
+ from .server import run_server
21
+ return run_server
22
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -9,12 +9,35 @@ import sys
9
9
  # - Block-buffered → JSON-RPC bytes get stuck in the kernel buffer until
10
10
  # the buffer fills, so Claude Code never sees the handshake response.
11
11
  # Both must be fixed BEFORE FastMCP touches stdio.
12
- try:
13
- sys.stdout.reconfigure(encoding="utf-8", errors="replace", newline="\n", line_buffering=True)
14
- sys.stdin.reconfigure(encoding="utf-8", errors="replace", newline="\n")
15
- sys.stderr.reconfigure(encoding="utf-8", errors="replace")
16
- except Exception:
17
- pass
12
+ def _force_utf8_stdio():
13
+ """Force UTF-8 + line-buffered stdout, UTF-8 stdin/stderr.
14
+
15
+ reconfigure() can fail on some Windows Python builds (3.14+) when
16
+ stdout is a pipe without a backing console. In that case, wrap the
17
+ raw buffer in a fresh TextIOWrapper.
18
+ """
19
+ import io
20
+ for stream_name in ("stdout", "stdin", "stderr"):
21
+ stream = getattr(sys, stream_name)
22
+ if stream is None:
23
+ continue
24
+ kw = {"encoding": "utf-8", "errors": "replace"}
25
+ if stream_name != "stderr":
26
+ kw["newline"] = "\n"
27
+ if stream_name == "stdout":
28
+ kw["line_buffering"] = True
29
+ try:
30
+ stream.reconfigure(**kw)
31
+ except Exception:
32
+ # reconfigure failed — wrap the raw buffer manually
33
+ try:
34
+ raw = stream.buffer
35
+ wrapper = io.TextIOWrapper(raw, **kw)
36
+ setattr(sys, stream_name, wrapper)
37
+ except Exception:
38
+ pass # nothing more we can do
39
+
40
+ _force_utf8_stdio()
18
41
  # Belt and suspenders: also export PYTHONIOENCODING in case any subprocess
19
42
  # we spawn inherits this env.
20
43
  os.environ.setdefault("PYTHONIOENCODING", "utf-8")
@@ -5,10 +5,13 @@ Zero deps beyond stdlib (urllib + http.client for connection pooling).
5
5
  """
6
6
  import http.client
7
7
  import json
8
+ import logging
8
9
  import os
9
10
  import ssl
10
11
  import time as _time
11
12
  import threading as _threading
13
+
14
+ log = logging.getLogger("meshcode-mcp.backend")
12
15
  from datetime import datetime
13
16
  from pathlib import Path
14
17
  from typing import Any, Dict, List, Optional
@@ -81,8 +84,8 @@ def _load_env_file() -> Dict[str, str]:
81
84
  if "=" in line and not line.startswith("#"):
82
85
  k, v = line.split("=", 1)
83
86
  out[k.strip()] = v.strip().strip('"').strip("'")
84
- except Exception:
85
- pass
87
+ except Exception as e:
88
+ log.debug(f"env file parse error ({env_path}): {e}")
86
89
  return out
87
90
 
88
91
  _env_file = _load_env_file()
@@ -322,8 +325,8 @@ def _bg_record(event_type: str, payload: dict):
322
325
  "p_event_type": event_type,
323
326
  "p_payload": payload,
324
327
  })
325
- except Exception:
326
- pass
328
+ except Exception as e:
329
+ log.debug(f"bg_record failed ({event_type}): {e}")
327
330
  threading.Thread(target=_do, daemon=True).start()
328
331
 
329
332
 
@@ -346,6 +349,8 @@ def sb_rpc_raw(fn_name: str, params: Dict) -> Any:
346
349
  # ============================================================
347
350
 
348
351
  def get_project_id(project_name: str) -> Optional[str]:
352
+ if not project_name:
353
+ return None
349
354
  rows = sb_select("mc_projects", f"name=eq.{quote(project_name)}")
350
355
  if rows:
351
356
  return rows[0]["id"]
@@ -805,3 +810,34 @@ def get_message_by_id(project_id: str, msg_id: str, api_key: Optional[str] = Non
805
810
  limit=1,
806
811
  )
807
812
  return results[0] if results else None
813
+
814
+
815
+ # ============================================================
816
+ # Convenience class so `from meshcode.meshcode_mcp.backend import MeshCodeBackend`
817
+ # works for users who expect a class-based API (e.g. Windows onboarding docs).
818
+ # ============================================================
819
+
820
+ class MeshCodeBackend:
821
+ """Namespace wrapper around the backend module functions.
822
+
823
+ All methods are static — no instance state. This exists purely so that
824
+ ``from meshcode.meshcode_mcp.backend import MeshCodeBackend`` resolves.
825
+ """
826
+ get_project_id = staticmethod(get_project_id)
827
+ register_agent = staticmethod(register_agent)
828
+ send_message = staticmethod(send_message)
829
+ read_inbox = staticmethod(read_inbox)
830
+ count_pending = staticmethod(count_pending)
831
+ get_board = staticmethod(get_board)
832
+ heartbeat = staticmethod(heartbeat)
833
+ set_status = staticmethod(set_status)
834
+ task_create = staticmethod(task_create)
835
+ task_list = staticmethod(task_list)
836
+ task_claim = staticmethod(task_claim)
837
+ task_complete = staticmethod(task_complete)
838
+ get_history = staticmethod(get_history)
839
+ get_message_by_id = staticmethod(get_message_by_id)
840
+ sb_rpc = staticmethod(sb_rpc)
841
+ sb_select = staticmethod(sb_select)
842
+ sb_insert = staticmethod(sb_insert)
843
+ sb_update = staticmethod(sb_update)
@@ -164,6 +164,12 @@ class RealtimeListener:
164
164
  "schema": "meshcode",
165
165
  "table": "mc_messages",
166
166
  "filter": f"to_agent=eq.{url_quote(self.agent_name, safe='')}",
167
+ },
168
+ {
169
+ "event": "INSERT",
170
+ "schema": "meshcode",
171
+ "table": "mc_messages",
172
+ "filter": "to_agent=eq.*",
167
173
  }
168
174
  ]
169
175
  }
@@ -200,6 +206,10 @@ class RealtimeListener:
200
206
  await ws.send(json.dumps(join_msg))
201
207
  if not self._subscription_ok:
202
208
  log.error(f"Realtime subscription FAILED after 3 attempts for {self.agent_name}")
209
+ # Close and let the outer _run() loop reconnect with backoff.
210
+ # Staying connected but unsubscribed wastes the WebSocket
211
+ # and forces the agent into slow DB polling forever.
212
+ return
203
213
 
204
214
  # Heartbeat task to keep the connection alive
205
215
  heartbeat_task = asyncio.create_task(self._heartbeat(ws))
@@ -209,7 +219,8 @@ class RealtimeListener:
209
219
  break
210
220
  try:
211
221
  msg = json.loads(raw)
212
- except Exception:
222
+ except Exception as e:
223
+ log.warning(f"Realtime JSON parse error: {e} (raw[:200]={str(raw)[:200]})")
213
224
  continue
214
225
  await self._handle_message(msg)
215
226
  finally:
@@ -234,7 +245,8 @@ class RealtimeListener:
234
245
  "payload": {},
235
246
  "ref": str(ref),
236
247
  }))
237
- except Exception:
248
+ except Exception as e:
249
+ log.warning(f"Realtime heartbeat send failed: {e}")
238
250
  return
239
251
 
240
252
  async def _handle_message(self, msg: Dict[str, Any]) -> None:
@@ -247,7 +259,11 @@ class RealtimeListener:
247
259
  data = payload.get("data") or {}
248
260
  if data.get("type") == "INSERT":
249
261
  record = data.get("record") or {}
250
- if record.get("to_agent") == self.agent_name and record.get("project_id") == self.project_id:
262
+ to = record.get("to_agent")
263
+ from_agent = record.get("from_agent")
264
+ # Accept direct messages AND broadcasts (to_agent='*'),
265
+ # but filter out self-broadcasts to avoid echo.
266
+ if (to in (self.agent_name, "*")) and record.get("project_id") == self.project_id and from_agent != self.agent_name:
251
267
  enriched = {
252
268
  "from": record.get("from_agent"),
253
269
  "type": record.get("type", "msg"),
@@ -300,6 +316,17 @@ class RealtimeListener:
300
316
  except asyncio.TimeoutError:
301
317
  return False
302
318
 
319
+ async def restart(self) -> None:
320
+ """Stop and restart the Realtime connection.
321
+
322
+ Called by the heartbeat thread when it detects the subscription
323
+ dropped (is_subscribed=False). The outer _run() loop handles
324
+ reconnect with exponential backoff.
325
+ """
326
+ log.info(f"Realtime restart requested for {self.agent_name}")
327
+ await self.stop()
328
+ await self.start()
329
+
303
330
  @property
304
331
  def is_connected(self) -> bool:
305
332
  return self._connected
@@ -95,9 +95,15 @@ def _mc_log(msg: str, level: str = "info") -> None:
95
95
  # meshcode_wait / meshcode_check so the same row doesn't show
96
96
  # up twice when realtime + polled paths race.
97
97
  # ============================================================
98
- _SEEN_MSG_IDS: set = set()
98
+ # TTL-based message dedup cache. Each entry stores (msg_key, timestamp).
99
+ # Entries older than _SEEN_TTL are evicted on access. This prevents
100
+ # the race where Realtime delivers a message and DB polling returns the
101
+ # same one before mark-read completes. The TTL ensures the cache doesn't
102
+ # grow unbounded across long sessions.
103
+ _SEEN_MSG_IDS: dict = {} # key -> timestamp (monotonic)
99
104
  _SEEN_MSG_ORDER: deque = deque()
100
- _SEEN_MSG_CAP = 1000
105
+ _SEEN_MSG_CAP = 2000
106
+ _SEEN_TTL = 300.0 # 5 minutes
101
107
 
102
108
  # ============================================================
103
109
  # Auto-wake: when agent is NOT in meshcode_wait and a message
@@ -216,18 +222,37 @@ def _seen_key(msg: Dict[str, Any]) -> str:
216
222
  return f"{msg.get('from') or msg.get('from_agent')}|{msg.get('ts') or msg.get('created_at')}|{payload_str}"
217
223
 
218
224
 
225
+ def _evict_expired() -> None:
226
+ """Remove entries older than _SEEN_TTL from the dedup cache."""
227
+ now = _time.monotonic()
228
+ while _SEEN_MSG_ORDER:
229
+ oldest_key = _SEEN_MSG_ORDER[0]
230
+ ts = _SEEN_MSG_IDS.get(oldest_key)
231
+ if ts is not None and (now - ts) > _SEEN_TTL:
232
+ _SEEN_MSG_ORDER.popleft()
233
+ _SEEN_MSG_IDS.pop(oldest_key, None)
234
+ else:
235
+ break
236
+
237
+
219
238
  def _mark_seen(key: str) -> None:
239
+ now = _time.monotonic()
220
240
  if key in _SEEN_MSG_IDS:
241
+ # Refresh timestamp on re-sight (extends TTL)
242
+ _SEEN_MSG_IDS[key] = now
221
243
  return
222
- _SEEN_MSG_IDS.add(key)
244
+ _SEEN_MSG_IDS[key] = now
223
245
  _SEEN_MSG_ORDER.append(key)
246
+ # Evict expired + cap enforcement
247
+ _evict_expired()
224
248
  while len(_SEEN_MSG_ORDER) > _SEEN_MSG_CAP:
225
249
  old = _SEEN_MSG_ORDER.popleft()
226
- _SEEN_MSG_IDS.discard(old)
250
+ _SEEN_MSG_IDS.pop(old, None)
227
251
 
228
252
 
229
253
  def _filter_and_mark(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
230
254
  """Drop already-seen messages; mark the rest as seen."""
255
+ _evict_expired() # Clean stale entries before checking
231
256
  out = []
232
257
  for m in messages:
233
258
  k = _seen_key(m)
@@ -912,7 +937,15 @@ what CLI command to run next (e.g. "meshcode run backend in a new terminal").
912
937
  Setup help → README.md or https://meshcode.io/docs
913
938
  """
914
939
  # Inject commander protocol if this agent is a leader
915
- is_leader = any(k in (_ROLE_DESCRIPTION or '').lower() + AGENT_NAME.lower() for k in ('commander', 'lead', 'orchestrat'))
940
+ # Match leader-like agent names and roles across languages.
941
+ # Substring match: "orchestrator" hits on "orchestrat", "coordinador" hits on "coordinat".
942
+ _leader_haystack = ((_ROLE_DESCRIPTION or '') + ' ' + AGENT_NAME).lower()
943
+ _LEADER_KEYWORDS = (
944
+ 'commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
945
+ 'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
946
+ 'chief', 'captain', 'boss', 'head agent',
947
+ )
948
+ is_leader = any(k in _leader_haystack for k in _LEADER_KEYWORDS)
916
949
  if is_leader:
917
950
  base += """
918
951
  COMMANDER PROTOCOL (you are the team lead):
@@ -1097,13 +1130,36 @@ def _get_parent_cpu() -> float:
1097
1130
  return 0.0
1098
1131
 
1099
1132
 
1133
+ _heartbeat_crash_count = 0
1134
+
1100
1135
  def _heartbeat_thread_fn():
1101
1136
  """Heartbeat in a DAEMON THREAD — independent of asyncio event loop.
1102
1137
 
1103
1138
  This ensures heartbeats continue even when tool calls are cancelled,
1104
1139
  meshcode_wait is rejected, or the asyncio loop is busy. The agent
1105
1140
  stays 'online' in the dashboard as long as the MCP process is alive.
1141
+
1142
+ Wrapped in an outer try/except so uncaught exceptions (including from
1143
+ _heartbeat_stop.wait()) restart the loop instead of silently killing
1144
+ the thread — which would leave the agent as a ghost on the dashboard.
1106
1145
  """
1146
+ global _heartbeat_crash_count
1147
+ while not _heartbeat_stop.is_set():
1148
+ try:
1149
+ _heartbeat_loop_inner()
1150
+ except Exception as e:
1151
+ _heartbeat_crash_count += 1
1152
+ log.error(
1153
+ f"heartbeat thread crashed ({_heartbeat_crash_count}x): {e} — "
1154
+ f"restarting in 5s to prevent ghost agent"
1155
+ )
1156
+ # Brief sleep before restart to avoid tight crash loop
1157
+ _heartbeat_stop.wait(5)
1158
+
1159
+
1160
+ def _heartbeat_loop_inner():
1161
+ """Single iteration of the heartbeat loop. Separated so the outer
1162
+ wrapper can catch any exception and restart cleanly."""
1107
1163
  lease_counter = 0
1108
1164
  while not _heartbeat_stop.is_set():
1109
1165
  try:
@@ -1136,12 +1192,33 @@ def _heartbeat_thread_fn():
1136
1192
  # Sync current state to DB (in case realtime missed it)
1137
1193
  try:
1138
1194
  be.set_status(_PROJECT_ID, AGENT_NAME, _current_state, _current_tool, api_key=_get_api_key())
1139
- except Exception:
1140
- pass
1141
- if _REALTIME and not _REALTIME.is_connected:
1142
- log.warning("heartbeat ok (HTTP) but WebSocket disconnected")
1195
+ except Exception as e:
1196
+ log.debug(f"status sync failed: {e}")
1197
+ # Realtime subscription recovery: if the WebSocket is connected but
1198
+ # the channel subscription dropped (e.g. Supabase maintenance), or
1199
+ # the WebSocket itself disconnected, trigger a restart so agents
1200
+ # don't stay stuck in slow DB polling for the entire session.
1201
+ if _REALTIME:
1202
+ if not _REALTIME.is_connected:
1203
+ log.warning("heartbeat ok (HTTP) but WebSocket disconnected — scheduling Realtime restart")
1204
+ try:
1205
+ loop = asyncio.get_event_loop()
1206
+ if loop.is_running():
1207
+ asyncio.run_coroutine_threadsafe(_REALTIME.restart(), loop)
1208
+ except Exception as e:
1209
+ log.debug(f"Realtime restart scheduling failed: {e}")
1210
+ elif not _REALTIME.is_subscribed:
1211
+ log.warning("WebSocket connected but subscription lost — scheduling Realtime restart")
1212
+ try:
1213
+ loop = asyncio.get_event_loop()
1214
+ if loop.is_running():
1215
+ asyncio.run_coroutine_threadsafe(_REALTIME.restart(), loop)
1216
+ except Exception as e:
1217
+ log.debug(f"Realtime restart scheduling failed: {e}")
1218
+ else:
1219
+ log.debug(f"heartbeat ok for {AGENT_NAME}")
1143
1220
  else:
1144
- log.debug(f"heartbeat ok for {AGENT_NAME}")
1221
+ log.debug(f"heartbeat ok for {AGENT_NAME} (no Realtime)")
1145
1222
  except Exception as e:
1146
1223
  log.warning(f"heartbeat failed: {e}")
1147
1224
 
@@ -1626,8 +1703,8 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
1626
1703
  "p_status": "sleeping",
1627
1704
  "p_task": f"idle {_CONSECUTIVE_IDLE_SECONDS}s — still listening",
1628
1705
  })
1629
- except Exception:
1630
- pass
1706
+ except Exception as e:
1707
+ log.debug(f"auto-sleep status update failed: {e}")
1631
1708
  # Do NOT return — keep looping. Status says sleeping but
1632
1709
  # we are still listening for messages via realtime.
1633
1710
  _set_state("sleeping", f"idle {_CONSECUTIVE_IDLE_SECONDS}s — still listening")
@@ -1652,8 +1729,8 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
1652
1729
  "p_tier": "critical",
1653
1730
  "p_project_name": PROJECT_NAME,
1654
1731
  })
1655
- except Exception:
1656
- pass
1732
+ except Exception as e:
1733
+ log.debug(f"last_seen memory persist failed: {e}")
1657
1734
  return result
1658
1735
  finally:
1659
1736
  _IN_WAIT = False
@@ -1681,8 +1758,8 @@ def _mark_realtime_msgs_read_in_db(messages: List[Dict[str, Any]]) -> None:
1681
1758
  "p_project_id": _PROJECT_ID,
1682
1759
  "p_message_id": mid,
1683
1760
  })
1684
- except Exception:
1685
- pass
1761
+ except Exception as e:
1762
+ log.debug(f"mark_read failed for msg {mid}: {e}")
1686
1763
  threading.Thread(target=_do, daemon=True).start()
1687
1764
 
1688
1765
 
@@ -1776,8 +1853,8 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
1776
1853
  split["acks"] = []
1777
1854
  if split["messages"] or split["done_signals"]:
1778
1855
  return {"got_message": True, "source": "db_poll_fallback", **split}
1779
- except Exception:
1780
- pass
1856
+ except Exception as e:
1857
+ log.debug(f"DB poll fallback error: {e}")
1781
1858
 
1782
1859
  # Final fallback: one last DB check (covers realtime path missing msgs)
1783
1860
  try:
@@ -1800,8 +1877,8 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
1800
1877
  split["acks"] = []
1801
1878
  if split["messages"] or split["done_signals"]:
1802
1879
  return {"got_message": True, "source": "db_fallback", **split}
1803
- except Exception:
1804
- pass
1880
+ except Exception as e:
1881
+ log.debug(f"final DB fallback error: {e}")
1805
1882
 
1806
1883
  # Check if there's any pending work before returning timeout
1807
1884
  pending_tasks = _get_pending_tasks_summary()
@@ -2671,7 +2748,7 @@ def meshcode_health() -> Dict[str, Any]:
2671
2748
  }
2672
2749
 
2673
2750
  # Realtime status
2674
- health["realtime_connected"] = getattr(_rt_state, 'connected', False) if '_rt_state' in dir() else "unknown"
2751
+ health["realtime_connected"] = _REALTIME.is_connected if _REALTIME else False
2675
2752
 
2676
2753
  # Process uptime
2677
2754
  try:
@@ -447,6 +447,38 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
447
447
  return 2
448
448
  ws, resolved_project = found
449
449
 
450
+ # ── Validate .mcp.json exists (required for MCP connection) ─────
451
+ # If .mcp.json is missing the editor launches but the agent never
452
+ # connects to the mesh — no error shown. Real incident: front-end
453
+ # agent had .mcp.json renamed to .mcp.json.bak by unknown cause.
454
+ mcp_json_path = ws / ".mcp.json"
455
+ mcp_json_bak = ws / ".mcp.json.bak"
456
+ if not mcp_json_path.exists():
457
+ # Try restoring from .bak first (cheapest fix)
458
+ if mcp_json_bak.exists():
459
+ try:
460
+ mcp_json_bak.rename(mcp_json_path)
461
+ print(f"[meshcode] Restored .mcp.json from backup (.mcp.json.bak)", file=sys.stderr)
462
+ except Exception as e:
463
+ print(f"[meshcode] WARNING: .mcp.json.bak exists but restore failed: {e}", file=sys.stderr)
464
+
465
+ if not mcp_json_path.exists():
466
+ # Regenerate via setup_workspace (has all data it needs from server)
467
+ print(f"[meshcode] .mcp.json missing from workspace — regenerating...", file=sys.stderr)
468
+ try:
469
+ from .setup_clients import setup_workspace
470
+ rc = setup_workspace(resolved_project, agent)
471
+ if rc == 0 and mcp_json_path.exists():
472
+ print(f"[meshcode] .mcp.json regenerated successfully.", file=sys.stderr)
473
+ else:
474
+ print(f"[meshcode] ERROR: could not regenerate .mcp.json.", file=sys.stderr)
475
+ print(f"[meshcode] Run `meshcode setup {resolved_project} {agent}` manually.", file=sys.stderr)
476
+ return 2
477
+ except Exception as e:
478
+ print(f"[meshcode] ERROR: .mcp.json missing and regeneration failed: {e}", file=sys.stderr)
479
+ print(f"[meshcode] Run `meshcode setup {resolved_project} {agent}` to fix.", file=sys.stderr)
480
+ return 2
481
+
450
482
  # ── Ownership pre-check ──────────────────────────────────────────
451
483
  # Before launching the editor, verify the caller owns this agent.
452
484
  # This prevents hijacking another user's agent in shared meshworks.
@@ -479,7 +511,11 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
479
511
  from .ascii_art import generate_art, render_welcome
480
512
  from . import __version__ as cli_version
481
513
  ascii_art, agent_role, profile_color = _fetch_or_generate_art(agent, resolved_project)
482
- is_cmd = "commander" in agent.lower() or "commander" in agent_role.lower()
514
+ _leader_haystack = (agent + ' ' + agent_role).lower()
515
+ _LEADER_KW = ('commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
516
+ 'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
517
+ 'chief', 'captain', 'boss', 'head agent')
518
+ is_cmd = any(k in _leader_haystack for k in _LEADER_KW)
483
519
  agent_stats = _fetch_agent_stats(agent, resolved_project)
484
520
  print(render_welcome(
485
521
  agent, resolved_project, ascii_art, cli_version,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.34
3
+ Version: 2.10.35
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -26,11 +26,27 @@ Requires-Dist: cryptography>=41.0
26
26
 
27
27
  # MeshCode
28
28
 
29
- **Persistent AI coworkers.** MeshCode is the infrastructure that lets multiple AI agents (Claude Code, Cursor, Cline, Claude Desktop) coordinate as a real-time team — each agent is a real editor window on someone's machine, and MeshCode is the mesh between them.
29
+ **The easiest way to connect multiple AI agents in real time so they collaborate like a team.** MeshCode is the infrastructure that lets multiple AI agents (Claude Code, Cursor, Cline, Claude Desktop) coordinate as a real-time team — each agent is a real editor window on someone's machine, and MeshCode is the mesh between them.
30
30
 
31
31
  - Docs: https://meshcode.io/docs
32
32
  - Dashboard: https://meshcode.io/dashboard
33
33
  - GitHub: https://github.com/meshcode/meshcode
34
+ - **Why MeshCode** (positioning): [docs/WHY_MESHCODE.md](./docs/WHY_MESHCODE.md)
35
+ - **Compare to CrewAI / AutoGen / Swarm / LangGraph**: [docs/COMPARISON.md](./docs/COMPARISON.md)
36
+ - **Use cases** (query → solution): [docs/USE_CASES.md](./docs/USE_CASES.md)
37
+
38
+ ---
39
+
40
+ ## When to use MeshCode
41
+
42
+ - **Connect multiple Claude Code instances** so they coordinate as a team
43
+ - **Run AI agents across editors** (Claude Code + Cursor + Cline) on the same project
44
+ - **Collaborate with a friend's AI agent** across different laptops
45
+ - **Persistent AI coworkers** that remember across sessions, days, weeks
46
+ - **Orchestrate an AI engineering team** (commander + backend + frontend + QA)
47
+ - **Observe + replay** agent sessions in a hosted dashboard
48
+
49
+ Not the right fit if you need a single-agent chatbot, headless batch pipelines, or enterprise SSO/SOC2 paperwork. See [docs/COMPARISON.md](./docs/COMPARISON.md) for honest trade-offs.
34
50
 
35
51
  ---
36
52
 
@@ -59,6 +75,22 @@ That's it. `meshcode go` handles everything: auth check, workspace creation, edi
59
75
 
60
76
  ---
61
77
 
78
+ ## How MeshCode compares
79
+
80
+ | | MeshCode | CrewAI | AutoGen | OpenAI Swarm | Anthropic subagents |
81
+ |---|---|---|---|---|---|
82
+ | Persistent agents across sessions | **yes** | no | no | no | no |
83
+ | Real editor windows (Claude Code, Cursor, Cline) | **yes** | no | no | no | partial |
84
+ | Cross-machine collaboration | **yes** | no | no | no | no |
85
+ | MCP-native | **yes** | no | no | no | yes |
86
+ | Hosted dashboard (status, replay, graph) | **yes** | no | no | no | no |
87
+ | No vendor lock-in | **yes** | yes | partial | **no (OpenAI only)** | **no (Anthropic only)** |
88
+ | One-command setup (<30s) | **yes** | no | no | no | partial |
89
+
90
+ Full comparison with LangGraph, Google A2A, and DIY → [docs/COMPARISON.md](./docs/COMPARISON.md).
91
+
92
+ ---
93
+
62
94
  ## How it works
63
95
 
64
96
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.10.34"
7
+ version = "2.10.35"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -1,2 +0,0 @@
1
- """MeshCode MCP server — exposes meshcode tools to MCP clients (Claude Code, etc)."""
2
- __version__ = "1.0.0"
File without changes
File without changes