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.
- {meshcode-2.10.34 → meshcode-2.10.36}/PKG-INFO +34 -2
- {meshcode-2.10.34 → meshcode-2.10.36}/README.md +33 -1
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/__init__.py +1 -1
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/comms_v4.py +108 -8
- meshcode-2.10.36/meshcode/meshcode_mcp/__init__.py +22 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/meshcode_mcp/__main__.py +29 -6
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/meshcode_mcp/backend.py +41 -5
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/meshcode_mcp/realtime.py +30 -3
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/meshcode_mcp/server.py +145 -50
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/run_agent.py +37 -1
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode.egg-info/PKG-INFO +34 -2
- {meshcode-2.10.34 → meshcode-2.10.36}/pyproject.toml +1 -1
- meshcode-2.10.34/meshcode/meshcode_mcp/__init__.py +0 -2
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/cli.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/invites.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/launcher.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/preferences.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/secrets.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/self_update.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.34 → meshcode-2.10.36}/setup.cfg +0 -0
- {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.
|
|
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
|
-
**
|
|
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.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
|
-
|
|
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
|
-
|
|
2011
|
-
|
|
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
|
-
#
|
|
2234
|
-
|
|
2235
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -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=
|
|
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
|
-
|
|
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)
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1141
|
-
if
|
|
1142
|
-
|
|
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
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
#
|
|
2075
|
-
|
|
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).
|
|
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"] =
|
|
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
|
-
|
|
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.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
|
-
**
|
|
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
|