meshcode 2.10.22__tar.gz → 2.10.26__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 (32) hide show
  1. {meshcode-2.10.22 → meshcode-2.10.26}/PKG-INFO +1 -1
  2. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/__init__.py +1 -1
  3. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/meshcode_mcp/server.py +194 -31
  4. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode.egg-info/PKG-INFO +1 -1
  5. {meshcode-2.10.22 → meshcode-2.10.26}/pyproject.toml +1 -1
  6. {meshcode-2.10.22 → meshcode-2.10.26}/README.md +0 -0
  7. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/ascii_art.py +0 -0
  8. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/cli.py +0 -0
  9. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/comms_v4.py +0 -0
  10. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/invites.py +0 -0
  11. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/launcher.py +0 -0
  12. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/launcher_install.py +0 -0
  13. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/meshcode_mcp/__init__.py +0 -0
  14. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/meshcode_mcp/__main__.py +0 -0
  15. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/meshcode_mcp/backend.py +0 -0
  16. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/meshcode_mcp/realtime.py +0 -0
  17. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/meshcode_mcp/test_backend.py +0 -0
  18. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  19. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  20. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/preferences.py +0 -0
  21. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/run_agent.py +0 -0
  23. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/secrets.py +0 -0
  24. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/self_update.py +0 -0
  25. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode/setup_clients.py +0 -0
  26. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode.egg-info/SOURCES.txt +0 -0
  27. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode.egg-info/requires.txt +0 -0
  30. {meshcode-2.10.22 → meshcode-2.10.26}/meshcode.egg-info/top_level.txt +0 -0
  31. {meshcode-2.10.22 → meshcode-2.10.26}/setup.cfg +0 -0
  32. {meshcode-2.10.22 → meshcode-2.10.26}/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.22
3
+ Version: 2.10.26
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.10.22"
2
+ __version__ = "2.10.26"
@@ -156,9 +156,11 @@ def _try_auto_wake(from_agent: str, preview: str) -> None:
156
156
  # Sanitize inputs: strip everything except alphanumeric, spaces, basic punctuation
157
157
  safe_agent = re.sub(r'[^a-zA-Z0-9_\- ]', '', from_agent)[:50]
158
158
  safe_preview = re.sub(r'[^a-zA-Z0-9_\-.,!? ]', '', preview)[:60]
159
- # Color-coded nudge with sender's agent color
160
- ac = _agent_color(safe_agent)
161
- nudge = f"{ac}{_ANSI_BOLD}[mesh]{_ANSI_RESET} {ac}{safe_agent}{_ANSI_RESET} {_ANSI_DIM}>{_ANSI_RESET} {safe_preview} {_ANSI_DIM}— meshcode_check(){_ANSI_RESET}"
159
+ # Plain-text nudge. ANSI colors were stripped 2026-04-16 because terminals
160
+ # that don't interpret escape sequences (TypeScript runners, some VSCode
161
+ # configurations, pipelines, tmux without termguicolors) render the raw
162
+ # bytes — "[91m[1m[mesh][0m …" — which is worse UX than no color at all.
163
+ nudge = f"[mesh] {safe_agent} > {safe_preview} — meshcode_check()"
162
164
  system = platform.system()
163
165
  try:
164
166
  if system == "Darwin":
@@ -799,12 +801,11 @@ def _log_crash_to_db(reason: str = "unknown", error_detail: str = "") -> None:
799
801
 
800
802
 
801
803
 
802
- # NOTE: Do NOT install signal handlers (SIGTERM, SIGINT) or atexit hooks here.
803
- # FastMCP/anyio manages its own event loop and signal handling. Custom signal
804
- # handlers override Python's default KeyboardInterrupt, preventing anyio from
805
- # cancelling tasks cleanly. Network calls inside signal/atexit handlers can
806
- # deadlock or corrupt the event loop. The lease will be released by the
804
+ # NOTE: Do NOT install custom SIGTERM handlers or atexit hooks here — network
805
+ # calls inside them can deadlock the event loop. The lease is released by the
807
806
  # lifespan shutdown handler instead.
807
+ #
808
+ # SIGINT handling is special — see run_server() for why we set SIG_IGN there.
808
809
 
809
810
 
810
811
  # ============================================================
@@ -1345,14 +1346,12 @@ def meshcode_broadcast(payload: Any) -> Dict[str, Any]:
1345
1346
  elif not isinstance(payload, dict):
1346
1347
  return {"error": "payload must be a string or object", "got_type": type(payload).__name__}
1347
1348
 
1348
- agents = be.get_board(_PROJECT_ID)
1349
- sent = 0
1350
- for a in agents:
1351
- if a["name"] != AGENT_NAME:
1352
- be.send_message(_PROJECT_ID, AGENT_NAME, a["name"], payload, msg_type="broadcast",
1353
- api_key=_get_api_key())
1354
- sent += 1
1355
- return {"broadcast": True, "agents_notified": sent}
1349
+ # Canonical broadcast: one row with to_agent='*'. Every agent's inbox
1350
+ # query includes `to_agent = '*'` so all recipients see it, without the
1351
+ # N-row fanout that caused duplicate-render bugs.
1352
+ be.send_message(_PROJECT_ID, AGENT_NAME, "*", payload, msg_type="broadcast",
1353
+ api_key=_get_api_key())
1354
+ return {"broadcast": True, "to": "*"}
1356
1355
 
1357
1356
 
1358
1357
  @mcp.tool()
@@ -1488,7 +1487,7 @@ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
1488
1487
 
1489
1488
  @mcp.tool()
1490
1489
  @with_working_status
1491
- async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False) -> Dict[str, Any]:
1490
+ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -> Dict[str, Any]:
1492
1491
  """Block until a mesh message arrives or a task needs attention.
1493
1492
 
1494
1493
  INTERNAL LOOP: This function loops internally and only returns when
@@ -1498,7 +1497,10 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
1498
1497
  accidentally using ScheduleWakeup or exiting the loop.
1499
1498
 
1500
1499
  Args:
1501
- timeout_seconds: Max wait time per poll cycle (default 120, hard cap 120).
1500
+ timeout_seconds: Max wait time per poll cycle (default 20, hard cap 20).
1501
+ Short cap keeps the outer tool call bounded so the user can press
1502
+ ESC in Claude Code without killing the MCP server — the inner
1503
+ loop continues polling across cycles at zero token cost.
1502
1504
  """
1503
1505
  global _IN_WAIT, _CONSECUTIVE_IDLE_SECONDS, _LAST_SEEN_TS
1504
1506
 
@@ -1541,7 +1543,7 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
1541
1543
 
1542
1544
  _IN_WAIT = True
1543
1545
  _set_state("waiting", "listening for messages")
1544
- capped_timeout = min(max(1, int(timeout_seconds)), 120)
1546
+ capped_timeout = min(max(1, int(timeout_seconds)), 20)
1545
1547
  try:
1546
1548
  # ── INTERNAL LOOP ──────────────────────────────────────────
1547
1549
  # Keep polling until something actionable arrives.
@@ -1944,17 +1946,44 @@ def meshcode_task_create(title: str, description: str = "", assignee: str = "*",
1944
1946
 
1945
1947
  @mcp.tool()
1946
1948
  @with_working_status
1947
- def meshcode_tasks(status_filter: Optional[str] = None) -> Dict[str, Any]:
1949
+ def meshcode_tasks(status_filter: Optional[str] = None, verbose: bool = False) -> Dict[str, Any]:
1948
1950
  """List tasks. Filter by status to find work.
1949
1951
 
1950
1952
  Args:
1951
1953
  status_filter: 'open' / 'in_progress' / 'done' / etc. None = all.
1954
+ verbose: When False (default), descriptions are truncated to 200 chars
1955
+ and null/empty fields are dropped to save tokens. Pass True for
1956
+ the full payload.
1952
1957
 
1953
1958
  Returns: {"ok": true, "tasks": [...]}
1954
1959
  """
1955
1960
  api_key = _get_api_key()
1956
1961
  include_done = status_filter == "done"
1957
- return be.task_list(api_key, _PROJECT_ID, AGENT_NAME, status_filter=status_filter, include_done=include_done)
1962
+ result = be.task_list(api_key, _PROJECT_ID, AGENT_NAME, status_filter=status_filter, include_done=include_done)
1963
+ if verbose or not isinstance(result, dict) or not result.get("ok"):
1964
+ return result
1965
+ compact: List[Dict[str, Any]] = []
1966
+ for t in result.get("tasks", []):
1967
+ desc = t.get("description") or ""
1968
+ row: Dict[str, Any] = {
1969
+ "id": t.get("id"),
1970
+ "title": t.get("title"),
1971
+ "status": t.get("status"),
1972
+ "priority": t.get("priority"),
1973
+ "assignee": t.get("assignee"),
1974
+ }
1975
+ if t.get("claimed_by"):
1976
+ row["claimed_by"] = t["claimed_by"]
1977
+ if t.get("parent_task_id"):
1978
+ row["parent_task_id"] = t["parent_task_id"]
1979
+ if desc:
1980
+ if len(desc) > 200:
1981
+ row["description"] = desc[:200]
1982
+ row["_description_truncated"] = True
1983
+ else:
1984
+ row["description"] = desc
1985
+ compact.append(row)
1986
+ return {"ok": True, "tasks": compact}
1958
1987
 
1959
1988
 
1960
1989
  @mcp.tool()
@@ -1967,9 +1996,32 @@ def meshcode_task_claim(task_id: str) -> Dict[str, Any]:
1967
1996
 
1968
1997
  @mcp.tool()
1969
1998
  @with_working_status
1970
- def meshcode_task_complete(task_id: str, summary: str = "") -> Dict[str, Any]:
1971
- """Complete a claimed task with summary. Auto-remembers the task summary."""
1999
+ def meshcode_task_complete(task_id: str, summary: str = "", force: bool = False) -> Dict[str, Any]:
2000
+ """Complete a claimed task with summary. Auto-remembers the task summary.
2001
+
2002
+ Refuses if the task has open subtasks, unless force=True is passed with a
2003
+ reason in `summary`. This prevents closing a parent while lanes are still
2004
+ in flight.
2005
+ """
1972
2006
  api_key = _get_api_key()
2007
+ if not force:
2008
+ try:
2009
+ listing = be.task_list(api_key, _PROJECT_ID, AGENT_NAME, status_filter=None, include_done=False)
2010
+ if isinstance(listing, dict) and listing.get("ok"):
2011
+ open_subs = [
2012
+ {"id": t["id"][:8], "title": t["title"][:80], "status": t["status"]}
2013
+ for t in listing.get("tasks", [])
2014
+ if t.get("parent_task_id") == task_id
2015
+ and t.get("status") in ("open", "in_progress", "in_review")
2016
+ ]
2017
+ if open_subs:
2018
+ return {
2019
+ "refused": True,
2020
+ "reason": f"Cannot complete — {len(open_subs)} open subtasks. Finish them or pass force=True with a reason in summary.",
2021
+ "open_subtasks": open_subs,
2022
+ }
2023
+ except Exception:
2024
+ pass # Best-effort check; don't block on listing failure.
1973
2025
  result = be.task_complete(api_key, _PROJECT_ID, task_id, AGENT_NAME, summary=summary)
1974
2026
  # Auto-remember task completion for future context
1975
2027
  if isinstance(result, dict) and result.get("ok") and summary:
@@ -2020,6 +2072,62 @@ def meshcode_task_reject(task_id: str, feedback: str = "") -> Dict[str, Any]:
2020
2072
  })
2021
2073
 
2022
2074
 
2075
+ @mcp.tool()
2076
+ @with_working_status
2077
+ def meshcode_task_search(title_contains: Optional[str] = None,
2078
+ status: Optional[str] = None,
2079
+ assignee: Optional[str] = None,
2080
+ limit: int = 20) -> Dict[str, Any]:
2081
+ """Search tasks by title/status/assignee. Use before task_create to dedupe."""
2082
+ return be.sb_rpc("mc_search_tasks", {
2083
+ "p_api_key": _get_api_key(),
2084
+ "p_project_id": _PROJECT_ID,
2085
+ "p_caller_agent": AGENT_NAME,
2086
+ "p_title_contains": title_contains,
2087
+ "p_status": status,
2088
+ "p_assignee": assignee,
2089
+ "p_limit": limit,
2090
+ })
2091
+
2092
+
2093
+ @mcp.tool()
2094
+ @with_working_status
2095
+ def meshcode_agent_list() -> Dict[str, Any]:
2096
+ """Roster of agents in this meshwork with status + last_heartbeat."""
2097
+ return be.sb_rpc("mc_list_agents", {
2098
+ "p_api_key": _get_api_key(),
2099
+ "p_project_id": _PROJECT_ID,
2100
+ })
2101
+
2102
+
2103
+ @mcp.tool()
2104
+ @with_working_status
2105
+ def meshcode_task_cancel(task_id: str, reason: str = "") -> Dict[str, Any]:
2106
+ """Cancel a task. Creator or commander only. Works on open/in_progress/in_review."""
2107
+ api_key = _get_api_key()
2108
+ return be.sb_rpc("mc_task_cancel", {
2109
+ "p_api_key": api_key,
2110
+ "p_project_id": _PROJECT_ID,
2111
+ "p_task_id": task_id,
2112
+ "p_cancelling_agent": AGENT_NAME,
2113
+ "p_reason": reason,
2114
+ })
2115
+
2116
+
2117
+ @mcp.tool()
2118
+ @with_working_status
2119
+ def meshcode_task_reassign(task_id: str, new_assignee: str) -> Dict[str, Any]:
2120
+ """Reassign an unclaimed open task to a different agent. Creator or commander only."""
2121
+ api_key = _get_api_key()
2122
+ return be.sb_rpc("mc_task_reassign", {
2123
+ "p_api_key": api_key,
2124
+ "p_project_id": _PROJECT_ID,
2125
+ "p_task_id": task_id,
2126
+ "p_reassigning_agent": AGENT_NAME,
2127
+ "p_new_assignee": new_assignee,
2128
+ })
2129
+
2130
+
2023
2131
  # ----------------- PROACTIVE HEALTH SCAN -----------------
2024
2132
 
2025
2133
  @mcp.tool()
@@ -2149,6 +2257,47 @@ def meshcode_links() -> Dict[str, Any]:
2149
2257
  })
2150
2258
 
2151
2259
 
2260
+ @mcp.tool()
2261
+ @with_working_status
2262
+ def meshcode_link_invite(source_project: str, note: str = "") -> Dict[str, Any]:
2263
+ """Generate a one-time invite token for another meshwork to link in.
2264
+
2265
+ Returns {token, expires_at}. Share the token out-of-band. The recipient
2266
+ calls meshcode_link_redeem(token, their_meshwork) to auto-link
2267
+ (no separate accept step).
2268
+ """
2269
+ return be.sb_rpc("mc_create_link_invite", {
2270
+ "p_source_project": source_project,
2271
+ "p_note": note,
2272
+ })
2273
+
2274
+
2275
+ @mcp.tool()
2276
+ @with_working_status
2277
+ def meshcode_link_redeem(token: str, target_meshwork: str) -> Dict[str, Any]:
2278
+ """Redeem an invite token — creates an ACTIVE mesh link in one step."""
2279
+ return be.sb_rpc("mc_redeem_link_invite", {
2280
+ "p_token": token,
2281
+ "p_target_project": target_meshwork,
2282
+ })
2283
+
2284
+
2285
+ @mcp.tool()
2286
+ @with_working_status
2287
+ def meshcode_link_invites(source_project: Optional[str] = None) -> Dict[str, Any]:
2288
+ """List link invites this user has created (active/redeemed/expired/revoked)."""
2289
+ return be.sb_rpc("mc_list_my_link_invites", {
2290
+ "p_source_project": source_project,
2291
+ })
2292
+
2293
+
2294
+ @mcp.tool()
2295
+ @with_working_status
2296
+ def meshcode_link_invite_revoke(token: str) -> Dict[str, Any]:
2297
+ """Revoke an outstanding (unredeemed) invite token."""
2298
+ return be.sb_rpc("mc_revoke_link_invite", {"p_token": token})
2299
+
2300
+
2152
2301
  @mcp.tool()
2153
2302
  @with_working_status
2154
2303
  def meshcode_expand_link(link_id: str, agents: str) -> Dict[str, Any]:
@@ -2647,14 +2796,7 @@ def _auto_update() -> None:
2647
2796
 
2648
2797
 
2649
2798
  def run_server():
2650
- """Start the MCP server on stdio (default for Claude Code).
2651
-
2652
- IMPORTANT: Do NOT wrap mcp.run() with try/except, signal handlers, or
2653
- atexit hooks. FastMCP/anyio manages its own event loop lifecycle and
2654
- signal handling. Any interference (custom SIGINT handler, atexit network
2655
- calls, catching KeyboardInterrupt) corrupts the event loop and causes
2656
- the MCP server to crash when Claude Code cancels a tool call (ESC).
2657
- """
2799
+ """Start the MCP server on stdio (default for Claude Code)."""
2658
2800
  _auto_update()
2659
2801
  print(
2660
2802
  f"[meshcode-mcp] Starting server for {AGENT_NAME}@{PROJECT_NAME}",
@@ -2664,4 +2806,25 @@ def run_server():
2664
2806
  # sys.stdout was redirected to stderr at module load to prevent
2665
2807
  # accidental stdout writes from corrupting the MCP protocol.
2666
2808
  sys.stdout = _REAL_STDOUT
2809
+
2810
+ # Claude Code cancels tool calls on ESC via TWO parallel channels:
2811
+ # (1) JSON-RPC `notifications/cancelled` over stdin (the correct path)
2812
+ # (2) SIGINT to the stdio subprocess (a destructive side-channel)
2813
+ # anyio's asyncio backend installs its own SIGINT handler inside
2814
+ # Runner.run() only if the current handler is `default_int_handler`
2815
+ # (see anyio/_backends/_asyncio.py ~L197-210). That handler cancels the
2816
+ # *main task* — which for a stdio server IS the whole server run —
2817
+ # tearing the event loop down and killing the subprocess. Setting
2818
+ # SIG_IGN before mcp.run() trips anyio's guard, so it leaves SIGINT
2819
+ # alone; the kernel drops the signal; only the notifications/cancelled
2820
+ # path remains, which cancels just the in-flight tool call as intended.
2821
+ # SIGTERM is deliberately untouched so Claude Code can still force-shut
2822
+ # the server on session end. stdin EOF remains the normal exit path.
2823
+ import signal
2824
+ try:
2825
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
2826
+ except (ValueError, OSError):
2827
+ # Non-main thread or platform that can't install handlers — harmless.
2828
+ pass
2829
+
2667
2830
  mcp.run()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.22
3
+ Version: 2.10.26
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.10.22"
7
+ version = "2.10.26"
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