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.
Files changed (29) hide show
  1. {meshcode-2.0.3 → meshcode-2.0.5}/PKG-INFO +1 -1
  2. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/__init__.py +1 -1
  3. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/comms_v4.py +25 -10
  4. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/meshcode_mcp/backend.py +12 -0
  5. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/meshcode_mcp/server.py +93 -4
  6. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode.egg-info/PKG-INFO +1 -1
  7. {meshcode-2.0.3 → meshcode-2.0.5}/pyproject.toml +1 -1
  8. {meshcode-2.0.3 → meshcode-2.0.5}/README.md +0 -0
  9. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/cli.py +0 -0
  10. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/invites.py +0 -0
  11. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/launcher.py +0 -0
  12. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/launcher_install.py +0 -0
  13. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/meshcode_mcp/__init__.py +0 -0
  14. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/meshcode_mcp/__main__.py +0 -0
  15. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/meshcode_mcp/realtime.py +0 -0
  16. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/meshcode_mcp/test_backend.py +0 -0
  17. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  18. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/preferences.py +0 -0
  19. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/protocol_v2.py +0 -0
  20. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/run_agent.py +0 -0
  21. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/secrets.py +0 -0
  22. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/self_update.py +0 -0
  23. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode/setup_clients.py +0 -0
  24. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode.egg-info/SOURCES.txt +0 -0
  25. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode.egg-info/dependency_links.txt +0 -0
  26. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode.egg-info/entry_points.txt +0 -0
  27. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode.egg-info/requires.txt +0 -0
  28. {meshcode-2.0.3 → meshcode-2.0.5}/meshcode.egg-info/top_level.txt +0 -0
  29. {meshcode-2.0.3 → meshcode-2.0.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.0.3
3
+ Version: 2.0.5
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,2 +1,2 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.0.3"
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
- SETUP:
1638
- connect <proj> <name> [claude|codex] One-command setup + hook install
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
- UNIVERSAL MULTI-CLIENT SETUP:
1642
- setup <client> <proj> <name> [role] Install MCP server config for client
1643
- Clients: claude-code, cursor, cline,
1644
- claude-desktop
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 == "run":
2046
- # meshcode run <agent> [--project <name>] [--editor claude|cursor|code]
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 run <agent> [--project <name>] [--editor claude|cursor|code]")
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
- _try_auto_wake(from_agent, preview[:60])
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
- return {"timed_out": True}
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
- return be.task_create(api_key, _PROJECT_ID, AGENT_NAME, title,
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
- _sync_to_obsidian(key, json_value)
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.0.3
3
+ Version: 2.0.5
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.0.3"
7
+ version = "2.0.5"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes