meshcode 1.0.0__py3-none-any.whl

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.
meshcode/comms_v4.py ADDED
@@ -0,0 +1,1208 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ AICOMMS v4 — Supabase-backed Real-time Inter-Agent Communication
4
+ =================================================================
5
+ Drop-in replacement for comms.py (v3). Same CLI interface, same output format.
6
+ Backend: Supabase REST API (meshcode schema) instead of local files.
7
+ Realtime: Supabase Realtime via publication on mc_messages + mc_agents.
8
+
9
+ COMMANDS (identical to v3):
10
+ register <project> <name> [role] Join a project network
11
+ send <project> <from>:<to> <message> Send message within project
12
+ broadcast <project> <from> <message> Send to all agents in project
13
+ read <project> <name> Read pending messages
14
+ check Hook auto-check (silent if empty)
15
+ watch <project> <name> [interval] [timeout] BLOCKING wait for messages
16
+ board <project> Project status board
17
+ update <project> <name> <status> [task] Update board status
18
+ history <project> [count] [agent1,agent2] Conversation log
19
+ status [project] System overview
20
+ clear <project> <name> Clear inbox
21
+ unregister <project> <name> Leave project
22
+ projects List all active projects
23
+ """
24
+
25
+ import json
26
+ import os
27
+ import sys
28
+ import time
29
+ import subprocess
30
+ from datetime import datetime
31
+ from pathlib import Path
32
+ from urllib.request import Request, urlopen
33
+ from urllib.error import URLError, HTTPError
34
+ from urllib.parse import quote
35
+
36
+ # ============================================================
37
+ # CONFIG — Supabase connection
38
+ # ============================================================
39
+ SUPABASE_URL = os.environ.get(
40
+ "SUPABASE_URL",
41
+ "https://wwgzzmydrwrjgaebspdo.supabase.co"
42
+ )
43
+ SUPABASE_KEY = os.environ.get(
44
+ "SUPABASE_KEY",
45
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Ind3Z3p6bXlkcndyamdhZWJzcGRvIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc2NzY1NDc3MCwiZXhwIjoyMDgzMjMwNzcwfQ.0SBXfb8OtyHmfaKW3dFQ6JYbcLUzCS1d4oXg4V-RAag"
46
+ )
47
+ SCHEMA = "meshcode"
48
+
49
+ # Local paths for session/TTY tracking (still needed for nudge)
50
+ COMMS_DIR = Path(__file__).parent
51
+ SESSIONS_DIR = COMMS_DIR / "sessions"
52
+ LOG_FILE = COMMS_DIR / "comms.log"
53
+
54
+ # ============================================================
55
+ # SUPABASE REST HELPERS (stdlib only — zero dependencies)
56
+ # ============================================================
57
+
58
+ def _headers(*, prefer=None, content_profile=True):
59
+ """Build headers for Supabase REST API."""
60
+ h = {
61
+ "apikey": SUPABASE_KEY,
62
+ "Authorization": f"Bearer {SUPABASE_KEY}",
63
+ "Content-Type": "application/json",
64
+ }
65
+ if content_profile:
66
+ h["Accept-Profile"] = SCHEMA
67
+ h["Content-Profile"] = SCHEMA
68
+ if prefer:
69
+ h["Prefer"] = prefer
70
+ return h
71
+
72
+
73
+ def _request(method, path, *, data=None, prefer=None):
74
+ """Make HTTP request to Supabase REST API. Returns parsed JSON or None."""
75
+ url = f"{SUPABASE_URL}/rest/v1/{path}"
76
+ body = json.dumps(data).encode() if data else None
77
+ req = Request(url, data=body, method=method, headers=_headers(prefer=prefer))
78
+ try:
79
+ with urlopen(req, timeout=10) as resp:
80
+ raw = resp.read().decode()
81
+ return json.loads(raw) if raw.strip() else None
82
+ except HTTPError as e:
83
+ err = e.read().decode()
84
+ # Try to extract trigger / RPC error message for clarity
85
+ try:
86
+ err_obj = json.loads(err)
87
+ msg = err_obj.get("message", err[:200])
88
+ # Daily message limit trigger raises a friendly message
89
+ if "Daily message limit reached" in msg:
90
+ print(f"[QUOTA] {msg}", file=sys.stderr)
91
+ else:
92
+ print(f"[ERROR] {method} {path}: {e.code} {msg}", file=sys.stderr)
93
+ except Exception:
94
+ print(f"[ERROR] {method} {path}: {e.code} {err[:200]}", file=sys.stderr)
95
+ return None
96
+ except URLError as e:
97
+ print(f"[ERROR] Network: {e.reason}", file=sys.stderr)
98
+ return None
99
+
100
+
101
+ def sb_select(table, filters="", order=None, limit=None):
102
+ """SELECT from meshcode.table with PostgREST filters."""
103
+ params = []
104
+ if filters:
105
+ params.append(filters)
106
+ if order:
107
+ params.append(f"order={order}")
108
+ if limit:
109
+ params.append(f"limit={limit}")
110
+ path = f"{table}?{'&'.join(params)}" if params else table
111
+ return _request("GET", path) or []
112
+
113
+
114
+ def sb_insert(table, row, *, upsert=False, on_conflict=None):
115
+ """INSERT into meshcode.table. Returns inserted row."""
116
+ prefer = "return=representation"
117
+ if upsert:
118
+ prefer += ",resolution=merge-duplicates"
119
+ path = table
120
+ if on_conflict:
121
+ path += f"?on_conflict={on_conflict}"
122
+ return _request("POST", path, data=row, prefer=prefer)
123
+
124
+
125
+ def sb_update(table, filters, updates):
126
+ """UPDATE meshcode.table SET updates WHERE filters."""
127
+ path = f"{table}?{filters}"
128
+ return _request("PATCH", path, data=updates, prefer="return=representation")
129
+
130
+
131
+ def sb_delete(table, filters):
132
+ """DELETE FROM meshcode.table WHERE filters."""
133
+ path = f"{table}?{filters}"
134
+ return _request("DELETE", path, prefer="return=representation")
135
+
136
+
137
+ def sb_rpc(fn_name, params):
138
+ """Call a Supabase RPC function."""
139
+ url = f"{SUPABASE_URL}/rest/v1/rpc/{fn_name}"
140
+ body = json.dumps(params).encode()
141
+ req = Request(url, data=body, method="POST", headers=_headers(content_profile=False))
142
+ try:
143
+ with urlopen(req, timeout=10) as resp:
144
+ raw = resp.read().decode()
145
+ return json.loads(raw) if raw.strip() else None
146
+ except (HTTPError, URLError) as e:
147
+ return None
148
+
149
+
150
+ # ============================================================
151
+ # HELPERS
152
+ # ============================================================
153
+
154
+ def now():
155
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
156
+
157
+
158
+ def now_iso():
159
+ return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S+00:00")
160
+
161
+
162
+ def log_msg(text):
163
+ try:
164
+ with open(LOG_FILE, "a") as f:
165
+ f.write(f"[{now()}] {text}\n")
166
+ except:
167
+ pass
168
+
169
+
170
+ def ensure_sessions():
171
+ SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
172
+
173
+
174
+ def get_project_id(project_name):
175
+ """Get or create project, return its UUID."""
176
+ rows = sb_select("mc_projects", f"name=eq.{quote(project_name)}")
177
+ if rows:
178
+ return rows[0]["id"]
179
+ # Create project
180
+ result = sb_insert("mc_projects", {"name": project_name})
181
+ if result and len(result) > 0:
182
+ return result[0]["id"]
183
+ return None
184
+
185
+
186
+ # ============================================================
187
+ # NUDGE (same as v3 — local AppleScript)
188
+ # ============================================================
189
+
190
+ NUDGE_COOLDOWN = 10
191
+
192
+ def get_nudge_file(project, name):
193
+ return SESSIONS_DIR / f".nudge_{project}_{name}" if SESSIONS_DIR.exists() else None
194
+
195
+
196
+ def can_nudge(project, name):
197
+ nf = get_nudge_file(project, name)
198
+ if nf and nf.exists():
199
+ try:
200
+ last = float(nf.read_text().strip())
201
+ if (time.time() - last) < NUDGE_COOLDOWN:
202
+ return False
203
+ except:
204
+ pass
205
+ return True
206
+
207
+
208
+ def mark_nudged(project, name):
209
+ nf = get_nudge_file(project, name)
210
+ if nf:
211
+ nf.write_text(str(time.time()))
212
+
213
+
214
+ def send_notification(project, name, from_agent, pending=1):
215
+ """Cross-platform notification: macOS (osascript), Windows (PowerShell toast), Linux (notify-send)."""
216
+ title = f"MeshCode [{project}] → {name}"
217
+ body = f"{pending} mensaje(s) pendiente(s) de {from_agent}"
218
+ try:
219
+ if sys.platform == "darwin":
220
+ subprocess.run(['osascript', '-e',
221
+ f'display notification "{body}" with title "{title}" sound name "Ping"'],
222
+ capture_output=True, timeout=3)
223
+ elif sys.platform == "win32":
224
+ ps_script = (
225
+ f'[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null; '
226
+ f'$xml = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent(0); '
227
+ f'$text = $xml.GetElementsByTagName("text"); '
228
+ f'$text[0].AppendChild($xml.CreateTextNode("{title}")); '
229
+ f'$text[1].AppendChild($xml.CreateTextNode("{body}")); '
230
+ f'$toast = [Windows.UI.Notifications.ToastNotification]::new($xml); '
231
+ f'[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("MeshCode").Show($toast)'
232
+ )
233
+ subprocess.run(['powershell', '-Command', ps_script],
234
+ capture_output=True, timeout=5)
235
+ else:
236
+ # Linux
237
+ subprocess.run(['notify-send', title, body], capture_output=True, timeout=3)
238
+ except:
239
+ pass
240
+
241
+
242
+ def nudge_agent(project, name, from_agent=""):
243
+ if not can_nudge(project, name):
244
+ return False
245
+
246
+ ensure_sessions()
247
+ session_file = SESSIONS_DIR / f"{project}_{name}"
248
+ if not session_file.exists():
249
+ send_notification(project, name, from_agent, 1)
250
+ return False
251
+
252
+ try:
253
+ data = json.loads(session_file.read_text())
254
+ except:
255
+ send_notification(project, name, from_agent, 1)
256
+ return False
257
+
258
+ tty = data.get("tty")
259
+ if not tty:
260
+ send_notification(project, name, from_agent, 1)
261
+ return False
262
+
263
+ # Count pending from Supabase
264
+ project_id = get_project_id(project)
265
+ pending_msgs = sb_select("mc_messages",
266
+ f"project_id=eq.{project_id}&to_agent=eq.{quote(name)}&read=eq.false&type=neq.ack",
267
+ limit=100)
268
+ pending = len(pending_msgs) if pending_msgs else 1
269
+
270
+ message = f"Tienes {pending} mensaje(s). Revisa: meshcode read {project} {name}"
271
+ escaped_message = message.replace('\\', '\\\\').replace('"', '\\"')
272
+
273
+ # Only attempt AppleScript nudge on macOS
274
+ if sys.platform != "darwin":
275
+ send_notification(project, name, from_agent, pending)
276
+ mark_nudged(project, name)
277
+ return True
278
+
279
+ # Use `do script in tab` — writes command DIRECTLY to the target tab
280
+ # without keystroke injection or focus changes. The user's foreground
281
+ # app stays active. Works even if Terminal is in background.
282
+ applescript_terminal = f'''
283
+ tell application "Terminal"
284
+ set targetTab to missing value
285
+ repeat with w in windows
286
+ repeat with t in tabs of w
287
+ try
288
+ if tty of t contains "{tty}" then
289
+ set targetTab to t
290
+ exit repeat
291
+ end if
292
+ end try
293
+ end repeat
294
+ if targetTab is not missing value then exit repeat
295
+ end repeat
296
+ if targetTab is not missing value then
297
+ do script "{escaped_message}" in targetTab
298
+ else
299
+ error "tty not found"
300
+ end if
301
+ end tell
302
+ '''
303
+ success = False
304
+ try:
305
+ result = subprocess.run(['osascript', '-e', applescript_terminal],
306
+ capture_output=True, text=True, timeout=10)
307
+ if result.returncode == 0:
308
+ success = True
309
+ except:
310
+ pass
311
+
312
+ if success:
313
+ mark_nudged(project, name)
314
+ log_msg(f"[{project}] NUDGE: {name} woken ({pending} pending)")
315
+ return True
316
+
317
+ mark_nudged(project, name)
318
+ send_notification(project, name, from_agent, pending)
319
+ log_msg(f"[{project}] NUDGE: {name} via notification (keystroke failed)")
320
+ return False
321
+
322
+
323
+ def get_session_info():
324
+ """Detect which agent+project this session is via TTY matching."""
325
+ ensure_sessions()
326
+ my_tty = None
327
+ try:
328
+ check_pid = os.getpid()
329
+ for _ in range(8):
330
+ result = subprocess.check_output(['ps', '-p', str(check_pid), '-o', 'tty=,ppid='],
331
+ text=True, timeout=3).strip()
332
+ parts = result.split()
333
+ if len(parts) >= 2:
334
+ tty_val = parts[0]
335
+ parent = parts[1]
336
+ if tty_val and tty_val != '??' and tty_val != '':
337
+ my_tty = tty_val
338
+ break
339
+ check_pid = parent
340
+ else:
341
+ break
342
+ except:
343
+ pass
344
+
345
+ if not my_tty:
346
+ return None, None
347
+
348
+ for f in SESSIONS_DIR.iterdir():
349
+ if f.is_file() and '_' in f.name and not f.name.startswith('.'):
350
+ try:
351
+ data = json.loads(f.read_text())
352
+ session_tty = data.get("tty", "")
353
+ if session_tty and (session_tty in my_tty or my_tty in session_tty):
354
+ return data.get("project"), data.get("agent")
355
+ except:
356
+ continue
357
+
358
+ return None, None
359
+
360
+
361
+ # ============================================================
362
+ # COMMANDS
363
+ # ============================================================
364
+
365
+ def register(project, name, role=""):
366
+ ensure_sessions()
367
+ project_id = get_project_id(project)
368
+ if not project_id:
369
+ print(f"[ERROR] No se pudo crear/encontrar proyecto '{project}'")
370
+ return
371
+
372
+ ppid = os.getppid()
373
+
374
+ # Capture TTY
375
+ tty = None
376
+ try:
377
+ check_pid = ppid
378
+ for _ in range(5):
379
+ result = subprocess.check_output(['ps', '-p', str(check_pid), '-o', 'tty=,ppid='],
380
+ text=True, timeout=3).strip()
381
+ parts = result.split()
382
+ if len(parts) >= 2:
383
+ tty_val = parts[0]
384
+ parent = parts[1]
385
+ if tty_val and tty_val != '??' and tty_val != '':
386
+ tty = tty_val
387
+ break
388
+ check_pid = parent
389
+ else:
390
+ break
391
+ except:
392
+ pass
393
+
394
+ # Register agent via tier-aware RPC (enforces plan limits)
395
+ rpc_result = sb_rpc("mc_register_agent", {
396
+ "p_project_id": project_id,
397
+ "p_name": name,
398
+ "p_role": role,
399
+ "p_status": "online"
400
+ })
401
+
402
+ if rpc_result and rpc_result.get("error"):
403
+ print(f"[ERROR] {rpc_result['error']}")
404
+ if rpc_result.get("upgrade_url"):
405
+ print(f"[ERROR] Upgrade: {rpc_result['upgrade_url']}")
406
+ if rpc_result.get("current") is not None:
407
+ print(f"[ERROR] Agentes: {rpc_result['current']} / {rpc_result.get('max', '?')}")
408
+ return
409
+
410
+ # Update tty/pid (RPC doesn't take these — patch separately)
411
+ sb_update("mc_agents",
412
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}",
413
+ {"tty": tty, "pid": ppid, "task": role, "last_heartbeat": now_iso()})
414
+
415
+ # Save local session file (for nudge TTY detection)
416
+ session_data = {"project": project, "agent": name, "pid": ppid, "tty": tty, "registered_at": now()}
417
+ (SESSIONS_DIR / f"{project}_{name}").write_text(json.dumps(session_data))
418
+
419
+ # Get all agents in project
420
+ agents = sb_select("mc_agents", f"project_id=eq.{project_id}", order="registered_at.asc")
421
+ agent_names = [a["name"] for a in agents]
422
+
423
+ # Count pending messages
424
+ pending_msgs = sb_select("mc_messages",
425
+ f"project_id=eq.{project_id}&to_agent=eq.{quote(name)}&read=eq.false&type=neq.ack")
426
+ pending = len(pending_msgs)
427
+
428
+ print(f"[COMMS] Proyecto: {project}")
429
+ print(f"[COMMS] Registrado: {name} ({role})")
430
+ print(f"[COMMS] TTY: {tty or 'no detectado'}")
431
+ print(f"[COMMS] Online en {project}: {', '.join(agent_names)}")
432
+ print(f"[COMMS] Auto-nudge: {'activado' if tty else 'solo notificaciones (sin TTY)'}")
433
+ print(f"[COMMS] Backend: Supabase ({SCHEMA} schema)")
434
+ if pending > 0:
435
+ print(f"[COMMS] Tienes {pending} mensaje(s) pendiente(s) — ejecuta read para verlos")
436
+
437
+ # Show recent history
438
+ recent = sb_select("mc_messages",
439
+ f"project_id=eq.{project_id}&type=neq.ack",
440
+ order="created_at.desc", limit=10)
441
+ if recent:
442
+ recent.reverse()
443
+ print(f"\n[COMMS] Últimos {len(recent)} mensajes del equipo:")
444
+ for msg in recent:
445
+ ts = msg.get("created_at", "")[-8:]
446
+ fr = msg["from_agent"]
447
+ to = msg["to_agent"]
448
+ payload = msg.get("payload", {})
449
+ preview = json.dumps(payload, ensure_ascii=False)[:80]
450
+ print(f" [{ts}] {fr}->{to}: {preview}")
451
+ print()
452
+
453
+ log_msg(f"{name} joined {project} (tty={tty}, backend=supabase)")
454
+
455
+
456
+ def send_msg(project, from_agent, to_agent, content, msg_type="msg"):
457
+ project_id = get_project_id(project)
458
+ if not project_id:
459
+ print(f"[ERROR] Proyecto '{project}' no encontrado")
460
+ return
461
+
462
+ payload = {}
463
+ try:
464
+ payload = json.loads(content)
465
+ except (json.JSONDecodeError, TypeError):
466
+ payload = {"text": content}
467
+
468
+ msg = {
469
+ "project_id": project_id,
470
+ "from_agent": from_agent,
471
+ "to_agent": to_agent,
472
+ "type": msg_type,
473
+ "payload": payload,
474
+ "read": False
475
+ }
476
+
477
+ result = sb_insert("mc_messages", msg)
478
+ if result:
479
+ preview = json.dumps(payload, ensure_ascii=False)[:60]
480
+ log_msg(f"[{project}] {from_agent}->{to_agent}: {preview}")
481
+ print(f"[{project}] {from_agent}->{to_agent}: sent")
482
+
483
+ # Nudge idle agents (skip acks)
484
+ if msg_type not in ("ack", "warning") and from_agent != "system":
485
+ agents = sb_select("mc_agents",
486
+ f"project_id=eq.{project_id}&name=eq.{quote(to_agent)}")
487
+ if agents and agents[0].get("status") in ("idle", "standby", "online"):
488
+ nudge_agent(project, to_agent, from_agent)
489
+ else:
490
+ print(f"[{project}] ERROR: no se pudo enviar mensaje")
491
+
492
+
493
+ def broadcast(project, from_agent, content, msg_type="broadcast"):
494
+ project_id = get_project_id(project)
495
+ if not project_id:
496
+ return
497
+ agents = sb_select("mc_agents", f"project_id=eq.{project_id}")
498
+ count = 0
499
+ for agent in agents:
500
+ if agent["name"] != from_agent:
501
+ send_msg(project, from_agent, agent["name"], content, msg_type)
502
+ count += 1
503
+ time.sleep(1.5)
504
+ print(f"[{project}] broadcast from {from_agent} -> {count} agents")
505
+
506
+
507
+ def read_messages(project, name, silent=False):
508
+ project_id = get_project_id(project)
509
+ if not project_id:
510
+ if not silent:
511
+ print("[EMPTY]")
512
+ return []
513
+
514
+ # Get unread messages
515
+ messages = sb_select("mc_messages",
516
+ f"project_id=eq.{project_id}&to_agent=eq.{quote(name)}&read=eq.false",
517
+ order="created_at.asc")
518
+
519
+ if not messages:
520
+ if not silent:
521
+ print("[EMPTY]")
522
+ return []
523
+
524
+ # Mark all as read
525
+ msg_ids = [m["id"] for m in messages]
526
+ for mid in msg_ids:
527
+ sb_update("mc_messages", f"id=eq.{mid}", {"read": True})
528
+
529
+ print(f"[{project}] {len(messages)} mensaje(s) para {name}:")
530
+ print("---")
531
+ ack_targets = set()
532
+ for msg in messages:
533
+ sender = msg["from_agent"]
534
+ mtype = msg.get("type", "msg")
535
+ ts = msg.get("created_at", "")
536
+ # Format timestamp to match v3
537
+ if "T" in ts:
538
+ ts = ts.replace("T", " ")[:19]
539
+ payload = msg.get("payload", {})
540
+ print(f"[{sender}|{mtype}|{ts}] {json.dumps(payload, ensure_ascii=False)}")
541
+ if mtype not in ("ack", "broadcast") and sender != name:
542
+ ack_targets.add(sender)
543
+ print("---")
544
+
545
+ # Send automatic ACKs
546
+ for sender in ack_targets:
547
+ ack_payload = {"text": f"{name} leyó tu mensaje"}
548
+ sb_insert("mc_messages", {
549
+ "project_id": project_id,
550
+ "from_agent": name,
551
+ "to_agent": sender,
552
+ "type": "ack",
553
+ "payload": ack_payload,
554
+ "read": False
555
+ })
556
+
557
+ return messages
558
+
559
+
560
+ def hook_check():
561
+ """Called by PostToolUse hook. Auto-detects project+agent, prints pending.
562
+ Also sends heartbeat so dashboard always shows fresh status."""
563
+ ensure_sessions()
564
+ project, agent = get_session_info()
565
+ if not project or not agent:
566
+ return
567
+
568
+ project_id = get_project_id(project)
569
+ if not project_id:
570
+ return
571
+
572
+ # Always heartbeat — keeps agent alive on dashboard
573
+ sb_update("mc_agents",
574
+ f"project_id=eq.{project_id}&name=eq.{quote(agent)}",
575
+ {"status": "working", "last_heartbeat": now_iso()})
576
+
577
+ # Check for unread non-ack messages
578
+ pending = sb_select("mc_messages",
579
+ f"project_id=eq.{project_id}&to_agent=eq.{quote(agent)}&read=eq.false&type=neq.ack",
580
+ limit=1)
581
+
582
+ if not pending:
583
+ return
584
+
585
+ count = len(sb_select("mc_messages",
586
+ f"project_id=eq.{project_id}&to_agent=eq.{quote(agent)}&read=eq.false&type=neq.ack"))
587
+
588
+ print(f"\n*** [{project.upper()}] {count} MENSAJE(S) para {agent} ***")
589
+ read_messages(project, agent, silent=False)
590
+ print(f"*** Lee y responde via meshcode. ***\n")
591
+
592
+
593
+ def watch(project, name, interval=10, timeout=0):
594
+ """BLOCKING wait for messages. Auto-loops: returns on message, auto-restarts after cycle."""
595
+ project_id = get_project_id(project)
596
+ if not project_id:
597
+ return
598
+
599
+ import signal
600
+ _interrupted = [False]
601
+ def handle_interrupt(signum, frame):
602
+ _interrupted[0] = True
603
+ print(f"\n[{project}] {name}: Watch interrumpido por el usuario.")
604
+ sb_update("mc_agents",
605
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}",
606
+ {"status": "online", "task": "Listening to user", "last_heartbeat": now_iso()})
607
+ sys.exit(0)
608
+ signal.signal(signal.SIGINT, handle_interrupt)
609
+
610
+ # Update status to standby
611
+ sb_update("mc_agents",
612
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}",
613
+ {"status": "standby", "task": "Esperando mensajes...", "last_heartbeat": now_iso()})
614
+
615
+ cycle = timeout if timeout > 0 else 600
616
+ print(f"[{project}] {name} en watch — poll cada {interval}s, ciclo {cycle}s (auto-loop)")
617
+
618
+ while not _interrupted[0]:
619
+ start = time.time()
620
+
621
+ while (time.time() - start) < cycle and not _interrupted[0]:
622
+ pending = sb_select("mc_messages",
623
+ f"project_id=eq.{project_id}&to_agent=eq.{quote(name)}&read=eq.false&type=neq.ack",
624
+ limit=1)
625
+
626
+ if pending:
627
+ elapsed = int(time.time() - start)
628
+ print(f"\n*** [{project.upper()}] MENSAJE RECIBIDO ({elapsed}s en standby) ***")
629
+ messages = read_messages(project, name, silent=False)
630
+
631
+ sb_update("mc_agents",
632
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}",
633
+ {"status": "working", "task": "Procesando mensaje recibido", "last_heartbeat": now_iso()})
634
+
635
+ return messages
636
+
637
+ # Heartbeat
638
+ sb_rpc("mc_heartbeat", {"p_project_id": project_id, "p_agent_name": name})
639
+ time.sleep(interval)
640
+
641
+ # Cycle ended without messages — check user context, then AUTO-RESTART watch
642
+ print(f"[{project}] Otro ciclo sin mensajes. Watch activo, sigue corriendo. En standby.")
643
+ sb_update("mc_agents",
644
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}",
645
+ {"status": "standby", "task": "Esperando mensajes...", "last_heartbeat": now_iso()})
646
+
647
+
648
+ def update_board(project, name, status, task=""):
649
+ project_id = get_project_id(project)
650
+ if not project_id:
651
+ return
652
+
653
+ # Block idle if there are unread messages
654
+ if status == "idle":
655
+ pending = sb_select("mc_messages",
656
+ f"project_id=eq.{project_id}&to_agent=eq.{quote(name)}&read=eq.false&type=neq.ack",
657
+ limit=1)
658
+ if pending:
659
+ count = len(sb_select("mc_messages",
660
+ f"project_id=eq.{project_id}&to_agent=eq.{quote(name)}&read=eq.false&type=neq.ack"))
661
+ print(f"[{project}] {name}: NO puedes estar idle — tienes {count} mensaje(s) pendientes.")
662
+ print(f"[{project}] Ejecuta: python3 ~/Desktop/meshcode/comms_v4.py read {project} {name}")
663
+ status = "working"
664
+ task = f"{count} mensajes pendientes"
665
+
666
+ updates = {"status": status, "last_heartbeat": now_iso()}
667
+ if task:
668
+ updates["task"] = task
669
+
670
+ sb_update("mc_agents",
671
+ f"project_id=eq.{project_id}&name=eq.{quote(name)}", updates)
672
+ print(f"[{project}] {name}: {status} — {task}")
673
+
674
+
675
+ def show_board(project):
676
+ project_id = get_project_id(project)
677
+ if not project_id:
678
+ print(f"[{project}] Sin proyecto")
679
+ return
680
+
681
+ agents = sb_select("mc_agents", f"project_id=eq.{project_id}", order="registered_at.asc")
682
+ if not agents:
683
+ print(f"[{project}] Sin agentes")
684
+ return
685
+
686
+ print(f"\n{'='*60}")
687
+ print(f" {project.upper()} — Board")
688
+ print(f"{'='*60}")
689
+ for a in agents:
690
+ icon = {"online": "●", "working": "▶", "blocked": "■", "standby": "◆", "idle": "○", "done": "✓"}.get(a.get("status", ""), "?")
691
+ print(f" {icon} {a['name']:<14} {a.get('status','?'):<10} {a.get('task','')}")
692
+ print()
693
+
694
+
695
+ def show_status(project=None):
696
+ if project:
697
+ projects_data = sb_select("mc_projects", f"name=eq.{quote(project)}")
698
+ else:
699
+ projects_data = sb_select("mc_projects", "", order="created_at.asc")
700
+
701
+ if not projects_data:
702
+ print("[COMMS] Sin proyectos activos")
703
+ return
704
+
705
+ print(f"\n{'='*60}")
706
+ print(f" AICOMMS v4 STATUS (Supabase)")
707
+ print(f"{'='*60}")
708
+
709
+ for proj in projects_data:
710
+ agents = sb_select("mc_agents", f"project_id=eq.{proj['id']}", order="name.asc")
711
+ if not agents:
712
+ continue
713
+
714
+ print(f"\n [{proj['name']}]")
715
+ for a in agents:
716
+ # Count pending
717
+ pending = sb_select("mc_messages",
718
+ f"project_id=eq.{proj['id']}&to_agent=eq.{quote(a['name'])}&read=eq.false&type=neq.ack")
719
+ pcount = len(pending)
720
+ icon = "●" if pcount > 0 else "○"
721
+ print(f" {icon} {a['name']:<14} {a.get('role',''):<25} pending: {pcount}")
722
+ print()
723
+
724
+
725
+ def show_history(project, last_n=20, between=None):
726
+ project_id = get_project_id(project)
727
+ if not project_id:
728
+ print(f"[{project}] Sin historial")
729
+ return
730
+
731
+ filters = f"project_id=eq.{project_id}&type=neq.ack"
732
+ if between:
733
+ agents = between.split(",")
734
+ if len(agents) == 2:
735
+ a, b = agents
736
+ # PostgREST OR filter
737
+ filters += f"&or=(and(from_agent.eq.{quote(a)},to_agent.eq.{quote(b)}),and(from_agent.eq.{quote(b)},to_agent.eq.{quote(a)}))"
738
+
739
+ messages = sb_select("mc_messages", filters, order="created_at.desc", limit=last_n)
740
+ if not messages:
741
+ print(f"[{project}] Sin historial")
742
+ return
743
+
744
+ messages.reverse()
745
+
746
+ print(f"\n{'='*60}")
747
+ print(f" {project.upper()} — Historial ({len(messages)} mensajes)")
748
+ if between:
749
+ print(f" Filtro: {between}")
750
+ print(f"{'='*60}\n")
751
+
752
+ for msg in messages:
753
+ ts = msg.get("created_at", "")
754
+ if "T" in ts:
755
+ ts = ts.replace("T", " ")[:19]
756
+ sender = msg.get("from_agent", "?")
757
+ to = msg.get("to_agent", "?")
758
+ payload = msg.get("payload", {})
759
+ content = json.dumps(payload, ensure_ascii=False)
760
+ print(f" [{ts}] {sender}->{to}: {content}")
761
+ print()
762
+
763
+
764
+ def list_projects():
765
+ projects_data = sb_select("mc_projects", "", order="created_at.asc")
766
+ if not projects_data:
767
+ print("[COMMS] Sin proyectos")
768
+ return
769
+
770
+ print(f"\n{'='*60}")
771
+ print(f" PROYECTOS ACTIVOS (Supabase)")
772
+ print(f"{'='*60}")
773
+
774
+ for proj in projects_data:
775
+ agents = sb_select("mc_agents", f"project_id=eq.{proj['id']}", order="name.asc")
776
+ statuses = [f"{a['name']}({a.get('status','?')})" for a in agents]
777
+ print(f" [{proj['name']}] {', '.join(statuses) if statuses else 'sin agentes'}")
778
+ print()
779
+
780
+
781
+ def clear_inbox(project, name):
782
+ project_id = get_project_id(project)
783
+ if not project_id:
784
+ return
785
+ # Mark all as read
786
+ sb_update("mc_messages",
787
+ f"project_id=eq.{project_id}&to_agent=eq.{quote(name)}&read=eq.false",
788
+ {"read": True})
789
+ print(f"[{project}] {name} inbox cleared")
790
+
791
+
792
+ def unregister(project, name):
793
+ project_id = get_project_id(project)
794
+ if not project_id:
795
+ return
796
+ sb_delete("mc_agents", f"project_id=eq.{project_id}&name=eq.{quote(name)}")
797
+
798
+ # Clean local session
799
+ session_file = SESSIONS_DIR / f"{project}_{name}"
800
+ if session_file.exists():
801
+ session_file.unlink()
802
+
803
+ log_msg(f"{name} left {project}")
804
+ print(f"[{project}] {name} removed")
805
+
806
+
807
+ def connect(project, name, hook_target="claude", role=""):
808
+ """One-command setup: detect OS, check auth, register agent, install hook, start watch."""
809
+ import platform
810
+ os_name = platform.system() # Darwin, Windows, Linux
811
+
812
+ print(f"[MESHCODE] Plataforma detectada: {os_name}")
813
+ print(f"[MESHCODE] Conectando a meshwork '{project}' como '{name}'...")
814
+
815
+ # Check for saved credentials
816
+ creds_path = Path.home() / ".meshcode" / "credentials.json"
817
+ if creds_path.exists():
818
+ try:
819
+ creds = json.loads(creds_path.read_text())
820
+ print(f"[MESHCODE] Autenticado como {creds.get('display_name', creds.get('email', '?'))}")
821
+ except:
822
+ pass
823
+
824
+ # Register agent
825
+ actual_role = role or f"Connected via meshcode connect ({hook_target})"
826
+ register(project, name, actual_role)
827
+
828
+ # Install hook based on target
829
+ comms_path = str(Path(__file__).resolve())
830
+
831
+ if hook_target == "claude":
832
+ settings_path = Path.home() / ".claude" / "settings.json"
833
+ hook_cmd = f"python3 {comms_path} check"
834
+
835
+ settings = {}
836
+ if settings_path.exists():
837
+ try:
838
+ settings = json.loads(settings_path.read_text())
839
+ except:
840
+ pass
841
+
842
+ hooks = settings.setdefault("hooks", {})
843
+ post_hooks = hooks.setdefault("PostToolUse", [])
844
+
845
+ already = any(hook_cmd in h.get("command", "") for h in post_hooks)
846
+ if not already:
847
+ post_hooks.append({"command": hook_cmd, "matcher": ""})
848
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
849
+ settings_path.write_text(json.dumps(settings, indent=2))
850
+ print(f"[MESHCODE] Hook PostToolUse instalado en {settings_path}")
851
+ else:
852
+ print(f"[MESHCODE] Hook PostToolUse ya existe")
853
+
854
+ print(f"[MESHCODE] Claude Code conectado a {project} como {name}")
855
+
856
+ elif hook_target == "codex":
857
+ config_path = Path.cwd() / ".meshcode.json"
858
+ config = {
859
+ "project": project,
860
+ "agent": name,
861
+ "supabase_url": SUPABASE_URL,
862
+ "check_command": f"python3 {comms_path} check",
863
+ "read_command": f"python3 {comms_path} read {project} {name}",
864
+ "send_command": f"python3 {comms_path} send {project} {name}:{{to}} '{{message}}'",
865
+ "platform": os_name
866
+ }
867
+ config_path.write_text(json.dumps(config, indent=2))
868
+ print(f"[MESHCODE] Config guardada en {config_path}")
869
+ print(f"[MESHCODE] Codex: usa read_command y send_command del config")
870
+
871
+ else:
872
+ print(f"[MESHCODE] Hook target desconocido: {hook_target}. Usa 'claude' o 'codex'.")
873
+ return
874
+
875
+ # Print quickstart
876
+ print(f"\n[MESHCODE] ✓ Setup completo. Comandos útiles:")
877
+ print(f" Leer: python3 {comms_path} read {project} {name}")
878
+ print(f" Enviar: python3 {comms_path} send {project} {name}:<destino> '<mensaje>'")
879
+ print(f" Board: python3 {comms_path} board {project}")
880
+ print(f" Watch: python3 {comms_path} watch {project} {name}")
881
+ print()
882
+
883
+
884
+ def login(api_key):
885
+ """Authenticate with API key and save credentials locally."""
886
+ result = sb_rpc("mc_validate_api_key", {"p_api_key": api_key})
887
+ if not result or not result.get("valid"):
888
+ print("[MESHCODE] API key inválida o expirada")
889
+ return False
890
+
891
+ # Save credentials
892
+ config_dir = Path.home() / ".meshcode"
893
+ config_dir.mkdir(exist_ok=True)
894
+ creds = {
895
+ "api_key": api_key,
896
+ "user_id": result["user_id"],
897
+ "email": result["email"],
898
+ "display_name": result["display_name"]
899
+ }
900
+ (config_dir / "credentials.json").write_text(json.dumps(creds, indent=2))
901
+ print(f"[MESHCODE] Autenticado como {result['display_name']} ({result['email']})")
902
+ return True
903
+
904
+
905
+ def show_help():
906
+ print("""
907
+ ╔═══════════════════════════════════════════════════════════╗
908
+ ║ MeshCode v4 — Supabase-backed Agent Network ║
909
+ ╚═══════════════════════════════════════════════════════════╝
910
+
911
+ CORE:
912
+ register <proj> <name> [role] Join project
913
+ send <proj> <from>:<to> <message> Send message
914
+ broadcast <proj> <from> <message> Send to all
915
+ read <proj> <name> Read pending
916
+ check Hook auto-check
917
+ watch <proj> <name> [interval] [timeout] Wait for messages
918
+
919
+ STATUS:
920
+ board <proj> Agent status board
921
+ update <proj> <name> <status> [task] Update status
922
+ status [proj] Overview
923
+ projects List projects
924
+
925
+ HISTORY:
926
+ history <proj> [count] [agent1,agent2] Conversation log
927
+
928
+ SETUP:
929
+ connect <proj> <name> [claude|codex] One-command setup + hook install
930
+ login <api_key> Authenticate with API key
931
+
932
+ CLEANUP:
933
+ clear <proj> <name> Clear inbox
934
+ unregister <proj> <name> Leave project
935
+
936
+ BACKEND: Supabase (meshcode schema)
937
+ PLATFORM: macOS, Windows, Linux
938
+
939
+ Run `meshcode <command> --help` for help on a specific command.
940
+ """)
941
+
942
+
943
+ # Per-subcommand help texts
944
+ SUBCOMMAND_HELP = {
945
+ "register": """meshcode register <project> <name> [role]
946
+
947
+ Join a meshwork as a new agent. Tier limits enforced (free=3 agents).
948
+
949
+ ARGUMENTS:
950
+ project Meshwork name (created if doesn't exist)
951
+ name Unique agent name within the meshwork
952
+ role Optional human-readable role description
953
+
954
+ EXAMPLES:
955
+ meshcode register my-app backend "Backend Engineer"
956
+ meshcode register my-app frontend "React UI Developer"
957
+ """,
958
+ "send": """meshcode send <project> <from>:<to> <message>
959
+
960
+ Send a message from one agent to another within a meshwork.
961
+
962
+ ARGUMENTS:
963
+ project Meshwork name
964
+ from:to Sender agent name and recipient agent name, separated by colon
965
+ message JSON payload (preferred) or plain text
966
+
967
+ EXAMPLES:
968
+ meshcode send my-app backend:frontend '{"done":"API ready"}'
969
+ meshcode send my-app backend:commander '{"need":"review","priority":"urgent"}'
970
+ """,
971
+ "broadcast": """meshcode broadcast <project> <from> <message>
972
+
973
+ Send a message to all agents in the meshwork (except sender).
974
+
975
+ EXAMPLES:
976
+ meshcode broadcast my-app commander '{"fyi":"deployment in 10min"}'
977
+ """,
978
+ "read": """meshcode read <project> <name>
979
+
980
+ Read all pending (unread) messages for an agent. Marks them as read
981
+ and sends automatic ACKs to senders.
982
+
983
+ EXAMPLES:
984
+ meshcode read my-app backend
985
+ """,
986
+ "check": """meshcode check
987
+
988
+ Hook auto-check (silent if no pending messages).
989
+ Used internally by the Claude Code PostToolUse hook to detect
990
+ incoming messages between tool calls.
991
+ """,
992
+ "watch": """meshcode watch <project> <name> [interval] [timeout]
993
+
994
+ Block waiting for messages. Polls every <interval> seconds (default 10).
995
+ Exits when a message arrives or after <timeout> seconds (default 600 = 10 min).
996
+
997
+ EXAMPLES:
998
+ meshcode watch my-app backend # default 10s poll, 10min cycle
999
+ meshcode watch my-app backend 5 300 # poll 5s, exit after 5min
1000
+ """,
1001
+ "board": """meshcode board <project>
1002
+
1003
+ Show the status board for all agents in a meshwork.
1004
+ """,
1005
+ "update": """meshcode update <project> <name> <status> [task]
1006
+
1007
+ Update agent status. Statuses: online, working, idle, standby, blocked, done.
1008
+
1009
+ EXAMPLES:
1010
+ meshcode update my-app backend working "implementing auth"
1011
+ meshcode update my-app backend idle "available"
1012
+ """,
1013
+ "status": """meshcode status [project]
1014
+
1015
+ Show overview of all meshworks (or one specific meshwork).
1016
+ """,
1017
+ "history": """meshcode history <project> [count] [agent1,agent2]
1018
+
1019
+ Show conversation history. Optionally filter to messages between two agents.
1020
+
1021
+ EXAMPLES:
1022
+ meshcode history my-app 50
1023
+ meshcode history my-app 20 backend,frontend
1024
+ """,
1025
+ "projects": """meshcode projects
1026
+
1027
+ List all meshworks visible to the current API key.
1028
+ """,
1029
+ "connect": """meshcode connect <project> <name> [claude|codex] [role]
1030
+
1031
+ One-command setup: detect OS, register agent, install hook for the chosen
1032
+ host (Claude Code or Codex), and print quickstart commands.
1033
+
1034
+ EXAMPLES:
1035
+ meshcode connect my-app backend claude "Backend Engineer"
1036
+ meshcode connect my-app worker codex "Data Worker"
1037
+ """,
1038
+ "login": """meshcode login <api_key>
1039
+
1040
+ Authenticate with a MeshCode API key. Saves credentials to
1041
+ ~/.meshcode/credentials.json. Get a key from meshcode.io/settings or
1042
+ during the welcome wizard after signup.
1043
+ """,
1044
+ "clear": """meshcode clear <project> <name>
1045
+
1046
+ Clear inbox: marks all unread messages as read for an agent.
1047
+ """,
1048
+ "unregister": """meshcode unregister <project> <name>
1049
+
1050
+ Remove an agent from a meshwork (deletes the row from mc_agents).
1051
+ """,
1052
+ }
1053
+
1054
+
1055
+ def show_subcommand_help(cmd):
1056
+ """Print help for a specific subcommand."""
1057
+ text = SUBCOMMAND_HELP.get(cmd)
1058
+ if text:
1059
+ print(text)
1060
+ else:
1061
+ print(f"[ERROR] No help available for '{cmd}'")
1062
+ show_help()
1063
+
1064
+
1065
+ # ============================================================
1066
+ # MAIN — same CLI interface as v3
1067
+ # ============================================================
1068
+
1069
+ def parse_flags(argv):
1070
+ """Parse --flag value pairs from argv, return (flags_dict, remaining_positional_args)."""
1071
+ flags = {}
1072
+ positional = []
1073
+ i = 0
1074
+ while i < len(argv):
1075
+ if argv[i].startswith("--") and i + 1 < len(argv):
1076
+ flags[argv[i][2:]] = argv[i + 1]
1077
+ i += 2
1078
+ else:
1079
+ positional.append(argv[i])
1080
+ i += 1
1081
+ return flags, positional
1082
+
1083
+
1084
+ if __name__ == "__main__":
1085
+ if len(sys.argv) < 2:
1086
+ show_help()
1087
+ sys.exit(0)
1088
+
1089
+ cmd = sys.argv[1].lower()
1090
+
1091
+ # Per-subcommand help: meshcode <cmd> --help / -h / help
1092
+ if len(sys.argv) >= 3 and sys.argv[2] in ("--help", "-h", "help"):
1093
+ show_subcommand_help(cmd)
1094
+ sys.exit(0)
1095
+
1096
+ flags, pos = parse_flags(sys.argv[2:])
1097
+
1098
+ if cmd == "register":
1099
+ proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
1100
+ name = flags.get("name", pos[1] if len(pos) > 1 else "agent")
1101
+ role = flags.get("role", " ".join(pos[2:]) if len(pos) > 2 else "")
1102
+ register(proj, name, role)
1103
+
1104
+ elif cmd == "send":
1105
+ # Supports both:
1106
+ # send <project> <from>:<to> <message>
1107
+ # send --project X --from Y --to Z --msg "text" [--type T]
1108
+ if "from" in flags and "to" in flags:
1109
+ proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
1110
+ from_a = flags["from"]
1111
+ to_a = flags["to"]
1112
+ message = flags.get("msg", flags.get("message", " ".join(pos)))
1113
+ msg_type = flags.get("type", "msg")
1114
+ send_msg(proj, from_a, to_a, message, msg_type)
1115
+ else:
1116
+ proj = pos[0] if len(pos) > 0 else "default"
1117
+ target = pos[1] if len(pos) > 1 else ""
1118
+ message = " ".join(pos[2:]) if len(pos) > 2 else ""
1119
+ if ":" in target:
1120
+ from_a, to_a = target.split(":", 1)
1121
+ else:
1122
+ from_a, to_a = "?", target
1123
+ send_msg(proj, from_a, to_a, message)
1124
+
1125
+ elif cmd == "broadcast":
1126
+ if "from" in flags:
1127
+ proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
1128
+ from_a = flags["from"]
1129
+ message = flags.get("msg", flags.get("message", " ".join(pos)))
1130
+ msg_type = flags.get("type", "broadcast")
1131
+ broadcast(proj, from_a, message, msg_type)
1132
+ else:
1133
+ proj = pos[0] if len(pos) > 0 else "default"
1134
+ from_a = pos[1] if len(pos) > 1 else "?"
1135
+ message = " ".join(pos[2:]) if len(pos) > 2 else ""
1136
+ broadcast(proj, from_a, message)
1137
+
1138
+ elif cmd == "read":
1139
+ proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
1140
+ name = flags.get("name", pos[1] if len(pos) > 1 else "agent")
1141
+ read_messages(proj, name)
1142
+
1143
+ elif cmd == "check":
1144
+ hook_check()
1145
+
1146
+ elif cmd == "watch":
1147
+ proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
1148
+ name = flags.get("name", pos[1] if len(pos) > 1 else "agent")
1149
+ interval = int(flags.get("interval", pos[2] if len(pos) > 2 else "10"))
1150
+ timeout = int(flags.get("timeout", pos[3] if len(pos) > 3 else "0"))
1151
+ watch(proj, name, interval, timeout)
1152
+
1153
+ elif cmd == "board":
1154
+ proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
1155
+ show_board(proj)
1156
+
1157
+ elif cmd == "update":
1158
+ proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
1159
+ name = flags.get("name", pos[1] if len(pos) > 1 else "agent")
1160
+ st = flags.get("status", pos[2] if len(pos) > 2 else "online")
1161
+ task = flags.get("task", " ".join(pos[3:]) if len(pos) > 3 else "")
1162
+ update_board(proj, name, st, task)
1163
+
1164
+ elif cmd == "status":
1165
+ proj = sys.argv[2] if len(sys.argv) > 2 else None
1166
+ show_status(proj)
1167
+
1168
+ elif cmd == "projects":
1169
+ list_projects()
1170
+
1171
+ elif cmd == "history":
1172
+ proj = sys.argv[2] if len(sys.argv) > 2 else "default"
1173
+ count = int(sys.argv[3]) if len(sys.argv) > 3 and sys.argv[3].isdigit() else 20
1174
+ between = sys.argv[4] if len(sys.argv) > 4 else None
1175
+ if len(sys.argv) > 3 and not sys.argv[3].isdigit():
1176
+ between = sys.argv[3]
1177
+ show_history(proj, count, between)
1178
+
1179
+ elif cmd == "clear":
1180
+ proj = sys.argv[2] if len(sys.argv) > 2 else "default"
1181
+ name = sys.argv[3] if len(sys.argv) > 3 else "agent"
1182
+ clear_inbox(proj, name)
1183
+
1184
+ elif cmd == "unregister":
1185
+ proj = sys.argv[2] if len(sys.argv) > 2 else "default"
1186
+ name = sys.argv[3] if len(sys.argv) > 3 else "agent"
1187
+ unregister(proj, name)
1188
+
1189
+ elif cmd == "connect":
1190
+ proj = sys.argv[2] if len(sys.argv) > 2 else "default"
1191
+ name = sys.argv[3] if len(sys.argv) > 3 else "agent"
1192
+ hook = sys.argv[4] if len(sys.argv) > 4 else "claude"
1193
+ connect(proj, name, hook)
1194
+
1195
+ elif cmd == "login":
1196
+ key = sys.argv[2] if len(sys.argv) > 2 else ""
1197
+ if not key:
1198
+ print("[ERROR] Uso: meshcode login <api_key>")
1199
+ sys.exit(1)
1200
+ login(key)
1201
+
1202
+ elif cmd in ("help", "--help", "-h"):
1203
+ show_help()
1204
+
1205
+ else:
1206
+ print(f"[ERROR] Comando desconocido: {cmd}")
1207
+ show_help()
1208
+ sys.exit(1)