meshcode 2.10.33__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.33 → meshcode-2.10.35}/PKG-INFO +34 -2
- {meshcode-2.10.33 → meshcode-2.10.35}/README.md +33 -1
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/__init__.py +1 -1
- meshcode-2.10.35/meshcode/meshcode_mcp/__init__.py +22 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/meshcode_mcp/__main__.py +29 -6
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/meshcode_mcp/backend.py +40 -4
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/meshcode_mcp/realtime.py +30 -3
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/meshcode_mcp/server.py +98 -21
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/run_agent.py +127 -60
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode.egg-info/PKG-INFO +34 -2
- {meshcode-2.10.33 → meshcode-2.10.35}/pyproject.toml +1 -1
- meshcode-2.10.33/meshcode/meshcode_mcp/__init__.py +0 -2
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/cli.py +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/invites.py +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/launcher.py +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/preferences.py +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/secrets.py +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/self_update.py +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.33 → meshcode-2.10.35}/setup.cfg +0 -0
- {meshcode-2.10.33 → 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:
|
|
@@ -177,7 +177,7 @@ def _check_agent_ownership(agent: str, project: str) -> Optional[str]:
|
|
|
177
177
|
import importlib
|
|
178
178
|
secrets_mod = importlib.import_module("meshcode.secrets")
|
|
179
179
|
except Exception:
|
|
180
|
-
return
|
|
180
|
+
return "cannot load auth modules; re-install meshcode"
|
|
181
181
|
|
|
182
182
|
# Try scoped profile first (from invite join), then default
|
|
183
183
|
scoped_profile = f"mesh:{project}:{agent}"
|
|
@@ -186,7 +186,7 @@ def _check_agent_ownership(agent: str, project: str) -> Optional[str]:
|
|
|
186
186
|
profile = os.environ.get("MESHCODE_KEYCHAIN_PROFILE") or "default"
|
|
187
187
|
api_key = secrets_mod.get_api_key(profile=profile)
|
|
188
188
|
if not api_key:
|
|
189
|
-
return
|
|
189
|
+
return "not logged in — run `meshcode login <api_key>` first"
|
|
190
190
|
|
|
191
191
|
sb = _load_supabase_env()
|
|
192
192
|
try:
|
|
@@ -208,35 +208,35 @@ def _check_agent_ownership(agent: str, project: str) -> Optional[str]:
|
|
|
208
208
|
)
|
|
209
209
|
with urlopen(req, timeout=10) as resp:
|
|
210
210
|
data = json.loads(resp.read().decode())
|
|
211
|
-
except Exception:
|
|
212
|
-
return
|
|
211
|
+
except Exception as e:
|
|
212
|
+
return f"could not verify meshwork access ({e}); refusing to launch"
|
|
213
213
|
|
|
214
214
|
if isinstance(data, dict) and data.get("error"):
|
|
215
215
|
return data["error"]
|
|
216
|
+
if not isinstance(data, dict) or not data.get("ok"):
|
|
217
|
+
return "ownership check returned unexpected response; refusing to launch"
|
|
216
218
|
return None
|
|
217
219
|
|
|
218
220
|
|
|
219
|
-
def
|
|
220
|
-
"""
|
|
221
|
+
def _resolve_user_projects_for_agent(agent: str) -> Optional[list]:
|
|
222
|
+
"""Ask the server which meshworks owned by the current user have this agent.
|
|
221
223
|
|
|
222
|
-
Returns
|
|
224
|
+
Returns list of dicts [{project_name, project_id, role}], or None on auth/network error.
|
|
225
|
+
Always scoped to the authenticated user (RPC enforces owner_id = caller user).
|
|
223
226
|
"""
|
|
224
227
|
try:
|
|
225
|
-
from .setup_clients import _load_supabase_env
|
|
228
|
+
from .setup_clients import _load_supabase_env
|
|
226
229
|
import importlib
|
|
227
230
|
secrets_mod = importlib.import_module("meshcode.secrets")
|
|
228
231
|
except Exception:
|
|
229
232
|
return None
|
|
230
233
|
|
|
231
|
-
|
|
232
|
-
profile = _os.environ.get("MESHCODE_KEYCHAIN_PROFILE") or "default"
|
|
234
|
+
profile = os.environ.get("MESHCODE_KEYCHAIN_PROFILE") or "default"
|
|
233
235
|
api_key = secrets_mod.get_api_key(profile=profile)
|
|
234
236
|
if not api_key:
|
|
235
237
|
return None
|
|
236
238
|
|
|
237
239
|
sb = _load_supabase_env()
|
|
238
|
-
|
|
239
|
-
# Ask the server which project(s) this agent belongs to
|
|
240
240
|
try:
|
|
241
241
|
from urllib.request import Request, urlopen
|
|
242
242
|
body = json.dumps({"p_api_key": api_key, "p_agent_name": agent}).encode()
|
|
@@ -256,27 +256,36 @@ def _try_auto_setup(agent: str, project: Optional[str] = None) -> Optional[Tuple
|
|
|
256
256
|
return None
|
|
257
257
|
|
|
258
258
|
if not isinstance(data, dict) or data.get("error"):
|
|
259
|
+
return []
|
|
260
|
+
projects = data.get("projects") or []
|
|
261
|
+
return projects if isinstance(projects, list) else []
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _try_auto_setup(agent: str, project: str) -> Optional[Tuple[Path, str]]:
|
|
265
|
+
"""If agent exists on the server but has no local workspace, auto-create it.
|
|
266
|
+
|
|
267
|
+
Caller MUST pass an explicit project — this function never auto-picks one.
|
|
268
|
+
Returns (workspace_path, project_name) on success, None on failure.
|
|
269
|
+
"""
|
|
270
|
+
if not project:
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
from .setup_clients import setup_workspace
|
|
275
|
+
except Exception:
|
|
259
276
|
return None
|
|
260
277
|
|
|
261
|
-
projects =
|
|
278
|
+
projects = _resolve_user_projects_for_agent(agent)
|
|
262
279
|
if not projects:
|
|
263
280
|
return None
|
|
264
281
|
|
|
265
|
-
#
|
|
266
|
-
if project
|
|
267
|
-
|
|
268
|
-
if not projects:
|
|
269
|
-
return None
|
|
270
|
-
|
|
271
|
-
if len(projects) > 1:
|
|
272
|
-
print(f"[meshcode] Agent '{agent}' exists in multiple projects:", file=sys.stderr)
|
|
273
|
-
for p in projects:
|
|
274
|
-
print(f"[meshcode] meshcode run {agent} --project {p['project_name']}", file=sys.stderr)
|
|
275
|
-
print(f"[meshcode] Specify which one with --project.", file=sys.stderr)
|
|
282
|
+
# Filter to the explicitly requested project. Never silently auto-pick.
|
|
283
|
+
match = [p for p in projects if p.get("project_name") == project]
|
|
284
|
+
if not match:
|
|
276
285
|
return None
|
|
277
286
|
|
|
278
|
-
resolved_project =
|
|
279
|
-
role =
|
|
287
|
+
resolved_project = match[0]["project_name"]
|
|
288
|
+
role = match[0].get("role", "")
|
|
280
289
|
|
|
281
290
|
print(f"[meshcode] Workspace recreated automatically for agent '{agent}' (project: {resolved_project})")
|
|
282
291
|
rc = setup_workspace(resolved_project, agent, role)
|
|
@@ -298,50 +307,50 @@ def _load_registry() -> dict:
|
|
|
298
307
|
return {}
|
|
299
308
|
|
|
300
309
|
|
|
301
|
-
def _find_agent_workspace(agent: str, project:
|
|
302
|
-
"""Look up the agent
|
|
310
|
+
def _find_agent_workspace(agent: str, project: str, quiet: bool = False) -> Optional[Tuple[Path, str]]:
|
|
311
|
+
"""Look up the agent's workspace for an EXPLICIT project. Returns (path, project) or None.
|
|
303
312
|
|
|
304
|
-
|
|
305
|
-
user to disambiguate by passing project explicitly.
|
|
313
|
+
Caller MUST pass project — this function never auto-picks across meshworks.
|
|
306
314
|
When quiet=True, suppresses error messages (used before auto-setup fallback).
|
|
307
315
|
"""
|
|
316
|
+
if not project:
|
|
317
|
+
return None
|
|
318
|
+
|
|
308
319
|
reg = _load_registry()
|
|
309
320
|
agents = reg.get("agents", {})
|
|
310
321
|
|
|
311
|
-
# Direct hit by agent name (current model: agent names unique within registry)
|
|
312
322
|
info = agents.get(agent)
|
|
313
|
-
if info:
|
|
323
|
+
if info and info.get("project") == project:
|
|
314
324
|
ws = Path(info["workspace"])
|
|
315
|
-
if not ws.exists():
|
|
316
|
-
# Don't print error in quiet mode — caller will try auto-setup
|
|
317
|
-
return None
|
|
318
|
-
if project and info.get("project") != project:
|
|
319
|
-
if not quiet:
|
|
320
|
-
print(f"[meshcode] ERROR: agent '{agent}' belongs to project '{info.get('project')}', not '{project}'", file=sys.stderr)
|
|
321
|
-
return None
|
|
322
|
-
return ws, info.get("project", "")
|
|
323
|
-
|
|
324
|
-
# Fallback: scan ~/meshcode for any dir matching <project>-<agent>
|
|
325
|
-
if project:
|
|
326
|
-
ws = WORKSPACES_ROOT / f"{project}-{agent}"
|
|
327
325
|
if ws.exists():
|
|
328
326
|
return ws, project
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
print(f"[meshcode] Disambiguate: meshcode run {agent} --project <name>", file=sys.stderr)
|
|
340
|
-
return None
|
|
327
|
+
# Registry points at a missing dir — caller will try auto-setup
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
if info and info.get("project") != project and not quiet:
|
|
331
|
+
print(f"[meshcode] NOTE: registry has agent '{agent}' under project '{info.get('project')}', not '{project}'", file=sys.stderr)
|
|
332
|
+
|
|
333
|
+
# Filesystem fallback: ~/meshcode/<project>-<agent>
|
|
334
|
+
ws = WORKSPACES_ROOT / f"{project}-{agent}"
|
|
335
|
+
if ws.exists():
|
|
336
|
+
return ws, project
|
|
341
337
|
|
|
342
338
|
return None
|
|
343
339
|
|
|
344
340
|
|
|
341
|
+
def _list_local_projects_for_agent(agent: str) -> list:
|
|
342
|
+
"""Return project names whose local workspace dir matches <project>-<agent>."""
|
|
343
|
+
if not WORKSPACES_ROOT.exists():
|
|
344
|
+
return []
|
|
345
|
+
out = []
|
|
346
|
+
for p in WORKSPACES_ROOT.iterdir():
|
|
347
|
+
if p.is_dir() and p.name.endswith(f"-{agent}"):
|
|
348
|
+
proj = p.name.rsplit(f"-{agent}", 1)[0]
|
|
349
|
+
if proj:
|
|
350
|
+
out.append(proj)
|
|
351
|
+
return out
|
|
352
|
+
|
|
353
|
+
|
|
345
354
|
def _detect_editor() -> Optional[str]:
|
|
346
355
|
"""Pick the user's preferred MCP-aware editor."""
|
|
347
356
|
override = os.environ.get("MESHCODE_EDITOR", "").strip().lower()
|
|
@@ -406,16 +415,70 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
406
415
|
print(f"[meshcode] (or copy this exact line into a new terminal: meshcode run {agent})", file=sys.stderr)
|
|
407
416
|
return 2
|
|
408
417
|
|
|
418
|
+
# ── Require explicit meshwork ────────────────────────────────────
|
|
419
|
+
# Agent names are not globally unique (e.g. "commander" exists in many
|
|
420
|
+
# meshworks). Without an explicit meshwork, we cannot safely pick one
|
|
421
|
+
# — even if only one is visible locally, another could exist on the
|
|
422
|
+
# server. Force the user to disambiguate and authenticate against a
|
|
423
|
+
# specific meshwork they own.
|
|
424
|
+
if not project:
|
|
425
|
+
print(f"[meshcode] ERROR: specify which meshwork '{agent}' belongs to.", file=sys.stderr)
|
|
426
|
+
local_projects = _list_local_projects_for_agent(agent)
|
|
427
|
+
server_projects = _resolve_user_projects_for_agent(agent) or []
|
|
428
|
+
candidates = set(local_projects) | {p.get("project_name") for p in server_projects if p.get("project_name")}
|
|
429
|
+
if candidates:
|
|
430
|
+
print(f"[meshcode] '{agent}' is available in these meshworks of yours:", file=sys.stderr)
|
|
431
|
+
for name in sorted(candidates):
|
|
432
|
+
print(f"[meshcode] meshcode run {name}/{agent}", file=sys.stderr)
|
|
433
|
+
else:
|
|
434
|
+
print(f"[meshcode] No meshworks of yours contain agent '{agent}'.", file=sys.stderr)
|
|
435
|
+
print(f"[meshcode] Create one first: meshcode setup <meshwork> {agent}", file=sys.stderr)
|
|
436
|
+
print(f"[meshcode] Usage: meshcode run <meshwork>/{agent} (or --project <meshwork>)", file=sys.stderr)
|
|
437
|
+
return 2
|
|
438
|
+
|
|
409
439
|
found = _find_agent_workspace(agent, project, quiet=True)
|
|
410
440
|
if not found:
|
|
411
|
-
# Auto-setup: if agent exists on the server, recreate workspace
|
|
441
|
+
# Auto-setup: if agent exists on the server under THIS meshwork, recreate workspace
|
|
412
442
|
found = _try_auto_setup(agent, project)
|
|
413
443
|
if not found:
|
|
414
|
-
print(f"[meshcode] ERROR:
|
|
415
|
-
print(f"[meshcode] Run `meshcode setup
|
|
444
|
+
print(f"[meshcode] ERROR: agent '{agent}' not found in meshwork '{project}' for your account.", file=sys.stderr)
|
|
445
|
+
print(f"[meshcode] Run `meshcode setup {project} {agent}` first,", file=sys.stderr)
|
|
446
|
+
print(f"[meshcode] or check the meshwork name at https://meshcode.io", file=sys.stderr)
|
|
416
447
|
return 2
|
|
417
448
|
ws, resolved_project = found
|
|
418
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
|
+
|
|
419
482
|
# ── Ownership pre-check ──────────────────────────────────────────
|
|
420
483
|
# Before launching the editor, verify the caller owns this agent.
|
|
421
484
|
# This prevents hijacking another user's agent in shared meshworks.
|
|
@@ -448,7 +511,11 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
448
511
|
from .ascii_art import generate_art, render_welcome
|
|
449
512
|
from . import __version__ as cli_version
|
|
450
513
|
ascii_art, agent_role, profile_color = _fetch_or_generate_art(agent, resolved_project)
|
|
451
|
-
|
|
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)
|
|
452
519
|
agent_stats = _fetch_agent_stats(agent, resolved_project)
|
|
453
520
|
print(render_welcome(
|
|
454
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
|