meshcode 2.0.3__tar.gz → 2.0.5__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.0.3 → meshcode-2.0.5}/PKG-INFO +1 -1
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/__init__.py +1 -1
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/comms_v4.py +25 -10
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/meshcode_mcp/backend.py +12 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/meshcode_mcp/server.py +93 -4
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.0.3 → meshcode-2.0.5}/pyproject.toml +1 -1
- {meshcode-2.0.3 → meshcode-2.0.5}/README.md +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/cli.py +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/invites.py +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/launcher.py +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/launcher_install.py +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/preferences.py +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/run_agent.py +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/secrets.py +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/self_update.py +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/setup_clients.py +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.0.3 → meshcode-2.0.5}/setup.cfg +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "2.0.
|
|
2
|
+
__version__ = "2.0.5"
|
|
@@ -1634,14 +1634,15 @@ STATUS:
|
|
|
1634
1634
|
HISTORY:
|
|
1635
1635
|
history <proj> [count] [agent1,agent2] Conversation log
|
|
1636
1636
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1637
|
+
QUICK START:
|
|
1638
|
+
go <agent> [--project <name>] One command to connect (recommended)
|
|
1639
1639
|
login <api_key> Authenticate with API key
|
|
1640
1640
|
|
|
1641
|
-
|
|
1642
|
-
setup <
|
|
1643
|
-
|
|
1644
|
-
|
|
1641
|
+
SETUP (advanced):
|
|
1642
|
+
setup <proj> <name> [role] Create workspace (auto by 'go')
|
|
1643
|
+
run <agent> [--project <name>] Launch agent (auto by 'go')
|
|
1644
|
+
setup <client> <proj> <name> [role] Legacy: global MCP config
|
|
1645
|
+
Clients: claude-desktop
|
|
1645
1646
|
|
|
1646
1647
|
CLEANUP:
|
|
1647
1648
|
clear <proj> <name> Clear inbox
|
|
@@ -2042,10 +2043,11 @@ if __name__ == "__main__":
|
|
|
2042
2043
|
_setup_dispatcher = importlib.import_module("meshcode.setup_clients").setup
|
|
2043
2044
|
sys.exit(_setup_dispatcher(*sys.argv[2:]))
|
|
2044
2045
|
|
|
2045
|
-
elif cmd
|
|
2046
|
-
# meshcode
|
|
2046
|
+
elif cmd in ("run", "go"):
|
|
2047
|
+
# meshcode go <agent> [--project <name>] [--editor claude|cursor|code]
|
|
2048
|
+
# meshcode run <agent> (backwards compat, same as go)
|
|
2047
2049
|
if len(sys.argv) < 3:
|
|
2048
|
-
print("Usage: meshcode
|
|
2050
|
+
print(f"Usage: meshcode {cmd} <agent> [--project <name>] [--editor claude|cursor|code]")
|
|
2049
2051
|
sys.exit(1)
|
|
2050
2052
|
agent = sys.argv[2]
|
|
2051
2053
|
proj_override = flags.get("project")
|
|
@@ -2055,6 +2057,19 @@ if __name__ == "__main__":
|
|
|
2055
2057
|
perm_override = "bypass"
|
|
2056
2058
|
elif "--safe" in sys.argv:
|
|
2057
2059
|
perm_override = "safe"
|
|
2060
|
+
# Auth pre-check: if not logged in, prompt inline
|
|
2061
|
+
api_key = _load_api_key_for_cli()
|
|
2062
|
+
if not api_key:
|
|
2063
|
+
print("[meshcode] Not logged in. Get your API key at: https://meshcode.io/settings")
|
|
2064
|
+
try:
|
|
2065
|
+
api_key = input("[meshcode] Paste your API key (mc_...): ").strip()
|
|
2066
|
+
except (EOFError, KeyboardInterrupt):
|
|
2067
|
+
api_key = ""
|
|
2068
|
+
if api_key:
|
|
2069
|
+
login(api_key)
|
|
2070
|
+
else:
|
|
2071
|
+
print("[meshcode] Login required. Run: meshcode login <api_key>")
|
|
2072
|
+
sys.exit(1)
|
|
2058
2073
|
import importlib
|
|
2059
2074
|
_run = importlib.import_module("meshcode.run_agent").run
|
|
2060
2075
|
sys.exit(_run(agent, project=proj_override, editor_override=editor_override, permission_override=perm_override))
|
|
@@ -2198,7 +2213,7 @@ if __name__ == "__main__":
|
|
|
2198
2213
|
"register", "send", "broadcast", "read", "check", "watch",
|
|
2199
2214
|
"board", "update", "status", "projects", "list", "ls",
|
|
2200
2215
|
"history", "clear", "unregister", "connect", "disconnect",
|
|
2201
|
-
"setup", "run", "invite", "join", "invites", "members",
|
|
2216
|
+
"setup", "run", "go", "invite", "join", "invites", "members",
|
|
2202
2217
|
"revoke-invite", "revoke-member", "login", "prefs", "launcher",
|
|
2203
2218
|
"help", "profile", "validate-sessions", "wake-headless",
|
|
2204
2219
|
]
|
|
@@ -307,6 +307,18 @@ def task_complete(api_key, project_id, task_id, completing_agent, summary=""):
|
|
|
307
307
|
})
|
|
308
308
|
|
|
309
309
|
|
|
310
|
+
def record_event(api_key, project_id, agent_name, session_id, event_type, payload=None):
|
|
311
|
+
"""Fire-and-forget event recording for session replay."""
|
|
312
|
+
return sb_rpc("mc_record_event", {
|
|
313
|
+
"p_api_key": api_key,
|
|
314
|
+
"p_project_id": project_id,
|
|
315
|
+
"p_agent_name": agent_name,
|
|
316
|
+
"p_session_id": session_id,
|
|
317
|
+
"p_event_type": event_type,
|
|
318
|
+
"p_payload": payload or {},
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
|
|
310
322
|
def get_history(project_id: str, limit: int = 20) -> List[Dict]:
|
|
311
323
|
return sb_select(
|
|
312
324
|
"mc_messages",
|
|
@@ -145,6 +145,57 @@ def _split_messages(messages: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
|
145
145
|
from . import backend as be
|
|
146
146
|
from .realtime import RealtimeListener
|
|
147
147
|
|
|
148
|
+
# ============================================================
|
|
149
|
+
# Session Replay: unique session ID per MCP server boot.
|
|
150
|
+
# Events are recorded in background threads (fire-and-forget).
|
|
151
|
+
# ============================================================
|
|
152
|
+
import uuid as _uuid
|
|
153
|
+
_SESSION_ID = str(_uuid.uuid4())[:12]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _record_event_bg(event_type: str, payload: dict = None) -> None:
|
|
157
|
+
"""Fire-and-forget: record a session event in background thread."""
|
|
158
|
+
try:
|
|
159
|
+
api_key = _get_api_key()
|
|
160
|
+
import threading
|
|
161
|
+
threading.Thread(
|
|
162
|
+
target=be.record_event,
|
|
163
|
+
args=(api_key, _PROJECT_ID, AGENT_NAME, _SESSION_ID, event_type, payload or {}),
|
|
164
|
+
daemon=True,
|
|
165
|
+
).start()
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ============================================================
|
|
171
|
+
# Hot-reload: detect when backend.py changes on disk (e.g. after
|
|
172
|
+
# pip install --upgrade meshcode) and reload without restart.
|
|
173
|
+
# Only backend.py is safe to reload (stateless). server.py has
|
|
174
|
+
# module-level state that would be destroyed by reload.
|
|
175
|
+
# ============================================================
|
|
176
|
+
import importlib as _importlib
|
|
177
|
+
_be_mtime: float = 0.0
|
|
178
|
+
try:
|
|
179
|
+
_be_path = os.path.abspath(be.__file__)
|
|
180
|
+
_be_mtime = os.path.getmtime(_be_path)
|
|
181
|
+
except Exception:
|
|
182
|
+
_be_path = ""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _check_hot_reload() -> None:
|
|
186
|
+
"""If backend.py was modified since boot/last reload, reimport it."""
|
|
187
|
+
global _be_mtime, be
|
|
188
|
+
if not _be_path:
|
|
189
|
+
return
|
|
190
|
+
try:
|
|
191
|
+
current_mtime = os.path.getmtime(_be_path)
|
|
192
|
+
if current_mtime > _be_mtime:
|
|
193
|
+
_be_mtime = current_mtime
|
|
194
|
+
_importlib.reload(be)
|
|
195
|
+
log.info("[meshcode] Hot-reloaded backend.py (new version detected)")
|
|
196
|
+
except Exception as e:
|
|
197
|
+
log.debug(f"hot-reload check failed: {e}")
|
|
198
|
+
|
|
148
199
|
try:
|
|
149
200
|
from mcp.server.fastmcp import FastMCP
|
|
150
201
|
except ImportError:
|
|
@@ -317,8 +368,10 @@ def with_working_status(func):
|
|
|
317
368
|
if asyncio.iscoroutinefunction(func):
|
|
318
369
|
@_functools.wraps(func)
|
|
319
370
|
async def awrapper(*args, **kwargs):
|
|
371
|
+
_check_hot_reload()
|
|
320
372
|
if not skip:
|
|
321
373
|
_schedule_flip("working", name)
|
|
374
|
+
_record_event_bg("tool_call", {"tool": name, "args_keys": list(kwargs.keys())})
|
|
322
375
|
try:
|
|
323
376
|
return await func(*args, **kwargs)
|
|
324
377
|
finally:
|
|
@@ -328,8 +381,10 @@ def with_working_status(func):
|
|
|
328
381
|
else:
|
|
329
382
|
@_functools.wraps(func)
|
|
330
383
|
def swrapper(*args, **kwargs):
|
|
384
|
+
_check_hot_reload()
|
|
331
385
|
if not skip:
|
|
332
386
|
_schedule_flip("working", name)
|
|
387
|
+
_record_event_bg("tool_call", {"tool": name, "args_keys": list(kwargs.keys())})
|
|
333
388
|
try:
|
|
334
389
|
return func(*args, **kwargs)
|
|
335
390
|
finally:
|
|
@@ -574,7 +629,10 @@ async def _on_new_message(msg: Dict[str, Any]) -> None:
|
|
|
574
629
|
from_agent = msg.get("from") or msg.get("from_agent") or "unknown"
|
|
575
630
|
payload = msg.get("payload") or {}
|
|
576
631
|
preview = payload.get("text", "") if isinstance(payload, dict) else str(payload)
|
|
577
|
-
|
|
632
|
+
try:
|
|
633
|
+
_try_auto_wake(from_agent, preview[:60])
|
|
634
|
+
except Exception:
|
|
635
|
+
pass # auto-wake is best-effort, never block message handling
|
|
578
636
|
|
|
579
637
|
try:
|
|
580
638
|
srv = mcp._mcp_server # FastMCP exposes the lowlevel server here
|
|
@@ -660,9 +718,11 @@ async def lifespan(_app):
|
|
|
660
718
|
hb_thread = _threading.Thread(target=_heartbeat_thread_fn, daemon=True, name="meshcode-heartbeat")
|
|
661
719
|
hb_thread.start()
|
|
662
720
|
log.info(f"lifespan started — Realtime + heartbeat thread active for {AGENT_NAME}")
|
|
721
|
+
_record_event_bg("boot", {"agent": AGENT_NAME, "project": PROJECT_NAME, "session_id": _SESSION_ID})
|
|
663
722
|
try:
|
|
664
723
|
yield {"realtime": _REALTIME}
|
|
665
724
|
finally:
|
|
725
|
+
_record_event_bg("shutdown", {"agent": AGENT_NAME, "session_id": _SESSION_ID})
|
|
666
726
|
log.info("lifespan shutdown — stopping heartbeat + realtime + releasing lease")
|
|
667
727
|
_heartbeat_stop.set()
|
|
668
728
|
hb_thread.join(timeout=5)
|
|
@@ -852,7 +912,20 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
852
912
|
# Realtime unavailable — plain sleep fallback so we still honor timeout.
|
|
853
913
|
await asyncio.sleep(actual_timeout)
|
|
854
914
|
|
|
855
|
-
|
|
915
|
+
# On timeout, check for unclaimed tasks assigned to this agent
|
|
916
|
+
timeout_result: Dict[str, Any] = {"timed_out": True}
|
|
917
|
+
try:
|
|
918
|
+
api_key = _get_api_key()
|
|
919
|
+
open_tasks = be.task_list(api_key, _PROJECT_ID, AGENT_NAME, status_filter="open")
|
|
920
|
+
if isinstance(open_tasks, dict) and open_tasks.get("ok"):
|
|
921
|
+
my_tasks = [t for t in open_tasks.get("tasks", [])
|
|
922
|
+
if t.get("assignee") == AGENT_NAME and not t.get("claimed_by")]
|
|
923
|
+
if my_tasks:
|
|
924
|
+
timeout_result["unclaimed_tasks"] = len(my_tasks)
|
|
925
|
+
timeout_result["hint"] = f"You have {len(my_tasks)} unclaimed task(s) assigned to you. Run meshcode_tasks(status_filter='open') to see them."
|
|
926
|
+
except Exception:
|
|
927
|
+
pass
|
|
928
|
+
return timeout_result
|
|
856
929
|
|
|
857
930
|
|
|
858
931
|
@mcp.tool()
|
|
@@ -995,9 +1068,22 @@ def meshcode_task_create(title: str, description: str = "", assignee: str = "*",
|
|
|
995
1068
|
Returns: {"ok": true, "task_id": "...", "title": "..."}
|
|
996
1069
|
"""
|
|
997
1070
|
api_key = _get_api_key()
|
|
998
|
-
|
|
1071
|
+
result = be.task_create(api_key, _PROJECT_ID, AGENT_NAME, title,
|
|
999
1072
|
description=description, assignee=assignee,
|
|
1000
1073
|
priority=priority, parent_task_id=parent_task_id)
|
|
1074
|
+
# Auto-notify assignee so they wake from meshcode_wait
|
|
1075
|
+
if isinstance(result, dict) and result.get("ok") and assignee and assignee != "*" and assignee != AGENT_NAME:
|
|
1076
|
+
try:
|
|
1077
|
+
be.send_message(_PROJECT_ID, AGENT_NAME, assignee, {
|
|
1078
|
+
"type": "task_assigned",
|
|
1079
|
+
"task_id": result.get("task_id", ""),
|
|
1080
|
+
"title": title,
|
|
1081
|
+
"priority": priority,
|
|
1082
|
+
"text": f"New {priority} task assigned to you: {title}",
|
|
1083
|
+
}, msg_type="system")
|
|
1084
|
+
except Exception:
|
|
1085
|
+
pass # best-effort notification
|
|
1086
|
+
return result
|
|
1001
1087
|
|
|
1002
1088
|
|
|
1003
1089
|
@mcp.tool()
|
|
@@ -1338,7 +1424,10 @@ def meshcode_remember(key: str, value: Any) -> Dict[str, Any]:
|
|
|
1338
1424
|
})
|
|
1339
1425
|
# Best-effort sync to Obsidian vault (if configured)
|
|
1340
1426
|
if isinstance(result, dict) and result.get("ok"):
|
|
1341
|
-
|
|
1427
|
+
try:
|
|
1428
|
+
_sync_to_obsidian(key, json_value)
|
|
1429
|
+
except Exception:
|
|
1430
|
+
pass # Obsidian sync is best-effort, never block memory writes
|
|
1342
1431
|
return result
|
|
1343
1432
|
|
|
1344
1433
|
|
|
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
|
|
File without changes
|