gemini-computer-use 0.2.4 → 0.4.1

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.
Files changed (41) hide show
  1. package/README.md +733 -16
  2. package/bin/cli.js +112 -17
  3. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  4. package/core/__pycache__/agent.cpython-313.pyc +0 -0
  5. package/core/__pycache__/code_cache.cpython-313.pyc +0 -0
  6. package/core/__pycache__/mcp_tools.cpython-313.pyc +0 -0
  7. package/core/__pycache__/schemas.cpython-313.pyc +0 -0
  8. package/core/__pycache__/server.cpython-313.pyc +0 -0
  9. package/core/__pycache__/vitals.cpython-313.pyc +0 -0
  10. package/core/__pycache__/web_share.cpython-313.pyc +0 -0
  11. package/core/agent.py +503 -18
  12. package/core/agent_harness.py +9 -2
  13. package/core/code_cache.py +15 -3
  14. package/core/device.py +2400 -0
  15. package/core/domain.py +303 -0
  16. package/core/mcp_tools.py +1840 -112
  17. package/core/policies.py +35 -5
  18. package/core/server.py +78 -32
  19. package/core/termux.py +916 -0
  20. package/core/updater.py +1713 -0
  21. package/core/version.py +31 -0
  22. package/core/vitals.py +66 -2
  23. package/core/web_share.py +814 -0
  24. package/gateway.py +1469 -98
  25. package/install-gui.cmd +40 -0
  26. package/install-gui.ps1 +1882 -0
  27. package/install-gui.strings.json +211 -0
  28. package/install.ps1 +960 -92
  29. package/install.sh +1214 -81
  30. package/ops/doctor.ps1 +735 -0
  31. package/ops/mesh-domain.sh +86 -0
  32. package/ops/nginx/README.md +77 -0
  33. package/ops/nginx/antigravity-mesh-mcp.conf +172 -0
  34. package/ops/nginx/mesh-domain.vhost.template +58 -0
  35. package/ops/nginx/render-domain.sh +123 -0
  36. package/ops/update.ps1 +373 -0
  37. package/ops/windows/README.md +298 -0
  38. package/ops/windows/agent-watchdog.ps1 +575 -0
  39. package/ops/windows/run-hidden.vbs +64 -0
  40. package/package.json +6 -2
  41. package/skills/orchestrator.md +12 -0
@@ -0,0 +1,575 @@
1
+ # ==============================================================================
2
+ # Antigravity Mesh - Windows agent watchdog (single shot supervisor)
3
+ #
4
+ # Meant to be run every 5 minutes by the Scheduled Task "AntigravityMeshWatchdog".
5
+ # It answers one question: is this node's agent alive, and if it is not, start it
6
+ # exactly the way the logon launcher would?
7
+ #
8
+ # WHY THIS EXISTS
9
+ # After a Windows Update reboot the logon launcher silently did nothing. The
10
+ # interpreter pinned in the Startup .vbs was the Microsoft Store App Execution
11
+ # Alias <...>\WindowsApps\python.exe: a file that exists and can be launched, but
12
+ # runs no Python at all. The only trace was a truncated "Python " line appended to
13
+ # agent.log without a trailing newline, and the node stayed offline for ~29 hours
14
+ # while the Gemini client showed a generic frontend error. This watchdog refuses
15
+ # that interpreter outright, and recovers the node from a pinned, verified one.
16
+ #
17
+ # WHAT IT DOES, IN ORDER
18
+ # 1. reads <ConfigDir>\agent.env (MESH_GATEWAY / MESH_USER / MESH_TOKEN);
19
+ # 2. reads the Startup launcher and takes the interpreter from its
20
+ # WshShell.Run line and the payload directory from WshShell.CurrentDirectory;
21
+ # 3. refuses (exit 2) when the interpreter is missing, is not a file, or lives
22
+ # under \WindowsApps\;
23
+ # 4. probes liveness: (a) a fresh <ConfigDir>\agent.heartbeat whose ts is newer
24
+ # than -GraceSeconds (default 60), else (b) the gateway's
25
+ # /health?user=<node> reporting node_online=true, else (c) a verified agent
26
+ # process - a python.exe/pythonw.exe/py.exe running "-m core.agent", or the
27
+ # cmd.exe launcher wrapper that also carries the agent.log redirect. A
28
+ # process that merely MENTIONS core.agent (a PowerShell session, a grep)
29
+ # never counts, or a dead node would look alive forever;
30
+ # 5. when the node is dead, appends exactly one diagnostic line to agent.log,
31
+ # starts the agent hidden with the same command line / environment / working
32
+ # directory contract as the .vbs, waits up to 20 s for liveness and logs the
33
+ # outcome.
34
+ #
35
+ # EXIT CODES
36
+ # 0 the node is online (already was, or was recovered by this run), or another
37
+ # watchdog instance is currently handling it
38
+ # 2 configuration / launcher problem: agent.env, the Startup .vbs or the pinned
39
+ # interpreter is missing, unusable, or is the Microsoft Store alias
40
+ # 3 the node is still offline after the recovery attempt
41
+ #
42
+ # PARAMETERS
43
+ # -ConfigDir where agent.env / agent.log / agent.heartbeat live.
44
+ # Default: %USERPROFILE%\.config\antigravity-mesh
45
+ # -LogFile this script's journal. Default: <ConfigDir>\watchdog.log
46
+ # -GraceSeconds heartbeat freshness threshold, in seconds, default 60: a
47
+ # heartbeat whose ts is older than this is not fresh. The agent
48
+ # beats every 15 s, so the default tolerates three missed beats.
49
+ # -Quiet write to -LogFile only, print nothing to the console.
50
+ #
51
+ # IDEMPOTENT BY DESIGN
52
+ # * A watchdog that sees a live node (or a verified agent process) never starts
53
+ # a second one.
54
+ # * A named mutex serialises overlapping runs, so a slow run cannot double-start
55
+ # the agent and the script never depends on the task scheduler being up.
56
+ # * The token is never printed and never written to any log; it is masked as
57
+ # "****" wherever a human would see it.
58
+ #
59
+ # This file is deliberately ASCII-only: Windows PowerShell 5.1 reads a BOM-less
60
+ # script with the ANSI code page, so non-ASCII text here is a portability bug.
61
+ #
62
+ # Usage:
63
+ # .\agent-watchdog.ps1
64
+ # .\agent-watchdog.ps1 -ConfigDir "$env:USERPROFILE\.config\antigravity-mesh"
65
+ # .\agent-watchdog.ps1 -GraceSeconds 120
66
+ # .\agent-watchdog.ps1 -Quiet -LogFile C:\logs\watchdog.log
67
+ # ==============================================================================
68
+ [CmdletBinding()]
69
+ param(
70
+ [string]$ConfigDir = '',
71
+ [string]$LogFile = '',
72
+ [int]$GraceSeconds = 60,
73
+ [switch]$Quiet
74
+ )
75
+
76
+ $ErrorActionPreference = 'Continue'
77
+
78
+ # ------------------------------------------------------------------ logging ---
79
+ # Two logs are in play and they must never be confused:
80
+ # * agent.log - the running agent's stdout/stderr; the watchdog appends ONE
81
+ # diagnostic line to it so the reason for a restart is visible
82
+ # next to the failure that caused it;
83
+ # * watchdog.log - this script's own journal (-LogFile), append-only.
84
+ $script:QuietMode = [bool]$Quiet
85
+ $script:LogTarget = ''
86
+ $script:Utf8NoBom = New-Object System.Text.UTF8Encoding -ArgumentList @($false)
87
+
88
+ function Write-LogLine {
89
+ param([string]$Text)
90
+ if ([string]::IsNullOrEmpty($script:LogTarget)) { return }
91
+ try {
92
+ $stamp = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ssK')
93
+ [System.IO.File]::AppendAllText($script:LogTarget, "[$stamp] $Text" + [Environment]::NewLine, $script:Utf8NoBom)
94
+ } catch { }
95
+ }
96
+
97
+ function Write-Note {
98
+ param([string]$Text)
99
+ Write-LogLine $Text
100
+ if (-not $script:QuietMode) { Write-Host $Text }
101
+ }
102
+
103
+ function Stop-WithConfigError {
104
+ param([string]$Message)
105
+ Write-Note ("CONFIG ERROR: " + $Message)
106
+ exit 2
107
+ }
108
+
109
+ function Add-AgentLogLine {
110
+ param([string]$Line)
111
+ $path = Join-Path $ConfigDir 'agent.log'
112
+ $single = ($Line -replace '[\r\n]+', ' ').Trim()
113
+ try {
114
+ [System.IO.File]::AppendAllText($path, $single + [Environment]::NewLine, $script:Utf8NoBom)
115
+ return $true
116
+ } catch {
117
+ Write-Note ("could not append to " + $path + ": " + $_.Exception.Message)
118
+ return $false
119
+ }
120
+ }
121
+
122
+ # ------------------------------------------------------------------- secrets --
123
+ # Anything a human can read goes through here. The literal secret is replaced when
124
+ # it is long enough to be unambiguous, and the two shipped shapes of the token
125
+ # (agent.env KEY=value and the .vbs Environment(...) call) are always masked, so a
126
+ # short token cannot leak through pattern matching alone.
127
+ function Hide-Secret {
128
+ param([string]$Text, [string]$Secret)
129
+ if ([string]::IsNullOrEmpty($Text)) { return $Text }
130
+ $result = $Text
131
+ if ($Secret -and $Secret.Length -ge 4) { $result = $result.Replace($Secret, '****') }
132
+ $result = [regex]::Replace($result, '(?im)^(\s*MESH_TOKEN\s*=\s*).*$', '${1}****')
133
+ $result = [regex]::Replace($result, '(?im)(MESH_TOKEN"\)\s*=\s*").*?(")', '${1}****${2}')
134
+ return $result
135
+ }
136
+
137
+ # ---------------------------------------------------------------- file input --
138
+ function Read-TextFile {
139
+ param([string]$Path)
140
+ $bytes = [System.IO.File]::ReadAllBytes($Path)
141
+ if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
142
+ return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3)
143
+ }
144
+ if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
145
+ return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
146
+ }
147
+ if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) {
148
+ return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2)
149
+ }
150
+ # No BOM: UTF-16 without a BOM is full of zero bytes, UTF-8/ANSI is not. The
151
+ # launcher is written as UTF-16 ("Unicode") by install.ps1, but a hand-edited
152
+ # file may be ANSI or UTF-8, and both must be readable.
153
+ $hasNul = $false
154
+ $limit = [Math]::Min($bytes.Length, 512)
155
+ for ($i = 0; $i -lt $limit; $i++) {
156
+ if ($bytes[$i] -eq 0) { $hasNul = $true; break }
157
+ }
158
+ if ($hasNul) { return [System.Text.Encoding]::Unicode.GetString($bytes) }
159
+ return [System.Text.Encoding]::UTF8.GetString($bytes)
160
+ }
161
+
162
+ function Read-EnvFile {
163
+ param([string]$Path)
164
+ $values = @{}
165
+ try { $lines = @(Get-Content -LiteralPath $Path -Encoding UTF8 -ErrorAction Stop) } catch { return $values }
166
+ foreach ($line in $lines) {
167
+ if ($null -eq $line) { continue }
168
+ $text = ([string]$line).Trim()
169
+ if ($text.Length -eq 0) { continue }
170
+ if ($text.StartsWith('#')) { continue }
171
+ $index = $text.IndexOf('=')
172
+ if ($index -lt 1) { continue }
173
+ $key = $text.Substring(0, $index).Trim()
174
+ $value = $text.Substring($index + 1).Trim().Trim('"').Trim("'")
175
+ $values[$key] = $value
176
+ }
177
+ return $values
178
+ }
179
+
180
+ # ------------------------------------------------------------ launcher (.vbs) --
181
+ function Get-LauncherInfo {
182
+ param([string]$VbsPath)
183
+ $info = @{ Present = $false; Text = ''; Interpreter = ''; PayloadDir = ''; Error = '' }
184
+ if (-not (Test-Path -LiteralPath $VbsPath -PathType Leaf)) { $info.Error = 'not found'; return $info }
185
+ $info.Present = $true
186
+ try {
187
+ $info.Text = Read-TextFile $VbsPath
188
+ } catch {
189
+ $info.Error = $_.Exception.Message
190
+ return $info
191
+ }
192
+ # The Run line, as written by install.ps1:
193
+ # WshShell.Run "cmd /c """"<python.exe>"" -u -m core.agent >> """"<agent.log>"""" 2>&1""", 0, False
194
+ # The number of doubled quotes is a function of which layer wrote the file, so
195
+ # the pattern tolerates one or more quote characters on either side of the path.
196
+ # The pattern stops at the line boundary ([^\r\n]*, no $ anchor: a .vbs written
197
+ # by Out-File has CRLF endings and .NET's multiline $ refuses to match before
198
+ # the \r of a CRLF pair).
199
+ $run = [regex]::Match($info.Text, '(?im)^[^\r\n]*WshShell\.Run[^\r\n]*?cmd[ \t]+/c[ \t]+"+(?<exe>[^"]+?)"+[ \t]+(?:-u[ \t]+)?-m[ \t]+core\.agent[^\r\n]*')
200
+ if (-not $run.Success) {
201
+ # A launcher that no longer spells out "-m core.agent" is still worth
202
+ # reading: take the first absolute executable on the Run line.
203
+ $run = [regex]::Match($info.Text, '(?im)^[^\r\n]*WshShell\.Run[^\r\n]*?(?<exe>[A-Za-z]:\\[^"]*?\.(?:exe|cmd|bat))')
204
+ }
205
+ if (-not $run.Success) {
206
+ $run = [regex]::Match($info.Text, '(?im)^[^\r\n]*WshShell\.Run[^\r\n]*?cmd[ \t]+/c[ \t]+"+(?<exe>[^"]+?)"+')
207
+ }
208
+ if ($run.Success) { $info.Interpreter = $run.Groups['exe'].Value.Trim() }
209
+ $dir = [regex]::Match($info.Text, '(?im)^[^\r\n]*WshShell\.CurrentDirectory[ \t]*=[ \t]*"(?<dir>[^"]+)"')
210
+ if ($dir.Success) { $info.PayloadDir = $dir.Groups['dir'].Value.Trim() }
211
+ return $info
212
+ }
213
+
214
+ # ------------------------------------------------------------- liveness (a/b/c) --
215
+ function ConvertTo-UtcDateTime {
216
+ param($Value)
217
+ if ($null -eq $Value) { return $null }
218
+ if ($Value -is [datetime]) {
219
+ $stamp = [datetime]$Value
220
+ if ($stamp.Kind -eq [System.DateTimeKind]::Utc) { return $stamp }
221
+ if ($stamp.Kind -eq [System.DateTimeKind]::Local) { return $stamp.ToUniversalTime() }
222
+ return [System.DateTime]::SpecifyKind($stamp, [System.DateTimeKind]::Utc)
223
+ }
224
+ $text = ([string]$Value).Trim().Trim('"')
225
+ if ($text.Length -eq 0) { return $null }
226
+ $epoch = New-Object System.DateTime -ArgumentList @(1970, 1, 1, 0, 0, 0, [System.DateTimeKind]::Utc)
227
+ $number = 0.0
228
+ $isNumber = $false
229
+ if ($Value -is [double] -or $Value -is [single] -or $Value -is [decimal] -or $Value -is [int] -or $Value -is [long]) {
230
+ $number = [double]$Value
231
+ $isNumber = $true
232
+ } elseif ([double]::TryParse($text, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$number)) {
233
+ $isNumber = $true
234
+ }
235
+ # core/agent.py writes "ts" as a Unix timestamp in seconds, often fractional.
236
+ # Numbers are read with the invariant culture on purpose: on a comma-decimal
237
+ # locale "1767225600.25" would otherwise fail to parse and a live node would be
238
+ # declared dead.
239
+ if ($isNumber) {
240
+ # Unix seconds (10 digits) or Unix milliseconds (13 digits).
241
+ if ($number -gt 100000000000.0) { return $epoch.AddMilliseconds($number) }
242
+ if ($number -gt 100000000.0) { return $epoch.AddSeconds($number) }
243
+ return $null
244
+ }
245
+ $styles = [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal
246
+ $parsed = [datetime]::MinValue
247
+ if ([datetime]::TryParse($text, [System.Globalization.CultureInfo]::InvariantCulture, $styles, [ref]$parsed)) {
248
+ return [System.DateTime]::SpecifyKind($parsed, [System.DateTimeKind]::Utc)
249
+ }
250
+ return $null
251
+ }
252
+
253
+ function Get-HeartbeatState {
254
+ param([string]$Path, [int]$Grace)
255
+ $state = @{ Present = $false; Parsed = $false; AgeSeconds = $null; Fresh = $false; Detail = 'missing' }
256
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $state }
257
+ $state.Present = $true
258
+ try {
259
+ $raw = [System.IO.File]::ReadAllText($Path)
260
+ } catch {
261
+ $state.Detail = 'unreadable: ' + $_.Exception.Message
262
+ return $state
263
+ }
264
+ try {
265
+ $json = $raw | ConvertFrom-Json
266
+ } catch {
267
+ $state.Detail = 'not valid JSON'
268
+ return $state
269
+ }
270
+ $state.Parsed = $true
271
+ $stampRaw = $null
272
+ foreach ($name in @('ts', 'timestamp', 'time', 'last_seen', 'checked_at')) {
273
+ $prop = $json.PSObject.Properties[$name]
274
+ if ($prop -and $null -ne $prop.Value -and ([string]$prop.Value).Trim().Length -gt 0) {
275
+ $stampRaw = $prop.Value
276
+ break
277
+ }
278
+ }
279
+ if ($null -eq $stampRaw) { $state.Detail = 'JSON has no ts field'; return $state }
280
+ $stamp = ConvertTo-UtcDateTime $stampRaw
281
+ if ($null -eq $stamp) { $state.Detail = 'ts is not a timestamp: ' + ([string]$stampRaw); return $state }
282
+ $age = ((Get-Date).ToUniversalTime() - $stamp.ToUniversalTime()).TotalSeconds
283
+ $state.AgeSeconds = [Math]::Round($age, 1)
284
+ if ($age -le $Grace) {
285
+ $state.Fresh = $true
286
+ $state.Detail = 'fresh (' + $state.AgeSeconds + 's old)'
287
+ } else {
288
+ $state.Detail = 'stale (' + $state.AgeSeconds + 's old, grace ' + $Grace + 's)'
289
+ }
290
+ return $state
291
+ }
292
+
293
+ function Get-GatewayHealth {
294
+ param([string]$Gateway, [string]$NodeUser, [int]$TimeoutSec = 10)
295
+ $state = @{ Url = ''; Ok = $false; Text = ''; Detail = 'not checked' }
296
+ if ([string]::IsNullOrWhiteSpace($Gateway) -or [string]::IsNullOrWhiteSpace($NodeUser)) {
297
+ $state.Detail = 'gateway or node name missing'
298
+ return $state
299
+ }
300
+ $hostName = ($Gateway.Trim() -replace '^[A-Za-z][A-Za-z0-9+.-]*://', '').Trim().TrimEnd('/')
301
+ if ($hostName.Length -eq 0) {
302
+ $state.Detail = 'gateway is empty after normalisation'
303
+ return $state
304
+ }
305
+ $state.Url = 'https://' + $hostName + '/health?user=' + [uri]::EscapeDataString($NodeUser)
306
+ try {
307
+ $response = Invoke-RestMethod -Uri $state.Url -Method Get -TimeoutSec $TimeoutSec -ErrorAction Stop
308
+ } catch {
309
+ $state.Detail = 'unreachable: ' + $_.Exception.Message
310
+ return $state
311
+ }
312
+ try { $state.Text = ($response | ConvertTo-Json -Compress -Depth 5) } catch { $state.Text = '' }
313
+ $prop = $response.PSObject.Properties['node_online']
314
+ $online = $false
315
+ if ($prop) {
316
+ if ($prop.Value -is [bool]) { $online = [bool]$prop.Value } else { $online = (([string]$prop.Value).Trim().ToLower() -eq 'true') }
317
+ }
318
+ $state.Ok = $online
319
+ if ($online) { $state.Detail = 'node_online=true' } else { $state.Detail = 'node_online=false' }
320
+ return $state
321
+ }
322
+
323
+ function Test-AgentProcess {
324
+ # The command line ALONE is not evidence. A PowerShell session that merely
325
+ # mentions "core.agent" (a grep, a script that prints the module name) matches
326
+ # the bare substring, so a dead node would look alive and would never be
327
+ # restarted. The process must be a Python interpreter running the module, or
328
+ # the cmd.exe wrapper the logon launcher uses - which also carries the
329
+ # agent.log append redirect. PowerShell, wscript and everything else never
330
+ # qualifies, whatever its command line says.
331
+ param([string]$Name, [string]$CommandLine)
332
+ if ([string]::IsNullOrWhiteSpace($CommandLine)) { return $false }
333
+ $processName = ''
334
+ if ($Name) { $processName = ([string]$Name).Trim().ToLowerInvariant() }
335
+ foreach ($shell in @('powershell', 'pwsh', 'wscript', 'cscript')) {
336
+ if ($processName.StartsWith($shell)) { return $false }
337
+ }
338
+ # "-m core.agent" has to be a real argument, so it must open the command line
339
+ # or follow a space or a quote.
340
+ if ($CommandLine -notmatch '(?i)(^|[\s"])-m[\s"]+core\.agent\b') { return $false }
341
+ if ($processName -eq 'py.exe') { return $true }
342
+ if ($processName.StartsWith('python')) { return $true }
343
+ if ($processName -eq 'cmd.exe' -or $processName -eq 'cmd') {
344
+ # Launcher wrapper: cmd /c ""<python>" -u -m core.agent >> "<agent.log>" 2>&1"
345
+ return [bool]($CommandLine -match '(?i)agent\.log')
346
+ }
347
+ return $false
348
+ }
349
+
350
+ function Get-AgentProcesses {
351
+ # A verified agent process is the third and last liveness signal: it is the
352
+ # only one that still works when the gateway is unreachable and the heartbeat
353
+ # was never written.
354
+ $found = @()
355
+ $procs = @()
356
+ try {
357
+ $procs = @(Get-CimInstance -ClassName Win32_Process -ErrorAction Stop)
358
+ } catch {
359
+ try { $procs = @(Get-WmiObject -Class Win32_Process -ErrorAction Stop) } catch { $procs = @() }
360
+ }
361
+ foreach ($proc in $procs) {
362
+ $commandLine = ''
363
+ $prop = $proc.PSObject.Properties['CommandLine']
364
+ if ($prop -and $prop.Value) { $commandLine = [string]$prop.Value }
365
+ if ($proc.ProcessId -eq $PID) { continue }
366
+ if (-not (Test-AgentProcess -Name ([string]$proc.Name) -CommandLine $commandLine)) { continue }
367
+ $found += @{ Pid = [int]$proc.ProcessId; Name = [string]$proc.Name; CommandLine = $commandLine }
368
+ }
369
+ return $found
370
+ }
371
+
372
+ function Get-LivenessState {
373
+ param([string]$HeartbeatPath, [string]$Gateway, [string]$NodeUser, [int]$Grace, [int]$TimeoutSec)
374
+ $state = @{ Alive = $false; Via = ''; Heartbeat = $null; Gateway = $null; Processes = @(); Reason = '' }
375
+ # (a) heartbeat, (b) gateway health, (c) a verified agent process - in order.
376
+ $state.Heartbeat = Get-HeartbeatState -Path $HeartbeatPath -Grace $Grace
377
+ if ($state.Heartbeat.Fresh) { $state.Alive = $true; $state.Via = 'fresh heartbeat'; return $state }
378
+ $state.Gateway = Get-GatewayHealth -Gateway $Gateway -NodeUser $NodeUser -TimeoutSec $TimeoutSec
379
+ if ($state.Gateway.Ok) { $state.Alive = $true; $state.Via = 'gateway health'; return $state }
380
+ $state.Processes = @(Get-AgentProcesses)
381
+ if ($state.Processes.Count -gt 0) {
382
+ $state.Alive = $true
383
+ $state.Via = 'running process (pid ' + $state.Processes[0].Pid + ')'
384
+ return $state
385
+ }
386
+ $state.Reason = 'no fresh heartbeat (' + $state.Heartbeat.Detail + '), gateway ' + $state.Gateway.Detail + ', no verified agent process (a python.exe running -m core.agent, or the cmd.exe launcher wrapper)'
387
+ return $state
388
+ }
389
+
390
+ function Resolve-FullPath {
391
+ param([string]$Path)
392
+ if ([string]::IsNullOrWhiteSpace($Path)) { return $Path }
393
+ try { return [System.IO.Path]::GetFullPath($Path) } catch { return $Path }
394
+ }
395
+
396
+ # ==============================================================================
397
+ # Main
398
+ # ==============================================================================
399
+
400
+ # --- 1. paths ----------------------------------------------------------------
401
+ if ([string]::IsNullOrWhiteSpace($ConfigDir)) {
402
+ $profileDir = $env:USERPROFILE
403
+ if ([string]::IsNullOrWhiteSpace($profileDir)) { $profileDir = [Environment]::GetFolderPath('UserProfile') }
404
+ if ([string]::IsNullOrWhiteSpace($profileDir)) {
405
+ if (-not $Quiet) { Write-Host 'CONFIG ERROR: cannot resolve the user profile directory; pass -ConfigDir explicitly.' }
406
+ exit 2
407
+ }
408
+ $ConfigDir = Join-Path $profileDir '.config\antigravity-mesh'
409
+ }
410
+ $ConfigDir = Resolve-FullPath $ConfigDir
411
+ if ([string]::IsNullOrWhiteSpace($LogFile)) { $LogFile = Join-Path $ConfigDir 'watchdog.log' }
412
+ $LogFile = Resolve-FullPath $LogFile
413
+ try {
414
+ $logDir = Split-Path -Parent $LogFile
415
+ if ($logDir -and -not (Test-Path -LiteralPath $logDir -PathType Container)) {
416
+ $null = New-Item -ItemType Directory -Path $logDir -Force -ErrorAction Stop
417
+ }
418
+ $script:LogTarget = $LogFile
419
+ } catch {
420
+ $script:LogTarget = ''
421
+ }
422
+
423
+ # --- 2. single instance ------------------------------------------------------
424
+ # Overlapping runs (a slow gateway probe plus the next 5-minute tick) must not
425
+ # start two agents. The OS releases the mutex when this process exits, so a
426
+ # crashed run can never wedge the watchdog.
427
+ $mutexName = 'AntigravityMeshWatchdog-' + ($env:USERNAME)
428
+ $mutex = $null
429
+ $acquired = $true
430
+ foreach ($candidate in @(('Global\' + $mutexName), $mutexName)) {
431
+ try {
432
+ $mutex = New-Object System.Threading.Mutex -ArgumentList @($false, $candidate)
433
+ $acquired = [bool]$mutex.WaitOne(0)
434
+ break
435
+ } catch [System.Threading.AbandonedMutexException] {
436
+ $acquired = $true
437
+ break
438
+ } catch {
439
+ $mutex = $null
440
+ $acquired = $true
441
+ }
442
+ }
443
+ if (-not $acquired) {
444
+ Write-LogLine 'another watchdog instance is already running; nothing to do'
445
+ exit 0
446
+ }
447
+
448
+ if ($GraceSeconds -lt 0) { $GraceSeconds = 0 }
449
+ $heartbeatPath = Join-Path $ConfigDir 'agent.heartbeat'
450
+
451
+ # --- 3. configuration --------------------------------------------------------
452
+ $envFile = Join-Path $ConfigDir 'agent.env'
453
+ if (-not (Test-Path -LiteralPath $envFile -PathType Leaf)) {
454
+ Stop-WithConfigError ('no agent.env at ' + $envFile + ' - this node was never installed; run install.ps1')
455
+ }
456
+ $settings = Read-EnvFile $envFile
457
+ $missing = @()
458
+ foreach ($key in @('MESH_GATEWAY', 'MESH_USER', 'MESH_TOKEN')) {
459
+ if (-not $settings.ContainsKey($key) -or [string]::IsNullOrWhiteSpace($settings[$key])) { $missing += $key }
460
+ }
461
+ if ($missing.Count -gt 0) {
462
+ Stop-WithConfigError ('agent.env is missing or has an empty ' + ($missing -join ', ') + ' (' + $envFile + ') - refusing to start an agent from an incomplete configuration')
463
+ }
464
+ $gateway = $settings['MESH_GATEWAY'].Trim()
465
+ $nodeUser = $settings['MESH_USER'].Trim()
466
+ $token = $settings['MESH_TOKEN'].Trim()
467
+
468
+ Write-Note ('watchdog start: config=' + $ConfigDir + ' gateway=' + $gateway + ' user=' + $nodeUser + ' token=****')
469
+
470
+ # --- 4. the logon launcher ---------------------------------------------------
471
+ $startupDir = [Environment]::GetFolderPath('Startup')
472
+ if ([string]::IsNullOrWhiteSpace($startupDir)) {
473
+ Stop-WithConfigError 'cannot resolve the Startup folder ([Environment]::GetFolderPath("Startup") returned nothing)'
474
+ }
475
+ $vbsPath = Join-Path $startupDir 'antigravity-agent.vbs'
476
+ if (-not (Test-Path -LiteralPath $vbsPath -PathType Leaf)) {
477
+ Stop-WithConfigError ('the autostart launcher is missing: ' + $vbsPath + ' - this node would not start after a reboot; re-run install.ps1')
478
+ }
479
+ $launcher = Get-LauncherInfo $vbsPath
480
+ if (-not $launcher.Present) {
481
+ Stop-WithConfigError ('cannot read the autostart launcher ' + $vbsPath + ': ' + $launcher.Error)
482
+ }
483
+ $interpreter = $launcher.Interpreter
484
+ if ([string]::IsNullOrWhiteSpace($interpreter)) {
485
+ Stop-WithConfigError ('no interpreter found in the WshShell.Run line of ' + $vbsPath + ' - the launcher is unusable; re-run install.ps1')
486
+ }
487
+ if ($interpreter -match '(?i)\\WindowsApps\\') {
488
+ Stop-WithConfigError ('the pinned interpreter is the Microsoft Store App Execution Alias: ' + $interpreter + ' - that alias never runs Python and was the silent post-reboot failure; re-run install.ps1 so a real python.exe is pinned')
489
+ }
490
+ if (-not (Test-Path -LiteralPath $interpreter -PathType Leaf)) {
491
+ Stop-WithConfigError ('the pinned interpreter does not exist: ' + $interpreter + ' - re-run install.ps1, or fix the WshShell.Run line in ' + $vbsPath)
492
+ }
493
+
494
+ # The payload directory decides where "import core.agent" resolves from. Prefer
495
+ # exactly what the .vbs sets; fall back to the payload this watchdog was shipped
496
+ # inside, then to the config directory, and always say which one was used.
497
+ $payloadReason = 'WshShell.CurrentDirectory'
498
+ if ([string]::IsNullOrWhiteSpace($launcher.PayloadDir)) {
499
+ $payloadProblem = 'missing'
500
+ } elseif (-not (Test-Path -LiteralPath $launcher.PayloadDir -PathType Container)) {
501
+ $payloadProblem = 'not a directory (' + $launcher.PayloadDir + ')'
502
+ } else {
503
+ $payloadProblem = ''
504
+ }
505
+ if ($payloadProblem) {
506
+ $fallbackDir = ''
507
+ if ($PSScriptRoot) { $fallbackDir = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) }
508
+ if ($fallbackDir -and (Test-Path -LiteralPath $fallbackDir -PathType Container)) {
509
+ $payloadDir = $fallbackDir
510
+ $payloadReason = 'the watchdog location (WshShell.CurrentDirectory is ' + $payloadProblem + ')'
511
+ } else {
512
+ $payloadDir = $ConfigDir
513
+ $payloadReason = 'the config directory (WshShell.CurrentDirectory is ' + $payloadProblem + ' and no payload was found next to the watchdog)'
514
+ }
515
+ } else {
516
+ $payloadDir = $launcher.PayloadDir
517
+ }
518
+ Write-Note ('launcher=' + $vbsPath + ' interpreter=' + $interpreter + ' payload=' + $payloadDir + ' (' + $payloadReason + ')')
519
+
520
+ # --- 5. is the node alive? ---------------------------------------------------
521
+ $state = Get-LivenessState -HeartbeatPath $heartbeatPath -Gateway $gateway -NodeUser $nodeUser -Grace $GraceSeconds -TimeoutSec 10
522
+ if ($state.Alive) {
523
+ Write-Note ('node is online via ' + $state.Via + '; nothing to do')
524
+ exit 0
525
+ }
526
+ Write-Note ('node is offline: ' + $state.Reason)
527
+
528
+ # --- 6. recover --------------------------------------------------------------
529
+ # Exactly one line goes into agent.log: it says why the node was considered dead
530
+ # and which interpreter is being started. Everything written to a log passes
531
+ # through Hide-Secret, so the token cannot leak into agent.log even by accident.
532
+ $stamp = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ssK')
533
+ $diagnostic = '[' + $stamp + '] watchdog: node is offline (' + $state.Reason + '); starting the agent with interpreter ' + $interpreter + ' from ' + $payloadDir
534
+ $null = Add-AgentLogLine (Hide-Secret -Text $diagnostic -Secret $token)
535
+
536
+ # Same contract as the logon launcher: cmd /c ""<interpreter>" -u -m core.agent
537
+ # >> "<agent.log>" 2>&1 - cmd strips the outer quote pair, leaving one quoted
538
+ # interpreter, an unbuffered core.agent run and an append redirection.
539
+ $env:MESH_GATEWAY = $gateway
540
+ $env:MESH_USER = $nodeUser
541
+ $env:MESH_TOKEN = $token
542
+ $agentLog = Join-Path $ConfigDir 'agent.log'
543
+ $argumentLine = '/c ""' + $interpreter + '" -u -m core.agent >> "' + $agentLog + '" 2>&1"'
544
+ $launched = $false
545
+ try {
546
+ $agentProcess = Start-Process -FilePath 'cmd.exe' -ArgumentList $argumentLine -WorkingDirectory $payloadDir -WindowStyle Hidden -PassThru -ErrorAction Stop
547
+ $launched = $true
548
+ Write-Note ('started the agent: pid=' + $agentProcess.Id + ' interpreter=' + $interpreter + ' cwd=' + $payloadDir)
549
+ } catch {
550
+ Write-Note ('could not start the agent: ' + $_.Exception.Message)
551
+ }
552
+
553
+ # --- 7. wait for liveness ----------------------------------------------------
554
+ $finalState = $null
555
+ if ($launched) {
556
+ $deadline = (Get-Date).AddSeconds(20)
557
+ while ((Get-Date) -lt $deadline) {
558
+ Start-Sleep -Seconds 2
559
+ $finalState = Get-LivenessState -HeartbeatPath $heartbeatPath -Gateway $gateway -NodeUser $nodeUser -Grace $GraceSeconds -TimeoutSec 5
560
+ if ($finalState.Alive) { break }
561
+ }
562
+ } else {
563
+ Write-Note 'not waiting for liveness: the launch failed'
564
+ }
565
+
566
+ if ($finalState -and $finalState.Alive) {
567
+ Write-Note ('node is online again via ' + $finalState.Via + '; recovery succeeded')
568
+ exit 0
569
+ }
570
+ if ($finalState) {
571
+ Write-Note ('node is STILL offline after the recovery attempt: ' + $finalState.Reason)
572
+ } else {
573
+ Write-Note 'node is STILL offline: no agent process was started'
574
+ }
575
+ exit 3
@@ -0,0 +1,64 @@
1
+ ' ==============================================================================
2
+ ' Antigravity Mesh - run a node script with no console window at all
3
+ '
4
+ ' WHY THIS FILE EXISTS
5
+ ' Task Scheduler starts a task inside the interactive session, and the task has
6
+ ' no console of its own. When the action is powershell.exe, Windows therefore
7
+ ' allocates a NEW console for it and only then applies "-WindowStyle Hidden":
8
+ ' the window is created, drawn and hidden again, which the user sees as a black
9
+ ' window that appears and disappears. The watchdog task fires every five minutes,
10
+ ' so that flash is the most noticeable thing this project does on a desktop.
11
+ '
12
+ ' wscript.exe is a GUI host: it never allocates a console. WshShell.Run with a
13
+ ' window style of 0 passes SW_HIDE in the child's STARTUPINFO, so cmd.exe (and
14
+ ' therefore PowerShell) is created hidden from the very first moment - nothing is
15
+ ' ever drawn. This is the same mechanism the logon launcher
16
+ ' (antigravity-agent.vbs) uses, which is why a node starts without a window.
17
+ '
18
+ ' USAGE - the two scheduled tasks use exactly this. The script path is relative
19
+ ' to the payload (the directory that holds core\, ops\ and install.ps1), which
20
+ ' this file finds from its own location:
21
+ '
22
+ ' wscript.exe run-hidden.vbs "ops\windows\agent-watchdog.ps1" -Quiet
23
+ ' wscript.exe run-hidden.vbs "ops\update.ps1" -Quiet
24
+ '
25
+ ' Output goes to <ConfigDir>\<script name>.out.log: a window that never appears
26
+ ' also never shows an error.
27
+ '
28
+ ' ASCII only, and no BOM: wscript.exe reads a .vbs with the ANSI code page.
29
+ ' ==============================================================================
30
+ Option Explicit
31
+
32
+ Dim shell, fso, payload, scriptPath, logDir, logPath, command, index, extra, quote
33
+
34
+ Set shell = CreateObject("WScript.Shell")
35
+ Set fso = CreateObject("Scripting.FileSystemObject")
36
+
37
+ If WScript.Arguments.Count < 1 Then
38
+ WScript.Quit 2
39
+ End If
40
+
41
+ ' <payload>\ops\windows\run-hidden.vbs -> three levels up is <payload>
42
+ payload = fso.GetParentFolderName(fso.GetParentFolderName(fso.GetParentFolderName(WScript.ScriptFullName)))
43
+ scriptPath = payload & "\" & WScript.Arguments(0)
44
+
45
+ extra = ""
46
+ For index = 1 To WScript.Arguments.Count - 1
47
+ extra = extra & " " & WScript.Arguments(index)
48
+ Next
49
+
50
+ logDir = shell.ExpandEnvironmentStrings("%USERPROFILE%") & "\.config\antigravity-mesh"
51
+ If Not fso.FolderExists(logDir) Then
52
+ logDir = payload
53
+ End If
54
+ logPath = logDir & "\" & fso.GetFileName(scriptPath) & ".out.log"
55
+
56
+ quote = Chr(34)
57
+ command = "cmd.exe /c " & quote & quote & "powershell.exe" & quote _
58
+ & " -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -NonInteractive -File " _
59
+ & quote & scriptPath & quote & extra _
60
+ & " >> " & quote & logPath & quote & " 2>&1" & quote
61
+
62
+ ' 0 = the child window is never shown, False = do not wait for it
63
+ shell.Run command, 0, False
64
+ WScript.Quit 0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gemini-computer-use",
3
- "version": "0.2.4",
4
- "description": "Turn your PC, Laptop or Server into an autonomous execution node for Google Gemini via native MCP: 14 tools, write/edit/search, background jobs, secure reverse tunnel.",
3
+ "version": "0.4.1",
4
+ "description": "Turn your PC, Laptop, Server or Android phone into an autonomous execution node for Google Gemini via native MCP: 26 tools, write/edit/search, background jobs, public file and page sharing, secure reverse tunnel, self-update, and a device branch (battery, signal, sensors, camera, microphone, notifications) for phones and laptops.",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
7
7
  "gemini-computer-use": "bin/cli.js"
@@ -13,8 +13,12 @@
13
13
  "bin",
14
14
  "core",
15
15
  "skills",
16
+ "ops",
16
17
  "install.sh",
17
18
  "install.ps1",
19
+ "install-gui.ps1",
20
+ "install-gui.strings.json",
21
+ "install-gui.cmd",
18
22
  "README.md",
19
23
  "LICENSE",
20
24
  "gateway.py"