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.
Files changed (29) hide show
  1. {meshcode-2.0.4 → meshcode-2.0.6}/PKG-INFO +1 -1
  2. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/__init__.py +1 -1
  3. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/comms_v4.py +25 -10
  4. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/meshcode_mcp/backend.py +12 -0
  5. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/meshcode_mcp/realtime.py +4 -0
  6. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/meshcode_mcp/server.py +95 -15
  7. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode.egg-info/PKG-INFO +1 -1
  8. {meshcode-2.0.4 → meshcode-2.0.6}/pyproject.toml +1 -1
  9. {meshcode-2.0.4 → meshcode-2.0.6}/README.md +0 -0
  10. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/cli.py +0 -0
  11. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/invites.py +0 -0
  12. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/launcher.py +0 -0
  13. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/launcher_install.py +0 -0
  14. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/meshcode_mcp/__init__.py +0 -0
  15. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/meshcode_mcp/__main__.py +0 -0
  16. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/meshcode_mcp/test_backend.py +0 -0
  17. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  18. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/preferences.py +0 -0
  19. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/protocol_v2.py +0 -0
  20. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/run_agent.py +0 -0
  21. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/secrets.py +0 -0
  22. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/self_update.py +0 -0
  23. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode/setup_clients.py +0 -0
  24. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode.egg-info/SOURCES.txt +0 -0
  25. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode.egg-info/dependency_links.txt +0 -0
  26. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode.egg-info/entry_points.txt +0 -0
  27. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode.egg-info/requires.txt +0 -0
  28. {meshcode-2.0.4 → meshcode-2.0.6}/meshcode.egg-info/top_level.txt +0 -0
  29. {meshcode-2.0.4 → meshcode-2.0.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.0.4
3
+ Version: 2.0.6
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.4"
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
- 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",
@@ -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
- # Working/idle status decorator for @mcp.tool() functions.
311
- # Fire-and-forget flips so tool execution is never blocked.
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
- _schedule_flip("working", name)
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
- _schedule_flip("idle", "")
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
- _schedule_flip("working", name)
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
- _schedule_flip("idle", "")
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
- # Also ensure status is at least "idle" (not "offline") between tool calls
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, "idle", "")
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
- return await _meshcode_wait_inner(actual_timeout=max(1, int(timeout_seconds)), include_acks=include_acks)
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
- realtime_buffered = _REALTIME.drain() if _REALTIME else []
939
- deduped = _filter_and_mark(realtime_buffered)
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 so they're not invisible to the agent.
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 = _filter_and_mark([
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.0.4
3
+ Version: 2.0.6
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.4"
7
+ version = "2.0.6"
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