gemini-computer-use 0.4.3 → 0.4.4

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/install.sh DELETED
@@ -1,2013 +0,0 @@
1
- #!/usr/bin/env bash
2
- # ==============================================================================
3
- # Antigravity Mesh - Universal Turnkey Installer (v0.1.2)
4
- # Bilingual: English (Default) & Russian, Device Detection, SSH & Autostart
5
- # Targets: Linux, macOS, Windows (Git Bash) and Android/Termux (phone/tablet)
6
- # ==============================================================================
7
- set -e
8
-
9
- # Defaults
10
- MODE=""
11
- TLS="none"
12
- PORT="8096"
13
- DOMAIN=""
14
- GATEWAY=""
15
- USERNAME=""
16
- TOKEN=""
17
- DRY_RUN=false
18
- QUICK=false
19
- SSH_TARGET=""
20
- SSH_PORT="22"
21
- LANG_CHOICE=""
22
- EXPLICIT_LANG=""
23
-
24
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
25
- [ -z "$SCRIPT_DIR" ] && SCRIPT_DIR="."
26
-
27
- # ------------------------------------------------------------------------------
28
- # PUBLIC DOMAIN - resolved from the one source of truth
29
- #
30
- # The domain is declared exactly once, in core/domain.py. Everything below asks
31
- # that same chain, in the same order, and this is the ONLY place in this script
32
- # that picks a hostname:
33
- #
34
- # 1. an explicit --domain= / --gateway= on the command line;
35
- # 2. MESH_PUBLIC_URL in the environment;
36
- # 3. AGY_PUBLIC_BASE_URL in the environment (legacy alias, same meaning);
37
- # 4. the domain file - MESH_DOMAIN_FILE when set, otherwise
38
- # /etc/antigravity-mesh/domain.env, or on Windows/Git Bash
39
- # %USERPROFILE%\.config\antigravity-mesh\domain.env;
40
- # 5. $PUBLISHED_DOMAIN - the domain this copy was published with. The repository
41
- # carries the placeholder __MESH_DOMAIN__ here and deploy_gateway.sh rewrites
42
- # that line while publishing, so a served copy already knows its domain;
43
- # 6. core/domain.py's DEFAULT_PUBLIC_BASE_URL, but only when that module sits
44
- # next to this script (a repository checkout, or a node that already
45
- # bootstrapped). A clone has no published value and refusing to install from
46
- # a clone would be wrong.
47
- #
48
- # If none of them yields a real hostname the installer stops: registering a node
49
- # against a domain that does not resolve produces a node that can never connect
50
- # and an autostart entry that retries forever.
51
- # ------------------------------------------------------------------------------
52
- DOMAIN_PLACEHOLDER="__MESH_DOMAIN__"
53
- # deploy_gateway.sh rewrites THIS line only (it is anchored on PUBLISHED_DOMAIN=),
54
- # so the placeholder token above survives and still proves whether this copy was
55
- # published. In the repository the two are equal, which means "not published".
56
- PUBLISHED_DOMAIN="__MESH_DOMAIN__"
57
-
58
- # ------------------------------------------------------------------------------
59
- # TERMUX / ANDROID
60
- #
61
- # Termux is NOT FHS-compliant: there is no /etc, no /bin and no /usr at the
62
- # usual locations, there is no root and no sudo, and its packages are built
63
- # against Android's bionic libc. A phone is therefore neither "just Linux" (the
64
- # Debian package names and the host-level domain file do not exist) nor
65
- # something the Linux branches can serve unchanged. Everything Termux-specific
66
- # in this script keys off the single flag below:
67
- #
68
- # IS_TERMUX - the one question every branch asks;
69
- # TERMUX_PREFIX - $PREFIX, the Termux root (.../com.termux/files/usr);
70
- # TERMUX_SUPERVISED - set by configure_termux_node when runit supervises.
71
- #
72
- # Detection never trusts one signal: Termux exports TERMUX_VERSION, its $PREFIX
73
- # points into com.termux, and its application data directory has a fixed path.
74
- # A Linux host that happens to export an unrelated $PREFIX is not a phone.
75
- # ------------------------------------------------------------------------------
76
- IS_TERMUX=false
77
- if [ -n "${TERMUX_VERSION:-}" ]; then
78
- IS_TERMUX=true
79
- fi
80
- if [ "$IS_TERMUX" = false ] && [ -n "${PREFIX:-}" ]; then
81
- case "$PREFIX" in
82
- *com.termux*) IS_TERMUX=true ;;
83
- esac
84
- fi
85
- if [ "$IS_TERMUX" = false ] && [ -d /data/data/com.termux/files/usr/bin ]; then
86
- IS_TERMUX=true
87
- fi
88
-
89
- TERMUX_PREFIX=""
90
- TERMUX_SUPERVISED=false
91
- if [ "$IS_TERMUX" = true ]; then
92
- TERMUX_PREFIX="${PREFIX:-}"
93
- if [ -z "$TERMUX_PREFIX" ] || [ ! -d "$TERMUX_PREFIX/bin" ]; then
94
- TERMUX_PREFIX="/data/data/com.termux/files/usr"
95
- fi
96
- fi
97
-
98
- case "$(uname -s 2>/dev/null)" in
99
- MINGW*|MSYS*|CYGWIN*) DEFAULT_DOMAIN_DIR="${USERPROFILE:-$HOME}/.config/antigravity-mesh" ;;
100
- *) DEFAULT_DOMAIN_DIR="/etc/antigravity-mesh" ;;
101
- esac
102
- # A phone has no /etc and no root, so the per-user directory is the only place
103
- # that can hold the domain file there. core/domain.py answers the same question
104
- # in default_domain_file(), so the installer and the node never disagree about
105
- # where the domain lives.
106
- if [ "$IS_TERMUX" = true ]; then
107
- DEFAULT_DOMAIN_DIR="$HOME/.config/antigravity-mesh"
108
- fi
109
- DEFAULT_DOMAIN_FILE="${MESH_DOMAIN_FILE:-$DEFAULT_DOMAIN_DIR/domain.env}"
110
-
111
- # The command that stores the domain, spelled for the platform it is printed on:
112
- # there is no sudo in Termux and no writable /etc, so the hint must not send a
113
- # phone user to a command that cannot exist there.
114
- domain_file_write_cmd() {
115
- if [ "$IS_TERMUX" = true ]; then
116
- printf "mkdir -p %s && echo 'MESH_PUBLIC_URL=%s' > %s" \
117
- "$DEFAULT_DOMAIN_DIR" "$1" "$DEFAULT_DOMAIN_FILE"
118
- else
119
- printf "echo 'MESH_PUBLIC_URL=%s' | sudo tee %s" "$1" "$DEFAULT_DOMAIN_FILE"
120
- fi
121
- }
122
-
123
- # https://mesh.example.com/path/ -> mesh.example.com (the authority only)
124
- normalise_host() {
125
- printf '%s' "$1" | sed -e 's|^[A-Za-z][A-Za-z0-9+.-]*://||' -e 's|/.*$||' \
126
- -e 's|^[[:space:]]*||' -e 's|[[:space:]]*$||'
127
- }
128
-
129
- # A KEY=VALUE file without its leading UTF-8 BOM (Windows PowerShell writes one).
130
- strip_bom() {
131
- if [ "$(head -c 3 "$1" 2>/dev/null | od -An -tx1 | tr -d ' \n')" = "efbbbf" ]; then
132
- tail -c +4 "$1"
133
- else
134
- cat "$1"
135
- fi
136
- }
137
-
138
- # MESH_PUBLIC_URL outranks the legacy alias, whatever their order in the file.
139
- domain_file_value() {
140
- local file="$1" value=""
141
- if [ ! -f "$file" ]; then
142
- return 0
143
- fi
144
- value="$(strip_bom "$file" | sed -n 's/^[[:space:]]*MESH_PUBLIC_URL[[:space:]]*=[[:space:]]*//p' | head -n 1)"
145
- if [ -z "$value" ]; then
146
- value="$(strip_bom "$file" | sed -n 's/^[[:space:]]*AGY_PUBLIC_BASE_URL[[:space:]]*=[[:space:]]*//p' | head -n 1)"
147
- fi
148
- printf '%s' "$value" | tr -d '"' | tr -d "'" | tr -d '\r' | sed -e 's|^[[:space:]]*||' -e 's|[[:space:]]*$||'
149
- }
150
-
151
- # The project default, taken from core/domain.py when that file is available here.
152
- module_default_domain() {
153
- local module="$SCRIPT_DIR/core/domain.py" value=""
154
- if [ ! -f "$module" ]; then
155
- return 0
156
- fi
157
- if command -v python3 >/dev/null 2>&1; then
158
- value="$(cd "$SCRIPT_DIR" 2>/dev/null && python3 -c 'from core import domain; print(domain.DEFAULT_PUBLIC_BASE_URL)' 2>/dev/null)" || true
159
- fi
160
- if [ -z "$value" ]; then
161
- value="$(sed -n 's/^DEFAULT_PUBLIC_BASE_URL[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$module" | head -n 1)"
162
- fi
163
- printf '%s' "$value"
164
- }
165
-
166
- resolve_domain() {
167
- local explicit_value="${1:-}" candidate=""
168
- if [ -n "$explicit_value" ]; then
169
- printf '%s' "$(normalise_host "$explicit_value")"
170
- return 0
171
- fi
172
- for candidate in "${MESH_PUBLIC_URL:-}" "${AGY_PUBLIC_BASE_URL:-}"; do
173
- if [ -n "$candidate" ]; then
174
- printf '%s' "$(normalise_host "$candidate")"
175
- return 0
176
- fi
177
- done
178
- candidate="$(domain_file_value "$DEFAULT_DOMAIN_FILE")"
179
- if [ -n "$candidate" ]; then
180
- printf '%s' "$(normalise_host "$candidate")"
181
- return 0
182
- fi
183
- if [ -n "$PUBLISHED_DOMAIN" ] && [ "$PUBLISHED_DOMAIN" != "$DOMAIN_PLACEHOLDER" ]; then
184
- printf '%s' "$(normalise_host "$PUBLISHED_DOMAIN")"
185
- return 0
186
- fi
187
- candidate="$(module_default_domain)"
188
- if [ -n "$candidate" ]; then
189
- printf '%s' "$(normalise_host "$candidate")"
190
- return 0
191
- fi
192
- printf '%s' "$DOMAIN_PLACEHOLDER"
193
- }
194
-
195
- # ------------------------------------------------------------------------------
196
- # PERSIST THE DOMAIN (what the node reads to build its public links)
197
- #
198
- # core/domain.py resolves the public domain from MESH_PUBLIC_URL in the
199
- # environment, then from the domain file, then from its built-in default - and it
200
- # never reads the legacy MESH_GATEWAY that agent.env carries. A node installed
201
- # with only agent.env therefore dials the right gateway (gateway_host() does
202
- # honour MESH_GATEWAY) while minting share links on the built-in default: a real
203
- # Termux node published links on the default relay while its tunnel went to the
204
- # gateway it was actually registered on.
205
- #
206
- # This script has already resolved the domain, so it writes it down here - the
207
- # same thing deploy_gateway.sh does for /etc/antigravity-mesh/domain.env on the
208
- # gateway host. The place that decides the domain is then also the place that
209
- # records it, and the node's links, its tunnel and the gateway agree by
210
- # construction.
211
- #
212
- # Never fatal: an unwritable /etc (no sudo, a locked-down box) still leaves a
213
- # working node - the legacy MESH_GATEWAY keeps the tunnel where it was - so a
214
- # failure only prints the command the operator can run by hand.
215
- # ------------------------------------------------------------------------------
216
- write_domain_file() {
217
- local domain existing content module_default parent
218
- domain="$(normalise_host "${1:-}")"
219
- if [ -z "$domain" ] || [ "$domain" = "$DOMAIN_PLACEHOLDER" ]; then
220
- return 0
221
- fi
222
- # Never record the project's built-in default. It is a fallback, not a decision:
223
- # writing it would turn "nothing is configured" into a pinned domain file, and
224
- # once that file exists core/domain.py stops consulting the legacy MESH_GATEWAY
225
- # at all - the operator's own agent.env knob would silently go inert. Nothing is
226
- # lost by skipping it: with no file the resolver's last fallback IS that value,
227
- # so the node's links and its tunnel still name the same host.
228
- module_default="$(normalise_host "$(module_default_domain)")"
229
- if [ -n "$module_default" ] && [ "$domain" = "$module_default" ]; then
230
- return 0
231
- fi
232
- existing="$(normalise_host "$(domain_file_value "$DEFAULT_DOMAIN_FILE")")"
233
- if [ "$existing" = "$domain" ]; then
234
- return 0
235
- fi
236
- content="MESH_PUBLIC_URL=https://${domain}"
237
- # The parent of the file that is actually used, not of the default path: with
238
- # MESH_DOMAIN_FILE pointing into a directory that does not exist yet, the old
239
- # code created a directory nobody would read and then failed to write.
240
- parent="$(dirname "$DEFAULT_DOMAIN_FILE")"
241
-
242
- if mkdir -p "$parent" 2>/dev/null \
243
- && printf '%s\n' "$content" > "$DEFAULT_DOMAIN_FILE" 2>/dev/null; then
244
- :
245
- elif run_privileged mkdir -p "$parent" >/dev/null 2>&1 \
246
- && printf '%s\n' "$content" | run_privileged tee "$DEFAULT_DOMAIN_FILE" >/dev/null 2>&1; then
247
- :
248
- else
249
- if [ "$LANG_CHOICE" = "ru" ]; then
250
- echo -e "${YELLOW}[!] Не удалось записать домен в ${DEFAULT_DOMAIN_FILE}.${RESET}"
251
- echo -e " Ссылки узла будут строиться на встроенном домене по умолчанию."
252
- echo -e " Запишите вручную: ${CYAN}$(domain_file_write_cmd "https://${domain}")${RESET}"
253
- else
254
- echo -e "${YELLOW}[!] Could not write the domain to ${DEFAULT_DOMAIN_FILE}.${RESET}"
255
- echo -e " The node's links would use the built-in default domain."
256
- echo -e " Write it by hand: ${CYAN}$(domain_file_write_cmd "https://${domain}")${RESET}"
257
- fi
258
- return 0
259
- fi
260
-
261
- if [ "$LANG_CHOICE" = "ru" ]; then
262
- if [ -n "$existing" ]; then
263
- echo -e "${GREEN}[OK] Домен узла: ${content} (${DEFAULT_DOMAIN_FILE}, было https://${existing})${RESET}"
264
- else
265
- echo -e "${GREEN}[OK] Домен узла: ${content} (${DEFAULT_DOMAIN_FILE})${RESET}"
266
- fi
267
- else
268
- if [ -n "$existing" ]; then
269
- echo -e "${GREEN}[OK] Node domain: ${content} (${DEFAULT_DOMAIN_FILE}, was https://${existing})${RESET}"
270
- else
271
- echo -e "${GREEN}[OK] Node domain: ${content} (${DEFAULT_DOMAIN_FILE})${RESET}"
272
- fi
273
- fi
274
- return 0
275
- }
276
-
277
- # Downloading the node code needs the domain, and so does every cloud-gateway
278
- # mode. A standalone install that already has the code next to it needs neither.
279
- domain_required() {
280
- if [ ! -f "$SCRIPT_DIR/core/agent.py" ]; then
281
- return 0
282
- fi
283
- if [ "$MODE" != "standalone" ]; then
284
- return 0
285
- fi
286
- return 1
287
- }
288
-
289
- CONFIG_DIR="$HOME/.config/antigravity-mesh"
290
- CONFIG_FILE="$CONFIG_DIR/agent.env"
291
-
292
- # Colors
293
- BOLD="\033[1m"
294
- GREEN="\033[0;32m"
295
- CYAN="\033[0;36m"
296
- YELLOW="\033[1;33m"
297
- BLUE="\033[0;34m"
298
- MAGENTA="\033[0;35m"
299
- RESET="\033[0m"
300
-
301
- # ------------------------------------------------------------------------------
302
- # PYTHON AND ITS DEPENDENCIES
303
- #
304
- # websockets is the node's ONLY external requirement: core/mcp_tools.py and
305
- # core/server.py are standard library only. Everything below exists because a
306
- # fresh or minimal system can fail to provide it for a different reason each time:
307
- #
308
- # * no Python at all -> install it from the system package manager;
309
- # * Python but no pip -> ensurepip, or uv, which needs no pip;
310
- # * pip refuses to write -> PEP 668 "externally managed", or no permission
311
- # on site-packages: --user, then a venv;
312
- # * no usable package manager -> uv, which fetches its own CPython;
313
- # * nothing works at all -> STOP, like the Windows installer does.
314
- #
315
- # The last point is the important one. This script used to continue with
316
- # PYTHON_BIN=/usr/bin/python3 whether or not that file existed, wrote a systemd
317
- # unit or a launchd plist naming it, and reported success. The node then never
318
- # started and the only evidence was one log line - the exact silent failure the
319
- # Windows installer already refuses to produce.
320
- #
321
- # PYTHON_BIN is the single interpreter everything afterwards must use, including
322
- # the autostart unit. It is NOT necessarily "python3" from PATH: the venv and uv
323
- # fallbacks both live outside PATH on purpose.
324
- # ------------------------------------------------------------------------------
325
- PYTHON_BIN=""
326
- UV_BIN=""
327
-
328
- # Is this a real, runnable Python 3? A name that answers to "command -v" is not
329
- # evidence: a broken shim, a half-removed distro package and a stale symlink all
330
- # pass that test and then fail every call.
331
- python_works() {
332
- [ -n "$1" ] || return 1
333
- command -v "$1" >/dev/null 2>&1 || [ -x "$1" ] || return 1
334
- "$1" -c 'import sys; raise SystemExit(0 if sys.version_info[0] == 3 else 1)' >/dev/null 2>&1
335
- }
336
-
337
- python_has_websockets() {
338
- [ -n "$1" ] || return 1
339
- "$1" -c 'import websockets' >/dev/null 2>&1
340
- }
341
-
342
- python_has_pip() {
343
- [ -n "$1" ] || return 1
344
- "$1" -m pip --version >/dev/null 2>&1
345
- }
346
-
347
- # Every call site after ensure_dependencies uses "$PYTHON_BIN"; this is what finds
348
- # it. The uv-managed interpreter is searched too, because on a machine where uv is
349
- # what provided Python there is no python3 on PATH at all.
350
- resolve_python() {
351
- local cand uv_root
352
- for cand in python3 python; do
353
- if python_works "$cand"; then
354
- PYTHON_BIN="$(command -v "$cand")"
355
- return 0
356
- fi
357
- done
358
- uv_root="${UV_PYTHON_INSTALL_DIR:-$HOME/.local/share/uv/python}"
359
- if [ -d "$uv_root" ]; then
360
- for cand in "$uv_root"/cpython-3*/bin/python3; do
361
- if [ -x "$cand" ] && python_works "$cand"; then
362
- PYTHON_BIN="$cand"
363
- return 0
364
- fi
365
- done
366
- fi
367
- return 1
368
- }
369
-
370
- # sudo when not already root, and nothing at all when neither is available. The old
371
- # code spelled this out per branch and forgot it for apk, so `apk add` silently did
372
- # nothing on a non-root Alpine - the one platform where Python is never preinstalled.
373
- run_privileged() {
374
- if [ "$(id -u)" -eq 0 ]; then
375
- "$@"
376
- elif command -v sudo >/dev/null 2>&1; then
377
- sudo "$@"
378
- else
379
- return 1
380
- fi
381
- }
382
-
383
- # One system-package install per platform, in a subshell with errexit off: a package
384
- # manager that fails (no network, no sudo, a locked database, a dead mirror) must
385
- # fall through to the next source instead of aborting the installer. Whether Python
386
- # actually arrived is decided by resolve_python afterwards, never by these exit codes.
387
- install_python_packages() {
388
- (
389
- set +e
390
- if [ "$IS_TERMUX" = true ]; then
391
- # Termux comes first: it HAS apt-get, but the Debian package names below
392
- # (python3-pip, python3-venv) do not exist there, `pkg` is the wrapper
393
- # that refreshes the index itself, and run_privileged cannot help -
394
- # a phone is never root and has no sudo. pip and the venv wheels are
395
- # separate packages, and python3 is a virtual name provided by python.
396
- pkg install -y python python-pip python-ensurepip-wheels curl ca-certificates
397
- elif command -v apt-get >/dev/null 2>&1; then
398
- run_privileged apt-get update -qq
399
- run_privileged apt-get install -y -qq python3 python3-pip python3-venv curl ca-certificates
400
- elif command -v dnf >/dev/null 2>&1; then
401
- run_privileged dnf install -y -q python3 python3-pip curl ca-certificates
402
- elif command -v yum >/dev/null 2>&1; then
403
- run_privileged yum install -y -q python3 python3-pip curl ca-certificates
404
- elif command -v zypper >/dev/null 2>&1; then
405
- run_privileged zypper --non-interactive install python3 python3-pip curl ca-certificates
406
- elif command -v pacman >/dev/null 2>&1; then
407
- run_privileged pacman -Sy --noconfirm python python-pip curl ca-certificates
408
- elif command -v apk >/dev/null 2>&1; then
409
- run_privileged apk add --no-cache python3 py3-pip curl ca-certificates
410
- elif command -v xbps-install >/dev/null 2>&1; then
411
- run_privileged xbps-install -Sy python3 python3-pip curl ca-certificates
412
- elif [ "$(uname -s 2>/dev/null)" = "Darwin" ] && command -v brew >/dev/null 2>&1; then
413
- brew install python3
414
- else
415
- return 1
416
- fi
417
- return 0
418
- )
419
- }
420
-
421
- # curl is the script's transport, not a Python dependency: the bootstrap download,
422
- # node registration and the standalone health check all go through it. A machine can
423
- # have a perfectly good Python 3 and no curl at all (a minimal WSL image, a slim
424
- # container, a phone where only python was installed), and install_python_packages()
425
- # never runs in that case - it is called only when Python itself is missing. So the
426
- # tool is asked for separately, BEFORE the step that needs it.
427
- install_helper_packages() {
428
- (
429
- set +e
430
- if [ "$IS_TERMUX" = true ]; then
431
- pkg install -y curl ca-certificates
432
- elif command -v apt-get >/dev/null 2>&1; then
433
- run_privileged apt-get update -qq
434
- run_privileged apt-get install -y -qq curl ca-certificates
435
- elif command -v dnf >/dev/null 2>&1; then
436
- run_privileged dnf install -y -q curl ca-certificates
437
- elif command -v yum >/dev/null 2>&1; then
438
- run_privileged yum install -y -q curl ca-certificates
439
- elif command -v zypper >/dev/null 2>&1; then
440
- run_privileged zypper --non-interactive install curl ca-certificates
441
- elif command -v pacman >/dev/null 2>&1; then
442
- run_privileged pacman -Sy --noconfirm curl ca-certificates
443
- elif command -v apk >/dev/null 2>&1; then
444
- run_privileged apk add --no-cache curl ca-certificates
445
- elif command -v xbps-install >/dev/null 2>&1; then
446
- run_privileged xbps-install -Sy curl ca-certificates
447
- elif [ "$(uname -s 2>/dev/null)" = "Darwin" ] && command -v brew >/dev/null 2>&1; then
448
- brew install curl
449
- else
450
- return 1
451
- fi
452
- return 0
453
- )
454
- }
455
-
456
- # True when curl is usable. Installs it if it is not, and fails closed when it cannot
457
- # be obtained: without it the node can never be registered, and the alternative - the
458
- # raw "curl: command not found" at the registration line - names neither the cause nor
459
- # the fix. Called before the bootstrap download, before registration and before the
460
- # standalone health check.
461
- ensure_curl() {
462
- command -v curl >/dev/null 2>&1 && return 0
463
- if [ "$LANG_CHOICE" = "ru" ]; then
464
- echo -e "\033[1;33m[!] curl не найден. Устанавливаю пакеты...\033[0m"
465
- else
466
- echo -e "\033[1;33m[!] curl not found. Installing it...\033[0m"
467
- fi
468
- install_helper_packages || true
469
- command -v curl >/dev/null 2>&1 && return 0
470
-
471
- if [ "$LANG_CHOICE" = "ru" ]; then
472
- echo -e "\033[1;31m[!] Без curl установка невозможна.\033[0m" >&2
473
- echo -e " Через него скачивается код узла, регистрируется узел и проверяется локальный порт." >&2
474
- if [ "$IS_TERMUX" = true ]; then
475
- echo -e " Установите вручную и повторите: ${CYAN}pkg install curl${RESET}" >&2
476
- else
477
- echo -e " Установите вручную и повторите: ${CYAN}sudo apt install curl${RESET} # или ваш менеджер пакетов" >&2
478
- fi
479
- echo -e " Остановка: без curl ничего не создано." >&2
480
- else
481
- echo -e "\033[1;31m[!] curl is required and could not be installed.\033[0m" >&2
482
- echo -e " It downloads the node code, registers the node and checks the local port." >&2
483
- if [ "$IS_TERMUX" = true ]; then
484
- echo -e " Install it and re-run: ${CYAN}pkg install curl${RESET}" >&2
485
- else
486
- echo -e " Install it and re-run: ${CYAN}sudo apt install curl${RESET} # or your package manager" >&2
487
- fi
488
- echo -e " Stopping: nothing was created without it." >&2
489
- fi
490
- return 1
491
- }
492
-
493
- # uv is the last resort: one static binary that fetches its own CPython and installs
494
- # packages without pip. The release archive is downloaded and unpacked here rather
495
- # than piping astral.sh's install script into sh - a script that arrives over the
496
- # network and is executed unread is a worse default than an archive this unpacks.
497
- ensure_uv() {
498
- if command -v uv >/dev/null 2>&1; then
499
- UV_BIN="$(command -v uv)"
500
- return 0
501
- fi
502
- if [ -x "$CONFIG_DIR/bin/uv" ]; then
503
- UV_BIN="$CONFIG_DIR/bin/uv"
504
- return 0
505
- fi
506
- # UV_TARGET is empty on an architecture uv publishes no build for, and asking
507
- # for the wrong one produces an "Exec format error" that names no cause.
508
- [ -n "$UV_TARGET" ] || return 1
509
- command -v curl >/dev/null 2>&1 || return 1
510
- local tmp
511
- tmp="$(mktemp -d 2>/dev/null)" || return 1
512
- if curl -fsSL "https://github.com/astral-sh/uv/releases/latest/download/uv-${UV_TARGET}.tar.gz" \
513
- -o "$tmp/uv.tar.gz" 2>/dev/null; then
514
- if tar -xzf "$tmp/uv.tar.gz" -C "$tmp" 2>/dev/null && [ -f "$tmp/uv-${UV_TARGET}/uv" ]; then
515
- mkdir -p "$CONFIG_DIR/bin"
516
- cp "$tmp/uv-${UV_TARGET}/uv" "$CONFIG_DIR/bin/uv" 2>/dev/null && chmod +x "$CONFIG_DIR/bin/uv"
517
- fi
518
- fi
519
- rm -rf "$tmp"
520
- if [ -x "$CONFIG_DIR/bin/uv" ]; then
521
- UV_BIN="$CONFIG_DIR/bin/uv"
522
- return 0
523
- fi
524
- return 1
525
- }
526
-
527
- uv_pip_install() {
528
- [ -n "$UV_BIN" ] || return 1
529
- "$UV_BIN" pip install --python "$1" websockets >/dev/null 2>&1
530
- }
531
-
532
- # The two packages that make a Termux node behave like a supervised Linux node
533
- # rather than a bare process: termux-services (runit: Restart=always) and
534
- # termux-api (every termux-* command the device branch uses; the Termux:API app
535
- # must be installed as well for these to answer). Both are optional - the install
536
- # degrades instead of failing - so this never reports an error.
537
- ensure_termux_extras() {
538
- [ "$IS_TERMUX" = true ] || return 0
539
- command -v pkg >/dev/null 2>&1 || return 0
540
- local missing=""
541
- command -v sv >/dev/null 2>&1 || missing="termux-services"
542
- # termux-wake-lock is NOT proof that termux-api is installed: on a real phone
543
- # (Android 16, OPPO PHY110) that one script existed while termux-battery-status,
544
- # termux-wifi-connectioninfo, termux-sensor and every other command were
545
- # missing. Checking the wake lock therefore declared the package present and
546
- # left the whole device branch dead with nothing to explain it. A command that
547
- # only termux-api ships is the honest probe.
548
- command -v termux-battery-status >/dev/null 2>&1 || missing="$missing termux-api"
549
- # Termux ships pip separately from python, and its interpreter is built
550
- # --without-ensurepip: without these two packages neither `pip install` nor the
551
- # venv fallback below can work, and the failure would look like a broken node
552
- # rather than a missing package.
553
- if ! python_has_pip "$PYTHON_BIN"; then
554
- missing="$missing python-pip python-ensurepip-wheels"
555
- fi
556
- [ -n "$missing" ] || return 0
557
- # Unquoted on purpose: this is a package list, and pkg install takes them all.
558
- # shellcheck disable=SC2086
559
- pkg install -y $missing >/dev/null 2>&1 || true
560
- return 0
561
- }
562
-
563
- # True when a usable Python 3 is on the machine. Installs one if it is not.
564
- ensure_python() {
565
- resolve_python && return 0
566
- if [ "$LANG_CHOICE" = "ru" ]; then
567
- echo -e "\033[1;33m[!] Python 3 не найден. Автоматическая установка системных пакетов...\033[0m"
568
- else
569
- echo -e "\033[1;33m[!] Python 3 not found. Installing system packages...\033[0m"
570
- fi
571
- install_python_packages || true
572
- resolve_python && return 0
573
-
574
- # No usable package manager: uv can still bring an interpreter of its own.
575
- if [ "$LANG_CHOICE" = "ru" ]; then
576
- echo -e "\033[1;33m[!] Пакетный менеджер не дал Python. Пробую uv...\033[0m"
577
- else
578
- echo -e "\033[1;33m[!] The package manager did not provide Python. Trying uv...\033[0m"
579
- fi
580
- if ensure_uv && "$UV_BIN" python install 3.12 >/dev/null 2>&1; then
581
- resolve_python && return 0
582
- fi
583
- return 1
584
- }
585
-
586
- ensure_dependencies() {
587
- if ! ensure_python; then
588
- # Fail closed. An autostart unit naming an interpreter that does not exist
589
- # is a node that never connects, and nothing in the output would have said so.
590
- if [ "$LANG_CHOICE" = "ru" ]; then
591
- echo -e "\033[1;31m[!] Рабочий Python 3 получить не удалось.\033[0m" >&2
592
- if [ "$IS_TERMUX" = true ]; then
593
- echo -e " Проверено: ${CYAN}pkg${RESET} (пакетный менеджер Termux; uv не используется," >&2
594
- echo -e " его сборки линкуются с glibc и на Android/bionic не запускаются)." >&2
595
- echo -e " Установите Python 3 вручную и повторите:" >&2
596
- echo -e " ${CYAN}pkg install python python-pip${RESET}" >&2
597
- else
598
- echo -e " Проверены: пакетный менеджер (apt/dnf/yum/zypper/pacman/apk/xbps/brew) и uv." >&2
599
- echo -e " Установите Python 3 вручную и повторите:" >&2
600
- echo -e " ${CYAN}sudo apt install python3 python3-pip python3-venv${RESET} # или ваш менеджер" >&2
601
- fi
602
- echo -e " Остановка: без интерпретатора автозапуск узла работать не будет, ничего не создано." >&2
603
- else
604
- echo -e "\033[1;31m[!] Could not obtain a working Python 3.\033[0m" >&2
605
- if [ "$IS_TERMUX" = true ]; then
606
- echo -e " Tried: ${CYAN}pkg${RESET}, the Termux package manager (uv is skipped there: its" >&2
607
- echo -e " builds link against glibc and cannot run on Android's bionic libc)." >&2
608
- echo -e " Install Python 3 by hand and re-run:" >&2
609
- echo -e " ${CYAN}pkg install python python-pip${RESET}" >&2
610
- else
611
- echo -e " Tried: the system package manager (apt/dnf/yum/zypper/pacman/apk/xbps/brew) and uv." >&2
612
- echo -e " Install Python 3 by hand and re-run:" >&2
613
- echo -e " ${CYAN}sudo apt install python3 python3-pip python3-venv${RESET} # or your package manager" >&2
614
- fi
615
- echo -e " Stopping: an autostart entry without an interpreter can never work, nothing was created." >&2
616
- fi
617
- exit 1
618
- fi
619
-
620
- ensure_termux_extras
621
-
622
- if python_has_websockets "$PYTHON_BIN"; then
623
- return 0
624
- fi
625
- if [ "$LANG_CHOICE" = "ru" ]; then
626
- echo "[1/4] Автономная установка зависимостей (websockets)..."
627
- else
628
- echo "[1/4] Installing dependencies (websockets)..."
629
- fi
630
-
631
- # 1. pip, in both spellings. The --break-system-packages retry is the escape
632
- # hatch a PEP 668 distribution documents; without it a Debian 12 or Fedora 38
633
- # refuses the install and the message never mentions the flag.
634
- "$PYTHON_BIN" -m pip install --quiet websockets 2>/dev/null || true
635
- python_has_websockets "$PYTHON_BIN" && return 0
636
- "$PYTHON_BIN" -m pip install --quiet --break-system-packages websockets 2>/dev/null || true
637
- python_has_websockets "$PYTHON_BIN" && return 0
638
-
639
- # 2. --user: site-packages is read-only, the home directory is not.
640
- "$PYTHON_BIN" -m pip install --quiet --user websockets 2>/dev/null || true
641
- python_has_websockets "$PYTHON_BIN" && return 0
642
-
643
- # 3. ensurepip: a distribution can ship python3 without a pip at all.
644
- "$PYTHON_BIN" -m ensurepip --default-pip >/dev/null 2>&1 || true
645
- "$PYTHON_BIN" -m pip install --quiet websockets 2>/dev/null || true
646
- python_has_websockets "$PYTHON_BIN" && return 0
647
-
648
- # 4. uv: installs into a managed environment without the interpreter's own pip.
649
- if ensure_uv; then
650
- uv_pip_install "$PYTHON_BIN" || true
651
- python_has_websockets "$PYTHON_BIN" && return 0
652
- fi
653
-
654
- # 5. venv: needs no write access to the system Python, only the interpreter.
655
- local venv_dir="$CONFIG_DIR/venv"
656
- if [ ! -x "$venv_dir/bin/python3" ]; then
657
- "$PYTHON_BIN" -m venv "$venv_dir" >/dev/null 2>&1 || true
658
- fi
659
- if [ -x "$venv_dir/bin/python3" ]; then
660
- "$venv_dir/bin/python3" -m pip install --quiet --upgrade pip >/dev/null 2>&1 || true
661
- "$venv_dir/bin/python3" -m pip install --quiet websockets >/dev/null 2>&1 || true
662
- if python_has_websockets "$venv_dir/bin/python3"; then
663
- PYTHON_BIN="$venv_dir/bin/python3"
664
- return 0
665
- fi
666
- if uv_pip_install "$venv_dir/bin/python3" && python_has_websockets "$venv_dir/bin/python3"; then
667
- PYTHON_BIN="$venv_dir/bin/python3"
668
- return 0
669
- fi
670
- fi
671
-
672
- if [ "$LANG_CHOICE" = "ru" ]; then
673
- echo -e "\033[1;31m[!] Не удалось установить websockets автоматически.\033[0m" >&2
674
- echo -e " Проверены: pip, pip --break-system-packages, pip --user, ensurepip, uv и venv." >&2
675
- echo -e " Выполните вручную: ${CYAN}$PYTHON_BIN -m pip install websockets${RESET}" >&2
676
- if [ "$IS_TERMUX" = true ]; then
677
- echo -e " Termux: ${CYAN}pkg install python-pip python-ensurepip-wheels${RESET}, затем повторите." >&2
678
- fi
679
- echo -e " Остановка: узел без websockets не сможет подключиться к шлюзу." >&2
680
- else
681
- echo -e "\033[1;31m[!] Could not install websockets automatically.\033[0m" >&2
682
- echo -e " Tried: pip, pip --break-system-packages, pip --user, ensurepip, uv and a venv." >&2
683
- echo -e " Run manually: ${CYAN}$PYTHON_BIN -m pip install websockets${RESET}" >&2
684
- if [ "$IS_TERMUX" = true ]; then
685
- echo -e " Termux: ${CYAN}pkg install python-pip python-ensurepip-wheels${RESET}, then re-run." >&2
686
- fi
687
- echo -e " Stopping: a node without websockets can never reach the gateway." >&2
688
- fi
689
- exit 1
690
- }
691
-
692
-
693
- # Parse CLI args
694
- while [[ $# -gt 0 ]]; do
695
- case "$1" in
696
- --lang=*)
697
- LANG_CHOICE="${1#*=}"
698
- EXPLICIT_LANG=true
699
- shift
700
- ;;
701
- -q|--quick)
702
- QUICK=true
703
- shift
704
- ;;
705
- --mode=*)
706
- MODE="${1#*=}"
707
- shift
708
- ;;
709
- --user=*)
710
- USERNAME="${1#*=}"
711
- shift
712
- ;;
713
- --token=*)
714
- TOKEN="${1#*=}"
715
- shift
716
- ;;
717
- --gateway=*)
718
- # Historical alias of --domain=: both are the explicit override.
719
- DOMAIN="${1#*=}"
720
- shift
721
- ;;
722
- --port=*)
723
- PORT="${1#*=}"
724
- shift
725
- ;;
726
- --ssh=*)
727
- SSH_ARG="${1#*=}"
728
- if [[ "$SSH_ARG" =~ ^([^:]+):([0-9]+)$ ]]; then
729
- SSH_TARGET="${BASH_REMATCH[1]}"
730
- SSH_PORT="${BASH_REMATCH[2]}"
731
- else
732
- SSH_TARGET="$SSH_ARG"
733
- fi
734
- shift
735
- ;;
736
- --tls=*)
737
- TLS="${1#*=}"
738
- shift
739
- ;;
740
- --domain=*)
741
- # The shared public domain that serves every node. This is NOT a
742
- # per-device subdomain: nodes are selected with ?user=<node>.
743
- DOMAIN="${1#*=}"
744
- GATEWAY="$DOMAIN"
745
- shift
746
- ;;
747
- --dry-run)
748
- DRY_RUN=true
749
- shift
750
- ;;
751
- -h|--help)
752
- echo "Usage: $0 [--lang=en|ru] [-q|--quick] [--mode=tunnel|standalone|gateway] [--user=<node-name>] [--token=<token>] [--domain=<shared-domain>] [--port=<port>] [--ssh=user@host] [--dry-run]"
753
- echo ""
754
- echo "All nodes use ONE shared domain; the node is selected by ?user=<node-name>:"
755
- echo " MCP URL: https://<shared-domain>/sse?user=<node-name>&token=<token>"
756
- echo ""
757
- echo "The shared domain is resolved from the one place that declares it, in this order:"
758
- echo " --domain= / --gateway=, then MESH_PUBLIC_URL, then AGY_PUBLIC_BASE_URL,"
759
- echo " then ${DEFAULT_DOMAIN_FILE}, then the value this copy was published with,"
760
- echo " then core/domain.py's default when the repository is present."
761
- echo ""
762
- echo "Android/Termux is detected automatically: packages come from pkg, the node is"
763
- echo "named after the device model, and autostart is a runit service plus the"
764
- echo "Termux:Boot app instead of systemd (see docs/TERMUX.md)."
765
- exit 0
766
- ;;
767
- *)
768
- echo -e "${YELLOW}[ERROR] Unknown parameter: $1${RESET}"
769
- exit 1
770
- ;;
771
- esac
772
- done
773
-
774
- # ------------------------------------------------------------------------------
775
- # Resolve the shared domain, then fetch the node code from it.
776
- #
777
- # Order matters: the CLI arguments are parsed first (so --domain= counts), the
778
- # domain is resolved second, and only then is the bootstrap downloaded - from
779
- # the resolved domain, never from a name baked into this file.
780
- # ------------------------------------------------------------------------------
781
- GATEWAY="$(resolve_domain "$DOMAIN")"
782
-
783
- if [ "$GATEWAY" = "$DOMAIN_PLACEHOLDER" ]; then
784
- if domain_required; then
785
- echo ""
786
- if [ "$LANG_CHOICE" = "ru" ]; then
787
- echo -e "${RED}${BOLD}[ОШИБКА] Домен шлюза не задан.${RESET}"
788
- echo -e " В этой копии установщика остался плейсхолдер ${BOLD}${DOMAIN_PLACEHOLDER}${RESET}: она не была опубликована"
789
- echo -e " скриптом deploy_gateway.sh, а домена нет ни в окружении, ни в файле ${BOLD}${DEFAULT_DOMAIN_FILE}${RESET}."
790
- echo -e " Укажите домен одним из способов:"
791
- echo -e " ${CYAN}./install.sh --domain=<общий-домен>${RESET}"
792
- echo -e " ${CYAN}MESH_PUBLIC_URL=https://<общий-домен> ./install.sh${RESET}"
793
- echo -e " ${CYAN}$(domain_file_write_cmd 'https://<общий-домен>')${RESET}"
794
- echo -e " ${YELLOW}Установка остановлена: регистрация на несуществующем домене не выполняется, ничего не создано.${RESET}"
795
- else
796
- echo -e "${RED}${BOLD}[ERROR] No gateway domain configured.${RESET}"
797
- echo -e " This copy of the installer still carries the ${BOLD}${DOMAIN_PLACEHOLDER}${RESET} placeholder: it was not"
798
- echo -e " published by deploy_gateway.sh, and no domain was found in the environment or in ${BOLD}${DEFAULT_DOMAIN_FILE}${RESET}."
799
- echo -e " Pass the domain in one of these ways:"
800
- echo -e " ${CYAN}./install.sh --domain=<shared-domain>${RESET}"
801
- echo -e " ${CYAN}MESH_PUBLIC_URL=https://<shared-domain> ./install.sh${RESET}"
802
- echo -e " ${CYAN}$(domain_file_write_cmd 'https://<shared-domain>')${RESET}"
803
- echo -e " ${YELLOW}Stopping: a node is never registered against a domain that does not exist, nothing was created.${RESET}"
804
- fi
805
- echo ""
806
- exit 1
807
- fi
808
- # Standalone install with the node code already next to this script: no domain
809
- # is needed, so this is a warning rather than a stop.
810
- GATEWAY=""
811
- if [ "$LANG_CHOICE" = "ru" ]; then
812
- echo -e "${YELLOW}[!] Домен не задан - автономному режиму он не нужен, продолжаю.${RESET}"
813
- else
814
- echo -e "${YELLOW}[!] No domain configured - not needed for standalone mode, continuing.${RESET}"
815
- fi
816
- fi
817
-
818
- # ------------------------------------------------------------------------------
819
- # GATEWAY REACHABILITY (the one preflight that cannot be deferred)
820
- #
821
- # A node downloads its code from the gateway, registers there, and then dials the
822
- # same name for its tunnel. A name this device cannot resolve therefore dooms
823
- # every later step - and the failure used to surface much later, as "Failed to
824
- # obtain authentication token", which blames the gateway's answer instead of the
825
- # missing DNS record. Phones are where this bites: a gateway on a private network
826
- # (Tailscale, a VPN, a DNS override on the operator's laptop) resolves on that
827
- # laptop and nowhere else, and mobile data resolves nothing private.
828
- #
829
- # Only curl's exit code 6 ("could not resolve host") stops the install: it is the
830
- # one answer that cannot become true later in this run. A timeout, a 404 or a 5xx
831
- # is not fatal - a gateway without /health still serves the tunnel, and refusing
832
- # to install for that would be wrong.
833
- # ------------------------------------------------------------------------------
834
- require_gateway_dns() {
835
- [ -n "$GATEWAY" ] || return 0
836
- command -v curl >/dev/null 2>&1 || return 0
837
- local rc=0
838
- curl -fsS -m 8 -o /dev/null "https://${GATEWAY}/health" >/dev/null 2>&1 || rc=$?
839
- [ "$rc" -eq 6 ] || return 0
840
-
841
- if [ "$LANG_CHOICE" = "ru" ]; then
842
- echo ""
843
- echo -e "${RED}${BOLD}[ОШИБКА] Имя шлюза не разрешается на этом устройстве: ${GATEWAY}${RESET}"
844
- echo -e " DNS не знает такого имени, поэтому ни скачать код узла, ни зарегистрировать его не выйдет."
845
- echo -e " Обычно так бывает, когда шлюз живёт в приватной сети (Tailscale, VPN, локальный DNS):"
846
- echo -e " на рабочем ноутбуке имя резолвится, а на телефоне в мобильной сети - нет."
847
- echo -e " Что делать:"
848
- echo -e " • подключить это устройство к той же сети (на Android - приложение Tailscale);"
849
- echo -e " • или указать домен, который резолвится публично: ${CYAN}--domain=<домен>${RESET};"
850
- echo -e " • проверить вручную: ${CYAN}curl -v https://${GATEWAY}/health${RESET}"
851
- echo -e " ${YELLOW}Остановка: без DNS узел не подключится к шлюзу, ничего не создано.${RESET}"
852
- echo ""
853
- else
854
- echo ""
855
- echo -e "${RED}${BOLD}[ERROR] This device cannot resolve the gateway name: ${GATEWAY}${RESET}"
856
- echo -e " DNS does not know the name, so neither the node code nor a registration can arrive."
857
- echo -e " This is the usual state of a gateway on a private network (Tailscale, a VPN, a local"
858
- echo -e " DNS override): it resolves on the operator's laptop and not on a phone on mobile data."
859
- echo -e " What to do:"
860
- echo -e " • put this device on the same network (on Android: the Tailscale app);"
861
- echo -e " • or name a domain that resolves publicly: ${CYAN}--domain=<domain>${RESET};"
862
- echo -e " • check by hand: ${CYAN}curl -v https://${GATEWAY}/health${RESET}"
863
- echo -e " ${YELLOW}Stopping: without DNS the node can never reach the gateway, nothing was created.${RESET}"
864
- echo ""
865
- fi
866
- return 1
867
- }
868
-
869
- # Bootstrap when piped from curl or run outside the project: the node code is
870
- # fetched from the SAME domain that was just resolved. core/domain.py travels with
871
- # it, because the node asks that module for the domain as well.
872
- if [ ! -f "$SCRIPT_DIR/core/agent.py" ]; then
873
- # The node code is fetched with curl. Piping this script through curl means curl
874
- # exists; arriving through wget, a copy-paste or an embedded runner does not.
875
- ensure_curl || exit 1
876
- require_gateway_dns || exit 1
877
- BOOTSTRAP_DIR="$HOME/.gemini-computer-use"
878
- mkdir -p "$BOOTSTRAP_DIR/core" "$BOOTSTRAP_DIR/skills"
879
- # EVERY module the node can import, not a hand-picked few: the list below used to
880
- # stop at the files that existed when it was written, and a fresh phone install
881
- # then answered "the device layer (core/device.py) is missing from this checkout"
882
- # while the branch was advertised - the tool was there, its module was not.
883
- # tests/test_installer_bootstrap.py fails when a core module is missing here.
884
- for f in core/__init__.py core/agent.py core/agent_harness.py core/code_cache.py \
885
- core/device.py core/domain.py core/hooks.py core/mcp_tools.py core/policies.py \
886
- core/schemas.py core/server.py core/subagents.py core/termux.py core/triggers.py \
887
- core/updater.py core/version.py core/vitals.py core/web_share.py \
888
- skills/orchestrator.md; do
889
- bootstrap_tmp="$BOOTSTRAP_DIR/${f}.part"
890
- if curl -fsSL "https://${GATEWAY}/${f}" -o "$bootstrap_tmp" 2>/dev/null \
891
- || curl -fsSL "https://raw.githubusercontent.com/LevRa7/Computer-use-for-Gemini-App-Web/main/${f}" -o "$bootstrap_tmp" 2>/dev/null; then
892
- mv "$bootstrap_tmp" "$BOOTSTRAP_DIR/${f}"
893
- else
894
- rm -f "$bootstrap_tmp"
895
- fi
896
- done
897
- unset bootstrap_tmp
898
- # A bootstrap that downloaded nothing (an unreachable gateway and no GitHub)
899
- # leaves a directory that names modules which do not exist: every step after it
900
- # would write a launcher for code that is not there, and the node would fail
901
- # with a Python import error at boot.
902
- if [ ! -f "$BOOTSTRAP_DIR/core/agent.py" ]; then
903
- if [ "$LANG_CHOICE" = "ru" ]; then
904
- echo -e "${RED}${BOLD}[ОШИБКА] Не удалось скачать код узла.${RESET}" >&2
905
- echo -e " Пробовали: ${CYAN}https://${GATEWAY}/core/agent.py${RESET} и ${CYAN}github.com/LevRa7/Computer-use-for-Gemini-App-Web${RESET}." >&2
906
- echo -e " Проверьте сеть и DNS либо скопируйте репозиторий на устройство и запустите из него:" >&2
907
- echo -e " ${CYAN}git clone https://github.com/LevRa7/Computer-use-for-Gemini-App-Web.git && cd Computer-use-for-Gemini-App-Web && ./install.sh --quick${RESET}" >&2
908
- echo -e " ${YELLOW}Остановка: без кода узла запускать нечего, ничего не создано.${RESET}" >&2
909
- else
910
- echo -e "${RED}${BOLD}[ERROR] Could not download the node code.${RESET}" >&2
911
- echo -e " Tried: ${CYAN}https://${GATEWAY}/core/agent.py${RESET} and ${CYAN}github.com/LevRa7/Computer-use-for-Gemini-App-Web${RESET}." >&2
912
- echo -e " Check the network and DNS, or put the repository on this device and install from it:" >&2
913
- echo -e " ${CYAN}git clone https://github.com/LevRa7/Computer-use-for-Gemini-App-Web.git && cd Computer-use-for-Gemini-App-Web && ./install.sh --quick${RESET}" >&2
914
- echo -e " ${YELLOW}Stopping: there is no node code to run, nothing was created.${RESET}" >&2
915
- fi
916
- echo ""
917
- exit 1
918
- fi
919
- SCRIPT_DIR="$BOOTSTRAP_DIR"
920
- fi
921
-
922
- # ------------------------------------------------------------------------------
923
- # SHARED-DOMAIN CONTRACT (canonical step)
924
- # Every node is reached through ONE public domain ($GATEWAY); the node is
925
- # selected by the ?user= query parameter. The installer never mints a
926
- # per-device subdomain: each extra hostname would need its own DNS record and
927
- # TLS SAN, and a name missing from the certificate fails the handshake with an
928
- # opaque "cannot connect to host" error in the Gemini client.
929
- # sse https://<shared-domain>/sse?user=<node-name>&token=<token>
930
- # http https://<shared-domain>/mcp?user=<node-name>&token=<token>
931
- # tunnel wss://<shared-domain>/ws/tunnel?user=<node-name>&token=<token>
932
- # $GATEWAY is read at call time so the interactive menu can still change it.
933
- # ------------------------------------------------------------------------------
934
- node_sse_url() { printf 'https://%s/sse?user=%s&token=%s' "$GATEWAY" "$1" "$2"; }
935
-
936
- # Language prompt if interactive and not specified
937
- if [ -z "$LANG_CHOICE" ]; then
938
- if [ "$QUICK" = false ] && [ -z "$MODE" ] && [ -t 0 ]; then
939
- echo -e "\n${BOLD}${CYAN}╔════════════════════════════════════════════════════════════════════════╗${RESET}"
940
- echo -e "${BOLD}${CYAN}║ 🌐 LANGUAGE SELECTION / ВЫБОР ЯЗЫКА ║${RESET}"
941
- echo -e "${BOLD}${CYAN}╚════════════════════════════════════════════════════════════════════════╝${RESET}"
942
- echo -e " ${GREEN}1)${RESET} English (Default)"
943
- echo -e " ${BLUE}2)${RESET} Русский"
944
- read -rp "Select / Выберите [1]: " LANG_INPUT
945
- if [ "$LANG_INPUT" = "2" ] || [ "$LANG_INPUT" = "ru" ] || [ "$LANG_INPUT" = "RU" ]; then
946
- LANG_CHOICE="ru"
947
- else
948
- LANG_CHOICE="en"
949
- fi
950
- else
951
- LANG_CHOICE="en"
952
- fi
953
- fi
954
-
955
- # Device detection
956
- # ------------------------------------------------------------------------------
957
- # ARCHITECTURE
958
- #
959
- # The system package managers resolve their own architecture, so on Linux this
960
- # matters for exactly two things: the uv archive, which is published per CPU and
961
- # answers the wrong one with an unhelpful "Exec format error", and the
962
- # registration payload, so a gateway operator can see which build a node runs.
963
- # macOS reports the same machine names as Linux but needs the -apple-darwin assets.
964
- # ------------------------------------------------------------------------------
965
- detect_arch() {
966
- DETECTED_ARCH="$(uname -m 2>/dev/null || echo unknown)"
967
- case "$DETECTED_ARCH" in
968
- x86_64|amd64) ARCH_LABEL="x86_64 (64-bit)"; UV_TARGET="x86_64-unknown-linux-gnu" ;;
969
- aarch64|arm64) ARCH_LABEL="aarch64 (64-bit ARM)"; UV_TARGET="aarch64-unknown-linux-gnu" ;;
970
- armv7l|armv7|armv6l) ARCH_LABEL="armv7 (32-bit ARM)"; UV_TARGET="armv7-unknown-linux-gnueabihf" ;;
971
- i386|i486|i586|i686) ARCH_LABEL="x86 (32-bit)"; UV_TARGET="i686-unknown-linux-gnu" ;;
972
- *) ARCH_LABEL="$DETECTED_ARCH"; UV_TARGET="" ;;
973
- esac
974
- if [ "$(uname -s 2>/dev/null)" = "Darwin" ]; then
975
- case "$DETECTED_ARCH" in
976
- x86_64) UV_TARGET="x86_64-apple-darwin" ;;
977
- arm64) UV_TARGET="aarch64-apple-darwin" ;;
978
- esac
979
- fi
980
- if [ "$IS_TERMUX" = true ]; then
981
- # uv's archives are glibc/musl builds; neither runs against Android's
982
- # bionic libc, and asking for one yields an "Exec format error" that names
983
- # no cause. On a phone the package manager is the only source of Python.
984
- UV_TARGET=""
985
- fi
986
- }
987
-
988
- # Android system properties. getprop lives in /system/bin on every Android build
989
- # and is readable without root; a missing getprop (or an empty property) simply
990
- # yields an empty value, never a failed install.
991
- android_prop() {
992
- command -v getprop >/dev/null 2>&1 || return 0
993
- getprop "$1" 2>/dev/null | tr -d '\r' | head -n 1
994
- }
995
-
996
- detect_device() {
997
- # `|| true` on purpose: a minimal container (and Termux, which has no
998
- # hostname(1) of its own) answers neither spelling, and an assignment whose
999
- # substitution fails would abort the whole installer under `set -e`.
1000
- DETECTED_HOSTNAME=$(hostname -s 2>/dev/null || hostname 2>/dev/null || true)
1001
- detect_arch
1002
-
1003
- if [ "$IS_TERMUX" = true ]; then
1004
- ANDROID_MODEL="$(android_prop ro.product.model)"
1005
- [ -n "$ANDROID_MODEL" ] || ANDROID_MODEL="$(android_prop ro.product.device)"
1006
- ANDROID_RELEASE="$(android_prop ro.build.version.release)"
1007
- [ -n "$ANDROID_RELEASE" ] || ANDROID_RELEASE="$(android_prop ro.build.version.sdk)"
1008
- DETECTED_OS="Android ${ANDROID_RELEASE:-unknown} (Termux)"
1009
- # The phone itself names the node. Termux has no hostname binary, and
1010
- # Android's gethostname(2) answers "localhost" - a name every phone would
1011
- # claim, and the gateway keeps exactly one tunnel per name, so they would
1012
- # knock each other offline in a loop. The device model is stable, and it is
1013
- # sanitised below exactly like a hostname.
1014
- DETECTED_HOSTNAME="${ANDROID_MODEL:-android-node}"
1015
- if [ "$LANG_CHOICE" = "ru" ]; then
1016
- DETECTED_TYPE="Смартфон/планшет Android (Termux)"
1017
- else
1018
- DETECTED_TYPE="Android phone/tablet (Termux)"
1019
- fi
1020
- elif [ "$(uname -s)" = "Darwin" ]; then
1021
- DETECTED_OS="macOS $(sw_vers -productVersion 2>/dev/null || '')"
1022
- if [ "$LANG_CHOICE" = "ru" ]; then
1023
- DETECTED_TYPE="Apple Mac"
1024
- sysctl -n hw.model 2>/dev/null | grep -qi "book" && DETECTED_TYPE="Apple MacBook (Ноутбук)"
1025
- else
1026
- DETECTED_TYPE="Apple Mac"
1027
- sysctl -n hw.model 2>/dev/null | grep -qi "book" && DETECTED_TYPE="Apple MacBook (Laptop)"
1028
- fi
1029
- elif [ -f /etc/os-release ]; then
1030
- # shellcheck disable=SC1091
1031
- . /etc/os-release
1032
- DETECTED_OS="${PRETTY_NAME:-$NAME}"
1033
- else
1034
- DETECTED_OS=$(uname -s)
1035
- fi
1036
-
1037
- if [ "$(uname -s)" != "Darwin" ] && [ "$IS_TERMUX" != true ]; then
1038
- # The virtualised environments are asked FIRST, before the battery. WSL2
1039
- # forwards the host's battery into /sys/class/power_supply, so the battery
1040
- # test matched inside WSL and reported the user's laptop instead of the
1041
- # subsystem they were actually installing into - the node registered as
1042
- # "Laptop" while running under Windows.
1043
- if [ "$LANG_CHOICE" = "ru" ]; then
1044
- DETECTED_TYPE="Десктоп / Сервер"
1045
- if grep -q -i "microsoft" /proc/version 2>/dev/null; then
1046
- DETECTED_TYPE="WSL (Windows Subsystem for Linux)"
1047
- elif [ -f /.dockerenv ] || grep -q "docker\|containerd" /proc/1/cgroup 2>/dev/null; then
1048
- DETECTED_TYPE="Контейнер (Docker/LXC)"
1049
- elif [ -d /sys/class/power_supply ] && ls /sys/class/power_supply/BAT* 1>/dev/null 2>&1; then
1050
- DETECTED_TYPE="Ноутбук (Laptop)"
1051
- elif command -v systemd-detect-virt >/dev/null 2>&1 && systemd-detect-virt -q; then
1052
- DETECTED_TYPE="Облачный сервер / VPS ($(systemd-detect-virt))"
1053
- fi
1054
- else
1055
- DETECTED_TYPE="Desktop / Server"
1056
- if grep -q -i "microsoft" /proc/version 2>/dev/null; then
1057
- DETECTED_TYPE="WSL (Windows Subsystem for Linux)"
1058
- elif [ -f /.dockerenv ] || grep -q "docker\|containerd" /proc/1/cgroup 2>/dev/null; then
1059
- DETECTED_TYPE="Container (Docker/LXC)"
1060
- elif [ -d /sys/class/power_supply ] && ls /sys/class/power_supply/BAT* 1>/dev/null 2>&1; then
1061
- DETECTED_TYPE="Laptop"
1062
- elif command -v systemd-detect-virt >/dev/null 2>&1 && systemd-detect-virt -q; then
1063
- DETECTED_TYPE="Cloud VPS ($(systemd-detect-virt))"
1064
- fi
1065
- fi
1066
- fi
1067
- }
1068
-
1069
- detect_device
1070
-
1071
- print_device_info() {
1072
- if [ "$LANG_CHOICE" = "ru" ]; then
1073
- echo -e "${CYAN}╔════════════════════════════════════════════════════════════════════════╗${RESET}"
1074
- echo -e "${CYAN}║${RESET} ${BOLD}🔍 Обнаружено устройство:${RESET}"
1075
- echo -e "${CYAN}║${RESET} • Имя хоста : ${GREEN}${DETECTED_HOSTNAME}${RESET}"
1076
- echo -e "${CYAN}║${RESET} • Тип : ${YELLOW}${DETECTED_TYPE}${RESET}"
1077
- echo -e "${CYAN}║${RESET} • ОС : ${DETECTED_OS}"
1078
- echo -e "${CYAN}║${RESET} • Архитектура : ${ARCH_LABEL}"
1079
- echo -e "${CYAN}╚════════════════════════════════════════════════════════════════════════╝${RESET}"
1080
- else
1081
- echo -e "${CYAN}╔════════════════════════════════════════════════════════════════════════╗${RESET}"
1082
- echo -e "${CYAN}║${RESET} ${BOLD}🔍 Detected Device:${RESET}"
1083
- echo -e "${CYAN}║${RESET} • Hostname : ${GREEN}${DETECTED_HOSTNAME}${RESET}"
1084
- echo -e "${CYAN}║${RESET} • Type : ${YELLOW}${DETECTED_TYPE}${RESET}"
1085
- echo -e "${CYAN}║${RESET} • OS : ${DETECTED_OS}"
1086
- echo -e "${CYAN}║${RESET} • Architecture: ${ARCH_LABEL}"
1087
- echo -e "${CYAN}╚════════════════════════════════════════════════════════════════════════╝${RESET}"
1088
- fi
1089
- }
1090
-
1091
- print_mcp_box() {
1092
- local title="$1"
1093
- local url="$2"
1094
- local copy_msg="$3"
1095
-
1096
- local max_len=${#title}
1097
- [ ${#url} -gt $max_len ] && max_len=${#url}
1098
- [ -n "$copy_msg" ] && [ ${#copy_msg} -gt $max_len ] && max_len=${#copy_msg}
1099
-
1100
- local width=$(( max_len + 4 ))
1101
- [ $width -lt 84 ] && width=84
1102
-
1103
- local dashes=$(printf '%*s' "$width" '' | tr ' ' '-')
1104
- local border=" +${dashes}+"
1105
- local empty=" |$(printf '%*s' "$width" '')|"
1106
-
1107
- echo -e "${GREEN}${border}${RESET}"
1108
-
1109
- # Title
1110
- local pad_title=$(( width - 2 - ${#title} ))
1111
- printf " ${GREEN}|${RESET} ${BOLD}%s${RESET}%*s ${GREEN}|${RESET}\n" "$title" "$pad_title" ""
1112
-
1113
- echo -e "${GREEN}${empty}${RESET}"
1114
-
1115
- # URL
1116
- local pad_url=$(( width - 2 - ${#url} ))
1117
- printf " ${GREEN}|${RESET} ${BOLD}${YELLOW}%s${RESET}%*s ${GREEN}|${RESET}\n" "$url" "$pad_url" ""
1118
-
1119
- echo -e "${GREEN}${empty}${RESET}"
1120
-
1121
- # Copy message
1122
- if [ -n "$copy_msg" ]; then
1123
- local pad_copy=$(( width - 2 - ${#copy_msg} ))
1124
- printf " ${GREEN}|${RESET} ${BOLD}${GREEN}%s${RESET}%*s ${GREEN}|${RESET}\n" "$copy_msg" "$pad_copy" ""
1125
- fi
1126
-
1127
- echo -e "${GREEN}${border}${RESET}"
1128
- }
1129
-
1130
- # ------------------------------------------------------------------------------
1131
- # TERMUX (ANDROID) AUTOSTART
1132
- #
1133
- # An unprivileged Android app gets no service manager, so there is no systemd
1134
- # unit, no launchd agent and no init script to install. Two Termux mechanisms
1135
- # replace all three, and configure_termux_node sets up both:
1136
- #
1137
- # 1. runit, from the termux-services package: $PREFIX/var/service/<name>/run is
1138
- # supervised inside the running Termux session, so a node that dies is
1139
- # restarted - the systemd Restart=always equivalent;
1140
- # 2. ~/.termux/boot/<name>.sh, executed by the Termux:Boot app (F-Droid) after
1141
- # the device boots: it takes the wake lock and starts the supervisor.
1142
- #
1143
- # Without termux-services the node still runs and still autostarts at boot, but
1144
- # nothing restarts it when Android kills the process. The installer says that
1145
- # plainly instead of implying supervision that is not there.
1146
- #
1147
- # configure_termux_node <service name> <python module> <extra args> <env file>
1148
- # ------------------------------------------------------------------------------
1149
- configure_termux_node() {
1150
- local name="$1" module="$2" extra="$3" env_file="$4"
1151
- local run_script="$CONFIG_DIR/${name}.sh"
1152
- local boot_script="$HOME/.termux/boot/${name}.sh"
1153
- local svdir="$TERMUX_PREFIX/var/service"
1154
- local svlogdir="$TERMUX_PREFIX/var/log"
1155
- local log_file="$CONFIG_DIR/${name}.log"
1156
- local env_block=""
1157
- local module_args="$module"
1158
- local alive=false
1159
- local waited=0
1160
-
1161
- [ -n "$extra" ] && module_args="$module $extra"
1162
-
1163
- TERMUX_SUPERVISED=false
1164
- mkdir -p "$CONFIG_DIR" "$HOME/.termux/boot"
1165
-
1166
- if [ -n "$env_file" ]; then
1167
- env_block="if [ -f \"$env_file\" ]; then
1168
- set -a
1169
- . \"$env_file\"
1170
- set +a
1171
- fi"
1172
- fi
1173
-
1174
- # The launcher is the one thing every mechanism below starts. Absolute paths
1175
- # only: runit and the boot script run it with a minimal environment in which
1176
- # $PATH may not even contain $PREFIX/bin.
1177
- cat << RUNNER_EOF > "$run_script"
1178
- #!$TERMUX_PREFIX/bin/sh
1179
- # Antigravity Mesh - $name launcher (written by install.sh; do not edit).
1180
- # Run this file to start the node by hand; docs/TERMUX.md documents the automatic
1181
- # paths (runit service, Termux:Boot) and where the logs are.
1182
- export PATH="$TERMUX_PREFIX/bin:/system/bin:\$PATH"
1183
- $env_block
1184
- cd "$SCRIPT_DIR" || exit 1
1185
- exec "$PYTHON_BIN" -m $module_args
1186
- RUNNER_EOF
1187
- chmod 700 "$run_script"
1188
-
1189
- # 1. runit supervision, when termux-services is installed. install.sh asks the
1190
- # package manager for it first; a phone that refused (no network, an old
1191
- # mirror) simply keeps the unattended launcher below.
1192
- if command -v sv >/dev/null 2>&1 && command -v runsvdir >/dev/null 2>&1; then
1193
- mkdir -p "$svdir/$name/log"
1194
- cat << SV_EOF > "$svdir/$name/run"
1195
- #!$TERMUX_PREFIX/bin/sh
1196
- exec "$run_script"
1197
- SV_EOF
1198
- chmod 755 "$svdir/$name/run"
1199
- # runit refuses to supervise without log/run, and svlogger is a script, not
1200
- # a program: the documented setup is a symlink to it. Two cases need the
1201
- # equivalent script instead, because a link that cannot resolve is worse
1202
- # than no link at all: a $PREFIX that refuses symlinks (a FUSE-backed home,
1203
- # an unusual mount), and a phone where sv and runsvdir exist but
1204
- # termux-services never installed its svlogger - 'ln -sf' happily creates a
1205
- # dangling log/run against a missing target, and runit then has a logger it
1206
- # cannot execute.
1207
- local svlogger="$TERMUX_PREFIX/share/termux-services/svlogger"
1208
- if [ ! -e "$svlogger" ] \
1209
- || ! ln -sf "$svlogger" "$svdir/$name/log/run" 2>/dev/null; then
1210
- printf '#!%s/bin/sh\nexec "%s"\n' \
1211
- "$TERMUX_PREFIX" "$svlogger" > "$svdir/$name/log/run"
1212
- chmod 755 "$svdir/$name/log/run"
1213
- fi
1214
- export SVDIR="$svdir"
1215
- export LOGDIR="$svlogdir"
1216
- # The supervisor is normally started by the login shell (profile.d), which
1217
- # has not necessarily run when this installer is piped straight into bash.
1218
- # Starting it here is what makes the node run *now*; the pgrep guard keeps a
1219
- # second runsvdir off an SVDIR that already has one.
1220
- if ! pgrep -f "runsvdir" >/dev/null 2>&1; then
1221
- command -v service-daemon >/dev/null 2>&1 && service-daemon start >/dev/null 2>&1 || true
1222
- fi
1223
- sv up "$name" >/dev/null 2>&1 || true
1224
- sv-enable "$name" >/dev/null 2>&1 || true
1225
- TERMUX_SUPERVISED=true
1226
- fi
1227
-
1228
- # 2. The boot entry, written even when runit supervises: after a reboot the
1229
- # supervisor itself has to be started by something, and on Android that
1230
- # something is the Termux:Boot app.
1231
- cat << BOOT_EOF > "$boot_script"
1232
- #!$TERMUX_PREFIX/bin/sh
1233
- # Antigravity Mesh - starts the $name node after the device boots (Termux:Boot).
1234
- #
1235
- # Requirements, both of them outside this file (docs/TERMUX.md):
1236
- # * the Termux:Boot app (F-Droid), opened once so Android lets it run;
1237
- # * battery optimisation OFF for Termux and for Termux:Boot.
1238
- export PATH="$TERMUX_PREFIX/bin:/system/bin:\$PATH"
1239
- export HOME="$HOME"
1240
- export SVDIR="$svdir"
1241
- export LOGDIR="$svlogdir"
1242
-
1243
- # A partial wake lock keeps Android from freezing the node while the screen is
1244
- # off. It needs the termux-api package plus the Termux:API app; without either,
1245
- # this line is a no-op rather than a failure.
1246
- command -v termux-wake-lock >/dev/null 2>&1 && termux-wake-lock >/dev/null 2>&1
1247
-
1248
- if command -v sv >/dev/null 2>&1 && [ -d "$svdir/$name" ]; then
1249
- if ! pgrep -f "runsvdir" >/dev/null 2>&1; then
1250
- command -v service-daemon >/dev/null 2>&1 && service-daemon start >/dev/null 2>&1
1251
- fi
1252
- sv up "$name" >/dev/null 2>&1
1253
- else
1254
- # No supervisor on this phone: start the launcher directly and keep its output.
1255
- "$run_script" >> "$CONFIG_DIR/$name.boot.log" 2>&1 &
1256
- fi
1257
- BOOT_EOF
1258
- chmod 700 "$boot_script"
1259
-
1260
- if [ "$TERMUX_SUPERVISED" != true ]; then
1261
- # The systemd branch's fallback, spelled for a phone: nothing supervises
1262
- # the process, so it is started detached and its log is the only evidence.
1263
- pkill -f "$module" 2>/dev/null || true
1264
- nohup "$run_script" >> "$log_file" 2>&1 &
1265
- fi
1266
-
1267
- # Termux has no `systemctl status` to consult, so the installer waits for the
1268
- # process it just started (runit may take a moment) and reports what it saw.
1269
- while [ "$waited" -lt 10 ]; do
1270
- if pgrep -f "$module" >/dev/null 2>&1; then
1271
- alive=true
1272
- break
1273
- fi
1274
- sleep 1
1275
- waited=$((waited + 1))
1276
- done
1277
-
1278
- if [ "$LANG_CHOICE" = "ru" ]; then
1279
- if [ "$TERMUX_SUPERVISED" = true ]; then
1280
- echo -e "${GREEN}[OK] runit (termux-services) следит за узлом и перезапустит его сам.${RESET}"
1281
- echo -e " Статус: ${CYAN}sv status $name${RESET} Логи: ${CYAN}$svlogdir/sv/$name/current${RESET}"
1282
- else
1283
- echo -e "${YELLOW}[!] Узел запущен без супервизора: termux-services не установлен.${RESET}"
1284
- echo -e " Автоперезапуск после сбоя: ${CYAN}pkg install termux-services${RESET}, затем повторите установку."
1285
- echo -e " Логи: ${CYAN}$log_file${RESET}"
1286
- fi
1287
- echo -e " Автозапуск после перезагрузки: ${CYAN}$boot_script${RESET} (нужно приложение Termux:Boot)."
1288
- if [ "$alive" != true ]; then
1289
- echo -e "${YELLOW}[!] Процесс узла пока не виден — смотрите лог: ${CYAN}$log_file${RESET}"
1290
- fi
1291
- else
1292
- if [ "$TERMUX_SUPERVISED" = true ]; then
1293
- echo -e "${GREEN}[OK] runit (termux-services) supervises the node and restarts it.${RESET}"
1294
- echo -e " Status: ${CYAN}sv status $name${RESET} Logs: ${CYAN}$svlogdir/sv/$name/current${RESET}"
1295
- else
1296
- echo -e "${YELLOW}[!] The node runs unsupervised: termux-services is not installed.${RESET}"
1297
- echo -e " Restart after a crash: ${CYAN}pkg install termux-services${RESET}, then re-run the installer."
1298
- echo -e " Logs: ${CYAN}$log_file${RESET}"
1299
- fi
1300
- echo -e " Autostart after a reboot: ${CYAN}$boot_script${RESET} (the Termux:Boot app is required)."
1301
- if [ "$alive" != true ]; then
1302
- echo -e "${YELLOW}[!] The node process is not visible yet - check: ${CYAN}$log_file${RESET}"
1303
- fi
1304
- fi
1305
- }
1306
-
1307
- # The Android-side switches no installer can flip for the user: whether the node
1308
- # comes back after a reboot and whether Android keeps it alive with the screen
1309
- # off. Printed after the MCP URL, in the language of the run.
1310
- print_termux_hints() {
1311
- local name="$1"
1312
- if [ "$LANG_CHOICE" = "ru" ]; then
1313
- echo -e " ${BOLD}📱 Termux (Android): что проверить на телефоне${RESET} (служба: ${CYAN}$name${RESET})"
1314
- echo -e " 1. Автозапуск после перезагрузки делает приложение ${CYAN}Termux:Boot${RESET} (F-Droid)."
1315
- echo -e " Установите и ${BOLD}запустите его один раз${RESET}, иначе скрипт в ~/.termux/boot не выполнится."
1316
- echo -e " 2. Отключите оптимизацию батареи для ${CYAN}Termux${RESET} и Termux:Boot: иначе Android"
1317
- echo -e " выгружает фоновый процесс вместе с туннелем."
1318
- echo -e " 3. Скопировать ссылку выше: долгое нажатие в терминале, либо приложение Termux:API и"
1319
- echo -e " ${CYAN}termux-clipboard-set '<ссылка>'${RESET}."
1320
- echo -e " 4. Выдайте приложению ${CYAN}Termux:API${RESET} разрешения для инструментов устройства"
1321
- echo -e " (Настройки Android → Приложения → Termux:API → Разрешения): камера, микрофон,"
1322
- echo -e " местоположение, SMS, журнал вызовов, контакты. Без них ${CYAN}device_capture${RESET} и"
1323
- echo -e " ${CYAN}device_messages${RESET} ответят, какого именно разрешения не хватает."
1324
- echo -e " 5. Ветка устройства включена по умолчанию (${CYAN}MESH_DEVICE=auto${RESET}). SMS, контакты и"
1325
- echo -e " журнал вызовов открывает ${CYAN}MESH_DEVICE_PIM=1${RESET}; ${CYAN}MESH_DEVICE_QUICK=1${RESET} оставляет"
1326
- echo -e " system_info мгновенным (без сети, камер, микрофонов и датчиков)."
1327
- echo -e " 6. Полная инструкция: ${CYAN}docs/TERMUX.md${RESET} (установка, автозапуск, логи, удаление)."
1328
- echo ""
1329
- else
1330
- echo -e " ${BOLD}📱 Termux (Android): what to check on the phone${RESET} (service: ${CYAN}$name${RESET})"
1331
- echo -e " 1. Autostart after a reboot is done by the ${CYAN}Termux:Boot${RESET} app (F-Droid)."
1332
- echo -e " Install it and ${BOLD}open it once${RESET}, or the script in ~/.termux/boot never runs."
1333
- echo -e " 2. Turn battery optimisation OFF for ${CYAN}Termux${RESET} and Termux:Boot: otherwise Android"
1334
- echo -e " unloads the background process together with the tunnel."
1335
- echo -e " 3. Copy the URL above: long-press the terminal, or install the Termux:API app and use"
1336
- echo -e " ${CYAN}termux-clipboard-set '<url>'${RESET}."
1337
- echo -e " 4. Grant the ${CYAN}Termux:API${RESET} app the permissions the device tools need"
1338
- echo -e " (Android Settings → Apps → Termux:API → Permissions): camera, microphone, location,"
1339
- echo -e " SMS, call log, contacts. Without them ${CYAN}device_capture${RESET} / ${CYAN}device_messages${RESET}"
1340
- echo -e " answer with the exact permission that is missing."
1341
- echo -e " 5. The device branch is on by default (${CYAN}MESH_DEVICE=auto${RESET}). ${CYAN}MESH_DEVICE_PIM=1${RESET}"
1342
- echo -e " enables SMS, contacts and the call log; ${CYAN}MESH_DEVICE_QUICK=1${RESET} keeps system_info"
1343
- echo -e " instant (no network, cameras, microphones or sensors)."
1344
- echo -e " 6. Full guide: ${CYAN}docs/TERMUX.md${RESET} (install, autostart, logs, uninstall)."
1345
- echo ""
1346
- fi
1347
- }
1348
-
1349
- # Dry-run
1350
- if [ "$DRY_RUN" = true ]; then
1351
- echo "[DRY-RUN] Simulating Antigravity Mesh installation..."
1352
- echo "[DRY-RUN] Language: $LANG_CHOICE"
1353
- echo "[DRY-RUN] Detected Host: $DETECTED_HOSTNAME ($DETECTED_TYPE)"
1354
- echo "[DRY-RUN] Architecture: ${ARCH_LABEL:-$DETECTED_ARCH}"
1355
- echo "[DRY-RUN] Selected Mode: ${MODE:-standalone}"
1356
- echo "[DRY-RUN] Port: $PORT"
1357
- echo "[DRY-RUN] TLS: $TLS"
1358
- if [ -n "$GATEWAY" ]; then echo "[DRY-RUN] Domain: $GATEWAY"; fi
1359
- if [ -n "$USERNAME" ]; then
1360
- echo "[DRY-RUN] User: $USERNAME"
1361
- if [ -n "$GATEWAY" ]; then
1362
- echo "[DRY-RUN] Canonical MCP URL: $(node_sse_url "$USERNAME" "${TOKEN:-<token>}")"
1363
- fi
1364
- fi
1365
- if [ -n "$TOKEN" ]; then echo "[DRY-RUN] Token: set (standalone: stored in ${CONFIG_DIR}/standalone.env, chmod 600)"; fi
1366
- if [ "$IS_TERMUX" = true ]; then
1367
- echo "[DRY-RUN] Platform: Termux (Android)"
1368
- echo "[DRY-RUN] Termux prefix: $TERMUX_PREFIX"
1369
- echo "[DRY-RUN] Package manager: pkg (sudo, systemd and uv do not exist on Android)"
1370
- echo "[DRY-RUN] Autostart: runit service <name> + ~/.termux/boot/<name>.sh (Termux:Boot)"
1371
- echo "[DRY-RUN] Domain file: $DEFAULT_DOMAIN_FILE"
1372
- else
1373
- echo "[DRY-RUN] Domain file: $DEFAULT_DOMAIN_FILE"
1374
- echo "[DRY-RUN] Systemd service and dependencies check: OK"
1375
- fi
1376
- if [ -z "$GATEWAY" ]; then
1377
- echo "[DRY-RUN] Domain file value: not written (no domain configured)"
1378
- elif [ "$GATEWAY" = "$(normalise_host "$(module_default_domain)")" ]; then
1379
- echo "[DRY-RUN] Domain file value: not written (the built-in default is a fallback, not a configuration)"
1380
- else
1381
- echo "[DRY-RUN] Domain file value: MESH_PUBLIC_URL=https://$GATEWAY"
1382
- fi
1383
- exit 0
1384
- fi
1385
-
1386
- # SSH Remote install branch
1387
- if [ -n "$SSH_TARGET" ]; then
1388
- if [ "$LANG_CHOICE" = "ru" ]; then
1389
- echo -e "${BOLD}${BLUE}=== Удаленная установка Antigravity Mesh через SSH ===${RESET}"
1390
- echo -e "Целевой сервер: ${CYAN}${SSH_TARGET}${RESET} (порт: ${SSH_PORT})"
1391
- else
1392
- echo -e "${BOLD}${BLUE}=== Remote Antigravity Mesh SSH Installation ===${RESET}"
1393
- echo -e "Target Server: ${CYAN}${SSH_TARGET}${RESET} (port: ${SSH_PORT})"
1394
- fi
1395
-
1396
- SSH_BIN="ssh -o StrictHostKeyChecking=no"
1397
- SCP_BIN="scp -o StrictHostKeyChecking=no"
1398
- if [ -n "$SSHPASS" ] && command -v sshpass >/dev/null 2>&1; then
1399
- SSH_BIN="sshpass -e $SSH_BIN"
1400
- SCP_BIN="sshpass -e $SCP_BIN"
1401
- fi
1402
-
1403
- $SSH_BIN -p "$SSH_PORT" "$SSH_TARGET" "mkdir -p ~/antigravity-mesh/core ~/antigravity-mesh/skills"
1404
- $SCP_BIN -P "$SSH_PORT" -r "$SCRIPT_DIR/core"/* "$SSH_TARGET:~/antigravity-mesh/core/"
1405
- $SCP_BIN -P "$SSH_PORT" "$SCRIPT_DIR/install.sh" "$SSH_TARGET:~/antigravity-mesh/"
1406
- if [ -d "$SCRIPT_DIR/skills" ]; then
1407
- $SCP_BIN -P "$SSH_PORT" -r "$SCRIPT_DIR/skills"/* "$SSH_TARGET:~/antigravity-mesh/skills/" 2>/dev/null || true
1408
- fi
1409
- # The domain was resolved on THIS machine, so it is handed to the target: the
1410
- # remote copy otherwise re-resolves from its own environment, and a repository
1411
- # copy whose $PUBLISHED_DOMAIN is still the placeholder falls back to the
1412
- # built-in default - which the target would now also record as its domain.
1413
- REMOTE_DOMAIN_ARG=""
1414
- if [ -n "$GATEWAY" ] && [ "$GATEWAY" != "$DOMAIN_PLACEHOLDER" ]; then
1415
- REMOTE_DOMAIN_ARG=" --domain=$GATEWAY"
1416
- fi
1417
- $SSH_BIN -p "$SSH_PORT" "$SSH_TARGET" "cd ~/antigravity-mesh && bash install.sh --quick --lang=$LANG_CHOICE$REMOTE_DOMAIN_ARG"
1418
- exit 0
1419
- fi
1420
-
1421
- # Interactive Menu if no mode is specified and not in quick mode
1422
- if [ "$QUICK" = false ] && [ -z "$MODE" ] && [ -t 0 ]; then
1423
- print_device_info
1424
- echo ""
1425
- if [ "$LANG_CHOICE" = "ru" ]; then
1426
- echo -e "${BOLD}Выберите режим установки:${RESET}"
1427
- echo -e " ${GREEN}1)${RESET} ⚡ ${BOLD}Быстрая настройка${RESET} (Имя узла как имя ПК + Общий домен + Автозапуск) [Рекомендуется]"
1428
- echo -e " ${BLUE}2)${RESET} 🖥️ ${BOLD}Локальный Standalone${RESET} (Только localhost:${PORT}, без облачного шлюза)"
1429
- echo -e " ${YELLOW}3)${RESET} ⚙️ ${BOLD}Кастомная настройка${RESET} (Ввести имя узла вручную, выбор общего домена)"
1430
- echo -e " ${CYAN}4)${RESET} 📡 ${BOLD}Удаленная установка на SSH-сервер${RESET}"
1431
- echo -e " ${RESET}0) Выход"
1432
- echo ""
1433
- read -rp "Ваш выбор [1]: " MENU_CHOICE
1434
- else
1435
- echo -e "${BOLD}Select Installation Mode:${RESET}"
1436
- echo -e " ${GREEN}1)${RESET} ⚡ ${BOLD}Quick Setup${RESET} (Node name from PC name + shared domain + Autostart) [Recommended]"
1437
- echo -e " ${BLUE}2)${RESET} 🖥️ ${BOLD}Local Standalone${RESET} (localhost:${PORT} only, no cloud gateway)"
1438
- echo -e " ${YELLOW}3)${RESET} ⚙️ ${BOLD}Custom Setup${RESET} (Custom node name, custom shared domain)"
1439
- echo -e " ${CYAN}4)${RESET} 📡 ${BOLD}Remote SSH Installation${RESET}"
1440
- echo -e " ${RESET}0) Exit"
1441
- echo ""
1442
- read -rp "Select [1]: " MENU_CHOICE
1443
- fi
1444
- MENU_CHOICE=${MENU_CHOICE:-1}
1445
-
1446
- case "$MENU_CHOICE" in
1447
- 1)
1448
- MODE="tunnel"
1449
- QUICK=true
1450
- ;;
1451
- 2)
1452
- MODE="standalone"
1453
- ;;
1454
- 3)
1455
- MODE="tunnel"
1456
- echo ""
1457
- if [ "$LANG_CHOICE" = "ru" ]; then
1458
- read -rp "Введите имя узла [по умолчанию: ${DETECTED_HOSTNAME}]: " CUSTOM_SUB
1459
- read -rp "Общий домен шлюза [по умолчанию: ${GATEWAY}]: " CUSTOM_GW
1460
- else
1461
- read -rp "Enter node name [default: ${DETECTED_HOSTNAME}]: " CUSTOM_SUB
1462
- read -rp "Shared gateway domain [default: ${GATEWAY}]: " CUSTOM_GW
1463
- fi
1464
- if [ -n "$CUSTOM_SUB" ]; then USERNAME="$CUSTOM_SUB"; fi
1465
- if [ -n "$CUSTOM_GW" ]; then GATEWAY="$CUSTOM_GW"; fi
1466
- ;;
1467
- 4)
1468
- echo ""
1469
- if [ "$LANG_CHOICE" = "ru" ]; then
1470
- read -rp "Введите SSH цель (например, user@<host-ip>): " REMOTE_TARGET
1471
- read -rp "Порт SSH [22]: " REMOTE_PORT
1472
- else
1473
- read -rp "Enter SSH target (e.g., user@<host-ip>): " REMOTE_TARGET
1474
- read -rp "SSH Port [22]: " REMOTE_PORT
1475
- fi
1476
- REMOTE_PORT=${REMOTE_PORT:-22}
1477
- exec "$0" "--ssh=${REMOTE_TARGET}:${REMOTE_PORT}" "--lang=${LANG_CHOICE}"
1478
- ;;
1479
- 0)
1480
- echo "Cancelled."
1481
- exit 0
1482
- ;;
1483
- *)
1484
- MODE="tunnel"
1485
- QUICK=true
1486
- ;;
1487
- esac
1488
- fi
1489
-
1490
- if [ -z "$MODE" ]; then
1491
- MODE="tunnel"
1492
- fi
1493
-
1494
- print_device_info
1495
-
1496
- # ==============================================================================
1497
- # BRANCH: STANDALONE MODE
1498
- # ==============================================================================
1499
- if [ "$MODE" = "standalone" ]; then
1500
- if [ "$LANG_CHOICE" = "ru" ]; then
1501
- echo -e "\n${BOLD}${BLUE}=== Настройка локального Standalone FastMCP сервера ===${RESET}"
1502
- echo "[1/3] Проверка окружения Python..."
1503
- else
1504
- echo -e "\n${BOLD}${BLUE}=== Setting up Local Standalone FastMCP Server ===${RESET}"
1505
- echo "[1/3] Checking Python environment..."
1506
- fi
1507
- ensure_dependencies
1508
- # The endpoint check at the end of this branch runs through curl.
1509
- ensure_curl || exit 1
1510
- mkdir -p "$CONFIG_DIR"
1511
- # A standalone node answers share_file too, so the domain it resolved is written
1512
- # down here as well - before the server is started, not after.
1513
- write_domain_file "$GATEWAY"
1514
-
1515
- # Honour --token in standalone mode without embedding the secret in the
1516
- # (world-readable) unit/plist: keep it in a 0600 env file for systemd
1517
- # (EnvironmentFile) and use a 0600 plist environment block on macOS.
1518
- # core.server reads MESH_TOKEN as the default for --token.
1519
- STANDALONE_ENV_FILE="$CONFIG_DIR/standalone.env"
1520
- ENV_FILE_LINE=""
1521
- PLIST_ENV_BLOCK=""
1522
- if [ -n "$TOKEN" ]; then
1523
- printf 'MESH_TOKEN=%s\n' "$TOKEN" > "$STANDALONE_ENV_FILE"
1524
- chmod 600 "$STANDALONE_ENV_FILE"
1525
- ENV_FILE_LINE="EnvironmentFile=$STANDALONE_ENV_FILE"
1526
- PLIST_TOKEN=$(printf '%s' "$TOKEN" | sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g')
1527
- PLIST_ENV_BLOCK=" <key>EnvironmentVariables</key>
1528
- <dict>
1529
- <key>MESH_TOKEN</key>
1530
- <string>${PLIST_TOKEN}</string>
1531
- </dict>"
1532
- if [ "$LANG_CHOICE" = "ru" ]; then
1533
- echo -e "${GREEN}[✓] Токен авторизации включён (chmod 600).${RESET}"
1534
- else
1535
- echo -e "${GREEN}[OK] Authorization token enabled (chmod 600).${RESET}"
1536
- fi
1537
- else
1538
- rm -f "$STANDALONE_ENV_FILE" 2>/dev/null || true
1539
- if [ "$LANG_CHOICE" = "ru" ]; then
1540
- echo -e "${YELLOW}[!] Токен не задан (--token): сервер доступен без авторизации на 127.0.0.1.${RESET}"
1541
- else
1542
- echo -e "${YELLOW}[!] No --token given: server is unauthenticated on 127.0.0.1.${RESET}"
1543
- fi
1544
- fi
1545
-
1546
- if [ "$LANG_CHOICE" = "ru" ]; then
1547
- if [ "$IS_TERMUX" = true ]; then
1548
- echo "[2/3] Настройка автозапуска на телефоне (runit + Termux:Boot)..."
1549
- else
1550
- echo "[2/3] Настройка systemd автозапуска на ПК..."
1551
- fi
1552
- else
1553
- echo "[2/3] Configuring autostart background service..."
1554
- fi
1555
-
1556
- if [ "$(uname -s)" = "Darwin" ]; then
1557
- LAUNCH_AGENTS="$HOME/Library/LaunchAgents"
1558
- mkdir -p "$LAUNCH_AGENTS"
1559
- PLIST_FILE="$LAUNCH_AGENTS/com.antigravity.mesh.standalone.plist"
1560
- cat << EOF > "$PLIST_FILE"
1561
- <?xml version="1.0" encoding="UTF-8"?>
1562
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1563
- <plist version="1.0">
1564
- <dict>
1565
- <key>Label</key>
1566
- <string>com.antigravity.mesh.standalone</string>
1567
- <key>ProgramArguments</key>
1568
- <array>
1569
- <string>$PYTHON_BIN</string>
1570
- <string>-m</string>
1571
- <string>core.server</string>
1572
- <string>--host=127.0.0.1</string>
1573
- <string>--port=$PORT</string>
1574
- </array>
1575
- <key>WorkingDirectory</key>
1576
- <string>$SCRIPT_DIR</string>
1577
- <key>RunAtLoad</key>
1578
- <true/>
1579
- <key>KeepAlive</key>
1580
- <true/>
1581
- $PLIST_ENV_BLOCK
1582
- </dict>
1583
- </plist>
1584
- EOF
1585
- if [ -n "$TOKEN" ]; then
1586
- chmod 600 "$PLIST_FILE"
1587
- fi
1588
- launchctl unload "$PLIST_FILE" 2>/dev/null || true
1589
- launchctl load -w "$PLIST_FILE" 2>/dev/null || true
1590
- elif [ "$IS_TERMUX" = true ]; then
1591
- # Android: neither systemd nor launchd exists - runit inside Termux
1592
- # supervises the server, and Termux:Boot brings it back after a reboot.
1593
- configure_termux_node "agy-standalone" "core.server" "--host 127.0.0.1 --port=$PORT" "$STANDALONE_ENV_FILE"
1594
- else
1595
- USER_SYSTEMD_DIR="$HOME/.config/systemd/user"
1596
- mkdir -p "$USER_SYSTEMD_DIR"
1597
- SERVICE_FILE="$USER_SYSTEMD_DIR/agy-standalone.service"
1598
- cat << EOF > "$SERVICE_FILE"
1599
- [Unit]
1600
- Description=Antigravity Mesh Local Standalone MCP Server
1601
- After=network.target
1602
-
1603
- [Service]
1604
- Type=simple
1605
- $ENV_FILE_LINE
1606
- WorkingDirectory=$SCRIPT_DIR
1607
- ExecStart=$PYTHON_BIN -m core.server --host 127.0.0.1 --port=$PORT
1608
- Restart=always
1609
- RestartSec=5
1610
-
1611
- [Install]
1612
- WantedBy=default.target
1613
- EOF
1614
- systemctl --user daemon-reload 2>/dev/null || true
1615
- systemctl --user enable --now agy-standalone.service 2>/dev/null || {
1616
- pkill -f "core.server" 2>/dev/null || true
1617
- if [ -n "$TOKEN" ]; then
1618
- nohup env MESH_TOKEN="$TOKEN" $PYTHON_BIN -m core.server --host 127.0.0.1 --port="$PORT" > "$CONFIG_DIR/standalone.log" 2>&1 &
1619
- else
1620
- nohup $PYTHON_BIN -m core.server --host 127.0.0.1 --port="$PORT" > "$CONFIG_DIR/standalone.log" 2>&1 &
1621
- fi
1622
- }
1623
- loginctl enable-linger "$USER" 2>/dev/null || true
1624
- fi
1625
-
1626
- if [ "$LANG_CHOICE" = "ru" ]; then
1627
- echo "[3/3] Проверка локального эндпоинта..."
1628
- else
1629
- echo "[3/3] Verifying local endpoint..."
1630
- fi
1631
- HEALTH_URL="http://127.0.0.1:${PORT}/health"
1632
- HEALTH_OK=false
1633
- for _attempt in 1 2 3 4 5 6 7 8 9 10; do
1634
- HTTP_CODE=$(curl -s -o /dev/null -m 2 -w "%{http_code}" "$HEALTH_URL" 2>/dev/null || true)
1635
- if [ "$HTTP_CODE" = "200" ]; then
1636
- HEALTH_OK=true
1637
- break
1638
- fi
1639
- sleep 1
1640
- done
1641
- if [ "$HEALTH_OK" != true ]; then
1642
- if [ "$LANG_CHOICE" = "ru" ]; then
1643
- echo -e "\033[1;31m[ОШИБКА] Standalone MCP-сервер не отвечает (HTTP ${HTTP_CODE:-нет ответа}): $HEALTH_URL\033[0m" >&2
1644
- if [ "$IS_TERMUX" = true ]; then
1645
- echo -e " Статус: ${CYAN}sv status agy-standalone${RESET} (runit, termux-services)" >&2
1646
- echo -e " Логи: ${CYAN}$TERMUX_PREFIX/var/log/sv/agy-standalone/current${RESET}" >&2
1647
- echo -e " Локальный сервер на телефоне доступен только внутри Termux (127.0.0.1):" >&2
1648
- echo -e " для Gemini с телефона используйте режим туннеля (--mode=tunnel)." >&2
1649
- else
1650
- echo -e " Проверьте статус: ${CYAN}systemctl --user status agy-standalone.service${RESET}" >&2
1651
- echo -e " Логи: ${CYAN}journalctl --user -u agy-standalone.service -n 50 --no-pager${RESET}" >&2
1652
- fi
1653
- else
1654
- echo -e "\033[1;31m[ERROR] Standalone MCP server is not responding (HTTP ${HTTP_CODE:-no response}): $HEALTH_URL\033[0m" >&2
1655
- if [ "$IS_TERMUX" = true ]; then
1656
- echo -e " Status: ${CYAN}sv status agy-standalone${RESET} (runit, termux-services)" >&2
1657
- echo -e " Logs: ${CYAN}$TERMUX_PREFIX/var/log/sv/agy-standalone/current${RESET}" >&2
1658
- echo -e " A local server on a phone is reachable inside Termux only (127.0.0.1):" >&2
1659
- echo -e " use --mode=tunnel to drive this phone from Gemini." >&2
1660
- else
1661
- echo -e " Check status: ${CYAN}systemctl --user status agy-standalone.service${RESET}" >&2
1662
- echo -e " Logs: ${CYAN}journalctl --user -u agy-standalone.service -n 50 --no-pager${RESET}" >&2
1663
- fi
1664
- fi
1665
- exit 1
1666
- fi
1667
-
1668
- MCP_LOCAL_URL="http://localhost:${PORT}/sse"
1669
-
1670
- # Quick Copy to Clipboard
1671
- COPIED=false
1672
- B64_URL=$(printf "%s" "$MCP_LOCAL_URL" | base64 | tr -d '\r\n')
1673
- printf "\033]52;c;%s\a" "$B64_URL" 2>/dev/null || true
1674
- if command -v termux-clipboard-set >/dev/null 2>&1; then
1675
- # Termux on a phone: termux-api plus the Termux:API app put the URL on
1676
- # Android's clipboard, where the Gemini app can paste it. Without the app
1677
- # the command exists but fails, so COPIED stays false and the hint below
1678
- # tells the user to copy by hand.
1679
- printf "%s" "$MCP_LOCAL_URL" | termux-clipboard-set 2>/dev/null && COPIED=true
1680
- elif command -v wl-copy >/dev/null 2>&1; then
1681
- printf "%s" "$MCP_LOCAL_URL" | wl-copy 2>/dev/null && COPIED=true
1682
- elif command -v xclip >/dev/null 2>&1; then
1683
- printf "%s" "$MCP_LOCAL_URL" | xclip -selection clipboard 2>/dev/null && COPIED=true
1684
- elif command -v xsel >/dev/null 2>&1; then
1685
- printf "%s" "$MCP_LOCAL_URL" | xsel --clipboard --input 2>/dev/null && COPIED=true
1686
- elif command -v pbcopy >/dev/null 2>&1; then
1687
- printf "%s" "$MCP_LOCAL_URL" | pbcopy 2>/dev/null && COPIED=true
1688
- elif command -v clip.exe >/dev/null 2>&1; then
1689
- printf "%s" "$MCP_LOCAL_URL" | clip.exe 2>/dev/null && COPIED=true
1690
- fi
1691
-
1692
- clear 2>/dev/null || printf "\033[2J\033[H" || true
1693
-
1694
- if [ "$LANG_CHOICE" = "ru" ]; then
1695
- echo ""
1696
- copy_msg=""
1697
- if [ "$COPIED" = true ]; then
1698
- copy_msg="[OK] ССЫЛКА СКОПИРОВАНА В БУФЕР ОБМЕНА! (Вставьте через Ctrl+V)"
1699
- else
1700
- copy_msg="Скопируйте ссылку выше"
1701
- fi
1702
- print_mcp_box "ССЫЛКА ЛОКАЛЬНОГО MCP-СЕРВЕРА (СКОПИРУЙТЕ):" "$MCP_LOCAL_URL" "$copy_msg"
1703
- echo ""
1704
- echo -e " ${BOLD}Куда вставлять ссылку в Google Gemini:${RESET}"
1705
- echo -e " 1. Откройте в браузере: ${CYAN}https://gemini.google.com/spark/apps${RESET}"
1706
- echo -e " 2. Нажмите ${BOLD}'Добавить приложение'${RESET} (Add app / Настройки MCP)"
1707
- echo -e " 3. Вставьте скопированную ссылку в поле ${BOLD}'URL сервера'${RESET} (${BOLD}Ctrl+V${RESET}) и нажмите ${BOLD}Подключить${RESET}."
1708
- echo ""
1709
- else
1710
- echo ""
1711
- copy_msg=""
1712
- if [ "$COPIED" = true ]; then
1713
- copy_msg="[OK] URL COPIED TO CLIPBOARD! (Press Ctrl+V to paste)"
1714
- else
1715
- copy_msg="Copy the URL above"
1716
- fi
1717
- print_mcp_box "LOCAL MCP SERVER URL (COPY THIS):" "$MCP_LOCAL_URL" "$copy_msg"
1718
- echo ""
1719
- echo -e " ${BOLD}Where to paste this URL in Google Gemini:${RESET}"
1720
- echo -e " 1. Open in your browser: ${CYAN}https://gemini.google.com/spark/apps${RESET}"
1721
- echo -e " 2. Click ${BOLD}'Add app'${RESET} (or navigate to MCP settings)"
1722
- echo -e " 3. Paste the URL into the ${BOLD}'Server URL'${RESET} field (${BOLD}Ctrl+V${RESET}) and click ${BOLD}Connect${RESET}."
1723
- echo ""
1724
- fi
1725
- if [ "$IS_TERMUX" = true ]; then
1726
- print_termux_hints "agy-standalone"
1727
- fi
1728
- exit 0
1729
- fi
1730
-
1731
- # ==============================================================================
1732
- # BRANCH: CLOUD GATEWAY + REVERSE TUNNEL
1733
- # ==============================================================================
1734
- if [ "$LANG_CHOICE" = "ru" ]; then
1735
- echo -e "\n${BOLD}${MAGENTA}=== Настройка облачного туннеля (общий домен) ===${RESET}"
1736
- else
1737
- echo -e "\n${BOLD}${MAGENTA}=== Configuring Cloud Gateway Tunnel (shared domain) ===${RESET}"
1738
- fi
1739
-
1740
- if [ -z "$USERNAME" ]; then
1741
- CLEAN_HOST=$(echo "$DETECTED_HOSTNAME" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_-')
1742
- USERNAME="$CLEAN_HOST"
1743
- fi
1744
- USERNAME=$(echo "$USERNAME" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_-')
1745
- [ -z "$USERNAME" ] && USERNAME="mesh-node"
1746
-
1747
- mkdir -p "$CONFIG_DIR"
1748
-
1749
- if [ "$LANG_CHOICE" = "ru" ]; then
1750
- echo "[1/4] Проверка Python зависимостей (websockets)..."
1751
- else
1752
- echo "[1/4] Checking Python dependencies (websockets)..."
1753
- fi
1754
-
1755
- ensure_dependencies
1756
- # Registration - the step right after this - is a curl POST. A machine with a usable
1757
- # Python and no curl (a minimal WSL image, a slim container) used to die there with
1758
- # "curl: command not found" and no hint about what to install.
1759
- ensure_curl || exit 1
1760
- # Registration is also the first step that truly needs the gateway: a name that does
1761
- # not resolve here would be reported a moment later as "no authentication token",
1762
- # which blames the gateway's answer instead of this device's DNS.
1763
- require_gateway_dns || exit 1
1764
-
1765
- if [ "$LANG_CHOICE" = "ru" ]; then
1766
- echo "[2/4] Регистрация узла '${USERNAME}' на общем домене (${GATEWAY})..."
1767
- else
1768
- echo "[2/4] Registering node '${USERNAME}' on the shared domain (${GATEWAY})..."
1769
- fi
1770
-
1771
- EXISTING_TOKEN=""
1772
- if [ -f "$CONFIG_FILE" ]; then
1773
- CFG_USER=$(grep "MESH_USER" "$CONFIG_FILE" 2>/dev/null | cut -d '=' -f2 || true)
1774
- CFG_TOKEN=$(grep "MESH_TOKEN" "$CONFIG_FILE" 2>/dev/null | cut -d '=' -f2 || true)
1775
- if [ "$CFG_USER" = "$USERNAME" ] && [ -n "$CFG_TOKEN" ]; then
1776
- EXISTING_TOKEN="$CFG_TOKEN"
1777
- fi
1778
- fi
1779
-
1780
- if [ -n "$TOKEN" ]; then
1781
- REQ_TOKEN="$TOKEN"
1782
- elif [ -n "$EXISTING_TOKEN" ]; then
1783
- REQ_TOKEN="$EXISTING_TOKEN"
1784
- else
1785
- REQ_TOKEN=""
1786
- fi
1787
-
1788
- DETECTED_MAC=""
1789
- # "$PYTHON_BIN", never a bare python3: after the venv or uv fallback the working
1790
- # interpreter is not on PATH, and the bare name would silently produce no MAC.
1791
- if python_works "$PYTHON_BIN"; then
1792
- DETECTED_MAC=$("$PYTHON_BIN" -c "import uuid; print(':'.join(['{:02x}'.format((uuid.getnode() >> ele) & 0xff) for ele in range(0,8*6,8)][::-1]))" 2>/dev/null || true)
1793
- fi
1794
- if [ -z "$DETECTED_MAC" ] || [ "$DETECTED_MAC" = "00:00:00:00:00:00" ]; then
1795
- for iface in /sys/class/net/*; do
1796
- if [ -f "$iface/address" ] && [ "$(cat "$iface/type" 2>/dev/null)" = "1" ]; then
1797
- addr=$(cat "$iface/address" 2>/dev/null)
1798
- if [ -n "$addr" ] && [ "$addr" != "00:00:00:00:00:00" ]; then
1799
- DETECTED_MAC="$addr"
1800
- break
1801
- fi
1802
- fi
1803
- done
1804
- fi
1805
-
1806
- # One field out of the registration answer. The JSON arrives on stdin rather than
1807
- # in argv (a token or an error message can contain quotes), and "$PYTHON_BIN" is
1808
- # used because after the venv or uv fallback there is no python3 on PATH - a bare
1809
- # name would return an empty token, which the check below would then blame on the
1810
- # gateway.
1811
- json_field() {
1812
- # tr strips the CR that a Windows python.exe appends under Git Bash/MSYS.
1813
- # install.sh explicitly supports MINGW/MSYS/CYGWIN (see DEFAULT_DOMAIN_DIR), and
1814
- # a trailing CR inside the token would be pasted straight into the wss:// URL.
1815
- printf '%s' "$1" | "$PYTHON_BIN" -c 'import json, sys; print(json.load(sys.stdin).get(sys.argv[1], ""))' "$2" 2>/dev/null | tr -d '\r\n' || true
1816
- }
1817
-
1818
- REG_BODY="{\"username\": \"${USERNAME}\", \"auto_suffix\": true, \"mac_address\": \"${DETECTED_MAC}\", \"os\": \"${DETECTED_OS}\", \"arch\": \"${DETECTED_ARCH}\""
1819
- if [ -n "$REQ_TOKEN" ]; then
1820
- REG_BODY="${REG_BODY}, \"token\": \"${REQ_TOKEN}\""
1821
- fi
1822
- REG_BODY="${REG_BODY}}"
1823
-
1824
- REG_RESP=$(curl -s -X POST "https://${GATEWAY}/api/register" \
1825
- -H "Content-Type: application/json" \
1826
- -d "$REG_BODY")
1827
-
1828
- ASSIGNED_USER=$(json_field "$REG_RESP" username)
1829
- ASSIGNED_TOKEN=$(json_field "$REG_RESP" token)
1830
-
1831
- if [ -z "$ASSIGNED_TOKEN" ]; then
1832
- FALLBACK_USER="${USERNAME}-$(date +%s | tail -c 4)"
1833
- REG_RESP=$(curl -s -X POST "https://${GATEWAY}/api/register" \
1834
- -H "Content-Type: application/json" \
1835
- -d "{\"username\": \"${FALLBACK_USER}\", \"auto_suffix\": true, \"mac_address\": \"${DETECTED_MAC}\", \"os\": \"${DETECTED_OS}\", \"arch\": \"${DETECTED_ARCH}\"}")
1836
- ASSIGNED_USER=$(json_field "$REG_RESP" username)
1837
- ASSIGNED_TOKEN=$(json_field "$REG_RESP" token)
1838
- fi
1839
-
1840
- if [ -z "$ASSIGNED_TOKEN" ]; then
1841
- echo -e "${RED}[ERROR] Failed to obtain authentication token from gateway.${RESET}"
1842
- exit 1
1843
- fi
1844
-
1845
- if [ "$ASSIGNED_USER" != "$USERNAME" ]; then
1846
- if [ "$LANG_CHOICE" = "ru" ]; then
1847
- echo -e "${YELLOW}ℹ️ Имя '${USERNAME}' уже было занято. Автоматически назначено имя узла:${RESET} ${BOLD}${GREEN}${ASSIGNED_USER}${RESET}"
1848
- else
1849
- echo -e "${YELLOW}ℹ️ Name '${USERNAME}' was already taken. Automatically assigned node name:${RESET} ${BOLD}${GREEN}${ASSIGNED_USER}${RESET}"
1850
- fi
1851
- fi
1852
-
1853
- if [ "$LANG_CHOICE" = "ru" ]; then
1854
- echo "[3/4] Сохранение конфигурации в ${CONFIG_FILE}..."
1855
- else
1856
- echo "[3/4] Saving configuration to ${CONFIG_FILE}..."
1857
- fi
1858
-
1859
- cat << EOF > "$CONFIG_FILE"
1860
- MESH_GATEWAY=${GATEWAY}
1861
- MESH_USER=${ASSIGNED_USER}
1862
- MESH_TOKEN=${ASSIGNED_TOKEN}
1863
- EOF
1864
- chmod 600 "$CONFIG_FILE"
1865
-
1866
- # agent.env carries only the legacy MESH_GATEWAY, which core/domain.py honours for
1867
- # the tunnel but never for the links a node publishes. Writing the domain down is
1868
- # what keeps share links on the gateway this node is registered on.
1869
- write_domain_file "$GATEWAY"
1870
-
1871
- if [ "$LANG_CHOICE" = "ru" ]; then
1872
- if [ "$IS_TERMUX" = true ]; then
1873
- echo "[4/4] Настройка и запуск автозапуска на телефоне (runit + Termux:Boot)..."
1874
- else
1875
- echo "[4/4] Настройка и запуск службы автозапуска на ПК..."
1876
- fi
1877
- else
1878
- echo "[4/4] Configuring and starting background autostart service..."
1879
- fi
1880
-
1881
- if [ "$(uname -s)" = "Darwin" ]; then
1882
- LAUNCH_AGENTS="$HOME/Library/LaunchAgents"
1883
- mkdir -p "$LAUNCH_AGENTS"
1884
- PLIST_FILE="$LAUNCH_AGENTS/com.antigravity.mesh.agent.plist"
1885
- cat << EOF > "$PLIST_FILE"
1886
- <?xml version="1.0" encoding="UTF-8"?>
1887
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1888
- <plist version="1.0">
1889
- <dict>
1890
- <key>Label</key>
1891
- <string>com.antigravity.mesh.agent</string>
1892
- <key>ProgramArguments</key>
1893
- <array>
1894
- <string>$PYTHON_BIN</string>
1895
- <string>-m</string>
1896
- <string>core.agent</string>
1897
- </array>
1898
- <key>WorkingDirectory</key>
1899
- <string>$SCRIPT_DIR</string>
1900
- <key>EnvironmentVariables</key>
1901
- <dict>
1902
- <key>MESH_GATEWAY</key>
1903
- <string>$GATEWAY</string>
1904
- <key>MESH_USER</key>
1905
- <string>$ASSIGNED_USER</string>
1906
- <key>MESH_TOKEN</key>
1907
- <string>$ASSIGNED_TOKEN</string>
1908
- </dict>
1909
- <key>RunAtLoad</key>
1910
- <true/>
1911
- <key>KeepAlive</key>
1912
- <true/>
1913
- </dict>
1914
- </plist>
1915
- EOF
1916
- launchctl unload "$PLIST_FILE" 2>/dev/null || true
1917
- launchctl load -w "$PLIST_FILE" 2>/dev/null || true
1918
- elif [ "$IS_TERMUX" = true ]; then
1919
- # Android: the tunnel node is supervised by runit inside Termux, and the
1920
- # Termux:Boot entry brings it back after a reboot - see configure_termux_node.
1921
- configure_termux_node "agy-agent" "core.agent" "" "$CONFIG_FILE"
1922
- else
1923
- USER_SYSTEMD_DIR="$HOME/.config/systemd/user"
1924
- mkdir -p "$USER_SYSTEMD_DIR"
1925
- cat << EOF > "$USER_SYSTEMD_DIR/agy-agent.service"
1926
- [Unit]
1927
- Description=Antigravity Mesh Reverse RPC Agent
1928
- After=network.target
1929
-
1930
- [Service]
1931
- Type=simple
1932
- EnvironmentFile=$CONFIG_FILE
1933
- WorkingDirectory=$SCRIPT_DIR
1934
- ExecStart=$PYTHON_BIN -m core.agent
1935
- Restart=always
1936
- RestartSec=5
1937
-
1938
- [Install]
1939
- WantedBy=default.target
1940
- EOF
1941
- systemctl --user daemon-reload 2>/dev/null || true
1942
- systemctl --user enable --now agy-agent.service 2>/dev/null || {
1943
- pkill -f "core.agent" 2>/dev/null || true
1944
- nohup $PYTHON_BIN -m core.agent > "$CONFIG_DIR/agent.log" 2>&1 &
1945
- }
1946
- loginctl enable-linger "$USER" 2>/dev/null || true
1947
- fi
1948
-
1949
- sleep 1
1950
-
1951
- # Canonical shared-domain MCP URL (one domain for every node; ?user= selects it).
1952
- MCP_URL="$(node_sse_url "$ASSIGNED_USER" "$ASSIGNED_TOKEN")"
1953
-
1954
- # Quick Copy to Clipboard
1955
- COPIED=false
1956
- B64_URL=$(printf "%s" "$MCP_URL" | base64 | tr -d '\r\n')
1957
- printf "\033]52;c;%s\a" "$B64_URL" 2>/dev/null || true
1958
- if command -v termux-clipboard-set >/dev/null 2>&1; then
1959
- # Termux on a phone: termux-api plus the Termux:API app put the URL on
1960
- # Android's clipboard, where the Gemini app can paste it. Without the app the
1961
- # command exists but fails, so COPIED stays false and the hint says so.
1962
- printf "%s" "$MCP_URL" | termux-clipboard-set 2>/dev/null && COPIED=true
1963
- elif command -v wl-copy >/dev/null 2>&1; then
1964
- printf "%s" "$MCP_URL" | wl-copy 2>/dev/null && COPIED=true
1965
- elif command -v xclip >/dev/null 2>&1; then
1966
- printf "%s" "$MCP_URL" | xclip -selection clipboard 2>/dev/null && COPIED=true
1967
- elif command -v xsel >/dev/null 2>&1; then
1968
- printf "%s" "$MCP_URL" | xsel --clipboard --input 2>/dev/null && COPIED=true
1969
- elif command -v pbcopy >/dev/null 2>&1; then
1970
- printf "%s" "$MCP_URL" | pbcopy 2>/dev/null && COPIED=true
1971
- elif command -v clip.exe >/dev/null 2>&1; then
1972
- printf "%s" "$MCP_URL" | clip.exe 2>/dev/null && COPIED=true
1973
- fi
1974
-
1975
- clear 2>/dev/null || printf "\033[2J\033[H" || true
1976
-
1977
- if [ "$LANG_CHOICE" = "ru" ]; then
1978
- echo ""
1979
- copy_msg=""
1980
- if [ "$COPIED" = true ]; then
1981
- copy_msg="[OK] ССЫЛКА СКОПИРОВАНА В БУФЕР ОБМЕНА! (Вставьте через Ctrl+V)"
1982
- else
1983
- copy_msg="Скопируйте ссылку выше"
1984
- fi
1985
- print_mcp_box "ССЫЛКА MCP-СЕРВЕРА ДЛЯ ПОДКЛЮЧЕНИЯ (СКОПИРУЙТЕ):" "$MCP_URL" "$copy_msg"
1986
- echo ""
1987
- echo -e " ${BOLD}Куда вставлять ссылку в Google Gemini:${RESET}"
1988
- echo -e " 1. Откройте в браузере: ${CYAN}https://gemini.google.com/spark/apps${RESET}"
1989
- echo -e " 2. Нажмите ${BOLD}'Добавить приложение'${RESET} (Add app / Настройки MCP)"
1990
- echo -e " 3. Вставьте скопированную ссылку в поле ${BOLD}'URL сервера'${RESET} (${BOLD}Ctrl+V${RESET}) и нажмите ${BOLD}Подключить${RESET}."
1991
- echo ""
1992
- else
1993
- echo ""
1994
- copy_msg=""
1995
- if [ "$COPIED" = true ]; then
1996
- copy_msg="[OK] URL COPIED TO CLIPBOARD! (Paste with Ctrl+V)"
1997
- else
1998
- copy_msg="Copy the URL above"
1999
- fi
2000
- print_mcp_box "MCP SERVER CONNECTION URL (COPY THIS):" "$MCP_URL" "$copy_msg"
2001
- echo ""
2002
- echo -e " ${BOLD}Where to paste this URL in Google Gemini:${RESET}"
2003
- echo -e " 1. Open in your browser: ${CYAN}https://gemini.google.com/spark/apps${RESET}"
2004
- echo -e " 2. Click ${BOLD}'Add app'${RESET} (or navigate to MCP settings)"
2005
- echo -e " 3. Paste the URL into the ${BOLD}'Server URL'${RESET} field (${BOLD}Ctrl+V${RESET}) and click ${BOLD}Connect${RESET}."
2006
- echo ""
2007
- fi
2008
-
2009
- # The phone-specific steps (Termux:Boot, battery optimisation, clipboard) are the
2010
- # same in both modes; on a Linux/macOS/Windows node there is nothing to add.
2011
- if [ "$IS_TERMUX" = true ]; then
2012
- print_termux_hints "agy-agent"
2013
- fi