meshcode 1.8.6__tar.gz → 1.8.9__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-1.8.6 → meshcode-1.8.9}/PKG-INFO +1 -1
  2. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/__init__.py +1 -1
  3. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/comms_v4.py +61 -12
  4. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/invites.py +2 -2
  5. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/meshcode_mcp/backend.py +2 -2
  6. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/meshcode_mcp/server.py +70 -5
  7. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/run_agent.py +10 -1
  8. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/setup_clients.py +24 -4
  9. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode.egg-info/PKG-INFO +1 -1
  10. {meshcode-1.8.6 → meshcode-1.8.9}/pyproject.toml +1 -1
  11. {meshcode-1.8.6 → meshcode-1.8.9}/README.md +0 -0
  12. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/cli.py +0 -0
  13. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/launcher.py +0 -0
  14. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/launcher_install.py +0 -0
  15. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/meshcode_mcp/__init__.py +0 -0
  16. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/meshcode_mcp/__main__.py +0 -0
  17. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/meshcode_mcp/realtime.py +0 -0
  18. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/meshcode_mcp/test_backend.py +0 -0
  19. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  20. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/preferences.py +0 -0
  21. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/secrets.py +0 -0
  23. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode/self_update.py +0 -0
  24. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode.egg-info/SOURCES.txt +0 -0
  25. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode.egg-info/dependency_links.txt +0 -0
  26. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode.egg-info/entry_points.txt +0 -0
  27. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode.egg-info/requires.txt +0 -0
  28. {meshcode-1.8.6 → meshcode-1.8.9}/meshcode.egg-info/top_level.txt +0 -0
  29. {meshcode-1.8.6 → meshcode-1.8.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 1.8.6
3
+ Version: 1.8.9
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__ = "1.8.6"
2
+ __version__ = "1.8.9"
@@ -47,8 +47,8 @@ from urllib.parse import quote
47
47
  # Production defaults baked in. The publishable key is the anon/public key
48
48
  # (RLS-protected, safe to ship — same one the frontend at meshcode.io uses
49
49
  # in the browser). Override via env vars or ~/.meshcode/env if you self-host.
50
- _DEFAULT_SUPABASE_URL = "https://wwgzzmydrwrjgaebspdo.supabase.co"
51
- _DEFAULT_SUPABASE_KEY = "sb_publishable_0qf0U1GURopPIxLR8Vu7eQ_5grflPP4"
50
+ _DEFAULT_SUPABASE_URL = "https://gjinagyyjttyxnaoavnz.supabase.co"
51
+ _DEFAULT_SUPABASE_KEY = "sb_publishable_qwN9PO1L7jUXhhbhhVk2CQ_z1FXG2Qf"
52
52
 
53
53
  def _load_env_file():
54
54
  """Read SUPABASE_URL/KEY from ~/.meshcode/env if present (overrides defaults)."""
@@ -190,7 +190,7 @@ def log_msg(text):
190
190
  try:
191
191
  with open(LOG_FILE, "a") as f:
192
192
  f.write(f"[{now()}] {text}\n")
193
- except:
193
+ except (IOError, OSError):
194
194
  pass
195
195
 
196
196
 
@@ -198,12 +198,61 @@ def ensure_sessions():
198
198
  SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
199
199
 
200
200
 
201
+ def _load_api_key_for_cli() -> str:
202
+ """Pull the user's api key from the keychain (or env fallback) so the
203
+ CLI can call SECURITY DEFINER RPCs that use api_key auth instead of
204
+ relying on the publishable anon key + RLS, which is what CLI verbs
205
+ used to do and which always failed because the CLI has no JWT context.
206
+ """
207
+ # 1) explicit env override
208
+ k = os.environ.get("MESHCODE_API_KEY", "").strip()
209
+ if k:
210
+ return k
211
+ # 2) keychain via secrets module
212
+ try:
213
+ import importlib
214
+ secrets_mod = importlib.import_module("meshcode.secrets")
215
+ profile = os.environ.get("MESHCODE_KEYCHAIN_PROFILE") or secrets_mod.DEFAULT_PROFILE
216
+ key = secrets_mod.get_api_key(profile=profile) or ""
217
+ if key:
218
+ return key
219
+ except Exception:
220
+ pass
221
+ return ""
222
+
223
+
201
224
  def get_project_id(project_name):
202
- """Get or create project, return its UUID."""
225
+ """Resolve a project's UUID for the authenticated CLI user.
226
+
227
+ Resolution order:
228
+ 1. api_key + mc_resolve_project RPC (SECURITY DEFINER, the only
229
+ path that actually works for an authenticated CLI session)
230
+ 2. legacy publishable-anon SELECT (only useful for shared/public
231
+ projects; will fail under RLS for owned projects)
232
+ 3. legacy publishable-anon INSERT (will fail under RLS — kept as
233
+ last resort for backwards compat with very old test scripts)
234
+ """
235
+ api_key = _load_api_key_for_cli()
236
+ if api_key:
237
+ try:
238
+ r = sb_rpc("mc_resolve_project", {
239
+ "p_api_key": api_key,
240
+ "p_project_name": project_name,
241
+ })
242
+ if isinstance(r, dict) and r.get("project_id"):
243
+ return r["project_id"]
244
+ # Some deployments return rows
245
+ if isinstance(r, list) and r and r[0].get("project_id"):
246
+ return r[0]["project_id"]
247
+ except Exception:
248
+ pass
249
+
203
250
  rows = sb_select("mc_projects", f"name=eq.{quote(project_name)}")
204
251
  if rows:
205
252
  return rows[0]["id"]
206
- # Create project
253
+
254
+ # Last resort: try to create. Will fail under RLS for unauthenticated
255
+ # contexts, but kept for legacy callers + admin tooling.
207
256
  result = sb_insert("mc_projects", {"name": project_name})
208
257
  if result and len(result) > 0:
209
258
  return result[0]["id"]
@@ -227,7 +276,7 @@ def can_nudge(project, name):
227
276
  last = float(nf.read_text().strip())
228
277
  if (time.time() - last) < NUDGE_COOLDOWN:
229
278
  return False
230
- except:
279
+ except (ValueError, IOError, OSError):
231
280
  pass
232
281
  return True
233
282
 
@@ -262,7 +311,7 @@ def send_notification(project, name, from_agent, pending=1):
262
311
  else:
263
312
  # Linux
264
313
  subprocess.run(['notify-send', title, body], capture_output=True, timeout=3)
265
- except:
314
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError, OSError):
266
315
  pass
267
316
 
268
317
 
@@ -598,7 +647,7 @@ def nudge_agent(project, name, from_agent=""):
598
647
 
599
648
  try:
600
649
  data = json.loads(session_file.read_text())
601
- except:
650
+ except (json.JSONDecodeError, IOError, OSError):
602
651
  if _headless_spawn_allowed():
603
652
  log_msg(f"[{project}] NUDGE: {name} session file unreadable, spawning headless (opt-in)")
604
653
  ok = _spawn_headless_for_pending(project, name, from_agent)
@@ -704,7 +753,7 @@ end tell
704
753
  capture_output=True, text=True, timeout=10)
705
754
  if result.returncode == 0:
706
755
  success = True
707
- except:
756
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError, OSError):
708
757
  pass
709
758
 
710
759
  if success:
@@ -737,7 +786,7 @@ def get_session_info():
737
786
  check_pid = parent
738
787
  else:
739
788
  break
740
- except:
789
+ except (subprocess.SubprocessError, OSError, ValueError):
741
790
  pass
742
791
 
743
792
  if not my_tty:
@@ -750,7 +799,7 @@ def get_session_info():
750
799
  session_tty = data.get("tty", "")
751
800
  if session_tty and (session_tty in my_tty or my_tty in session_tty):
752
801
  return data.get("project"), data.get("agent")
753
- except:
802
+ except (json.JSONDecodeError, IOError, OSError):
754
803
  continue
755
804
 
756
805
  return None, None
@@ -786,7 +835,7 @@ def register(project, name, role=""):
786
835
  check_pid = parent
787
836
  else:
788
837
  break
789
- except:
838
+ except (subprocess.SubprocessError, OSError, ValueError):
790
839
  pass
791
840
 
792
841
  # Register agent via tier-aware RPC (enforces plan limits)
@@ -41,8 +41,8 @@ from urllib.request import Request, urlopen
41
41
  # Supabase RPC helpers
42
42
  # ============================================================
43
43
 
44
- _DEFAULT_SUPABASE_URL = "https://wwgzzmydrwrjgaebspdo.supabase.co"
45
- _DEFAULT_SUPABASE_KEY = "sb_publishable_0qf0U1GURopPIxLR8Vu7eQ_5grflPP4"
44
+ _DEFAULT_SUPABASE_URL = "https://gjinagyyjttyxnaoavnz.supabase.co"
45
+ _DEFAULT_SUPABASE_KEY = "sb_publishable_qwN9PO1L7jUXhhbhhVk2CQ_z1FXG2Qf"
46
46
 
47
47
 
48
48
  def _sb() -> Dict[str, str]:
@@ -13,8 +13,8 @@ from urllib.parse import quote
13
13
  from urllib.request import Request, urlopen
14
14
 
15
15
  # Bake in production defaults — RLS-protected publishable key, safe to ship.
16
- _DEFAULT_SUPABASE_URL = "https://wwgzzmydrwrjgaebspdo.supabase.co"
17
- _DEFAULT_SUPABASE_KEY = "sb_publishable_0qf0U1GURopPIxLR8Vu7eQ_5grflPP4"
16
+ _DEFAULT_SUPABASE_URL = "https://gjinagyyjttyxnaoavnz.supabase.co"
17
+ _DEFAULT_SUPABASE_KEY = "sb_publishable_qwN9PO1L7jUXhhbhhVk2CQ_z1FXG2Qf"
18
18
 
19
19
  def _load_env_file() -> Dict[str, str]:
20
20
  env_path = Path.home() / ".meshcode" / "env"
@@ -221,24 +221,32 @@ if not _flip_status("idle", ""):
221
221
  # Fire-and-forget flips so tool execution is never blocked.
222
222
  # ============================================================
223
223
  import functools as _functools
224
+ import threading as _threading
225
+
226
+ _flip_lock = _threading.Lock()
224
227
 
225
228
 
226
229
  async def _async_flip_status(status: str, task: str = "") -> None:
227
230
  try:
228
- await asyncio.to_thread(_flip_status, status, task)
231
+ await asyncio.to_thread(_flip_status_locked, status, task)
229
232
  except Exception as e:
230
233
  log.debug(f"flip_status({status}) failed: {e}")
231
234
 
232
235
 
236
+ def _flip_status_locked(status: str, task: str = "") -> bool:
237
+ """Thread-safe wrapper around _flip_status to prevent concurrent RPCs."""
238
+ with _flip_lock:
239
+ return _flip_status(status, task)
240
+
241
+
233
242
  def _schedule_flip(status: str, task: str = "") -> None:
234
243
  try:
235
244
  loop = asyncio.get_running_loop()
236
245
  loop.create_task(_async_flip_status(status, task))
237
246
  except RuntimeError:
238
247
  # No running loop (sync context) — run in a throwaway thread
239
- import threading
240
- threading.Thread(
241
- target=_flip_status, args=(status, task), daemon=True
248
+ _threading.Thread(
249
+ target=_flip_status_locked, args=(status, task), daemon=True
242
250
  ).start()
243
251
 
244
252
 
@@ -434,6 +442,38 @@ LOOP-SAFETY RULES (IMPORTANT — prevent token-burning ping-pong):
434
442
  - If you find yourself sending more than ~10 messages on the same topic
435
443
  in a row, stop and reassess — you may be in a feedback loop.
436
444
 
445
+ COMMUNICATION EFFICIENCY PROTOCOL (MANDATORY):
446
+
447
+ 1. TASKS OVER MESSAGES: use meshcode_task_create/claim/complete for all
448
+ trackable work assignments, NOT messages. Messages are only for short
449
+ signals: "review needed", "blocked on X", "approved", "question: ...".
450
+ The Tasks panel in the dashboard is where the human sees progress — if
451
+ there are no tasks, the human has no visibility into what you're doing.
452
+
453
+ 2. COMPRESSED REPORTS: when reporting findings, use structured JSON in the
454
+ payload, not prose:
455
+ {{"findings": [{{"severity":"high","file":"X","issue":"Y","fix":"Z"}}],
456
+ "commits":["abc1234"], "next":"awaiting_review"}}
457
+
458
+ 3. NO PROSE PADDING: never write "As you asked, I reviewed the schema and
459
+ found that..." — go straight to the data. Max 2 sentences of context,
460
+ then structured output. Every token costs money.
461
+
462
+ 4. SIGNAL-ONLY MESSAGES: meshcode_send messages must be <100 tokens. If you
463
+ need to communicate more, create a task with the full description and
464
+ send a signal: "task created: <title>".
465
+
466
+ 5. TASK STATUS AS SOURCE OF TRUTH: always create tasks for trackable work.
467
+ Claim them when you start. Complete them when done. The human watches the
468
+ task board, not the chat. No task = invisible work = wasted effort.
469
+
470
+ 6. POLL TASKS, DON'T WAIT FOR MESSAGES: when coordinating, use
471
+ meshcode_tasks() to check task status instead of waiting for a message.
472
+ If you assigned a task, poll the board to see if it's done — don't
473
+ assume the agent will message you. Messages can be missed; task status
474
+ is persistent and reliable. Check meshcode_tasks() before each
475
+ meshcode_wait() call to avoid stale waits.
476
+
437
477
  YOUR FIRST ACTIONS WHEN THIS SESSION STARTS:
438
478
  1. Call meshcode_status() once to see who else is in the meshwork.
439
479
  2. Call meshcode_set_status(status="online", task="ready") to announce
@@ -505,12 +545,37 @@ async def _on_new_message(msg: Dict[str, Any]) -> None:
505
545
 
506
546
 
507
547
  async def _heartbeat_loop():
548
+ """Send heartbeat every 30s via HTTP. Also renews the agent lease and
549
+ logs when the WebSocket is disconnected (heartbeat still works via HTTP
550
+ so the agent stays 'online' in the DB even during WS reconnects)."""
551
+ _lease_counter = 0
508
552
  while True:
509
553
  try:
510
554
  be.sb_rpc("mc_heartbeat", {"p_project_id": _PROJECT_ID, "p_agent_name": AGENT_NAME})
511
- log.debug(f"heartbeat ok for {AGENT_NAME}")
555
+ # Log WS health for diagnostics
556
+ if _REALTIME and not _REALTIME.is_connected:
557
+ log.warning("heartbeat ok (HTTP) but WebSocket is disconnected — realtime messages may be delayed")
558
+ else:
559
+ log.debug(f"heartbeat ok for {AGENT_NAME}")
512
560
  except Exception as e:
513
561
  log.warning(f"heartbeat failed: {e}")
562
+
563
+ # Renew lease every ~2 minutes (every 4th heartbeat) to prevent
564
+ # another instance from stealing it during long sessions.
565
+ _lease_counter += 1
566
+ if _lease_counter % 4 == 0:
567
+ try:
568
+ api_key = _get_api_key()
569
+ if api_key:
570
+ be.sb_rpc("mc_acquire_agent_lease", {
571
+ "p_api_key": api_key,
572
+ "p_project_id": _PROJECT_ID,
573
+ "p_agent_name": AGENT_NAME,
574
+ "p_instance_id": _INSTANCE_ID,
575
+ })
576
+ except Exception as e:
577
+ log.warning(f"lease renewal failed: {e}")
578
+
514
579
  await asyncio.sleep(30)
515
580
 
516
581
 
@@ -112,6 +112,16 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
112
112
  except Exception:
113
113
  pass
114
114
 
115
+ # Detect: are we already inside a Claude Code session? os.execvp(claude)
116
+ # from inside an existing claude won't work — claude needs a fresh
117
+ # interactive terminal it owns. Refuse with a clear message.
118
+ if os.environ.get("CLAUDECODE") == "1" or os.environ.get("CLAUDE_CODE_SESSION"):
119
+ print("[meshcode] ERROR: meshcode run cannot bootstrap a new agent from inside an", file=sys.stderr)
120
+ print("[meshcode] existing Claude Code session.", file=sys.stderr)
121
+ print("[meshcode] Open a fresh Terminal / iTerm window and run the command there.", file=sys.stderr)
122
+ print(f"[meshcode] (or copy this exact line into a new terminal: meshcode run {agent})", file=sys.stderr)
123
+ return 2
124
+
115
125
  found = _find_agent_workspace(agent, project)
116
126
  if not found:
117
127
  return 2
@@ -138,7 +148,6 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
138
148
  cmd = [
139
149
  editor,
140
150
  "--mcp-config", str(ws / ".mcp.json"),
141
- "--strict-mcp-config",
142
151
  ]
143
152
  if mode == "bypass":
144
153
  cmd.append("--dangerously-skip-permissions")
@@ -93,9 +93,9 @@ def _load_supabase_env() -> Dict[str, str]:
93
93
  elif k == "SUPABASE_KEY" and not key:
94
94
  key = v
95
95
  if not url:
96
- url = "https://wwgzzmydrwrjgaebspdo.supabase.co"
96
+ url = "https://gjinagyyjttyxnaoavnz.supabase.co"
97
97
  if not key:
98
- key = "sb_publishable_0qf0U1GURopPIxLR8Vu7eQ_5grflPP4"
98
+ key = "sb_publishable_qwN9PO1L7jUXhhbhhVk2CQ_z1FXG2Qf"
99
99
  return {"SUPABASE_URL": url, "SUPABASE_KEY": key}
100
100
 
101
101
 
@@ -300,10 +300,30 @@ def setup_workspace(project: str, agent: str, role: str = "",
300
300
  print(f"[meshcode] ERROR: cannot load secrets module: {e}", file=sys.stderr)
301
301
  return 2
302
302
 
303
+ if not api_key and keychain_profile == "default" and sys.stdin.isatty():
304
+ # No key in keychain — prompt inline instead of failing
305
+ print("[meshcode] No API key found. You can get one from meshcode.io/settings.", file=sys.stderr)
306
+ try:
307
+ api_key = input("[meshcode] Paste your API key (mc_...): ").strip()
308
+ except (EOFError, KeyboardInterrupt):
309
+ api_key = ""
310
+ if api_key:
311
+ # Store it so they never have to paste again
312
+ try:
313
+ import importlib as _il
314
+ _login = _il.import_module("meshcode.comms_v4").login
315
+ _login(api_key)
316
+ except Exception:
317
+ # Fallback: just store raw in keychain
318
+ try:
319
+ secrets_mod.set_api_key(api_key, profile="default")
320
+ except Exception:
321
+ pass
322
+
303
323
  if not api_key:
304
- print(f"[meshcode] ERROR: no api key found in keychain profile '{keychain_profile}'", file=sys.stderr)
324
+ print(f"[meshcode] ERROR: no api key found.", file=sys.stderr)
305
325
  if keychain_profile == "default":
306
- print("[meshcode] Run `meshcode login <api_key>` first.", file=sys.stderr)
326
+ print("[meshcode] Run `meshcode login <api_key>` or paste it when prompted.", file=sys.stderr)
307
327
  else:
308
328
  print(f"[meshcode] This profile is created by `meshcode join <token>`.", file=sys.stderr)
309
329
  return 2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 1.8.6
3
+ Version: 1.8.9
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 = "1.8.6"
7
+ version = "1.8.9"
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