@geekbeer/minion 2.23.0 → 2.32.0

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 (47) hide show
  1. package/core/lib/platform.js +117 -0
  2. package/{routes → core/routes}/health.js +1 -1
  3. package/{routes → core/routes}/routines.js +44 -4
  4. package/{routes → core/routes}/skills.js +3 -3
  5. package/{routes → core/routes}/workflows.js +4 -4
  6. package/{chat-store.js → core/stores/chat-store.js} +1 -1
  7. package/{execution-store.js → core/stores/execution-store.js} +1 -1
  8. package/{routine-store.js → core/stores/routine-store.js} +1 -1
  9. package/{workflow-store.js → core/stores/workflow-store.js} +1 -1
  10. package/{minion-cli.sh → linux/minion-cli.sh} +245 -4
  11. package/{routes → linux/routes}/chat.js +3 -3
  12. package/{routes → linux/routes}/commands.js +1 -1
  13. package/{routes → linux/routes}/config.js +3 -3
  14. package/{routes → linux/routes}/directives.js +5 -5
  15. package/{routes → linux/routes}/files.js +2 -2
  16. package/{routes → linux/routes}/terminal.js +2 -2
  17. package/{routine-runner.js → linux/routine-runner.js} +4 -4
  18. package/{server.js → linux/server.js} +71 -36
  19. package/{workflow-runner.js → linux/workflow-runner.js} +4 -4
  20. package/package.json +16 -20
  21. package/win/bin/hq-win.js +18 -0
  22. package/win/bin/hq.ps1 +108 -0
  23. package/win/bin/minion-cli-win.js +20 -0
  24. package/win/lib/llm-checker.js +115 -0
  25. package/win/lib/log-manager.js +119 -0
  26. package/win/lib/process-manager.js +112 -0
  27. package/win/minion-cli.ps1 +869 -0
  28. package/win/routes/chat.js +280 -0
  29. package/win/routes/commands.js +101 -0
  30. package/win/routes/config.js +227 -0
  31. package/win/routes/directives.js +136 -0
  32. package/win/routes/files.js +283 -0
  33. package/win/routes/terminal.js +316 -0
  34. package/win/routine-runner.js +324 -0
  35. package/win/server.js +230 -0
  36. package/win/terminal-server.js +234 -0
  37. package/win/workflow-runner.js +380 -0
  38. package/routes/index.js +0 -106
  39. /package/{api.js → core/api.js} +0 -0
  40. /package/{config.js → core/config.js} +0 -0
  41. /package/{lib → core/lib}/auth.js +0 -0
  42. /package/{lib → core/lib}/llm-checker.js +0 -0
  43. /package/{lib → core/lib}/log-manager.js +0 -0
  44. /package/{routes → core/routes}/auth.js +0 -0
  45. /package/{bin → linux/bin}/hq +0 -0
  46. /package/{lib → linux/lib}/process-manager.js +0 -0
  47. /package/{terminal-proxy.js → linux/terminal-proxy.js} +0 -0
@@ -0,0 +1,869 @@
1
+ #Requires -Version 5.1
2
+ # Minion Agent CLI for Windows (@geekbeer/minion)
3
+ # Usage:
4
+ # minion-cli-win setup --hq-url https://... --minion-id <UUID> --api-token <TOKEN>
5
+ # minion-cli-win setup --setup-tunnel
6
+ # minion-cli-win start | stop | restart | status | health | version | help
7
+
8
+ # Parse arguments manually to avoid issues with npm wrapper passing $args as array
9
+ $Command = 'help'
10
+ $HqUrl = ''
11
+ $MinionId = ''
12
+ $ApiToken = ''
13
+ $SetupTunnel = $false
14
+
15
+ $i = 0
16
+ while ($i -lt $args.Count) {
17
+ $arg = [string]$args[$i]
18
+ switch -Regex ($arg) {
19
+ '^(setup|reconfigure|start|stop|restart|status|health|version|help)$' { $Command = $arg }
20
+ '^(-v|--version)$' { $Command = 'version' }
21
+ '^--hq-url$' { $i++; if ($i -lt $args.Count) { $HqUrl = [string]$args[$i] } }
22
+ '^--minion-id$' { $i++; if ($i -lt $args.Count) { $MinionId = [string]$args[$i] } }
23
+ '^--api-token$' { $i++; if ($i -lt $args.Count) { $ApiToken = [string]$args[$i] } }
24
+ '^--setup-tunnel$' { $SetupTunnel = $true }
25
+ '^(-h|--help)$' { $Command = 'help' }
26
+ }
27
+ $i++
28
+ }
29
+
30
+ $ErrorActionPreference = 'Stop'
31
+
32
+ # Constants
33
+ $DataDir = Join-Path $env:USERPROFILE '.minion'
34
+ $EnvFile = Join-Path $DataDir '.env'
35
+ $PidFile = Join-Path $DataDir 'minion-agent.pid'
36
+ $LogDir = Join-Path $DataDir 'logs'
37
+ $StartAgentScript = Join-Path $DataDir 'start-agent.ps1'
38
+ $AgentUrl = if ($env:MINION_AGENT_URL) { $env:MINION_AGENT_URL } else { 'http://localhost:8080' }
39
+
40
+ # Resolve CLI version from package.json
41
+ $CliDir = Split-Path -Parent $PSScriptRoot
42
+ $CliVersion = try {
43
+ (Get-Content (Join-Path $CliDir 'package.json') -Raw | ConvertFrom-Json).version
44
+ }
45
+ catch { 'unknown' }
46
+
47
+ # ============================================================
48
+ # Utility functions
49
+ # ============================================================
50
+
51
+ function Write-Step {
52
+ param([int]$Current, [int]$Total, [string]$Message)
53
+ Write-Host "[$Current/$Total] $Message" -ForegroundColor Cyan
54
+ }
55
+
56
+ function Write-Detail {
57
+ param([string]$Message)
58
+ Write-Host " -> $Message" -ForegroundColor Green
59
+ }
60
+
61
+ function Write-Warn {
62
+ param([string]$Message)
63
+ Write-Host " WARNING: $Message" -ForegroundColor Yellow
64
+ }
65
+
66
+ function Test-CommandExists {
67
+ param([string]$Name)
68
+ $null -ne (Get-Command $Name -ErrorAction SilentlyContinue)
69
+ }
70
+
71
+ function Get-LanIPAddress {
72
+ try {
73
+ $ip = (Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
74
+ Where-Object {
75
+ ($_.PrefixOrigin -eq 'Dhcp' -or $_.PrefixOrigin -eq 'Manual') -and
76
+ $_.IPAddress -notlike '169.254.*' -and
77
+ $_.IPAddress -ne '127.0.0.1' -and
78
+ $_.InterfaceAlias -notlike 'vEthernet*'
79
+ } |
80
+ Select-Object -First 1).IPAddress
81
+ return $ip
82
+ }
83
+ catch { return $null }
84
+ }
85
+
86
+ function Get-MinionServerJs {
87
+ $npmRoot = & npm root -g 2>$null
88
+ return Join-Path $npmRoot '@geekbeer\minion\win\server.js'
89
+ }
90
+
91
+ function Read-EnvFile {
92
+ param([string]$Path)
93
+ $result = @{}
94
+ if (-not (Test-Path $Path)) { return $result }
95
+ Get-Content $Path | ForEach-Object {
96
+ $line = $_.Trim()
97
+ if ($line -and -not $line.StartsWith('#') -and $line.Contains('=')) {
98
+ $idx = $line.IndexOf('=')
99
+ $key = $line.Substring(0, $idx).Trim()
100
+ $value = $line.Substring($idx + 1).Trim()
101
+ $result[$key] = $value
102
+ }
103
+ }
104
+ return $result
105
+ }
106
+
107
+ function Write-EnvFile {
108
+ param([string]$Path, [hashtable]$Values)
109
+ $lines = @(
110
+ '# Minion Agent Configuration',
111
+ '# Generated by minion-cli.ps1 setup',
112
+ ''
113
+ )
114
+ foreach ($key in ($Values.Keys | Sort-Object)) {
115
+ $lines += "$key=$($Values[$key])"
116
+ }
117
+ $lines += ''
118
+ New-Item -Path (Split-Path $Path) -ItemType Directory -Force | Out-Null
119
+ Set-Content -Path $Path -Value ($lines -join "`n") -Encoding UTF8 -NoNewline
120
+ }
121
+
122
+ function Invoke-HealthCheck {
123
+ param([int]$Retries = 5, [int]$DelaySeconds = 2)
124
+ for ($i = 1; $i -le $Retries; $i++) {
125
+ try {
126
+ $response = Invoke-RestMethod -Uri "$AgentUrl/api/health" -TimeoutSec 5 -ErrorAction Stop
127
+ if ($response) { return $true }
128
+ }
129
+ catch { }
130
+ Write-Host " Waiting for agent to start... (attempt $i/$Retries)"
131
+ Start-Sleep -Seconds $DelaySeconds
132
+ }
133
+ return $false
134
+ }
135
+
136
+ function Start-MinionProcess {
137
+ # Check if already running via PID file
138
+ if (Test-Path $PidFile) {
139
+ $existingPid = (Get-Content $PidFile -Raw).Trim()
140
+ $proc = Get-Process -Id $existingPid -ErrorAction SilentlyContinue
141
+ if ($proc) {
142
+ Write-Host "minion-agent is already running (PID: $existingPid)"
143
+ return
144
+ }
145
+ Remove-Item $PidFile -Force
146
+ }
147
+
148
+ if (-not (Test-Path $StartAgentScript)) {
149
+ Write-Error "start-agent.ps1 not found. Run 'minion-cli-win setup' first."
150
+ exit 1
151
+ }
152
+
153
+ # Launch start-agent.ps1 as a hidden background process
154
+ Start-Process powershell -ArgumentList "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$StartAgentScript`"" `
155
+ -WindowStyle Hidden
156
+ Start-Sleep -Seconds 2
157
+
158
+ if (Test-Path $PidFile) {
159
+ $newPid = (Get-Content $PidFile -Raw).Trim()
160
+ Write-Host "minion-agent started (PID: $newPid)"
161
+ } else {
162
+ Write-Host "minion-agent starting... (check logs at $LogDir)"
163
+ }
164
+ }
165
+
166
+ function Stop-MinionProcess {
167
+ if (-not (Test-Path $PidFile)) {
168
+ Write-Host "minion-agent is not running (no PID file)"
169
+ return
170
+ }
171
+ $agentPid = (Get-Content $PidFile -Raw).Trim()
172
+ $proc = Get-Process -Id $agentPid -ErrorAction SilentlyContinue
173
+ if ($proc) {
174
+ # Also stop child processes (node, websockify, cloudflared)
175
+ Get-CimInstance Win32_Process -Filter "ParentProcessId = $agentPid" -ErrorAction SilentlyContinue |
176
+ ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
177
+ Stop-Process -Id $agentPid -Force
178
+ Write-Host "minion-agent stopped (PID: $agentPid)"
179
+ } else {
180
+ Write-Host "minion-agent was not running (stale PID file)"
181
+ }
182
+ Remove-Item $PidFile -Force -ErrorAction SilentlyContinue
183
+ }
184
+
185
+ function Restart-MinionProcess {
186
+ Stop-MinionProcess
187
+ Start-Sleep -Seconds 2
188
+ Start-MinionProcess
189
+ }
190
+
191
+ # ============================================================
192
+ # Setup
193
+ # ============================================================
194
+
195
+ function Invoke-Setup {
196
+ $totalSteps = 8
197
+ if ($SetupTunnel) { $totalSteps = 9 }
198
+
199
+ # Minionization warning
200
+ Write-Host ""
201
+ Write-Host "=========================================" -ForegroundColor Red
202
+ Write-Host " WARNING: This machine will be minionized" -ForegroundColor Red
203
+ Write-Host "=========================================" -ForegroundColor Red
204
+ Write-Host ""
205
+
206
+ # Check if current user is in Administrators group
207
+ $adminGroupMembers = net localgroup Administrators 2>$null
208
+ if ($adminGroupMembers -match [regex]::Escape($env:USERNAME)) {
209
+ Write-Host " SECURITY WARNING: User '$env:USERNAME' has Administrator privileges." -ForegroundColor Yellow
210
+ Write-Host " We recommend setting up the minion under a Standard user account." -ForegroundColor Yellow
211
+ Write-Host " If this machine is compromised, an Administrator account gives" -ForegroundColor Yellow
212
+ Write-Host " the attacker full control over the system." -ForegroundColor Yellow
213
+ Write-Host ""
214
+ Write-Host " To create a Standard user:" -ForegroundColor Gray
215
+ Write-Host " net user minion <password> /add" -ForegroundColor Gray
216
+ Write-Host " Then log in as that user and run setup again." -ForegroundColor Gray
217
+ Write-Host ""
218
+ }
219
+
220
+ Write-Host " This setup will:" -ForegroundColor Yellow
221
+ Write-Host " - Install and configure software (Node.js, Claude Code)"
222
+ Write-Host " - Register auto-start via Startup folder"
223
+ Write-Host " - Overwrite Claude Code settings (permissions, rules, skills)"
224
+ Write-Host ""
225
+ Write-Host " This machine should be DEDICATED to the minion agent." -ForegroundColor Yellow
226
+ Write-Host " Do not use it as your daily workstation after setup." -ForegroundColor Yellow
227
+ Write-Host ""
228
+ $confirm = Read-Host " Type 'yes' to continue"
229
+ if ($confirm -ne 'yes') {
230
+ Write-Host "Setup cancelled."
231
+ exit 0
232
+ }
233
+ Write-Host ""
234
+
235
+ # Load existing .env for redeploy scenario
236
+ $envValues = @{}
237
+ if (-not $HqUrl -and -not $MinionId -and -not $ApiToken -and (Test-Path $EnvFile)) {
238
+ Write-Host "Reading existing .env values (redeploy mode)..."
239
+ $envValues = Read-EnvFile $EnvFile
240
+ if (-not $HqUrl -and $envValues['HQ_URL']) { $HqUrl = $envValues['HQ_URL'] }
241
+ if (-not $MinionId -and $envValues['MINION_ID']) { $MinionId = $envValues['MINION_ID'] }
242
+ if (-not $ApiToken -and $envValues['API_TOKEN']) { $ApiToken = $envValues['API_TOKEN'] }
243
+ }
244
+
245
+ Write-Host "=========================================" -ForegroundColor Cyan
246
+ Write-Host " @geekbeer/minion Windows Setup" -ForegroundColor Cyan
247
+ Write-Host "=========================================" -ForegroundColor Cyan
248
+ Write-Host "Platform: Windows (no admin required)"
249
+ Write-Host "User: $env:USERNAME ($env:USERPROFILE)"
250
+ Write-Host "Data: $DataDir"
251
+ if ($HqUrl) {
252
+ Write-Host "Mode: Connected to HQ ($HqUrl)"
253
+ }
254
+ else {
255
+ Write-Host "Mode: Standalone (no HQ connection)"
256
+ }
257
+ if ($SetupTunnel) { Write-Host "Tunnel: Enabled" }
258
+ Write-Host ""
259
+
260
+ # Step 1: Check/Install Node.js
261
+ Write-Step 1 $totalSteps "Checking Node.js..."
262
+ if (Test-CommandExists 'node') {
263
+ $nodeVersion = & node --version 2>$null
264
+ Write-Detail "Node.js $nodeVersion already installed"
265
+ }
266
+ else {
267
+ Write-Host " Installing Node.js via winget..."
268
+ try {
269
+ & winget install --id OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements
270
+ # Refresh PATH
271
+ $env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH', 'User')
272
+ if (Test-CommandExists 'node') {
273
+ Write-Detail "Node.js installed successfully"
274
+ }
275
+ else {
276
+ Write-Warn "Node.js installed but not in PATH. Please restart this terminal."
277
+ exit 1
278
+ }
279
+ }
280
+ catch {
281
+ Write-Error "Failed to install Node.js. Please install manually from https://nodejs.org/"
282
+ exit 1
283
+ }
284
+ }
285
+
286
+ # Step 2: Install Claude Code CLI
287
+ Write-Step 2 $totalSteps "Installing Claude Code..."
288
+ if (Test-CommandExists 'claude') {
289
+ $claudeVersion = & claude --version 2>$null
290
+ Write-Detail "Claude Code already installed ($claudeVersion)"
291
+ }
292
+ else {
293
+ Write-Host " Installing Claude Code via npm..."
294
+ & npm install -g @anthropic-ai/claude-code
295
+ if (Test-CommandExists 'claude') {
296
+ Write-Detail "Claude Code installed successfully"
297
+ }
298
+ else {
299
+ Write-Warn "Claude Code installation may have failed"
300
+ Write-Host " You can install manually: npm install -g @anthropic-ai/claude-code"
301
+ }
302
+ }
303
+ Write-Host ""
304
+ Write-Host " IMPORTANT: Claude Code requires authentication." -ForegroundColor Yellow
305
+ Write-Host " Please run 'claude' in a terminal to complete the authentication process." -ForegroundColor Yellow
306
+ Write-Host ""
307
+
308
+ # Step 3: Create config directory and .env
309
+ Write-Step 3 $totalSteps "Creating config directory and .env..."
310
+ New-Item -Path $DataDir -ItemType Directory -Force | Out-Null
311
+ New-Item -Path $LogDir -ItemType Directory -Force | Out-Null
312
+ $envValues = @{
313
+ 'AGENT_PORT' = '8080'
314
+ 'HEARTBEAT_INTERVAL' = '30'
315
+ 'MINION_USER' = $env:USERNAME
316
+ }
317
+ if ($HqUrl) { $envValues['HQ_URL'] = $HqUrl }
318
+ if ($ApiToken) { $envValues['API_TOKEN'] = $ApiToken }
319
+ if ($MinionId) { $envValues['MINION_ID'] = $MinionId }
320
+ Write-EnvFile $EnvFile $envValues
321
+ Write-Detail "$EnvFile generated"
322
+
323
+ # Step 4: Install node-pty (required for Windows terminal management)
324
+ Write-Step 4 $totalSteps "Installing terminal support (node-pty)..."
325
+ $npmRoot = & npm root -g 2>$null
326
+ $minionPkgDir = Join-Path $npmRoot '@geekbeer\minion'
327
+ if (Test-Path $minionPkgDir) {
328
+ Push-Location $minionPkgDir
329
+ $ptyInstalled = $false
330
+
331
+ # Try prebuilt version first (no Build Tools required)
332
+ try {
333
+ & npm install node-pty-prebuilt-multiarch 2>$null
334
+ Write-Detail "node-pty-prebuilt-multiarch installed (no Build Tools needed)"
335
+ $ptyInstalled = $true
336
+ } catch {}
337
+
338
+ # Fallback: source-compiled version (requires Visual Studio Build Tools)
339
+ if (-not $ptyInstalled) {
340
+ try {
341
+ & npm install node-pty 2>$null
342
+ Write-Detail "node-pty installed (compiled from source)"
343
+ $ptyInstalled = $true
344
+ } catch {}
345
+ }
346
+
347
+ if (-not $ptyInstalled) {
348
+ Write-Warn "node-pty installation failed. Terminal/workflow features may not work."
349
+ Write-Host " Try manually: cd $minionPkgDir && npm install node-pty-prebuilt-multiarch"
350
+ }
351
+ Pop-Location
352
+ }
353
+ else {
354
+ Write-Warn "Minion package not found at $minionPkgDir"
355
+ Write-Host " Please run: npm install -g @geekbeer/minion"
356
+ }
357
+
358
+ # Step 5: Generate start-agent.ps1 and register auto-start
359
+ Write-Step 5 $totalSteps "Registering auto-start..."
360
+ $serverJs = Join-Path $minionPkgDir 'win\server.js'
361
+ if (-not (Test-Path $serverJs)) {
362
+ $serverJs = Join-Path $minionPkgDir 'win' 'server.js'
363
+ }
364
+ if (-not (Test-Path $serverJs)) {
365
+ Write-Error "server.js not found at $serverJs"
366
+ Write-Host " Please run: npm install -g @geekbeer/minion"
367
+ exit 1
368
+ }
369
+
370
+ # Generate start-agent.ps1 (watchdog script)
371
+ $startAgentContent = @"
372
+ # Auto-generated by minion-cli-win setup
373
+ # Starts minion agent with watchdog (auto-restart on crash)
374
+
375
+ `$DataDir = '$DataDir'
376
+ `$EnvFile = Join-Path `$DataDir '.env'
377
+ `$PidFile = Join-Path `$DataDir 'minion-agent.pid'
378
+ `$LogDir = Join-Path `$DataDir 'logs'
379
+ `$ServerJs = '$serverJs'
380
+
381
+ # Load .env
382
+ if (Test-Path `$EnvFile) {
383
+ Get-Content `$EnvFile | ForEach-Object {
384
+ `$line = `$_.Trim()
385
+ if (`$line -and -not `$line.StartsWith('#') -and `$line.Contains('=')) {
386
+ `$idx = `$line.IndexOf('=')
387
+ `$key = `$line.Substring(0, `$idx).Trim()
388
+ `$value = `$line.Substring(`$idx + 1).Trim()
389
+ [Environment]::SetEnvironmentVariable(`$key, `$value, 'Process')
390
+ }
391
+ }
392
+ }
393
+
394
+ New-Item -Path `$LogDir -ItemType Directory -Force | Out-Null
395
+
396
+ # Write PID of this watchdog process
397
+ Set-Content -Path `$PidFile -Value `$PID
398
+
399
+ # Start TightVNC + websockify if available
400
+ `$vncExe = `$null
401
+ if (Test-Path 'C:\Program Files\TightVNC\tvnserver.exe') {
402
+ `$vncExe = 'C:\Program Files\TightVNC\tvnserver.exe'
403
+ } elseif (Test-Path (Join-Path `$DataDir 'tightvnc\PFiles\TightVNC\tvnserver.exe')) {
404
+ `$vncExe = Join-Path `$DataDir 'tightvnc\PFiles\TightVNC\tvnserver.exe'
405
+ }
406
+ if (`$vncExe) {
407
+ # Start VNC server in application mode (no admin, no service registration)
408
+ `$vncProc = Get-Process -Name tvnserver -ErrorAction SilentlyContinue
409
+ if (-not `$vncProc) {
410
+ Start-Process -FilePath `$vncExe -ArgumentList '-run' -WindowStyle Hidden
411
+ # Wait for VNC server to bind port 5900 before starting websockify
412
+ Start-Sleep -Seconds 3
413
+ }
414
+ # Reload registry config (ensures no-auth settings are applied)
415
+ & `$vncExe -controlapp -reload 2>`$null
416
+ if (Get-Command websockify -ErrorAction SilentlyContinue) {
417
+ `$wsProc = Get-Process -Name websockify -ErrorAction SilentlyContinue
418
+ if (-not `$wsProc) {
419
+ Start-Process -FilePath (Get-Command websockify).Source -ArgumentList '6080', 'localhost:5900' -WindowStyle Hidden
420
+ }
421
+ }
422
+ }
423
+
424
+ # Start cloudflared tunnel if configured
425
+ `$cfConfig = Join-Path `$env:USERPROFILE '.cloudflared\config.yml'
426
+ if ((Test-Path `$cfConfig) -and (Get-Command cloudflared -ErrorAction SilentlyContinue)) {
427
+ Start-Process -FilePath (Get-Command cloudflared).Source -ArgumentList 'tunnel', 'run' -WindowStyle Hidden
428
+ }
429
+
430
+ # Watchdog loop: restart node if it crashes
431
+ `$nodePath = (Get-Command node).Source
432
+ while (`$true) {
433
+ `$stdoutLog = Join-Path `$LogDir 'service-stdout.log'
434
+ `$stderrLog = Join-Path `$LogDir 'service-stderr.log'
435
+ `$proc = Start-Process -FilePath `$nodePath -ArgumentList `$ServerJs ``
436
+ -WorkingDirectory `$DataDir ``
437
+ -RedirectStandardOutput `$stdoutLog ``
438
+ -RedirectStandardError `$stderrLog ``
439
+ -WindowStyle Hidden -PassThru
440
+ `$proc.WaitForExit()
441
+ if (`$proc.ExitCode -eq 0) { break }
442
+ Start-Sleep -Seconds 5
443
+ }
444
+
445
+ # Cleanup PID file on normal exit
446
+ Remove-Item `$PidFile -Force -ErrorAction SilentlyContinue
447
+ "@
448
+ Set-Content -Path $StartAgentScript -Value $startAgentContent -Encoding UTF8
449
+ Write-Detail "Generated $StartAgentScript"
450
+
451
+ # Create shortcut in Startup folder
452
+ $startupDir = [Environment]::GetFolderPath('Startup')
453
+ $shortcutPath = Join-Path $startupDir 'MinionAgent.lnk'
454
+ $wsShell = New-Object -ComObject WScript.Shell
455
+ $shortcut = $wsShell.CreateShortcut($shortcutPath)
456
+ $shortcut.TargetPath = 'powershell.exe'
457
+ $shortcut.Arguments = "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$StartAgentScript`""
458
+ $shortcut.WorkingDirectory = $DataDir
459
+ $shortcut.WindowStyle = 7 # Minimized
460
+ $shortcut.Description = 'Minion Agent auto-start'
461
+ $shortcut.Save()
462
+ Write-Detail "Startup shortcut created: $shortcutPath"
463
+
464
+ # Step 6: Install TightVNC Server
465
+ Write-Step 6 $totalSteps "Setting up TightVNC Server..."
466
+ $vncSystemPath = 'C:\Program Files\TightVNC\tvnserver.exe'
467
+ $vncPortableDir = Join-Path $DataDir 'tightvnc'
468
+ $vncPortablePath = Join-Path $vncPortableDir 'PFiles\TightVNC\tvnserver.exe'
469
+
470
+ if (Test-Path $vncSystemPath) {
471
+ Write-Detail "TightVNC Server already installed (system)"
472
+ }
473
+ elseif (Test-Path $vncPortablePath) {
474
+ Write-Detail "TightVNC Server already installed (portable)"
475
+ }
476
+ else {
477
+ Write-Host " Downloading TightVNC portable..."
478
+ try {
479
+ $msiUrl = 'https://www.tightvnc.com/download/2.8.85/tightvnc-2.8.85-gpl-setup-64bit.msi'
480
+ $msiPath = Join-Path $env:TEMP 'tightvnc-setup.msi'
481
+ Invoke-WebRequest -Uri $msiUrl -OutFile $msiPath -UseBasicParsing
482
+ New-Item -Path $vncPortableDir -ItemType Directory -Force | Out-Null
483
+ # Extract MSI without admin (administrative install = extract only)
484
+ # Creates PFiles\TightVNC\ subdirectory structure
485
+ Start-Process msiexec -ArgumentList "/a `"$msiPath`" /qn TARGETDIR=`"$vncPortableDir`"" -Wait -NoNewWindow
486
+ Remove-Item $msiPath -Force -ErrorAction SilentlyContinue
487
+ if (Test-Path $vncPortablePath) {
488
+ Write-Detail "TightVNC portable extracted to $vncPortableDir"
489
+ } else {
490
+ Write-Warn "TightVNC extraction failed. Check $vncPortableDir"
491
+ }
492
+ }
493
+ catch {
494
+ Write-Warn "Failed to download TightVNC: $_"
495
+ Write-Host " Manual download: https://www.tightvnc.com/download.php" -ForegroundColor Gray
496
+ }
497
+ }
498
+
499
+ # Configure TightVNC for no-auth localhost-only mode via registry
500
+ # - LoopbackOnly=1: only accept connections from localhost (security)
501
+ # - AllowLoopback=1: enable loopback connections
502
+ # - UseVncAuthentication=0: no password prompt (websockify connects via localhost,
503
+ # HQ WebSocket proxy provides its own Supabase session authentication)
504
+ $vncRegPath = 'HKCU:\Software\TightVNC\Server'
505
+ if ((Test-Path $vncSystemPath) -or (Test-Path $vncPortablePath)) {
506
+ if (-not (Test-Path $vncRegPath)) {
507
+ New-Item -Path $vncRegPath -Force | Out-Null
508
+ }
509
+ Set-ItemProperty -Path $vncRegPath -Name 'LoopbackOnly' -Value 1 -Type DWord
510
+ Set-ItemProperty -Path $vncRegPath -Name 'AllowLoopback' -Value 1 -Type DWord
511
+ Set-ItemProperty -Path $vncRegPath -Name 'UseVncAuthentication' -Value 0 -Type DWord
512
+ Set-ItemProperty -Path $vncRegPath -Name 'UseControlAuthentication' -Value 0 -Type DWord
513
+ Set-ItemProperty -Path $vncRegPath -Name 'RfbPort' -Value 5900 -Type DWord
514
+ # Reload config if tvnserver is already running (re-setup scenario)
515
+ $vncProc = Get-Process -Name tvnserver -ErrorAction SilentlyContinue
516
+ if ($vncProc) {
517
+ $vncExePath = if (Test-Path $vncSystemPath) { $vncSystemPath } else { $vncPortablePath }
518
+ & $vncExePath -controlapp -reload 2>$null
519
+ Write-Detail "TightVNC config reloaded"
520
+ }
521
+ Write-Detail "TightVNC configured: localhost-only, no VNC password (auth via HQ proxy)"
522
+ }
523
+
524
+ # Step 7: Setup websockify (WebSocket proxy for VNC)
525
+ Write-Step 7 $totalSteps "Setting up websockify..."
526
+ if (Test-CommandExists 'websockify') {
527
+ Write-Detail "websockify already installed"
528
+ }
529
+ else {
530
+ # Ensure Python is installed (Microsoft Store stub doesn't include pip)
531
+ $pythonUsable = $false
532
+ if (Test-CommandExists 'python') {
533
+ $pyVer = & python --version 2>&1
534
+ # Microsoft Store stub returns just "Python" with no version number
535
+ if ($pyVer -match '\d+\.\d+') { $pythonUsable = $true }
536
+ }
537
+ if (-not $pythonUsable) {
538
+ Write-Host " Python not found. Installing via winget..."
539
+ try {
540
+ & winget install --id Python.Python.3.12 --accept-package-agreements --accept-source-agreements
541
+ $env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH', 'User')
542
+ Write-Detail "Python installed"
543
+ }
544
+ catch {
545
+ Write-Warn "Failed to install Python: $_"
546
+ Write-Host " Install manually: winget install Python.Python.3.12"
547
+ }
548
+ }
549
+
550
+ Write-Host " Installing websockify via pip..."
551
+ try {
552
+ if (Test-CommandExists 'pip') {
553
+ & pip install websockify 2>$null
554
+ Write-Detail "websockify installed"
555
+ }
556
+ elseif (Test-CommandExists 'pip3') {
557
+ & pip3 install websockify 2>$null
558
+ Write-Detail "websockify installed"
559
+ }
560
+ elseif (Test-CommandExists 'python') {
561
+ & python -m pip install websockify 2>$null
562
+ Write-Detail "websockify installed (via python -m pip)"
563
+ }
564
+ else {
565
+ Write-Warn "pip not found. Install Python first, then: pip install websockify"
566
+ }
567
+ }
568
+ catch {
569
+ Write-Warn "Failed to install websockify: $_"
570
+ }
571
+ }
572
+
573
+ # Step 8 (optional): Cloudflare Tunnel
574
+ if ($SetupTunnel) {
575
+ $currentStep = $totalSteps - 1
576
+ Write-Step $currentStep $totalSteps "Setting up Cloudflare Tunnel..."
577
+
578
+ if (-not $HqUrl -or -not $ApiToken) {
579
+ Write-Error "--setup-tunnel requires --hq-url and --api-token"
580
+ exit 1
581
+ }
582
+
583
+ # Install cloudflared
584
+ if (-not (Test-CommandExists 'cloudflared')) {
585
+ Write-Host " Installing cloudflared..."
586
+ if (Test-CommandExists 'winget') {
587
+ & winget install --id Cloudflare.cloudflared --accept-package-agreements --accept-source-agreements
588
+ $env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH', 'User')
589
+ }
590
+ elseif (Test-CommandExists 'choco') {
591
+ & choco install cloudflared -y
592
+ }
593
+ else {
594
+ Write-Host " Downloading cloudflared..."
595
+ $cfUrl = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe'
596
+ $cfPath = Join-Path $DataDir 'cloudflared.exe'
597
+ Invoke-WebRequest -Uri $cfUrl -OutFile $cfPath
598
+ Write-Detail "cloudflared downloaded to $cfPath"
599
+ }
600
+ }
601
+ else {
602
+ Write-Detail "cloudflared already installed"
603
+ }
604
+
605
+ # Fetch tunnel config from HQ
606
+ Write-Host " Fetching tunnel configuration from HQ..."
607
+ try {
608
+ $headers = @{ 'Authorization' = "Bearer $ApiToken" }
609
+ $tunnelData = Invoke-RestMethod -Uri "$HqUrl/api/minion/tunnel-credentials" -Headers $headers
610
+
611
+ if ($tunnelData.tunnel_id) {
612
+ $cfConfigDir = Join-Path $env:USERPROFILE '.cloudflared'
613
+ New-Item -Path $cfConfigDir -ItemType Directory -Force | Out-Null
614
+
615
+ # Save credentials
616
+ $tunnelData.credentials_json | Set-Content (Join-Path $cfConfigDir "$($tunnelData.tunnel_id).json") -Encoding UTF8
617
+ Write-Detail "Tunnel credentials saved"
618
+
619
+ # Save config
620
+ $tunnelData.config_yml | Set-Content (Join-Path $cfConfigDir 'config.yml') -Encoding UTF8
621
+ Write-Detail "Tunnel config saved (will run as user process on start)"
622
+ }
623
+ else {
624
+ Write-Warn "Tunnel not configured for this minion"
625
+ }
626
+ }
627
+ catch {
628
+ Write-Warn "Failed to fetch tunnel credentials: $_"
629
+ }
630
+ }
631
+
632
+ # Deploy bundled skills
633
+ Write-Host ""
634
+ Write-Host "Deploying bundled assets..."
635
+ $claudeSkillsDir = Join-Path $env:USERPROFILE '.claude\skills'
636
+ $bundledSkillsDir = Join-Path $minionPkgDir 'skills'
637
+ if (Test-Path $bundledSkillsDir) {
638
+ New-Item -Path $claudeSkillsDir -ItemType Directory -Force | Out-Null
639
+ Get-ChildItem -Path $bundledSkillsDir -Directory | ForEach-Object {
640
+ Copy-Item $_.FullName -Destination (Join-Path $claudeSkillsDir $_.Name) -Recurse -Force
641
+ Write-Detail "Deployed skill: $($_.Name)"
642
+ }
643
+ }
644
+
645
+ # Deploy bundled rules
646
+ $claudeRulesDir = Join-Path $env:USERPROFILE '.claude\rules'
647
+ $bundledRulesDir = Join-Path $minionPkgDir 'rules'
648
+ if (Test-Path $bundledRulesDir) {
649
+ New-Item -Path $claudeRulesDir -ItemType Directory -Force | Out-Null
650
+ $coreRule = Join-Path $bundledRulesDir 'core.md'
651
+ if (Test-Path $coreRule) {
652
+ Copy-Item $coreRule -Destination (Join-Path $claudeRulesDir 'core.md') -Force
653
+ Write-Detail "Deployed rules: core.md"
654
+ }
655
+ }
656
+
657
+ # Start (or restart) agent so updated .env takes effect
658
+ Write-Step $totalSteps $totalSteps "Starting agent..."
659
+ Restart-MinionProcess
660
+
661
+ # Health check
662
+ if (Invoke-HealthCheck) {
663
+ Write-Detail "Health check passed"
664
+ }
665
+ else {
666
+ Write-Warn "Health check failed after 5 attempts"
667
+ Write-Host " Check logs at: $LogDir"
668
+ }
669
+
670
+ # Firewall notice
671
+ Write-Host ""
672
+ Write-Host "NOTE: Firewall rules are NOT configured automatically (requires admin)." -ForegroundColor Yellow
673
+ Write-Host " If you need LAN access without Cloudflare Tunnel, ask an administrator to run:" -ForegroundColor Yellow
674
+ Write-Host " New-NetFirewallRule -DisplayName 'Minion Agent' -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Allow" -ForegroundColor Gray
675
+ Write-Host " New-NetFirewallRule -DisplayName 'Minion Terminal' -Direction Inbound -Protocol TCP -LocalPort 7681 -Action Allow" -ForegroundColor Gray
676
+ Write-Host " New-NetFirewallRule -DisplayName 'Minion VNC' -Direction Inbound -Protocol TCP -LocalPort 6080 -Action Allow" -ForegroundColor Gray
677
+ Write-Host " Or use Cloudflare Tunnel (--setup-tunnel) to avoid firewall configuration." -ForegroundColor Yellow
678
+
679
+ # Notify HQ
680
+ if ($HqUrl -and $ApiToken) {
681
+ Write-Host ""
682
+ Write-Host "Notifying HQ of setup completion..."
683
+ try {
684
+ $lanIp = Get-LanIPAddress
685
+ $headers = @{
686
+ 'Authorization' = "Bearer $ApiToken"
687
+ 'Content-Type' = 'application/json'
688
+ }
689
+ $bodyHash = @{}
690
+ if ($lanIp) {
691
+ $bodyHash['ip_address'] = $lanIp
692
+ $bodyHash['internal_ip_address'] = $lanIp
693
+ } else {
694
+ $bodyHash['internal_ip_address'] = [System.Net.Dns]::GetHostName()
695
+ }
696
+ $body = $bodyHash | ConvertTo-Json
697
+ Invoke-RestMethod -Uri "$HqUrl/api/minion/setup-complete" -Method Post -Headers $headers -Body $body -ErrorAction Stop | Out-Null
698
+ if ($lanIp) {
699
+ Write-Detail "HQ notified successfully (LAN IP: $lanIp)"
700
+ } else {
701
+ Write-Detail "HQ notified successfully (LAN IP detection failed, hostname sent)"
702
+ }
703
+ }
704
+ catch {
705
+ Write-Detail "HQ notification skipped (HQ may not be reachable)"
706
+ }
707
+ }
708
+
709
+ Write-Host ""
710
+ Write-Host "=========================================" -ForegroundColor Green
711
+ Write-Host " Setup Complete!" -ForegroundColor Green
712
+ Write-Host "=========================================" -ForegroundColor Green
713
+ Write-Host ""
714
+ Write-Host "Useful commands:"
715
+ Write-Host " minion-cli-win status # Agent status"
716
+ Write-Host " minion-cli-win health # Health check"
717
+ Write-Host " minion-cli-win restart # Restart agent"
718
+ Write-Host " minion-cli-win stop # Stop agent"
719
+ Write-Host " Get-Content $(Join-Path $LogDir 'service-stdout.log') -Tail 50 # View logs"
720
+ }
721
+
722
+ # ============================================================
723
+ # Reconfigure
724
+ # ============================================================
725
+
726
+ function Invoke-Reconfigure {
727
+ if (-not $HqUrl -or -not $MinionId -or -not $ApiToken) {
728
+ Write-Error "All three parameters are required: --hq-url, --minion-id, --api-token"
729
+ exit 1
730
+ }
731
+ if (-not (Test-Path $EnvFile)) {
732
+ Write-Error "$EnvFile not found. Run 'setup' first."
733
+ exit 1
734
+ }
735
+
736
+ Write-Host "=========================================" -ForegroundColor Cyan
737
+ Write-Host " @geekbeer/minion Reconfigure" -ForegroundColor Cyan
738
+ Write-Host "=========================================" -ForegroundColor Cyan
739
+ Write-Host "HQ: $HqUrl"
740
+ Write-Host "Minion ID: $MinionId"
741
+ Write-Host ""
742
+
743
+ # Step 1: Update .env
744
+ Write-Step 1 4 "Updating .env credentials..."
745
+ $existing = Read-EnvFile $EnvFile
746
+ $existing['HQ_URL'] = $HqUrl
747
+ $existing['API_TOKEN'] = $ApiToken
748
+ $existing['MINION_ID'] = $MinionId
749
+ Write-EnvFile $EnvFile $existing
750
+ Write-Detail "$EnvFile updated"
751
+
752
+ # Step 2: Restart agent process
753
+ Write-Step 2 4 "Restarting minion-agent..."
754
+ Restart-MinionProcess
755
+
756
+ # Step 3: Health check
757
+ Write-Step 3 4 "Verifying agent health..."
758
+ if (Invoke-HealthCheck) {
759
+ Write-Detail "Agent is healthy"
760
+ }
761
+ else {
762
+ Write-Warn "Agent health check failed after 5 attempts"
763
+ }
764
+
765
+ # Step 4: Notify HQ
766
+ Write-Step 4 4 "Notifying HQ..."
767
+ try {
768
+ $lanIp = Get-LanIPAddress
769
+ $headers = @{
770
+ 'Authorization' = "Bearer $ApiToken"
771
+ 'Content-Type' = 'application/json'
772
+ }
773
+ $bodyHash = @{}
774
+ if ($lanIp) {
775
+ $bodyHash['ip_address'] = $lanIp
776
+ $bodyHash['internal_ip_address'] = $lanIp
777
+ } else {
778
+ $bodyHash['internal_ip_address'] = [System.Net.Dns]::GetHostName()
779
+ }
780
+ $body = $bodyHash | ConvertTo-Json
781
+ Invoke-RestMethod -Uri "$HqUrl/api/minion/setup-complete" -Method Post -Headers $headers -Body $body -ErrorAction Stop | Out-Null
782
+ if ($lanIp) {
783
+ Write-Detail "HQ notified successfully (LAN IP: $lanIp)"
784
+ } else {
785
+ Write-Detail "HQ notified successfully (LAN IP detection failed, hostname sent)"
786
+ }
787
+ }
788
+ catch {
789
+ Write-Detail "Skipped (heartbeat will notify HQ within 30s)"
790
+ }
791
+
792
+ Write-Host ""
793
+ Write-Host "=========================================" -ForegroundColor Green
794
+ Write-Host " Reconfigure Complete!" -ForegroundColor Green
795
+ Write-Host "=========================================" -ForegroundColor Green
796
+ Write-Host "The minion should appear online in HQ shortly."
797
+ }
798
+
799
+ # ============================================================
800
+ # Main command dispatch
801
+ # ============================================================
802
+
803
+ switch ($Command) {
804
+ 'setup' {
805
+ Invoke-Setup
806
+ }
807
+ 'reconfigure' {
808
+ Invoke-Reconfigure
809
+ }
810
+ 'start' {
811
+ Start-MinionProcess
812
+ }
813
+ 'stop' {
814
+ Stop-MinionProcess
815
+ }
816
+ 'restart' {
817
+ Restart-MinionProcess
818
+ }
819
+ 'status' {
820
+ try {
821
+ $response = Invoke-RestMethod -Uri "$AgentUrl/api/status" -TimeoutSec 5
822
+ $response | ConvertTo-Json -Depth 5
823
+ }
824
+ catch {
825
+ Write-Error "Failed to get status. Is the agent running?"
826
+ }
827
+ }
828
+ 'health' {
829
+ try {
830
+ $response = Invoke-RestMethod -Uri "$AgentUrl/api/health" -TimeoutSec 5
831
+ $response | ConvertTo-Json -Depth 5
832
+ }
833
+ catch {
834
+ Write-Error "Health check failed. Is the agent running?"
835
+ }
836
+ }
837
+ 'version' {
838
+ Write-Host "@geekbeer/minion v$CliVersion (Windows)"
839
+ }
840
+ 'help' {
841
+ Write-Host "Minion Agent CLI (@geekbeer/minion) v$CliVersion (Windows)" -ForegroundColor Cyan
842
+ Write-Host ""
843
+ Write-Host "Usage (no admin required):"
844
+ Write-Host " minion-cli-win setup [options] # Set up agent (auto-start on login)"
845
+ Write-Host " minion-cli-win reconfigure [options] # Re-register with new HQ credentials"
846
+ Write-Host " minion-cli-win start # Start agent process"
847
+ Write-Host " minion-cli-win stop # Stop agent process"
848
+ Write-Host " minion-cli-win restart # Restart agent process"
849
+ Write-Host " minion-cli-win status # Get current status"
850
+ Write-Host " minion-cli-win health # Health check"
851
+ Write-Host " minion-cli-win version # Show version"
852
+ Write-Host ""
853
+ Write-Host "Setup options:"
854
+ Write-Host " --hq-url <URL> HQ server URL (optional, omit for standalone mode)"
855
+ Write-Host " --minion-id <UUID> Minion ID (optional)"
856
+ Write-Host " --api-token <TOKEN> API token (optional)"
857
+ Write-Host " --setup-tunnel Set up cloudflared tunnel (requires --hq-url and --api-token)"
858
+ Write-Host ""
859
+ Write-Host "Reconfigure options:"
860
+ Write-Host " --hq-url <URL> HQ server URL (required)"
861
+ Write-Host " --minion-id <UUID> Minion ID (required)"
862
+ Write-Host " --api-token <TOKEN> API token (required)"
863
+ Write-Host ""
864
+ Write-Host "Data directory: $DataDir"
865
+ Write-Host ""
866
+ Write-Host "Environment:"
867
+ Write-Host " MINION_AGENT_URL Agent URL (default: http://localhost:8080)"
868
+ }
869
+ }