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.
- {meshcode-2.10.34 → meshcode-2.10.35}/PKG-INFO +34 -2
- {meshcode-2.10.34 → meshcode-2.10.35}/README.md +33 -1
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/__init__.py +1 -1
- meshcode-2.10.35/meshcode/meshcode_mcp/__init__.py +22 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/meshcode_mcp/__main__.py +29 -6
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/meshcode_mcp/backend.py +40 -4
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/meshcode_mcp/realtime.py +30 -3
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/meshcode_mcp/server.py +98 -21
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/run_agent.py +37 -1
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode.egg-info/PKG-INFO +34 -2
- {meshcode-2.10.34 → meshcode-2.10.35}/pyproject.toml +1 -1
- meshcode-2.10.34/meshcode/meshcode_mcp/__init__.py +0 -2
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/cli.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/invites.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/launcher.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/preferences.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/secrets.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/self_update.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.34 → meshcode-2.10.35}/setup.cfg +0 -0
- {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.
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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.
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1141
|
-
if
|
|
1142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"] =
|
|
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
|
-
|
|
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.
|
|
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
|
-
**
|
|
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
|
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|