meshcode 2.10.66__tar.gz → 2.10.70__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 (49) hide show
  1. {meshcode-2.10.66 → meshcode-2.10.70}/PKG-INFO +1 -1
  2. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/__init__.py +1 -1
  3. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/ascii_art.py +135 -2
  4. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/meshcode_mcp/server.py +120 -1
  5. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/run_agent.py +16 -11
  6. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode.egg-info/PKG-INFO +1 -1
  7. {meshcode-2.10.66 → meshcode-2.10.70}/pyproject.toml +1 -1
  8. {meshcode-2.10.66 → meshcode-2.10.70}/README.md +0 -0
  9. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/cli.py +0 -0
  10. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/comms_v4.py +0 -0
  11. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/compat.py +0 -0
  12. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/error_hints.py +0 -0
  13. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/exceptions.py +0 -0
  14. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/invites.py +0 -0
  15. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/launcher.py +0 -0
  16. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/launcher_install.py +0 -0
  17. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/meshcode_mcp/__init__.py +0 -0
  18. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/meshcode_mcp/__main__.py +0 -0
  19. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/meshcode_mcp/backend.py +0 -0
  20. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/meshcode_mcp/realtime.py +0 -0
  21. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/meshcode_mcp/test_backend.py +0 -0
  22. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  23. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  24. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/preferences.py +0 -0
  25. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/protocol_v2.py +0 -0
  26. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/quickstart.py +0 -0
  27. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/secrets.py +0 -0
  28. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/self_update.py +0 -0
  29. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/setup_clients.py +0 -0
  30. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/supervisor.py +0 -0
  31. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode/upload.py +0 -0
  32. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode.egg-info/SOURCES.txt +0 -0
  33. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode.egg-info/dependency_links.txt +0 -0
  34. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode.egg-info/entry_points.txt +0 -0
  35. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode.egg-info/requires.txt +0 -0
  36. {meshcode-2.10.66 → meshcode-2.10.70}/meshcode.egg-info/top_level.txt +0 -0
  37. {meshcode-2.10.66 → meshcode-2.10.70}/setup.cfg +0 -0
  38. {meshcode-2.10.66 → meshcode-2.10.70}/tests/test_core.py +0 -0
  39. {meshcode-2.10.66 → meshcode-2.10.70}/tests/test_cross_agent_messaging.py +0 -0
  40. {meshcode-2.10.66 → meshcode-2.10.70}/tests/test_esc_deaf_state.py +0 -0
  41. {meshcode-2.10.66 → meshcode-2.10.70}/tests/test_exceptions.py +0 -0
  42. {meshcode-2.10.66 → meshcode-2.10.70}/tests/test_mark_read_batch.py +0 -0
  43. {meshcode-2.10.66 → meshcode-2.10.70}/tests/test_migration_integrity.py +0 -0
  44. {meshcode-2.10.66 → meshcode-2.10.70}/tests/test_realtime_event_freshness.py +0 -0
  45. {meshcode-2.10.66 → meshcode-2.10.70}/tests/test_rls_cross_tenant.py +0 -0
  46. {meshcode-2.10.66 → meshcode-2.10.70}/tests/test_rpc_migrations.py +0 -0
  47. {meshcode-2.10.66 → meshcode-2.10.70}/tests/test_security_regressions.py +0 -0
  48. {meshcode-2.10.66 → meshcode-2.10.70}/tests/test_sentinel.py +0 -0
  49. {meshcode-2.10.66 → meshcode-2.10.70}/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.66
3
+ Version: 2.10.70
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,5 +1,5 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.10.66"
2
+ __version__ = "2.10.70"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -148,6 +148,135 @@ def generate_art(agent_name: str, meshwork_name: str = "", size: int = 7) -> str
148
148
  return "\n".join(lines)
149
149
 
150
150
 
151
+ # ── Pixel Mascot renderer (Unicode block art) ──────────────────────
152
+ # Renders a small pixel character using Unicode half-block chars.
153
+ # Deterministic from agent name hash, like the identicon but cuter.
154
+
155
+ _MASCOT_BODY_TEMPLATES = {
156
+ "round": [
157
+ " ████ ",
158
+ " ██████ ",
159
+ "████████",
160
+ "████████",
161
+ " ██████ ",
162
+ " ████ ",
163
+ ],
164
+ "square": [
165
+ "████████",
166
+ "██ ██",
167
+ "██ ██",
168
+ "██ ██",
169
+ "██ ██",
170
+ "████████",
171
+ ],
172
+ "tall": [
173
+ " ████ ",
174
+ " ██████ ",
175
+ " ██████ ",
176
+ " ██████ ",
177
+ " ██████ ",
178
+ " ████ ",
179
+ " ████ ",
180
+ " ████ ",
181
+ ],
182
+ "wide": [
183
+ "██████████",
184
+ "██ ██",
185
+ "██████████",
186
+ " ████████ ",
187
+ ],
188
+ "triangle": [
189
+ " ██ ",
190
+ " ████ ",
191
+ " ██████ ",
192
+ "████████",
193
+ "████████",
194
+ " ██ ██ ",
195
+ ],
196
+ }
197
+
198
+ _MASCOT_EYES = {
199
+ "dots": ("●", "●"),
200
+ "circles": ("◉", "◉"),
201
+ "angry": ("▼", "▼"),
202
+ "happy": ("◠", "◠"),
203
+ "sleepy": ("—", "—"),
204
+ "star": ("★", "★"),
205
+ }
206
+
207
+ _MASCOT_HATS = {
208
+ "none": [],
209
+ "hat": [" ▄▄ ", " ████ "],
210
+ "glasses": [], # applied as eye overlay
211
+ "antenna": [" | ", " (◉) "],
212
+ "headphones": [" ╔══╗ "],
213
+ "crown": [" ▲ ▲ ▲ ", " ████ "],
214
+ }
215
+
216
+
217
+ def render_pixel_mascot(agent_name: str, mascot_config: dict = None) -> str:
218
+ """Render a pixel mascot as Unicode art for terminal display.
219
+
220
+ If mascot_config is None, generates deterministic defaults from agent name.
221
+ """
222
+ h = _hash_bytes(agent_name)
223
+ hi = _hash_int(agent_name)
224
+
225
+ if mascot_config is None or mascot_config.get("generated"):
226
+ body_types = list(_MASCOT_BODY_TEMPLATES.keys())
227
+ eye_styles = list(_MASCOT_EYES.keys())
228
+ body_type = body_types[hi % len(body_types)]
229
+ eye_style = eye_styles[(hi // 10) % len(eye_styles)]
230
+ accessory = "none"
231
+ equipped = []
232
+ else:
233
+ body_type = mascot_config.get("body_type", "round")
234
+ eye_style = mascot_config.get("eye_style", "dots")
235
+ accessory = mascot_config.get("accessory", "none")
236
+ equipped = mascot_config.get("equipped", [])
237
+
238
+ # Get body template
239
+ body = list(_MASCOT_BODY_TEMPLATES.get(body_type, _MASCOT_BODY_TEMPLATES["round"]))
240
+
241
+ # Add eyes (on the 2nd or 3rd row depending on body)
242
+ eye_row = min(1, len(body) - 1)
243
+ if len(body) > 3:
244
+ eye_row = 2
245
+ eyes = _MASCOT_EYES.get(eye_style, _MASCOT_EYES["dots"])
246
+ if eye_row < len(body):
247
+ row = list(body[eye_row])
248
+ # Place eyes at 1/3 and 2/3 positions
249
+ w = len(row)
250
+ left_eye = w // 3
251
+ right_eye = 2 * w // 3
252
+ if left_eye < w:
253
+ row[left_eye] = eyes[0]
254
+ if right_eye < w:
255
+ row[right_eye] = eyes[1]
256
+ body[eye_row] = "".join(row)
257
+
258
+ # Add hat/accessory on top
259
+ hat_lines = _MASCOT_HATS.get(accessory, [])
260
+ # Check equipped accessories for hat/crown
261
+ for item in equipped:
262
+ if "crown" in item:
263
+ hat_lines = _MASCOT_HATS.get("crown", [])
264
+ break
265
+ elif "hat" in item:
266
+ hat_lines = _MASCOT_HATS.get("hat", [])
267
+ break
268
+ elif "antenna" in item:
269
+ hat_lines = _MASCOT_HATS.get("antenna", [])
270
+ break
271
+
272
+ lines = hat_lines + body
273
+
274
+ # Add feet
275
+ lines.append(" ▀ ▀ ")
276
+
277
+ return "\n".join(f" {line}" for line in lines)
278
+
279
+
151
280
  # ── ANSI colors ─────────────────────────────────────────────────────
152
281
  COLORS = [
153
282
  "\033[36m", "\033[35m", "\033[32m", "\033[33m", "\033[34m",
@@ -441,7 +570,8 @@ def get_tip(agent_name: str) -> str:
441
570
  def render_welcome(agent_name: str, meshwork_name: str, ascii_art: str,
442
571
  version: str = "", is_commander: bool = False,
443
572
  role: str = "", stats: dict = None,
444
- profile_color: str = None) -> str:
573
+ profile_color: str = None,
574
+ mascot_config: dict = None) -> str:
445
575
  h = _hash_bytes(agent_name)
446
576
  # Use dashboard profile color if available, otherwise MD5 hash fallback
447
577
  color = None
@@ -465,7 +595,10 @@ def render_welcome(agent_name: str, meshwork_name: str, ascii_art: str,
465
595
  lines.append(f"{color}{BOLD} ╚══════════════════════════════════════════╝{RESET}")
466
596
  lines.append("")
467
597
 
468
- for line in ascii_art.split("\n"):
598
+ # Always use pixel mascot — generates deterministic defaults from agent name if no config
599
+ art_to_render = render_pixel_mascot(agent_name, mascot_config)
600
+
601
+ for line in art_to_render.split("\n"):
469
602
  lines.append(f" {color}{line}{RESET}")
470
603
 
471
604
  lines.append("")
@@ -1678,8 +1678,17 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1678
1678
  else:
1679
1679
  payload = {"text": str(message)}
1680
1680
 
1681
- # Enforce message size limit long content belongs in task descriptions
1681
+ # Universal DM render guarantee: dashboard chat renderer keys on
1682
+ # payload.text. Structured-dict DMs without a text field render blank.
1682
1683
  import json as _json
1684
+ if "text" not in payload:
1685
+ _synth = _json.dumps(
1686
+ {k: v for k, v in payload.items() if not k.startswith("_")},
1687
+ default=str, ensure_ascii=False,
1688
+ )
1689
+ payload["text"] = _synth if len(_synth) <= 280 else _synth[:279] + "…"
1690
+
1691
+ # Enforce message size limit — long content belongs in task descriptions
1683
1692
  _payload_len = len(_json.dumps(payload, default=str))
1684
1693
  if _payload_len > 2000:
1685
1694
  return {"error": f"message too large ({_payload_len} chars). Use meshcode_task_create for long content. Messages must be structured JSON <2000 chars."}
@@ -1899,6 +1908,116 @@ def meshcode_read_message(msg_id: str) -> Dict[str, Any]:
1899
1908
  }
1900
1909
 
1901
1910
 
1911
+ @mcp.tool()
1912
+ @with_working_status
1913
+ def meshcode_download_file(file_id: str) -> Dict[str, Any]:
1914
+ """Download a file attachment from a mesh message.
1915
+
1916
+ Returns file content for text/JSON files, or saves to temp file and
1917
+ returns the path for images (Claude can view image files directly).
1918
+
1919
+ Args:
1920
+ file_id: UUID of the file (from message payload's file.file_id).
1921
+ """
1922
+ import tempfile
1923
+ import urllib.request as _req
1924
+ import urllib.error as _uerr
1925
+
1926
+ api_key = _get_api_key()
1927
+ if not api_key:
1928
+ return {"error": "no api key", "error_code": "auth_failed"}
1929
+
1930
+ # Step 1: Get file metadata via RPC
1931
+ file_info = be.sb_rpc("mc_get_file_download", {
1932
+ "p_api_key": api_key,
1933
+ "p_file_id": file_id,
1934
+ })
1935
+ if not file_info or not file_info.get("ok"):
1936
+ return {"error": file_info.get("error", "file not found"), "error_code": "not_found"}
1937
+
1938
+ storage_path = file_info["storage_path"]
1939
+ bucket = file_info.get("bucket", "meshcode-files")
1940
+ mime_type = file_info.get("mime_type", "application/octet-stream")
1941
+ file_name = file_info.get("file_name", "download")
1942
+
1943
+ # Step 2: Download from Supabase Storage (using service-level anon key)
1944
+ sb_url = os.environ.get("SUPABASE_URL", be._sb_url if hasattr(be, '_sb_url') else "")
1945
+ sb_key = os.environ.get("SUPABASE_KEY", be._sb_key if hasattr(be, '_sb_key') else "")
1946
+
1947
+ if not sb_url or not sb_key:
1948
+ return {"error": "storage not configured", "error_code": "config_error"}
1949
+
1950
+ # URL-encode each path segment so filenames with spaces/unicode (the common
1951
+ # 400 cause when dashboard composer attaches files like "Screenshot 1.png")
1952
+ # don't produce a malformed Storage URL.
1953
+ from urllib.parse import quote as _quote
1954
+ _enc_path = "/".join(_quote(seg, safe="") for seg in storage_path.split("/") if seg)
1955
+ _enc_bucket = _quote(bucket, safe="")
1956
+
1957
+ def _try_download(url: str):
1958
+ req = _req.Request(
1959
+ url,
1960
+ headers={
1961
+ "apikey": sb_key,
1962
+ "Authorization": f"Bearer {sb_key}",
1963
+ },
1964
+ )
1965
+ with _req.urlopen(req, timeout=30) as resp:
1966
+ return resp.read()
1967
+
1968
+ # Try authenticated object endpoint first; fall back to public endpoint
1969
+ # for buckets that have public read enabled. Many 400/403 cases on private
1970
+ # buckets resolve when the bucket is configured as public-readable.
1971
+ auth_url = f"{sb_url}/storage/v1/object/{_enc_bucket}/{_enc_path}"
1972
+ public_url = f"{sb_url}/storage/v1/object/public/{_enc_bucket}/{_enc_path}"
1973
+ last_err = None
1974
+ content = None
1975
+ for _url in (auth_url, public_url):
1976
+ try:
1977
+ content = _try_download(_url)
1978
+ break
1979
+ except _uerr.HTTPError as e:
1980
+ last_err = f"HTTP {e.code} on {_url}: {e.reason}"
1981
+ continue
1982
+ except Exception as e:
1983
+ last_err = f"{type(e).__name__} on {_url}: {e}"
1984
+ continue
1985
+ if content is None:
1986
+ return {
1987
+ "error": f"download failed: {last_err}",
1988
+ "error_code": "download_error",
1989
+ "tried_urls": [auth_url, public_url],
1990
+ "storage_path": storage_path,
1991
+ }
1992
+
1993
+ # Step 3: Return content based on type
1994
+ if mime_type.startswith("text/") or mime_type == "application/json":
1995
+ try:
1996
+ text = content.decode("utf-8")
1997
+ if mime_type == "application/json":
1998
+ return {"ok": True, "file_name": file_name, "mime_type": mime_type,
1999
+ "content": json.loads(text)}
2000
+ return {"ok": True, "file_name": file_name, "mime_type": mime_type,
2001
+ "content": text[:50000]} # cap at 50k chars
2002
+ except Exception:
2003
+ pass
2004
+
2005
+ # For images and binary: save to temp file, return path
2006
+ suffix = "." + file_name.rsplit(".", 1)[-1] if "." in file_name else ""
2007
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix, prefix="meshcode_")
2008
+ tmp.write(content)
2009
+ tmp.close()
2010
+
2011
+ return {
2012
+ "ok": True,
2013
+ "file_name": file_name,
2014
+ "mime_type": mime_type,
2015
+ "size_bytes": len(content),
2016
+ "local_path": tmp.name,
2017
+ "note": "File saved to local_path. For images, use Read tool to view.",
2018
+ }
2019
+
2020
+
1902
2021
  def _detect_global_done(messages: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
1903
2022
  """Return {reason, from} if the list contains a global_done signal, else None."""
1904
2023
  for m in messages:
@@ -33,23 +33,24 @@ REGISTRY_PATH = WORKSPACES_ROOT / ".registry.json"
33
33
 
34
34
 
35
35
  def _fetch_or_generate_art(agent: str, project: str) -> tuple:
36
- """Fetch ASCII art + role + profile color from server.
37
- Returns (ascii_art, role_description, profile_color)."""
36
+ """Fetch ASCII art + role + profile color + mascot config from server.
37
+ Returns (ascii_art, role_description, profile_color, mascot_config)."""
38
38
  from .ascii_art import generate_art
39
39
  try:
40
40
  from .setup_clients import _load_supabase_env
41
41
  import importlib
42
42
  secrets_mod = importlib.import_module("meshcode.secrets")
43
43
  except Exception:
44
- return generate_art(agent), agent, None
44
+ return generate_art(agent), agent, None, None
45
45
 
46
46
  profile = os.environ.get("MESHCODE_KEYCHAIN_PROFILE") or "default"
47
47
  api_key = secrets_mod.get_api_key(profile=profile)
48
48
  if not api_key:
49
- return generate_art(agent), agent, None
49
+ return generate_art(agent), agent, None, None
50
50
 
51
51
  sb = _load_supabase_env()
52
52
  profile_color = None
53
+ mascot_config = None
53
54
  try:
54
55
  from urllib.request import Request, urlopen
55
56
  import urllib.parse
@@ -69,7 +70,7 @@ def _fetch_or_generate_art(agent: str, project: str) -> tuple:
69
70
  with urlopen(proj_req, timeout=5) as resp:
70
71
  proj_data = json.loads(resp.read().decode())
71
72
  if not proj_data or not proj_data.get("project_id"):
72
- return generate_art(agent), agent, None
73
+ return generate_art(agent), agent, None, None
73
74
  project_id = proj_data["project_id"]
74
75
  # Step 2: fetch existing art + role via mc_get_agents RPC
75
76
  data = None
@@ -120,7 +121,7 @@ def _fetch_or_generate_art(agent: str, project: str) -> tuple:
120
121
  if agent_id:
121
122
  try:
122
123
  prof_req = Request(
123
- f"{sb['SUPABASE_URL']}/rest/v1/mc_agent_profiles?select=color&agent_id=eq.{agent_id}",
124
+ f"{sb['SUPABASE_URL']}/rest/v1/mc_agent_profiles?select=color,mascot_config&agent_id=eq.{agent_id}",
124
125
  headers={
125
126
  "apikey": sb["SUPABASE_KEY"],
126
127
  "Authorization": f"Bearer {sb['SUPABASE_KEY']}",
@@ -129,12 +130,15 @@ def _fetch_or_generate_art(agent: str, project: str) -> tuple:
129
130
  )
130
131
  with urlopen(prof_req, timeout=5) as resp:
131
132
  prof_data = json.loads(resp.read().decode())
132
- if prof_data and prof_data[0].get("color"):
133
- profile_color = prof_data[0]["color"]
133
+ if prof_data:
134
+ if prof_data[0].get("color"):
135
+ profile_color = prof_data[0]["color"]
136
+ if prof_data[0].get("mascot_config"):
137
+ mascot_config = prof_data[0]["mascot_config"]
134
138
  except Exception:
135
139
  pass
136
140
  if data[0].get("ascii_art"):
137
- return data[0]["ascii_art"], data[0].get("role") or agent, profile_color
141
+ return data[0]["ascii_art"], data[0].get("role") or agent, profile_color, mascot_config
138
142
  except Exception:
139
143
  pass
140
144
 
@@ -156,7 +160,7 @@ def _fetch_or_generate_art(agent: str, project: str) -> tuple:
156
160
  urlopen(req, timeout=5)
157
161
  except Exception:
158
162
  pass
159
- return art, agent, profile_color
163
+ return art, agent, profile_color, mascot_config
160
164
 
161
165
 
162
166
  def _fetch_agent_stats(agent: str, project: str) -> dict:
@@ -784,7 +788,7 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
784
788
  break
785
789
  except Exception:
786
790
  pass
787
- ascii_art, agent_role, profile_color = _fetch_or_generate_art(agent, resolved_project)
791
+ ascii_art, agent_role, profile_color, mascot_cfg = _fetch_or_generate_art(agent, resolved_project)
788
792
  _leader_haystack = (agent + ' ' + agent_role).lower()
789
793
  _LEADER_KW = ('commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
790
794
  'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
@@ -795,6 +799,7 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
795
799
  agent, resolved_project, ascii_art, cli_version,
796
800
  is_commander=is_cmd, role=agent_role, stats=agent_stats,
797
801
  profile_color=profile_color,
802
+ mascot_config=mascot_cfg,
798
803
  ), file=sys.stderr, flush=True)
799
804
  except Exception as _banner_err:
800
805
  import traceback as _tb
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.66
3
+ Version: 2.10.70
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.10.66"
7
+ version = "2.10.70"
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