meshcode 2.0.0__tar.gz → 2.0.2__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.0 → meshcode-2.0.2}/PKG-INFO +24 -1
  2. {meshcode-2.0.0 → meshcode-2.0.2}/README.md +23 -0
  3. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/__init__.py +1 -1
  4. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/comms_v4.py +59 -14
  5. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/meshcode_mcp/server.py +171 -67
  6. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/run_agent.py +91 -10
  7. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/setup_clients.py +48 -3
  8. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode.egg-info/PKG-INFO +24 -1
  9. {meshcode-2.0.0 → meshcode-2.0.2}/pyproject.toml +1 -1
  10. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/cli.py +0 -0
  11. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/invites.py +0 -0
  12. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/launcher.py +0 -0
  13. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/launcher_install.py +0 -0
  14. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/meshcode_mcp/__init__.py +0 -0
  15. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/meshcode_mcp/__main__.py +0 -0
  16. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/meshcode_mcp/backend.py +0 -0
  17. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/meshcode_mcp/realtime.py +0 -0
  18. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/meshcode_mcp/test_backend.py +0 -0
  19. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  20. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/preferences.py +0 -0
  21. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/secrets.py +0 -0
  23. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode/self_update.py +0 -0
  24. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode.egg-info/SOURCES.txt +0 -0
  25. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode.egg-info/dependency_links.txt +0 -0
  26. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode.egg-info/entry_points.txt +0 -0
  27. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode.egg-info/requires.txt +0 -0
  28. {meshcode-2.0.0 → meshcode-2.0.2}/meshcode.egg-info/top_level.txt +0 -0
  29. {meshcode-2.0.0 → meshcode-2.0.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -158,6 +158,29 @@ meshcode revoke-member my-project <user> # kick a member instantly
158
158
 
159
159
  ---
160
160
 
161
+ ## Agent account management
162
+
163
+ Your AI agents can manage your MeshCode account from inside the mesh. Just tell your agent what you need:
164
+
165
+ - **"Create a backend agent"** → agent calls `meshcode_create_meshwork` + `meshcode_add_agent`
166
+ - **"Change the frontend role to UI designer"** → agent calls `meshcode_edit_agent`
167
+ - **"Give the backend agent a note about our conventions"** → agent calls `meshcode_edit_memory`
168
+
169
+ Available MCP tools for agents:
170
+
171
+ | Tool | What it does |
172
+ |------|-------------|
173
+ | `meshcode_create_meshwork(name)` | Create a new meshwork |
174
+ | `meshcode_add_agent(name, role)` | Add an agent to the current meshwork |
175
+ | `meshcode_edit_agent(name, role?, launch_prompt?)` | Update agent role or system prompt |
176
+ | `meshcode_edit_memory(agent_name, key, value)` | Edit another agent's persistent memory |
177
+ | `meshcode_scratchpad_set(key, value)` | Write to shared meshwork memory |
178
+ | `meshcode_link(target_meshwork)` | Link two meshworks for cross-mesh communication |
179
+
180
+ The agent will always tell you what CLI command to run next (e.g., "Open a new terminal and run `meshcode run backend`").
181
+
182
+ ---
183
+
161
184
  ## Editor support
162
185
 
163
186
  | Editor | Auto-detected? | Config file written |
@@ -133,6 +133,29 @@ meshcode revoke-member my-project <user> # kick a member instantly
133
133
 
134
134
  ---
135
135
 
136
+ ## Agent account management
137
+
138
+ Your AI agents can manage your MeshCode account from inside the mesh. Just tell your agent what you need:
139
+
140
+ - **"Create a backend agent"** → agent calls `meshcode_create_meshwork` + `meshcode_add_agent`
141
+ - **"Change the frontend role to UI designer"** → agent calls `meshcode_edit_agent`
142
+ - **"Give the backend agent a note about our conventions"** → agent calls `meshcode_edit_memory`
143
+
144
+ Available MCP tools for agents:
145
+
146
+ | Tool | What it does |
147
+ |------|-------------|
148
+ | `meshcode_create_meshwork(name)` | Create a new meshwork |
149
+ | `meshcode_add_agent(name, role)` | Add an agent to the current meshwork |
150
+ | `meshcode_edit_agent(name, role?, launch_prompt?)` | Update agent role or system prompt |
151
+ | `meshcode_edit_memory(agent_name, key, value)` | Edit another agent's persistent memory |
152
+ | `meshcode_scratchpad_set(key, value)` | Write to shared meshwork memory |
153
+ | `meshcode_link(target_meshwork)` | Link two meshworks for cross-mesh communication |
154
+
155
+ The agent will always tell you what CLI command to run next (e.g., "Open a new terminal and run `meshcode run backend`").
156
+
157
+ ---
158
+
136
159
  ## Editor support
137
160
 
138
161
  | Editor | Auto-detected? | Config file written |
@@ -1,2 +1,2 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.0.0"
2
+ __version__ = "2.0.2"
@@ -1248,19 +1248,32 @@ def show_history(project, last_n=20, between=None):
1248
1248
 
1249
1249
 
1250
1250
  def list_projects():
1251
- projects_data = sb_select("mc_projects", "", order="created_at.asc")
1252
- if not projects_data:
1253
- print("[COMMS] Sin proyectos")
1251
+ api_key = _load_api_key_for_cli()
1252
+ if not api_key:
1253
+ print("[meshcode] Not authenticated. Run `meshcode login <api_key>` first.")
1254
+ return
1255
+
1256
+ data = sb_rpc("mc_list_user_projects", {"p_api_key": api_key})
1257
+ if isinstance(data, dict) and data.get("error"):
1258
+ print(f"[meshcode] ERROR: {data['error']}")
1259
+ return
1260
+
1261
+ projects = []
1262
+ if isinstance(data, dict):
1263
+ projects = data.get("projects", [])
1264
+
1265
+ if not projects:
1266
+ print("[meshcode] No projects found. Create one at meshcode.io or with `meshcode_create_meshwork`.")
1254
1267
  return
1255
1268
 
1256
1269
  print(f"\n{'='*60}")
1257
- print(f" PROYECTOS ACTIVOS (Supabase)")
1270
+ print(f" YOUR MESHWORKS")
1258
1271
  print(f"{'='*60}")
1259
1272
 
1260
- for proj in projects_data:
1261
- agents = sb_select("mc_agents", f"project_id=eq.{proj['id']}", order="name.asc")
1273
+ for proj in projects:
1274
+ agents = proj.get("agents", [])
1262
1275
  statuses = [f"{a['name']}({a.get('status','?')})" for a in agents]
1263
- print(f" [{proj['name']}] {', '.join(statuses) if statuses else 'sin agentes'}")
1276
+ print(f" [{proj['name']}] {', '.join(statuses) if statuses else 'no agents'}")
1264
1277
  print()
1265
1278
 
1266
1279
 
@@ -1503,11 +1516,12 @@ def connect(project, name, hook_target="claude", role=""):
1503
1516
  comms_path = str(Path(__file__).resolve())
1504
1517
 
1505
1518
  if hook_target == "claude":
1506
- # Delegate to the universal setup_clients writer. `meshcode connect` is
1507
- # now a backwards-compat alias for `meshcode setup claude-code`.
1519
+ # Use workspace flow (NOT global) to avoid polluting ~/.claude.json.
1520
+ # Global configs cause all Claude Code windows to load all agents,
1521
+ # triggering lease conflicts and "x failed" errors.
1508
1522
  import importlib
1509
- _setup_client = importlib.import_module("meshcode.setup_clients").setup
1510
- _setup_client("claude-code", project, name, actual_role)
1523
+ _setup_ws = importlib.import_module("meshcode.setup_clients").setup_workspace
1524
+ _setup_ws(project, name, actual_role)
1511
1525
 
1512
1526
  elif hook_target == "codex":
1513
1527
  config_path = Path.cwd() / ".meshcode.json"
@@ -1893,7 +1907,7 @@ if __name__ == "__main__":
1893
1907
  proj = sys.argv[2] if len(sys.argv) > 2 else None
1894
1908
  show_status(proj)
1895
1909
 
1896
- elif cmd == "projects":
1910
+ elif cmd in ("projects", "list", "ls"):
1897
1911
  list_projects()
1898
1912
 
1899
1913
  elif cmd == "history":
@@ -2180,6 +2194,37 @@ if __name__ == "__main__":
2180
2194
  show_help()
2181
2195
 
2182
2196
  else:
2183
- print(f"[ERROR] Comando desconocido: {cmd}")
2184
- show_help()
2197
+ known_cmds = [
2198
+ "register", "send", "broadcast", "read", "check", "watch",
2199
+ "board", "update", "status", "projects", "list", "ls",
2200
+ "history", "clear", "unregister", "connect", "disconnect",
2201
+ "setup", "run", "invite", "join", "invites", "members",
2202
+ "revoke-invite", "revoke-member", "login", "prefs", "launcher",
2203
+ "help", "profile", "validate-sessions", "wake-headless",
2204
+ ]
2205
+ # Simple fuzzy: prefix match + Levenshtein-like best match
2206
+ suggestions = [c for c in known_cmds if c.startswith(cmd)]
2207
+ if not suggestions:
2208
+ # Try substring match
2209
+ suggestions = [c for c in known_cmds if cmd in c]
2210
+ if not suggestions:
2211
+ # Levenshtein distance 2 or less
2212
+ def _dist(a, b):
2213
+ if len(a) > len(b): a, b = b, a
2214
+ dists = list(range(len(a) + 1))
2215
+ for j, cb in enumerate(b):
2216
+ new = [j + 1]
2217
+ for i, ca in enumerate(a):
2218
+ cost = 0 if ca == cb else 1
2219
+ new.append(min(new[-1] + 1, dists[i + 1] + 1, dists[i] + cost))
2220
+ dists = new
2221
+ return dists[-1]
2222
+ scored = [(c, _dist(cmd, c)) for c in known_cmds]
2223
+ suggestions = [c for c, d in scored if d <= 2]
2224
+
2225
+ if suggestions:
2226
+ print(f"[meshcode] Unknown command: '{cmd}'. Did you mean: {', '.join(suggestions[:3])}?")
2227
+ else:
2228
+ print(f"[meshcode] Unknown command: '{cmd}'.")
2229
+ print(f"[meshcode] Run `meshcode help` for all commands.")
2185
2230
  sys.exit(1)
@@ -240,30 +240,31 @@ if isinstance(_register_result, dict) and _register_result.get("error"):
240
240
  # bypass RLS — the publishable key has no JWT context and cannot UPDATE
241
241
  # mc_agents directly. The RPC validates ownership via api_key.
242
242
  def _flip_status(status: str, task: str = "") -> bool:
243
- api_key = _get_api_key()
244
- if not api_key:
245
- # Last-resort fallback: try the direct PATCH (may be denied by RLS)
243
+ """Write status directly to mc_agents table for instant Realtime propagation.
244
+
245
+ Uses direct PATCH (not RPC) so Supabase Realtime fires an UPDATE event
246
+ immediately — the dashboard sees the change in <100ms instead of waiting
247
+ for the next heartbeat cycle.
248
+ """
249
+ try:
250
+ be.set_status(_PROJECT_ID, AGENT_NAME, status, task)
251
+ return True
252
+ except Exception:
253
+ # Fallback to RPC if direct PATCH fails (RLS issue)
254
+ api_key = _get_api_key()
255
+ if not api_key:
256
+ return False
246
257
  try:
247
- be.set_status(_PROJECT_ID, AGENT_NAME, status, task)
248
- return True
258
+ r = be.sb_rpc("mc_agent_set_status_by_api_key", {
259
+ "p_api_key": api_key,
260
+ "p_project_id": _PROJECT_ID,
261
+ "p_agent_name": AGENT_NAME,
262
+ "p_status": status,
263
+ "p_task": task,
264
+ })
265
+ return isinstance(r, dict) and r.get("ok", False)
249
266
  except Exception:
250
267
  return False
251
- try:
252
- r = be.sb_rpc("mc_agent_set_status_by_api_key", {
253
- "p_api_key": api_key,
254
- "p_project_id": _PROJECT_ID,
255
- "p_agent_name": AGENT_NAME,
256
- "p_status": status,
257
- "p_task": task,
258
- })
259
- if isinstance(r, dict) and r.get("ok"):
260
- return True
261
- if isinstance(r, dict) and r.get("error"):
262
- log.warning(f"set_status RPC: {r['error']}")
263
- return False
264
- except Exception as e:
265
- log.warning(f"set_status RPC threw: {e}")
266
- return False
267
268
 
268
269
  if not _flip_status("idle", ""):
269
270
  print(f"[meshcode-mcp] WARNING: could not flip status to idle", file=sys.stderr)
@@ -341,23 +342,67 @@ def _acquire_lease() -> bool:
341
342
  api_key = _get_api_key()
342
343
  if not api_key:
343
344
  return True # legacy clients without api_key skip lease check
344
- try:
345
- r = be.sb_rpc("mc_acquire_agent_lease", {
346
- "p_api_key": api_key,
347
- "p_project_id": _PROJECT_ID,
348
- "p_agent_name": AGENT_NAME,
349
- "p_instance_id": _INSTANCE_ID,
350
- })
351
- if isinstance(r, dict) and r.get("ok"):
352
- return True
353
- if isinstance(r, dict) and r.get("error"):
354
- print(f"[meshcode-mcp] LEASE DENIED: {r['error']}", file=sys.stderr)
355
- print(f"[meshcode-mcp] Another window is already running this agent.", file=sys.stderr)
356
- print(f"[meshcode-mcp] Close the other window first, or use a different agent name.", file=sys.stderr)
357
- return False
358
- except Exception as e:
359
- print(f"[meshcode-mcp] WARNING: lease RPC failed: {e}", file=sys.stderr)
360
- return True # don't hard-fail on transient network issues
345
+ import time as _time
346
+ for attempt in range(3):
347
+ try:
348
+ r = be.sb_rpc("mc_acquire_agent_lease", {
349
+ "p_api_key": api_key,
350
+ "p_project_id": _PROJECT_ID,
351
+ "p_agent_name": AGENT_NAME,
352
+ "p_instance_id": _INSTANCE_ID,
353
+ })
354
+ if isinstance(r, dict) and r.get("ok"):
355
+ return True
356
+ if isinstance(r, dict) and r.get("error"):
357
+ err = str(r.get("error", ""))
358
+ if "already running" in err:
359
+ if attempt < 2:
360
+ # The old lease might be stale — wait and retry
361
+ # (the 90s stale check in the RPC should clear it)
362
+ print(f"[meshcode-mcp] Lease held by another instance — retrying in {2 * (attempt+1)}s...", file=sys.stderr)
363
+ _time.sleep(2 * (attempt + 1))
364
+ continue
365
+ # Final attempt — force release the old lease and try once more
366
+ print(f"[meshcode-mcp] Force-releasing stale lease...", file=sys.stderr)
367
+ try:
368
+ be.sb_rpc("mc_release_agent_lease", {
369
+ "p_api_key": api_key,
370
+ "p_project_id": _PROJECT_ID,
371
+ "p_agent_name": AGENT_NAME,
372
+ "p_instance_id": r.get("held_by", "unknown"),
373
+ })
374
+ except Exception:
375
+ # Force clear via direct update
376
+ try:
377
+ be.set_status(_PROJECT_ID, AGENT_NAME, "offline", "")
378
+ except Exception:
379
+ pass
380
+ _time.sleep(1)
381
+ # One more try after force-release
382
+ try:
383
+ r2 = be.sb_rpc("mc_acquire_agent_lease", {
384
+ "p_api_key": api_key,
385
+ "p_project_id": _PROJECT_ID,
386
+ "p_agent_name": AGENT_NAME,
387
+ "p_instance_id": _INSTANCE_ID,
388
+ })
389
+ if isinstance(r2, dict) and r2.get("ok"):
390
+ print(f"[meshcode-mcp] Lease acquired after force-release.", file=sys.stderr)
391
+ return True
392
+ except Exception:
393
+ pass
394
+ print(f"[meshcode-mcp] ERROR: Could not start — agent '{AGENT_NAME}' is running in another window.", file=sys.stderr)
395
+ print(f"[meshcode-mcp] Close the other window first, or use a different agent name.", file=sys.stderr)
396
+ return False
397
+ print(f"[meshcode-mcp] lease attempt {attempt+1}: {r.get('error')}", file=sys.stderr)
398
+ else:
399
+ return True
400
+ except Exception as e:
401
+ print(f"[meshcode-mcp] lease attempt {attempt+1} failed: {e}", file=sys.stderr)
402
+ if attempt < 2:
403
+ _time.sleep(2)
404
+ print(f"[meshcode-mcp] WARNING: lease failed after 3 attempts — proceeding anyway", file=sys.stderr)
405
+ return True
361
406
 
362
407
  if not _acquire_lease():
363
408
  sys.exit(2)
@@ -418,7 +463,7 @@ def _build_instructions() -> str:
418
463
  f"\nUSER-PROVIDED ROLE PROMPT (from the dashboard):\n---\n{_LAUNCH_PROMPT}\n---\n"
419
464
  if _LAUNCH_PROMPT else ""
420
465
  )
421
- return f"""You are agent "{AGENT_NAME}" in meshwork "{PROJECT_NAME}".{role_block}{launch_block}
466
+ base = f"""You are agent "{AGENT_NAME}" in meshwork "{PROJECT_NAME}".{role_block}{launch_block}
422
467
 
423
468
  BEHAVIOR LOOP (your default state — never exit unless told):
424
469
  1. Act on task/message → 2. meshcode_send if needed → 3. meshcode_wait()
@@ -427,6 +472,9 @@ BEHAVIOR LOOP (your default state — never exit unless told):
427
472
  5. Only break loop if: user says stop, fatal error, or "tell the human X".
428
473
 
429
474
  RULES:
475
+ - NEVER use CLI commands (meshcode watch, meshcode read, meshcode send) in bash.
476
+ Use ONLY the MCP tools: meshcode_check, meshcode_read, meshcode_send, meshcode_wait.
477
+ CLI commands are for humans in terminal. You have MCP tools — use them.
430
478
  - Tasks > messages. Use meshcode_tasks/claim/complete for trackable work.
431
479
  - Messages <100 tokens, signal-only. Long content → create task instead.
432
480
  - No empty acks ("OK"/"Got it"). No prose padding. JSON reports only.
@@ -436,8 +484,10 @@ RULES:
436
484
  - No feedback loops: stop if >10 messages on same topic.
437
485
 
438
486
  SESSION START:
439
- 1. meshcode_status() — see who's online
440
- 2. Act on user task or meshcode_wait()
487
+ 1. meshcode_set_status(status="online", task="ready") — announce you're online
488
+ 2. meshcode_check() read any messages waiting in your inbox
489
+ 3. meshcode_status() — see who's online
490
+ 4. Act on user task or meshcode_wait()
441
491
  (Memories are pre-loaded below — no need to call meshcode_recall on boot.)
442
492
 
443
493
  CROSS-MESH: meshcode_send(to="agent@meshwork") routes via active link.
@@ -459,6 +509,23 @@ what CLI command to run next (e.g. "meshcode run backend in a new terminal").
459
509
 
460
510
  Setup help → README.md or https://meshcode.io/docs
461
511
  """
512
+ # Inject commander protocol if this agent is a leader
513
+ is_leader = any(k in (_ROLE_DESCRIPTION or '').lower() + AGENT_NAME.lower() for k in ('commander', 'lead', 'orchestrat'))
514
+ if is_leader:
515
+ base += """
516
+ COMMANDER PROTOCOL (you are the team lead):
517
+ - ALWAYS delegate via meshcode_task_create, NEVER via long messages.
518
+ - Tasks are the source of truth. Messages are signals only (<100 tokens).
519
+ - Workflow: identify work → create task → signal assignee → poll progress → verify → next.
520
+ - Poll meshcode_tasks() to track completion. Don't wait for messages.
521
+ - Verify builds/quality before approving. Don't approve blindly.
522
+ - Communicate changes to affected agents IMMEDIATELY after you make them.
523
+ - Organize: break big requests into multiple tasks, assign to the right agent.
524
+ - Keep the human informed with brief status updates at milestones.
525
+ - You are autonomous: fix small issues yourself, delegate big ones.
526
+ - After each sprint: consolidate learnings, update scratchpad, save to memory.
527
+ """
528
+ return base
462
529
 
463
530
 
464
531
  _INSTRUCTIONS = _build_instructions()
@@ -513,26 +580,34 @@ async def _on_new_message(msg: Dict[str, Any]) -> None:
513
580
  log.debug(f"send_resource_updated unavailable: {e}")
514
581
 
515
582
 
516
- async def _heartbeat_loop():
517
- """Send heartbeat every 30s via HTTP. Also renews the agent lease and
518
- logs when the WebSocket is disconnected (heartbeat still works via HTTP
519
- so the agent stays 'online' in the DB even during WS reconnects)."""
520
- _lease_counter = 0
521
- while True:
583
+ _heartbeat_stop = _threading.Event()
584
+
585
+
586
+ def _heartbeat_thread_fn():
587
+ """Heartbeat in a DAEMON THREAD — independent of asyncio event loop.
588
+
589
+ This ensures heartbeats continue even when tool calls are cancelled,
590
+ meshcode_wait is rejected, or the asyncio loop is busy. The agent
591
+ stays 'online' in the dashboard as long as the MCP process is alive.
592
+ """
593
+ lease_counter = 0
594
+ while not _heartbeat_stop.is_set():
522
595
  try:
523
- be.sb_rpc("mc_heartbeat", {"p_project_id": _PROJECT_ID, "p_agent_name": AGENT_NAME})
524
- # Log WS health for diagnostics
596
+ be.sb_rpc("mc_heartbeat", {"p_project_id": _PROJECT_ID, "p_agent_name": AGENT_NAME, "p_version": "2.0.0"})
597
+ # Also ensure status is at least "idle" (not "offline") between tool calls
598
+ try:
599
+ be.set_status(_PROJECT_ID, AGENT_NAME, "idle", "")
600
+ except Exception:
601
+ pass
525
602
  if _REALTIME and not _REALTIME.is_connected:
526
- log.warning("heartbeat ok (HTTP) but WebSocket is disconnected — realtime messages may be delayed")
603
+ log.warning("heartbeat ok (HTTP) but WebSocket disconnected")
527
604
  else:
528
605
  log.debug(f"heartbeat ok for {AGENT_NAME}")
529
606
  except Exception as e:
530
607
  log.warning(f"heartbeat failed: {e}")
531
608
 
532
- # Renew lease every ~2 minutes (every 4th heartbeat) to prevent
533
- # another instance from stealing it during long sessions.
534
- _lease_counter += 1
535
- if _lease_counter % 4 == 0:
609
+ lease_counter += 1
610
+ if lease_counter % 4 == 0:
536
611
  try:
537
612
  api_key = _get_api_key()
538
613
  if api_key:
@@ -545,7 +620,7 @@ async def _heartbeat_loop():
545
620
  except Exception as e:
546
621
  log.warning(f"lease renewal failed: {e}")
547
622
 
548
- await asyncio.sleep(30)
623
+ _heartbeat_stop.wait(30) # sleep but interruptible on shutdown
549
624
 
550
625
 
551
626
  @asynccontextmanager
@@ -560,17 +635,30 @@ async def lifespan(_app):
560
635
  notify_callback=_on_new_message,
561
636
  )
562
637
  await _REALTIME.start()
563
- hb_task = asyncio.create_task(_heartbeat_loop())
564
- log.info(f"lifespan started Realtime + heartbeat (30s) active for {AGENT_NAME}")
638
+
639
+ # IMMEDIATE: send first heartbeat + set online status BEFORE any tool calls.
640
+ # Without this, the agent appears offline for up to 30s after boot.
641
+ for _attempt in range(3):
642
+ try:
643
+ be.sb_rpc("mc_heartbeat", {"p_project_id": _PROJECT_ID, "p_agent_name": AGENT_NAME, "p_version": "2.0.0"})
644
+ be.set_status(_PROJECT_ID, AGENT_NAME, "idle", "MCP session active")
645
+ log.info(f"[meshcode] Agent {AGENT_NAME} online — initial heartbeat sent")
646
+ break
647
+ except Exception as e:
648
+ log.warning(f"initial heartbeat attempt {_attempt+1} failed: {e}")
649
+ import time; time.sleep(2)
650
+
651
+ # Heartbeat in daemon thread — independent of asyncio event loop.
652
+ _heartbeat_stop.clear()
653
+ hb_thread = _threading.Thread(target=_heartbeat_thread_fn, daemon=True, name="meshcode-heartbeat")
654
+ hb_thread.start()
655
+ log.info(f"lifespan started — Realtime + heartbeat thread active for {AGENT_NAME}")
565
656
  try:
566
657
  yield {"realtime": _REALTIME}
567
658
  finally:
568
659
  log.info("lifespan shutdown — stopping heartbeat + realtime + releasing lease")
569
- hb_task.cancel()
570
- try:
571
- await hb_task
572
- except asyncio.CancelledError:
573
- pass
660
+ _heartbeat_stop.set()
661
+ hb_thread.join(timeout=5)
574
662
  await _REALTIME.stop()
575
663
  # Flip to offline + release lease so the dashboard reflects reality
576
664
  # within seconds (not waiting for the 30s cron to notice).
@@ -786,15 +874,32 @@ def meshcode_done(reason: str) -> Dict[str, Any]:
786
874
  @mcp.tool()
787
875
  @with_working_status
788
876
  def meshcode_check(include_acks: bool = False) -> Dict[str, Any]:
789
- """Non-blocking: returns pending message count + buffered messages from the
790
- Realtime listener since the last check.
877
+ """Non-blocking: returns pending message count + any new messages.
791
878
 
792
- Use this to check if there's anything new without marking messages as read.
793
- Useful as the first call in a tool loop or after a long thinking phase.
879
+ Checks realtime buffer first, then falls back to DB if buffer is empty
880
+ but there are pending messages (handles messages that arrived before
881
+ the realtime listener connected).
794
882
  """
795
883
  pending = be.count_pending(_PROJECT_ID, AGENT_NAME)
796
884
  realtime_buffered = _REALTIME.drain() if _REALTIME else []
797
885
  deduped = _filter_and_mark(realtime_buffered)
886
+
887
+ # Fallback: if realtime buffer is empty but DB has pending messages,
888
+ # fetch them from the DB so they're not invisible to the agent.
889
+ if not deduped and pending > 0:
890
+ raw = be.read_inbox(_PROJECT_ID, AGENT_NAME)
891
+ deduped = _filter_and_mark([
892
+ {
893
+ "from": m["from_agent"],
894
+ "type": m.get("type", "msg"),
895
+ "ts": m.get("created_at"),
896
+ "payload": m.get("payload", {}),
897
+ "id": m.get("id"),
898
+ "parent_id": m.get("parent_msg_id"),
899
+ }
900
+ for m in raw
901
+ ])
902
+
798
903
  split = _split_messages(deduped)
799
904
  if not include_acks:
800
905
  split["acks"] = []
@@ -1271,7 +1376,6 @@ def meshcode_forget(key: str) -> Dict[str, Any]:
1271
1376
 
1272
1377
  # ----------------- RESOURCES -----------------
1273
1378
 
1274
- @mcp.resource("meshcode://inbox")
1275
1379
  @mcp.tool()
1276
1380
  def meshcode_auto_wake(enabled: bool) -> Dict[str, Any]:
1277
1381
  """Toggle auto-wake: when enabled, if this agent receives a mesh message
@@ -32,6 +32,77 @@ WORKSPACES_ROOT = Path.home() / "meshcode"
32
32
  REGISTRY_PATH = WORKSPACES_ROOT / ".registry.json"
33
33
 
34
34
 
35
+ def _try_auto_setup(agent: str, project: Optional[str] = None) -> Optional[Tuple[Path, str]]:
36
+ """If agent exists on the server but has no local workspace, auto-create it.
37
+
38
+ Returns (workspace_path, project_name) on success, None on failure.
39
+ """
40
+ try:
41
+ from .setup_clients import _load_supabase_env, setup_workspace
42
+ import importlib
43
+ secrets_mod = importlib.import_module("meshcode.secrets")
44
+ except Exception:
45
+ return None
46
+
47
+ api_key = secrets_mod.get_api_key(profile="default")
48
+ if not api_key:
49
+ return None
50
+
51
+ sb = _load_supabase_env()
52
+
53
+ # Ask the server which project(s) this agent belongs to
54
+ try:
55
+ from urllib.request import Request, urlopen
56
+ body = json.dumps({"p_api_key": api_key, "p_agent_name": agent}).encode()
57
+ req = Request(
58
+ f"{sb['SUPABASE_URL']}/rest/v1/rpc/mc_resolve_agent_projects",
59
+ data=body,
60
+ method="POST",
61
+ headers={
62
+ "apikey": sb["SUPABASE_KEY"],
63
+ "Authorization": f"Bearer {sb['SUPABASE_KEY']}",
64
+ "Content-Type": "application/json",
65
+ },
66
+ )
67
+ with urlopen(req, timeout=10) as resp:
68
+ data = json.loads(resp.read().decode())
69
+ except Exception:
70
+ return None
71
+
72
+ if not isinstance(data, dict) or data.get("error"):
73
+ return None
74
+
75
+ projects = data.get("projects", [])
76
+ if not projects:
77
+ return None
78
+
79
+ # If user specified a project, filter to that one
80
+ if project:
81
+ projects = [p for p in projects if p["project_name"] == project]
82
+ if not projects:
83
+ return None
84
+
85
+ if len(projects) > 1:
86
+ print(f"[meshcode] Agent '{agent}' exists in multiple projects:", file=sys.stderr)
87
+ for p in projects:
88
+ print(f"[meshcode] meshcode run {agent} --project {p['project_name']}", file=sys.stderr)
89
+ print(f"[meshcode] Specify which one with --project.", file=sys.stderr)
90
+ return None
91
+
92
+ resolved_project = projects[0]["project_name"]
93
+ role = projects[0].get("role", "")
94
+
95
+ print(f"[meshcode] Workspace recreated automatically for agent '{agent}' (project: {resolved_project})")
96
+ rc = setup_workspace(resolved_project, agent, role)
97
+ if rc != 0:
98
+ return None
99
+
100
+ ws = WORKSPACES_ROOT / f"{resolved_project}-{agent}"
101
+ if ws.exists():
102
+ return ws, resolved_project
103
+ return None
104
+
105
+
35
106
  def _load_registry() -> dict:
36
107
  if not REGISTRY_PATH.exists():
37
108
  return {}
@@ -41,11 +112,12 @@ def _load_registry() -> dict:
41
112
  return {}
42
113
 
43
114
 
44
- def _find_agent_workspace(agent: str, project: Optional[str] = None) -> Optional[Tuple[Path, str]]:
115
+ def _find_agent_workspace(agent: str, project: Optional[str] = None, quiet: bool = False) -> Optional[Tuple[Path, str]]:
45
116
  """Look up the agent in the registry. Returns (workspace_path, project_name) or None.
46
117
 
47
118
  If multiple agents share the same name across projects, requires the
48
119
  user to disambiguate by passing project explicitly.
120
+ When quiet=True, suppresses error messages (used before auto-setup fallback).
49
121
  """
50
122
  reg = _load_registry()
51
123
  agents = reg.get("agents", {})
@@ -55,11 +127,11 @@ def _find_agent_workspace(agent: str, project: Optional[str] = None) -> Optional
55
127
  if info:
56
128
  ws = Path(info["workspace"])
57
129
  if not ws.exists():
58
- print(f"[meshcode] ERROR: workspace dir for '{agent}' is missing: {ws}", file=sys.stderr)
59
- print(f"[meshcode] Re-run: meshcode setup {info.get('project','<project>')} {agent}", file=sys.stderr)
130
+ # Don't print error in quiet mode caller will try auto-setup
60
131
  return None
61
132
  if project and info.get("project") != project:
62
- print(f"[meshcode] ERROR: agent '{agent}' belongs to project '{info.get('project')}', not '{project}'", file=sys.stderr)
133
+ if not quiet:
134
+ print(f"[meshcode] ERROR: agent '{agent}' belongs to project '{info.get('project')}', not '{project}'", file=sys.stderr)
63
135
  return None
64
136
  return ws, info.get("project", "")
65
137
 
@@ -81,8 +153,6 @@ def _find_agent_workspace(agent: str, project: Optional[str] = None) -> Optional
81
153
  print(f"[meshcode] Disambiguate: meshcode run {agent} --project <name>", file=sys.stderr)
82
154
  return None
83
155
 
84
- print(f"[meshcode] ERROR: no workspace found for agent '{agent}'", file=sys.stderr)
85
- print(f"[meshcode] Run `meshcode setup <project> {agent}` first.", file=sys.stderr)
86
156
  return None
87
157
 
88
158
 
@@ -122,9 +192,14 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
122
192
  print(f"[meshcode] (or copy this exact line into a new terminal: meshcode run {agent})", file=sys.stderr)
123
193
  return 2
124
194
 
125
- found = _find_agent_workspace(agent, project)
195
+ found = _find_agent_workspace(agent, project, quiet=True)
126
196
  if not found:
127
- return 2
197
+ # Auto-setup: if agent exists on the server, recreate workspace
198
+ found = _try_auto_setup(agent, project)
199
+ if not found:
200
+ print(f"[meshcode] ERROR: no workspace found for agent '{agent}'", file=sys.stderr)
201
+ print(f"[meshcode] Run `meshcode setup <project> {agent}` first.", file=sys.stderr)
202
+ return 2
128
203
  ws, resolved_project = found
129
204
  server_id = f"meshcode-{resolved_project}-{agent}"
130
205
 
@@ -183,8 +258,14 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
183
258
  cmd = [editor, str(ws)]
184
259
 
185
260
  try:
186
- # Replace this process with the editor so the user gets a clean tab.
187
- os.execvp(cmd[0], cmd)
261
+ if sys.platform == "win32":
262
+ # Windows: no execvp, use subprocess and wait
263
+ import subprocess as _sp
264
+ result = _sp.run(cmd)
265
+ sys.exit(result.returncode)
266
+ else:
267
+ # Unix: replace this process with the editor
268
+ os.execvp(cmd[0], cmd)
188
269
  except FileNotFoundError:
189
270
  print(f"[meshcode] ERROR: '{editor}' not found in PATH", file=sys.stderr)
190
271
  return 127
@@ -120,6 +120,8 @@ def _resolve_project_id(api_key: str, project: str, sb: Dict[str, str]) -> str:
120
120
  return data["project_id"]
121
121
  if isinstance(data, dict) and data.get("error"):
122
122
  print(f"[meshcode] ERROR: could not resolve project '{project}': {data['error']}", file=sys.stderr)
123
+ # Try to suggest the user's actual projects
124
+ _suggest_projects(api_key, sb)
123
125
  sys.exit(2)
124
126
  except Exception as e:
125
127
  print(f"[meshcode] ERROR: could not resolve project '{project}': {e}", file=sys.stderr)
@@ -128,6 +130,32 @@ def _resolve_project_id(api_key: str, project: str, sb: Dict[str, str]) -> str:
128
130
  return ""
129
131
 
130
132
 
133
+ def _suggest_projects(api_key: str, sb: dict):
134
+ """Try to list the user's projects to help with typos."""
135
+ try:
136
+ from urllib.request import Request as _Req, urlopen as _urlopen
137
+ body = json.dumps({"p_api_key": api_key}).encode()
138
+ req = _Req(
139
+ f"{sb['SUPABASE_URL']}/rest/v1/rpc/mc_list_user_projects",
140
+ data=body,
141
+ method="POST",
142
+ headers={
143
+ "apikey": sb["SUPABASE_KEY"],
144
+ "Authorization": f"Bearer {sb['SUPABASE_KEY']}",
145
+ "Content-Type": "application/json",
146
+ },
147
+ )
148
+ with _urlopen(req, timeout=10) as resp:
149
+ data = json.loads(resp.read().decode())
150
+ projects = data.get("projects", []) if isinstance(data, dict) else []
151
+ if projects:
152
+ print(f"[meshcode] Your projects:", file=sys.stderr)
153
+ for p in projects:
154
+ print(f"[meshcode] - {p['name']}", file=sys.stderr)
155
+ except Exception:
156
+ pass
157
+
158
+
131
159
  def _build_server_block(project: str, project_id: str, agent: str, role: str,
132
160
  api_key: str, sb: Dict[str, str],
133
161
  keychain_profile: str = "default") -> Dict[str, Any]:
@@ -588,12 +616,29 @@ def setup(*args) -> int:
588
616
  return 1
589
617
 
590
618
  if args[0] in CLIENT_CONFIG_PATHS:
619
+ if args[0] == "claude-desktop":
620
+ # Claude Desktop needs global config — only exception
621
+ if len(args) < 3:
622
+ print(f"[meshcode] Missing agent name.", file=sys.stderr)
623
+ if len(args) >= 2:
624
+ print(f"[meshcode] Usage: meshcode setup claude-desktop {args[1]} <agent-name>", file=sys.stderr)
625
+ else:
626
+ print(f"[meshcode] Usage: meshcode setup claude-desktop <project> <agent>", file=sys.stderr)
627
+ return 1
628
+ return setup_global(args[0], args[1], args[2], args[3] if len(args) > 3 else "")
629
+ # All other clients: redirect to workspace flow (no global pollution)
630
+ print(f"[meshcode] NOTE: 'meshcode setup {args[0]}' is deprecated. Using workspace flow.", file=sys.stderr)
591
631
  if len(args) < 3:
592
- print("Usage: meshcode setup <client> <project> <agent> [role]", file=sys.stderr)
632
+ print(f"[meshcode] Missing agent name.", file=sys.stderr)
633
+ if len(args) >= 2:
634
+ print(f"[meshcode] Usage: meshcode setup {args[1]} <agent-name>", file=sys.stderr)
635
+ else:
636
+ print(f"[meshcode] Usage: meshcode setup <project> <agent>", file=sys.stderr)
593
637
  return 1
594
- return setup_global(args[0], args[1], args[2], args[3] if len(args) > 3 else "")
638
+ return setup_workspace(args[1], args[2], args[3] if len(args) > 3 else "")
595
639
 
596
640
  if len(args) < 2:
597
- print("Usage: meshcode setup <project> <agent> [role]", file=sys.stderr)
641
+ print(f"[meshcode] Missing agent name.", file=sys.stderr)
642
+ print(f"[meshcode] Usage: meshcode setup {args[0]} <agent-name>", file=sys.stderr)
598
643
  return 1
599
644
  return setup_workspace(args[0], args[1], args[2] if len(args) > 2 else "")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -158,6 +158,29 @@ meshcode revoke-member my-project <user> # kick a member instantly
158
158
 
159
159
  ---
160
160
 
161
+ ## Agent account management
162
+
163
+ Your AI agents can manage your MeshCode account from inside the mesh. Just tell your agent what you need:
164
+
165
+ - **"Create a backend agent"** → agent calls `meshcode_create_meshwork` + `meshcode_add_agent`
166
+ - **"Change the frontend role to UI designer"** → agent calls `meshcode_edit_agent`
167
+ - **"Give the backend agent a note about our conventions"** → agent calls `meshcode_edit_memory`
168
+
169
+ Available MCP tools for agents:
170
+
171
+ | Tool | What it does |
172
+ |------|-------------|
173
+ | `meshcode_create_meshwork(name)` | Create a new meshwork |
174
+ | `meshcode_add_agent(name, role)` | Add an agent to the current meshwork |
175
+ | `meshcode_edit_agent(name, role?, launch_prompt?)` | Update agent role or system prompt |
176
+ | `meshcode_edit_memory(agent_name, key, value)` | Edit another agent's persistent memory |
177
+ | `meshcode_scratchpad_set(key, value)` | Write to shared meshwork memory |
178
+ | `meshcode_link(target_meshwork)` | Link two meshworks for cross-mesh communication |
179
+
180
+ The agent will always tell you what CLI command to run next (e.g., "Open a new terminal and run `meshcode run backend`").
181
+
182
+ ---
183
+
161
184
  ## Editor support
162
185
 
163
186
  | Editor | Auto-detected? | Config file written |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.0.0"
7
+ version = "2.0.2"
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