gemini-computer-use 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/install.ps1 DELETED
@@ -1,1287 +0,0 @@
1
- # ==============================================================================
2
- # Antigravity Mesh - Windows PowerShell Universal Installer
3
- # ==============================================================================
4
- param(
5
- [switch]$Quick,
6
- [string]$Mode = "tunnel",
7
- [string]$User = "",
8
- [string]$Token = "",
9
- # Empty on purpose: the shared domain is resolved from the one source of truth
10
- # (see the PUBLIC DOMAIN block below), never from a literal in this file.
11
- [string]$Gateway = "",
12
- [int]$Port = 8096,
13
- [string]$Lang = "en",
14
- [switch]$DryRun
15
- )
16
-
17
- # Ensure UTF-8 output encoding in PowerShell console
18
- try {
19
- [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
20
- $OutputEncoding = [System.Text.Encoding]::UTF8
21
- } catch {}
22
-
23
- $ErrorActionPreference = "Continue"
24
- if (Test-Path variable:global:PSNativeCommandUseErrorActionPreference) {
25
- $global:PSNativeCommandUseErrorActionPreference = $false
26
- }
27
-
28
- $Hostname = $env:COMPUTERNAME.ToLower() -replace '[^a-z0-9_-]', ''
29
- $IsLaptop = [bool](Get-CimInstance -ClassName Win32_Battery -ErrorAction SilentlyContinue)
30
-
31
- if ($Lang -eq "ru") {
32
- $DetectedType = if ($IsLaptop) { "Windows Ноутбук (Laptop)" } else { "Windows Десктоп / Сервер" }
33
- Write-Host "+----------------------------------------------------------------------+" -ForegroundColor Cyan
34
- Write-Host "| [*] Обнаружено устройство: |" -ForegroundColor Cyan
35
- Write-Host "| * Имя хоста : $Hostname" -ForegroundColor Green
36
- Write-Host "| * Тип : $DetectedType" -ForegroundColor Yellow
37
- Write-Host "| * ОС : Windows $([System.Environment]::OSVersion.Version)" -ForegroundColor White
38
- Write-Host "+----------------------------------------------------------------------+" -ForegroundColor Cyan
39
- } else {
40
- $DetectedType = if ($IsLaptop) { "Windows Laptop" } else { "Windows Desktop / Server" }
41
- Write-Host "+----------------------------------------------------------------------+" -ForegroundColor Cyan
42
- Write-Host "| [*] Detected Device: |" -ForegroundColor Cyan
43
- Write-Host "| * Hostname : $Hostname" -ForegroundColor Green
44
- Write-Host "| * Type : $DetectedType" -ForegroundColor Yellow
45
- Write-Host "| * OS : Windows $([System.Environment]::OSVersion.Version)" -ForegroundColor White
46
- Write-Host "+----------------------------------------------------------------------+" -ForegroundColor Cyan
47
- }
48
-
49
- # The dry run is handled after the (read-only) interpreter detection below: a
50
- # preflight that exits before checking anything cannot honestly report that the
51
- # dependencies were verified.
52
-
53
- # Python check & auto-install.
54
- # Windows ships a "python.exe" App Execution Alias under
55
- # %LOCALAPPDATA%\Microsoft\WindowsApps that runs Python only while the Microsoft
56
- # Store package stays healthy: it can be on PATH, print nothing usable and exit
57
- # non-zero, and it stops working silently after a Store repair or an update. A
58
- # node whose autostart pins that alias never comes back after a reboot - the only
59
- # trace is a truncated "Python " line in agent.log - so an alias is never
60
- # accepted here. Every candidate must be a real interpreter outside WindowsApps
61
- # that prints 3 for sys.version_info[0] and whose own sys.executable does not
62
- # resolve back into WindowsApps.
63
- $WindowsAppsMarker = "\WindowsApps\"
64
-
65
- # ------------------------------------------------------------------------------
66
- # ARCHITECTURE - which build of Python this machine actually needs
67
- #
68
- # This installer used to download python-3.12.5-amd64.exe unconditionally, which
69
- # is wrong in two measurable ways:
70
- #
71
- # * on 32-bit Windows the amd64 installer refuses to run at all, so the
72
- # automatic step failed and the operator was told to install Python by hand;
73
- # * on ARM64 Windows it installs an x64 build that only works under emulation,
74
- # which is slower and breaks as soon as a dependency has no emulated wheel.
75
- #
76
- # RuntimeInformation.OSArchitecture is asked FIRST because it reports the native
77
- # OS architecture even when this PowerShell is itself emulated. The environment
78
- # variables only describe the process: a 32-bit PowerShell on 64-bit Windows
79
- # reports PROCESSOR_ARCHITECTURE=x86 and hides the truth in PROCESSOR_ARCHITEW6432.
80
- # Both are read so the answer still works on a .NET runtime without
81
- # RuntimeInformation (it needs 4.7.1+).
82
- # ------------------------------------------------------------------------------
83
- function Get-OsArchitecture {
84
- $raw = ""
85
- try { $raw = "$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)" } catch {}
86
- if (-not $raw) {
87
- if ($env:PROCESSOR_ARCHITEW6432) { $raw = $env:PROCESSOR_ARCHITEW6432 }
88
- elseif ($env:PROCESSOR_ARCHITECTURE) { $raw = $env:PROCESSOR_ARCHITECTURE }
89
- }
90
- switch -Regex ("$raw".Trim().ToUpperInvariant()) {
91
- # PyOrgSuffix is the python.org file-name suffix; UvTarget is the
92
- # astral-sh/uv release asset. uv ships no 32-bit Windows build, so that
93
- # column stays empty for x86 and the chain simply skips it there.
94
- '^(AMD64|X64)$' { return @{ Key='amd64'; Label='x64'; PyOrgSuffix='-amd64'; UvTarget='x86_64-pc-windows-msvc'; Supported=$true } }
95
- '^(ARM64)$' { return @{ Key='arm64'; Label='ARM64'; PyOrgSuffix='-arm64'; UvTarget='aarch64-pc-windows-msvc'; Supported=$true } }
96
- '^(X86|I386|I686)$' { return @{ Key='x86'; Label='x86 (32-bit)'; PyOrgSuffix=''; UvTarget=$null; Supported=$true } }
97
- default { return @{ Key='unknown'; Label="$raw"; PyOrgSuffix=$null; UvTarget=$null; Supported=$false } }
98
- }
99
- }
100
-
101
- $OsArch = Get-OsArchitecture
102
-
103
- # The exact CPython the direct-download fallback installs. Kept in one place so the
104
- # URL, the cached file name and the version can never drift apart.
105
- $PyOrgVersion = '3.12.5'
106
-
107
- # uv is the last resort: one static binary that can fetch its own CPython and
108
- # install packages without pip. That is what makes it useful on a machine where
109
- # winget is blocked and pip is missing or refuses to write (PEP 668).
110
- $UvDir = "$env:LOCALAPPDATA\uv"
111
- $UvExe = $null
112
-
113
- function Get-PyOrgInstallerName {
114
- param($Arch)
115
- if (-not $Arch -or -not $Arch.Supported -or $null -eq $Arch.PyOrgSuffix) { return "" }
116
- return "python-$PyOrgVersion$($Arch.PyOrgSuffix).exe"
117
- }
118
-
119
- # A failed download usually leaves an HTML error page on disk, and running that as
120
- # an installer produces a bare "not a valid Win32 application" with no hint of the
121
- # real cause. Checking the DOS 'MZ' magic keeps the diagnosis inside this script.
122
- # The size floor only has to separate a real binary from an error page, so it stays
123
- # small: a large floor would reject a perfectly valid small executable and turn this
124
- # into an "is it big" test rather than an "is it a PE" test.
125
- function Test-PeExecutable {
126
- param([string]$Path)
127
- if (-not $Path -or -not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false }
128
- try {
129
- if ((Get-Item -LiteralPath $Path).Length -lt 64KB) { return $false }
130
- # Two bytes through the stream API: "Get-Content -Encoding Byte" is
131
- # PowerShell 5.1 only, and this file also runs under pwsh 7.
132
- $fs = [System.IO.File]::OpenRead($Path)
133
- try { $b0 = $fs.ReadByte(); $b1 = $fs.ReadByte() } finally { $fs.Dispose() }
134
- return ($b0 -eq 0x4D -and $b1 -eq 0x5A)
135
- } catch { return $false }
136
- }
137
-
138
- function Test-RealPython {
139
- param([string]$Path)
140
- if (-not $Path) { return $false }
141
- if ($Path -like "*$WindowsAppsMarker*") { return $false }
142
- if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false }
143
- try {
144
- # Two answers in one probe: the major version, and the interpreter the
145
- # alias would actually run (an alias reports a WindowsApps path there).
146
- $probe = @(& $Path -c "import sys; print(sys.version_info[0]); print(sys.executable)" 2>&1)
147
- if ($LASTEXITCODE -ne 0) { return $false }
148
- $rows = @($probe | ForEach-Object { "$_".Trim() } | Where-Object { $_ })
149
- if ($rows.Count -lt 1 -or "$($rows[0])" -ne "3") { return $false }
150
- if ($rows.Count -ge 2 -and "$($rows[1])" -like "*$WindowsAppsMarker*") { return $false }
151
- return $true
152
- } catch { return $false }
153
- }
154
-
155
- function Test-PythonWebsockets {
156
- param([string]$Path)
157
- try {
158
- & $Path -c "import websockets" 2>$null
159
- return ($LASTEXITCODE -eq 0)
160
- } catch { return $false }
161
- }
162
-
163
- # ------------------------------------------------------------------------------
164
- # AUTOMATIC PYTHON INSTALLATION - three sources, tried in that order
165
- #
166
- # winget first, because it is the platform's own mechanism and picks the correct
167
- # architecture by itself; the python.org download second, because it also works
168
- # where winget is missing, blocked by policy or has no configured source; uv last,
169
- # because it is the only one of the three that needs neither an installer nor a
170
- # working pip.
171
- #
172
- # Every step re-resolves the interpreter with Select-RealPython instead of trusting
173
- # the installer's exit code. A silent per-user install, a queued Store update and
174
- # an "already installed" no-op all report success, and only a probe tells them
175
- # apart - which is exactly the failure this chain exists to remove.
176
- # ------------------------------------------------------------------------------
177
-
178
- function Install-PythonFromPythonOrg {
179
- param($Arch)
180
- $name = Get-PyOrgInstallerName $Arch
181
- if (-not $name) { return $false }
182
- $url = "https://www.python.org/ftp/python/$PyOrgVersion/$name"
183
- $installer = "$env:TEMP\$name"
184
- # Reuse an earlier good download: this is the same ~25 MB file on every re-run,
185
- # and a half-written .exe from an interrupted attempt is rejected by the MZ
186
- # check rather than executed.
187
- if (-not (Test-PeExecutable $installer)) {
188
- try { Invoke-WebRequest -Uri $url -OutFile $installer -UseBasicParsing -ErrorAction Stop } catch { return $false }
189
- }
190
- if (-not (Test-PeExecutable $installer)) { return $false }
191
- try {
192
- # InstallAllUsers=0 keeps this per-user and therefore elevation-free.
193
- # Include_pip=1 is explicit so the dependency step always has a pip.
194
- $proc = Start-Process -FilePath $installer `
195
- -ArgumentList '/quiet InstallAllUsers=0 PrependPath=1 Include_test=0 Include_pip=1' `
196
- -Wait -PassThru -ErrorAction Stop
197
- # 3010 is "success, reboot required" and is a success here.
198
- return ($proc.ExitCode -eq 0 -or $proc.ExitCode -eq 3010)
199
- } catch { return $false }
200
- }
201
-
202
- function Install-UvWindows {
203
- # Returns the path to uv.exe, or $null. The release archive is fetched and
204
- # unpacked here rather than piping astral.sh's install script into iex: a script
205
- # that arrives over the network and is executed unread is a worse default than
206
- # an archive this function unpacks itself.
207
- param($Arch)
208
- if (-not $Arch -or -not $Arch.UvTarget) { return $null }
209
- $exe = Join-Path $UvDir 'uv.exe'
210
- if (Test-Path -LiteralPath $exe) { return $exe }
211
- $zip = Join-Path $env:TEMP "uv-$($Arch.Key).zip"
212
- try {
213
- Invoke-WebRequest -Uri "https://github.com/astral-sh/uv/releases/latest/download/uv-$($Arch.UvTarget).zip" `
214
- -OutFile $zip -UseBasicParsing -ErrorAction Stop
215
- if (!(Test-Path $UvDir)) { New-Item -ItemType Directory -Path $UvDir -Force | Out-Null }
216
- # The asset carries uv.exe, uvw.exe and uvx.exe at its top level.
217
- Expand-Archive -LiteralPath $zip -DestinationPath $UvDir -Force -ErrorAction Stop
218
- } catch { return $null }
219
- if (Test-Path -LiteralPath $exe) { return $exe }
220
- return $null
221
- }
222
-
223
- function Install-PythonViaUv {
224
- # uv downloads a standalone CPython into its own directory, so no installer,
225
- # no elevation and no writable system Python is involved. Get-PythonCandidates
226
- # knows that directory, which is how the freshly fetched interpreter is found
227
- # by the ordinary resolution path.
228
- param([string]$Uv)
229
- if (-not $Uv) { return $false }
230
- try {
231
- & $Uv python install $PyOrgVersion 2>&1 | Out-Null
232
- return $true
233
- } catch { return $false }
234
- }
235
-
236
- # ------------------------------------------------------------------------------
237
- # DEPENDENCY INSTALLATION - websockets is the node's only external requirement
238
- #
239
- # core/mcp_tools.py and core/server.py are standard library only, so this one
240
- # package is the entire dependency surface. It is installed through five different
241
- # mechanisms because each fails for a different reason on a locked-down machine:
242
- # a plain pip needs a writable site-packages, --user needs a writable profile,
243
- # ensurepip needs an interpreter that shipped without pip, uv needs neither pip nor
244
- # a writable Python directory, and a venv needs only the interpreter itself.
245
- #
246
- # Returns the interpreter that actually ended up with the package. That return
247
- # value matters: when only the venv fallback works, the pinned autostart
248
- # interpreter must BECOME the venv's python.exe, or the node autostarts with an
249
- # interpreter that cannot import websockets.
250
- # ------------------------------------------------------------------------------
251
- function Install-WebsocketsFor {
252
- param([string]$Python, [string]$Uv)
253
-
254
- if (Test-PythonWebsockets $Python) { return @{ Ok = $true; Python = $Python } }
255
-
256
- # 1. plain pip, after upgrading it: an old pip cannot find or build wheels.
257
- & $Python -m pip install --disable-pip-version-check --quiet --upgrade pip 2>&1 | Out-Null
258
- & $Python -m pip install --disable-pip-version-check --quiet websockets 2>&1 | Out-Null
259
- if (Test-PythonWebsockets $Python) { return @{ Ok = $true; Python = $Python } }
260
-
261
- # 2. --user: site-packages is read-only, the profile is not.
262
- & $Python -m pip install --disable-pip-version-check --quiet --user websockets 2>&1 | Out-Null
263
- if (Test-PythonWebsockets $Python) { return @{ Ok = $true; Python = $Python } }
264
-
265
- # 3. ensurepip: a per-user install lands without pip when the feature set was
266
- # trimmed, or when the launcher is disabled by policy.
267
- & $Python -m ensurepip --default-pip 2>&1 | Out-Null
268
- & $Python -m pip install --disable-pip-version-check --quiet websockets 2>&1 | Out-Null
269
- if (Test-PythonWebsockets $Python) { return @{ Ok = $true; Python = $Python } }
270
-
271
- # 4. uv: installs into a managed environment without needing pip at all.
272
- if ($Uv) {
273
- & $Uv pip install --python $Python websockets 2>&1 | Out-Null
274
- if (Test-PythonWebsockets $Python) { return @{ Ok = $true; Python = $Python } }
275
- }
276
-
277
- # 5. venv: the only fallback that needs no write access to the system Python.
278
- $venvDir = "$env:USERPROFILE\.config\antigravity-mesh\venv"
279
- $venvPython = "$venvDir\Scripts\python.exe"
280
- if (-not (Test-Path -LiteralPath $venvPython)) { & $Python -m venv $venvDir 2>&1 | Out-Null }
281
- if (Test-Path -LiteralPath $venvPython) {
282
- & $venvPython -m pip install --disable-pip-version-check --quiet --upgrade pip 2>&1 | Out-Null
283
- & $venvPython -m pip install --disable-pip-version-check --quiet websockets 2>&1 | Out-Null
284
- if (Test-PythonWebsockets $venvPython) { return @{ Ok = $true; Python = $venvPython } }
285
- if ($Uv) {
286
- & $Uv pip install --python $venvPython websockets 2>&1 | Out-Null
287
- if (Test-PythonWebsockets $venvPython) { return @{ Ok = $true; Python = $venvPython } }
288
- }
289
- }
290
- return @{ Ok = $false; Python = $Python }
291
- }
292
-
293
- function Get-PythonCandidates {
294
- $list = @()
295
- # Every "python" on PATH, not just the first: the first hit is usually the
296
- # Store alias while a real install sits further down the same PATH.
297
- foreach ($cmd in @(Get-Command python -All -ErrorAction SilentlyContinue)) {
298
- if ($cmd -and $cmd.Source) { $list += $cmd.Source }
299
- }
300
- $list += @(
301
- "$env:LOCALAPPDATA\Programs\Python\Python313\python.exe",
302
- "$env:LOCALAPPDATA\Programs\Python\Python312\python.exe",
303
- "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe",
304
- "$env:ProgramFiles\Python313\python.exe",
305
- "$env:ProgramFiles\Python312\python.exe",
306
- "$env:ProgramFiles\Python311\python.exe"
307
- )
308
- # Any per-user 3.x the python.org installer left behind, newest first. This
309
- # used to be a second, separate fallback that forgot to record the path it
310
- # found, so a machine with only 3.13 was pushed into a fresh download.
311
- $localRoot = "$env:LOCALAPPDATA\Programs\Python"
312
- if (Test-Path -LiteralPath $localRoot) {
313
- $list += @(Get-ChildItem -Path $localRoot -Directory -Filter 'Python3*' -ErrorAction SilentlyContinue |
314
- Sort-Object Name -Descending |
315
- ForEach-Object { Join-Path $_.FullName 'python.exe' })
316
- }
317
- # The uv-managed CPython, for the case where the uv fallback is what produced
318
- # an interpreter: it lives outside PATH and outside the python.org directories,
319
- # so without this the freshly fetched Python would be installed and then never
320
- # found again. UV_PYTHON_INSTALL_DIR is honoured because uv lets a user move it.
321
- $uvRoots = @("$env:APPDATA\uv\python")
322
- if ($env:UV_PYTHON_INSTALL_DIR) { $uvRoots += $env:UV_PYTHON_INSTALL_DIR }
323
- foreach ($uvRoot in $uvRoots) {
324
- if (Test-Path -LiteralPath $uvRoot) {
325
- $list += @(Get-ChildItem -Path $uvRoot -Directory -Filter 'cpython-3*' -ErrorAction SilentlyContinue |
326
- Sort-Object Name -Descending |
327
- ForEach-Object { Join-Path $_.FullName 'python.exe' })
328
- }
329
- }
330
- # The "py" launcher is not an interpreter: ask it which real python.exe it
331
- # runs, so the autostart entry always names a python.exe, never "py -3".
332
- $cmdPy = Get-Command py -ErrorAction SilentlyContinue
333
- if ($cmdPy -and $cmdPy.Source) {
334
- try {
335
- $real = @(& $cmdPy.Source -3 -c "import sys; print(sys.executable)" 2>$null)
336
- if ($LASTEXITCODE -eq 0 -and $real.Count -ge 1 -and "$($real[0])".Trim()) {
337
- $list += "$($real[0])".Trim()
338
- }
339
- } catch {}
340
- }
341
- return @($list | Where-Object { $_ } | Select-Object -Unique)
342
- }
343
-
344
- function Select-RealPython {
345
- # An interpreter that already has websockets wins: the dependency step then
346
- # cannot silently install into a different, more fragile Python.
347
- $fallback = $null
348
- foreach ($cand in (Get-PythonCandidates)) {
349
- if (-not (Test-RealPython $cand)) { continue }
350
- if (Test-PythonWebsockets $cand) { return $cand }
351
- if (-not $fallback) { $fallback = $cand }
352
- }
353
- return $fallback
354
- }
355
-
356
- function Add-PythonToPath {
357
- param([string]$Path)
358
- if (-not $Path) { return }
359
- $dir = Split-Path -Parent $Path
360
- if ($dir -and ($env:Path -notlike "*$dir*")) {
361
- $env:Path = "$dir;$dir\Scripts;$env:Path"
362
- }
363
- }
364
-
365
- $hasPython = $false
366
- $PyExe = $null
367
- $PyExe = Select-RealPython
368
- if ($PyExe) {
369
- Add-PythonToPath $PyExe
370
- $hasPython = $true
371
- $pyVer = & $PyExe --version 2>&1
372
- if ($Lang -eq "ru") {
373
- Write-Host "[1/3] Python обнаружен: $pyVer ($PyExe)" -ForegroundColor Green
374
- } else {
375
- Write-Host "[1/3] Python detected: $pyVer ($PyExe)" -ForegroundColor Green
376
- }
377
- if (-not (Test-PythonWebsockets $PyExe)) {
378
- if ($Lang -eq "ru") {
379
- Write-Host "[1/3] В выбранном Python нет websockets - он будет установлен на шаге 2." -ForegroundColor DarkYellow
380
- } else {
381
- Write-Host "[1/3] The selected Python has no websockets yet - it will be installed in step 2." -ForegroundColor DarkYellow
382
- }
383
- }
384
- } elseif ($Lang -eq "ru") {
385
- Write-Host "[1/3] Рабочий Python 3 не найден (ярлык Microsoft Store за интерпретатор не считается)." -ForegroundColor Yellow
386
- } else {
387
- Write-Host "[1/3] No working Python 3 found (the Microsoft Store alias does not count)." -ForegroundColor Yellow
388
- }
389
-
390
-
391
- # ------------------------------------------------------------------------------
392
- # PUBLIC DOMAIN - resolved from the one source of truth
393
- #
394
- # The domain is declared exactly once, in core/domain.py. This block asks that
395
- # same chain, in the same order, and it is the ONLY place in this script that
396
- # picks a hostname:
397
- #
398
- # 1. -Gateway on the command line;
399
- # 2. $env:MESH_PUBLIC_URL;
400
- # 3. $env:AGY_PUBLIC_BASE_URL (legacy alias, same meaning);
401
- # 4. the domain file - $env:MESH_DOMAIN_FILE when set, otherwise
402
- # %USERPROFILE%\.config\antigravity-mesh\domain.env;
403
- # 5. $PublishedDomain - the domain this copy was published with. The repository
404
- # carries the placeholder __MESH_DOMAIN__ there and deploy_gateway.sh
405
- # rewrites that line while publishing, so a served copy knows its domain;
406
- # 6. core/domain.py's DEFAULT_PUBLIC_BASE_URL, read through the interpreter
407
- # found above (a repository checkout, or a node that bootstrapped already).
408
- #
409
- # If none of them yields a real hostname the installer stops. A node registered
410
- # against a domain that does not resolve can never connect, and its autostart
411
- # entry would retry that dead endpoint forever.
412
- # ------------------------------------------------------------------------------
413
- $DomainPlaceholder = "__MESH_DOMAIN__"
414
- # deploy_gateway.sh rewrites THIS line only (anchored on $PublishedDomain), so the
415
- # placeholder above survives as the "this copy was not published" marker.
416
- $PublishedDomain = "__MESH_DOMAIN__"
417
- $ScriptRoot = $null
418
- if ($MyInvocation.MyCommand -and $MyInvocation.MyCommand.Path) {
419
- $ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
420
- }
421
-
422
- function Get-NormalisedHost([string]$Value) {
423
- if ([string]::IsNullOrWhiteSpace($Value)) { return "" }
424
- $text = $Value.Trim().Trim('"').Trim("'")
425
- $text = $text -replace '^[A-Za-z][A-Za-z0-9+.-]*://', ''
426
- $text = $text.Split('/')[0]
427
- return $text.Trim()
428
- }
429
-
430
- function Get-DomainFileValue([string]$Path) {
431
- if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { return "" }
432
- $values = @{}
433
- try {
434
- # -Encoding UTF8 also absorbs the BOM that PowerShell itself writes.
435
- foreach ($line in (Get-Content -LiteralPath $Path -Encoding UTF8)) {
436
- $trimmed = $line.Trim()
437
- if (-not $trimmed -or $trimmed.StartsWith('#') -or -not $trimmed.Contains('=')) { continue }
438
- $index = $trimmed.IndexOf('=')
439
- $key = $trimmed.Substring(0, $index).Trim()
440
- $value = $trimmed.Substring($index + 1).Trim().Trim('"').Trim("'")
441
- if ($key) { $values[$key] = $value }
442
- }
443
- } catch { return "" }
444
- if ($values['MESH_PUBLIC_URL']) { return $values['MESH_PUBLIC_URL'] }
445
- if ($values['AGY_PUBLIC_BASE_URL']) { return $values['AGY_PUBLIC_BASE_URL'] }
446
- return ""
447
- }
448
-
449
- function Get-ModuleDefaultDomain {
450
- if (-not $ScriptRoot) { return "" }
451
- $module = Join-Path $ScriptRoot 'core\domain.py'
452
- if (-not (Test-Path -LiteralPath $module)) { return "" }
453
- $value = ""
454
- if ($PyExe) {
455
- $interpreter = ($PyExe -split ' ')[0]
456
- $probe = "import sys; sys.path.insert(0, r'$ScriptRoot'); from core import domain; print(domain.DEFAULT_PUBLIC_BASE_URL)"
457
- try { $value = & $interpreter -c $probe 2>$null } catch { $value = "" }
458
- }
459
- if (-not $value) {
460
- $match = Select-String -LiteralPath $module -Pattern '^DEFAULT_PUBLIC_BASE_URL\s*=\s*"([^"]+)"' |
461
- Select-Object -First 1
462
- if ($match) { $value = $match.Matches[0].Groups[1].Value }
463
- }
464
- return "$value".Trim()
465
- }
466
-
467
- # ------------------------------------------------------------------------------
468
- # PERSIST THE DOMAIN (what the node reads to build its public links)
469
- #
470
- # core/domain.py resolves the public domain from MESH_PUBLIC_URL in the
471
- # environment, then from the domain file, then from its built-in default - and it
472
- # never reads the legacy MESH_GATEWAY that agent.env carries. A node installed
473
- # with only agent.env therefore dials the right gateway (gateway_host() does
474
- # honour MESH_GATEWAY) while minting share links on the built-in default.
475
- #
476
- # The domain is already resolved here, so it is written down here: the place that
477
- # decides the domain is then also the place that records it, and the node's
478
- # links, its tunnel and the gateway agree by construction.
479
- #
480
- # Never fatal: a profile directory that cannot be written still leaves a working
481
- # node, so a failure only prints the command the operator can run by hand.
482
- # ------------------------------------------------------------------------------
483
- function Write-DomainFile {
484
- param([string]$Domain, [string]$Path)
485
-
486
- # The caller hands over a bare host (the resolution block above normalises it),
487
- # but this function is the last thing between a value and the file the runtime
488
- # trusts: a scheme or a trailing slash would be written verbatim and become
489
- # "MESH_PUBLIC_URL=https://https://host/", which no reader recovers from.
490
- # Normalise exactly the way the resolution chain does.
491
- $Domain = Get-NormalisedHost $Domain
492
- if ([string]::IsNullOrWhiteSpace($Domain) -or $Domain -eq $DomainPlaceholder) { return }
493
- if ([string]::IsNullOrWhiteSpace($Path)) { return }
494
-
495
- # Never record the project's built-in default. It is a fallback, not a decision:
496
- # writing it would turn "nothing is configured" into a pinned domain file, and
497
- # once that file exists core/domain.py stops consulting the legacy MESH_GATEWAY
498
- # at all - the operator's own agent.env knob would silently go inert. Nothing is
499
- # lost by skipping it: with no file the resolver's last fallback IS that value,
500
- # so the node's links and its tunnel still name the same host.
501
- $moduleDefault = Get-NormalisedHost (Get-ModuleDefaultDomain)
502
- if ($moduleDefault -and $Domain -eq $moduleDefault) { return }
503
-
504
- $existing = Get-NormalisedHost (Get-DomainFileValue $Path)
505
- if ($existing -eq $Domain) { return }
506
-
507
- $line = "MESH_PUBLIC_URL=https://$Domain"
508
- $directory = Split-Path -Parent $Path
509
- try {
510
- if ($directory -and -not (Test-Path -LiteralPath $directory)) {
511
- New-Item -ItemType Directory -Force -Path $directory | Out-Null
512
- }
513
- # Written without a BOM. Every reader tolerates one (core/domain.py reads
514
- # utf-8-sig, install.sh strips it), but a BOM-less file is readable by all
515
- # of them, sed and grep included.
516
- [System.IO.File]::WriteAllText(
517
- $Path, $line + "`r`n", (New-Object System.Text.UTF8Encoding($false)))
518
- } catch {
519
- if ($Lang -eq "ru") {
520
- Write-Host "[!] Не удалось записать домен в $Path : $($_.Exception.Message)" -ForegroundColor Yellow
521
- Write-Host " Ссылки узла будут строиться на встроенном домене по умолчанию." -ForegroundColor Yellow
522
- Write-Host " Запишите вручную: Set-Content '$Path' '$line'" -ForegroundColor Yellow
523
- } else {
524
- Write-Host "[!] Could not write the domain to $Path : $($_.Exception.Message)" -ForegroundColor Yellow
525
- Write-Host " The node's links would use the built-in default domain." -ForegroundColor Yellow
526
- Write-Host " Write it by hand: Set-Content '$Path' '$line'" -ForegroundColor Yellow
527
- }
528
- return
529
- }
530
-
531
- if ($Lang -eq "ru") {
532
- if ($existing) {
533
- Write-Host "[OK] Домен узла: $line ($Path, было https://$existing)" -ForegroundColor Green
534
- } else {
535
- Write-Host "[OK] Домен узла: $line ($Path)" -ForegroundColor Green
536
- }
537
- } else {
538
- if ($existing) {
539
- Write-Host "[OK] Node domain: $line ($Path, was https://$existing)" -ForegroundColor Green
540
- } else {
541
- Write-Host "[OK] Node domain: $line ($Path)" -ForegroundColor Green
542
- }
543
- }
544
- }
545
-
546
- $DomainFilePath = if ($env:MESH_DOMAIN_FILE) { $env:MESH_DOMAIN_FILE } else { "$env:USERPROFILE\.config\antigravity-mesh\domain.env" }
547
- $ResolvedDomain = Get-NormalisedHost $Gateway
548
- if (-not $ResolvedDomain) { $ResolvedDomain = Get-NormalisedHost $env:MESH_PUBLIC_URL }
549
- if (-not $ResolvedDomain) { $ResolvedDomain = Get-NormalisedHost $env:AGY_PUBLIC_BASE_URL }
550
- if (-not $ResolvedDomain) { $ResolvedDomain = Get-NormalisedHost (Get-DomainFileValue $DomainFilePath) }
551
- if (-not $ResolvedDomain -and $PublishedDomain -ne $DomainPlaceholder) {
552
- $ResolvedDomain = Get-NormalisedHost $PublishedDomain
553
- }
554
- if (-not $ResolvedDomain) { $ResolvedDomain = Get-NormalisedHost (Get-ModuleDefaultDomain) }
555
-
556
- $DomainMissing = [string]::IsNullOrWhiteSpace($ResolvedDomain) -or $ResolvedDomain -eq $DomainPlaceholder
557
- if ($DomainMissing) {
558
- if ($Lang -eq "ru") {
559
- Write-Host ""
560
- Write-Host "[!] Домен шлюза не задан." -ForegroundColor Red
561
- # ${...} matters: "$DomainPlaceholder:" would be read as a drive-qualified
562
- # variable reference and the whole script fails to parse in PowerShell 5.1.
563
- Write-Host " В этой копии установщика остался плейсхолдер ${DomainPlaceholder}: она не была" -ForegroundColor Red
564
- Write-Host " опубликована скриптом deploy_gateway.sh, а домена нет ни в окружении, ни в" -ForegroundColor Red
565
- Write-Host " файле $DomainFilePath." -ForegroundColor Red
566
- Write-Host " Укажите домен одним из способов:" -ForegroundColor Yellow
567
- Write-Host " .\install.ps1 -Gateway <общий-домен>" -ForegroundColor Yellow
568
- Write-Host " `$env:MESH_PUBLIC_URL='https://<общий-домен>'; .\install.ps1" -ForegroundColor Yellow
569
- Write-Host " Set-Content '$DomainFilePath' 'MESH_PUBLIC_URL=https://<общий-домен>'" -ForegroundColor Yellow
570
- Write-Host " Установка остановлена: регистрация на несуществующем домене не выполняется," -ForegroundColor Yellow
571
- Write-Host " файл конфигурации и автозапуск не создаются." -ForegroundColor Yellow
572
- } else {
573
- Write-Host ""
574
- Write-Host "[!] No gateway domain configured." -ForegroundColor Red
575
- Write-Host " This copy of the installer still carries the $DomainPlaceholder placeholder: it" -ForegroundColor Red
576
- Write-Host " was not published by deploy_gateway.sh, and no domain was found in the" -ForegroundColor Red
577
- Write-Host " environment or in $DomainFilePath." -ForegroundColor Red
578
- Write-Host " Pass the domain in one of these ways:" -ForegroundColor Yellow
579
- Write-Host " .\install.ps1 -Gateway <shared-domain>" -ForegroundColor Yellow
580
- Write-Host " `$env:MESH_PUBLIC_URL='https://<shared-domain>'; .\install.ps1" -ForegroundColor Yellow
581
- Write-Host " Set-Content '$DomainFilePath' 'MESH_PUBLIC_URL=https://<shared-domain>'" -ForegroundColor Yellow
582
- Write-Host " Stopping: a node is never registered against a domain that does not exist," -ForegroundColor Yellow
583
- Write-Host " no config file and no autostart are created." -ForegroundColor Yellow
584
- }
585
- Write-Host ""
586
- if (-not $DryRun) { exit 1 }
587
- } else {
588
- $Gateway = $ResolvedDomain
589
- }
590
-
591
- # Preflight: report what was detected and change nothing. This runs after the
592
- # detection steps above and before any install (winget, the Python installer,
593
- # pip), so the reported state is real and the machine is untouched.
594
- if ($DryRun) {
595
- $probeExe = if ($PyExe) { ($PyExe -split " ")[0] } else { $null }
596
- $wsState = "unknown"
597
- if ($probeExe) {
598
- & $probeExe -c "import websockets" 2>$null
599
- $wsState = if ($LASTEXITCODE -eq 0) { "installed" } else { "missing" }
600
- }
601
- $pyState = if ($hasPython) { $PyExe } else { "not found" }
602
- # The architecture is reported here because it decides which build every
603
- # automatic step above will fetch, and "why did it install the x64 build on my
604
- # ARM machine" is not answerable from the rest of this report.
605
- $archState = if ($OsArch.Supported) { $OsArch.Label } else { "$($OsArch.Label) (unsupported)" }
606
- $domainState = if ($DomainMissing) { "NOT CONFIGURED" } else { $Gateway }
607
- # The live node state, so a preflight can say "your node is offline right now"
608
- # before anything on disk is touched. The name may still gain a suffix at
609
- # registration (auto_suffix), so this stays best effort.
610
- $nodeState = "not checked"
611
- if (-not $DomainMissing) {
612
- $probeNode = if ($User) { $User } else { $Hostname }
613
- try {
614
- $probeHealth = Invoke-RestMethod -Uri "https://$Gateway/health?user=$probeNode" -TimeoutSec 5 -ErrorAction Stop
615
- $nodeState = if ($probeHealth.node_online) { "online ($probeNode)" } else { "offline ($probeNode)" }
616
- } catch {
617
- $nodeState = "gateway unreachable"
618
- }
619
- }
620
- # What the domain file would receive: the very line the real run writes. A dry
621
- # run changes nothing, so this only reports it - and it reports a skip when the
622
- # writer would skip, so the two never disagree.
623
- $domainFileState = if ($DomainMissing) {
624
- "$DomainFilePath (not written: no domain configured)"
625
- } elseif ($Gateway -eq (Get-NormalisedHost (Get-ModuleDefaultDomain))) {
626
- "$DomainFilePath (not written: the built-in default is a fallback, not a configuration)"
627
- } else {
628
- "$DomainFilePath -> MESH_PUBLIC_URL=https://$Gateway"
629
- }
630
- if ($Lang -eq "ru") {
631
- Write-Host "[DRY-RUN] Python : $pyState"
632
- Write-Host "[DRY-RUN] websockets : $wsState"
633
- Write-Host "[DRY-RUN] Домен : $domainState"
634
- Write-Host "[DRY-RUN] Конфиг : $env:USERPROFILE\.config\antigravity-mesh"
635
- Write-Host "[DRY-RUN] Автозапуск : $([Environment]::GetFolderPath('Startup'))\antigravity-agent.vbs"
636
- Write-Host "[DRY-RUN] Узел : $nodeState"
637
- Write-Host "[DRY-RUN] Архитектура : $archState"
638
- Write-Host "[DRY-RUN] Файл домена : $domainFileState"
639
- Write-Host "[DRY-RUN] Действий не выполнено." -ForegroundColor Yellow
640
- } else {
641
- Write-Host "[DRY-RUN] Python : $pyState"
642
- Write-Host "[DRY-RUN] websockets : $wsState"
643
- Write-Host "[DRY-RUN] Domain : $domainState"
644
- Write-Host "[DRY-RUN] Config : $env:USERPROFILE\.config\antigravity-mesh"
645
- Write-Host "[DRY-RUN] Autostart : $([Environment]::GetFolderPath('Startup'))\antigravity-agent.vbs"
646
- Write-Host "[DRY-RUN] Node : $nodeState"
647
- Write-Host "[DRY-RUN] Arch : $archState"
648
- Write-Host "[DRY-RUN] Domain file : $domainFileState"
649
- Write-Host "[DRY-RUN] Nothing was changed." -ForegroundColor Yellow
650
- }
651
- if ($DomainMissing -or -not $hasPython) { exit 2 }
652
- exit 0
653
- }
654
-
655
- function Resolve-PythonAfterInstall {
656
- # Re-resolve with exactly the same rules the detection above used, and put the
657
- # winner on PATH for the child processes below. This is a function because the
658
- # chain re-resolves after each of the three sources and those call sites must
659
- # stay identical: a fresh interpreter only counts if it is a real python.exe
660
- # outside WindowsApps and it actually runs.
661
- $found = Select-RealPython
662
- if ($found) { Add-PythonToPath $found }
663
- return $found
664
- }
665
-
666
- if (-not $hasPython) {
667
- if ($Lang -eq "ru") {
668
- Write-Host "[1/3] Python не найден. Архитектура: $($OsArch.Label). Автоматическая установка..." -ForegroundColor Yellow
669
- } else {
670
- Write-Host "[1/3] Python not found. Architecture: $($OsArch.Label). Installing automatically..." -ForegroundColor Yellow
671
- }
672
-
673
- # 1. winget - the platform's own mechanism, and the only source that needs no
674
- # architecture mapping from this script.
675
- if (-not $hasPython -and (Get-Command winget -ErrorAction SilentlyContinue)) {
676
- if ($Lang -eq "ru") { Write-Host "[1/3] Источник 1/3: winget..." -ForegroundColor Yellow }
677
- else { Write-Host "[1/3] Source 1/3: winget..." -ForegroundColor Yellow }
678
- & winget install --id Python.Python.3.12 -e --silent --accept-source-agreements --accept-package-agreements
679
- $fresh = Resolve-PythonAfterInstall
680
- if ($fresh) { $PyExe = $fresh; $hasPython = $true }
681
- }
682
-
683
- # 2. python.org, for the architecture this machine actually runs. This used to
684
- # be the "winget is missing" branch, so a machine WITH winget never reached
685
- # it - and a winget that is present but blocked by policy, has no source, or
686
- # silently fails left the installer with nothing to fall back on.
687
- if (-not $hasPython -and $OsArch.Supported) {
688
- if ($Lang -eq "ru") { Write-Host "[1/3] Источник 2/3: установщик python.org для $($OsArch.Label)..." -ForegroundColor Yellow }
689
- else { Write-Host "[1/3] Source 2/3: the python.org installer for $($OsArch.Label)..." -ForegroundColor Yellow }
690
- if (Install-PythonFromPythonOrg $OsArch) {
691
- $fresh = Resolve-PythonAfterInstall
692
- if ($fresh) { $PyExe = $fresh; $hasPython = $true }
693
- }
694
- }
695
-
696
- # 3. uv - brings its own CPython and needs neither an installer nor a pip.
697
- if (-not $hasPython) {
698
- if ($Lang -eq "ru") { Write-Host "[1/3] Источник 3/3: uv..." -ForegroundColor Yellow }
699
- else { Write-Host "[1/3] Source 3/3: uv..." -ForegroundColor Yellow }
700
- $UvExe = Install-UvWindows $OsArch
701
- if ($UvExe -and (Install-PythonViaUv $UvExe)) {
702
- $fresh = Resolve-PythonAfterInstall
703
- if ($fresh) { $PyExe = $fresh; $hasPython = $true }
704
- }
705
- }
706
-
707
- if ($hasPython) {
708
- if ($Lang -eq "ru") { Write-Host "[1/3] Python обнаружен: $PyExe" -ForegroundColor Green }
709
- else { Write-Host "[1/3] Python detected: $PyExe" -ForegroundColor Green }
710
- }
711
- }
712
-
713
- # Fail closed. Without a real interpreter the autostart entry points at nothing and
714
- # the node stays offline until a human reads the log - the exact state the
715
- # Microsoft Store alias produced, where the only trace was one torn log line.
716
- if (-not $hasPython -or -not $PyExe) {
717
- if ($Lang -eq "ru") {
718
- Write-Host "[!] Рабочий Python 3 не найден." -ForegroundColor Red
719
- Write-Host " Установите Python с python.org (НЕ из Microsoft Store) и повторите установку." -ForegroundColor Yellow
720
- Write-Host " Остановка: без интерпретатора автозапуск узла работать не будет." -ForegroundColor Yellow
721
- } else {
722
- Write-Host "[!] No working Python 3 found." -ForegroundColor Red
723
- Write-Host " Install Python from python.org (NOT from the Microsoft Store) and re-run." -ForegroundColor Yellow
724
- Write-Host " Stopping: an autostart entry without an interpreter can never work." -ForegroundColor Yellow
725
- }
726
- exit 1
727
- }
728
- if ("$PyExe" -like "*$WindowsAppsMarker*") {
729
- if ($Lang -eq "ru") {
730
- Write-Host "[!] Отказ: выбранный интерпретатор - ярлык Microsoft Store ($PyExe)." -ForegroundColor Red
731
- } else {
732
- Write-Host "[!] Refusing to use the Microsoft Store alias as the interpreter ($PyExe)." -ForegroundColor Red
733
- }
734
- exit 1
735
- }
736
-
737
- # Install dependencies.
738
- # The tunnel agent needs "websockets"; core/mcp_tools.py and core/server.py
739
- # are standard library only, so this is the single external requirement.
740
- if ($Lang -eq "ru") {
741
- Write-Host "[2/3] Проверка библиотек (websockets)..." -ForegroundColor Yellow
742
- } else {
743
- Write-Host "[2/3] Checking dependencies (websockets)..." -ForegroundColor Yellow
744
- }
745
-
746
- # Use the interpreter resolved above; fall back to PATH only if that failed.
747
- $pyRun = if ($PyExe) { $PyExe } else { "python" }
748
- $pyExeOnly = ($pyRun -split " ")[0]
749
-
750
- # The dependency step may have to SWITCH interpreters (the venv fallback), and
751
- # everything below - the Startup launcher, the watchdog task, the final "run
752
- # now" - must use the interpreter that can actually import websockets. Pinning
753
- # the pre-install choice here is what left a node autostarting against a Python
754
- # that could not import the one package the agent needs.
755
- $result = Install-WebsocketsFor -Python $pyExeOnly -Uv $UvExe
756
-
757
- # uv is only fetched when everything else failed. It was not needed for Python
758
- # itself on this machine, so downloading it up front would be an 18 MB cost for
759
- # a machine that never needs it - but a pip that cannot write anywhere is
760
- # precisely the case uv fixes, so the chain is retried once with it.
761
- if (-not $result.Ok -and -not $UvExe) {
762
- $UvExe = Install-UvWindows $OsArch
763
- if ($UvExe) { $result = Install-WebsocketsFor -Python $pyExeOnly -Uv $UvExe }
764
- }
765
-
766
- if ($result.Ok -and $result.Python) {
767
- $pyExeOnly = $result.Python
768
- $PyExe = $result.Python
769
- }
770
-
771
- if (-not $result.Ok) {
772
- if ($Lang -eq "ru") {
773
- Write-Host "[!] Не удалось установить websockets автоматически." -ForegroundColor Red
774
- Write-Host " Проверены: pip, pip --user, ensurepip, uv и venv." -ForegroundColor Yellow
775
- Write-Host " Выполните вручную: $pyExeOnly -m pip install websockets" -ForegroundColor Yellow
776
- } else {
777
- Write-Host "[!] Could not install websockets automatically." -ForegroundColor Red
778
- Write-Host " Tried: pip, pip --user, ensurepip, uv and a venv." -ForegroundColor Yellow
779
- Write-Host " Run manually: $pyExeOnly -m pip install websockets" -ForegroundColor Yellow
780
- }
781
- exit 1
782
- }
783
- if ($Lang -eq "ru") {
784
- Write-Host "[OK] websockets установлен ($pyExeOnly)." -ForegroundColor Green
785
- } else {
786
- Write-Host "[OK] websockets is installed ($pyExeOnly)." -ForegroundColor Green
787
- }
788
-
789
- $ConfigDir = "$env:USERPROFILE\.config\antigravity-mesh"
790
- if (!(Test-Path $ConfigDir)) { New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null }
791
-
792
- # Bootstrap project files if piped from irm/iex
793
- $ScriptDir = if ($MyInvocation.MyCommand -and $MyInvocation.MyCommand.Path) { Split-Path -Parent $MyInvocation.MyCommand.Path } else { $null }
794
- if (-not $ScriptDir -or -not (Test-Path "$ScriptDir\core\agent.py")) {
795
- $BootstrapDir = "$env:USERPROFILE\.gemini-computer-use"
796
- $CoreDir = "$BootstrapDir\core"
797
- $SkillsDir = "$BootstrapDir\skills"
798
- # The watchdog, the doctor and the updater are fetched too: the scheduled
799
- # tasks registered below run the first and the last, and a node whose agent
800
- # dies - or whose code goes stale - must heal and update itself without
801
- # anyone re-running the installer by hand.
802
- $OpsDir = "$BootstrapDir\ops\windows"
803
- if (!(Test-Path $CoreDir)) { New-Item -ItemType Directory -Path $CoreDir -Force | Out-Null }
804
- if (!(Test-Path $SkillsDir)) { New-Item -ItemType Directory -Path $SkillsDir -Force | Out-Null }
805
- if (!(Test-Path $OpsDir)) { New-Item -ItemType Directory -Path $OpsDir -Force | Out-Null }
806
-
807
- $files = @(
808
- "core/agent.py",
809
- "core/server.py",
810
- "core/mcp_tools.py",
811
- "core/web_share.py",
812
- "core/domain.py",
813
- "core/vitals.py",
814
- "core/updater.py",
815
- "core/version.py",
816
- "core/__init__.py",
817
- "skills/orchestrator.md",
818
- "ops/windows/agent-watchdog.ps1",
819
- "ops/windows/run-hidden.vbs",
820
- "ops/update.ps1",
821
- "ops/doctor.ps1"
822
- )
823
- foreach ($f in $files) {
824
- $dest = "$BootstrapDir\$($f -replace '/', '\')"
825
- try {
826
- Invoke-WebRequest -Uri "https://$Gateway/$f" -OutFile $dest -UseBasicParsing -ErrorAction Stop
827
- } catch {
828
- try {
829
- Invoke-WebRequest -Uri "https://raw.githubusercontent.com/LevRa7/Computer-use-for-Gemini-App-Web/main/$f" -OutFile $dest -UseBasicParsing
830
- } catch {}
831
- }
832
- }
833
- $ScriptDir = $BootstrapDir
834
- }
835
-
836
- function Print-McpBox($Title, $Url, $CopiedText, $BorderColor = "Green") {
837
- $MinLen = [Math]::Max($Title.Length, $Url.Length)
838
- if ($CopiedText) {
839
- $MinLen = [Math]::Max($MinLen, $CopiedText.Length)
840
- }
841
- $ContentWidth = [Math]::Max($MinLen + 4, 84)
842
- $Border = " +" + ("-" * $ContentWidth) + "+"
843
- $Empty = " |" + (" " * $ContentWidth) + "|"
844
-
845
- Write-Host $Border -ForegroundColor $BorderColor
846
-
847
- $padTitle = $ContentWidth - 2 - $Title.Length
848
- Write-Host " | " -NoNewline -ForegroundColor $BorderColor
849
- Write-Host $Title -NoNewline -ForegroundColor White
850
- Write-Host ((" " * $padTitle) + " |") -ForegroundColor $BorderColor
851
-
852
- Write-Host $Empty -ForegroundColor $BorderColor
853
-
854
- $padUrl = $ContentWidth - 2 - $Url.Length
855
- Write-Host " | " -NoNewline -ForegroundColor $BorderColor
856
- Write-Host $Url -NoNewline -ForegroundColor Yellow
857
- Write-Host ((" " * $padUrl) + " |") -ForegroundColor $BorderColor
858
-
859
- Write-Host $Empty -ForegroundColor $BorderColor
860
-
861
- if ($CopiedText) {
862
- $padCopy = $ContentWidth - 2 - $CopiedText.Length
863
- Write-Host " | " -NoNewline -ForegroundColor $BorderColor
864
- Write-Host $CopiedText -NoNewline -ForegroundColor Green
865
- Write-Host ((" " * $padCopy) + " |") -ForegroundColor $BorderColor
866
- }
867
-
868
- Write-Host $Border -ForegroundColor $BorderColor
869
- }
870
-
871
- if ($Mode -eq "standalone") {
872
- if ($Lang -eq "ru") {
873
- Write-Host "=== Запуск в режиме Local Standalone на порту $Port ===" -ForegroundColor Blue
874
- } else {
875
- Write-Host "=== Starting in Local Standalone Mode on port $Port ===" -ForegroundColor Blue
876
- }
877
- Start-Process $pyExeOnly -ArgumentList "-m core.server --host 127.0.0.1 --port=$Port" -WorkingDirectory $ScriptDir -WindowStyle Hidden
878
- $LocalUrl = "http://localhost:$Port/sse"
879
- $Copied = $false
880
- try {
881
- Set-Clipboard -Value $LocalUrl -ErrorAction Stop
882
- $Copied = $true
883
- } catch {
884
- try { $LocalUrl | clip.exe 2>$null; $Copied = $true } catch {}
885
- }
886
-
887
- Clear-Host
888
-
889
- if ($Lang -eq "ru") {
890
- Write-Host ""
891
- $copyMsg = if ($Copied) { "[OK] ССЫЛКА СКОПИРОВАНА В БУФЕР ОБМЕНА! (Вставьте через Ctrl+V)" } else { "Скопируйте ссылку выше" }
892
- Print-McpBox "ССЫЛКА ЛОКАЛЬНОГО MCP-СЕРВЕРА (СКОПИРУЙТЕ):" $LocalUrl $copyMsg
893
- Write-Host ""
894
- Write-Host " Куда вставлять ссылку в Google Gemini:" -ForegroundColor Yellow
895
- Write-Host " 1. Откройте в браузере: https://gemini.google.com/spark/apps" -ForegroundColor Cyan
896
- Write-Host " 2. Нажмите 'Добавить приложение' (Add app / Настройки MCP)"
897
- Write-Host " 3. Вставьте скопированную ссылку в поле 'URL сервера' (Ctrl+V) и нажмите Подключить." -ForegroundColor White
898
- Write-Host ""
899
- } else {
900
- Write-Host ""
901
- $copyMsg = if ($Copied) { "[OK] URL COPIED TO CLIPBOARD! (Press Ctrl+V to paste)" } else { "Copy the URL above" }
902
- Print-McpBox "LOCAL MCP SERVER URL (COPY THIS):" $LocalUrl $copyMsg
903
- Write-Host ""
904
- Write-Host " Where to paste this URL in Google Gemini:" -ForegroundColor Yellow
905
- Write-Host " 1. Open in your browser: https://gemini.google.com/spark/apps" -ForegroundColor Cyan
906
- Write-Host " 2. Click 'Add app' (or navigate to MCP settings)"
907
- Write-Host " 3. Paste the URL into the 'Server URL' field (Ctrl+V) and click Connect." -ForegroundColor White
908
- Write-Host ""
909
- }
910
- exit 0
911
- }
912
-
913
- # Gateway Tunnel Mode
914
- if ([string]::IsNullOrWhiteSpace($User)) {
915
- $User = $Hostname
916
- }
917
-
918
- if ($Lang -eq "ru") {
919
- Write-Host "[2/3] Регистрация узла '$User' на общем домене ($Gateway)..." -ForegroundColor Yellow
920
- } else {
921
- Write-Host "[2/3] Registering node '$User' on the shared domain ($Gateway)..." -ForegroundColor Yellow
922
- }
923
- $detectedMac = ""
924
- try {
925
- $nic = Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "IPEnabled = 'TRUE'" -ErrorAction SilentlyContinue | Select-Object -First 1
926
- if ($nic -and $nic.MACAddress) {
927
- $detectedMac = $nic.MACAddress
928
- }
929
- } catch {}
930
- if ([string]::IsNullOrWhiteSpace($detectedMac)) {
931
- try {
932
- # Use the interpreter resolved above: a bare "python" may be the Microsoft
933
- # Store App Execution Alias, which is on PATH but runs nothing.
934
- $res = & $pyExeOnly -c "import uuid; print(':'.join(['{:02x}'.format((uuid.getnode() >> ele) & 0xff) for ele in range(0,8*6,8)][::-1]))" 2>&1
935
- if ($LASTEXITCODE -eq 0 -and $res) {
936
- $detectedMac = $res.ToString().Trim()
937
- }
938
- } catch {}
939
- }
940
-
941
- $detectedOs = "Windows $([System.Environment]::OSVersion.Version.ToString())"
942
-
943
- $regPayloadObj = @{
944
- username = $User
945
- auto_suffix = $true
946
- mac_address = $detectedMac
947
- os = $detectedOs
948
- # The architecture is part of the device fingerprint: two machines behind one
949
- # NAT can share a MAC-derived identity only if their OS and architecture agree,
950
- # and a gateway operator needs to know which build a node is running when a
951
- # wheel-related failure is reported.
952
- arch = $OsArch.Key
953
- }
954
- if (-not [string]::IsNullOrWhiteSpace($Token)) {
955
- $regPayloadObj.token = $Token
956
- }
957
- $regPayload = $regPayloadObj | ConvertTo-Json
958
- # Registration is the one step that cannot be skipped or guessed: without a real
959
- # node name and token the agent can never connect. This used to run under
960
- # $ErrorActionPreference = "Continue", so a DNS/TLS/HTTP failure printed an error,
961
- # the script carried on, wrote an EMPTY agent.env, installed a permanently broken
962
- # Startup launcher, printed a link with empty user/token and claimed that link had
963
- # been copied - leaving the operator with a node that retries a dead endpoint after
964
- # every login. Fail closed instead, before anything on disk is touched.
965
- try {
966
- $response = Invoke-RestMethod -Uri "https://$Gateway/api/register" -Method Post `
967
- -Body $regPayload -ContentType "application/json" -ErrorAction Stop
968
- } catch {
969
- if ($Lang -eq "ru") {
970
- Write-Host "[!] Не удалось зарегистрировать узел на '$Gateway': $($_.Exception.Message)" -ForegroundColor Red
971
- Write-Host " Проверьте, что домен шлюза разрешается в DNS и сервер доступен, затем повторите:" -ForegroundColor Yellow
972
- Write-Host " .\install.ps1 -Gateway <ваш-домен>" -ForegroundColor Yellow
973
- Write-Host " Ничего не установлено: файл конфигурации и автозапуск не создавались." -ForegroundColor Yellow
974
- } else {
975
- Write-Host "[!] Could not register this node on '$Gateway': $($_.Exception.Message)" -ForegroundColor Red
976
- Write-Host " Check that the gateway domain resolves in DNS and the server is reachable, then re-run:" -ForegroundColor Yellow
977
- Write-Host " .\install.ps1 -Gateway <your-domain>" -ForegroundColor Yellow
978
- Write-Host " Nothing was installed: no config file and no autostart were created." -ForegroundColor Yellow
979
- }
980
- exit 1
981
- }
982
-
983
- $AssignedUser = $response.username
984
- $AssignedToken = $response.token
985
-
986
- if ([string]::IsNullOrWhiteSpace($AssignedUser) -or [string]::IsNullOrWhiteSpace($AssignedToken)) {
987
- if ($Lang -eq "ru") {
988
- Write-Host "[!] Шлюз '$Gateway' ответил без имени узла или токена - установка остановлена." -ForegroundColor Red
989
- } else {
990
- Write-Host "[!] Gateway '$Gateway' answered without a node name or token - stopping." -ForegroundColor Red
991
- }
992
- exit 1
993
- }
994
-
995
- # ------------------------------------------------------------------------------
996
- # SHARED-DOMAIN CONTRACT: one public domain serves every node and the node is
997
- # selected by ?user=<node-name>. Never build a per-device subdomain URL: each
998
- # extra hostname needs its own DNS record and TLS SAN, and a missing SAN makes
999
- # the Gemini client fail with an opaque "cannot connect to host" error.
1000
- # The gateway returns this canonical URL; the fallback keeps older gateways
1001
- # working without ever advertising a subdomain.
1002
- # ------------------------------------------------------------------------------
1003
- $PublicBaseUrl = "https://$Gateway"
1004
- $SseUrl = $response.sse_url
1005
- if ([string]::IsNullOrWhiteSpace($SseUrl)) {
1006
- $SseUrl = "$PublicBaseUrl/sse?user=$AssignedUser&token=$AssignedToken"
1007
- }
1008
-
1009
- # Quick Copy to Clipboard
1010
- $Copied = $false
1011
- try {
1012
- Set-Clipboard -Value $SseUrl -ErrorAction Stop
1013
- $Copied = $true
1014
- } catch {
1015
- try {
1016
- $SseUrl | clip.exe 2>$null
1017
- $Copied = $true
1018
- } catch {}
1019
- }
1020
-
1021
- if ($Lang -eq "ru") {
1022
- Write-Host "[3/3] Настройка автозапуска в Windows..." -ForegroundColor Green
1023
- } else {
1024
- Write-Host "[3/3] Configuring Windows background autostart..." -ForegroundColor Green
1025
- }
1026
- $envFile = "$ConfigDir\agent.env"
1027
- @"
1028
- MESH_GATEWAY=$Gateway
1029
- MESH_USER=$AssignedUser
1030
- MESH_TOKEN=$AssignedToken
1031
- "@ | Out-File -FilePath $envFile -Encoding utf8
1032
-
1033
- # agent.env carries only the legacy MESH_GATEWAY, which core/domain.py honours for
1034
- # the tunnel but never for the links a node publishes. The domain file is what
1035
- # keeps share links on the gateway this node is registered on, so it is written
1036
- # down before the agent is started.
1037
- Write-DomainFile -Domain $Gateway -Path $DomainFilePath
1038
-
1039
- # Start agent in background
1040
- $env:MESH_GATEWAY = $Gateway
1041
- $env:MESH_USER = $AssignedUser
1042
- $env:MESH_TOKEN = $AssignedToken
1043
-
1044
- # Create Windows Startup launcher for persistent autostart.
1045
- # The interpreter is pinned by full path because PATH may hold the Microsoft
1046
- # Store alias, which stops working after a Store repair or an update. "-u"
1047
- # keeps stdout unbuffered, so a killed process cannot leave a torn log line as
1048
- # the only evidence of what happened.
1049
- if (-not $pyExeOnly -or "$pyExeOnly" -like "*$WindowsAppsMarker*") {
1050
- Write-Host "[!] Refusing to write an autostart entry for '$pyExeOnly'." -ForegroundColor Red
1051
- exit 1
1052
- }
1053
- $startupDir = [Environment]::GetFolderPath("Startup")
1054
- $startupVbs = "$startupDir\antigravity-agent.vbs"
1055
- $pyExeForVbs = $pyExeOnly
1056
- $agentLog = "$ConfigDir\agent.log"
1057
- @"
1058
- Set WshShell = CreateObject("WScript.Shell")
1059
- WshShell.Environment("PROCESS")("MESH_GATEWAY") = "$Gateway"
1060
- WshShell.Environment("PROCESS")("MESH_USER") = "$AssignedUser"
1061
- WshShell.Environment("PROCESS")("MESH_TOKEN") = "$AssignedToken"
1062
- WshShell.CurrentDirectory = "$ScriptDir"
1063
- ' Log the output so a silent autostart failure can be diagnosed later.
1064
- WshShell.Run "cmd /c """"$pyExeForVbs"" -u -m core.agent >> """"$agentLog"""" 2>&1""", 0, False
1065
- "@ | Out-File -FilePath $startupVbs -Encoding Unicode
1066
-
1067
- # A Startup entry runs once per logon: it cannot recover a node whose agent died
1068
- # (crash, Windows Update reboot, broken dependency). A Scheduled Task repeats the
1069
- # watchdog every five minutes, so a dead agent comes back on its own.
1070
- #
1071
- # Both tasks are started through ops\windows\run-hidden.vbs rather than
1072
- # powershell.exe directly. A task has no console of its own, so Windows allocates
1073
- # one for powershell.exe and only then honours "-WindowStyle Hidden": the window is
1074
- # created, drawn and hidden again, and the user sees a black window flash every
1075
- # five minutes. wscript.exe is a GUI host - it never allocates a console - and
1076
- # WshShell.Run with window style 0 passes SW_HIDE to the child, so nothing is ever
1077
- # drawn. The task argument names the node script relative to the payload, which
1078
- # run-hidden.vbs resolves from its own location, so the action keeps working if the
1079
- # payload moves.
1080
- $watchdog = Join-Path $ScriptDir 'ops\windows\agent-watchdog.ps1'
1081
- $runHidden = Join-Path $ScriptDir 'ops\windows\run-hidden.vbs'
1082
- if (Test-Path -LiteralPath $watchdog) {
1083
- # Two spellings of the same action, because the two registration paths parse
1084
- # quotes differently: schtasks.exe needs the quotes around the script path
1085
- # backslash-escaped (otherwise a profile path with a space is split into two
1086
- # arguments and the task is rejected outright), while the ScheduledTasks
1087
- # cmdlets take the argument literally.
1088
- if (Test-Path -LiteralPath $runHidden) {
1089
- $watchdogHost = 'wscript.exe'
1090
- $watchdogArguments = '"' + $runHidden + '" "ops\windows\agent-watchdog.ps1" -Quiet'
1091
- $watchdogTaskCommand = 'wscript.exe \"' + $runHidden + '\" \"ops\windows\agent-watchdog.ps1\" -Quiet'
1092
- } else {
1093
- # A payload without the launcher still self-heals; it just flashes. Saying
1094
- # so is better than leaving the operator with a window they cannot explain.
1095
- if ($Lang -eq "ru") {
1096
- Write-Host "[!] $runHidden не найден: задача-сторож будет запускаться напрямую и мелькать окном." -ForegroundColor Yellow
1097
- } else {
1098
- Write-Host "[!] $runHidden is missing: the watchdog task will run PowerShell directly and flash a window." -ForegroundColor Yellow
1099
- }
1100
- $watchdogHost = 'powershell.exe'
1101
- $watchdogArguments = '-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + $watchdog + '" -Quiet'
1102
- $watchdogTaskCommand = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"' + $watchdog + '\" -Quiet'
1103
- }
1104
- $watchdogRegistered = $false
1105
- $watchdogError = ''
1106
- # schtasks.exe is tried first because it registers a task for the CURRENT user
1107
- # without elevation, while Register-ScheduledTask needs write access to the task
1108
- # store (denied for a normal user on a hardened or domain-joined machine - that
1109
- # was measured on the machine this watchdog was built for). The minute schedule
1110
- # repeats indefinitely, so logon is already covered by the Startup entry above
1111
- # and needs no second trigger here.
1112
- $watchdogError = (& schtasks.exe /Create /TN 'AntigravityMeshWatchdog' /TR $watchdogTaskCommand `
1113
- /SC MINUTE /MO 5 /F 2>&1 | Out-String).Trim()
1114
- if ($LASTEXITCODE -eq 0) { $watchdogRegistered = $true }
1115
- if (-not $watchdogRegistered) {
1116
- try {
1117
- $watchdogAction = New-ScheduledTaskAction -Execute $watchdogHost -Argument $watchdogArguments -WorkingDirectory $ScriptDir
1118
- # No -RepetitionDuration on purpose: PowerShell serialises
1119
- # [TimeSpan]::MaxValue as P99999999DT23H59M59S, which the Task Scheduler
1120
- # schema rejects outright; an omitted duration means "repeat
1121
- # indefinitely", which is exactly what this needs.
1122
- $watchdogTriggers = @(
1123
- (New-ScheduledTaskTrigger -AtLogOn),
1124
- (New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Minutes 5))
1125
- )
1126
- $watchdogSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
1127
- -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 10)
1128
- Register-ScheduledTask -TaskName 'AntigravityMeshWatchdog' -Action $watchdogAction `
1129
- -Trigger $watchdogTriggers -Settings $watchdogSettings -Force `
1130
- -Description 'Restarts the Antigravity Mesh node agent when it dies.' | Out-Null
1131
- $watchdogRegistered = $true
1132
- $watchdogError = ''
1133
- } catch {
1134
- $watchdogRegistered = $false
1135
- $watchdogError = $_.Exception.Message
1136
- }
1137
- }
1138
- if ($watchdogRegistered) {
1139
- if ($Lang -eq "ru") {
1140
- Write-Host "[OK] Задача-сторож 'AntigravityMeshWatchdog' зарегистрирована (каждые 5 минут)." -ForegroundColor Green
1141
- } else {
1142
- Write-Host "[OK] Watchdog task 'AntigravityMeshWatchdog' registered (every 5 minutes)." -ForegroundColor Green
1143
- }
1144
- } else {
1145
- if ($Lang -eq "ru") {
1146
- Write-Host "[!] Не удалось зарегистрировать задачу-сторож. Автозапуск при входе в систему всё равно создан." -ForegroundColor Yellow
1147
- Write-Host " Запустить проверку вручную: $watchdog" -ForegroundColor Yellow
1148
- } else {
1149
- Write-Host "[!] Could not register the watchdog task. The Startup entry still starts the agent at logon." -ForegroundColor Yellow
1150
- Write-Host " Run the check by hand: $watchdog" -ForegroundColor Yellow
1151
- }
1152
- if ($watchdogError) { Write-Host " $watchdogError" -ForegroundColor DarkYellow }
1153
- }
1154
- } elseif ($Lang -eq "ru") {
1155
- Write-Host "[!] $watchdog не найден: самовосстановления у узла не будет." -ForegroundColor Yellow
1156
- } else {
1157
- Write-Host "[!] $watchdog not found: the node has no self-healing task." -ForegroundColor Yellow
1158
- }
1159
-
1160
- # The watchdog above recovers a node that is DOWN; this task moves a node that is
1161
- # merely OUT OF DATE. A machine nobody logs into would otherwise keep running
1162
- # whatever was installed the day it was set up, so the updater checks the newest
1163
- # GitHub release once a day, installs it with a rollback backup and lets its
1164
- # helper bring the agent back on the new code. The agent also checks for itself
1165
- # (MESH_UPDATE_* in core/updater.py); this task is what covers a node whose agent
1166
- # is not running at 03:30.
1167
- $updaterScript = Join-Path $ScriptDir 'ops\update.ps1'
1168
- if (Test-Path -LiteralPath $updaterScript) {
1169
- # Same windowless host as the watchdog: a daily flash is still a flash.
1170
- if (Test-Path -LiteralPath $runHidden) {
1171
- $updaterHost = 'wscript.exe'
1172
- $updaterArguments = '"' + $runHidden + '" "ops\update.ps1" -Quiet'
1173
- $updaterTaskCommand = 'wscript.exe \"' + $runHidden + '\" \"ops\update.ps1\" -Quiet'
1174
- } else {
1175
- $updaterHost = 'powershell.exe'
1176
- $updaterArguments = '-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + $updaterScript + '" -Quiet'
1177
- $updaterTaskCommand = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"' + $updaterScript + '\" -Quiet'
1178
- }
1179
- $updaterRegistered = $false
1180
- $updaterError = ''
1181
- # schtasks.exe first, for the same reason as the watchdog: it needs no write
1182
- # access to the task store, which a normal user does not have on a hardened
1183
- # or domain-joined machine.
1184
- $updaterError = (& schtasks.exe /Create /TN 'AntigravityMeshUpdater' /TR $updaterTaskCommand `
1185
- /SC DAILY /MO 1 /ST 03:30 /F 2>&1 | Out-String).Trim()
1186
- if ($LASTEXITCODE -eq 0) { $updaterRegistered = $true }
1187
- if (-not $updaterRegistered) {
1188
- try {
1189
- $updaterAction = New-ScheduledTaskAction -Execute $updaterHost -Argument $updaterArguments -WorkingDirectory $ScriptDir
1190
- $updaterTriggers = @((New-ScheduledTaskTrigger -Daily -At '03:30'))
1191
- $updaterSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
1192
- -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 30)
1193
- Register-ScheduledTask -TaskName 'AntigravityMeshUpdater' -Action $updaterAction `
1194
- -Trigger $updaterTriggers -Settings $updaterSettings -Force `
1195
- -Description 'Updates the Antigravity Mesh node from the newest GitHub release.' | Out-Null
1196
- $updaterRegistered = $true
1197
- $updaterError = ''
1198
- } catch {
1199
- $updaterRegistered = $false
1200
- $updaterError = $_.Exception.Message
1201
- }
1202
- }
1203
- if ($updaterRegistered) {
1204
- if ($Lang -eq "ru") {
1205
- Write-Host "[OK] Задача обновления 'AntigravityMeshUpdater' зарегистрирована (ежедневно в 03:30)." -ForegroundColor Green
1206
- Write-Host " Проверить сейчас: powershell -File `"$updaterScript`" -Check" -ForegroundColor DarkGray
1207
- } else {
1208
- Write-Host "[OK] Update task 'AntigravityMeshUpdater' registered (daily at 03:30)." -ForegroundColor Green
1209
- Write-Host " Check right now: powershell -File `"$updaterScript`" -Check" -ForegroundColor DarkGray
1210
- }
1211
- } else {
1212
- if ($Lang -eq "ru") {
1213
- Write-Host "[!] Не удалось зарегистрировать задачу обновления. Узел всё равно проверяет версию сам, пока агент запущен." -ForegroundColor Yellow
1214
- Write-Host " Запустить проверку вручную: powershell -File `"$updaterScript`" -Check" -ForegroundColor Yellow
1215
- } else {
1216
- Write-Host "[!] Could not register the update task. A running agent still checks for releases itself." -ForegroundColor Yellow
1217
- Write-Host " Check by hand: powershell -File `"$updaterScript`" -Check" -ForegroundColor Yellow
1218
- }
1219
- if ($updaterError) { Write-Host " $updaterError" -ForegroundColor DarkYellow }
1220
- }
1221
- } elseif ($Lang -eq "ru") {
1222
- Write-Host "[!] $updaterScript не найден: автоматического обновления у узла не будет." -ForegroundColor Yellow
1223
- } else {
1224
- Write-Host "[!] $updaterScript not found: the node has no automatic update task." -ForegroundColor Yellow
1225
- }
1226
-
1227
- # Launch now
1228
- Start-Process $pyExeOnly -ArgumentList "-u -m core.agent" -WorkingDirectory $ScriptDir -WindowStyle Hidden
1229
-
1230
- Clear-Host
1231
-
1232
- if ($Lang -eq "ru") {
1233
- Write-Host ""
1234
- $copyMsg = if ($Copied) { "[OK] ССЫЛКА СКОПИРОВАНА В БУФЕР ОБМЕНА! (Вставьте через Ctrl+V)" } else { "Скопируйте ссылку выше" }
1235
- Print-McpBox "ССЫЛКА MCP-СЕРВЕРА ДЛЯ ПОДКЛЮЧЕНИЯ (СКОПИРУЙТЕ):" $SseUrl $copyMsg
1236
- Write-Host ""
1237
- Write-Host " Куда вставлять ссылку в Google Gemini:" -ForegroundColor Yellow
1238
- Write-Host " 1. Откройте в браузере: https://gemini.google.com/spark/apps" -ForegroundColor Cyan
1239
- Write-Host " 2. Нажмите 'Добавить приложение' (Add app / Настройки MCP)"
1240
- Write-Host " 3. Вставьте скопированную ссылку в поле 'URL сервера' (Ctrl+V) и нажмите Подключить." -ForegroundColor White
1241
- Write-Host ""
1242
- } else {
1243
- Write-Host ""
1244
- $copyMsg = if ($Copied) { "[OK] URL COPIED TO CLIPBOARD! (Press Ctrl+V to paste)" } else { "Copy the URL above" }
1245
- Print-McpBox "MCP SERVER URL (COPY THIS):" $SseUrl $copyMsg
1246
- Write-Host ""
1247
- Write-Host " Where to paste this URL in Google Gemini:" -ForegroundColor Yellow
1248
- Write-Host " 1. Open in your browser: https://gemini.google.com/spark/apps" -ForegroundColor Cyan
1249
- Write-Host " 2. Click 'Add app' (or navigate to MCP settings)"
1250
- Write-Host " 3. Paste the URL into the 'Server URL' field (Ctrl+V) and click Connect." -ForegroundColor White
1251
- Write-Host ""
1252
- }
1253
-
1254
- # ------------------------------------------------------------------------------
1255
- # Post-install check. An install that looks successful while the agent never
1256
- # connects is what leaves a client with an opaque frontend error, so the node's
1257
- # state is verified here and reported with the interpreter and the log to read.
1258
- #
1259
- # Deliberately no non-zero exit code: install-gui.ps1 only extracts the MCP URL
1260
- # from a successful run, and that link matters most exactly when the node is not
1261
- # up yet. The watchdog task retries every five minutes in the meantime.
1262
- # ------------------------------------------------------------------------------
1263
- $NodeOnline = $false
1264
- for ($attempt = 1; $attempt -le 15; $attempt++) {
1265
- Start-Sleep -Seconds 2
1266
- try {
1267
- $health = Invoke-RestMethod -Uri "$PublicBaseUrl/health?user=$AssignedUser" -TimeoutSec 10 -ErrorAction Stop
1268
- if ($health.node_online) { $NodeOnline = $true; break }
1269
- } catch {}
1270
- }
1271
- if ($NodeOnline) {
1272
- if ($Lang -eq "ru") {
1273
- Write-Host "[OK] Узел '$AssignedUser' на связи со шлюзом." -ForegroundColor Green
1274
- } else {
1275
- Write-Host "[OK] Node '$AssignedUser' is online on the gateway." -ForegroundColor Green
1276
- }
1277
- } elseif ($Lang -eq "ru") {
1278
- Write-Host "[!] Узел '$AssignedUser' не вышел на связь за 30 с." -ForegroundColor Red
1279
- Write-Host " Интерпретатор автозапуска: $pyExeOnly" -ForegroundColor Yellow
1280
- Write-Host " Смотрите хвост $agentLog и запустите .\ops\doctor.ps1" -ForegroundColor Yellow
1281
- Write-Host " Задача 'AntigravityMeshWatchdog' повторит попытку через 5 минут." -ForegroundColor Yellow
1282
- } else {
1283
- Write-Host "[!] Node '$AssignedUser' did not come online within 30 s." -ForegroundColor Red
1284
- Write-Host " Autostart interpreter: $pyExeOnly" -ForegroundColor Yellow
1285
- Write-Host " Read the tail of $agentLog and run .\ops\doctor.ps1" -ForegroundColor Yellow
1286
- Write-Host " The 'AntigravityMeshWatchdog' task retries every 5 minutes." -ForegroundColor Yellow
1287
- }