meshcode 2.8.6__tar.gz → 2.8.8__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.8.6 → meshcode-2.8.8}/PKG-INFO +1 -1
  2. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/__init__.py +1 -1
  3. meshcode-2.8.8/meshcode/ascii_art.py +130 -0
  4. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/comms_v4.py +14 -0
  5. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/meshcode_mcp/server.py +56 -26
  6. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/run_agent.py +82 -0
  7. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode.egg-info/PKG-INFO +1 -1
  8. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode.egg-info/SOURCES.txt +1 -0
  9. {meshcode-2.8.6 → meshcode-2.8.8}/pyproject.toml +1 -1
  10. {meshcode-2.8.6 → meshcode-2.8.8}/README.md +0 -0
  11. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/cli.py +0 -0
  12. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/invites.py +0 -0
  13. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/launcher.py +0 -0
  14. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/launcher_install.py +0 -0
  15. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/meshcode_mcp/__init__.py +0 -0
  16. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/meshcode_mcp/__main__.py +0 -0
  17. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/meshcode_mcp/backend.py +0 -0
  18. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/meshcode_mcp/realtime.py +0 -0
  19. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/meshcode_mcp/test_backend.py +0 -0
  20. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  21. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  22. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/preferences.py +0 -0
  23. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/protocol_v2.py +0 -0
  24. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/secrets.py +0 -0
  25. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/self_update.py +0 -0
  26. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode/setup_clients.py +0 -0
  27. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode.egg-info/requires.txt +0 -0
  30. {meshcode-2.8.6 → meshcode-2.8.8}/meshcode.egg-info/top_level.txt +0 -0
  31. {meshcode-2.8.6 → meshcode-2.8.8}/setup.cfg +0 -0
  32. {meshcode-2.8.6 → meshcode-2.8.8}/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.8.6
3
+ Version: 2.8.8
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,2 +1,2 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.8.6"
2
+ __version__ = "2.8.8"
@@ -0,0 +1,130 @@
1
+ """Procedural ASCII art generator for MeshCode agents.
2
+
3
+ Each agent gets a unique visual identity — like a QR code / identicon
4
+ but made of block characters. Generated deterministically from the
5
+ agent name hash, so it's always the same for the same agent.
6
+ Stored in mc_agents.ascii_art, displayed on terminal launch.
7
+ """
8
+ import hashlib
9
+
10
+ # Block characters for the pattern
11
+ BLOCKS = {
12
+ 0: " ", # empty
13
+ 1: "██", # full block
14
+ 2: "░░", # light shade
15
+ 3: "▓▓", # dark shade
16
+ 4: "▒▒", # medium shade
17
+ 5: "╬╬", # cross
18
+ 6: "◆◆", # diamond
19
+ 7: "●●", # circle
20
+ }
21
+
22
+ # ANSI colors
23
+ COLORS = [
24
+ "\033[36m", # cyan
25
+ "\033[35m", # magenta
26
+ "\033[32m", # green
27
+ "\033[33m", # yellow
28
+ "\033[34m", # blue
29
+ "\033[91m", # bright red
30
+ "\033[96m", # bright cyan
31
+ "\033[95m", # bright magenta
32
+ "\033[92m", # bright green
33
+ "\033[94m", # bright blue
34
+ ]
35
+ RESET = "\033[0m"
36
+ DIM = "\033[2m"
37
+ BOLD = "\033[1m"
38
+
39
+
40
+ def _hash_bytes(name: str) -> bytes:
41
+ """Get deterministic hash bytes from agent name."""
42
+ return hashlib.sha256(name.encode("utf-8")).digest()
43
+
44
+
45
+ def generate_art(agent_name: str, size: int = 7) -> str:
46
+ """Generate a unique symmetric block pattern from the agent name.
47
+
48
+ Creates a (size x size) grid that's mirrored horizontally for symmetry
49
+ (like identicons / QR codes). The pattern is deterministic — same name
50
+ always produces the same art.
51
+ """
52
+ h = _hash_bytes(agent_name)
53
+ half = (size + 1) // 2 # columns to generate (left half + center)
54
+ rows = []
55
+
56
+ for y in range(size):
57
+ row = []
58
+ for x in range(half):
59
+ # Each cell uses a byte from the hash
60
+ idx = (y * half + x) % len(h)
61
+ byte_val = h[idx]
62
+
63
+ # Decide if cell is filled (60% chance) and which block
64
+ if byte_val < 100:
65
+ block = BLOCKS[0] # empty
66
+ else:
67
+ block_idx = (byte_val % 6) + 1 # blocks 1-6
68
+ block = BLOCKS[block_idx]
69
+ row.append(block)
70
+
71
+ # Mirror: build full row (left + center + right)
72
+ if size % 2 == 1:
73
+ full_row = row[:-1] + [row[-1]] + row[-2::-1]
74
+ else:
75
+ full_row = row + row[::-1]
76
+ rows.append("".join(full_row))
77
+
78
+ # Add border
79
+ width = len(rows[0]) if rows else 0
80
+ border_h = "─" * (width + 2)
81
+ lines = []
82
+ lines.append(f" ┌{border_h}┐")
83
+ for row in rows:
84
+ lines.append(f" │ {row} │")
85
+ lines.append(f" └{border_h}┘")
86
+ lines.append(f" ⟨ {agent_name} ⟩")
87
+
88
+ return "\n".join(lines)
89
+
90
+
91
+ YELLOW = "\033[33m"
92
+ STAR = "★"
93
+
94
+
95
+ def render_welcome(agent_name: str, meshwork_name: str, ascii_art: str,
96
+ version: str = "", is_commander: bool = False) -> str:
97
+ """Render the full colored welcome banner for terminal display."""
98
+ h = _hash_bytes(agent_name)
99
+ color = COLORS[h[0] % len(COLORS)]
100
+
101
+ ver_str = f"v{version}" if version else ""
102
+ commander_badge = f" {YELLOW}{STAR} COMMANDER{RESET}" if is_commander else ""
103
+ lines = []
104
+ lines.append("")
105
+ lines.append(f"{color}{BOLD} ╔══════════════════════════════════════════╗{RESET}")
106
+ lines.append(f"{color}{BOLD} ║{RESET} {DIM}M E S H C O D E{RESET} {color}{ver_str}{RESET} {color}{BOLD}║{RESET}")
107
+ lines.append(f"{color}{BOLD} ╚══════════════════════════════════════════╝{RESET}")
108
+ lines.append("")
109
+
110
+ for line in ascii_art.split("\n"):
111
+ lines.append(f" {color}{line}{RESET}")
112
+
113
+ lines.append("")
114
+ lines.append(f" {BOLD}{color}●{RESET} {BOLD}{agent_name}{RESET}{commander_badge} {DIM}is online{RESET}")
115
+ lines.append(f" {DIM}meshwork:{RESET} {meshwork_name}")
116
+ if version:
117
+ lines.append(f" {DIM}version:{RESET} {version}")
118
+ lines.append("")
119
+
120
+ return "\n".join(lines)
121
+
122
+
123
+ if __name__ == "__main__":
124
+ # Demo: show a few agents
125
+ import sys
126
+ names = sys.argv[1:] or ["sammy", "ian", "fis", "backend", "security", "commander"]
127
+ for name in names:
128
+ art = generate_art(name)
129
+ print(render_welcome(name, "demo-mesh", art, "2.8.6"))
130
+ print()
@@ -1484,6 +1484,20 @@ def connect_terminal(project, name, role=""):
1484
1484
  session_data = {"project": project, "agent": name, "pid": ppid, "tty": tty, "registered_at": now()}
1485
1485
  (SESSIONS_DIR / f"{project}_{name}").write_text(json.dumps(session_data), encoding="utf-8")
1486
1486
 
1487
+ # Track install/connection for admin metrics (fire-and-forget)
1488
+ try:
1489
+ import platform as _platform
1490
+ sb_rpc("mc_track_install", {
1491
+ "p_api_key": _api_key,
1492
+ "p_project_id": project_id,
1493
+ "p_agent_name": name,
1494
+ "p_cli_version": VERSION,
1495
+ "p_platform": sys.platform,
1496
+ "p_editor": os.environ.get("MESHCODE_EDITOR", "unknown"),
1497
+ })
1498
+ except Exception:
1499
+ pass # Non-critical — don't block agent boot
1500
+
1487
1501
  hb_pid = _start_heartbeat_daemon(project, name)
1488
1502
 
1489
1503
  pending = (rpc_result or {}).get("pending_messages", 0)
@@ -12,11 +12,41 @@ import json
12
12
  import logging
13
13
  import os
14
14
  import sys
15
+ import hashlib as _hashlib
15
16
  from collections import deque
16
17
  from contextlib import asynccontextmanager
17
18
  from typing import Any, Dict, List, Optional, Union
18
19
 
19
20
 
21
+ # ── Agent color for terminal logs (pure ANSI, zero tokens) ──────
22
+ _ANSI_COLORS = [
23
+ "\033[36m", "\033[35m", "\033[32m", "\033[33m", "\033[34m",
24
+ "\033[91m", "\033[96m", "\033[95m", "\033[92m", "\033[94m",
25
+ ]
26
+ _ANSI_RESET = "\033[0m"
27
+ _ANSI_BOLD = "\033[1m"
28
+ _ANSI_DIM = "\033[2m"
29
+
30
+
31
+ def _agent_color(name: str) -> str:
32
+ """Deterministic ANSI color from agent name hash."""
33
+ h = int(_hashlib.md5(name.encode()).hexdigest()[:8], 16)
34
+ return _ANSI_COLORS[h % len(_ANSI_COLORS)]
35
+
36
+
37
+ def _mc_log(msg: str, level: str = "info") -> None:
38
+ """Colored [meshcode-mcp] log line. Uses agent color if available."""
39
+ agent = os.environ.get("MESHCODE_AGENT", "")
40
+ c = _agent_color(agent) if agent else "\033[36m"
41
+ prefix = f"{c}{_ANSI_BOLD}[meshcode-mcp]{_ANSI_RESET}"
42
+ if level == "error":
43
+ print(f"{prefix} \033[91mERROR:{_ANSI_RESET} {msg}", "warn")
44
+ elif level == "warn":
45
+ print(f"{prefix} \033[33mWARNING:{_ANSI_RESET} {msg}", "warn")
46
+ else:
47
+ print(f"{prefix} {c}{msg}{_ANSI_RESET}", "warn")
48
+
49
+
20
50
  # ============================================================
21
51
  # Dedupe: track IDs of messages we've already returned from
22
52
  # meshcode_wait / meshcode_check so the same row doesn't show
@@ -309,7 +339,7 @@ def _get_api_key() -> str:
309
339
  _API_KEY_CACHE = kc_val
310
340
  return kc_val
311
341
  except Exception as e:
312
- print(f"[meshcode-mcp] WARNING: keychain lookup failed for profile '{profile}': {e}", file=sys.stderr)
342
+ _mc_log(f" keychain lookup failed for profile '{profile}': {e}", "warn")
313
343
  _API_KEY_CACHE = ""
314
344
  return ""
315
345
 
@@ -330,13 +360,13 @@ if not _PROJECT_ID:
330
360
  if isinstance(_r, dict) and _r.get("project_id"):
331
361
  _PROJECT_ID = _r["project_id"]
332
362
  elif isinstance(_r, dict) and _r.get("error"):
333
- print(f"[meshcode-mcp] WARNING: mc_resolve_project: {_r['error']}", file=sys.stderr)
363
+ _mc_log(f" mc_resolve_project: {_r['error']}", "warn")
334
364
  except Exception as _e:
335
- print(f"[meshcode-mcp] WARNING: mc_resolve_project failed: {_e}", file=sys.stderr)
365
+ _mc_log(f" mc_resolve_project failed: {_e}", "warn")
336
366
  if not _PROJECT_ID:
337
367
  _PROJECT_ID = be.get_project_id(PROJECT_NAME)
338
368
  if not _PROJECT_ID:
339
- print(f"[meshcode-mcp] ERROR: project '{PROJECT_NAME}' not found (check MESHCODE_KEYCHAIN_PROFILE / MESHCODE_API_KEY)", file=sys.stderr)
369
+ _mc_log(f"project '{PROJECT_NAME}' not found (check MESHCODE_KEYCHAIN_PROFILE / MESHCODE_API_KEY)", "error")
340
370
  sys.exit(2)
341
371
 
342
372
  # Resolve project plan for adaptive features (heartbeat interval, etc.)
@@ -350,7 +380,7 @@ except Exception:
350
380
 
351
381
  _register_result = be.register_agent(PROJECT_NAME, AGENT_NAME, AGENT_ROLE or "MCP-connected agent", api_key=_get_api_key())
352
382
  if isinstance(_register_result, dict) and _register_result.get("error"):
353
- print(f"[meshcode-mcp] WARNING: register failed: {_register_result['error']}", file=sys.stderr)
383
+ _mc_log(f" register failed: {_register_result['error']}", "warn")
354
384
 
355
385
  # Flip to online so the dashboard reflects the live MCP session.
356
386
  # Use the SECURITY DEFINER RPC (mc_agent_set_status_by_api_key) so we
@@ -368,7 +398,7 @@ def _flip_status(status: str, task: str = "") -> bool:
368
398
  return False
369
399
 
370
400
  if not _flip_status("idle", ""):
371
- print(f"[meshcode-mcp] WARNING: could not flip status to idle", file=sys.stderr)
401
+ _mc_log(f" could not flip status to idle", "warn")
372
402
 
373
403
 
374
404
  # ============================================================
@@ -530,7 +560,7 @@ def _acquire_lease() -> bool:
530
560
  })
531
561
  except Exception as e:
532
562
  # Non-fatal: RPC might not exist on older servers.
533
- print(f"[meshcode-mcp] stale-lease pre-clean skipped: {e}", file=sys.stderr)
563
+ _mc_log(f"stale-lease pre-clean skipped: {e}", "warn")
534
564
  for attempt in range(3):
535
565
  try:
536
566
  r = be.sb_rpc("mc_acquire_agent_lease", {
@@ -547,11 +577,11 @@ def _acquire_lease() -> bool:
547
577
  if attempt < 2:
548
578
  # The old lease might be stale — wait and retry
549
579
  # (the 90s stale check in the RPC should clear it)
550
- print(f"[meshcode-mcp] Lease held by another instance — retrying in {2 * (attempt+1)}s...", file=sys.stderr)
580
+ _mc_log(f"Lease held by another instance — retrying in {2 * (attempt+1)}s...")
551
581
  _time.sleep(2 * (attempt + 1))
552
582
  continue
553
583
  # Final attempt — force release the old lease and try once more
554
- print(f"[meshcode-mcp] Force-releasing stale lease...", file=sys.stderr)
584
+ _mc_log("Force-releasing stale lease...")
555
585
  try:
556
586
  be.sb_rpc("mc_release_agent_lease", {
557
587
  "p_api_key": api_key,
@@ -575,21 +605,21 @@ def _acquire_lease() -> bool:
575
605
  "p_instance_id": _INSTANCE_ID,
576
606
  })
577
607
  if isinstance(r2, dict) and r2.get("ok"):
578
- print(f"[meshcode-mcp] Lease acquired after force-release.", file=sys.stderr)
608
+ _mc_log("Lease acquired after force-release.")
579
609
  return True
580
610
  except Exception:
581
611
  pass
582
- print(f"[meshcode-mcp] ERROR: Could not start — agent '{AGENT_NAME}' is running in another window.", file=sys.stderr)
583
- print(f"[meshcode-mcp] Close the other window first, or use a different agent name.", file=sys.stderr)
612
+ _mc_log(f"Could not start — agent '{AGENT_NAME}' is running in another window.", "error")
613
+ _mc_log("Close the other window first, or use a different agent name.", "error")
584
614
  return False
585
- print(f"[meshcode-mcp] lease attempt {attempt+1}: {r.get('error')}", file=sys.stderr)
615
+ _mc_log(f"lease attempt {attempt+1}: {r.get('error')}", "warn")
586
616
  else:
587
617
  return True
588
618
  except Exception as e:
589
- print(f"[meshcode-mcp] lease attempt {attempt+1} failed: {e}", file=sys.stderr)
619
+ _mc_log(f"lease attempt {attempt+1} failed: {e}", "warn")
590
620
  if attempt < 2:
591
621
  _time.sleep(2)
592
- print(f"[meshcode-mcp] WARNING: lease failed after 3 attempts — proceeding anyway", file=sys.stderr)
622
+ _mc_log(f" lease failed after 3 attempts — proceeding anyway", "warn")
593
623
  return True
594
624
 
595
625
  if not _acquire_lease():
@@ -607,7 +637,7 @@ def _boot_diagnostic() -> None:
607
637
  be.sb_select("mc_projects", f"id=eq.{_PROJECT_ID}", limit=1)
608
638
  checks_passed += 1
609
639
  except Exception as e:
610
- print(f"[meshcode] BOOT CHECK FAILED: Supabase API unreachable ({e}). Fix: check network/VPN.", file=sys.stderr)
640
+ print(f"[meshcode] BOOT CHECK FAILED: Supabase API unreachable ({e}). Fix: check network/VPN.", "warn")
611
641
 
612
642
  # Check 2: Lease valid
613
643
  try:
@@ -617,11 +647,11 @@ def _boot_diagnostic() -> None:
617
647
  if agent.get("instance_id") == _INSTANCE_ID:
618
648
  checks_passed += 1
619
649
  else:
620
- print(f"[meshcode] BOOT CHECK FAILED: Lease mismatch — expected {_INSTANCE_ID}, got {agent.get('instance_id')}. Fix: restart agent.", file=sys.stderr)
650
+ print(f"[meshcode] BOOT CHECK FAILED: Lease mismatch — expected {_INSTANCE_ID}, got {agent.get('instance_id')}. Fix: restart agent.", "warn")
621
651
  else:
622
- print(f"[meshcode] BOOT CHECK FAILED: Agent '{AGENT_NAME}' not found in project. Fix: register agent first.", file=sys.stderr)
652
+ print(f"[meshcode] BOOT CHECK FAILED: Agent '{AGENT_NAME}' not found in project. Fix: register agent first.", "warn")
623
653
  except Exception as e:
624
- print(f"[meshcode] BOOT CHECK FAILED: Could not verify lease ({e}).", file=sys.stderr)
654
+ print(f"[meshcode] BOOT CHECK FAILED: Could not verify lease ({e}).", "warn")
625
655
 
626
656
  # Check 3: Heartbeat recent
627
657
  try:
@@ -630,7 +660,7 @@ def _boot_diagnostic() -> None:
630
660
  if hb:
631
661
  checks_passed += 1
632
662
  else:
633
- print(f"[meshcode] BOOT CHECK WARNING: No heartbeat recorded yet.", file=sys.stderr)
663
+ print(f"[meshcode] BOOT CHECK WARNING: No heartbeat recorded yet.", "warn")
634
664
  else:
635
665
  checks_passed += 1 # skip if no agent data
636
666
  except Exception:
@@ -647,9 +677,9 @@ def _boot_diagnostic() -> None:
647
677
  checks_passed += 1 # non-critical
648
678
 
649
679
  if checks_passed == checks_total:
650
- print(f"[meshcode] All boot checks passed ({checks_passed}/{checks_total}).", file=sys.stderr)
680
+ print(f"[meshcode] All boot checks passed ({checks_passed}/{checks_total}).", "warn")
651
681
  else:
652
- print(f"[meshcode] Boot checks: {checks_passed}/{checks_total} passed. Agent starting anyway.", file=sys.stderr)
682
+ print(f"[meshcode] Boot checks: {checks_passed}/{checks_total} passed. Agent starting anyway.", "warn")
653
683
 
654
684
 
655
685
  _boot_diagnostic()
@@ -1291,9 +1321,9 @@ try:
1291
1321
  elif isinstance(_ls_val, str):
1292
1322
  _LAST_SEEN_TS = _ls_val
1293
1323
  if _LAST_SEEN_TS:
1294
- print(f"[meshcode] Restored last_seen={_LAST_SEEN_TS} from mesh memory.", file=sys.stderr)
1324
+ print(f"[meshcode] Restored last_seen={_LAST_SEEN_TS} from mesh memory.", "warn")
1295
1325
  except Exception as _e:
1296
- print(f"[meshcode] Could not restore last_seen: {_e}", file=sys.stderr)
1326
+ print(f"[meshcode] Could not restore last_seen: {_e}", "warn")
1297
1327
 
1298
1328
 
1299
1329
  def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
@@ -2406,7 +2436,7 @@ def _auto_update() -> None:
2406
2436
  return
2407
2437
 
2408
2438
  # 3. Install the new version (blocking, 60s timeout)
2409
- print(f"[meshcode] Updating {current} → {latest}...", file=sys.stderr)
2439
+ print(f"[meshcode] Updating {current} → {latest}...", "warn")
2410
2440
  try:
2411
2441
  result = subprocess.run(
2412
2442
  [sys.executable, "-m", "pip", "install", "--upgrade",
@@ -2424,7 +2454,7 @@ def _auto_update() -> None:
2424
2454
  return
2425
2455
 
2426
2456
  # 4. Re-exec to load the new code
2427
- print(f"[meshcode] Updated to {latest}, restarting...", file=sys.stderr)
2457
+ print(f"[meshcode] Updated to {latest}, restarting...", "warn")
2428
2458
  os.environ["MESHCODE_UPDATED"] = "1"
2429
2459
  try:
2430
2460
  os.execv(sys.executable, [sys.executable] + sys.argv)
@@ -32,6 +32,76 @@ WORKSPACES_ROOT = Path.home() / "meshcode"
32
32
  REGISTRY_PATH = WORKSPACES_ROOT / ".registry.json"
33
33
 
34
34
 
35
+ def _fetch_or_generate_art(agent: str, project: str) -> str:
36
+ """Fetch ASCII art from server. If not stored yet, generate and save it."""
37
+ from .ascii_art import generate_art
38
+ try:
39
+ from .setup_clients import _load_supabase_env
40
+ import importlib
41
+ secrets_mod = importlib.import_module("meshcode.secrets")
42
+ except Exception:
43
+ return generate_art(agent)
44
+
45
+ profile = os.environ.get("MESHCODE_KEYCHAIN_PROFILE") or "default"
46
+ api_key = secrets_mod.get_api_key(profile=profile)
47
+ if not api_key:
48
+ return generate_art(agent)
49
+
50
+ sb = _load_supabase_env()
51
+ try:
52
+ from urllib.request import Request, urlopen
53
+ import urllib.parse
54
+ # Step 1: resolve project_id
55
+ proj_req = Request(
56
+ f"{sb['SUPABASE_URL']}/rest/v1/mc_projects?select=id&name=eq.{urllib.parse.quote(project)}",
57
+ headers={
58
+ "apikey": sb["SUPABASE_KEY"],
59
+ "Authorization": f"Bearer {sb['SUPABASE_KEY']}",
60
+ "Accept-Profile": "meshcode",
61
+ },
62
+ )
63
+ with urlopen(proj_req, timeout=5) as resp:
64
+ proj_data = json.loads(resp.read().decode())
65
+ if not proj_data:
66
+ return generate_art(agent)
67
+ project_id = proj_data[0]["id"]
68
+ # Step 2: fetch existing art
69
+ req = Request(
70
+ f"{sb['SUPABASE_URL']}/rest/v1/mc_agents?select=ascii_art&name=eq.{urllib.parse.quote(agent)}&project_id=eq.{project_id}",
71
+ headers={
72
+ "apikey": sb["SUPABASE_KEY"],
73
+ "Authorization": f"Bearer {sb['SUPABASE_KEY']}",
74
+ "Accept-Profile": "meshcode",
75
+ },
76
+ )
77
+ with urlopen(req, timeout=5) as resp:
78
+ data = json.loads(resp.read().decode())
79
+ if data and data[0].get("ascii_art"):
80
+ return data[0]["ascii_art"]
81
+ except Exception:
82
+ pass
83
+
84
+ # Generate and store
85
+ art = generate_art(agent)
86
+ try:
87
+ body = json.dumps({"p_project_name": project, "p_agent_name": agent, "p_ascii_art": art})
88
+ req = Request(
89
+ f"{sb['SUPABASE_URL']}/rest/v1/rpc/mc_store_agent_art",
90
+ data=body.encode(),
91
+ method="POST",
92
+ headers={
93
+ "apikey": sb["SUPABASE_KEY"],
94
+ "Authorization": f"Bearer {sb['SUPABASE_KEY']}",
95
+ "Content-Type": "application/json",
96
+ "Content-Profile": "meshcode",
97
+ },
98
+ )
99
+ urlopen(req, timeout=5)
100
+ except Exception:
101
+ pass
102
+ return art
103
+
104
+
35
105
  def _check_agent_ownership(agent: str, project: str) -> Optional[str]:
36
106
  """Pre-flight check: verify caller owns this agent before launching editor.
37
107
 
@@ -266,6 +336,18 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
266
336
  print("[meshcode] Or open the workspace manually with the editor of your choice.", file=sys.stderr)
267
337
  return 2
268
338
 
339
+ # ── Welcome banner with unique ASCII art ───────────────────────
340
+ try:
341
+ from .ascii_art import generate_art, render_welcome
342
+ from . import __version__ as cli_version
343
+ # Try to fetch stored art from server, generate if missing
344
+ ascii_art = _fetch_or_generate_art(agent, resolved_project)
345
+ # Check if this agent is the commander
346
+ is_cmd = "commander" in agent.lower()
347
+ print(render_welcome(agent, resolved_project, ascii_art, cli_version, is_commander=is_cmd))
348
+ except Exception:
349
+ pass # Non-critical — skip banner on error
350
+
269
351
  print(f"[meshcode] Launching {editor} for agent '{agent}' (project: {resolved_project})")
270
352
  print(f"[meshcode] Workspace: {ws}")
271
353
  print(f"[meshcode] MCP server: {server_id}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.8.6
3
+ Version: 2.8.8
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,6 +1,7 @@
1
1
  README.md
2
2
  pyproject.toml
3
3
  meshcode/__init__.py
4
+ meshcode/ascii_art.py
4
5
  meshcode/cli.py
5
6
  meshcode/comms_v4.py
6
7
  meshcode/invites.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.8.6"
7
+ version = "2.8.8"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes