meshcode 2.10.40__tar.gz → 2.10.41__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.40 → meshcode-2.10.41}/PKG-INFO +25 -3
- {meshcode-2.10.40 → meshcode-2.10.41}/README.md +24 -2
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/__init__.py +1 -1
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/comms_v4.py +184 -41
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/meshcode_mcp/backend.py +45 -3
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/meshcode_mcp/server.py +23 -12
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/run_agent.py +3 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/setup_clients.py +4 -1
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode.egg-info/PKG-INFO +25 -3
- {meshcode-2.10.40 → meshcode-2.10.41}/pyproject.toml +1 -1
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/cli.py +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/invites.py +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/launcher.py +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/preferences.py +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/secrets.py +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode/self_update.py +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/setup.cfg +0 -0
- {meshcode-2.10.40 → meshcode-2.10.41}/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.
|
|
3
|
+
Version: 2.10.41
|
|
4
4
|
Summary: Real-time communication between AI agents — Supabase-backed CLI
|
|
5
5
|
Author-email: MeshCode <hello@meshcode.io>
|
|
6
6
|
License: MIT
|
|
@@ -400,10 +400,32 @@ meshcode projects # lists all your meshworks
|
|
|
400
400
|
**8. Unknown command (e.g., `meshcode list`)**
|
|
401
401
|
Since v2.0.2, the CLI suggests the closest match instead of dumping the full help text. Common aliases: `list` and `ls` both work for `projects`.
|
|
402
402
|
|
|
403
|
-
**
|
|
403
|
+
**9. Windows: agent registers but never appears online**
|
|
404
|
+
On Windows, especially with Python 3.14+, you may see `ImportError` crashes or agents that register but stay offline. This was fixed in v2.10.40. Upgrade first:
|
|
405
|
+
```bash
|
|
406
|
+
pip install meshcode --upgrade
|
|
407
|
+
```
|
|
408
|
+
If the agent still fails after upgrading, check that your `.mcp.json` command path is valid. On Windows, the MCP config should use `python` (not `python3`, which doesn't exist on Windows):
|
|
409
|
+
```json
|
|
410
|
+
{
|
|
411
|
+
"command": "python",
|
|
412
|
+
"args": ["-m", "meshcode_mcp", "serve"]
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
If `meshcode setup` was run from a conda/venv environment that no longer exists, the baked path in `.mcp.json` will be stale. Re-run `meshcode setup <project> <agent>` from your current Python environment to fix it.
|
|
416
|
+
|
|
417
|
+
As a fast-path workaround, you can set `MESHCODE_PROJECT_ID` as an environment variable to bypass the project name lookup at boot (which can fail on Windows due to RLS visibility):
|
|
418
|
+
```bash
|
|
419
|
+
set MESHCODE_PROJECT_ID=your-project-uuid # Windows cmd
|
|
420
|
+
$env:MESHCODE_PROJECT_ID="your-project-uuid" # Windows PowerShell
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
You can also run `meshcode doctor` (v2.10.41+) to diagnose stale paths, missing dependencies, and config issues across all your workspaces.
|
|
424
|
+
|
|
425
|
+
**10. `MCP server failed to start` in the Claude Code `/mcp` panel**
|
|
404
426
|
Run `claude --debug` to see the underlying error. Nine times out of ten it's a stale or missing key — run `meshcode login mc_xxx` again.
|
|
405
427
|
|
|
406
|
-
**
|
|
428
|
+
**11. PyPI says `Could not find version 1.x.x` immediately after a publish**
|
|
407
429
|
CDN propagation. Wait ~60s and force-fetch directly from the origin:
|
|
408
430
|
```bash
|
|
409
431
|
pip install --no-cache-dir -i https://pypi.org/simple/ meshcode
|
|
@@ -374,10 +374,32 @@ meshcode projects # lists all your meshworks
|
|
|
374
374
|
**8. Unknown command (e.g., `meshcode list`)**
|
|
375
375
|
Since v2.0.2, the CLI suggests the closest match instead of dumping the full help text. Common aliases: `list` and `ls` both work for `projects`.
|
|
376
376
|
|
|
377
|
-
**
|
|
377
|
+
**9. Windows: agent registers but never appears online**
|
|
378
|
+
On Windows, especially with Python 3.14+, you may see `ImportError` crashes or agents that register but stay offline. This was fixed in v2.10.40. Upgrade first:
|
|
379
|
+
```bash
|
|
380
|
+
pip install meshcode --upgrade
|
|
381
|
+
```
|
|
382
|
+
If the agent still fails after upgrading, check that your `.mcp.json` command path is valid. On Windows, the MCP config should use `python` (not `python3`, which doesn't exist on Windows):
|
|
383
|
+
```json
|
|
384
|
+
{
|
|
385
|
+
"command": "python",
|
|
386
|
+
"args": ["-m", "meshcode_mcp", "serve"]
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
If `meshcode setup` was run from a conda/venv environment that no longer exists, the baked path in `.mcp.json` will be stale. Re-run `meshcode setup <project> <agent>` from your current Python environment to fix it.
|
|
390
|
+
|
|
391
|
+
As a fast-path workaround, you can set `MESHCODE_PROJECT_ID` as an environment variable to bypass the project name lookup at boot (which can fail on Windows due to RLS visibility):
|
|
392
|
+
```bash
|
|
393
|
+
set MESHCODE_PROJECT_ID=your-project-uuid # Windows cmd
|
|
394
|
+
$env:MESHCODE_PROJECT_ID="your-project-uuid" # Windows PowerShell
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
You can also run `meshcode doctor` (v2.10.41+) to diagnose stale paths, missing dependencies, and config issues across all your workspaces.
|
|
398
|
+
|
|
399
|
+
**10. `MCP server failed to start` in the Claude Code `/mcp` panel**
|
|
378
400
|
Run `claude --debug` to see the underlying error. Nine times out of ten it's a stale or missing key — run `meshcode login mc_xxx` again.
|
|
379
401
|
|
|
380
|
-
**
|
|
402
|
+
**11. PyPI says `Could not find version 1.x.x` immediately after a publish**
|
|
381
403
|
CDN propagation. Wait ~60s and force-fetch directly from the origin:
|
|
382
404
|
```bash
|
|
383
405
|
pip install --no-cache-dir -i https://pypi.org/simple/ meshcode
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "2.10.
|
|
2
|
+
__version__ = "2.10.41"
|
|
@@ -343,9 +343,26 @@ def _is_pid_alive(pid):
|
|
|
343
343
|
if not pid:
|
|
344
344
|
return False
|
|
345
345
|
try:
|
|
346
|
-
|
|
346
|
+
pid = int(pid)
|
|
347
|
+
except (ValueError, TypeError):
|
|
348
|
+
return False
|
|
349
|
+
if sys.platform == "win32":
|
|
350
|
+
# os.kill(pid, 0) doesn't work on Windows — use kernel32 OpenProcess
|
|
351
|
+
try:
|
|
352
|
+
import ctypes
|
|
353
|
+
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
|
354
|
+
handle = ctypes.windll.kernel32.OpenProcess(
|
|
355
|
+
PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
|
|
356
|
+
if handle:
|
|
357
|
+
ctypes.windll.kernel32.CloseHandle(handle)
|
|
358
|
+
return True
|
|
359
|
+
return False
|
|
360
|
+
except Exception:
|
|
361
|
+
return False
|
|
362
|
+
try:
|
|
363
|
+
os.kill(pid, 0)
|
|
347
364
|
return True
|
|
348
|
-
except
|
|
365
|
+
except OSError:
|
|
349
366
|
return False
|
|
350
367
|
|
|
351
368
|
|
|
@@ -533,7 +550,7 @@ def spawn_headless_agent(project, name, project_id, message_body, from_agent):
|
|
|
533
550
|
f"{message_body}\n\n"
|
|
534
551
|
f"Recent mesh activity:\n{history_text or '(none)'}\n\n"
|
|
535
552
|
"Process the message. If you need to reply, use this exact bash command (one line):\n"
|
|
536
|
-
f"
|
|
553
|
+
f" {sys.executable} -m meshcode send {project} {name}:{from_agent} '<json_response>'\n\n"
|
|
537
554
|
"Then exit. Do not start a long conversation."
|
|
538
555
|
)
|
|
539
556
|
|
|
@@ -579,7 +596,8 @@ def spawn_headless_agent(project, name, project_id, message_body, from_agent):
|
|
|
579
596
|
claude_bin = _find_claude()
|
|
580
597
|
# Augment PATH so any subprocess the agent itself spawns (git, npm,
|
|
581
598
|
# python3, supabase CLI, etc.) can find common tools.
|
|
582
|
-
|
|
599
|
+
if sys.platform != "win32":
|
|
600
|
+
env["PATH"] = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:" + env.get("PATH", "")
|
|
583
601
|
result = subprocess.run(
|
|
584
602
|
[claude_bin, "--print", "--dangerously-skip-permissions",
|
|
585
603
|
"--append-system-prompt", system_prompt, message_body or ""],
|
|
@@ -959,12 +977,34 @@ def register(project, name, role=""):
|
|
|
959
977
|
log_msg(f"{name} joined {project} (tty={tty}, backend=supabase)")
|
|
960
978
|
|
|
961
979
|
|
|
980
|
+
def _resolve_agent_name(project_id, name):
|
|
981
|
+
"""Resolve partial agent name to full registered name."""
|
|
982
|
+
if not name or name == "*":
|
|
983
|
+
return name
|
|
984
|
+
agents = sb_select("mc_agents", f"project_id=eq.{project_id}", limit=50)
|
|
985
|
+
if not agents:
|
|
986
|
+
return name
|
|
987
|
+
all_names = [a["name"] for a in agents]
|
|
988
|
+
if name in all_names:
|
|
989
|
+
return name
|
|
990
|
+
prefix = [n for n in all_names if n.startswith(name)]
|
|
991
|
+
if len(prefix) == 1:
|
|
992
|
+
return prefix[0]
|
|
993
|
+
sub = [n for n in all_names if name in n]
|
|
994
|
+
if len(sub) == 1:
|
|
995
|
+
return sub[0]
|
|
996
|
+
return name
|
|
997
|
+
|
|
998
|
+
|
|
962
999
|
def send_msg(project, from_agent, to_agent, content, msg_type="msg", compact=False):
|
|
963
1000
|
project_id = get_project_id(project)
|
|
964
1001
|
if not project_id:
|
|
965
1002
|
print(f"[ERROR] Proyecto '{project}' no encontrado")
|
|
966
1003
|
return
|
|
967
1004
|
|
|
1005
|
+
# Resolve partial agent names
|
|
1006
|
+
to_agent = _resolve_agent_name(project_id, to_agent)
|
|
1007
|
+
|
|
968
1008
|
payload = {}
|
|
969
1009
|
try:
|
|
970
1010
|
payload = json.loads(content)
|
|
@@ -1026,7 +1066,7 @@ def broadcast(project, from_agent, content, msg_type="broadcast"):
|
|
|
1026
1066
|
print(f"[{project}] broadcast from {from_agent} -> {count} agents")
|
|
1027
1067
|
|
|
1028
1068
|
|
|
1029
|
-
def read_messages(project, name, silent=False):
|
|
1069
|
+
def read_messages(project, name, silent=False, send_acks=True):
|
|
1030
1070
|
project_id = get_project_id(project)
|
|
1031
1071
|
if not project_id:
|
|
1032
1072
|
if not silent:
|
|
@@ -1064,21 +1104,68 @@ def read_messages(project, name, silent=False):
|
|
|
1064
1104
|
ack_targets.add(sender)
|
|
1065
1105
|
print("---")
|
|
1066
1106
|
|
|
1067
|
-
# Send automatic ACKs
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
"
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1107
|
+
# Send automatic ACKs (skip when called from watch to avoid ack spam)
|
|
1108
|
+
if send_acks:
|
|
1109
|
+
for sender in ack_targets:
|
|
1110
|
+
ack_payload = {"text": f"{name} read your message"}
|
|
1111
|
+
sb_insert("mc_messages", {
|
|
1112
|
+
"project_id": project_id,
|
|
1113
|
+
"from_agent": name,
|
|
1114
|
+
"to_agent": sender,
|
|
1115
|
+
"type": "ack",
|
|
1116
|
+
"payload": ack_payload,
|
|
1117
|
+
"read": True
|
|
1118
|
+
})
|
|
1078
1119
|
|
|
1079
1120
|
return messages
|
|
1080
1121
|
|
|
1081
1122
|
|
|
1123
|
+
def inbox(project, name):
|
|
1124
|
+
"""All-in-one: show agent status, pending count, tasks, and read new messages."""
|
|
1125
|
+
project_id = get_project_id(project)
|
|
1126
|
+
if not project_id:
|
|
1127
|
+
print(f"[meshcode] Project '{project}' not found")
|
|
1128
|
+
return
|
|
1129
|
+
|
|
1130
|
+
# Agent status
|
|
1131
|
+
agent_rows = sb_select("mc_agents",
|
|
1132
|
+
f"project_id=eq.{project_id}&name=eq.{quote(name)}", limit=1)
|
|
1133
|
+
if agent_rows:
|
|
1134
|
+
a = agent_rows[0]
|
|
1135
|
+
print(f"[{project}] {name} | status: {a.get('status', '?')} | task: {a.get('task', '-')}")
|
|
1136
|
+
else:
|
|
1137
|
+
print(f"[{project}] {name} (not registered)")
|
|
1138
|
+
|
|
1139
|
+
# Pending messages
|
|
1140
|
+
pending = sb_select("mc_messages",
|
|
1141
|
+
f"project_id=eq.{project_id}&to_agent=eq.{quote(name)}&read=eq.false&type=neq.ack",
|
|
1142
|
+
order="created_at.asc")
|
|
1143
|
+
if pending:
|
|
1144
|
+
print(f"\n {len(pending)} unread message(s):")
|
|
1145
|
+
for msg in pending:
|
|
1146
|
+
sender = msg["from_agent"]
|
|
1147
|
+
ts = msg.get("created_at", "")
|
|
1148
|
+
if "T" in ts:
|
|
1149
|
+
ts = ts.replace("T", " ")[:19]
|
|
1150
|
+
payload = msg.get("payload", {})
|
|
1151
|
+
preview = json.dumps(payload, ensure_ascii=False)[:100]
|
|
1152
|
+
print(f" [{ts}] {sender}: {preview}")
|
|
1153
|
+
# Ask if they want to read (mark as read)
|
|
1154
|
+
print(f"\n Run `meshcode read {project} {name}` to read and mark as read.")
|
|
1155
|
+
else:
|
|
1156
|
+
print(" No unread messages.")
|
|
1157
|
+
|
|
1158
|
+
# Open tasks assigned to this agent
|
|
1159
|
+
tasks = sb_select("mc_tasks",
|
|
1160
|
+
f"project_id=eq.{project_id}&assignee=eq.{quote(name)}&status=neq.done&status=neq.canceled",
|
|
1161
|
+
order="created_at.desc", limit=5)
|
|
1162
|
+
if tasks:
|
|
1163
|
+
print(f"\n {len(tasks)} open task(s):")
|
|
1164
|
+
for t in tasks:
|
|
1165
|
+
print(f" [{t.get('priority', '?')}] {t.get('title', '?')} ({t.get('status', '?')})")
|
|
1166
|
+
print()
|
|
1167
|
+
|
|
1168
|
+
|
|
1082
1169
|
def hook_check():
|
|
1083
1170
|
"""Called by PostToolUse hook. Auto-detects project+agent, prints pending.
|
|
1084
1171
|
Also sends heartbeat so dashboard always shows fresh status."""
|
|
@@ -1127,7 +1214,10 @@ def watch(project, name, interval=10, timeout=0):
|
|
|
1127
1214
|
f"project_id=eq.{project_id}&name=eq.{quote(name)}",
|
|
1128
1215
|
{"status": "online", "task": "Listening to user", "last_heartbeat": now_iso()})
|
|
1129
1216
|
sys.exit(0)
|
|
1130
|
-
|
|
1217
|
+
try:
|
|
1218
|
+
signal.signal(signal.SIGINT, handle_interrupt)
|
|
1219
|
+
except ValueError:
|
|
1220
|
+
pass # Non-main thread (Windows restriction)
|
|
1131
1221
|
|
|
1132
1222
|
# Update status to standby
|
|
1133
1223
|
sb_update("mc_agents",
|
|
@@ -1148,13 +1238,18 @@ def watch(project, name, interval=10, timeout=0):
|
|
|
1148
1238
|
if pending:
|
|
1149
1239
|
elapsed = int(time.time() - start)
|
|
1150
1240
|
print(f"\n*** [{project.upper()}] MENSAJE RECIBIDO ({elapsed}s en standby) ***")
|
|
1151
|
-
|
|
1241
|
+
read_messages(project, name, silent=False, send_acks=False)
|
|
1152
1242
|
|
|
1153
1243
|
sb_update("mc_agents",
|
|
1154
1244
|
f"project_id=eq.{project_id}&name=eq.{quote(name)}",
|
|
1155
1245
|
{"status": "working", "task": "Procesando mensaje recibido", "last_heartbeat": now_iso()})
|
|
1156
1246
|
|
|
1157
|
-
|
|
1247
|
+
# Reset to standby and keep polling (don't exit the loop)
|
|
1248
|
+
time.sleep(2)
|
|
1249
|
+
sb_update("mc_agents",
|
|
1250
|
+
f"project_id=eq.{project_id}&name=eq.{quote(name)}",
|
|
1251
|
+
{"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
|
|
1252
|
+
continue
|
|
1158
1253
|
|
|
1159
1254
|
# Heartbeat
|
|
1160
1255
|
sb_rpc("mc_heartbeat", {"p_project_id": project_id, "p_agent_name": name})
|
|
@@ -1181,7 +1276,7 @@ def update_board(project, name, status, task=""):
|
|
|
1181
1276
|
count = len(sb_select("mc_messages",
|
|
1182
1277
|
f"project_id=eq.{project_id}&to_agent=eq.{quote(name)}&read=eq.false&type=neq.ack"))
|
|
1183
1278
|
print(f"[{project}] {name}: NO puedes estar idle — tienes {count} mensaje(s) pendientes.")
|
|
1184
|
-
print(f"[{project}] Ejecuta:
|
|
1279
|
+
print(f"[{project}] Ejecuta: {sys.executable} -m meshcode read {project} {name}")
|
|
1185
1280
|
status = "working"
|
|
1186
1281
|
task = f"{count} pending messages"
|
|
1187
1282
|
|
|
@@ -1215,13 +1310,21 @@ def show_board(project):
|
|
|
1215
1310
|
|
|
1216
1311
|
|
|
1217
1312
|
def show_status(project=None):
|
|
1218
|
-
|
|
1313
|
+
# Try API-key-authenticated RPC first (RLS may block anon SELECT)
|
|
1314
|
+
api_key = _load_api_key_for_cli()
|
|
1315
|
+
if api_key and not project:
|
|
1316
|
+
rpc_data = sb_rpc("mc_list_user_projects", {"p_api_key": api_key})
|
|
1317
|
+
if isinstance(rpc_data, dict) and not rpc_data.get("error"):
|
|
1318
|
+
projects_data = rpc_data.get("projects", [])
|
|
1319
|
+
else:
|
|
1320
|
+
projects_data = sb_select("mc_projects", "", order="created_at.asc")
|
|
1321
|
+
elif project:
|
|
1219
1322
|
projects_data = sb_select("mc_projects", f"name=eq.{quote(project)}")
|
|
1220
1323
|
else:
|
|
1221
1324
|
projects_data = sb_select("mc_projects", "", order="created_at.asc")
|
|
1222
1325
|
|
|
1223
1326
|
if not projects_data:
|
|
1224
|
-
print("[
|
|
1327
|
+
print("[meshcode] No projects found. Try `meshcode projects` or `meshcode login <api_key>` first.")
|
|
1225
1328
|
return
|
|
1226
1329
|
|
|
1227
1330
|
print(f"\n{'='*60}")
|
|
@@ -1398,7 +1501,7 @@ def _start_heartbeat_daemon(project, name):
|
|
|
1398
1501
|
if pid_file.exists():
|
|
1399
1502
|
old = int(pid_file.read_text(encoding="utf-8").strip())
|
|
1400
1503
|
try:
|
|
1401
|
-
|
|
1504
|
+
_terminate_pid(old)
|
|
1402
1505
|
except Exception:
|
|
1403
1506
|
pass
|
|
1404
1507
|
except Exception:
|
|
@@ -1450,12 +1553,21 @@ def _start_heartbeat_daemon(project, name):
|
|
|
1450
1553
|
return proc.pid
|
|
1451
1554
|
|
|
1452
1555
|
|
|
1556
|
+
def _terminate_pid(pid):
|
|
1557
|
+
"""Terminate a process by PID, cross-platform."""
|
|
1558
|
+
if sys.platform == "win32":
|
|
1559
|
+
subprocess.run(["taskkill", "/F", "/PID", str(pid)],
|
|
1560
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
1561
|
+
else:
|
|
1562
|
+
os.kill(pid, 15)
|
|
1563
|
+
|
|
1564
|
+
|
|
1453
1565
|
def _stop_heartbeat_daemon(project, name):
|
|
1454
1566
|
pid_file = _heartbeat_loop_pidfile(project, name)
|
|
1455
1567
|
if pid_file.exists():
|
|
1456
1568
|
try:
|
|
1457
1569
|
pid = int(pid_file.read_text(encoding="utf-8").strip())
|
|
1458
|
-
|
|
1570
|
+
_terminate_pid(pid)
|
|
1459
1571
|
except Exception:
|
|
1460
1572
|
pass
|
|
1461
1573
|
try:
|
|
@@ -1603,19 +1715,11 @@ def connect(project, name, hook_target="claude", role=""):
|
|
|
1603
1715
|
_setup_ws(project, name, actual_role)
|
|
1604
1716
|
|
|
1605
1717
|
elif hook_target == "codex":
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
"check_command": f"python3 {comms_path} check",
|
|
1612
|
-
"read_command": f"python3 {comms_path} read {project} {name}",
|
|
1613
|
-
"send_command": f"python3 {comms_path} send {project} {name}:{{to}} '{{message}}'",
|
|
1614
|
-
"platform": os_name
|
|
1615
|
-
}
|
|
1616
|
-
config_path.write_text(json.dumps(config, indent=2), encoding="utf-8")
|
|
1617
|
-
print(f"[meshcode] Config saved to {config_path}")
|
|
1618
|
-
print(f"[meshcode] Codex: use read_command and send_command from the config")
|
|
1718
|
+
# Codex reads ~/.codex/config.toml globally — workspace .mcp.json is ignored.
|
|
1719
|
+
# Use setup_clients.setup_global to write the proper TOML config.
|
|
1720
|
+
import importlib
|
|
1721
|
+
_setup_global = importlib.import_module("meshcode.setup_clients").setup_global
|
|
1722
|
+
_setup_global("codex", project, name, actual_role)
|
|
1619
1723
|
|
|
1620
1724
|
else:
|
|
1621
1725
|
print(f"[meshcode] Unknown hook target: {hook_target}. Use 'claude' or 'codex'.")
|
|
@@ -1629,9 +1733,10 @@ def connect(project, name, hook_target="claude", role=""):
|
|
|
1629
1733
|
print(f" 3. Start using tools: meshcode_send, meshcode_read, etc.")
|
|
1630
1734
|
print(f" No extra terminal. No watch loops. No AppleScript.")
|
|
1631
1735
|
else:
|
|
1632
|
-
|
|
1633
|
-
print(f"
|
|
1634
|
-
print(f"
|
|
1736
|
+
_py = sys.executable
|
|
1737
|
+
print(f" Read: {_py} {comms_path} read {project} {name}")
|
|
1738
|
+
print(f" Send: {_py} {comms_path} send {project} {name}:<target> \"<message>\"")
|
|
1739
|
+
print(f" Board: {_py} {comms_path} board {project}")
|
|
1635
1740
|
print()
|
|
1636
1741
|
|
|
1637
1742
|
|
|
@@ -1840,6 +1945,15 @@ Show conversation history. Optionally filter to messages between two agents.
|
|
|
1840
1945
|
EXAMPLES:
|
|
1841
1946
|
meshcode history my-app 50
|
|
1842
1947
|
meshcode history my-app 20 backend,frontend
|
|
1948
|
+
""",
|
|
1949
|
+
"inbox": """meshcode inbox <project> <name>
|
|
1950
|
+
|
|
1951
|
+
All-in-one overview: agent status, unread messages, and open tasks.
|
|
1952
|
+
Alias: meshcode next
|
|
1953
|
+
|
|
1954
|
+
EXAMPLES:
|
|
1955
|
+
meshcode inbox my-app backend
|
|
1956
|
+
meshcode next my-app frontend
|
|
1843
1957
|
""",
|
|
1844
1958
|
"projects": """meshcode projects
|
|
1845
1959
|
|
|
@@ -2050,6 +2164,7 @@ if __name__ == "__main__":
|
|
|
2050
2164
|
# Supports both:
|
|
2051
2165
|
# send <project> <from>:<to> <message>
|
|
2052
2166
|
# send --project X --from Y --to Z --msg "text" [--type T]
|
|
2167
|
+
# echo '{"key":"val"}' | meshcode send <project> <from>:<to> -
|
|
2053
2168
|
compact = bool(flags.get("compact"))
|
|
2054
2169
|
if "from" in flags and "to" in flags:
|
|
2055
2170
|
proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
|
|
@@ -2062,6 +2177,9 @@ if __name__ == "__main__":
|
|
|
2062
2177
|
proj = pos[0] if len(pos) > 0 else "default"
|
|
2063
2178
|
target = pos[1] if len(pos) > 1 else ""
|
|
2064
2179
|
message = " ".join(pos[2:]) if len(pos) > 2 else ""
|
|
2180
|
+
# Support stdin: `meshcode send proj from:to -` reads from pipe
|
|
2181
|
+
if message.strip() == "-" and not sys.stdin.isatty():
|
|
2182
|
+
message = sys.stdin.read().strip()
|
|
2065
2183
|
if ":" in target:
|
|
2066
2184
|
from_a, to_a = target.split(":", 1)
|
|
2067
2185
|
else:
|
|
@@ -2092,6 +2210,11 @@ if __name__ == "__main__":
|
|
|
2092
2210
|
name = flags.get("name", pos[1] if len(pos) > 1 else "agent")
|
|
2093
2211
|
read_messages(proj, name)
|
|
2094
2212
|
|
|
2213
|
+
elif cmd in ("inbox", "next"):
|
|
2214
|
+
proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
|
|
2215
|
+
name = flags.get("name", pos[1] if len(pos) > 1 else "agent")
|
|
2216
|
+
inbox(proj, name)
|
|
2217
|
+
|
|
2095
2218
|
elif cmd == "check":
|
|
2096
2219
|
hook_check()
|
|
2097
2220
|
|
|
@@ -2427,6 +2550,7 @@ if __name__ == "__main__":
|
|
|
2427
2550
|
|
|
2428
2551
|
elif cmd == "whoami":
|
|
2429
2552
|
# Show the logged-in user identity from profile_meta.json
|
|
2553
|
+
verbose = "--verbose" in sys.argv or "-v" in sys.argv
|
|
2430
2554
|
meta_path = Path.home() / ".meshcode" / "profile_meta.json"
|
|
2431
2555
|
if meta_path.exists():
|
|
2432
2556
|
try:
|
|
@@ -2441,6 +2565,25 @@ if __name__ == "__main__":
|
|
|
2441
2565
|
print(f" user ID: {meta['user_id']}")
|
|
2442
2566
|
active = os.environ.get("MESHCODE_KEYCHAIN_PROFILE") or "default"
|
|
2443
2567
|
print(f" profile: {active}")
|
|
2568
|
+
|
|
2569
|
+
if verbose:
|
|
2570
|
+
# Show projects, agents, plan, API key status
|
|
2571
|
+
api_key = _load_api_key_for_cli()
|
|
2572
|
+
if api_key:
|
|
2573
|
+
print(f" api key: {api_key[:8]}...{api_key[-4:]}")
|
|
2574
|
+
# List projects
|
|
2575
|
+
result = sb_rpc("mc_list_user_projects", {"p_api_key": api_key})
|
|
2576
|
+
if isinstance(result, dict) and result.get("ok"):
|
|
2577
|
+
projects = result.get("projects", [])
|
|
2578
|
+
print(f"\n Projects ({len(projects)}):")
|
|
2579
|
+
for p in projects:
|
|
2580
|
+
agent_count = p.get("agent_count", "?")
|
|
2581
|
+
plan = p.get("plan", "free")
|
|
2582
|
+
print(f" {p.get('name', '?'):20s} plan={plan:12s} agents={agent_count}")
|
|
2583
|
+
else:
|
|
2584
|
+
print(f"\n Projects: (could not fetch)")
|
|
2585
|
+
else:
|
|
2586
|
+
print(f" api key: not found")
|
|
2444
2587
|
print()
|
|
2445
2588
|
except Exception:
|
|
2446
2589
|
print("[meshcode] Could not read profile. Run `meshcode login <api_key>`.")
|
|
@@ -2558,7 +2701,7 @@ if __name__ == "__main__":
|
|
|
2558
2701
|
|
|
2559
2702
|
else:
|
|
2560
2703
|
known_cmds = [
|
|
2561
|
-
"register", "send", "broadcast", "read", "check", "watch",
|
|
2704
|
+
"register", "send", "broadcast", "read", "check", "watch", "inbox", "next",
|
|
2562
2705
|
"board", "update", "status", "projects", "list", "ls",
|
|
2563
2706
|
"history", "clear", "unregister", "connect", "disconnect",
|
|
2564
2707
|
"setup", "run", "go", "invite", "join", "invites", "members",
|
|
@@ -409,7 +409,43 @@ def register_agent(project: str, name: str, role: str = "", api_key: Optional[st
|
|
|
409
409
|
}
|
|
410
410
|
|
|
411
411
|
|
|
412
|
+
def resolve_agent_name(project_id: str, name: str) -> str:
|
|
413
|
+
"""Resolve a partial or exact agent name to the full registered name.
|
|
414
|
+
|
|
415
|
+
Returns the exact match if found, or a unique prefix/substring match.
|
|
416
|
+
Returns the original name unchanged if no match or ambiguous.
|
|
417
|
+
Broadcast target '*' is passed through unchanged.
|
|
418
|
+
"""
|
|
419
|
+
if not name or name == "*":
|
|
420
|
+
return name
|
|
421
|
+
# Exact match — fast path
|
|
422
|
+
exact = sb_select("mc_agents",
|
|
423
|
+
f"project_id=eq.{project_id}&name=eq.{quote(name)}",
|
|
424
|
+
limit=1)
|
|
425
|
+
if exact:
|
|
426
|
+
return name
|
|
427
|
+
# Fuzzy: prefix or substring match among registered agents
|
|
428
|
+
agents = sb_select("mc_agents", f"project_id=eq.{project_id}",
|
|
429
|
+
columns="name")
|
|
430
|
+
if not agents:
|
|
431
|
+
return name
|
|
432
|
+
all_names = [a["name"] for a in agents]
|
|
433
|
+
# Try prefix match first
|
|
434
|
+
prefix_matches = [n for n in all_names if n.startswith(name)]
|
|
435
|
+
if len(prefix_matches) == 1:
|
|
436
|
+
return prefix_matches[0]
|
|
437
|
+
# Try substring match
|
|
438
|
+
sub_matches = [n for n in all_names if name in n]
|
|
439
|
+
if len(sub_matches) == 1:
|
|
440
|
+
return sub_matches[0]
|
|
441
|
+
# Ambiguous or no match — return original
|
|
442
|
+
return name
|
|
443
|
+
|
|
444
|
+
|
|
412
445
|
def send_message(project_id: str, from_agent: str, to_agent: str, payload: Any, msg_type: str = "msg", parent_msg_id: Optional[str] = None, sensitive: bool = False, api_key: Optional[str] = None, encrypted: bool = False) -> Dict:
|
|
446
|
+
# Normalize partial agent names to full registered names
|
|
447
|
+
to_agent = resolve_agent_name(project_id, to_agent)
|
|
448
|
+
|
|
413
449
|
if not isinstance(payload, dict):
|
|
414
450
|
payload = {"text": str(payload)}
|
|
415
451
|
|
|
@@ -698,16 +734,19 @@ def heartbeat(project_id: str, agent: str) -> Dict:
|
|
|
698
734
|
return result or {}
|
|
699
735
|
|
|
700
736
|
|
|
701
|
-
def set_status(project_id: str, agent: str, status: str, task: str = "", api_key: Optional[str] = None) -> Dict:
|
|
737
|
+
def set_status(project_id: str, agent: str, status: str, task: str = "", api_key: Optional[str] = None, editor: Optional[str] = None) -> Dict:
|
|
702
738
|
# Use SECURITY DEFINER RPC when api_key is available (anon PATCH silently fails)
|
|
703
739
|
if api_key:
|
|
704
|
-
|
|
740
|
+
params = {
|
|
705
741
|
"p_api_key": api_key,
|
|
706
742
|
"p_project_id": project_id,
|
|
707
743
|
"p_agent_name": agent,
|
|
708
744
|
"p_status": status,
|
|
709
745
|
"p_task": task,
|
|
710
|
-
}
|
|
746
|
+
}
|
|
747
|
+
if editor:
|
|
748
|
+
params["p_editor"] = editor
|
|
749
|
+
rpc_result = sb_rpc("mc_agent_set_status_by_api_key", params)
|
|
711
750
|
if isinstance(rpc_result, dict) and rpc_result.get("ok"):
|
|
712
751
|
return {"ok": True, "status": status}
|
|
713
752
|
# Fall through to direct PATCH if RPC doesn't exist yet
|
|
@@ -716,6 +755,8 @@ def set_status(project_id: str, agent: str, status: str, task: str = "", api_key
|
|
|
716
755
|
updates = {"status": status, "last_heartbeat": _now_iso()}
|
|
717
756
|
if task:
|
|
718
757
|
updates["task"] = task
|
|
758
|
+
if editor:
|
|
759
|
+
updates["editor"] = editor
|
|
719
760
|
result = sb_update("mc_agents", f"project_id=eq.{project_id}&name=eq.{quote(agent)}", updates)
|
|
720
761
|
if isinstance(result, dict) and result.get("_error"):
|
|
721
762
|
return {"error": result["_error"]}
|
|
@@ -836,6 +877,7 @@ class MeshCodeBackend:
|
|
|
836
877
|
``from meshcode.meshcode_mcp.backend import MeshCodeBackend`` resolves.
|
|
837
878
|
"""
|
|
838
879
|
get_project_id = staticmethod(get_project_id)
|
|
880
|
+
resolve_agent_name = staticmethod(resolve_agent_name)
|
|
839
881
|
register_agent = staticmethod(register_agent)
|
|
840
882
|
send_message = staticmethod(send_message)
|
|
841
883
|
read_inbox = staticmethod(read_inbox)
|
|
@@ -373,6 +373,7 @@ log = logging.getLogger("meshcode-mcp")
|
|
|
373
373
|
PROJECT_NAME = os.environ.get("MESHCODE_PROJECT", "")
|
|
374
374
|
AGENT_NAME = os.environ.get("MESHCODE_AGENT", "")
|
|
375
375
|
AGENT_ROLE = os.environ.get("MESHCODE_ROLE", "")
|
|
376
|
+
EDITOR_TYPE = os.environ.get("MESHCODE_EDITOR_TYPE", "")
|
|
376
377
|
|
|
377
378
|
if not PROJECT_NAME or not AGENT_NAME:
|
|
378
379
|
print(
|
|
@@ -488,18 +489,18 @@ except Exception:
|
|
|
488
489
|
# Use the SECURITY DEFINER RPC (mc_agent_set_status_by_api_key) so we
|
|
489
490
|
# bypass RLS — the publishable key has no JWT context and cannot UPDATE
|
|
490
491
|
# mc_agents directly. The RPC validates ownership via api_key.
|
|
491
|
-
def _flip_status(status: str, task: str = "") -> bool:
|
|
492
|
+
def _flip_status(status: str, task: str = "", editor: Optional[str] = None) -> bool:
|
|
492
493
|
"""Write status via RPC for reliable updates (anon PATCH silently fails).
|
|
493
494
|
|
|
494
495
|
Falls back to direct PATCH only when no api_key is available (tests).
|
|
495
496
|
"""
|
|
496
497
|
try:
|
|
497
|
-
result = be.set_status(_PROJECT_ID, AGENT_NAME, status, task, api_key=_get_api_key())
|
|
498
|
+
result = be.set_status(_PROJECT_ID, AGENT_NAME, status, task, api_key=_get_api_key(), editor=editor)
|
|
498
499
|
return isinstance(result, dict) and result.get("ok", False)
|
|
499
500
|
except Exception:
|
|
500
501
|
return False
|
|
501
502
|
|
|
502
|
-
if not _flip_status("idle", ""):
|
|
503
|
+
if not _flip_status("idle", "", editor=EDITOR_TYPE or None):
|
|
503
504
|
_mc_log(f" could not flip status to idle", "warn")
|
|
504
505
|
|
|
505
506
|
|
|
@@ -1195,8 +1196,10 @@ def _heartbeat_loop_inner():
|
|
|
1195
1196
|
elif cur_state == "online" and idle_secs > 30:
|
|
1196
1197
|
# Brief idle — show as idle, not sleeping yet
|
|
1197
1198
|
_set_state("idle", "idle")
|
|
1198
|
-
elif cur_state == "idle" and idle_secs > 300 and parent_cpu < 2.0 and not _STAY_AWAKE:
|
|
1199
|
-
# Extended idle + no CPU activity
|
|
1199
|
+
elif cur_state == "idle" and idle_secs > 300 and parent_cpu < 2.0 and not _STAY_AWAKE and not in_wait:
|
|
1200
|
+
# Extended idle + no CPU activity + NOT in wait loop → sleeping
|
|
1201
|
+
# Never auto-sleep during meshcode_wait — the agent is actively
|
|
1202
|
+
# listening for messages, not truly idle.
|
|
1200
1203
|
_set_state("sleeping", "sleeping")
|
|
1201
1204
|
|
|
1202
1205
|
# Sync current state to DB (in case realtime missed it)
|
|
@@ -1770,12 +1773,15 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
|
|
|
1770
1773
|
pass # All messages already seen — fall through to wait loop
|
|
1771
1774
|
else:
|
|
1772
1775
|
split = _split_messages(deduped)
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1776
|
+
# Only refuse for real messages — ack-only batches should not block wait
|
|
1777
|
+
if split["messages"] or split["done_signals"]:
|
|
1778
|
+
return {
|
|
1779
|
+
"refused": True,
|
|
1780
|
+
"reason": f"You have {split['count']} unread messages. Process them before waiting.",
|
|
1781
|
+
"got_message": True,
|
|
1782
|
+
**split,
|
|
1783
|
+
}
|
|
1784
|
+
# Ack-only batch — fall through to wait loop
|
|
1779
1785
|
except Exception:
|
|
1780
1786
|
pass
|
|
1781
1787
|
|
|
@@ -3123,7 +3129,12 @@ def _auto_update() -> None:
|
|
|
3123
3129
|
print(f"[meshcode] Updated to {latest}, restarting...", file=sys.stderr)
|
|
3124
3130
|
os.environ["MESHCODE_UPDATED"] = "1"
|
|
3125
3131
|
try:
|
|
3126
|
-
|
|
3132
|
+
if sys.platform == "win32":
|
|
3133
|
+
import subprocess as _sp_reexec
|
|
3134
|
+
_sp_reexec.Popen([sys.executable] + sys.argv)
|
|
3135
|
+
sys.exit(0)
|
|
3136
|
+
else:
|
|
3137
|
+
os.execv(sys.executable, [sys.executable] + sys.argv)
|
|
3127
3138
|
except Exception as e:
|
|
3128
3139
|
log.debug(f"[meshcode] Re-exec failed: {e}, continuing with old version")
|
|
3129
3140
|
|
|
@@ -531,6 +531,9 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
531
531
|
print(f"[meshcode] Closing the editor will flip this agent offline.")
|
|
532
532
|
print()
|
|
533
533
|
|
|
534
|
+
# Set editor type so MCP server can report it to the dashboard
|
|
535
|
+
os.environ["MESHCODE_EDITOR_TYPE"] = editor
|
|
536
|
+
|
|
534
537
|
if editor == "claude":
|
|
535
538
|
# Claude Code: run from inside the workspace and let Claude Code
|
|
536
539
|
# auto-discover .mcp.json as a PROJECT-scoped MCP. We used to pass
|
|
@@ -158,7 +158,8 @@ def _suggest_projects(api_key: str, sb: dict):
|
|
|
158
158
|
|
|
159
159
|
def _build_server_block(project: str, project_id: str, agent: str, role: str,
|
|
160
160
|
api_key: str, sb: Dict[str, str],
|
|
161
|
-
keychain_profile: str = "default"
|
|
161
|
+
keychain_profile: str = "default",
|
|
162
|
+
editor_type: str = "") -> Dict[str, Any]:
|
|
162
163
|
"""Build the MCP server config block for an agent.
|
|
163
164
|
|
|
164
165
|
1.4.1+ does NOT bake the api key into the env. Instead it bakes a
|
|
@@ -184,6 +185,8 @@ def _build_server_block(project: str, project_id: str, agent: str, role: str,
|
|
|
184
185
|
# parts of the package (e.g. self_update) skip auto-update inside it.
|
|
185
186
|
"MESHCODE_MCP_SERVE": "1",
|
|
186
187
|
}
|
|
188
|
+
if editor_type:
|
|
189
|
+
env["MESHCODE_EDITOR_TYPE"] = editor_type
|
|
187
190
|
# Belt-and-suspenders fallback: if the OS doesn't have a keychain backend
|
|
188
191
|
# the MCP server can't read the key from there, so we still need to pass
|
|
189
192
|
# it via env. Probe and only include it as a fallback path.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshcode
|
|
3
|
-
Version: 2.10.
|
|
3
|
+
Version: 2.10.41
|
|
4
4
|
Summary: Real-time communication between AI agents — Supabase-backed CLI
|
|
5
5
|
Author-email: MeshCode <hello@meshcode.io>
|
|
6
6
|
License: MIT
|
|
@@ -400,10 +400,32 @@ meshcode projects # lists all your meshworks
|
|
|
400
400
|
**8. Unknown command (e.g., `meshcode list`)**
|
|
401
401
|
Since v2.0.2, the CLI suggests the closest match instead of dumping the full help text. Common aliases: `list` and `ls` both work for `projects`.
|
|
402
402
|
|
|
403
|
-
**
|
|
403
|
+
**9. Windows: agent registers but never appears online**
|
|
404
|
+
On Windows, especially with Python 3.14+, you may see `ImportError` crashes or agents that register but stay offline. This was fixed in v2.10.40. Upgrade first:
|
|
405
|
+
```bash
|
|
406
|
+
pip install meshcode --upgrade
|
|
407
|
+
```
|
|
408
|
+
If the agent still fails after upgrading, check that your `.mcp.json` command path is valid. On Windows, the MCP config should use `python` (not `python3`, which doesn't exist on Windows):
|
|
409
|
+
```json
|
|
410
|
+
{
|
|
411
|
+
"command": "python",
|
|
412
|
+
"args": ["-m", "meshcode_mcp", "serve"]
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
If `meshcode setup` was run from a conda/venv environment that no longer exists, the baked path in `.mcp.json` will be stale. Re-run `meshcode setup <project> <agent>` from your current Python environment to fix it.
|
|
416
|
+
|
|
417
|
+
As a fast-path workaround, you can set `MESHCODE_PROJECT_ID` as an environment variable to bypass the project name lookup at boot (which can fail on Windows due to RLS visibility):
|
|
418
|
+
```bash
|
|
419
|
+
set MESHCODE_PROJECT_ID=your-project-uuid # Windows cmd
|
|
420
|
+
$env:MESHCODE_PROJECT_ID="your-project-uuid" # Windows PowerShell
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
You can also run `meshcode doctor` (v2.10.41+) to diagnose stale paths, missing dependencies, and config issues across all your workspaces.
|
|
424
|
+
|
|
425
|
+
**10. `MCP server failed to start` in the Claude Code `/mcp` panel**
|
|
404
426
|
Run `claude --debug` to see the underlying error. Nine times out of ten it's a stale or missing key — run `meshcode login mc_xxx` again.
|
|
405
427
|
|
|
406
|
-
**
|
|
428
|
+
**11. PyPI says `Could not find version 1.x.x` immediately after a publish**
|
|
407
429
|
CDN propagation. Wait ~60s and force-fetch directly from the origin:
|
|
408
430
|
```bash
|
|
409
431
|
pip install --no-cache-dir -i https://pypi.org/simple/ meshcode
|
|
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
|