meshcode 2.10.58__tar.gz → 2.10.60__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.58 → meshcode-2.10.60}/PKG-INFO +1 -1
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/__init__.py +1 -1
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/comms_v4.py +26 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/meshcode_mcp/server.py +61 -0
- meshcode-2.10.60/meshcode/supervisor.py +186 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode.egg-info/SOURCES.txt +1 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/pyproject.toml +1 -1
- {meshcode-2.10.58 → meshcode-2.10.60}/README.md +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/cli.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/compat.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/exceptions.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/invites.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/launcher.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/preferences.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/run_agent.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/secrets.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/self_update.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/setup.cfg +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/tests/test_core.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/tests/test_exceptions.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/tests/test_security_regressions.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/tests/test_sentinel.py +0 -0
- {meshcode-2.10.58 → meshcode-2.10.60}/tests/test_status_enum_coverage.py +0 -0
|
@@ -3327,6 +3327,31 @@ if __name__ == "__main__":
|
|
|
3327
3327
|
elif cmd in ("help", "--help", "-h"):
|
|
3328
3328
|
show_help()
|
|
3329
3329
|
|
|
3330
|
+
elif cmd == "supervisor":
|
|
3331
|
+
# meshcode supervisor start|stop|status [project] [agent]
|
|
3332
|
+
import importlib
|
|
3333
|
+
_sup = importlib.import_module("meshcode.supervisor")
|
|
3334
|
+
sub = pos[0] if pos else "status"
|
|
3335
|
+
if sub == "start":
|
|
3336
|
+
if len(pos) < 3:
|
|
3337
|
+
print("Usage: meshcode supervisor start <project> <agent>")
|
|
3338
|
+
sys.exit(1)
|
|
3339
|
+
_sup.start(pos[1], pos[2])
|
|
3340
|
+
elif sub == "stop":
|
|
3341
|
+
if len(pos) < 3:
|
|
3342
|
+
print("Usage: meshcode supervisor stop <project> <agent>")
|
|
3343
|
+
sys.exit(1)
|
|
3344
|
+
_sup.stop(pos[1], pos[2])
|
|
3345
|
+
elif sub == "status":
|
|
3346
|
+
_sup.status()
|
|
3347
|
+
elif sub == "--simple":
|
|
3348
|
+
if len(pos) < 3:
|
|
3349
|
+
print("Usage: meshcode supervisor --simple <project> <agent>")
|
|
3350
|
+
sys.exit(1)
|
|
3351
|
+
_sup.simple(pos[1], pos[2])
|
|
3352
|
+
else:
|
|
3353
|
+
print(f"Usage: meshcode supervisor [start|stop|status|--simple] <project> <agent>")
|
|
3354
|
+
|
|
3330
3355
|
else:
|
|
3331
3356
|
known_cmds = [
|
|
3332
3357
|
"register", "send", "broadcast", "read", "check", "watch", "inbox", "next",
|
|
@@ -3335,6 +3360,7 @@ if __name__ == "__main__":
|
|
|
3335
3360
|
"setup", "run", "go", "invite", "join", "invites", "members",
|
|
3336
3361
|
"revoke-invite", "revoke-member", "login", "prefs", "launcher",
|
|
3337
3362
|
"help", "init", "doctor", "compat", "upgrade", "profile", "validate-sessions", "wake-headless",
|
|
3363
|
+
"supervisor",
|
|
3338
3364
|
]
|
|
3339
3365
|
# Simple fuzzy: prefix match + Levenshtein-like best match
|
|
3340
3366
|
suggestions = [c for c in known_cmds if c.startswith(cmd)]
|
|
@@ -1949,6 +1949,47 @@ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
|
|
|
1949
1949
|
return None
|
|
1950
1950
|
|
|
1951
1951
|
|
|
1952
|
+
def _try_auto_claim_task() -> Optional[Dict[str, str]]:
|
|
1953
|
+
"""Auto-claim the oldest open task matching this agent's role.
|
|
1954
|
+
|
|
1955
|
+
Called during meshcode_wait idle loops. Only claims wildcard ('*')
|
|
1956
|
+
tasks to avoid stealing directly-assigned work. Returns the claimed
|
|
1957
|
+
task summary or None.
|
|
1958
|
+
"""
|
|
1959
|
+
try:
|
|
1960
|
+
api_key = _get_api_key()
|
|
1961
|
+
if not api_key:
|
|
1962
|
+
return None
|
|
1963
|
+
result = be.task_list(api_key, _PROJECT_ID, AGENT_NAME, status_filter="open")
|
|
1964
|
+
if not isinstance(result, dict) or not result.get("ok"):
|
|
1965
|
+
return None
|
|
1966
|
+
tasks = result.get("tasks", [])
|
|
1967
|
+
# Only auto-claim wildcard tasks that nobody claimed yet
|
|
1968
|
+
claimable = [
|
|
1969
|
+
t for t in tasks
|
|
1970
|
+
if t.get("assignee") == "*"
|
|
1971
|
+
and not t.get("claimed_by")
|
|
1972
|
+
and t.get("status") == "open"
|
|
1973
|
+
]
|
|
1974
|
+
if not claimable:
|
|
1975
|
+
return None
|
|
1976
|
+
# Sort by priority (urgent first), then oldest
|
|
1977
|
+
_PRIO = {"urgent": 0, "high": 1, "normal": 2, "low": 3}
|
|
1978
|
+
claimable.sort(key=lambda t: (_PRIO.get(t.get("priority", "normal"), 2), t.get("created_at", "")))
|
|
1979
|
+
target = claimable[0]
|
|
1980
|
+
# Claim it
|
|
1981
|
+
claim_result = be.sb_rpc("mc_task_claim", {
|
|
1982
|
+
"p_api_key": api_key,
|
|
1983
|
+
"p_task_id": target["id"],
|
|
1984
|
+
})
|
|
1985
|
+
if isinstance(claim_result, dict) and claim_result.get("ok"):
|
|
1986
|
+
log.info(f"[meshcode] auto-claimed task: {target['title'][:60]}")
|
|
1987
|
+
return {"id": target["id"][:8], "title": target["title"][:80], "priority": target.get("priority", "normal")}
|
|
1988
|
+
except Exception as e:
|
|
1989
|
+
log.debug(f"[meshcode] auto-claim failed: {e}")
|
|
1990
|
+
return None
|
|
1991
|
+
|
|
1992
|
+
|
|
1952
1993
|
@mcp.tool()
|
|
1953
1994
|
@with_working_status
|
|
1954
1995
|
async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -> Dict[str, Any]:
|
|
@@ -2042,6 +2083,16 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
|
|
|
2042
2083
|
result["pending_tasks"] = pending_tasks
|
|
2043
2084
|
break # Return so agent can work tasks
|
|
2044
2085
|
|
|
2086
|
+
# Auto-claim: when idle >30s with no assigned tasks, try to
|
|
2087
|
+
# claim the oldest open wildcard task. This makes agents
|
|
2088
|
+
# self-directing instead of waiting passively for assignment.
|
|
2089
|
+
if _CONSECUTIVE_IDLE_SECONDS >= 30 and not _is_leader_agent():
|
|
2090
|
+
claimed = _try_auto_claim_task()
|
|
2091
|
+
if claimed:
|
|
2092
|
+
result["auto_claimed"] = claimed
|
|
2093
|
+
result["pending_tasks"] = [claimed]
|
|
2094
|
+
break # Return so agent works the claimed task
|
|
2095
|
+
|
|
2045
2096
|
# Update status to sleeping after threshold, but keep looping
|
|
2046
2097
|
if _AUTO_SLEEP_THRESHOLD > 0 and _CONSECUTIVE_IDLE_SECONDS >= _AUTO_SLEEP_THRESHOLD and not _STAY_AWAKE:
|
|
2047
2098
|
try:
|
|
@@ -2132,6 +2183,16 @@ def _mark_realtime_msgs_read_in_db(messages: List[Dict[str, Any]]) -> None:
|
|
|
2132
2183
|
except Exception as e:
|
|
2133
2184
|
log.debug(f"mark_read failed for msg {mid}: {e}")
|
|
2134
2185
|
|
|
2186
|
+
# Confirm delivery (best-effort, background) — completes the
|
|
2187
|
+
# delivery pipeline: sent → read → confirmed.
|
|
2188
|
+
try:
|
|
2189
|
+
be.sb_rpc("mc_confirm_delivery", {
|
|
2190
|
+
"p_api_key": api_key,
|
|
2191
|
+
"p_message_ids": msg_ids,
|
|
2192
|
+
})
|
|
2193
|
+
except Exception:
|
|
2194
|
+
pass # Non-critical: messages still work without confirmation
|
|
2195
|
+
|
|
2135
2196
|
|
|
2136
2197
|
async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[str, Any]:
|
|
2137
2198
|
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""MeshCode Process Supervisor — auto-restart agents on crash.
|
|
2
|
+
|
|
3
|
+
Generates macOS launchd plists (or simple bash wrappers) that keep
|
|
4
|
+
agents running 24/7. When Claude crashes or MCP dies, the supervisor
|
|
5
|
+
restarts in 2-5 seconds automatically.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
meshcode supervisor start <project> <agent> — install + start launchd plist
|
|
9
|
+
meshcode supervisor stop <project> <agent> — stop + uninstall
|
|
10
|
+
meshcode supervisor status — list supervised agents
|
|
11
|
+
meshcode supervisor --simple <project> <agent> — bash while-loop fallback
|
|
12
|
+
"""
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import subprocess
|
|
16
|
+
import json
|
|
17
|
+
import platform
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_PLIST_DIR = Path.home() / "Library" / "LaunchAgents"
|
|
22
|
+
_PLIST_PREFIX = "io.meshcode.agent"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _plist_path(project: str, agent: str) -> Path:
|
|
26
|
+
safe_name = f"{project}-{agent}".replace("/", "-").replace(" ", "-")
|
|
27
|
+
return _PLIST_DIR / f"{_PLIST_PREFIX}.{safe_name}.plist"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _plist_label(project: str, agent: str) -> str:
|
|
31
|
+
safe_name = f"{project}-{agent}".replace("/", "-").replace(" ", "-")
|
|
32
|
+
return f"{_PLIST_PREFIX}.{safe_name}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _find_meshcode_bin() -> str:
|
|
36
|
+
"""Find the meshcode executable path."""
|
|
37
|
+
import shutil
|
|
38
|
+
path = shutil.which("meshcode")
|
|
39
|
+
if path:
|
|
40
|
+
return path
|
|
41
|
+
# Fallback: python -m meshcode
|
|
42
|
+
return f"{sys.executable} -m meshcode"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _generate_plist(project: str, agent: str) -> str:
|
|
46
|
+
"""Generate a launchd plist XML for auto-restarting an agent."""
|
|
47
|
+
label = _plist_label(project, agent)
|
|
48
|
+
meshcode_bin = _find_meshcode_bin()
|
|
49
|
+
log_dir = Path.home() / ".meshcode" / "logs"
|
|
50
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
stdout_log = log_dir / f"{project}-{agent}.stdout.log"
|
|
52
|
+
stderr_log = log_dir / f"{project}-{agent}.stderr.log"
|
|
53
|
+
|
|
54
|
+
# Build the command
|
|
55
|
+
if " -m " in meshcode_bin:
|
|
56
|
+
parts = meshcode_bin.split()
|
|
57
|
+
program_args = parts + ["run", project, agent]
|
|
58
|
+
else:
|
|
59
|
+
program_args = [meshcode_bin, "run", project, agent]
|
|
60
|
+
|
|
61
|
+
args_xml = "\n".join(f" <string>{a}</string>" for a in program_args)
|
|
62
|
+
|
|
63
|
+
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
64
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
65
|
+
<plist version="1.0">
|
|
66
|
+
<dict>
|
|
67
|
+
<key>Label</key>
|
|
68
|
+
<string>{label}</string>
|
|
69
|
+
<key>ProgramArguments</key>
|
|
70
|
+
<array>
|
|
71
|
+
{args_xml}
|
|
72
|
+
</array>
|
|
73
|
+
<key>KeepAlive</key>
|
|
74
|
+
<true/>
|
|
75
|
+
<key>ThrottleInterval</key>
|
|
76
|
+
<integer>5</integer>
|
|
77
|
+
<key>StandardOutPath</key>
|
|
78
|
+
<string>{stdout_log}</string>
|
|
79
|
+
<key>StandardErrorPath</key>
|
|
80
|
+
<string>{stderr_log}</string>
|
|
81
|
+
<key>EnvironmentVariables</key>
|
|
82
|
+
<dict>
|
|
83
|
+
<key>PATH</key>
|
|
84
|
+
<string>/usr/local/bin:/usr/bin:/bin:{os.path.dirname(sys.executable)}</string>
|
|
85
|
+
</dict>
|
|
86
|
+
</dict>
|
|
87
|
+
</plist>
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def start(project: str, agent: str) -> None:
|
|
92
|
+
"""Install and start a launchd supervisor for an agent."""
|
|
93
|
+
if platform.system() != "Darwin":
|
|
94
|
+
print("[meshcode supervisor] launchd is macOS only. Use --simple for other platforms.")
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
plist = _plist_path(project, agent)
|
|
98
|
+
_PLIST_DIR.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
# Write plist
|
|
101
|
+
plist.write_text(_generate_plist(project, agent))
|
|
102
|
+
print(f"[meshcode supervisor] Plist written: {plist}")
|
|
103
|
+
|
|
104
|
+
# Load it
|
|
105
|
+
result = subprocess.run(
|
|
106
|
+
["launchctl", "load", str(plist)],
|
|
107
|
+
capture_output=True, text=True
|
|
108
|
+
)
|
|
109
|
+
if result.returncode == 0:
|
|
110
|
+
print(f"[meshcode supervisor] Agent {agent}@{project} supervised (KeepAlive=true)")
|
|
111
|
+
print(f"[meshcode supervisor] Logs: ~/.meshcode/logs/{project}-{agent}.stderr.log")
|
|
112
|
+
else:
|
|
113
|
+
print(f"[meshcode supervisor] ERROR: launchctl load failed: {result.stderr.strip()}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def stop(project: str, agent: str) -> None:
|
|
117
|
+
"""Stop and uninstall a supervised agent."""
|
|
118
|
+
plist = _plist_path(project, agent)
|
|
119
|
+
if not plist.exists():
|
|
120
|
+
print(f"[meshcode supervisor] No plist found for {agent}@{project}")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
result = subprocess.run(
|
|
124
|
+
["launchctl", "unload", str(plist)],
|
|
125
|
+
capture_output=True, text=True
|
|
126
|
+
)
|
|
127
|
+
plist.unlink(missing_ok=True)
|
|
128
|
+
print(f"[meshcode supervisor] Agent {agent}@{project} unsupervised")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def status() -> None:
|
|
132
|
+
"""List all supervised agents."""
|
|
133
|
+
if not _PLIST_DIR.exists():
|
|
134
|
+
print("[meshcode supervisor] No supervised agents")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
plists = list(_PLIST_DIR.glob(f"{_PLIST_PREFIX}.*.plist"))
|
|
138
|
+
if not plists:
|
|
139
|
+
print("[meshcode supervisor] No supervised agents")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
print(f"[meshcode supervisor] {len(plists)} supervised agent(s):")
|
|
143
|
+
for p in sorted(plists):
|
|
144
|
+
label = p.stem.replace(f"{_PLIST_PREFIX}.", "")
|
|
145
|
+
# Check if running
|
|
146
|
+
result = subprocess.run(
|
|
147
|
+
["launchctl", "list", f"{_PLIST_PREFIX}.{label}"],
|
|
148
|
+
capture_output=True, text=True
|
|
149
|
+
)
|
|
150
|
+
running = result.returncode == 0
|
|
151
|
+
state = "RUNNING" if running else "STOPPED"
|
|
152
|
+
print(f" {label}: {state}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def simple(project: str, agent: str) -> None:
|
|
156
|
+
"""Simple bash while-loop supervisor (cross-platform fallback)."""
|
|
157
|
+
meshcode_bin = _find_meshcode_bin()
|
|
158
|
+
print(f"[meshcode supervisor] Starting simple supervisor for {agent}@{project}")
|
|
159
|
+
print("[meshcode supervisor] Press Ctrl+C to stop")
|
|
160
|
+
|
|
161
|
+
restart_count = 0
|
|
162
|
+
while True:
|
|
163
|
+
restart_count += 1
|
|
164
|
+
print(f"\n[meshcode supervisor] Starting agent (attempt #{restart_count})...")
|
|
165
|
+
try:
|
|
166
|
+
if " -m " in meshcode_bin:
|
|
167
|
+
parts = meshcode_bin.split()
|
|
168
|
+
cmd = parts + ["run", project, agent]
|
|
169
|
+
else:
|
|
170
|
+
cmd = [meshcode_bin, "run", project, agent]
|
|
171
|
+
result = subprocess.run(cmd)
|
|
172
|
+
print(f"[meshcode supervisor] Agent exited with code {result.returncode}")
|
|
173
|
+
except KeyboardInterrupt:
|
|
174
|
+
print("\n[meshcode supervisor] Stopped by user")
|
|
175
|
+
break
|
|
176
|
+
except Exception as e:
|
|
177
|
+
print(f"[meshcode supervisor] Error: {e}")
|
|
178
|
+
|
|
179
|
+
import time
|
|
180
|
+
backoff = min(2 + restart_count, 10)
|
|
181
|
+
print(f"[meshcode supervisor] Restarting in {backoff}s...")
|
|
182
|
+
try:
|
|
183
|
+
time.sleep(backoff)
|
|
184
|
+
except KeyboardInterrupt:
|
|
185
|
+
print("\n[meshcode supervisor] Stopped by user")
|
|
186
|
+
break
|
|
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
|