gemini-computer-use 0.4.2 → 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,1998 +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
- for f in core/agent.py core/server.py core/mcp_tools.py core/web_share.py core/domain.py core/vitals.py core/updater.py core/version.py core/__init__.py skills/orchestrator.md; do
880
- bootstrap_tmp="$BOOTSTRAP_DIR/${f}.part"
881
- if curl -fsSL "https://${GATEWAY}/${f}" -o "$bootstrap_tmp" 2>/dev/null \
882
- || curl -fsSL "https://raw.githubusercontent.com/LevRa7/Computer-use-for-Gemini-App-Web/main/${f}" -o "$bootstrap_tmp" 2>/dev/null; then
883
- mv "$bootstrap_tmp" "$BOOTSTRAP_DIR/${f}"
884
- else
885
- rm -f "$bootstrap_tmp"
886
- fi
887
- done
888
- unset bootstrap_tmp
889
- # A bootstrap that downloaded nothing (an unreachable gateway and no GitHub)
890
- # leaves a directory that names modules which do not exist: every step after it
891
- # would write a launcher for code that is not there, and the node would fail
892
- # with a Python import error at boot.
893
- if [ ! -f "$BOOTSTRAP_DIR/core/agent.py" ]; then
894
- if [ "$LANG_CHOICE" = "ru" ]; then
895
- echo -e "${RED}${BOLD}[ОШИБКА] Не удалось скачать код узла.${RESET}" >&2
896
- echo -e " Пробовали: ${CYAN}https://${GATEWAY}/core/agent.py${RESET} и ${CYAN}github.com/LevRa7/Computer-use-for-Gemini-App-Web${RESET}." >&2
897
- echo -e " Проверьте сеть и DNS либо скопируйте репозиторий на устройство и запустите из него:" >&2
898
- 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
899
- echo -e " ${YELLOW}Остановка: без кода узла запускать нечего, ничего не создано.${RESET}" >&2
900
- else
901
- echo -e "${RED}${BOLD}[ERROR] Could not download the node code.${RESET}" >&2
902
- echo -e " Tried: ${CYAN}https://${GATEWAY}/core/agent.py${RESET} and ${CYAN}github.com/LevRa7/Computer-use-for-Gemini-App-Web${RESET}." >&2
903
- echo -e " Check the network and DNS, or put the repository on this device and install from it:" >&2
904
- 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
905
- echo -e " ${YELLOW}Stopping: there is no node code to run, nothing was created.${RESET}" >&2
906
- fi
907
- echo ""
908
- exit 1
909
- fi
910
- SCRIPT_DIR="$BOOTSTRAP_DIR"
911
- fi
912
-
913
- # ------------------------------------------------------------------------------
914
- # SHARED-DOMAIN CONTRACT (canonical step)
915
- # Every node is reached through ONE public domain ($GATEWAY); the node is
916
- # selected by the ?user= query parameter. The installer never mints a
917
- # per-device subdomain: each extra hostname would need its own DNS record and
918
- # TLS SAN, and a name missing from the certificate fails the handshake with an
919
- # opaque "cannot connect to host" error in the Gemini client.
920
- # sse https://<shared-domain>/sse?user=<node-name>&token=<token>
921
- # http https://<shared-domain>/mcp?user=<node-name>&token=<token>
922
- # tunnel wss://<shared-domain>/ws/tunnel?user=<node-name>&token=<token>
923
- # $GATEWAY is read at call time so the interactive menu can still change it.
924
- # ------------------------------------------------------------------------------
925
- node_sse_url() { printf 'https://%s/sse?user=%s&token=%s' "$GATEWAY" "$1" "$2"; }
926
-
927
- # Language prompt if interactive and not specified
928
- if [ -z "$LANG_CHOICE" ]; then
929
- if [ "$QUICK" = false ] && [ -z "$MODE" ] && [ -t 0 ]; then
930
- echo -e "\n${BOLD}${CYAN}╔════════════════════════════════════════════════════════════════════════╗${RESET}"
931
- echo -e "${BOLD}${CYAN}║ 🌐 LANGUAGE SELECTION / ВЫБОР ЯЗЫКА ║${RESET}"
932
- echo -e "${BOLD}${CYAN}╚════════════════════════════════════════════════════════════════════════╝${RESET}"
933
- echo -e " ${GREEN}1)${RESET} English (Default)"
934
- echo -e " ${BLUE}2)${RESET} Русский"
935
- read -rp "Select / Выберите [1]: " LANG_INPUT
936
- if [ "$LANG_INPUT" = "2" ] || [ "$LANG_INPUT" = "ru" ] || [ "$LANG_INPUT" = "RU" ]; then
937
- LANG_CHOICE="ru"
938
- else
939
- LANG_CHOICE="en"
940
- fi
941
- else
942
- LANG_CHOICE="en"
943
- fi
944
- fi
945
-
946
- # Device detection
947
- # ------------------------------------------------------------------------------
948
- # ARCHITECTURE
949
- #
950
- # The system package managers resolve their own architecture, so on Linux this
951
- # matters for exactly two things: the uv archive, which is published per CPU and
952
- # answers the wrong one with an unhelpful "Exec format error", and the
953
- # registration payload, so a gateway operator can see which build a node runs.
954
- # macOS reports the same machine names as Linux but needs the -apple-darwin assets.
955
- # ------------------------------------------------------------------------------
956
- detect_arch() {
957
- DETECTED_ARCH="$(uname -m 2>/dev/null || echo unknown)"
958
- case "$DETECTED_ARCH" in
959
- x86_64|amd64) ARCH_LABEL="x86_64 (64-bit)"; UV_TARGET="x86_64-unknown-linux-gnu" ;;
960
- aarch64|arm64) ARCH_LABEL="aarch64 (64-bit ARM)"; UV_TARGET="aarch64-unknown-linux-gnu" ;;
961
- armv7l|armv7|armv6l) ARCH_LABEL="armv7 (32-bit ARM)"; UV_TARGET="armv7-unknown-linux-gnueabihf" ;;
962
- i386|i486|i586|i686) ARCH_LABEL="x86 (32-bit)"; UV_TARGET="i686-unknown-linux-gnu" ;;
963
- *) ARCH_LABEL="$DETECTED_ARCH"; UV_TARGET="" ;;
964
- esac
965
- if [ "$(uname -s 2>/dev/null)" = "Darwin" ]; then
966
- case "$DETECTED_ARCH" in
967
- x86_64) UV_TARGET="x86_64-apple-darwin" ;;
968
- arm64) UV_TARGET="aarch64-apple-darwin" ;;
969
- esac
970
- fi
971
- if [ "$IS_TERMUX" = true ]; then
972
- # uv's archives are glibc/musl builds; neither runs against Android's
973
- # bionic libc, and asking for one yields an "Exec format error" that names
974
- # no cause. On a phone the package manager is the only source of Python.
975
- UV_TARGET=""
976
- fi
977
- }
978
-
979
- # Android system properties. getprop lives in /system/bin on every Android build
980
- # and is readable without root; a missing getprop (or an empty property) simply
981
- # yields an empty value, never a failed install.
982
- android_prop() {
983
- command -v getprop >/dev/null 2>&1 || return 0
984
- getprop "$1" 2>/dev/null | tr -d '\r' | head -n 1
985
- }
986
-
987
- detect_device() {
988
- # `|| true` on purpose: a minimal container (and Termux, which has no
989
- # hostname(1) of its own) answers neither spelling, and an assignment whose
990
- # substitution fails would abort the whole installer under `set -e`.
991
- DETECTED_HOSTNAME=$(hostname -s 2>/dev/null || hostname 2>/dev/null || true)
992
- detect_arch
993
-
994
- if [ "$IS_TERMUX" = true ]; then
995
- ANDROID_MODEL="$(android_prop ro.product.model)"
996
- [ -n "$ANDROID_MODEL" ] || ANDROID_MODEL="$(android_prop ro.product.device)"
997
- ANDROID_RELEASE="$(android_prop ro.build.version.release)"
998
- [ -n "$ANDROID_RELEASE" ] || ANDROID_RELEASE="$(android_prop ro.build.version.sdk)"
999
- DETECTED_OS="Android ${ANDROID_RELEASE:-unknown} (Termux)"
1000
- # The phone itself names the node. Termux has no hostname binary, and
1001
- # Android's gethostname(2) answers "localhost" - a name every phone would
1002
- # claim, and the gateway keeps exactly one tunnel per name, so they would
1003
- # knock each other offline in a loop. The device model is stable, and it is
1004
- # sanitised below exactly like a hostname.
1005
- DETECTED_HOSTNAME="${ANDROID_MODEL:-android-node}"
1006
- if [ "$LANG_CHOICE" = "ru" ]; then
1007
- DETECTED_TYPE="Смартфон/планшет Android (Termux)"
1008
- else
1009
- DETECTED_TYPE="Android phone/tablet (Termux)"
1010
- fi
1011
- elif [ "$(uname -s)" = "Darwin" ]; then
1012
- DETECTED_OS="macOS $(sw_vers -productVersion 2>/dev/null || '')"
1013
- if [ "$LANG_CHOICE" = "ru" ]; then
1014
- DETECTED_TYPE="Apple Mac"
1015
- sysctl -n hw.model 2>/dev/null | grep -qi "book" && DETECTED_TYPE="Apple MacBook (Ноутбук)"
1016
- else
1017
- DETECTED_TYPE="Apple Mac"
1018
- sysctl -n hw.model 2>/dev/null | grep -qi "book" && DETECTED_TYPE="Apple MacBook (Laptop)"
1019
- fi
1020
- elif [ -f /etc/os-release ]; then
1021
- # shellcheck disable=SC1091
1022
- . /etc/os-release
1023
- DETECTED_OS="${PRETTY_NAME:-$NAME}"
1024
- else
1025
- DETECTED_OS=$(uname -s)
1026
- fi
1027
-
1028
- if [ "$(uname -s)" != "Darwin" ] && [ "$IS_TERMUX" != true ]; then
1029
- # The virtualised environments are asked FIRST, before the battery. WSL2
1030
- # forwards the host's battery into /sys/class/power_supply, so the battery
1031
- # test matched inside WSL and reported the user's laptop instead of the
1032
- # subsystem they were actually installing into - the node registered as
1033
- # "Laptop" while running under Windows.
1034
- if [ "$LANG_CHOICE" = "ru" ]; then
1035
- DETECTED_TYPE="Десктоп / Сервер"
1036
- if grep -q -i "microsoft" /proc/version 2>/dev/null; then
1037
- DETECTED_TYPE="WSL (Windows Subsystem for Linux)"
1038
- elif [ -f /.dockerenv ] || grep -q "docker\|containerd" /proc/1/cgroup 2>/dev/null; then
1039
- DETECTED_TYPE="Контейнер (Docker/LXC)"
1040
- elif [ -d /sys/class/power_supply ] && ls /sys/class/power_supply/BAT* 1>/dev/null 2>&1; then
1041
- DETECTED_TYPE="Ноутбук (Laptop)"
1042
- elif command -v systemd-detect-virt >/dev/null 2>&1 && systemd-detect-virt -q; then
1043
- DETECTED_TYPE="Облачный сервер / VPS ($(systemd-detect-virt))"
1044
- fi
1045
- else
1046
- DETECTED_TYPE="Desktop / Server"
1047
- if grep -q -i "microsoft" /proc/version 2>/dev/null; then
1048
- DETECTED_TYPE="WSL (Windows Subsystem for Linux)"
1049
- elif [ -f /.dockerenv ] || grep -q "docker\|containerd" /proc/1/cgroup 2>/dev/null; then
1050
- DETECTED_TYPE="Container (Docker/LXC)"
1051
- elif [ -d /sys/class/power_supply ] && ls /sys/class/power_supply/BAT* 1>/dev/null 2>&1; then
1052
- DETECTED_TYPE="Laptop"
1053
- elif command -v systemd-detect-virt >/dev/null 2>&1 && systemd-detect-virt -q; then
1054
- DETECTED_TYPE="Cloud VPS ($(systemd-detect-virt))"
1055
- fi
1056
- fi
1057
- fi
1058
- }
1059
-
1060
- detect_device
1061
-
1062
- print_device_info() {
1063
- if [ "$LANG_CHOICE" = "ru" ]; then
1064
- echo -e "${CYAN}╔════════════════════════════════════════════════════════════════════════╗${RESET}"
1065
- echo -e "${CYAN}║${RESET} ${BOLD}🔍 Обнаружено устройство:${RESET}"
1066
- echo -e "${CYAN}║${RESET} • Имя хоста : ${GREEN}${DETECTED_HOSTNAME}${RESET}"
1067
- echo -e "${CYAN}║${RESET} • Тип : ${YELLOW}${DETECTED_TYPE}${RESET}"
1068
- echo -e "${CYAN}║${RESET} • ОС : ${DETECTED_OS}"
1069
- echo -e "${CYAN}║${RESET} • Архитектура : ${ARCH_LABEL}"
1070
- echo -e "${CYAN}╚════════════════════════════════════════════════════════════════════════╝${RESET}"
1071
- else
1072
- echo -e "${CYAN}╔════════════════════════════════════════════════════════════════════════╗${RESET}"
1073
- echo -e "${CYAN}║${RESET} ${BOLD}🔍 Detected Device:${RESET}"
1074
- echo -e "${CYAN}║${RESET} • Hostname : ${GREEN}${DETECTED_HOSTNAME}${RESET}"
1075
- echo -e "${CYAN}║${RESET} • Type : ${YELLOW}${DETECTED_TYPE}${RESET}"
1076
- echo -e "${CYAN}║${RESET} • OS : ${DETECTED_OS}"
1077
- echo -e "${CYAN}║${RESET} • Architecture: ${ARCH_LABEL}"
1078
- echo -e "${CYAN}╚════════════════════════════════════════════════════════════════════════╝${RESET}"
1079
- fi
1080
- }
1081
-
1082
- print_mcp_box() {
1083
- local title="$1"
1084
- local url="$2"
1085
- local copy_msg="$3"
1086
-
1087
- local max_len=${#title}
1088
- [ ${#url} -gt $max_len ] && max_len=${#url}
1089
- [ -n "$copy_msg" ] && [ ${#copy_msg} -gt $max_len ] && max_len=${#copy_msg}
1090
-
1091
- local width=$(( max_len + 4 ))
1092
- [ $width -lt 84 ] && width=84
1093
-
1094
- local dashes=$(printf '%*s' "$width" '' | tr ' ' '-')
1095
- local border=" +${dashes}+"
1096
- local empty=" |$(printf '%*s' "$width" '')|"
1097
-
1098
- echo -e "${GREEN}${border}${RESET}"
1099
-
1100
- # Title
1101
- local pad_title=$(( width - 2 - ${#title} ))
1102
- printf " ${GREEN}|${RESET} ${BOLD}%s${RESET}%*s ${GREEN}|${RESET}\n" "$title" "$pad_title" ""
1103
-
1104
- echo -e "${GREEN}${empty}${RESET}"
1105
-
1106
- # URL
1107
- local pad_url=$(( width - 2 - ${#url} ))
1108
- printf " ${GREEN}|${RESET} ${BOLD}${YELLOW}%s${RESET}%*s ${GREEN}|${RESET}\n" "$url" "$pad_url" ""
1109
-
1110
- echo -e "${GREEN}${empty}${RESET}"
1111
-
1112
- # Copy message
1113
- if [ -n "$copy_msg" ]; then
1114
- local pad_copy=$(( width - 2 - ${#copy_msg} ))
1115
- printf " ${GREEN}|${RESET} ${BOLD}${GREEN}%s${RESET}%*s ${GREEN}|${RESET}\n" "$copy_msg" "$pad_copy" ""
1116
- fi
1117
-
1118
- echo -e "${GREEN}${border}${RESET}"
1119
- }
1120
-
1121
- # ------------------------------------------------------------------------------
1122
- # TERMUX (ANDROID) AUTOSTART
1123
- #
1124
- # An unprivileged Android app gets no service manager, so there is no systemd
1125
- # unit, no launchd agent and no init script to install. Two Termux mechanisms
1126
- # replace all three, and configure_termux_node sets up both:
1127
- #
1128
- # 1. runit, from the termux-services package: $PREFIX/var/service/<name>/run is
1129
- # supervised inside the running Termux session, so a node that dies is
1130
- # restarted - the systemd Restart=always equivalent;
1131
- # 2. ~/.termux/boot/<name>.sh, executed by the Termux:Boot app (F-Droid) after
1132
- # the device boots: it takes the wake lock and starts the supervisor.
1133
- #
1134
- # Without termux-services the node still runs and still autostarts at boot, but
1135
- # nothing restarts it when Android kills the process. The installer says that
1136
- # plainly instead of implying supervision that is not there.
1137
- #
1138
- # configure_termux_node <service name> <python module> <extra args> <env file>
1139
- # ------------------------------------------------------------------------------
1140
- configure_termux_node() {
1141
- local name="$1" module="$2" extra="$3" env_file="$4"
1142
- local run_script="$CONFIG_DIR/${name}.sh"
1143
- local boot_script="$HOME/.termux/boot/${name}.sh"
1144
- local svdir="$TERMUX_PREFIX/var/service"
1145
- local svlogdir="$TERMUX_PREFIX/var/log"
1146
- local log_file="$CONFIG_DIR/${name}.log"
1147
- local env_block=""
1148
- local module_args="$module"
1149
- local alive=false
1150
- local waited=0
1151
-
1152
- [ -n "$extra" ] && module_args="$module $extra"
1153
-
1154
- TERMUX_SUPERVISED=false
1155
- mkdir -p "$CONFIG_DIR" "$HOME/.termux/boot"
1156
-
1157
- if [ -n "$env_file" ]; then
1158
- env_block="if [ -f \"$env_file\" ]; then
1159
- set -a
1160
- . \"$env_file\"
1161
- set +a
1162
- fi"
1163
- fi
1164
-
1165
- # The launcher is the one thing every mechanism below starts. Absolute paths
1166
- # only: runit and the boot script run it with a minimal environment in which
1167
- # $PATH may not even contain $PREFIX/bin.
1168
- cat << RUNNER_EOF > "$run_script"
1169
- #!$TERMUX_PREFIX/bin/sh
1170
- # Antigravity Mesh - $name launcher (written by install.sh; do not edit).
1171
- # Run this file to start the node by hand; docs/TERMUX.md documents the automatic
1172
- # paths (runit service, Termux:Boot) and where the logs are.
1173
- export PATH="$TERMUX_PREFIX/bin:/system/bin:\$PATH"
1174
- $env_block
1175
- cd "$SCRIPT_DIR" || exit 1
1176
- exec "$PYTHON_BIN" -m $module_args
1177
- RUNNER_EOF
1178
- chmod 700 "$run_script"
1179
-
1180
- # 1. runit supervision, when termux-services is installed. install.sh asks the
1181
- # package manager for it first; a phone that refused (no network, an old
1182
- # mirror) simply keeps the unattended launcher below.
1183
- if command -v sv >/dev/null 2>&1 && command -v runsvdir >/dev/null 2>&1; then
1184
- mkdir -p "$svdir/$name/log"
1185
- cat << SV_EOF > "$svdir/$name/run"
1186
- #!$TERMUX_PREFIX/bin/sh
1187
- exec "$run_script"
1188
- SV_EOF
1189
- chmod 755 "$svdir/$name/run"
1190
- # runit refuses to supervise without log/run, and svlogger is a script, not
1191
- # a program: the documented setup is a symlink to it. A $PREFIX that refuses
1192
- # symlinks (a FUSE-backed home, an unusual mount) gets an equivalent script
1193
- # instead, so the node is never left unsupervised for want of a link.
1194
- if ! ln -sf "$TERMUX_PREFIX/share/termux-services/svlogger" "$svdir/$name/log/run" 2>/dev/null; then
1195
- printf '#!%s/bin/sh\nexec "%s/share/termux-services/svlogger"\n' \
1196
- "$TERMUX_PREFIX" "$TERMUX_PREFIX" > "$svdir/$name/log/run"
1197
- chmod 755 "$svdir/$name/log/run"
1198
- fi
1199
- export SVDIR="$svdir"
1200
- export LOGDIR="$svlogdir"
1201
- # The supervisor is normally started by the login shell (profile.d), which
1202
- # has not necessarily run when this installer is piped straight into bash.
1203
- # Starting it here is what makes the node run *now*; the pgrep guard keeps a
1204
- # second runsvdir off an SVDIR that already has one.
1205
- if ! pgrep -f "runsvdir" >/dev/null 2>&1; then
1206
- command -v service-daemon >/dev/null 2>&1 && service-daemon start >/dev/null 2>&1 || true
1207
- fi
1208
- sv up "$name" >/dev/null 2>&1 || true
1209
- sv-enable "$name" >/dev/null 2>&1 || true
1210
- TERMUX_SUPERVISED=true
1211
- fi
1212
-
1213
- # 2. The boot entry, written even when runit supervises: after a reboot the
1214
- # supervisor itself has to be started by something, and on Android that
1215
- # something is the Termux:Boot app.
1216
- cat << BOOT_EOF > "$boot_script"
1217
- #!$TERMUX_PREFIX/bin/sh
1218
- # Antigravity Mesh - starts the $name node after the device boots (Termux:Boot).
1219
- #
1220
- # Requirements, both of them outside this file (docs/TERMUX.md):
1221
- # * the Termux:Boot app (F-Droid), opened once so Android lets it run;
1222
- # * battery optimisation OFF for Termux and for Termux:Boot.
1223
- export PATH="$TERMUX_PREFIX/bin:/system/bin:\$PATH"
1224
- export HOME="$HOME"
1225
- export SVDIR="$svdir"
1226
- export LOGDIR="$svlogdir"
1227
-
1228
- # A partial wake lock keeps Android from freezing the node while the screen is
1229
- # off. It needs the termux-api package plus the Termux:API app; without either,
1230
- # this line is a no-op rather than a failure.
1231
- command -v termux-wake-lock >/dev/null 2>&1 && termux-wake-lock >/dev/null 2>&1
1232
-
1233
- if command -v sv >/dev/null 2>&1 && [ -d "$svdir/$name" ]; then
1234
- if ! pgrep -f "runsvdir" >/dev/null 2>&1; then
1235
- command -v service-daemon >/dev/null 2>&1 && service-daemon start >/dev/null 2>&1
1236
- fi
1237
- sv up "$name" >/dev/null 2>&1
1238
- else
1239
- # No supervisor on this phone: start the launcher directly and keep its output.
1240
- "$run_script" >> "$CONFIG_DIR/$name.boot.log" 2>&1 &
1241
- fi
1242
- BOOT_EOF
1243
- chmod 700 "$boot_script"
1244
-
1245
- if [ "$TERMUX_SUPERVISED" != true ]; then
1246
- # The systemd branch's fallback, spelled for a phone: nothing supervises
1247
- # the process, so it is started detached and its log is the only evidence.
1248
- pkill -f "$module" 2>/dev/null || true
1249
- nohup "$run_script" >> "$log_file" 2>&1 &
1250
- fi
1251
-
1252
- # Termux has no `systemctl status` to consult, so the installer waits for the
1253
- # process it just started (runit may take a moment) and reports what it saw.
1254
- while [ "$waited" -lt 10 ]; do
1255
- if pgrep -f "$module" >/dev/null 2>&1; then
1256
- alive=true
1257
- break
1258
- fi
1259
- sleep 1
1260
- waited=$((waited + 1))
1261
- done
1262
-
1263
- if [ "$LANG_CHOICE" = "ru" ]; then
1264
- if [ "$TERMUX_SUPERVISED" = true ]; then
1265
- echo -e "${GREEN}[OK] runit (termux-services) следит за узлом и перезапустит его сам.${RESET}"
1266
- echo -e " Статус: ${CYAN}sv status $name${RESET} Логи: ${CYAN}$svlogdir/sv/$name/current${RESET}"
1267
- else
1268
- echo -e "${YELLOW}[!] Узел запущен без супервизора: termux-services не установлен.${RESET}"
1269
- echo -e " Автоперезапуск после сбоя: ${CYAN}pkg install termux-services${RESET}, затем повторите установку."
1270
- echo -e " Логи: ${CYAN}$log_file${RESET}"
1271
- fi
1272
- echo -e " Автозапуск после перезагрузки: ${CYAN}$boot_script${RESET} (нужно приложение Termux:Boot)."
1273
- if [ "$alive" != true ]; then
1274
- echo -e "${YELLOW}[!] Процесс узла пока не виден — смотрите лог: ${CYAN}$log_file${RESET}"
1275
- fi
1276
- else
1277
- if [ "$TERMUX_SUPERVISED" = true ]; then
1278
- echo -e "${GREEN}[OK] runit (termux-services) supervises the node and restarts it.${RESET}"
1279
- echo -e " Status: ${CYAN}sv status $name${RESET} Logs: ${CYAN}$svlogdir/sv/$name/current${RESET}"
1280
- else
1281
- echo -e "${YELLOW}[!] The node runs unsupervised: termux-services is not installed.${RESET}"
1282
- echo -e " Restart after a crash: ${CYAN}pkg install termux-services${RESET}, then re-run the installer."
1283
- echo -e " Logs: ${CYAN}$log_file${RESET}"
1284
- fi
1285
- echo -e " Autostart after a reboot: ${CYAN}$boot_script${RESET} (the Termux:Boot app is required)."
1286
- if [ "$alive" != true ]; then
1287
- echo -e "${YELLOW}[!] The node process is not visible yet - check: ${CYAN}$log_file${RESET}"
1288
- fi
1289
- fi
1290
- }
1291
-
1292
- # The Android-side switches no installer can flip for the user: whether the node
1293
- # comes back after a reboot and whether Android keeps it alive with the screen
1294
- # off. Printed after the MCP URL, in the language of the run.
1295
- print_termux_hints() {
1296
- local name="$1"
1297
- if [ "$LANG_CHOICE" = "ru" ]; then
1298
- echo -e " ${BOLD}📱 Termux (Android): что проверить на телефоне${RESET} (служба: ${CYAN}$name${RESET})"
1299
- echo -e " 1. Автозапуск после перезагрузки делает приложение ${CYAN}Termux:Boot${RESET} (F-Droid)."
1300
- echo -e " Установите и ${BOLD}запустите его один раз${RESET}, иначе скрипт в ~/.termux/boot не выполнится."
1301
- echo -e " 2. Отключите оптимизацию батареи для ${CYAN}Termux${RESET} и Termux:Boot: иначе Android"
1302
- echo -e " выгружает фоновый процесс вместе с туннелем."
1303
- echo -e " 3. Скопировать ссылку выше: долгое нажатие в терминале, либо приложение Termux:API и"
1304
- echo -e " ${CYAN}termux-clipboard-set '<ссылка>'${RESET}."
1305
- echo -e " 4. Выдайте приложению ${CYAN}Termux:API${RESET} разрешения для инструментов устройства"
1306
- echo -e " (Настройки Android → Приложения → Termux:API → Разрешения): камера, микрофон,"
1307
- echo -e " местоположение, SMS, журнал вызовов, контакты. Без них ${CYAN}device_capture${RESET} и"
1308
- echo -e " ${CYAN}device_messages${RESET} ответят, какого именно разрешения не хватает."
1309
- echo -e " 5. Ветка устройства включена по умолчанию (${CYAN}MESH_DEVICE=auto${RESET}). SMS, контакты и"
1310
- echo -e " журнал вызовов открывает ${CYAN}MESH_DEVICE_PIM=1${RESET}; ${CYAN}MESH_DEVICE_QUICK=1${RESET} оставляет"
1311
- echo -e " system_info мгновенным (без сети, камер, микрофонов и датчиков)."
1312
- echo -e " 6. Полная инструкция: ${CYAN}docs/TERMUX.md${RESET} (установка, автозапуск, логи, удаление)."
1313
- echo ""
1314
- else
1315
- echo -e " ${BOLD}📱 Termux (Android): what to check on the phone${RESET} (service: ${CYAN}$name${RESET})"
1316
- echo -e " 1. Autostart after a reboot is done by the ${CYAN}Termux:Boot${RESET} app (F-Droid)."
1317
- echo -e " Install it and ${BOLD}open it once${RESET}, or the script in ~/.termux/boot never runs."
1318
- echo -e " 2. Turn battery optimisation OFF for ${CYAN}Termux${RESET} and Termux:Boot: otherwise Android"
1319
- echo -e " unloads the background process together with the tunnel."
1320
- echo -e " 3. Copy the URL above: long-press the terminal, or install the Termux:API app and use"
1321
- echo -e " ${CYAN}termux-clipboard-set '<url>'${RESET}."
1322
- echo -e " 4. Grant the ${CYAN}Termux:API${RESET} app the permissions the device tools need"
1323
- echo -e " (Android Settings → Apps → Termux:API → Permissions): camera, microphone, location,"
1324
- echo -e " SMS, call log, contacts. Without them ${CYAN}device_capture${RESET} / ${CYAN}device_messages${RESET}"
1325
- echo -e " answer with the exact permission that is missing."
1326
- echo -e " 5. The device branch is on by default (${CYAN}MESH_DEVICE=auto${RESET}). ${CYAN}MESH_DEVICE_PIM=1${RESET}"
1327
- echo -e " enables SMS, contacts and the call log; ${CYAN}MESH_DEVICE_QUICK=1${RESET} keeps system_info"
1328
- echo -e " instant (no network, cameras, microphones or sensors)."
1329
- echo -e " 6. Full guide: ${CYAN}docs/TERMUX.md${RESET} (install, autostart, logs, uninstall)."
1330
- echo ""
1331
- fi
1332
- }
1333
-
1334
- # Dry-run
1335
- if [ "$DRY_RUN" = true ]; then
1336
- echo "[DRY-RUN] Simulating Antigravity Mesh installation..."
1337
- echo "[DRY-RUN] Language: $LANG_CHOICE"
1338
- echo "[DRY-RUN] Detected Host: $DETECTED_HOSTNAME ($DETECTED_TYPE)"
1339
- echo "[DRY-RUN] Architecture: ${ARCH_LABEL:-$DETECTED_ARCH}"
1340
- echo "[DRY-RUN] Selected Mode: ${MODE:-standalone}"
1341
- echo "[DRY-RUN] Port: $PORT"
1342
- echo "[DRY-RUN] TLS: $TLS"
1343
- if [ -n "$GATEWAY" ]; then echo "[DRY-RUN] Domain: $GATEWAY"; fi
1344
- if [ -n "$USERNAME" ]; then
1345
- echo "[DRY-RUN] User: $USERNAME"
1346
- if [ -n "$GATEWAY" ]; then
1347
- echo "[DRY-RUN] Canonical MCP URL: $(node_sse_url "$USERNAME" "${TOKEN:-<token>}")"
1348
- fi
1349
- fi
1350
- if [ -n "$TOKEN" ]; then echo "[DRY-RUN] Token: set (standalone: stored in ${CONFIG_DIR}/standalone.env, chmod 600)"; fi
1351
- if [ "$IS_TERMUX" = true ]; then
1352
- echo "[DRY-RUN] Platform: Termux (Android)"
1353
- echo "[DRY-RUN] Termux prefix: $TERMUX_PREFIX"
1354
- echo "[DRY-RUN] Package manager: pkg (sudo, systemd and uv do not exist on Android)"
1355
- echo "[DRY-RUN] Autostart: runit service <name> + ~/.termux/boot/<name>.sh (Termux:Boot)"
1356
- echo "[DRY-RUN] Domain file: $DEFAULT_DOMAIN_FILE"
1357
- else
1358
- echo "[DRY-RUN] Domain file: $DEFAULT_DOMAIN_FILE"
1359
- echo "[DRY-RUN] Systemd service and dependencies check: OK"
1360
- fi
1361
- if [ -z "$GATEWAY" ]; then
1362
- echo "[DRY-RUN] Domain file value: not written (no domain configured)"
1363
- elif [ "$GATEWAY" = "$(normalise_host "$(module_default_domain)")" ]; then
1364
- echo "[DRY-RUN] Domain file value: not written (the built-in default is a fallback, not a configuration)"
1365
- else
1366
- echo "[DRY-RUN] Domain file value: MESH_PUBLIC_URL=https://$GATEWAY"
1367
- fi
1368
- exit 0
1369
- fi
1370
-
1371
- # SSH Remote install branch
1372
- if [ -n "$SSH_TARGET" ]; then
1373
- if [ "$LANG_CHOICE" = "ru" ]; then
1374
- echo -e "${BOLD}${BLUE}=== Удаленная установка Antigravity Mesh через SSH ===${RESET}"
1375
- echo -e "Целевой сервер: ${CYAN}${SSH_TARGET}${RESET} (порт: ${SSH_PORT})"
1376
- else
1377
- echo -e "${BOLD}${BLUE}=== Remote Antigravity Mesh SSH Installation ===${RESET}"
1378
- echo -e "Target Server: ${CYAN}${SSH_TARGET}${RESET} (port: ${SSH_PORT})"
1379
- fi
1380
-
1381
- SSH_BIN="ssh -o StrictHostKeyChecking=no"
1382
- SCP_BIN="scp -o StrictHostKeyChecking=no"
1383
- if [ -n "$SSHPASS" ] && command -v sshpass >/dev/null 2>&1; then
1384
- SSH_BIN="sshpass -e $SSH_BIN"
1385
- SCP_BIN="sshpass -e $SCP_BIN"
1386
- fi
1387
-
1388
- $SSH_BIN -p "$SSH_PORT" "$SSH_TARGET" "mkdir -p ~/antigravity-mesh/core ~/antigravity-mesh/skills"
1389
- $SCP_BIN -P "$SSH_PORT" -r "$SCRIPT_DIR/core"/* "$SSH_TARGET:~/antigravity-mesh/core/"
1390
- $SCP_BIN -P "$SSH_PORT" "$SCRIPT_DIR/install.sh" "$SSH_TARGET:~/antigravity-mesh/"
1391
- if [ -d "$SCRIPT_DIR/skills" ]; then
1392
- $SCP_BIN -P "$SSH_PORT" -r "$SCRIPT_DIR/skills"/* "$SSH_TARGET:~/antigravity-mesh/skills/" 2>/dev/null || true
1393
- fi
1394
- # The domain was resolved on THIS machine, so it is handed to the target: the
1395
- # remote copy otherwise re-resolves from its own environment, and a repository
1396
- # copy whose $PUBLISHED_DOMAIN is still the placeholder falls back to the
1397
- # built-in default - which the target would now also record as its domain.
1398
- REMOTE_DOMAIN_ARG=""
1399
- if [ -n "$GATEWAY" ] && [ "$GATEWAY" != "$DOMAIN_PLACEHOLDER" ]; then
1400
- REMOTE_DOMAIN_ARG=" --domain=$GATEWAY"
1401
- fi
1402
- $SSH_BIN -p "$SSH_PORT" "$SSH_TARGET" "cd ~/antigravity-mesh && bash install.sh --quick --lang=$LANG_CHOICE$REMOTE_DOMAIN_ARG"
1403
- exit 0
1404
- fi
1405
-
1406
- # Interactive Menu if no mode is specified and not in quick mode
1407
- if [ "$QUICK" = false ] && [ -z "$MODE" ] && [ -t 0 ]; then
1408
- print_device_info
1409
- echo ""
1410
- if [ "$LANG_CHOICE" = "ru" ]; then
1411
- echo -e "${BOLD}Выберите режим установки:${RESET}"
1412
- echo -e " ${GREEN}1)${RESET} ⚡ ${BOLD}Быстрая настройка${RESET} (Имя узла как имя ПК + Общий домен + Автозапуск) [Рекомендуется]"
1413
- echo -e " ${BLUE}2)${RESET} 🖥️ ${BOLD}Локальный Standalone${RESET} (Только localhost:${PORT}, без облачного шлюза)"
1414
- echo -e " ${YELLOW}3)${RESET} ⚙️ ${BOLD}Кастомная настройка${RESET} (Ввести имя узла вручную, выбор общего домена)"
1415
- echo -e " ${CYAN}4)${RESET} 📡 ${BOLD}Удаленная установка на SSH-сервер${RESET}"
1416
- echo -e " ${RESET}0) Выход"
1417
- echo ""
1418
- read -rp "Ваш выбор [1]: " MENU_CHOICE
1419
- else
1420
- echo -e "${BOLD}Select Installation Mode:${RESET}"
1421
- echo -e " ${GREEN}1)${RESET} ⚡ ${BOLD}Quick Setup${RESET} (Node name from PC name + shared domain + Autostart) [Recommended]"
1422
- echo -e " ${BLUE}2)${RESET} 🖥️ ${BOLD}Local Standalone${RESET} (localhost:${PORT} only, no cloud gateway)"
1423
- echo -e " ${YELLOW}3)${RESET} ⚙️ ${BOLD}Custom Setup${RESET} (Custom node name, custom shared domain)"
1424
- echo -e " ${CYAN}4)${RESET} 📡 ${BOLD}Remote SSH Installation${RESET}"
1425
- echo -e " ${RESET}0) Exit"
1426
- echo ""
1427
- read -rp "Select [1]: " MENU_CHOICE
1428
- fi
1429
- MENU_CHOICE=${MENU_CHOICE:-1}
1430
-
1431
- case "$MENU_CHOICE" in
1432
- 1)
1433
- MODE="tunnel"
1434
- QUICK=true
1435
- ;;
1436
- 2)
1437
- MODE="standalone"
1438
- ;;
1439
- 3)
1440
- MODE="tunnel"
1441
- echo ""
1442
- if [ "$LANG_CHOICE" = "ru" ]; then
1443
- read -rp "Введите имя узла [по умолчанию: ${DETECTED_HOSTNAME}]: " CUSTOM_SUB
1444
- read -rp "Общий домен шлюза [по умолчанию: ${GATEWAY}]: " CUSTOM_GW
1445
- else
1446
- read -rp "Enter node name [default: ${DETECTED_HOSTNAME}]: " CUSTOM_SUB
1447
- read -rp "Shared gateway domain [default: ${GATEWAY}]: " CUSTOM_GW
1448
- fi
1449
- if [ -n "$CUSTOM_SUB" ]; then USERNAME="$CUSTOM_SUB"; fi
1450
- if [ -n "$CUSTOM_GW" ]; then GATEWAY="$CUSTOM_GW"; fi
1451
- ;;
1452
- 4)
1453
- echo ""
1454
- if [ "$LANG_CHOICE" = "ru" ]; then
1455
- read -rp "Введите SSH цель (например, user@<host-ip>): " REMOTE_TARGET
1456
- read -rp "Порт SSH [22]: " REMOTE_PORT
1457
- else
1458
- read -rp "Enter SSH target (e.g., user@<host-ip>): " REMOTE_TARGET
1459
- read -rp "SSH Port [22]: " REMOTE_PORT
1460
- fi
1461
- REMOTE_PORT=${REMOTE_PORT:-22}
1462
- exec "$0" "--ssh=${REMOTE_TARGET}:${REMOTE_PORT}" "--lang=${LANG_CHOICE}"
1463
- ;;
1464
- 0)
1465
- echo "Cancelled."
1466
- exit 0
1467
- ;;
1468
- *)
1469
- MODE="tunnel"
1470
- QUICK=true
1471
- ;;
1472
- esac
1473
- fi
1474
-
1475
- if [ -z "$MODE" ]; then
1476
- MODE="tunnel"
1477
- fi
1478
-
1479
- print_device_info
1480
-
1481
- # ==============================================================================
1482
- # BRANCH: STANDALONE MODE
1483
- # ==============================================================================
1484
- if [ "$MODE" = "standalone" ]; then
1485
- if [ "$LANG_CHOICE" = "ru" ]; then
1486
- echo -e "\n${BOLD}${BLUE}=== Настройка локального Standalone FastMCP сервера ===${RESET}"
1487
- echo "[1/3] Проверка окружения Python..."
1488
- else
1489
- echo -e "\n${BOLD}${BLUE}=== Setting up Local Standalone FastMCP Server ===${RESET}"
1490
- echo "[1/3] Checking Python environment..."
1491
- fi
1492
- ensure_dependencies
1493
- # The endpoint check at the end of this branch runs through curl.
1494
- ensure_curl || exit 1
1495
- mkdir -p "$CONFIG_DIR"
1496
- # A standalone node answers share_file too, so the domain it resolved is written
1497
- # down here as well - before the server is started, not after.
1498
- write_domain_file "$GATEWAY"
1499
-
1500
- # Honour --token in standalone mode without embedding the secret in the
1501
- # (world-readable) unit/plist: keep it in a 0600 env file for systemd
1502
- # (EnvironmentFile) and use a 0600 plist environment block on macOS.
1503
- # core.server reads MESH_TOKEN as the default for --token.
1504
- STANDALONE_ENV_FILE="$CONFIG_DIR/standalone.env"
1505
- ENV_FILE_LINE=""
1506
- PLIST_ENV_BLOCK=""
1507
- if [ -n "$TOKEN" ]; then
1508
- printf 'MESH_TOKEN=%s\n' "$TOKEN" > "$STANDALONE_ENV_FILE"
1509
- chmod 600 "$STANDALONE_ENV_FILE"
1510
- ENV_FILE_LINE="EnvironmentFile=$STANDALONE_ENV_FILE"
1511
- PLIST_TOKEN=$(printf '%s' "$TOKEN" | sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g')
1512
- PLIST_ENV_BLOCK=" <key>EnvironmentVariables</key>
1513
- <dict>
1514
- <key>MESH_TOKEN</key>
1515
- <string>${PLIST_TOKEN}</string>
1516
- </dict>"
1517
- if [ "$LANG_CHOICE" = "ru" ]; then
1518
- echo -e "${GREEN}[✓] Токен авторизации включён (chmod 600).${RESET}"
1519
- else
1520
- echo -e "${GREEN}[OK] Authorization token enabled (chmod 600).${RESET}"
1521
- fi
1522
- else
1523
- rm -f "$STANDALONE_ENV_FILE" 2>/dev/null || true
1524
- if [ "$LANG_CHOICE" = "ru" ]; then
1525
- echo -e "${YELLOW}[!] Токен не задан (--token): сервер доступен без авторизации на 127.0.0.1.${RESET}"
1526
- else
1527
- echo -e "${YELLOW}[!] No --token given: server is unauthenticated on 127.0.0.1.${RESET}"
1528
- fi
1529
- fi
1530
-
1531
- if [ "$LANG_CHOICE" = "ru" ]; then
1532
- if [ "$IS_TERMUX" = true ]; then
1533
- echo "[2/3] Настройка автозапуска на телефоне (runit + Termux:Boot)..."
1534
- else
1535
- echo "[2/3] Настройка systemd автозапуска на ПК..."
1536
- fi
1537
- else
1538
- echo "[2/3] Configuring autostart background service..."
1539
- fi
1540
-
1541
- if [ "$(uname -s)" = "Darwin" ]; then
1542
- LAUNCH_AGENTS="$HOME/Library/LaunchAgents"
1543
- mkdir -p "$LAUNCH_AGENTS"
1544
- PLIST_FILE="$LAUNCH_AGENTS/com.antigravity.mesh.standalone.plist"
1545
- cat << EOF > "$PLIST_FILE"
1546
- <?xml version="1.0" encoding="UTF-8"?>
1547
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1548
- <plist version="1.0">
1549
- <dict>
1550
- <key>Label</key>
1551
- <string>com.antigravity.mesh.standalone</string>
1552
- <key>ProgramArguments</key>
1553
- <array>
1554
- <string>$PYTHON_BIN</string>
1555
- <string>-m</string>
1556
- <string>core.server</string>
1557
- <string>--host=127.0.0.1</string>
1558
- <string>--port=$PORT</string>
1559
- </array>
1560
- <key>WorkingDirectory</key>
1561
- <string>$SCRIPT_DIR</string>
1562
- <key>RunAtLoad</key>
1563
- <true/>
1564
- <key>KeepAlive</key>
1565
- <true/>
1566
- $PLIST_ENV_BLOCK
1567
- </dict>
1568
- </plist>
1569
- EOF
1570
- if [ -n "$TOKEN" ]; then
1571
- chmod 600 "$PLIST_FILE"
1572
- fi
1573
- launchctl unload "$PLIST_FILE" 2>/dev/null || true
1574
- launchctl load -w "$PLIST_FILE" 2>/dev/null || true
1575
- elif [ "$IS_TERMUX" = true ]; then
1576
- # Android: neither systemd nor launchd exists - runit inside Termux
1577
- # supervises the server, and Termux:Boot brings it back after a reboot.
1578
- configure_termux_node "agy-standalone" "core.server" "--host 127.0.0.1 --port=$PORT" "$STANDALONE_ENV_FILE"
1579
- else
1580
- USER_SYSTEMD_DIR="$HOME/.config/systemd/user"
1581
- mkdir -p "$USER_SYSTEMD_DIR"
1582
- SERVICE_FILE="$USER_SYSTEMD_DIR/agy-standalone.service"
1583
- cat << EOF > "$SERVICE_FILE"
1584
- [Unit]
1585
- Description=Antigravity Mesh Local Standalone MCP Server
1586
- After=network.target
1587
-
1588
- [Service]
1589
- Type=simple
1590
- $ENV_FILE_LINE
1591
- WorkingDirectory=$SCRIPT_DIR
1592
- ExecStart=$PYTHON_BIN -m core.server --host 127.0.0.1 --port=$PORT
1593
- Restart=always
1594
- RestartSec=5
1595
-
1596
- [Install]
1597
- WantedBy=default.target
1598
- EOF
1599
- systemctl --user daemon-reload 2>/dev/null || true
1600
- systemctl --user enable --now agy-standalone.service 2>/dev/null || {
1601
- pkill -f "core.server" 2>/dev/null || true
1602
- if [ -n "$TOKEN" ]; then
1603
- nohup env MESH_TOKEN="$TOKEN" $PYTHON_BIN -m core.server --host 127.0.0.1 --port="$PORT" > "$CONFIG_DIR/standalone.log" 2>&1 &
1604
- else
1605
- nohup $PYTHON_BIN -m core.server --host 127.0.0.1 --port="$PORT" > "$CONFIG_DIR/standalone.log" 2>&1 &
1606
- fi
1607
- }
1608
- loginctl enable-linger "$USER" 2>/dev/null || true
1609
- fi
1610
-
1611
- if [ "$LANG_CHOICE" = "ru" ]; then
1612
- echo "[3/3] Проверка локального эндпоинта..."
1613
- else
1614
- echo "[3/3] Verifying local endpoint..."
1615
- fi
1616
- HEALTH_URL="http://127.0.0.1:${PORT}/health"
1617
- HEALTH_OK=false
1618
- for _attempt in 1 2 3 4 5 6 7 8 9 10; do
1619
- HTTP_CODE=$(curl -s -o /dev/null -m 2 -w "%{http_code}" "$HEALTH_URL" 2>/dev/null || true)
1620
- if [ "$HTTP_CODE" = "200" ]; then
1621
- HEALTH_OK=true
1622
- break
1623
- fi
1624
- sleep 1
1625
- done
1626
- if [ "$HEALTH_OK" != true ]; then
1627
- if [ "$LANG_CHOICE" = "ru" ]; then
1628
- echo -e "\033[1;31m[ОШИБКА] Standalone MCP-сервер не отвечает (HTTP ${HTTP_CODE:-нет ответа}): $HEALTH_URL\033[0m" >&2
1629
- if [ "$IS_TERMUX" = true ]; then
1630
- echo -e " Статус: ${CYAN}sv status agy-standalone${RESET} (runit, termux-services)" >&2
1631
- echo -e " Логи: ${CYAN}$TERMUX_PREFIX/var/log/sv/agy-standalone/current${RESET}" >&2
1632
- echo -e " Локальный сервер на телефоне доступен только внутри Termux (127.0.0.1):" >&2
1633
- echo -e " для Gemini с телефона используйте режим туннеля (--mode=tunnel)." >&2
1634
- else
1635
- echo -e " Проверьте статус: ${CYAN}systemctl --user status agy-standalone.service${RESET}" >&2
1636
- echo -e " Логи: ${CYAN}journalctl --user -u agy-standalone.service -n 50 --no-pager${RESET}" >&2
1637
- fi
1638
- else
1639
- echo -e "\033[1;31m[ERROR] Standalone MCP server is not responding (HTTP ${HTTP_CODE:-no response}): $HEALTH_URL\033[0m" >&2
1640
- if [ "$IS_TERMUX" = true ]; then
1641
- echo -e " Status: ${CYAN}sv status agy-standalone${RESET} (runit, termux-services)" >&2
1642
- echo -e " Logs: ${CYAN}$TERMUX_PREFIX/var/log/sv/agy-standalone/current${RESET}" >&2
1643
- echo -e " A local server on a phone is reachable inside Termux only (127.0.0.1):" >&2
1644
- echo -e " use --mode=tunnel to drive this phone from Gemini." >&2
1645
- else
1646
- echo -e " Check status: ${CYAN}systemctl --user status agy-standalone.service${RESET}" >&2
1647
- echo -e " Logs: ${CYAN}journalctl --user -u agy-standalone.service -n 50 --no-pager${RESET}" >&2
1648
- fi
1649
- fi
1650
- exit 1
1651
- fi
1652
-
1653
- MCP_LOCAL_URL="http://localhost:${PORT}/sse"
1654
-
1655
- # Quick Copy to Clipboard
1656
- COPIED=false
1657
- B64_URL=$(printf "%s" "$MCP_LOCAL_URL" | base64 | tr -d '\r\n')
1658
- printf "\033]52;c;%s\a" "$B64_URL" 2>/dev/null || true
1659
- if command -v termux-clipboard-set >/dev/null 2>&1; then
1660
- # Termux on a phone: termux-api plus the Termux:API app put the URL on
1661
- # Android's clipboard, where the Gemini app can paste it. Without the app
1662
- # the command exists but fails, so COPIED stays false and the hint below
1663
- # tells the user to copy by hand.
1664
- printf "%s" "$MCP_LOCAL_URL" | termux-clipboard-set 2>/dev/null && COPIED=true
1665
- elif command -v wl-copy >/dev/null 2>&1; then
1666
- printf "%s" "$MCP_LOCAL_URL" | wl-copy 2>/dev/null && COPIED=true
1667
- elif command -v xclip >/dev/null 2>&1; then
1668
- printf "%s" "$MCP_LOCAL_URL" | xclip -selection clipboard 2>/dev/null && COPIED=true
1669
- elif command -v xsel >/dev/null 2>&1; then
1670
- printf "%s" "$MCP_LOCAL_URL" | xsel --clipboard --input 2>/dev/null && COPIED=true
1671
- elif command -v pbcopy >/dev/null 2>&1; then
1672
- printf "%s" "$MCP_LOCAL_URL" | pbcopy 2>/dev/null && COPIED=true
1673
- elif command -v clip.exe >/dev/null 2>&1; then
1674
- printf "%s" "$MCP_LOCAL_URL" | clip.exe 2>/dev/null && COPIED=true
1675
- fi
1676
-
1677
- clear 2>/dev/null || printf "\033[2J\033[H" || true
1678
-
1679
- if [ "$LANG_CHOICE" = "ru" ]; then
1680
- echo ""
1681
- copy_msg=""
1682
- if [ "$COPIED" = true ]; then
1683
- copy_msg="[OK] ССЫЛКА СКОПИРОВАНА В БУФЕР ОБМЕНА! (Вставьте через Ctrl+V)"
1684
- else
1685
- copy_msg="Скопируйте ссылку выше"
1686
- fi
1687
- print_mcp_box "ССЫЛКА ЛОКАЛЬНОГО MCP-СЕРВЕРА (СКОПИРУЙТЕ):" "$MCP_LOCAL_URL" "$copy_msg"
1688
- echo ""
1689
- echo -e " ${BOLD}Куда вставлять ссылку в Google Gemini:${RESET}"
1690
- echo -e " 1. Откройте в браузере: ${CYAN}https://gemini.google.com/spark/apps${RESET}"
1691
- echo -e " 2. Нажмите ${BOLD}'Добавить приложение'${RESET} (Add app / Настройки MCP)"
1692
- echo -e " 3. Вставьте скопированную ссылку в поле ${BOLD}'URL сервера'${RESET} (${BOLD}Ctrl+V${RESET}) и нажмите ${BOLD}Подключить${RESET}."
1693
- echo ""
1694
- else
1695
- echo ""
1696
- copy_msg=""
1697
- if [ "$COPIED" = true ]; then
1698
- copy_msg="[OK] URL COPIED TO CLIPBOARD! (Press Ctrl+V to paste)"
1699
- else
1700
- copy_msg="Copy the URL above"
1701
- fi
1702
- print_mcp_box "LOCAL MCP SERVER URL (COPY THIS):" "$MCP_LOCAL_URL" "$copy_msg"
1703
- echo ""
1704
- echo -e " ${BOLD}Where to paste this URL in Google Gemini:${RESET}"
1705
- echo -e " 1. Open in your browser: ${CYAN}https://gemini.google.com/spark/apps${RESET}"
1706
- echo -e " 2. Click ${BOLD}'Add app'${RESET} (or navigate to MCP settings)"
1707
- echo -e " 3. Paste the URL into the ${BOLD}'Server URL'${RESET} field (${BOLD}Ctrl+V${RESET}) and click ${BOLD}Connect${RESET}."
1708
- echo ""
1709
- fi
1710
- if [ "$IS_TERMUX" = true ]; then
1711
- print_termux_hints "agy-standalone"
1712
- fi
1713
- exit 0
1714
- fi
1715
-
1716
- # ==============================================================================
1717
- # BRANCH: CLOUD GATEWAY + REVERSE TUNNEL
1718
- # ==============================================================================
1719
- if [ "$LANG_CHOICE" = "ru" ]; then
1720
- echo -e "\n${BOLD}${MAGENTA}=== Настройка облачного туннеля (общий домен) ===${RESET}"
1721
- else
1722
- echo -e "\n${BOLD}${MAGENTA}=== Configuring Cloud Gateway Tunnel (shared domain) ===${RESET}"
1723
- fi
1724
-
1725
- if [ -z "$USERNAME" ]; then
1726
- CLEAN_HOST=$(echo "$DETECTED_HOSTNAME" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_-')
1727
- USERNAME="$CLEAN_HOST"
1728
- fi
1729
- USERNAME=$(echo "$USERNAME" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_-')
1730
- [ -z "$USERNAME" ] && USERNAME="mesh-node"
1731
-
1732
- mkdir -p "$CONFIG_DIR"
1733
-
1734
- if [ "$LANG_CHOICE" = "ru" ]; then
1735
- echo "[1/4] Проверка Python зависимостей (websockets)..."
1736
- else
1737
- echo "[1/4] Checking Python dependencies (websockets)..."
1738
- fi
1739
-
1740
- ensure_dependencies
1741
- # Registration - the step right after this - is a curl POST. A machine with a usable
1742
- # Python and no curl (a minimal WSL image, a slim container) used to die there with
1743
- # "curl: command not found" and no hint about what to install.
1744
- ensure_curl || exit 1
1745
- # Registration is also the first step that truly needs the gateway: a name that does
1746
- # not resolve here would be reported a moment later as "no authentication token",
1747
- # which blames the gateway's answer instead of this device's DNS.
1748
- require_gateway_dns || exit 1
1749
-
1750
- if [ "$LANG_CHOICE" = "ru" ]; then
1751
- echo "[2/4] Регистрация узла '${USERNAME}' на общем домене (${GATEWAY})..."
1752
- else
1753
- echo "[2/4] Registering node '${USERNAME}' on the shared domain (${GATEWAY})..."
1754
- fi
1755
-
1756
- EXISTING_TOKEN=""
1757
- if [ -f "$CONFIG_FILE" ]; then
1758
- CFG_USER=$(grep "MESH_USER" "$CONFIG_FILE" 2>/dev/null | cut -d '=' -f2 || true)
1759
- CFG_TOKEN=$(grep "MESH_TOKEN" "$CONFIG_FILE" 2>/dev/null | cut -d '=' -f2 || true)
1760
- if [ "$CFG_USER" = "$USERNAME" ] && [ -n "$CFG_TOKEN" ]; then
1761
- EXISTING_TOKEN="$CFG_TOKEN"
1762
- fi
1763
- fi
1764
-
1765
- if [ -n "$TOKEN" ]; then
1766
- REQ_TOKEN="$TOKEN"
1767
- elif [ -n "$EXISTING_TOKEN" ]; then
1768
- REQ_TOKEN="$EXISTING_TOKEN"
1769
- else
1770
- REQ_TOKEN=""
1771
- fi
1772
-
1773
- DETECTED_MAC=""
1774
- # "$PYTHON_BIN", never a bare python3: after the venv or uv fallback the working
1775
- # interpreter is not on PATH, and the bare name would silently produce no MAC.
1776
- if python_works "$PYTHON_BIN"; then
1777
- 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)
1778
- fi
1779
- if [ -z "$DETECTED_MAC" ] || [ "$DETECTED_MAC" = "00:00:00:00:00:00" ]; then
1780
- for iface in /sys/class/net/*; do
1781
- if [ -f "$iface/address" ] && [ "$(cat "$iface/type" 2>/dev/null)" = "1" ]; then
1782
- addr=$(cat "$iface/address" 2>/dev/null)
1783
- if [ -n "$addr" ] && [ "$addr" != "00:00:00:00:00:00" ]; then
1784
- DETECTED_MAC="$addr"
1785
- break
1786
- fi
1787
- fi
1788
- done
1789
- fi
1790
-
1791
- # One field out of the registration answer. The JSON arrives on stdin rather than
1792
- # in argv (a token or an error message can contain quotes), and "$PYTHON_BIN" is
1793
- # used because after the venv or uv fallback there is no python3 on PATH - a bare
1794
- # name would return an empty token, which the check below would then blame on the
1795
- # gateway.
1796
- json_field() {
1797
- # tr strips the CR that a Windows python.exe appends under Git Bash/MSYS.
1798
- # install.sh explicitly supports MINGW/MSYS/CYGWIN (see DEFAULT_DOMAIN_DIR), and
1799
- # a trailing CR inside the token would be pasted straight into the wss:// URL.
1800
- 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
1801
- }
1802
-
1803
- REG_BODY="{\"username\": \"${USERNAME}\", \"auto_suffix\": true, \"mac_address\": \"${DETECTED_MAC}\", \"os\": \"${DETECTED_OS}\", \"arch\": \"${DETECTED_ARCH}\""
1804
- if [ -n "$REQ_TOKEN" ]; then
1805
- REG_BODY="${REG_BODY}, \"token\": \"${REQ_TOKEN}\""
1806
- fi
1807
- REG_BODY="${REG_BODY}}"
1808
-
1809
- REG_RESP=$(curl -s -X POST "https://${GATEWAY}/api/register" \
1810
- -H "Content-Type: application/json" \
1811
- -d "$REG_BODY")
1812
-
1813
- ASSIGNED_USER=$(json_field "$REG_RESP" username)
1814
- ASSIGNED_TOKEN=$(json_field "$REG_RESP" token)
1815
-
1816
- if [ -z "$ASSIGNED_TOKEN" ]; then
1817
- FALLBACK_USER="${USERNAME}-$(date +%s | tail -c 4)"
1818
- REG_RESP=$(curl -s -X POST "https://${GATEWAY}/api/register" \
1819
- -H "Content-Type: application/json" \
1820
- -d "{\"username\": \"${FALLBACK_USER}\", \"auto_suffix\": true, \"mac_address\": \"${DETECTED_MAC}\", \"os\": \"${DETECTED_OS}\", \"arch\": \"${DETECTED_ARCH}\"}")
1821
- ASSIGNED_USER=$(json_field "$REG_RESP" username)
1822
- ASSIGNED_TOKEN=$(json_field "$REG_RESP" token)
1823
- fi
1824
-
1825
- if [ -z "$ASSIGNED_TOKEN" ]; then
1826
- echo -e "${RED}[ERROR] Failed to obtain authentication token from gateway.${RESET}"
1827
- exit 1
1828
- fi
1829
-
1830
- if [ "$ASSIGNED_USER" != "$USERNAME" ]; then
1831
- if [ "$LANG_CHOICE" = "ru" ]; then
1832
- echo -e "${YELLOW}ℹ️ Имя '${USERNAME}' уже было занято. Автоматически назначено имя узла:${RESET} ${BOLD}${GREEN}${ASSIGNED_USER}${RESET}"
1833
- else
1834
- echo -e "${YELLOW}ℹ️ Name '${USERNAME}' was already taken. Automatically assigned node name:${RESET} ${BOLD}${GREEN}${ASSIGNED_USER}${RESET}"
1835
- fi
1836
- fi
1837
-
1838
- if [ "$LANG_CHOICE" = "ru" ]; then
1839
- echo "[3/4] Сохранение конфигурации в ${CONFIG_FILE}..."
1840
- else
1841
- echo "[3/4] Saving configuration to ${CONFIG_FILE}..."
1842
- fi
1843
-
1844
- cat << EOF > "$CONFIG_FILE"
1845
- MESH_GATEWAY=${GATEWAY}
1846
- MESH_USER=${ASSIGNED_USER}
1847
- MESH_TOKEN=${ASSIGNED_TOKEN}
1848
- EOF
1849
- chmod 600 "$CONFIG_FILE"
1850
-
1851
- # agent.env carries only the legacy MESH_GATEWAY, which core/domain.py honours for
1852
- # the tunnel but never for the links a node publishes. Writing the domain down is
1853
- # what keeps share links on the gateway this node is registered on.
1854
- write_domain_file "$GATEWAY"
1855
-
1856
- if [ "$LANG_CHOICE" = "ru" ]; then
1857
- if [ "$IS_TERMUX" = true ]; then
1858
- echo "[4/4] Настройка и запуск автозапуска на телефоне (runit + Termux:Boot)..."
1859
- else
1860
- echo "[4/4] Настройка и запуск службы автозапуска на ПК..."
1861
- fi
1862
- else
1863
- echo "[4/4] Configuring and starting background autostart service..."
1864
- fi
1865
-
1866
- if [ "$(uname -s)" = "Darwin" ]; then
1867
- LAUNCH_AGENTS="$HOME/Library/LaunchAgents"
1868
- mkdir -p "$LAUNCH_AGENTS"
1869
- PLIST_FILE="$LAUNCH_AGENTS/com.antigravity.mesh.agent.plist"
1870
- cat << EOF > "$PLIST_FILE"
1871
- <?xml version="1.0" encoding="UTF-8"?>
1872
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1873
- <plist version="1.0">
1874
- <dict>
1875
- <key>Label</key>
1876
- <string>com.antigravity.mesh.agent</string>
1877
- <key>ProgramArguments</key>
1878
- <array>
1879
- <string>$PYTHON_BIN</string>
1880
- <string>-m</string>
1881
- <string>core.agent</string>
1882
- </array>
1883
- <key>WorkingDirectory</key>
1884
- <string>$SCRIPT_DIR</string>
1885
- <key>EnvironmentVariables</key>
1886
- <dict>
1887
- <key>MESH_GATEWAY</key>
1888
- <string>$GATEWAY</string>
1889
- <key>MESH_USER</key>
1890
- <string>$ASSIGNED_USER</string>
1891
- <key>MESH_TOKEN</key>
1892
- <string>$ASSIGNED_TOKEN</string>
1893
- </dict>
1894
- <key>RunAtLoad</key>
1895
- <true/>
1896
- <key>KeepAlive</key>
1897
- <true/>
1898
- </dict>
1899
- </plist>
1900
- EOF
1901
- launchctl unload "$PLIST_FILE" 2>/dev/null || true
1902
- launchctl load -w "$PLIST_FILE" 2>/dev/null || true
1903
- elif [ "$IS_TERMUX" = true ]; then
1904
- # Android: the tunnel node is supervised by runit inside Termux, and the
1905
- # Termux:Boot entry brings it back after a reboot - see configure_termux_node.
1906
- configure_termux_node "agy-agent" "core.agent" "" "$CONFIG_FILE"
1907
- else
1908
- USER_SYSTEMD_DIR="$HOME/.config/systemd/user"
1909
- mkdir -p "$USER_SYSTEMD_DIR"
1910
- cat << EOF > "$USER_SYSTEMD_DIR/agy-agent.service"
1911
- [Unit]
1912
- Description=Antigravity Mesh Reverse RPC Agent
1913
- After=network.target
1914
-
1915
- [Service]
1916
- Type=simple
1917
- EnvironmentFile=$CONFIG_FILE
1918
- WorkingDirectory=$SCRIPT_DIR
1919
- ExecStart=$PYTHON_BIN -m core.agent
1920
- Restart=always
1921
- RestartSec=5
1922
-
1923
- [Install]
1924
- WantedBy=default.target
1925
- EOF
1926
- systemctl --user daemon-reload 2>/dev/null || true
1927
- systemctl --user enable --now agy-agent.service 2>/dev/null || {
1928
- pkill -f "core.agent" 2>/dev/null || true
1929
- nohup $PYTHON_BIN -m core.agent > "$CONFIG_DIR/agent.log" 2>&1 &
1930
- }
1931
- loginctl enable-linger "$USER" 2>/dev/null || true
1932
- fi
1933
-
1934
- sleep 1
1935
-
1936
- # Canonical shared-domain MCP URL (one domain for every node; ?user= selects it).
1937
- MCP_URL="$(node_sse_url "$ASSIGNED_USER" "$ASSIGNED_TOKEN")"
1938
-
1939
- # Quick Copy to Clipboard
1940
- COPIED=false
1941
- B64_URL=$(printf "%s" "$MCP_URL" | base64 | tr -d '\r\n')
1942
- printf "\033]52;c;%s\a" "$B64_URL" 2>/dev/null || true
1943
- if command -v termux-clipboard-set >/dev/null 2>&1; then
1944
- # Termux on a phone: termux-api plus the Termux:API app put the URL on
1945
- # Android's clipboard, where the Gemini app can paste it. Without the app the
1946
- # command exists but fails, so COPIED stays false and the hint says so.
1947
- printf "%s" "$MCP_URL" | termux-clipboard-set 2>/dev/null && COPIED=true
1948
- elif command -v wl-copy >/dev/null 2>&1; then
1949
- printf "%s" "$MCP_URL" | wl-copy 2>/dev/null && COPIED=true
1950
- elif command -v xclip >/dev/null 2>&1; then
1951
- printf "%s" "$MCP_URL" | xclip -selection clipboard 2>/dev/null && COPIED=true
1952
- elif command -v xsel >/dev/null 2>&1; then
1953
- printf "%s" "$MCP_URL" | xsel --clipboard --input 2>/dev/null && COPIED=true
1954
- elif command -v pbcopy >/dev/null 2>&1; then
1955
- printf "%s" "$MCP_URL" | pbcopy 2>/dev/null && COPIED=true
1956
- elif command -v clip.exe >/dev/null 2>&1; then
1957
- printf "%s" "$MCP_URL" | clip.exe 2>/dev/null && COPIED=true
1958
- fi
1959
-
1960
- clear 2>/dev/null || printf "\033[2J\033[H" || true
1961
-
1962
- if [ "$LANG_CHOICE" = "ru" ]; then
1963
- echo ""
1964
- copy_msg=""
1965
- if [ "$COPIED" = true ]; then
1966
- copy_msg="[OK] ССЫЛКА СКОПИРОВАНА В БУФЕР ОБМЕНА! (Вставьте через Ctrl+V)"
1967
- else
1968
- copy_msg="Скопируйте ссылку выше"
1969
- fi
1970
- print_mcp_box "ССЫЛКА MCP-СЕРВЕРА ДЛЯ ПОДКЛЮЧЕНИЯ (СКОПИРУЙТЕ):" "$MCP_URL" "$copy_msg"
1971
- echo ""
1972
- echo -e " ${BOLD}Куда вставлять ссылку в Google Gemini:${RESET}"
1973
- echo -e " 1. Откройте в браузере: ${CYAN}https://gemini.google.com/spark/apps${RESET}"
1974
- echo -e " 2. Нажмите ${BOLD}'Добавить приложение'${RESET} (Add app / Настройки MCP)"
1975
- echo -e " 3. Вставьте скопированную ссылку в поле ${BOLD}'URL сервера'${RESET} (${BOLD}Ctrl+V${RESET}) и нажмите ${BOLD}Подключить${RESET}."
1976
- echo ""
1977
- else
1978
- echo ""
1979
- copy_msg=""
1980
- if [ "$COPIED" = true ]; then
1981
- copy_msg="[OK] URL COPIED TO CLIPBOARD! (Paste with Ctrl+V)"
1982
- else
1983
- copy_msg="Copy the URL above"
1984
- fi
1985
- print_mcp_box "MCP SERVER CONNECTION URL (COPY THIS):" "$MCP_URL" "$copy_msg"
1986
- echo ""
1987
- echo -e " ${BOLD}Where to paste this URL in Google Gemini:${RESET}"
1988
- echo -e " 1. Open in your browser: ${CYAN}https://gemini.google.com/spark/apps${RESET}"
1989
- echo -e " 2. Click ${BOLD}'Add app'${RESET} (or navigate to MCP settings)"
1990
- echo -e " 3. Paste the URL into the ${BOLD}'Server URL'${RESET} field (${BOLD}Ctrl+V${RESET}) and click ${BOLD}Connect${RESET}."
1991
- echo ""
1992
- fi
1993
-
1994
- # The phone-specific steps (Termux:Boot, battery optimisation, clipboard) are the
1995
- # same in both modes; on a Linux/macOS/Windows node there is nothing to add.
1996
- if [ "$IS_TERMUX" = true ]; then
1997
- print_termux_hints "agy-agent"
1998
- fi