@agentsdock/server 1.0.7-beta.5
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.
- package/LICENSE +202 -0
- package/NOTICE +19 -0
- package/README.md +60 -0
- package/npm/cli.cjs +289 -0
- package/package.json +32 -0
- package/server/LICENSE +202 -0
- package/server/NOTICE +19 -0
- package/server/VERSION +1 -0
- package/server/activation_transaction.py +2458 -0
- package/server/agent_server.py +93226 -0
- package/server/agentsdock_chats.py +1237 -0
- package/server/agentsdock_emergency.py +338 -0
- package/server/agentsdock_jobs.py +640 -0
- package/server/agentsdock_mail.py +325 -0
- package/server/agentsdock_publish.py +404 -0
- package/server/agentsdock_team.py +793 -0
- package/server/agentsdock_team_hub/__init__.py +33 -0
- package/server/agentsdock_team_hub/auth.py +710 -0
- package/server/agentsdock_team_hub/cli.py +647 -0
- package/server/agentsdock_team_hub/database.py +310 -0
- package/server/agentsdock_team_hub/mail_hint_streams.py +176 -0
- package/server/agentsdock_team_hub/mail_hints.py +201 -0
- package/server/agentsdock_team_hub/migrations/0001_identity_auth.sql +690 -0
- package/server/agentsdock_team_hub/migrations/0002_teamspace_ledger.sql +1336 -0
- package/server/agentsdock_team_hub/migrations/0003_service_runtime.sql +361 -0
- package/server/agentsdock_team_hub/migrations/0004_managed_host_binding.sql +22 -0
- package/server/agentsdock_team_hub/migrations/0005_tailnet_bootstrap_delegations.sql +63 -0
- package/server/agentsdock_team_hub/migrations/0006_team_network_mailbox.sql +423 -0
- package/server/agentsdock_team_hub/migrations/0007_local_agent_mail.sql +63 -0
- package/server/agentsdock_team_hub/migrations/0008_managed_server_session.sql +73 -0
- package/server/agentsdock_team_hub/migrations/0009_team_messages.sql +324 -0
- package/server/agentsdock_team_hub/migrations/0010_team_attachment_orphan_reclamation.sql +52 -0
- package/server/agentsdock_team_hub/migrations/0011_human_admin_paging.sql +65 -0
- package/server/agentsdock_team_hub/migrations/0012_network_content_deletions.sql +69 -0
- package/server/agentsdock_team_hub/migrations/0013_team_message_revisions.sql +48 -0
- package/server/agentsdock_team_hub/migrations/0014_managed_network_owner.sql +50 -0
- package/server/agentsdock_team_hub/migrations/0015_team_message_inbox_dismissals.sql +3 -0
- package/server/agentsdock_team_hub/migrations/0016_team_message_all_servers.sql +4 -0
- package/server/agentsdock_team_hub/migrations/0017_skill_announcement_deletions.sql +16 -0
- package/server/agentsdock_team_hub/migrations/0018_team_mail_subjects.sql +8 -0
- package/server/agentsdock_team_hub/migrations/0019_team_mailbox_state.sql +5 -0
- package/server/agentsdock_team_hub/migrations/0020_team_mail_arrivals.sql +44 -0
- package/server/agentsdock_team_hub/migrations/0021_team_mail_threads.sql +4 -0
- package/server/agentsdock_team_hub/migrations/0022_team_bulletin_changes.sql +49 -0
- package/server/agentsdock_team_hub/migrations/0023_team_message_search.sql +93 -0
- package/server/agentsdock_team_hub/migrations/__init__.py +1 -0
- package/server/agentsdock_team_hub/notification_hints.py +306 -0
- package/server/agentsdock_team_hub/secure_peer.py +11215 -0
- package/server/agentsdock_team_hub/secure_peer_hub.py +1372 -0
- package/server/agentsdock_team_hub/security.py +318 -0
- package/server/agentsdock_team_hub/service.py +1737 -0
- package/server/agentsdock_team_hub/store.py +16276 -0
- package/server/chat_mailbox.py +448 -0
- package/server/claude_background_reconciliation.py +92 -0
- package/server/claude_goals.py +245 -0
- package/server/claude_history_provenance.py +137 -0
- package/server/claude_history_repair.py +1601 -0
- package/server/claude_sdk_client.py +3731 -0
- package/server/claude_side_question.py +212 -0
- package/server/codex_app_server.py +3030 -0
- package/server/codex_auth.py +93 -0
- package/server/codex_history_repair.py +1247 -0
- package/server/codex_provider.py +1120 -0
- package/server/codex_side_question.py +301 -0
- package/server/cursor_agent_client.py +654 -0
- package/server/cursor_process_guard.py +347 -0
- package/server/execution_activation.py +610 -0
- package/server/execution_control.py +233 -0
- package/server/execution_durability.py +184 -0
- package/server/execution_http.py +256 -0
- package/server/execution_install.py +792 -0
- package/server/execution_legacy_runner.py +406 -0
- package/server/execution_maintenance.py +137 -0
- package/server/execution_manage.py +615 -0
- package/server/execution_ownership.py +83 -0
- package/server/execution_preparation.py +362 -0
- package/server/execution_recovery.py +845 -0
- package/server/execution_recovery_status.py +195 -0
- package/server/execution_service.py +387 -0
- package/server/execution_transport.py +622 -0
- package/server/execution_uninstall.py +406 -0
- package/server/execution_update_status.py +49 -0
- package/server/install.sh +7074 -0
- package/server/interactive_chat_controls.py +153 -0
- package/server/interactive_chat_native.py +66 -0
- package/server/interactive_chat_projection.py +152 -0
- package/server/interactive_chat_runtime.py +132 -0
- package/server/interactive_chat_share_routes.py +534 -0
- package/server/interactive_chat_share_web.py +12 -0
- package/server/interactive_chat_shares.py +238 -0
- package/server/provider_commands.py +425 -0
- package/server/public_chat_share_routes.py +426 -0
- package/server/public_chat_shares.py +759 -0
- package/server/public_chat_transcript.py +357 -0
- package/server/pyproject.toml +29 -0
- package/server/release-public-key.pem +3 -0
- package/server/secure_peer_delivery.py +1236 -0
- package/server/secure_peer_runtime.py +7694 -0
- package/server/shared_chat_video_stream.py +262 -0
- package/server/shared_chat_videos.py +155 -0
- package/server/side_questions.py +594 -0
- package/server/team_hub_host.py +1354 -0
- package/server/team_mail_grants.py +136 -0
- package/server/team_mail_runtime.py +302 -0
- package/server/team_mail_websocket.py +211 -0
- package/server/title_generation.py +333 -0
- package/server/uninstall.sh +359 -0
- package/server/update_handoff.py +237 -0
- package/server/update_preparation.py +264 -0
- package/server/update_recovery.py +140 -0
- package/server/update_runner.py +1819 -0
- package/server/uv.lock +1101 -0
- package/server/workspace_git.py +545 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""Small parent-death guard for a Cursor CLI subprocess.
|
|
2
|
+
|
|
3
|
+
AgentsServer launches this helper in its own process group. The helper launches
|
|
4
|
+
Cursor in a child group, forwards inherited stdio unchanged, and terminates the
|
|
5
|
+
entire Cursor tree when its leader exits or AgentsServer disappears abruptly.
|
|
6
|
+
This prevents a detached agent from continuing workspace edits after the turn.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import signal
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from threading import Event
|
|
17
|
+
from typing import NamedTuple
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
POLL_SECONDS = 0.02
|
|
21
|
+
DESCENDANT_SNAPSHOT_INTERVAL_SECONDS = 0.1
|
|
22
|
+
TERMINATE_GRACE_SECONDS = 0.5
|
|
23
|
+
# The server gives this wrapper a bounded grace window before killing it. Keep
|
|
24
|
+
# each process-table probe well below that outer deadline even if ``ps`` wedges.
|
|
25
|
+
PROCESS_SNAPSHOT_TIMEOUT_SECONDS = 0.2
|
|
26
|
+
MAX_PROCESS_SNAPSHOT_ROWS = 32_768
|
|
27
|
+
MAX_CAPTURED_DESCENDANTS = 4_096
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _ProcessIdentity(NamedTuple):
|
|
31
|
+
pid: int
|
|
32
|
+
parent_pid: int
|
|
33
|
+
process_group: int
|
|
34
|
+
started_at: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _process_snapshot() -> dict[int, _ProcessIdentity]:
|
|
38
|
+
"""Return one bounded POSIX process snapshot with stable-enough identities.
|
|
39
|
+
|
|
40
|
+
``lstart`` is intentionally retained as a PID-reuse fence. Before sending
|
|
41
|
+
an individual signal we require both the PID and captured start marker to
|
|
42
|
+
match a fresh snapshot, so a descendant that exits during the grace period
|
|
43
|
+
cannot cause its recycled PID to be killed.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
ps_path = next(
|
|
47
|
+
(candidate for candidate in ("/bin/ps", "/usr/bin/ps") if os.path.isfile(candidate)),
|
|
48
|
+
None,
|
|
49
|
+
)
|
|
50
|
+
if ps_path is None:
|
|
51
|
+
return {}
|
|
52
|
+
env = dict(os.environ)
|
|
53
|
+
env["LC_ALL"] = "C"
|
|
54
|
+
try:
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
[ps_path, "-axo", "pid=,ppid=,pgid=,lstart="],
|
|
57
|
+
text=True,
|
|
58
|
+
capture_output=True,
|
|
59
|
+
timeout=PROCESS_SNAPSHOT_TIMEOUT_SECONDS,
|
|
60
|
+
check=False,
|
|
61
|
+
env=env,
|
|
62
|
+
)
|
|
63
|
+
except (OSError, subprocess.SubprocessError):
|
|
64
|
+
return {}
|
|
65
|
+
if result.returncode != 0:
|
|
66
|
+
return {}
|
|
67
|
+
|
|
68
|
+
snapshot: dict[int, _ProcessIdentity] = {}
|
|
69
|
+
for index, raw_line in enumerate(result.stdout.splitlines()):
|
|
70
|
+
if index >= MAX_PROCESS_SNAPSHOT_ROWS:
|
|
71
|
+
break
|
|
72
|
+
parts = raw_line.strip().split(None, 3)
|
|
73
|
+
if len(parts) != 4:
|
|
74
|
+
continue
|
|
75
|
+
try:
|
|
76
|
+
pid, parent_pid, process_group = map(int, parts[:3])
|
|
77
|
+
except ValueError:
|
|
78
|
+
continue
|
|
79
|
+
if pid <= 1 or parent_pid < 0 or process_group <= 0:
|
|
80
|
+
continue
|
|
81
|
+
snapshot[pid] = _ProcessIdentity(
|
|
82
|
+
pid,
|
|
83
|
+
parent_pid,
|
|
84
|
+
process_group,
|
|
85
|
+
parts[3],
|
|
86
|
+
)
|
|
87
|
+
return snapshot
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _recursive_descendants(
|
|
91
|
+
root_pid: int,
|
|
92
|
+
snapshot: dict[int, _ProcessIdentity],
|
|
93
|
+
) -> tuple[_ProcessIdentity, ...]:
|
|
94
|
+
return _recursive_descendants_from_roots((root_pid,), snapshot)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _recursive_descendants_from_roots(
|
|
98
|
+
root_pids: tuple[int, ...],
|
|
99
|
+
snapshot: dict[int, _ProcessIdentity],
|
|
100
|
+
) -> tuple[_ProcessIdentity, ...]:
|
|
101
|
+
children: dict[int, list[_ProcessIdentity]] = {}
|
|
102
|
+
for identity in snapshot.values():
|
|
103
|
+
children.setdefault(identity.parent_pid, []).append(identity)
|
|
104
|
+
|
|
105
|
+
descendants: list[_ProcessIdentity] = []
|
|
106
|
+
pending = list(root_pids)
|
|
107
|
+
seen = set(root_pids)
|
|
108
|
+
while pending and len(descendants) < MAX_CAPTURED_DESCENDANTS:
|
|
109
|
+
parent_pid = pending.pop()
|
|
110
|
+
for identity in children.get(parent_pid, ()):
|
|
111
|
+
if identity.pid in seen:
|
|
112
|
+
continue
|
|
113
|
+
seen.add(identity.pid)
|
|
114
|
+
descendants.append(identity)
|
|
115
|
+
pending.append(identity.pid)
|
|
116
|
+
if len(descendants) >= MAX_CAPTURED_DESCENDANTS:
|
|
117
|
+
break
|
|
118
|
+
return tuple(descendants)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _matching_survivors(
|
|
122
|
+
identities: tuple[_ProcessIdentity, ...],
|
|
123
|
+
snapshot: dict[int, _ProcessIdentity],
|
|
124
|
+
) -> tuple[_ProcessIdentity, ...]:
|
|
125
|
+
return tuple(
|
|
126
|
+
identity
|
|
127
|
+
for identity in identities
|
|
128
|
+
if (
|
|
129
|
+
(current := snapshot.get(identity.pid)) is not None
|
|
130
|
+
and current.started_at == identity.started_at
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _signal_identities(
|
|
136
|
+
identities: tuple[_ProcessIdentity, ...],
|
|
137
|
+
signum: int,
|
|
138
|
+
snapshot: dict[int, _ProcessIdentity],
|
|
139
|
+
) -> None:
|
|
140
|
+
guard_pid = os.getpid()
|
|
141
|
+
parent_pid = os.getppid()
|
|
142
|
+
for identity in _matching_survivors(identities, snapshot):
|
|
143
|
+
if identity.pid in {guard_pid, parent_pid}:
|
|
144
|
+
continue
|
|
145
|
+
try:
|
|
146
|
+
os.kill(identity.pid, signum)
|
|
147
|
+
except (OSError, ProcessLookupError):
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _track_child_processes(
|
|
152
|
+
child_pid: int,
|
|
153
|
+
child_group: int,
|
|
154
|
+
known: dict[int, _ProcessIdentity],
|
|
155
|
+
snapshot: dict[int, _ProcessIdentity],
|
|
156
|
+
*,
|
|
157
|
+
child_alive: bool,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Accumulate bounded child identities before they can be reparented.
|
|
160
|
+
|
|
161
|
+
Same-group processes remain discoverable after the Cursor leader exits.
|
|
162
|
+
Detached descendants do not, so every still-matching captured descendant is
|
|
163
|
+
also treated as a root while the leader is alive. This follows children of
|
|
164
|
+
a previously detached tool without trusting a recycled PID.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
roots: list[int] = [child_pid] if child_alive else []
|
|
168
|
+
roots.extend(
|
|
169
|
+
identity.pid
|
|
170
|
+
for identity in known.values()
|
|
171
|
+
if (
|
|
172
|
+
(current := snapshot.get(identity.pid)) is not None
|
|
173
|
+
and current.started_at == identity.started_at
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
# A normal leader exit reparents children before the next snapshot, but
|
|
177
|
+
# members of its dedicated process group remain attributable to this run.
|
|
178
|
+
discovered: dict[int, _ProcessIdentity] = {}
|
|
179
|
+
for identity in snapshot.values():
|
|
180
|
+
if identity.pid != child_pid and identity.process_group == child_group:
|
|
181
|
+
discovered.setdefault(identity.pid, identity)
|
|
182
|
+
if len(discovered) >= MAX_CAPTURED_DESCENDANTS:
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
for identity in _recursive_descendants_from_roots(
|
|
186
|
+
tuple(roots),
|
|
187
|
+
snapshot,
|
|
188
|
+
):
|
|
189
|
+
discovered.setdefault(identity.pid, identity)
|
|
190
|
+
if len(discovered) >= MAX_CAPTURED_DESCENDANTS:
|
|
191
|
+
break
|
|
192
|
+
|
|
193
|
+
for identity in discovered.values():
|
|
194
|
+
if len(known) >= MAX_CAPTURED_DESCENDANTS:
|
|
195
|
+
break
|
|
196
|
+
previous = known.get(identity.pid)
|
|
197
|
+
if previous is None or previous.started_at == identity.started_at:
|
|
198
|
+
known[identity.pid] = identity
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _terminate_child_group(
|
|
202
|
+
child: subprocess.Popen[bytes],
|
|
203
|
+
group: int,
|
|
204
|
+
tracked_descendants: tuple[_ProcessIdentity, ...] = (),
|
|
205
|
+
) -> None:
|
|
206
|
+
known = {identity.pid: identity for identity in tracked_descendants}
|
|
207
|
+
|
|
208
|
+
initial_snapshot = _process_snapshot()
|
|
209
|
+
_track_child_processes(
|
|
210
|
+
child.pid,
|
|
211
|
+
group,
|
|
212
|
+
known,
|
|
213
|
+
initial_snapshot,
|
|
214
|
+
child_alive=child.poll() is None,
|
|
215
|
+
)
|
|
216
|
+
descendants = tuple(known.values())
|
|
217
|
+
detached = tuple(
|
|
218
|
+
identity for identity in descendants if identity.process_group != group
|
|
219
|
+
)
|
|
220
|
+
group_members = tuple(
|
|
221
|
+
identity for identity in descendants if identity.process_group == group
|
|
222
|
+
)
|
|
223
|
+
if child.poll() is None or group_members:
|
|
224
|
+
try:
|
|
225
|
+
os.killpg(group, signal.SIGTERM)
|
|
226
|
+
except (OSError, ProcessLookupError):
|
|
227
|
+
pass
|
|
228
|
+
# A child can use setsid()/setpgid() for a background shell and escape the
|
|
229
|
+
# Cursor process group. Signal only descendants captured before teardown,
|
|
230
|
+
# and revalidate their start marker immediately before each signal.
|
|
231
|
+
_signal_identities(detached, signal.SIGTERM, _process_snapshot())
|
|
232
|
+
|
|
233
|
+
deadline = time.monotonic() + TERMINATE_GRACE_SECONDS
|
|
234
|
+
survivors: tuple[_ProcessIdentity, ...] = descendants
|
|
235
|
+
while time.monotonic() < deadline:
|
|
236
|
+
snapshot = _process_snapshot()
|
|
237
|
+
_track_child_processes(
|
|
238
|
+
child.pid,
|
|
239
|
+
group,
|
|
240
|
+
known,
|
|
241
|
+
snapshot,
|
|
242
|
+
child_alive=child.poll() is None,
|
|
243
|
+
)
|
|
244
|
+
descendants = tuple(known.values())
|
|
245
|
+
survivors = _matching_survivors(descendants, snapshot)
|
|
246
|
+
if child.poll() is not None and not survivors:
|
|
247
|
+
return
|
|
248
|
+
time.sleep(0.05)
|
|
249
|
+
|
|
250
|
+
final_snapshot = _process_snapshot()
|
|
251
|
+
_track_child_processes(
|
|
252
|
+
child.pid,
|
|
253
|
+
group,
|
|
254
|
+
known,
|
|
255
|
+
final_snapshot,
|
|
256
|
+
child_alive=child.poll() is None,
|
|
257
|
+
)
|
|
258
|
+
descendants = tuple(known.values())
|
|
259
|
+
survivors = _matching_survivors(descendants, final_snapshot)
|
|
260
|
+
group_survives = child.poll() is None or any(
|
|
261
|
+
final_snapshot[identity.pid].process_group == group
|
|
262
|
+
for identity in survivors
|
|
263
|
+
)
|
|
264
|
+
if group_survives:
|
|
265
|
+
try:
|
|
266
|
+
os.killpg(group, signal.SIGKILL)
|
|
267
|
+
except (OSError, ProcessLookupError):
|
|
268
|
+
pass
|
|
269
|
+
_signal_identities(
|
|
270
|
+
tuple(
|
|
271
|
+
identity
|
|
272
|
+
for identity in survivors
|
|
273
|
+
if final_snapshot[identity.pid].process_group != group
|
|
274
|
+
),
|
|
275
|
+
signal.SIGKILL,
|
|
276
|
+
final_snapshot,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def main(argv: list[str]) -> int:
|
|
281
|
+
if len(argv) < 4 or argv[0] != "--parent-pid" or argv[2] != "--":
|
|
282
|
+
return 64
|
|
283
|
+
try:
|
|
284
|
+
expected_parent = int(argv[1])
|
|
285
|
+
except ValueError:
|
|
286
|
+
return 64
|
|
287
|
+
if expected_parent <= 1:
|
|
288
|
+
return 64
|
|
289
|
+
command = argv[3:]
|
|
290
|
+
if not command:
|
|
291
|
+
return 64
|
|
292
|
+
|
|
293
|
+
stop = Event()
|
|
294
|
+
|
|
295
|
+
def request_stop(_signum: int, _frame: object) -> None:
|
|
296
|
+
stop.set()
|
|
297
|
+
|
|
298
|
+
signal.signal(signal.SIGTERM, request_stop)
|
|
299
|
+
signal.signal(signal.SIGINT, request_stop)
|
|
300
|
+
signal.signal(signal.SIGHUP, request_stop)
|
|
301
|
+
|
|
302
|
+
# The server can disappear after spawning this wrapper but before Python
|
|
303
|
+
# reaches Popen. Do not briefly launch an editing agent in that window.
|
|
304
|
+
if stop.is_set() or os.getppid() != expected_parent:
|
|
305
|
+
return 143
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
child = subprocess.Popen(command, start_new_session=True)
|
|
309
|
+
except OSError:
|
|
310
|
+
return 127
|
|
311
|
+
|
|
312
|
+
interrupted = False
|
|
313
|
+
child_group = child.pid
|
|
314
|
+
tracked_descendants: dict[int, _ProcessIdentity] = {}
|
|
315
|
+
next_descendant_snapshot = 0.0
|
|
316
|
+
while True:
|
|
317
|
+
now = time.monotonic()
|
|
318
|
+
child_alive = child.poll() is None
|
|
319
|
+
if now >= next_descendant_snapshot:
|
|
320
|
+
snapshot = _process_snapshot()
|
|
321
|
+
child_alive = child.poll() is None
|
|
322
|
+
_track_child_processes(
|
|
323
|
+
child.pid,
|
|
324
|
+
child_group,
|
|
325
|
+
tracked_descendants,
|
|
326
|
+
snapshot,
|
|
327
|
+
child_alive=child_alive,
|
|
328
|
+
)
|
|
329
|
+
next_descendant_snapshot = (
|
|
330
|
+
now + DESCENDANT_SNAPSHOT_INTERVAL_SECONDS
|
|
331
|
+
)
|
|
332
|
+
if not child_alive:
|
|
333
|
+
break
|
|
334
|
+
if stop.is_set() or os.getppid() != expected_parent:
|
|
335
|
+
interrupted = True
|
|
336
|
+
break
|
|
337
|
+
time.sleep(POLL_SECONDS)
|
|
338
|
+
_terminate_child_group(
|
|
339
|
+
child,
|
|
340
|
+
child_group,
|
|
341
|
+
tuple(tracked_descendants.values()),
|
|
342
|
+
)
|
|
343
|
+
return 143 if interrupted else int(child.returncode or 0)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
if __name__ == "__main__":
|
|
347
|
+
raise SystemExit(main(sys.argv[1:]))
|