meshcode 2.10.40__tar.gz → 2.10.42__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.42}/PKG-INFO +25 -3
  2. {meshcode-2.10.40 → meshcode-2.10.42}/README.md +24 -2
  3. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/__init__.py +1 -1
  4. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/comms_v4.py +210 -53
  5. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/meshcode_mcp/backend.py +187 -21
  6. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/meshcode_mcp/server.py +141 -37
  7. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/preferences.py +22 -5
  8. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/run_agent.py +162 -12
  9. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/setup_clients.py +4 -1
  10. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode.egg-info/PKG-INFO +25 -3
  11. {meshcode-2.10.40 → meshcode-2.10.42}/pyproject.toml +1 -1
  12. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/ascii_art.py +0 -0
  13. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/cli.py +0 -0
  14. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/invites.py +0 -0
  15. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/launcher.py +0 -0
  16. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/launcher_install.py +0 -0
  17. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/meshcode_mcp/__init__.py +0 -0
  18. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/meshcode_mcp/__main__.py +0 -0
  19. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/meshcode_mcp/realtime.py +0 -0
  20. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/meshcode_mcp/test_backend.py +0 -0
  21. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  22. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  23. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/protocol_v2.py +0 -0
  24. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/secrets.py +0 -0
  25. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode/self_update.py +0 -0
  26. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode.egg-info/SOURCES.txt +0 -0
  27. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode.egg-info/requires.txt +0 -0
  30. {meshcode-2.10.40 → meshcode-2.10.42}/meshcode.egg-info/top_level.txt +0 -0
  31. {meshcode-2.10.40 → meshcode-2.10.42}/setup.cfg +0 -0
  32. {meshcode-2.10.40 → meshcode-2.10.42}/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.42
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.42"
@@ -74,7 +74,11 @@ SUPABASE_KEY = os.environ.get("SUPABASE_KEY") or _env_file.get("SUPABASE_KEY") o
74
74
  SCHEMA = "meshcode"
75
75
 
76
76
  # Local paths for session/TTY tracking (still needed for nudge)
77
- COMMS_DIR = Path(__file__).parent
77
+ # NEVER use Path(__file__).parent — writing into the package directory
78
+ # creates residual files that turn site-packages/meshcode/ into a
79
+ # namespace package, breaking editable installs and __version__ imports.
80
+ COMMS_DIR = Path.home() / ".meshcode"
81
+ COMMS_DIR.mkdir(parents=True, exist_ok=True)
78
82
  SESSIONS_DIR = COMMS_DIR / "sessions"
79
83
  LOG_FILE = COMMS_DIR / "comms.log"
80
84
 
@@ -310,29 +314,39 @@ def mark_nudged(project, name):
310
314
  nf.write_text(str(time.time()), encoding="utf-8")
311
315
 
312
316
 
317
+ def _sanitize_notification_text(text: str) -> str:
318
+ """Strip characters that could escape shell/PS quoting contexts."""
319
+ # Remove quotes, backticks, dollar signs, semicolons, pipes, and backslashes
320
+ # to prevent injection in osascript, PowerShell, and notify-send.
321
+ return "".join(c for c in text if c not in '"\'`$;|\\<>(){}')
322
+
323
+
313
324
  def send_notification(project, name, from_agent, pending=1):
314
325
  """Cross-platform notification: macOS (osascript), Windows (PowerShell toast), Linux (notify-send)."""
315
- title = f"MeshCode [{project}] → {name}"
316
- body = f"{pending} mensaje(s) pendiente(s) de {from_agent}"
326
+ title = _sanitize_notification_text(f"MeshCode [{project}] → {name}")
327
+ body = _sanitize_notification_text(f"{pending} mensaje(s) pendiente(s) de {from_agent}")
317
328
  try:
318
329
  if sys.platform == "darwin":
330
+ # osascript: pass as -e argument, quotes are stripped by sanitizer
319
331
  subprocess.run(['osascript', '-e',
320
332
  f'display notification "{body}" with title "{title}" sound name "Ping"'],
321
333
  capture_output=True, timeout=3)
322
334
  elif sys.platform == "win32":
335
+ # PowerShell: use single-quoted strings (no variable expansion)
336
+ # and pass title/body via -replace to avoid any interpolation
323
337
  ps_script = (
324
- f'[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null; '
325
- f'$xml = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent(0); '
326
- f'$text = $xml.GetElementsByTagName("text"); '
327
- f'$text[0].AppendChild($xml.CreateTextNode("{title}")); '
328
- f'$text[1].AppendChild($xml.CreateTextNode("{body}")); '
329
- f'$toast = [Windows.UI.Notifications.ToastNotification]::new($xml); '
330
- f'[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("MeshCode").Show($toast)'
338
+ '[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null; '
339
+ '$xml = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent(0); '
340
+ '$text = $xml.GetElementsByTagName("text"); '
341
+ f"$text[0].AppendChild($xml.CreateTextNode('{title}')); "
342
+ f"$text[1].AppendChild($xml.CreateTextNode('{body}')); "
343
+ '$toast = [Windows.UI.Notifications.ToastNotification]::new($xml); '
344
+ '[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("MeshCode").Show($toast)'
331
345
  )
332
- subprocess.run(['powershell', '-Command', ps_script],
346
+ subprocess.run(['powershell', '-NoProfile', '-Command', ps_script],
333
347
  capture_output=True, timeout=5)
334
348
  else:
335
- # Linux
349
+ # Linux: notify-send takes title and body as separate args (no shell)
336
350
  subprocess.run(['notify-send', title, body], capture_output=True, timeout=3)
337
351
  except (subprocess.TimeoutExpired, subprocess.SubprocessError, OSError):
338
352
  pass
@@ -343,9 +357,26 @@ def _is_pid_alive(pid):
343
357
  if not pid:
344
358
  return False
345
359
  try:
346
- os.kill(int(pid), 0)
360
+ pid = int(pid)
361
+ except (ValueError, TypeError):
362
+ return False
363
+ if sys.platform == "win32":
364
+ # os.kill(pid, 0) doesn't work on Windows — use kernel32 OpenProcess
365
+ try:
366
+ import ctypes
367
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
368
+ handle = ctypes.windll.kernel32.OpenProcess(
369
+ PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
370
+ if handle:
371
+ ctypes.windll.kernel32.CloseHandle(handle)
372
+ return True
373
+ return False
374
+ except Exception:
375
+ return False
376
+ try:
377
+ os.kill(pid, 0)
347
378
  return True
348
- except (OSError, ValueError, TypeError):
379
+ except OSError:
349
380
  return False
350
381
 
351
382
 
@@ -533,7 +564,7 @@ def spawn_headless_agent(project, name, project_id, message_body, from_agent):
533
564
  f"{message_body}\n\n"
534
565
  f"Recent mesh activity:\n{history_text or '(none)'}\n\n"
535
566
  "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"
567
+ f" {sys.executable} -m meshcode send {project} {name}:{from_agent} '<json_response>'\n\n"
537
568
  "Then exit. Do not start a long conversation."
538
569
  )
539
570
 
@@ -579,7 +610,8 @@ def spawn_headless_agent(project, name, project_id, message_body, from_agent):
579
610
  claude_bin = _find_claude()
580
611
  # Augment PATH so any subprocess the agent itself spawns (git, npm,
581
612
  # 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", "")
613
+ if sys.platform != "win32":
614
+ env["PATH"] = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:" + env.get("PATH", "")
583
615
  result = subprocess.run(
584
616
  [claude_bin, "--print", "--dangerously-skip-permissions",
585
617
  "--append-system-prompt", system_prompt, message_body or ""],
@@ -959,12 +991,34 @@ def register(project, name, role=""):
959
991
  log_msg(f"{name} joined {project} (tty={tty}, backend=supabase)")
960
992
 
961
993
 
994
+ def _resolve_agent_name(project_id, name):
995
+ """Resolve partial agent name to full registered name."""
996
+ if not name or name == "*":
997
+ return name
998
+ agents = sb_select("mc_agents", f"project_id=eq.{project_id}", limit=50)
999
+ if not agents:
1000
+ return name
1001
+ all_names = [a["name"] for a in agents]
1002
+ if name in all_names:
1003
+ return name
1004
+ prefix = [n for n in all_names if n.startswith(name)]
1005
+ if len(prefix) == 1:
1006
+ return prefix[0]
1007
+ sub = [n for n in all_names if name in n]
1008
+ if len(sub) == 1:
1009
+ return sub[0]
1010
+ return name
1011
+
1012
+
962
1013
  def send_msg(project, from_agent, to_agent, content, msg_type="msg", compact=False):
963
1014
  project_id = get_project_id(project)
964
1015
  if not project_id:
965
1016
  print(f"[ERROR] Proyecto '{project}' no encontrado")
966
1017
  return
967
1018
 
1019
+ # Resolve partial agent names
1020
+ to_agent = _resolve_agent_name(project_id, to_agent)
1021
+
968
1022
  payload = {}
969
1023
  try:
970
1024
  payload = json.loads(content)
@@ -1026,7 +1080,7 @@ def broadcast(project, from_agent, content, msg_type="broadcast"):
1026
1080
  print(f"[{project}] broadcast from {from_agent} -> {count} agents")
1027
1081
 
1028
1082
 
1029
- def read_messages(project, name, silent=False):
1083
+ def read_messages(project, name, silent=False, send_acks=True):
1030
1084
  project_id = get_project_id(project)
1031
1085
  if not project_id:
1032
1086
  if not silent:
@@ -1064,21 +1118,68 @@ def read_messages(project, name, silent=False):
1064
1118
  ack_targets.add(sender)
1065
1119
  print("---")
1066
1120
 
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
- })
1121
+ # Send automatic ACKs (skip when called from watch to avoid ack spam)
1122
+ if send_acks:
1123
+ for sender in ack_targets:
1124
+ ack_payload = {"text": f"{name} read your message"}
1125
+ sb_insert("mc_messages", {
1126
+ "project_id": project_id,
1127
+ "from_agent": name,
1128
+ "to_agent": sender,
1129
+ "type": "ack",
1130
+ "payload": ack_payload,
1131
+ "read": True
1132
+ })
1078
1133
 
1079
1134
  return messages
1080
1135
 
1081
1136
 
1137
+ def inbox(project, name):
1138
+ """All-in-one: show agent status, pending count, tasks, and read new messages."""
1139
+ project_id = get_project_id(project)
1140
+ if not project_id:
1141
+ print(f"[meshcode] Project '{project}' not found")
1142
+ return
1143
+
1144
+ # Agent status
1145
+ agent_rows = sb_select("mc_agents",
1146
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}", limit=1)
1147
+ if agent_rows:
1148
+ a = agent_rows[0]
1149
+ print(f"[{project}] {name} | status: {a.get('status', '?')} | task: {a.get('task', '-')}")
1150
+ else:
1151
+ print(f"[{project}] {name} (not registered)")
1152
+
1153
+ # Pending messages
1154
+ pending = sb_select("mc_messages",
1155
+ f"project_id=eq.{project_id}&to_agent=eq.{quote(name)}&read=eq.false&type=neq.ack",
1156
+ order="created_at.asc")
1157
+ if pending:
1158
+ print(f"\n {len(pending)} unread message(s):")
1159
+ for msg in pending:
1160
+ sender = msg["from_agent"]
1161
+ ts = msg.get("created_at", "")
1162
+ if "T" in ts:
1163
+ ts = ts.replace("T", " ")[:19]
1164
+ payload = msg.get("payload", {})
1165
+ preview = json.dumps(payload, ensure_ascii=False)[:100]
1166
+ print(f" [{ts}] {sender}: {preview}")
1167
+ # Ask if they want to read (mark as read)
1168
+ print(f"\n Run `meshcode read {project} {name}` to read and mark as read.")
1169
+ else:
1170
+ print(" No unread messages.")
1171
+
1172
+ # Open tasks assigned to this agent
1173
+ tasks = sb_select("mc_tasks",
1174
+ f"project_id=eq.{project_id}&assignee=eq.{quote(name)}&status=neq.done&status=neq.canceled",
1175
+ order="created_at.desc", limit=5)
1176
+ if tasks:
1177
+ print(f"\n {len(tasks)} open task(s):")
1178
+ for t in tasks:
1179
+ print(f" [{t.get('priority', '?')}] {t.get('title', '?')} ({t.get('status', '?')})")
1180
+ print()
1181
+
1182
+
1082
1183
  def hook_check():
1083
1184
  """Called by PostToolUse hook. Auto-detects project+agent, prints pending.
1084
1185
  Also sends heartbeat so dashboard always shows fresh status."""
@@ -1127,7 +1228,10 @@ def watch(project, name, interval=10, timeout=0):
1127
1228
  f"project_id=eq.{project_id}&name=eq.{quote(name)}",
1128
1229
  {"status": "online", "task": "Listening to user", "last_heartbeat": now_iso()})
1129
1230
  sys.exit(0)
1130
- signal.signal(signal.SIGINT, handle_interrupt)
1231
+ try:
1232
+ signal.signal(signal.SIGINT, handle_interrupt)
1233
+ except ValueError:
1234
+ pass # Non-main thread (Windows restriction)
1131
1235
 
1132
1236
  # Update status to standby
1133
1237
  sb_update("mc_agents",
@@ -1148,13 +1252,18 @@ def watch(project, name, interval=10, timeout=0):
1148
1252
  if pending:
1149
1253
  elapsed = int(time.time() - start)
1150
1254
  print(f"\n*** [{project.upper()}] MENSAJE RECIBIDO ({elapsed}s en standby) ***")
1151
- messages = read_messages(project, name, silent=False)
1255
+ read_messages(project, name, silent=False, send_acks=False)
1152
1256
 
1153
1257
  sb_update("mc_agents",
1154
1258
  f"project_id=eq.{project_id}&name=eq.{quote(name)}",
1155
1259
  {"status": "working", "task": "Procesando mensaje recibido", "last_heartbeat": now_iso()})
1156
1260
 
1157
- return messages
1261
+ # Reset to standby and keep polling (don't exit the loop)
1262
+ time.sleep(2)
1263
+ sb_update("mc_agents",
1264
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}",
1265
+ {"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
1266
+ continue
1158
1267
 
1159
1268
  # Heartbeat
1160
1269
  sb_rpc("mc_heartbeat", {"p_project_id": project_id, "p_agent_name": name})
@@ -1181,7 +1290,7 @@ def update_board(project, name, status, task=""):
1181
1290
  count = len(sb_select("mc_messages",
1182
1291
  f"project_id=eq.{project_id}&to_agent=eq.{quote(name)}&read=eq.false&type=neq.ack"))
1183
1292
  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}")
1293
+ print(f"[{project}] Ejecuta: {sys.executable} -m meshcode read {project} {name}")
1185
1294
  status = "working"
1186
1295
  task = f"{count} pending messages"
1187
1296
 
@@ -1215,13 +1324,21 @@ def show_board(project):
1215
1324
 
1216
1325
 
1217
1326
  def show_status(project=None):
1218
- if project:
1327
+ # Try API-key-authenticated RPC first (RLS may block anon SELECT)
1328
+ api_key = _load_api_key_for_cli()
1329
+ if api_key and not project:
1330
+ rpc_data = sb_rpc("mc_list_user_projects", {"p_api_key": api_key})
1331
+ if isinstance(rpc_data, dict) and not rpc_data.get("error"):
1332
+ projects_data = rpc_data.get("projects", [])
1333
+ else:
1334
+ projects_data = sb_select("mc_projects", "", order="created_at.asc")
1335
+ elif project:
1219
1336
  projects_data = sb_select("mc_projects", f"name=eq.{quote(project)}")
1220
1337
  else:
1221
1338
  projects_data = sb_select("mc_projects", "", order="created_at.asc")
1222
1339
 
1223
1340
  if not projects_data:
1224
- print("[COMMS] No active projects")
1341
+ print("[meshcode] No projects found. Try `meshcode projects` or `meshcode login <api_key>` first.")
1225
1342
  return
1226
1343
 
1227
1344
  print(f"\n{'='*60}")
@@ -1398,7 +1515,7 @@ def _start_heartbeat_daemon(project, name):
1398
1515
  if pid_file.exists():
1399
1516
  old = int(pid_file.read_text(encoding="utf-8").strip())
1400
1517
  try:
1401
- os.kill(old, 15)
1518
+ _terminate_pid(old)
1402
1519
  except Exception:
1403
1520
  pass
1404
1521
  except Exception:
@@ -1450,12 +1567,21 @@ def _start_heartbeat_daemon(project, name):
1450
1567
  return proc.pid
1451
1568
 
1452
1569
 
1570
+ def _terminate_pid(pid):
1571
+ """Terminate a process by PID, cross-platform."""
1572
+ if sys.platform == "win32":
1573
+ subprocess.run(["taskkill", "/F", "/PID", str(pid)],
1574
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
1575
+ else:
1576
+ os.kill(pid, 15)
1577
+
1578
+
1453
1579
  def _stop_heartbeat_daemon(project, name):
1454
1580
  pid_file = _heartbeat_loop_pidfile(project, name)
1455
1581
  if pid_file.exists():
1456
1582
  try:
1457
1583
  pid = int(pid_file.read_text(encoding="utf-8").strip())
1458
- os.kill(pid, 15)
1584
+ _terminate_pid(pid)
1459
1585
  except Exception:
1460
1586
  pass
1461
1587
  try:
@@ -1603,19 +1729,11 @@ def connect(project, name, hook_target="claude", role=""):
1603
1729
  _setup_ws(project, name, actual_role)
1604
1730
 
1605
1731
  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")
1732
+ # Codex reads ~/.codex/config.toml globally — workspace .mcp.json is ignored.
1733
+ # Use setup_clients.setup_global to write the proper TOML config.
1734
+ import importlib
1735
+ _setup_global = importlib.import_module("meshcode.setup_clients").setup_global
1736
+ _setup_global("codex", project, name, actual_role)
1619
1737
 
1620
1738
  else:
1621
1739
  print(f"[meshcode] Unknown hook target: {hook_target}. Use 'claude' or 'codex'.")
@@ -1629,9 +1747,10 @@ def connect(project, name, hook_target="claude", role=""):
1629
1747
  print(f" 3. Start using tools: meshcode_send, meshcode_read, etc.")
1630
1748
  print(f" No extra terminal. No watch loops. No AppleScript.")
1631
1749
  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}")
1750
+ _py = sys.executable
1751
+ print(f" Read: {_py} {comms_path} read {project} {name}")
1752
+ print(f" Send: {_py} {comms_path} send {project} {name}:<target> \"<message>\"")
1753
+ print(f" Board: {_py} {comms_path} board {project}")
1635
1754
  print()
1636
1755
 
1637
1756
 
@@ -1840,6 +1959,15 @@ Show conversation history. Optionally filter to messages between two agents.
1840
1959
  EXAMPLES:
1841
1960
  meshcode history my-app 50
1842
1961
  meshcode history my-app 20 backend,frontend
1962
+ """,
1963
+ "inbox": """meshcode inbox <project> <name>
1964
+
1965
+ All-in-one overview: agent status, unread messages, and open tasks.
1966
+ Alias: meshcode next
1967
+
1968
+ EXAMPLES:
1969
+ meshcode inbox my-app backend
1970
+ meshcode next my-app frontend
1843
1971
  """,
1844
1972
  "projects": """meshcode projects
1845
1973
 
@@ -2050,6 +2178,7 @@ if __name__ == "__main__":
2050
2178
  # Supports both:
2051
2179
  # send <project> <from>:<to> <message>
2052
2180
  # send --project X --from Y --to Z --msg "text" [--type T]
2181
+ # echo '{"key":"val"}' | meshcode send <project> <from>:<to> -
2053
2182
  compact = bool(flags.get("compact"))
2054
2183
  if "from" in flags and "to" in flags:
2055
2184
  proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
@@ -2062,6 +2191,9 @@ if __name__ == "__main__":
2062
2191
  proj = pos[0] if len(pos) > 0 else "default"
2063
2192
  target = pos[1] if len(pos) > 1 else ""
2064
2193
  message = " ".join(pos[2:]) if len(pos) > 2 else ""
2194
+ # Support stdin: `meshcode send proj from:to -` reads from pipe
2195
+ if message.strip() == "-" and not sys.stdin.isatty():
2196
+ message = sys.stdin.read().strip()
2065
2197
  if ":" in target:
2066
2198
  from_a, to_a = target.split(":", 1)
2067
2199
  else:
@@ -2092,6 +2224,11 @@ if __name__ == "__main__":
2092
2224
  name = flags.get("name", pos[1] if len(pos) > 1 else "agent")
2093
2225
  read_messages(proj, name)
2094
2226
 
2227
+ elif cmd in ("inbox", "next"):
2228
+ proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
2229
+ name = flags.get("name", pos[1] if len(pos) > 1 else "agent")
2230
+ inbox(proj, name)
2231
+
2095
2232
  elif cmd == "check":
2096
2233
  hook_check()
2097
2234
 
@@ -2427,6 +2564,7 @@ if __name__ == "__main__":
2427
2564
 
2428
2565
  elif cmd == "whoami":
2429
2566
  # Show the logged-in user identity from profile_meta.json
2567
+ verbose = "--verbose" in sys.argv or "-v" in sys.argv
2430
2568
  meta_path = Path.home() / ".meshcode" / "profile_meta.json"
2431
2569
  if meta_path.exists():
2432
2570
  try:
@@ -2441,6 +2579,25 @@ if __name__ == "__main__":
2441
2579
  print(f" user ID: {meta['user_id']}")
2442
2580
  active = os.environ.get("MESHCODE_KEYCHAIN_PROFILE") or "default"
2443
2581
  print(f" profile: {active}")
2582
+
2583
+ if verbose:
2584
+ # Show projects, agents, plan, API key status
2585
+ api_key = _load_api_key_for_cli()
2586
+ if api_key:
2587
+ print(f" api key: {api_key[:8]}...{api_key[-4:]}")
2588
+ # List projects
2589
+ result = sb_rpc("mc_list_user_projects", {"p_api_key": api_key})
2590
+ if isinstance(result, dict) and result.get("ok"):
2591
+ projects = result.get("projects", [])
2592
+ print(f"\n Projects ({len(projects)}):")
2593
+ for p in projects:
2594
+ agent_count = p.get("agent_count", "?")
2595
+ plan = p.get("plan", "free")
2596
+ print(f" {p.get('name', '?'):20s} plan={plan:12s} agents={agent_count}")
2597
+ else:
2598
+ print(f"\n Projects: (could not fetch)")
2599
+ else:
2600
+ print(f" api key: not found")
2444
2601
  print()
2445
2602
  except Exception:
2446
2603
  print("[meshcode] Could not read profile. Run `meshcode login <api_key>`.")
@@ -2558,7 +2715,7 @@ if __name__ == "__main__":
2558
2715
 
2559
2716
  else:
2560
2717
  known_cmds = [
2561
- "register", "send", "broadcast", "read", "check", "watch",
2718
+ "register", "send", "broadcast", "read", "check", "watch", "inbox", "next",
2562
2719
  "board", "update", "status", "projects", "list", "ls",
2563
2720
  "history", "clear", "unregister", "connect", "disconnect",
2564
2721
  "setup", "run", "go", "invite", "join", "invites", "members",