meshcode 2.10.65__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.
- {meshcode-2.10.65 → meshcode-2.10.70}/PKG-INFO +1 -1
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/__init__.py +1 -1
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/ascii_art.py +135 -2
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/comms_v4.py +69 -8
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/meshcode_mcp/server.py +120 -1
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/run_agent.py +16 -11
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.65 → meshcode-2.10.70}/pyproject.toml +1 -1
- {meshcode-2.10.65 → meshcode-2.10.70}/README.md +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/cli.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/compat.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/error_hints.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/exceptions.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/invites.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/launcher.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/preferences.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/quickstart.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/secrets.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/self_update.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/supervisor.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode/upload.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/setup.cfg +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/tests/test_core.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/tests/test_exceptions.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/tests/test_security_regressions.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/tests/test_sentinel.py +0 -0
- {meshcode-2.10.65 → meshcode-2.10.70}/tests/test_status_enum_coverage.py +0 -0
|
@@ -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
|
|
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
|
-
|
|
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("")
|
|
@@ -1171,7 +1171,8 @@ def send_msg(project, from_agent, to_agent, content, msg_type="msg", compact=Fal
|
|
|
1171
1171
|
if agents and agents[0].get("status") not in ("offline", "killed"):
|
|
1172
1172
|
nudge_agent(project, to_agent, from_agent)
|
|
1173
1173
|
else:
|
|
1174
|
-
print(f"[
|
|
1174
|
+
print(f"[meshcode] ERROR: Could not send message to {project}")
|
|
1175
|
+
print(f"[meshcode] Fix: Check project name with: meshcode projects")
|
|
1175
1176
|
|
|
1176
1177
|
|
|
1177
1178
|
def broadcast(project, from_agent, content, msg_type="broadcast"):
|
|
@@ -1200,7 +1201,7 @@ def read_messages(project, name, silent=False, send_acks=True):
|
|
|
1200
1201
|
project_id = get_project_id(project)
|
|
1201
1202
|
if not project_id:
|
|
1202
1203
|
if not silent:
|
|
1203
|
-
print("
|
|
1204
|
+
print("No new messages.")
|
|
1204
1205
|
return []
|
|
1205
1206
|
|
|
1206
1207
|
# Get unread messages
|
|
@@ -1210,7 +1211,7 @@ def read_messages(project, name, silent=False, send_acks=True):
|
|
|
1210
1211
|
|
|
1211
1212
|
if not messages:
|
|
1212
1213
|
if not silent:
|
|
1213
|
-
print("
|
|
1214
|
+
print("No new messages.")
|
|
1214
1215
|
return []
|
|
1215
1216
|
|
|
1216
1217
|
# Mark all as read
|
|
@@ -1463,14 +1464,36 @@ def show_board(project):
|
|
|
1463
1464
|
print(f"[{project}] No agents found")
|
|
1464
1465
|
return
|
|
1465
1466
|
|
|
1467
|
+
def _human_time(ts_str):
|
|
1468
|
+
"""Convert ISO timestamp to human-readable age."""
|
|
1469
|
+
if not ts_str:
|
|
1470
|
+
return ""
|
|
1471
|
+
try:
|
|
1472
|
+
from datetime import datetime, timezone
|
|
1473
|
+
dt = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
|
|
1474
|
+
age = (datetime.now(timezone.utc) - dt).total_seconds()
|
|
1475
|
+
if age < 60: return f"{int(age)}s ago"
|
|
1476
|
+
if age < 3600: return f"{int(age/60)}m ago"
|
|
1477
|
+
if age < 86400: return f"{int(age/3600)}h ago"
|
|
1478
|
+
return f"{int(age/86400)}d ago"
|
|
1479
|
+
except Exception:
|
|
1480
|
+
return ""
|
|
1481
|
+
|
|
1466
1482
|
print(f"\n{'='*60}")
|
|
1467
1483
|
print(f" {project.upper()} — Board")
|
|
1468
1484
|
print(f"{'='*60}")
|
|
1469
1485
|
for a in agents:
|
|
1486
|
+
status = a.get("status", "?")
|
|
1470
1487
|
icon = {"online": "●", "working": "▶", "blocked": "■", "standby": "◆",
|
|
1471
1488
|
"idle": "○", "done": "✓", "sleeping": "☾", "offline": "✗",
|
|
1472
|
-
"waiting": "◎"}.get(
|
|
1473
|
-
|
|
1489
|
+
"waiting": "◎"}.get(status, "?")
|
|
1490
|
+
task = a.get("task", "")
|
|
1491
|
+
# Clean up noisy task text
|
|
1492
|
+
if task and "idle" in task.lower() and "s —" in task:
|
|
1493
|
+
task = "" # hide "idle 29720s — still listening"
|
|
1494
|
+
hb_age = _human_time(a.get("last_heartbeat"))
|
|
1495
|
+
hb_info = f" ({hb_age})" if hb_age and status in ("offline", "sleeping") else ""
|
|
1496
|
+
print(f" {icon} {a['name']:<14} {status:<10} {task[:40]}{hb_info}")
|
|
1474
1497
|
|
|
1475
1498
|
# Enhanced: show health + tasks + activity from dashboard summary
|
|
1476
1499
|
if _ak:
|
|
@@ -2996,9 +3019,47 @@ if __name__ == "__main__":
|
|
|
2996
3019
|
# name exists in multiple meshworks
|
|
2997
3020
|
# meshcode run <agent> (backwards compat, same as go)
|
|
2998
3021
|
if len(sys.argv) < 3:
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3022
|
+
# Interactive agent selection
|
|
3023
|
+
_ak = _load_api_key_for_cli()
|
|
3024
|
+
if _ak:
|
|
3025
|
+
try:
|
|
3026
|
+
_projs = sb_rpc("mc_list_user_projects", {"p_api_key": _ak})
|
|
3027
|
+
_agents_list = []
|
|
3028
|
+
if isinstance(_projs, dict) and _projs.get("projects"):
|
|
3029
|
+
for _p in _projs["projects"]:
|
|
3030
|
+
for _a in (_p.get("agents") or []):
|
|
3031
|
+
_agents_list.append((_p["name"], _a.get("name", "?")))
|
|
3032
|
+
if _agents_list:
|
|
3033
|
+
print(f"\n Your agents:")
|
|
3034
|
+
for i, (pn, an) in enumerate(_agents_list, 1):
|
|
3035
|
+
print(f" {i}. {pn}/{an}")
|
|
3036
|
+
print()
|
|
3037
|
+
choice = input(" Which agent? (number or name): ").strip()
|
|
3038
|
+
if choice.isdigit() and 1 <= int(choice) <= len(_agents_list):
|
|
3039
|
+
pn, an = _agents_list[int(choice) - 1]
|
|
3040
|
+
sys.argv = [sys.argv[0], cmd, f"{pn}/{an}"] + sys.argv[2:]
|
|
3041
|
+
elif "/" in choice:
|
|
3042
|
+
sys.argv = [sys.argv[0], cmd, choice] + sys.argv[2:]
|
|
3043
|
+
else:
|
|
3044
|
+
# Try to find by name
|
|
3045
|
+
matches = [(pn, an) for pn, an in _agents_list if an == choice]
|
|
3046
|
+
if matches:
|
|
3047
|
+
pn, an = matches[0]
|
|
3048
|
+
sys.argv = [sys.argv[0], cmd, f"{pn}/{an}"] + sys.argv[2:]
|
|
3049
|
+
else:
|
|
3050
|
+
print(f" Agent '{choice}' not found.")
|
|
3051
|
+
sys.exit(1)
|
|
3052
|
+
else:
|
|
3053
|
+
print(f" No agents found. Run: meshcode quickstart")
|
|
3054
|
+
sys.exit(1)
|
|
3055
|
+
except Exception:
|
|
3056
|
+
print(f"Usage: meshcode {cmd} <agent> [--project <name>]")
|
|
3057
|
+
sys.exit(1)
|
|
3058
|
+
else:
|
|
3059
|
+
print(f"Usage: meshcode {cmd} <agent> [--project <name>] [--editor claude|cursor|code]")
|
|
3060
|
+
print(f" or: meshcode {cmd} <project>/<agent> (disambiguation)")
|
|
3061
|
+
print(f"\n Not logged in? Run: meshcode quickstart")
|
|
3062
|
+
sys.exit(1)
|
|
3002
3063
|
agent_arg = sys.argv[2]
|
|
3003
3064
|
# Parse project/agent syntax
|
|
3004
3065
|
if "/" in agent_arg and not agent_arg.startswith("/"):
|
|
@@ -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
|
-
#
|
|
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
|
|
133
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|