meshcode 2.0.4__tar.gz → 2.0.6__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.4 → meshcode-2.0.6}/PKG-INFO +1 -1
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/__init__.py +1 -1
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/comms_v4.py +25 -10
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/meshcode_mcp/backend.py +12 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/meshcode_mcp/realtime.py +4 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/meshcode_mcp/server.py +95 -15
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.0.4 → meshcode-2.0.6}/pyproject.toml +1 -1
- {meshcode-2.0.4 → meshcode-2.0.6}/README.md +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/cli.py +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/invites.py +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/launcher.py +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/launcher_install.py +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/preferences.py +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/run_agent.py +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/secrets.py +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/self_update.py +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/setup_clients.py +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.0.4 → meshcode-2.0.6}/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.6"
|
|
@@ -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",
|
|
@@ -216,6 +216,10 @@ class RealtimeListener:
|
|
|
216
216
|
except Exception as e:
|
|
217
217
|
log.warning(f"notify_callback failed: {e}")
|
|
218
218
|
|
|
219
|
+
def peek(self) -> list:
|
|
220
|
+
"""Return queued messages without removing them."""
|
|
221
|
+
return list(self.queue)
|
|
222
|
+
|
|
219
223
|
def drain(self) -> list:
|
|
220
224
|
"""Pop and return all queued messages."""
|
|
221
225
|
out = list(self.queue)
|
|
@@ -145,6 +145,28 @@ 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
|
+
|
|
148
170
|
# ============================================================
|
|
149
171
|
# Hot-reload: detect when backend.py changes on disk (e.g. after
|
|
150
172
|
# pip install --upgrade meshcode) and reload without restart.
|
|
@@ -307,13 +329,21 @@ if not _flip_status("idle", ""):
|
|
|
307
329
|
|
|
308
330
|
|
|
309
331
|
# ============================================================
|
|
310
|
-
#
|
|
311
|
-
#
|
|
332
|
+
# Automatic Agent Status State Machine
|
|
333
|
+
# States: ONLINE → WORKING → ONLINE → WAITING → ONLINE → IDLE → OFFLINE
|
|
334
|
+
# Transitions driven by tool calls and meshcode_wait, not the LLM.
|
|
312
335
|
# ============================================================
|
|
313
336
|
import functools as _functools
|
|
314
337
|
import threading as _threading
|
|
338
|
+
import time as _time
|
|
315
339
|
|
|
316
340
|
_flip_lock = _threading.Lock()
|
|
341
|
+
_current_state = "online"
|
|
342
|
+
_last_tool_at = _time.time()
|
|
343
|
+
_current_tool = ""
|
|
344
|
+
_IDLE_THRESHOLD_S = 120 # seconds without tool call → IDLE
|
|
345
|
+
_PROCESSING_COOLDOWN_S = 15 # seconds after tool returns before flipping to ONLINE
|
|
346
|
+
_processing_timer: Optional[_threading.Timer] = None
|
|
317
347
|
|
|
318
348
|
|
|
319
349
|
async def _async_flip_status(status: str, task: str = "") -> None:
|
|
@@ -340,6 +370,28 @@ def _schedule_flip(status: str, task: str = "") -> None:
|
|
|
340
370
|
).start()
|
|
341
371
|
|
|
342
372
|
|
|
373
|
+
def _set_state(state: str, tool: str = "") -> None:
|
|
374
|
+
"""Update the state machine and broadcast to dashboard."""
|
|
375
|
+
global _current_state, _current_tool, _last_tool_at, _processing_timer
|
|
376
|
+
# Cancel any pending processing→online timer
|
|
377
|
+
if _processing_timer is not None:
|
|
378
|
+
_processing_timer.cancel()
|
|
379
|
+
_processing_timer = None
|
|
380
|
+
_current_state = state
|
|
381
|
+
_current_tool = tool
|
|
382
|
+
if state == "working":
|
|
383
|
+
_last_tool_at = _time.time()
|
|
384
|
+
_schedule_flip(state, tool)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _processing_to_online() -> None:
|
|
388
|
+
"""Called after PROCESSING cooldown expires — flip to ONLINE."""
|
|
389
|
+
global _processing_timer
|
|
390
|
+
_processing_timer = None
|
|
391
|
+
if _current_state == "processing":
|
|
392
|
+
_set_state("online", "")
|
|
393
|
+
|
|
394
|
+
|
|
343
395
|
def with_working_status(func):
|
|
344
396
|
name = func.__name__
|
|
345
397
|
skip = (name == "meshcode_wait")
|
|
@@ -348,24 +400,37 @@ def with_working_status(func):
|
|
|
348
400
|
async def awrapper(*args, **kwargs):
|
|
349
401
|
_check_hot_reload()
|
|
350
402
|
if not skip:
|
|
351
|
-
|
|
403
|
+
_set_state("working", name)
|
|
404
|
+
_record_event_bg("tool_call", {"tool": name, "args_keys": list(kwargs.keys())})
|
|
352
405
|
try:
|
|
353
406
|
return await func(*args, **kwargs)
|
|
354
407
|
finally:
|
|
355
408
|
if not skip:
|
|
356
|
-
|
|
409
|
+
global _last_tool_at, _processing_timer
|
|
410
|
+
_last_tool_at = _time.time()
|
|
411
|
+
# Enter PROCESSING cooldown — stays green for 15s
|
|
412
|
+
_set_state("processing", "processing...")
|
|
413
|
+
_processing_timer = _threading.Timer(_PROCESSING_COOLDOWN_S, _processing_to_online)
|
|
414
|
+
_processing_timer.daemon = True
|
|
415
|
+
_processing_timer.start()
|
|
357
416
|
return awrapper
|
|
358
417
|
else:
|
|
359
418
|
@_functools.wraps(func)
|
|
360
419
|
def swrapper(*args, **kwargs):
|
|
361
420
|
_check_hot_reload()
|
|
362
421
|
if not skip:
|
|
363
|
-
|
|
422
|
+
_set_state("working", name)
|
|
423
|
+
_record_event_bg("tool_call", {"tool": name, "args_keys": list(kwargs.keys())})
|
|
364
424
|
try:
|
|
365
425
|
return func(*args, **kwargs)
|
|
366
426
|
finally:
|
|
367
427
|
if not skip:
|
|
368
|
-
|
|
428
|
+
global _last_tool_at, _processing_timer
|
|
429
|
+
_last_tool_at = _time.time()
|
|
430
|
+
_set_state("processing", "processing...")
|
|
431
|
+
_processing_timer = _threading.Timer(_PROCESSING_COOLDOWN_S, _processing_to_online)
|
|
432
|
+
_processing_timer.daemon = True
|
|
433
|
+
_processing_timer.start()
|
|
369
434
|
return swrapper
|
|
370
435
|
|
|
371
436
|
|
|
@@ -635,9 +700,13 @@ def _heartbeat_thread_fn():
|
|
|
635
700
|
while not _heartbeat_stop.is_set():
|
|
636
701
|
try:
|
|
637
702
|
be.sb_rpc("mc_heartbeat", {"p_project_id": _PROJECT_ID, "p_agent_name": AGENT_NAME, "p_version": "2.0.0"})
|
|
638
|
-
#
|
|
703
|
+
# Idle detection: only from online/processing states (NOT waiting)
|
|
704
|
+
# Agents in meshcode_wait should stay WAITING, not flip to IDLE
|
|
705
|
+
if _current_state in ("online", "processing") and (_time.time() - _last_tool_at) > _IDLE_THRESHOLD_S:
|
|
706
|
+
_set_state("idle", f"idle ({int((_time.time() - _last_tool_at) / 60)}m)")
|
|
707
|
+
# Sync current state to DB (in case realtime missed it)
|
|
639
708
|
try:
|
|
640
|
-
be.set_status(_PROJECT_ID, AGENT_NAME,
|
|
709
|
+
be.set_status(_PROJECT_ID, AGENT_NAME, _current_state, _current_tool)
|
|
641
710
|
except Exception:
|
|
642
711
|
pass
|
|
643
712
|
if _REALTIME and not _REALTIME.is_connected:
|
|
@@ -694,9 +763,11 @@ async def lifespan(_app):
|
|
|
694
763
|
hb_thread = _threading.Thread(target=_heartbeat_thread_fn, daemon=True, name="meshcode-heartbeat")
|
|
695
764
|
hb_thread.start()
|
|
696
765
|
log.info(f"lifespan started — Realtime + heartbeat thread active for {AGENT_NAME}")
|
|
766
|
+
_record_event_bg("boot", {"agent": AGENT_NAME, "project": PROJECT_NAME, "session_id": _SESSION_ID})
|
|
697
767
|
try:
|
|
698
768
|
yield {"realtime": _REALTIME}
|
|
699
769
|
finally:
|
|
770
|
+
_record_event_bg("shutdown", {"agent": AGENT_NAME, "session_id": _SESSION_ID})
|
|
700
771
|
log.info("lifespan shutdown — stopping heartbeat + realtime + releasing lease")
|
|
701
772
|
_heartbeat_stop.set()
|
|
702
773
|
hb_thread.join(timeout=5)
|
|
@@ -835,8 +906,12 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
|
|
|
835
906
|
"""
|
|
836
907
|
global _IN_WAIT
|
|
837
908
|
_IN_WAIT = True
|
|
909
|
+
_set_state("waiting", "listening for messages")
|
|
838
910
|
try:
|
|
839
|
-
|
|
911
|
+
result = await _meshcode_wait_inner(actual_timeout=max(1, int(timeout_seconds)), include_acks=include_acks)
|
|
912
|
+
if result.get("got_message"):
|
|
913
|
+
_set_state("online", "")
|
|
914
|
+
return result
|
|
840
915
|
finally:
|
|
841
916
|
_IN_WAIT = False
|
|
842
917
|
|
|
@@ -935,14 +1010,18 @@ def meshcode_check(include_acks: bool = False) -> Dict[str, Any]:
|
|
|
935
1010
|
the realtime listener connected).
|
|
936
1011
|
"""
|
|
937
1012
|
pending = be.count_pending(_PROJECT_ID, AGENT_NAME)
|
|
938
|
-
|
|
939
|
-
|
|
1013
|
+
# Peek at realtime buffer WITHOUT draining — check is non-destructive
|
|
1014
|
+
realtime_buffered = _REALTIME.peek() if _REALTIME else []
|
|
1015
|
+
# Don't mark as seen — meshcode_check is a peek, not a consume
|
|
1016
|
+
deduped = [m for m in realtime_buffered if _seen_key(m) not in _SEEN_MSG_IDS]
|
|
940
1017
|
|
|
941
1018
|
# Fallback: if realtime buffer is empty but DB has pending messages,
|
|
942
|
-
# fetch them from the DB
|
|
1019
|
+
# fetch them from the DB. Use mark_read=False so meshcode_check is a
|
|
1020
|
+
# non-destructive peek — messages stay pending until meshcode_wait or
|
|
1021
|
+
# meshcode_read consumes them.
|
|
943
1022
|
if not deduped and pending > 0:
|
|
944
|
-
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME)
|
|
945
|
-
deduped =
|
|
1023
|
+
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=False)
|
|
1024
|
+
deduped = [
|
|
946
1025
|
{
|
|
947
1026
|
"from": m["from_agent"],
|
|
948
1027
|
"type": m.get("type", "msg"),
|
|
@@ -952,7 +1031,8 @@ def meshcode_check(include_acks: bool = False) -> Dict[str, Any]:
|
|
|
952
1031
|
"parent_id": m.get("parent_msg_id"),
|
|
953
1032
|
}
|
|
954
1033
|
for m in raw
|
|
955
|
-
|
|
1034
|
+
if _seen_key({"id": m.get("id"), "from": m.get("from_agent"), "payload": m.get("payload", {}), "ts": m.get("created_at")}) not in _SEEN_MSG_IDS
|
|
1035
|
+
]
|
|
956
1036
|
|
|
957
1037
|
split = _split_messages(deduped)
|
|
958
1038
|
if not include_acks:
|
|
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
|