meshcode 2.10.40__tar.gz → 2.10.41__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.40 → meshcode-2.10.41}/PKG-INFO +25 -3
  2. {meshcode-2.10.40 → meshcode-2.10.41}/README.md +24 -2
  3. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/__init__.py +1 -1
  4. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/comms_v4.py +184 -41
  5. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/meshcode_mcp/backend.py +45 -3
  6. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/meshcode_mcp/server.py +23 -12
  7. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/run_agent.py +3 -0
  8. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/setup_clients.py +4 -1
  9. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode.egg-info/PKG-INFO +25 -3
  10. {meshcode-2.10.40 → meshcode-2.10.41}/pyproject.toml +1 -1
  11. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/ascii_art.py +0 -0
  12. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/cli.py +0 -0
  13. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/invites.py +0 -0
  14. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/launcher.py +0 -0
  15. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/launcher_install.py +0 -0
  16. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/meshcode_mcp/__init__.py +0 -0
  17. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/meshcode_mcp/__main__.py +0 -0
  18. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/meshcode_mcp/realtime.py +0 -0
  19. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/meshcode_mcp/test_backend.py +0 -0
  20. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  21. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  22. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/preferences.py +0 -0
  23. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/protocol_v2.py +0 -0
  24. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/secrets.py +0 -0
  25. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/self_update.py +0 -0
  26. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode.egg-info/SOURCES.txt +0 -0
  27. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode.egg-info/requires.txt +0 -0
  30. {meshcode-2.10.40 → meshcode-2.10.41}/meshcode.egg-info/top_level.txt +0 -0
  31. {meshcode-2.10.40 → meshcode-2.10.41}/setup.cfg +0 -0
  32. {meshcode-2.10.40 → meshcode-2.10.41}/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.40
3
+ Version: 2.10.41
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -400,10 +400,32 @@ meshcode projects # lists all your meshworks
400
400
  **8. Unknown command (e.g., `meshcode list`)**
401
401
  Since v2.0.2, the CLI suggests the closest match instead of dumping the full help text. Common aliases: `list` and `ls` both work for `projects`.
402
402
 
403
- **5. `MCP server failed to start` in the Claude Code `/mcp` panel**
403
+ **9. Windows: agent registers but never appears online**
404
+ On Windows, especially with Python 3.14+, you may see `ImportError` crashes or agents that register but stay offline. This was fixed in v2.10.40. Upgrade first:
405
+ ```bash
406
+ pip install meshcode --upgrade
407
+ ```
408
+ If the agent still fails after upgrading, check that your `.mcp.json` command path is valid. On Windows, the MCP config should use `python` (not `python3`, which doesn't exist on Windows):
409
+ ```json
410
+ {
411
+ "command": "python",
412
+ "args": ["-m", "meshcode_mcp", "serve"]
413
+ }
414
+ ```
415
+ If `meshcode setup` was run from a conda/venv environment that no longer exists, the baked path in `.mcp.json` will be stale. Re-run `meshcode setup <project> <agent>` from your current Python environment to fix it.
416
+
417
+ As a fast-path workaround, you can set `MESHCODE_PROJECT_ID` as an environment variable to bypass the project name lookup at boot (which can fail on Windows due to RLS visibility):
418
+ ```bash
419
+ set MESHCODE_PROJECT_ID=your-project-uuid # Windows cmd
420
+ $env:MESHCODE_PROJECT_ID="your-project-uuid" # Windows PowerShell
421
+ ```
422
+
423
+ You can also run `meshcode doctor` (v2.10.41+) to diagnose stale paths, missing dependencies, and config issues across all your workspaces.
424
+
425
+ **10. `MCP server failed to start` in the Claude Code `/mcp` panel**
404
426
  Run `claude --debug` to see the underlying error. Nine times out of ten it's a stale or missing key — run `meshcode login mc_xxx` again.
405
427
 
406
- **6. PyPI says `Could not find version 1.x.x` immediately after a publish**
428
+ **11. PyPI says `Could not find version 1.x.x` immediately after a publish**
407
429
  CDN propagation. Wait ~60s and force-fetch directly from the origin:
408
430
  ```bash
409
431
  pip install --no-cache-dir -i https://pypi.org/simple/ meshcode
@@ -374,10 +374,32 @@ meshcode projects # lists all your meshworks
374
374
  **8. Unknown command (e.g., `meshcode list`)**
375
375
  Since v2.0.2, the CLI suggests the closest match instead of dumping the full help text. Common aliases: `list` and `ls` both work for `projects`.
376
376
 
377
- **5. `MCP server failed to start` in the Claude Code `/mcp` panel**
377
+ **9. Windows: agent registers but never appears online**
378
+ On Windows, especially with Python 3.14+, you may see `ImportError` crashes or agents that register but stay offline. This was fixed in v2.10.40. Upgrade first:
379
+ ```bash
380
+ pip install meshcode --upgrade
381
+ ```
382
+ If the agent still fails after upgrading, check that your `.mcp.json` command path is valid. On Windows, the MCP config should use `python` (not `python3`, which doesn't exist on Windows):
383
+ ```json
384
+ {
385
+ "command": "python",
386
+ "args": ["-m", "meshcode_mcp", "serve"]
387
+ }
388
+ ```
389
+ If `meshcode setup` was run from a conda/venv environment that no longer exists, the baked path in `.mcp.json` will be stale. Re-run `meshcode setup <project> <agent>` from your current Python environment to fix it.
390
+
391
+ As a fast-path workaround, you can set `MESHCODE_PROJECT_ID` as an environment variable to bypass the project name lookup at boot (which can fail on Windows due to RLS visibility):
392
+ ```bash
393
+ set MESHCODE_PROJECT_ID=your-project-uuid # Windows cmd
394
+ $env:MESHCODE_PROJECT_ID="your-project-uuid" # Windows PowerShell
395
+ ```
396
+
397
+ You can also run `meshcode doctor` (v2.10.41+) to diagnose stale paths, missing dependencies, and config issues across all your workspaces.
398
+
399
+ **10. `MCP server failed to start` in the Claude Code `/mcp` panel**
378
400
  Run `claude --debug` to see the underlying error. Nine times out of ten it's a stale or missing key — run `meshcode login mc_xxx` again.
379
401
 
380
- **6. PyPI says `Could not find version 1.x.x` immediately after a publish**
402
+ **11. PyPI says `Could not find version 1.x.x` immediately after a publish**
381
403
  CDN propagation. Wait ~60s and force-fetch directly from the origin:
382
404
  ```bash
383
405
  pip install --no-cache-dir -i https://pypi.org/simple/ meshcode
@@ -1,2 +1,2 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.10.40"
2
+ __version__ = "2.10.41"
@@ -343,9 +343,26 @@ def _is_pid_alive(pid):
343
343
  if not pid:
344
344
  return False
345
345
  try:
346
- os.kill(int(pid), 0)
346
+ pid = int(pid)
347
+ except (ValueError, TypeError):
348
+ return False
349
+ if sys.platform == "win32":
350
+ # os.kill(pid, 0) doesn't work on Windows — use kernel32 OpenProcess
351
+ try:
352
+ import ctypes
353
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
354
+ handle = ctypes.windll.kernel32.OpenProcess(
355
+ PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
356
+ if handle:
357
+ ctypes.windll.kernel32.CloseHandle(handle)
358
+ return True
359
+ return False
360
+ except Exception:
361
+ return False
362
+ try:
363
+ os.kill(pid, 0)
347
364
  return True
348
- except (OSError, ValueError, TypeError):
365
+ except OSError:
349
366
  return False
350
367
 
351
368
 
@@ -533,7 +550,7 @@ def spawn_headless_agent(project, name, project_id, message_body, from_agent):
533
550
  f"{message_body}\n\n"
534
551
  f"Recent mesh activity:\n{history_text or '(none)'}\n\n"
535
552
  "Process the message. If you need to reply, use this exact bash command (one line):\n"
536
- f" SUPABASE_URL=\"$SUPABASE_URL\" SUPABASE_KEY=\"$SUPABASE_KEY\" python3 ~/Desktop/meshcode/comms_v4.py send {project} {name}:{from_agent} '<json_response>'\n\n"
553
+ f" {sys.executable} -m meshcode send {project} {name}:{from_agent} '<json_response>'\n\n"
537
554
  "Then exit. Do not start a long conversation."
538
555
  )
539
556
 
@@ -579,7 +596,8 @@ def spawn_headless_agent(project, name, project_id, message_body, from_agent):
579
596
  claude_bin = _find_claude()
580
597
  # Augment PATH so any subprocess the agent itself spawns (git, npm,
581
598
  # python3, supabase CLI, etc.) can find common tools.
582
- env["PATH"] = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:" + env.get("PATH", "")
599
+ if sys.platform != "win32":
600
+ env["PATH"] = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:" + env.get("PATH", "")
583
601
  result = subprocess.run(
584
602
  [claude_bin, "--print", "--dangerously-skip-permissions",
585
603
  "--append-system-prompt", system_prompt, message_body or ""],
@@ -959,12 +977,34 @@ def register(project, name, role=""):
959
977
  log_msg(f"{name} joined {project} (tty={tty}, backend=supabase)")
960
978
 
961
979
 
980
+ def _resolve_agent_name(project_id, name):
981
+ """Resolve partial agent name to full registered name."""
982
+ if not name or name == "*":
983
+ return name
984
+ agents = sb_select("mc_agents", f"project_id=eq.{project_id}", limit=50)
985
+ if not agents:
986
+ return name
987
+ all_names = [a["name"] for a in agents]
988
+ if name in all_names:
989
+ return name
990
+ prefix = [n for n in all_names if n.startswith(name)]
991
+ if len(prefix) == 1:
992
+ return prefix[0]
993
+ sub = [n for n in all_names if name in n]
994
+ if len(sub) == 1:
995
+ return sub[0]
996
+ return name
997
+
998
+
962
999
  def send_msg(project, from_agent, to_agent, content, msg_type="msg", compact=False):
963
1000
  project_id = get_project_id(project)
964
1001
  if not project_id:
965
1002
  print(f"[ERROR] Proyecto '{project}' no encontrado")
966
1003
  return
967
1004
 
1005
+ # Resolve partial agent names
1006
+ to_agent = _resolve_agent_name(project_id, to_agent)
1007
+
968
1008
  payload = {}
969
1009
  try:
970
1010
  payload = json.loads(content)
@@ -1026,7 +1066,7 @@ def broadcast(project, from_agent, content, msg_type="broadcast"):
1026
1066
  print(f"[{project}] broadcast from {from_agent} -> {count} agents")
1027
1067
 
1028
1068
 
1029
- def read_messages(project, name, silent=False):
1069
+ def read_messages(project, name, silent=False, send_acks=True):
1030
1070
  project_id = get_project_id(project)
1031
1071
  if not project_id:
1032
1072
  if not silent:
@@ -1064,21 +1104,68 @@ def read_messages(project, name, silent=False):
1064
1104
  ack_targets.add(sender)
1065
1105
  print("---")
1066
1106
 
1067
- # Send automatic ACKs
1068
- for sender in ack_targets:
1069
- ack_payload = {"text": f"{name} read your message"}
1070
- sb_insert("mc_messages", {
1071
- "project_id": project_id,
1072
- "from_agent": name,
1073
- "to_agent": sender,
1074
- "type": "ack",
1075
- "payload": ack_payload,
1076
- "read": False
1077
- })
1107
+ # Send automatic ACKs (skip when called from watch to avoid ack spam)
1108
+ if send_acks:
1109
+ for sender in ack_targets:
1110
+ ack_payload = {"text": f"{name} read your message"}
1111
+ sb_insert("mc_messages", {
1112
+ "project_id": project_id,
1113
+ "from_agent": name,
1114
+ "to_agent": sender,
1115
+ "type": "ack",
1116
+ "payload": ack_payload,
1117
+ "read": True
1118
+ })
1078
1119
 
1079
1120
  return messages
1080
1121
 
1081
1122
 
1123
+ def inbox(project, name):
1124
+ """All-in-one: show agent status, pending count, tasks, and read new messages."""
1125
+ project_id = get_project_id(project)
1126
+ if not project_id:
1127
+ print(f"[meshcode] Project '{project}' not found")
1128
+ return
1129
+
1130
+ # Agent status
1131
+ agent_rows = sb_select("mc_agents",
1132
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}", limit=1)
1133
+ if agent_rows:
1134
+ a = agent_rows[0]
1135
+ print(f"[{project}] {name} | status: {a.get('status', '?')} | task: {a.get('task', '-')}")
1136
+ else:
1137
+ print(f"[{project}] {name} (not registered)")
1138
+
1139
+ # Pending messages
1140
+ pending = sb_select("mc_messages",
1141
+ f"project_id=eq.{project_id}&to_agent=eq.{quote(name)}&read=eq.false&type=neq.ack",
1142
+ order="created_at.asc")
1143
+ if pending:
1144
+ print(f"\n {len(pending)} unread message(s):")
1145
+ for msg in pending:
1146
+ sender = msg["from_agent"]
1147
+ ts = msg.get("created_at", "")
1148
+ if "T" in ts:
1149
+ ts = ts.replace("T", " ")[:19]
1150
+ payload = msg.get("payload", {})
1151
+ preview = json.dumps(payload, ensure_ascii=False)[:100]
1152
+ print(f" [{ts}] {sender}: {preview}")
1153
+ # Ask if they want to read (mark as read)
1154
+ print(f"\n Run `meshcode read {project} {name}` to read and mark as read.")
1155
+ else:
1156
+ print(" No unread messages.")
1157
+
1158
+ # Open tasks assigned to this agent
1159
+ tasks = sb_select("mc_tasks",
1160
+ f"project_id=eq.{project_id}&assignee=eq.{quote(name)}&status=neq.done&status=neq.canceled",
1161
+ order="created_at.desc", limit=5)
1162
+ if tasks:
1163
+ print(f"\n {len(tasks)} open task(s):")
1164
+ for t in tasks:
1165
+ print(f" [{t.get('priority', '?')}] {t.get('title', '?')} ({t.get('status', '?')})")
1166
+ print()
1167
+
1168
+
1082
1169
  def hook_check():
1083
1170
  """Called by PostToolUse hook. Auto-detects project+agent, prints pending.
1084
1171
  Also sends heartbeat so dashboard always shows fresh status."""
@@ -1127,7 +1214,10 @@ def watch(project, name, interval=10, timeout=0):
1127
1214
  f"project_id=eq.{project_id}&name=eq.{quote(name)}",
1128
1215
  {"status": "online", "task": "Listening to user", "last_heartbeat": now_iso()})
1129
1216
  sys.exit(0)
1130
- signal.signal(signal.SIGINT, handle_interrupt)
1217
+ try:
1218
+ signal.signal(signal.SIGINT, handle_interrupt)
1219
+ except ValueError:
1220
+ pass # Non-main thread (Windows restriction)
1131
1221
 
1132
1222
  # Update status to standby
1133
1223
  sb_update("mc_agents",
@@ -1148,13 +1238,18 @@ def watch(project, name, interval=10, timeout=0):
1148
1238
  if pending:
1149
1239
  elapsed = int(time.time() - start)
1150
1240
  print(f"\n*** [{project.upper()}] MENSAJE RECIBIDO ({elapsed}s en standby) ***")
1151
- messages = read_messages(project, name, silent=False)
1241
+ read_messages(project, name, silent=False, send_acks=False)
1152
1242
 
1153
1243
  sb_update("mc_agents",
1154
1244
  f"project_id=eq.{project_id}&name=eq.{quote(name)}",
1155
1245
  {"status": "working", "task": "Procesando mensaje recibido", "last_heartbeat": now_iso()})
1156
1246
 
1157
- return messages
1247
+ # Reset to standby and keep polling (don't exit the loop)
1248
+ time.sleep(2)
1249
+ sb_update("mc_agents",
1250
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}",
1251
+ {"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
1252
+ continue
1158
1253
 
1159
1254
  # Heartbeat
1160
1255
  sb_rpc("mc_heartbeat", {"p_project_id": project_id, "p_agent_name": name})
@@ -1181,7 +1276,7 @@ def update_board(project, name, status, task=""):
1181
1276
  count = len(sb_select("mc_messages",
1182
1277
  f"project_id=eq.{project_id}&to_agent=eq.{quote(name)}&read=eq.false&type=neq.ack"))
1183
1278
  print(f"[{project}] {name}: NO puedes estar idle — tienes {count} mensaje(s) pendientes.")
1184
- print(f"[{project}] Ejecuta: python3 ~/Desktop/meshcode/comms_v4.py read {project} {name}")
1279
+ print(f"[{project}] Ejecuta: {sys.executable} -m meshcode read {project} {name}")
1185
1280
  status = "working"
1186
1281
  task = f"{count} pending messages"
1187
1282
 
@@ -1215,13 +1310,21 @@ def show_board(project):
1215
1310
 
1216
1311
 
1217
1312
  def show_status(project=None):
1218
- if project:
1313
+ # Try API-key-authenticated RPC first (RLS may block anon SELECT)
1314
+ api_key = _load_api_key_for_cli()
1315
+ if api_key and not project:
1316
+ rpc_data = sb_rpc("mc_list_user_projects", {"p_api_key": api_key})
1317
+ if isinstance(rpc_data, dict) and not rpc_data.get("error"):
1318
+ projects_data = rpc_data.get("projects", [])
1319
+ else:
1320
+ projects_data = sb_select("mc_projects", "", order="created_at.asc")
1321
+ elif project:
1219
1322
  projects_data = sb_select("mc_projects", f"name=eq.{quote(project)}")
1220
1323
  else:
1221
1324
  projects_data = sb_select("mc_projects", "", order="created_at.asc")
1222
1325
 
1223
1326
  if not projects_data:
1224
- print("[COMMS] No active projects")
1327
+ print("[meshcode] No projects found. Try `meshcode projects` or `meshcode login <api_key>` first.")
1225
1328
  return
1226
1329
 
1227
1330
  print(f"\n{'='*60}")
@@ -1398,7 +1501,7 @@ def _start_heartbeat_daemon(project, name):
1398
1501
  if pid_file.exists():
1399
1502
  old = int(pid_file.read_text(encoding="utf-8").strip())
1400
1503
  try:
1401
- os.kill(old, 15)
1504
+ _terminate_pid(old)
1402
1505
  except Exception:
1403
1506
  pass
1404
1507
  except Exception:
@@ -1450,12 +1553,21 @@ def _start_heartbeat_daemon(project, name):
1450
1553
  return proc.pid
1451
1554
 
1452
1555
 
1556
+ def _terminate_pid(pid):
1557
+ """Terminate a process by PID, cross-platform."""
1558
+ if sys.platform == "win32":
1559
+ subprocess.run(["taskkill", "/F", "/PID", str(pid)],
1560
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
1561
+ else:
1562
+ os.kill(pid, 15)
1563
+
1564
+
1453
1565
  def _stop_heartbeat_daemon(project, name):
1454
1566
  pid_file = _heartbeat_loop_pidfile(project, name)
1455
1567
  if pid_file.exists():
1456
1568
  try:
1457
1569
  pid = int(pid_file.read_text(encoding="utf-8").strip())
1458
- os.kill(pid, 15)
1570
+ _terminate_pid(pid)
1459
1571
  except Exception:
1460
1572
  pass
1461
1573
  try:
@@ -1603,19 +1715,11 @@ def connect(project, name, hook_target="claude", role=""):
1603
1715
  _setup_ws(project, name, actual_role)
1604
1716
 
1605
1717
  elif hook_target == "codex":
1606
- config_path = Path.cwd() / ".meshcode.json"
1607
- config = {
1608
- "project": project,
1609
- "agent": name,
1610
- "supabase_url": SUPABASE_URL,
1611
- "check_command": f"python3 {comms_path} check",
1612
- "read_command": f"python3 {comms_path} read {project} {name}",
1613
- "send_command": f"python3 {comms_path} send {project} {name}:{{to}} '{{message}}'",
1614
- "platform": os_name
1615
- }
1616
- config_path.write_text(json.dumps(config, indent=2), encoding="utf-8")
1617
- print(f"[meshcode] Config saved to {config_path}")
1618
- print(f"[meshcode] Codex: use read_command and send_command from the config")
1718
+ # Codex reads ~/.codex/config.toml globally — workspace .mcp.json is ignored.
1719
+ # Use setup_clients.setup_global to write the proper TOML config.
1720
+ import importlib
1721
+ _setup_global = importlib.import_module("meshcode.setup_clients").setup_global
1722
+ _setup_global("codex", project, name, actual_role)
1619
1723
 
1620
1724
  else:
1621
1725
  print(f"[meshcode] Unknown hook target: {hook_target}. Use 'claude' or 'codex'.")
@@ -1629,9 +1733,10 @@ def connect(project, name, hook_target="claude", role=""):
1629
1733
  print(f" 3. Start using tools: meshcode_send, meshcode_read, etc.")
1630
1734
  print(f" No extra terminal. No watch loops. No AppleScript.")
1631
1735
  else:
1632
- print(f" Read: python3 {comms_path} read {project} {name}")
1633
- print(f" Send: python3 {comms_path} send {project} {name}:<target> '<message>'")
1634
- print(f" Board: python3 {comms_path} board {project}")
1736
+ _py = sys.executable
1737
+ print(f" Read: {_py} {comms_path} read {project} {name}")
1738
+ print(f" Send: {_py} {comms_path} send {project} {name}:<target> \"<message>\"")
1739
+ print(f" Board: {_py} {comms_path} board {project}")
1635
1740
  print()
1636
1741
 
1637
1742
 
@@ -1840,6 +1945,15 @@ Show conversation history. Optionally filter to messages between two agents.
1840
1945
  EXAMPLES:
1841
1946
  meshcode history my-app 50
1842
1947
  meshcode history my-app 20 backend,frontend
1948
+ """,
1949
+ "inbox": """meshcode inbox <project> <name>
1950
+
1951
+ All-in-one overview: agent status, unread messages, and open tasks.
1952
+ Alias: meshcode next
1953
+
1954
+ EXAMPLES:
1955
+ meshcode inbox my-app backend
1956
+ meshcode next my-app frontend
1843
1957
  """,
1844
1958
  "projects": """meshcode projects
1845
1959
 
@@ -2050,6 +2164,7 @@ if __name__ == "__main__":
2050
2164
  # Supports both:
2051
2165
  # send <project> <from>:<to> <message>
2052
2166
  # send --project X --from Y --to Z --msg "text" [--type T]
2167
+ # echo '{"key":"val"}' | meshcode send <project> <from>:<to> -
2053
2168
  compact = bool(flags.get("compact"))
2054
2169
  if "from" in flags and "to" in flags:
2055
2170
  proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
@@ -2062,6 +2177,9 @@ if __name__ == "__main__":
2062
2177
  proj = pos[0] if len(pos) > 0 else "default"
2063
2178
  target = pos[1] if len(pos) > 1 else ""
2064
2179
  message = " ".join(pos[2:]) if len(pos) > 2 else ""
2180
+ # Support stdin: `meshcode send proj from:to -` reads from pipe
2181
+ if message.strip() == "-" and not sys.stdin.isatty():
2182
+ message = sys.stdin.read().strip()
2065
2183
  if ":" in target:
2066
2184
  from_a, to_a = target.split(":", 1)
2067
2185
  else:
@@ -2092,6 +2210,11 @@ if __name__ == "__main__":
2092
2210
  name = flags.get("name", pos[1] if len(pos) > 1 else "agent")
2093
2211
  read_messages(proj, name)
2094
2212
 
2213
+ elif cmd in ("inbox", "next"):
2214
+ proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
2215
+ name = flags.get("name", pos[1] if len(pos) > 1 else "agent")
2216
+ inbox(proj, name)
2217
+
2095
2218
  elif cmd == "check":
2096
2219
  hook_check()
2097
2220
 
@@ -2427,6 +2550,7 @@ if __name__ == "__main__":
2427
2550
 
2428
2551
  elif cmd == "whoami":
2429
2552
  # Show the logged-in user identity from profile_meta.json
2553
+ verbose = "--verbose" in sys.argv or "-v" in sys.argv
2430
2554
  meta_path = Path.home() / ".meshcode" / "profile_meta.json"
2431
2555
  if meta_path.exists():
2432
2556
  try:
@@ -2441,6 +2565,25 @@ if __name__ == "__main__":
2441
2565
  print(f" user ID: {meta['user_id']}")
2442
2566
  active = os.environ.get("MESHCODE_KEYCHAIN_PROFILE") or "default"
2443
2567
  print(f" profile: {active}")
2568
+
2569
+ if verbose:
2570
+ # Show projects, agents, plan, API key status
2571
+ api_key = _load_api_key_for_cli()
2572
+ if api_key:
2573
+ print(f" api key: {api_key[:8]}...{api_key[-4:]}")
2574
+ # List projects
2575
+ result = sb_rpc("mc_list_user_projects", {"p_api_key": api_key})
2576
+ if isinstance(result, dict) and result.get("ok"):
2577
+ projects = result.get("projects", [])
2578
+ print(f"\n Projects ({len(projects)}):")
2579
+ for p in projects:
2580
+ agent_count = p.get("agent_count", "?")
2581
+ plan = p.get("plan", "free")
2582
+ print(f" {p.get('name', '?'):20s} plan={plan:12s} agents={agent_count}")
2583
+ else:
2584
+ print(f"\n Projects: (could not fetch)")
2585
+ else:
2586
+ print(f" api key: not found")
2444
2587
  print()
2445
2588
  except Exception:
2446
2589
  print("[meshcode] Could not read profile. Run `meshcode login <api_key>`.")
@@ -2558,7 +2701,7 @@ if __name__ == "__main__":
2558
2701
 
2559
2702
  else:
2560
2703
  known_cmds = [
2561
- "register", "send", "broadcast", "read", "check", "watch",
2704
+ "register", "send", "broadcast", "read", "check", "watch", "inbox", "next",
2562
2705
  "board", "update", "status", "projects", "list", "ls",
2563
2706
  "history", "clear", "unregister", "connect", "disconnect",
2564
2707
  "setup", "run", "go", "invite", "join", "invites", "members",
@@ -409,7 +409,43 @@ def register_agent(project: str, name: str, role: str = "", api_key: Optional[st
409
409
  }
410
410
 
411
411
 
412
+ def resolve_agent_name(project_id: str, name: str) -> str:
413
+ """Resolve a partial or exact agent name to the full registered name.
414
+
415
+ Returns the exact match if found, or a unique prefix/substring match.
416
+ Returns the original name unchanged if no match or ambiguous.
417
+ Broadcast target '*' is passed through unchanged.
418
+ """
419
+ if not name or name == "*":
420
+ return name
421
+ # Exact match — fast path
422
+ exact = sb_select("mc_agents",
423
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}",
424
+ limit=1)
425
+ if exact:
426
+ return name
427
+ # Fuzzy: prefix or substring match among registered agents
428
+ agents = sb_select("mc_agents", f"project_id=eq.{project_id}",
429
+ columns="name")
430
+ if not agents:
431
+ return name
432
+ all_names = [a["name"] for a in agents]
433
+ # Try prefix match first
434
+ prefix_matches = [n for n in all_names if n.startswith(name)]
435
+ if len(prefix_matches) == 1:
436
+ return prefix_matches[0]
437
+ # Try substring match
438
+ sub_matches = [n for n in all_names if name in n]
439
+ if len(sub_matches) == 1:
440
+ return sub_matches[0]
441
+ # Ambiguous or no match — return original
442
+ return name
443
+
444
+
412
445
  def send_message(project_id: str, from_agent: str, to_agent: str, payload: Any, msg_type: str = "msg", parent_msg_id: Optional[str] = None, sensitive: bool = False, api_key: Optional[str] = None, encrypted: bool = False) -> Dict:
446
+ # Normalize partial agent names to full registered names
447
+ to_agent = resolve_agent_name(project_id, to_agent)
448
+
413
449
  if not isinstance(payload, dict):
414
450
  payload = {"text": str(payload)}
415
451
 
@@ -698,16 +734,19 @@ def heartbeat(project_id: str, agent: str) -> Dict:
698
734
  return result or {}
699
735
 
700
736
 
701
- def set_status(project_id: str, agent: str, status: str, task: str = "", api_key: Optional[str] = None) -> Dict:
737
+ def set_status(project_id: str, agent: str, status: str, task: str = "", api_key: Optional[str] = None, editor: Optional[str] = None) -> Dict:
702
738
  # Use SECURITY DEFINER RPC when api_key is available (anon PATCH silently fails)
703
739
  if api_key:
704
- rpc_result = sb_rpc("mc_agent_set_status_by_api_key", {
740
+ params = {
705
741
  "p_api_key": api_key,
706
742
  "p_project_id": project_id,
707
743
  "p_agent_name": agent,
708
744
  "p_status": status,
709
745
  "p_task": task,
710
- })
746
+ }
747
+ if editor:
748
+ params["p_editor"] = editor
749
+ rpc_result = sb_rpc("mc_agent_set_status_by_api_key", params)
711
750
  if isinstance(rpc_result, dict) and rpc_result.get("ok"):
712
751
  return {"ok": True, "status": status}
713
752
  # Fall through to direct PATCH if RPC doesn't exist yet
@@ -716,6 +755,8 @@ def set_status(project_id: str, agent: str, status: str, task: str = "", api_key
716
755
  updates = {"status": status, "last_heartbeat": _now_iso()}
717
756
  if task:
718
757
  updates["task"] = task
758
+ if editor:
759
+ updates["editor"] = editor
719
760
  result = sb_update("mc_agents", f"project_id=eq.{project_id}&name=eq.{quote(agent)}", updates)
720
761
  if isinstance(result, dict) and result.get("_error"):
721
762
  return {"error": result["_error"]}
@@ -836,6 +877,7 @@ class MeshCodeBackend:
836
877
  ``from meshcode.meshcode_mcp.backend import MeshCodeBackend`` resolves.
837
878
  """
838
879
  get_project_id = staticmethod(get_project_id)
880
+ resolve_agent_name = staticmethod(resolve_agent_name)
839
881
  register_agent = staticmethod(register_agent)
840
882
  send_message = staticmethod(send_message)
841
883
  read_inbox = staticmethod(read_inbox)
@@ -373,6 +373,7 @@ log = logging.getLogger("meshcode-mcp")
373
373
  PROJECT_NAME = os.environ.get("MESHCODE_PROJECT", "")
374
374
  AGENT_NAME = os.environ.get("MESHCODE_AGENT", "")
375
375
  AGENT_ROLE = os.environ.get("MESHCODE_ROLE", "")
376
+ EDITOR_TYPE = os.environ.get("MESHCODE_EDITOR_TYPE", "")
376
377
 
377
378
  if not PROJECT_NAME or not AGENT_NAME:
378
379
  print(
@@ -488,18 +489,18 @@ except Exception:
488
489
  # Use the SECURITY DEFINER RPC (mc_agent_set_status_by_api_key) so we
489
490
  # bypass RLS — the publishable key has no JWT context and cannot UPDATE
490
491
  # mc_agents directly. The RPC validates ownership via api_key.
491
- def _flip_status(status: str, task: str = "") -> bool:
492
+ def _flip_status(status: str, task: str = "", editor: Optional[str] = None) -> bool:
492
493
  """Write status via RPC for reliable updates (anon PATCH silently fails).
493
494
 
494
495
  Falls back to direct PATCH only when no api_key is available (tests).
495
496
  """
496
497
  try:
497
- result = be.set_status(_PROJECT_ID, AGENT_NAME, status, task, api_key=_get_api_key())
498
+ result = be.set_status(_PROJECT_ID, AGENT_NAME, status, task, api_key=_get_api_key(), editor=editor)
498
499
  return isinstance(result, dict) and result.get("ok", False)
499
500
  except Exception:
500
501
  return False
501
502
 
502
- if not _flip_status("idle", ""):
503
+ if not _flip_status("idle", "", editor=EDITOR_TYPE or None):
503
504
  _mc_log(f" could not flip status to idle", "warn")
504
505
 
505
506
 
@@ -1195,8 +1196,10 @@ def _heartbeat_loop_inner():
1195
1196
  elif cur_state == "online" and idle_secs > 30:
1196
1197
  # Brief idle — show as idle, not sleeping yet
1197
1198
  _set_state("idle", "idle")
1198
- elif cur_state == "idle" and idle_secs > 300 and parent_cpu < 2.0 and not _STAY_AWAKE:
1199
- # Extended idle + no CPU activity sleeping (5 min, not 90s)
1199
+ elif cur_state == "idle" and idle_secs > 300 and parent_cpu < 2.0 and not _STAY_AWAKE and not in_wait:
1200
+ # Extended idle + no CPU activity + NOT in wait loop → sleeping
1201
+ # Never auto-sleep during meshcode_wait — the agent is actively
1202
+ # listening for messages, not truly idle.
1200
1203
  _set_state("sleeping", "sleeping")
1201
1204
 
1202
1205
  # Sync current state to DB (in case realtime missed it)
@@ -1770,12 +1773,15 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
1770
1773
  pass # All messages already seen — fall through to wait loop
1771
1774
  else:
1772
1775
  split = _split_messages(deduped)
1773
- return {
1774
- "refused": True,
1775
- "reason": f"You have {len(deduped)} unread messages. Process them before waiting.",
1776
- "got_message": True,
1777
- **split,
1778
- }
1776
+ # Only refuse for real messages — ack-only batches should not block wait
1777
+ if split["messages"] or split["done_signals"]:
1778
+ return {
1779
+ "refused": True,
1780
+ "reason": f"You have {split['count']} unread messages. Process them before waiting.",
1781
+ "got_message": True,
1782
+ **split,
1783
+ }
1784
+ # Ack-only batch — fall through to wait loop
1779
1785
  except Exception:
1780
1786
  pass
1781
1787
 
@@ -3123,7 +3129,12 @@ def _auto_update() -> None:
3123
3129
  print(f"[meshcode] Updated to {latest}, restarting...", file=sys.stderr)
3124
3130
  os.environ["MESHCODE_UPDATED"] = "1"
3125
3131
  try:
3126
- os.execv(sys.executable, [sys.executable] + sys.argv)
3132
+ if sys.platform == "win32":
3133
+ import subprocess as _sp_reexec
3134
+ _sp_reexec.Popen([sys.executable] + sys.argv)
3135
+ sys.exit(0)
3136
+ else:
3137
+ os.execv(sys.executable, [sys.executable] + sys.argv)
3127
3138
  except Exception as e:
3128
3139
  log.debug(f"[meshcode] Re-exec failed: {e}, continuing with old version")
3129
3140
 
@@ -531,6 +531,9 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
531
531
  print(f"[meshcode] Closing the editor will flip this agent offline.")
532
532
  print()
533
533
 
534
+ # Set editor type so MCP server can report it to the dashboard
535
+ os.environ["MESHCODE_EDITOR_TYPE"] = editor
536
+
534
537
  if editor == "claude":
535
538
  # Claude Code: run from inside the workspace and let Claude Code
536
539
  # auto-discover .mcp.json as a PROJECT-scoped MCP. We used to pass
@@ -158,7 +158,8 @@ def _suggest_projects(api_key: str, sb: dict):
158
158
 
159
159
  def _build_server_block(project: str, project_id: str, agent: str, role: str,
160
160
  api_key: str, sb: Dict[str, str],
161
- keychain_profile: str = "default") -> Dict[str, Any]:
161
+ keychain_profile: str = "default",
162
+ editor_type: str = "") -> Dict[str, Any]:
162
163
  """Build the MCP server config block for an agent.
163
164
 
164
165
  1.4.1+ does NOT bake the api key into the env. Instead it bakes a
@@ -184,6 +185,8 @@ def _build_server_block(project: str, project_id: str, agent: str, role: str,
184
185
  # parts of the package (e.g. self_update) skip auto-update inside it.
185
186
  "MESHCODE_MCP_SERVE": "1",
186
187
  }
188
+ if editor_type:
189
+ env["MESHCODE_EDITOR_TYPE"] = editor_type
187
190
  # Belt-and-suspenders fallback: if the OS doesn't have a keychain backend
188
191
  # the MCP server can't read the key from there, so we still need to pass
189
192
  # it via env. Probe and only include it as a fallback path.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.40
3
+ Version: 2.10.41
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -400,10 +400,32 @@ meshcode projects # lists all your meshworks
400
400
  **8. Unknown command (e.g., `meshcode list`)**
401
401
  Since v2.0.2, the CLI suggests the closest match instead of dumping the full help text. Common aliases: `list` and `ls` both work for `projects`.
402
402
 
403
- **5. `MCP server failed to start` in the Claude Code `/mcp` panel**
403
+ **9. Windows: agent registers but never appears online**
404
+ On Windows, especially with Python 3.14+, you may see `ImportError` crashes or agents that register but stay offline. This was fixed in v2.10.40. Upgrade first:
405
+ ```bash
406
+ pip install meshcode --upgrade
407
+ ```
408
+ If the agent still fails after upgrading, check that your `.mcp.json` command path is valid. On Windows, the MCP config should use `python` (not `python3`, which doesn't exist on Windows):
409
+ ```json
410
+ {
411
+ "command": "python",
412
+ "args": ["-m", "meshcode_mcp", "serve"]
413
+ }
414
+ ```
415
+ If `meshcode setup` was run from a conda/venv environment that no longer exists, the baked path in `.mcp.json` will be stale. Re-run `meshcode setup <project> <agent>` from your current Python environment to fix it.
416
+
417
+ As a fast-path workaround, you can set `MESHCODE_PROJECT_ID` as an environment variable to bypass the project name lookup at boot (which can fail on Windows due to RLS visibility):
418
+ ```bash
419
+ set MESHCODE_PROJECT_ID=your-project-uuid # Windows cmd
420
+ $env:MESHCODE_PROJECT_ID="your-project-uuid" # Windows PowerShell
421
+ ```
422
+
423
+ You can also run `meshcode doctor` (v2.10.41+) to diagnose stale paths, missing dependencies, and config issues across all your workspaces.
424
+
425
+ **10. `MCP server failed to start` in the Claude Code `/mcp` panel**
404
426
  Run `claude --debug` to see the underlying error. Nine times out of ten it's a stale or missing key — run `meshcode login mc_xxx` again.
405
427
 
406
- **6. PyPI says `Could not find version 1.x.x` immediately after a publish**
428
+ **11. PyPI says `Could not find version 1.x.x` immediately after a publish**
407
429
  CDN propagation. Wait ~60s and force-fetch directly from the origin:
408
430
  ```bash
409
431
  pip install --no-cache-dir -i https://pypi.org/simple/ meshcode
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.10.40"
7
+ version = "2.10.41"
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