meshcode 2.10.34__tar.gz → 2.10.36__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.36}/PKG-INFO +34 -2
  2. {meshcode-2.10.34 → meshcode-2.10.36}/README.md +33 -1
  3. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/__init__.py +1 -1
  4. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/comms_v4.py +108 -8
  5. meshcode-2.10.36/meshcode/meshcode_mcp/__init__.py +22 -0
  6. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/meshcode_mcp/__main__.py +29 -6
  7. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/meshcode_mcp/backend.py +41 -5
  8. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/meshcode_mcp/realtime.py +30 -3
  9. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/meshcode_mcp/server.py +145 -50
  10. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/run_agent.py +37 -1
  11. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode.egg-info/PKG-INFO +34 -2
  12. {meshcode-2.10.34 → meshcode-2.10.36}/pyproject.toml +1 -1
  13. meshcode-2.10.34/meshcode/meshcode_mcp/__init__.py +0 -2
  14. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/ascii_art.py +0 -0
  15. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/cli.py +0 -0
  16. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/invites.py +0 -0
  17. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/launcher.py +0 -0
  18. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/launcher_install.py +0 -0
  19. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/meshcode_mcp/test_backend.py +0 -0
  20. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  21. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  22. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/preferences.py +0 -0
  23. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/protocol_v2.py +0 -0
  24. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/secrets.py +0 -0
  25. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/self_update.py +0 -0
  26. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/setup_clients.py +0 -0
  27. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode.egg-info/SOURCES.txt +0 -0
  28. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode.egg-info/dependency_links.txt +0 -0
  29. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode.egg-info/entry_points.txt +0 -0
  30. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode.egg-info/requires.txt +0 -0
  31. {meshcode-2.10.34 → meshcode-2.10.36}/meshcode.egg-info/top_level.txt +0 -0
  32. {meshcode-2.10.34 → meshcode-2.10.36}/setup.cfg +0 -0
  33. {meshcode-2.10.34 → meshcode-2.10.36}/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.36
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.36"
@@ -1730,10 +1730,21 @@ SETUP (advanced):
1730
1730
  register <proj> <name> [role] Register agent manually
1731
1731
  setup <client> <proj> <name> [role] Legacy: global MCP config
1732
1732
 
1733
+ AGENT CONTROL:
1734
+ scan Scan identicon from clipboard → run
1735
+ kill <proj> <name> Force disconnect agent
1736
+ wake <proj> <name> Send wake signal
1737
+ sleep <proj> <name> Send sleep signal
1738
+ disconnect <proj> <name> Graceful disconnect
1739
+ whoami Show logged-in identity
1740
+ profile [agent] Show/set agent profile
1741
+ connect <proj> <name> Connect existing agent
1742
+
1733
1743
  ADMIN:
1734
1744
  clear <proj> <name> Clear inbox
1735
1745
  unregister <proj> <name> Leave project
1736
1746
  prefs View/set preferences
1747
+ update Check for package updates
1737
1748
 
1738
1749
  PROFILES (multi-account):
1739
1750
  profiles List stored keychain profiles
@@ -1856,6 +1867,80 @@ Clear inbox: marks all unread messages as read for an agent.
1856
1867
  "unregister": """meshcode unregister <project> <name>
1857
1868
 
1858
1869
  Remove an agent from a meshwork (deletes the row from mc_agents).
1870
+ """,
1871
+ "setup": """meshcode setup <project> <agent> [role]
1872
+
1873
+ Create an isolated workspace at ~/meshcode/<project>-<agent>/ with
1874
+ .mcp.json configured for the agent's MCP server. Usually auto-created
1875
+ by `meshcode go`.
1876
+
1877
+ EXAMPLES:
1878
+ meshcode setup my-app backend "Backend Engineer"
1879
+ """,
1880
+ "run": """meshcode run <agent> [--project <name>] [--editor claude|cursor|code]
1881
+
1882
+ Launch an agent in your preferred editor. Detects Claude Code, Cursor,
1883
+ VS Code, Windsurf, or Codex. Use <project>/<agent> to disambiguate.
1884
+
1885
+ EXAMPLES:
1886
+ meshcode run backend
1887
+ meshcode run my-app/backend --editor cursor
1888
+ """,
1889
+ "go": """meshcode go <agent> [--project <name>]
1890
+
1891
+ Shortcut for setup + run. Creates workspace if needed, then launches.
1892
+
1893
+ EXAMPLES:
1894
+ meshcode go backend
1895
+ meshcode go my-app/frontend
1896
+ """,
1897
+ "scan": """meshcode scan
1898
+
1899
+ Read an agent identicon from clipboard (or stdin) and launch the agent.
1900
+ The identicon must contain a ⟨ project/agent ⟩ tag.
1901
+
1902
+ EXAMPLES:
1903
+ meshcode scan # reads from clipboard
1904
+ cat identicon.txt | meshcode scan
1905
+ """,
1906
+ "invite": """meshcode invite <project> <agent> [--role "..."] [--days 7]
1907
+
1908
+ Generate an invite token for a teammate to join as a specific agent.
1909
+ --days 0 = permanent (never expires).
1910
+
1911
+ EXAMPLES:
1912
+ meshcode invite my-app frontend --role "React Developer" --days 7
1913
+ """,
1914
+ "join": """meshcode join <token> [--display-name "alice"]
1915
+
1916
+ Accept an invite and create a workspace for the assigned agent.
1917
+
1918
+ EXAMPLES:
1919
+ meshcode join mc_invite_abc123 --display-name "Alice"
1920
+ """,
1921
+ "invites": """meshcode invites <project>
1922
+
1923
+ List outstanding and redeemed invites for a meshwork.
1924
+ """,
1925
+ "members": """meshcode members <project>
1926
+
1927
+ List all members (owner + invited) of a meshwork.
1928
+ """,
1929
+ "whoami": """meshcode whoami
1930
+
1931
+ Show the currently logged-in user (email, user_id, profile).
1932
+ """,
1933
+ "prefs": """meshcode prefs [key] [value]
1934
+
1935
+ View or set preferences (permission-mode, auto-update, etc.).
1936
+
1937
+ EXAMPLES:
1938
+ meshcode prefs # show all
1939
+ meshcode prefs permission-mode bypass # set
1940
+ """,
1941
+ "profiles": """meshcode profiles
1942
+
1943
+ List all stored keychain profiles (default + per-meshwork guest keys).
1859
1944
  """,
1860
1945
  }
1861
1946
 
@@ -1981,6 +2066,9 @@ if __name__ == "__main__":
1981
2066
  from_a, to_a = target.split(":", 1)
1982
2067
  else:
1983
2068
  from_a, to_a = "?", target
2069
+ if not message.strip():
2070
+ print("[meshcode] ERROR: message cannot be empty. Usage: meshcode send <project> <from>:<to> <message>")
2071
+ sys.exit(1)
1984
2072
  send_msg(proj, from_a, to_a, message, compact=compact)
1985
2073
 
1986
2074
  elif cmd == "broadcast":
@@ -1989,12 +2077,15 @@ if __name__ == "__main__":
1989
2077
  from_a = flags["from"]
1990
2078
  message = flags.get("msg", flags.get("message", " ".join(pos)))
1991
2079
  msg_type = flags.get("type", "broadcast")
1992
- broadcast(proj, from_a, message, msg_type)
1993
2080
  else:
1994
2081
  proj = pos[0] if len(pos) > 0 else "default"
1995
2082
  from_a = pos[1] if len(pos) > 1 else "?"
1996
2083
  message = " ".join(pos[2:]) if len(pos) > 2 else ""
1997
- broadcast(proj, from_a, message)
2084
+ msg_type = "broadcast"
2085
+ if not message.strip():
2086
+ print("[meshcode] ERROR: message cannot be empty. Usage: meshcode broadcast <project> <from> <message>")
2087
+ sys.exit(1)
2088
+ broadcast(proj, from_a, message, msg_type)
1998
2089
 
1999
2090
  elif cmd == "read":
2000
2091
  proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
@@ -2007,8 +2098,14 @@ if __name__ == "__main__":
2007
2098
  elif cmd == "watch":
2008
2099
  proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
2009
2100
  name = flags.get("name", pos[1] if len(pos) > 1 else "agent")
2010
- interval = int(flags.get("interval", pos[2] if len(pos) > 2 else "10"))
2011
- timeout = int(flags.get("timeout", pos[3] if len(pos) > 3 else "0"))
2101
+ try:
2102
+ interval = int(flags.get("interval", pos[2] if len(pos) > 2 else "10"))
2103
+ except (ValueError, TypeError):
2104
+ interval = 10
2105
+ try:
2106
+ timeout = int(flags.get("timeout", pos[3] if len(pos) > 3 else "0"))
2107
+ except (ValueError, TypeError):
2108
+ timeout = 0
2012
2109
  watch(proj, name, interval, timeout)
2013
2110
 
2014
2111
  elif cmd == "board":
@@ -2230,12 +2327,15 @@ if __name__ == "__main__":
2230
2327
  print(" meshcode scan # reads from clipboard")
2231
2328
  print(" cat identicon.txt | meshcode scan # reads from stdin")
2232
2329
  sys.exit(1)
2233
- # Look for agent or meshwork/agent
2234
- match = _re.search(r'⟨\s*(\S+)\s*⟩', art_text)
2235
- if not match:
2330
+ # Find ALL identicon tags, prefer the one with project/agent format.
2331
+ # Identicons contain two tags: ⟨ agent ⟩ and ⟨ project/agent ⟩.
2332
+ # re.search returns the first (agent-only), missing the project.
2333
+ all_tags = _re.findall(r'⟨\s*(\S+)\s*⟩', art_text)
2334
+ if not all_tags:
2236
2335
  print("[meshcode] No identicon tag found. Expected: ⟨ agent_name ⟩")
2237
2336
  sys.exit(1)
2238
- tag = match.group(1)
2337
+ # Prefer project/agent tag, fall back to agent-only
2338
+ tag = next((t for t in all_tags if "/" in t), all_tags[0])
2239
2339
  if "/" in tag:
2240
2340
  _proj, _agent = tag.split("/", 1)
2241
2341
  else:
@@ -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
@@ -62,7 +65,7 @@ class _CircuitBreaker:
62
65
  return self.state == self.OPEN
63
66
 
64
67
 
65
- _circuit = _CircuitBreaker(failure_threshold=5, recovery_timeout=30.0)
68
+ _circuit = _CircuitBreaker(failure_threshold=5, recovery_timeout=10.0)
66
69
 
67
70
  # Bake in production defaults — RLS-protected publishable key, safe to ship.
68
71
  _DEFAULT_SUPABASE_URL = "https://gjinagyyjttyxnaoavnz.supabase.co"
@@ -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)
@@ -898,7 +923,9 @@ meshcode_expand_link(). No sensitive msgs cross-mesh.
898
923
 
899
924
  MEMORY: meshcode_remember(key, value) persists across sessions.
900
925
  meshcode_recall(key?) retrieves. meshcode_forget(key) deletes.
901
- Auto-remember after each task: mistakes, feedback, patterns, preferences.
926
+ Only remember reusable learnings: mistakes, feedback, patterns, preferences.
927
+ Do NOT save task summaries — tasks already persist in the task system.
928
+ Do NOT use memory for session state or ephemeral data.
902
929
  Save reusable code patterns as template_* keys for instant recall.
903
930
 
904
931
  SCRATCHPAD: meshcode_scratchpad_set/get for shared meshwork-level context
@@ -912,7 +939,15 @@ what CLI command to run next (e.g. "meshcode run backend in a new terminal").
912
939
  Setup help → README.md or https://meshcode.io/docs
913
940
  """
914
941
  # 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'))
942
+ # Match leader-like agent names and roles across languages.
943
+ # Substring match: "orchestrator" hits on "orchestrat", "coordinador" hits on "coordinat".
944
+ _leader_haystack = ((_ROLE_DESCRIPTION or '') + ' ' + AGENT_NAME).lower()
945
+ _LEADER_KEYWORDS = (
946
+ 'commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
947
+ 'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
948
+ 'chief', 'captain', 'boss', 'head agent',
949
+ )
950
+ is_leader = any(k in _leader_haystack for k in _LEADER_KEYWORDS)
916
951
  if is_leader:
917
952
  base += """
918
953
  COMMANDER PROTOCOL (you are the team lead):
@@ -1097,13 +1132,36 @@ def _get_parent_cpu() -> float:
1097
1132
  return 0.0
1098
1133
 
1099
1134
 
1135
+ _heartbeat_crash_count = 0
1136
+
1100
1137
  def _heartbeat_thread_fn():
1101
1138
  """Heartbeat in a DAEMON THREAD — independent of asyncio event loop.
1102
1139
 
1103
1140
  This ensures heartbeats continue even when tool calls are cancelled,
1104
1141
  meshcode_wait is rejected, or the asyncio loop is busy. The agent
1105
1142
  stays 'online' in the dashboard as long as the MCP process is alive.
1143
+
1144
+ Wrapped in an outer try/except so uncaught exceptions (including from
1145
+ _heartbeat_stop.wait()) restart the loop instead of silently killing
1146
+ the thread — which would leave the agent as a ghost on the dashboard.
1106
1147
  """
1148
+ global _heartbeat_crash_count
1149
+ while not _heartbeat_stop.is_set():
1150
+ try:
1151
+ _heartbeat_loop_inner()
1152
+ except Exception as e:
1153
+ _heartbeat_crash_count += 1
1154
+ log.error(
1155
+ f"heartbeat thread crashed ({_heartbeat_crash_count}x): {e} — "
1156
+ f"restarting in 5s to prevent ghost agent"
1157
+ )
1158
+ # Brief sleep before restart to avoid tight crash loop
1159
+ _heartbeat_stop.wait(5)
1160
+
1161
+
1162
+ def _heartbeat_loop_inner():
1163
+ """Single iteration of the heartbeat loop. Separated so the outer
1164
+ wrapper can catch any exception and restart cleanly."""
1107
1165
  lease_counter = 0
1108
1166
  while not _heartbeat_stop.is_set():
1109
1167
  try:
@@ -1136,12 +1194,33 @@ def _heartbeat_thread_fn():
1136
1194
  # Sync current state to DB (in case realtime missed it)
1137
1195
  try:
1138
1196
  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")
1197
+ except Exception as e:
1198
+ log.debug(f"status sync failed: {e}")
1199
+ # Realtime subscription recovery: if the WebSocket is connected but
1200
+ # the channel subscription dropped (e.g. Supabase maintenance), or
1201
+ # the WebSocket itself disconnected, trigger a restart so agents
1202
+ # don't stay stuck in slow DB polling for the entire session.
1203
+ if _REALTIME:
1204
+ if not _REALTIME.is_connected:
1205
+ log.warning("heartbeat ok (HTTP) but WebSocket disconnected — scheduling Realtime restart")
1206
+ try:
1207
+ loop = asyncio.get_event_loop()
1208
+ if loop.is_running():
1209
+ asyncio.run_coroutine_threadsafe(_REALTIME.restart(), loop)
1210
+ except Exception as e:
1211
+ log.debug(f"Realtime restart scheduling failed: {e}")
1212
+ elif not _REALTIME.is_subscribed:
1213
+ log.warning("WebSocket connected but subscription lost — scheduling Realtime restart")
1214
+ try:
1215
+ loop = asyncio.get_event_loop()
1216
+ if loop.is_running():
1217
+ asyncio.run_coroutine_threadsafe(_REALTIME.restart(), loop)
1218
+ except Exception as e:
1219
+ log.debug(f"Realtime restart scheduling failed: {e}")
1220
+ else:
1221
+ log.debug(f"heartbeat ok for {AGENT_NAME}")
1143
1222
  else:
1144
- log.debug(f"heartbeat ok for {AGENT_NAME}")
1223
+ log.debug(f"heartbeat ok for {AGENT_NAME} (no Realtime)")
1145
1224
  except Exception as e:
1146
1225
  log.warning(f"heartbeat failed: {e}")
1147
1226
 
@@ -1321,6 +1400,9 @@ async def meshcode_debug_sleep(seconds: int = 30) -> Dict[str, Any]:
1321
1400
  def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1322
1401
  sensitive: bool = False, encrypted: bool = False) -> Dict[str, Any]:
1323
1402
  """Send message. Use "agent@meshwork" for cross-mesh. sensitive=True hides from exports. Pass encrypted=True for secrets/credentials (AES-256-GCM)."""
1403
+ if not to or not to.strip():
1404
+ return {"error": "recipient 'to' cannot be empty"}
1405
+ to = to.strip()
1324
1406
  if isinstance(message, str):
1325
1407
  # Auto-wrap strings into dict. Warn if very long but don't reject.
1326
1408
  if len(message) > 2000:
@@ -1528,6 +1610,22 @@ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
1528
1610
  or t.get("assignee") == AGENT_NAME # Directly assigned to me
1529
1611
  )
1530
1612
  ]
1613
+ # For leader agents: also include unclaimed '*' tasks as pending
1614
+ # so commanders auto-triage them instead of letting them pile up.
1615
+ _leader_haystack = ((_ROLE_DESCRIPTION or '') + ' ' + AGENT_NAME).lower()
1616
+ _LEADER_KW = ('commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
1617
+ 'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
1618
+ 'chief', 'captain', 'boss', 'head agent')
1619
+ is_leader = any(k in _leader_haystack for k in _LEADER_KW)
1620
+ if is_leader:
1621
+ wildcard_tasks = [
1622
+ {"id": t["id"][:8], "title": t["title"][:80], "priority": t.get("priority", "normal"), "status": t["status"]}
1623
+ for t in tasks
1624
+ if t.get("status") == "open"
1625
+ and t.get("assignee") == "*"
1626
+ and not t.get("claimed_by")
1627
+ ]
1628
+ pending.extend(wildcard_tasks)
1531
1629
  return pending if pending else None
1532
1630
  except Exception:
1533
1631
  return None
@@ -1553,14 +1651,22 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
1553
1651
  global _IN_WAIT, _CONSECUTIVE_IDLE_SECONDS, _LAST_SEEN_TS
1554
1652
 
1555
1653
  # PRODUCT RULE 1: If agent has open tasks, refuse to wait. Work first.
1654
+ # Exception: commander/leader agents can wait while monitoring — they
1655
+ # delegate tasks and need to stay in the wait loop to receive reports.
1556
1656
  pending_tasks = _get_pending_tasks_summary()
1557
1657
  if pending_tasks:
1558
- return {
1559
- "refused": True,
1560
- "reason": "You have open tasks. Work them before entering wait.",
1561
- "pending_tasks": pending_tasks,
1562
- "count": len(pending_tasks),
1563
- }
1658
+ _leader_haystack = ((_ROLE_DESCRIPTION or '') + ' ' + AGENT_NAME).lower()
1659
+ _LEADER_KW = ('commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
1660
+ 'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
1661
+ 'chief', 'captain', 'boss', 'head agent')
1662
+ _is_leader = any(k in _leader_haystack for k in _LEADER_KW)
1663
+ if not _is_leader:
1664
+ return {
1665
+ "refused": True,
1666
+ "reason": "You have open tasks. Work them before entering wait.",
1667
+ "pending_tasks": pending_tasks,
1668
+ "count": len(pending_tasks),
1669
+ }
1564
1670
 
1565
1671
  # PRODUCT RULE 2: If agent has unread messages in DB, refuse to wait.
1566
1672
  try:
@@ -1626,8 +1732,8 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
1626
1732
  "p_status": "sleeping",
1627
1733
  "p_task": f"idle {_CONSECUTIVE_IDLE_SECONDS}s — still listening",
1628
1734
  })
1629
- except Exception:
1630
- pass
1735
+ except Exception as e:
1736
+ log.debug(f"auto-sleep status update failed: {e}")
1631
1737
  # Do NOT return — keep looping. Status says sleeping but
1632
1738
  # we are still listening for messages via realtime.
1633
1739
  _set_state("sleeping", f"idle {_CONSECUTIVE_IDLE_SECONDS}s — still listening")
@@ -1652,8 +1758,8 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
1652
1758
  "p_tier": "critical",
1653
1759
  "p_project_name": PROJECT_NAME,
1654
1760
  })
1655
- except Exception:
1656
- pass
1761
+ except Exception as e:
1762
+ log.debug(f"last_seen memory persist failed: {e}")
1657
1763
  return result
1658
1764
  finally:
1659
1765
  _IN_WAIT = False
@@ -1681,8 +1787,8 @@ def _mark_realtime_msgs_read_in_db(messages: List[Dict[str, Any]]) -> None:
1681
1787
  "p_project_id": _PROJECT_ID,
1682
1788
  "p_message_id": mid,
1683
1789
  })
1684
- except Exception:
1685
- pass
1790
+ except Exception as e:
1791
+ log.debug(f"mark_read failed for msg {mid}: {e}")
1686
1792
  threading.Thread(target=_do, daemon=True).start()
1687
1793
 
1688
1794
 
@@ -1776,8 +1882,8 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
1776
1882
  split["acks"] = []
1777
1883
  if split["messages"] or split["done_signals"]:
1778
1884
  return {"got_message": True, "source": "db_poll_fallback", **split}
1779
- except Exception:
1780
- pass
1885
+ except Exception as e:
1886
+ log.debug(f"DB poll fallback error: {e}")
1781
1887
 
1782
1888
  # Final fallback: one last DB check (covers realtime path missing msgs)
1783
1889
  try:
@@ -1800,8 +1906,8 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
1800
1906
  split["acks"] = []
1801
1907
  if split["messages"] or split["done_signals"]:
1802
1908
  return {"got_message": True, "source": "db_fallback", **split}
1803
- except Exception:
1804
- pass
1909
+ except Exception as e:
1910
+ log.debug(f"final DB fallback error: {e}")
1805
1911
 
1806
1912
  # Check if there's any pending work before returning timeout
1807
1913
  pending_tasks = _get_pending_tasks_summary()
@@ -1973,8 +2079,10 @@ def meshcode_init(project: str, agent: str, role: str = "") -> Dict[str, Any]:
1973
2079
  def meshcode_task_create(title: str, description: str = "", assignee: str = "*",
1974
2080
  priority: str = "normal", parent_task_id: Optional[str] = None) -> Dict[str, Any]:
1975
2081
  """Create task. assignee="*" for any, priority: low/normal/high/urgent."""
2082
+ if not title or not title.strip():
2083
+ return {"error": "title cannot be empty"}
1976
2084
  api_key = _get_api_key()
1977
- result = be.task_create(api_key, _PROJECT_ID, AGENT_NAME, title,
2085
+ result = be.task_create(api_key, _PROJECT_ID, AGENT_NAME, title.strip(),
1978
2086
  description=description, assignee=assignee,
1979
2087
  priority=priority, parent_task_id=parent_task_id)
1980
2088
  # Auto-notify assignee so they wake from meshcode_wait
@@ -2045,7 +2153,7 @@ def meshcode_task_claim(task_id: str) -> Dict[str, Any]:
2045
2153
  @mcp.tool()
2046
2154
  @with_working_status
2047
2155
  def meshcode_task_complete(task_id: str, summary: str = "", force: bool = False) -> Dict[str, Any]:
2048
- """Complete a claimed task with summary. Auto-remembers the task summary.
2156
+ """Complete a claimed task with summary.
2049
2157
 
2050
2158
  Refuses if the task has open subtasks, unless force=True is passed with a
2051
2159
  reason in `summary`. This prevents closing a parent while lanes are still
@@ -2071,25 +2179,8 @@ def meshcode_task_complete(task_id: str, summary: str = "", force: bool = False)
2071
2179
  except Exception:
2072
2180
  pass # Best-effort check; don't block on listing failure.
2073
2181
  result = be.task_complete(api_key, _PROJECT_ID, task_id, AGENT_NAME, summary=summary)
2074
- # Auto-remember task completion for future context
2075
- if isinstance(result, dict) and result.get("ok") and summary:
2076
- try:
2077
- import threading
2078
- def _auto_remember():
2079
- try:
2080
- be.sb_rpc("mc_memory_set", {
2081
- "p_api_key": api_key,
2082
- "p_agent_name": AGENT_NAME,
2083
- "p_key": f"task_{task_id[:8]}",
2084
- "p_value": {"title": result.get("title", ""), "summary": summary, "completed": True},
2085
- "p_tier": "episodic",
2086
- "p_project_name": PROJECT_NAME,
2087
- })
2088
- except Exception:
2089
- pass
2090
- threading.Thread(target=_auto_remember, daemon=True).start()
2091
- except Exception:
2092
- pass
2182
+ # Task data persists in the task system do NOT duplicate to memory.
2183
+ # Samuel: "los tasks no deben guardarse en memoria, para eso salen en tasks"
2093
2184
  return result
2094
2185
 
2095
2186
 
@@ -2191,6 +2282,7 @@ def meshcode_auto_wake() -> Dict[str, Any]:
2191
2282
  suggestions: List[Dict[str, str]] = []
2192
2283
 
2193
2284
  # 1. Check for stale agents (heartbeat >10 min, not offline/sleeping)
2285
+ agents = [] # Initialize before try so downstream checks don't NameError
2194
2286
  try:
2195
2287
  agents = be.get_board(_PROJECT_ID)
2196
2288
  import datetime as _dt
@@ -2530,12 +2622,15 @@ source: meshcode
2530
2622
  @mcp.tool()
2531
2623
  @with_working_status
2532
2624
  def meshcode_remember(key: str, value: Any) -> Dict[str, Any]:
2533
- """Store a persistent memory (survives restarts). Auto-remember after each task.
2625
+ """Store a persistent memory (survives restarts). Only for reusable learnings, NOT task data.
2534
2626
 
2535
2627
  Args:
2536
2628
  key: Short key (e.g. "team_conventions").
2537
2629
  value: Any JSON-serializable value.
2538
2630
  """
2631
+ if not key or not key.strip():
2632
+ return {"error": "key cannot be empty"}
2633
+ key = key.strip()
2539
2634
  api_key = _get_api_key()
2540
2635
  json_value = value if isinstance(value, (dict, list)) else json.dumps(value)
2541
2636
  if isinstance(json_value, str):
@@ -2671,7 +2766,7 @@ def meshcode_health() -> Dict[str, Any]:
2671
2766
  }
2672
2767
 
2673
2768
  # Realtime status
2674
- health["realtime_connected"] = getattr(_rt_state, 'connected', False) if '_rt_state' in dir() else "unknown"
2769
+ health["realtime_connected"] = _REALTIME.is_connected if _REALTIME else False
2675
2770
 
2676
2771
  # Process uptime
2677
2772
  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.36
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.36"
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