@geekbeer/minion 3.5.1 → 3.5.6

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 (2) hide show
  1. package/package.json +1 -1
  2. package/win/minion-cli.ps1 +156 -163
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "3.5.1",
3
+ "version": "3.5.6",
4
4
  "description": "AI Agent runtime for Minion - manages status and skill deployment on VPS",
5
5
  "main": "linux/server.js",
6
6
  "bin": {
@@ -35,8 +35,6 @@ while ($i -lt $args.Count) {
35
35
 
36
36
  $ErrorActionPreference = 'Stop'
37
37
 
38
- # Load System.Web for password generation (used in setup)
39
- Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue
40
38
 
41
39
  # ============================================================
42
40
  # Require Administrator for service management commands
@@ -280,7 +278,7 @@ function Invoke-HealthCheck {
280
278
  function Assert-NssmAvailable {
281
279
  if (-not $NssmPath -or -not (Test-Path $NssmPath)) {
282
280
  Write-Error "NSSM not found. Expected at: $vendorNssm"
283
- Write-Host " Reinstall the package: npm install -g @geekbeer/minion" -ForegroundColor Yellow
281
+ Write-Host " Reinstall the package (admin PowerShell): npm install -g @geekbeer/minion" -ForegroundColor Yellow
284
282
  exit 1
285
283
  }
286
284
  }
@@ -392,7 +390,7 @@ function Restart-MinionService {
392
390
  # ============================================================
393
391
 
394
392
  function Invoke-Setup {
395
- $totalSteps = 10
393
+ $totalSteps = 11
396
394
 
397
395
  # Minionization warning
398
396
  Write-Host ""
@@ -403,7 +401,6 @@ function Invoke-Setup {
403
401
 
404
402
  Write-Host " This setup will:" -ForegroundColor Yellow
405
403
  Write-Host " - Install and configure software (Node.js, Claude Code, VNC)"
406
- Write-Host " - Create dedicated 'minion' service account"
407
404
  Write-Host " - Register Windows Services via NSSM"
408
405
  Write-Host " - Configure firewall rules"
409
406
  Write-Host ""
@@ -431,6 +428,14 @@ function Invoke-Setup {
431
428
 
432
429
  # Save setup user's SID for SDDL grants (so non-admin can control services later)
433
430
  $setupUserSid = ([System.Security.Principal.WindowsIdentity]::GetCurrent().User).Value
431
+ # Also resolve target user's SID (may differ from setup user when run from admin account)
432
+ $targetUserName = Split-Path $TargetUserProfile -Leaf
433
+ try {
434
+ $targetUserSid = (New-Object System.Security.Principal.NTAccount($targetUserName)).Translate(
435
+ [System.Security.Principal.SecurityIdentifier]).Value
436
+ } catch {
437
+ $targetUserSid = $null
438
+ }
434
439
  New-Item -Path $DataDir -ItemType Directory -Force | Out-Null
435
440
  [System.IO.File]::WriteAllText((Join-Path $DataDir '.setup-user-sid'), $setupUserSid)
436
441
  # Save target user profile so configure/uninstall can find it
@@ -571,74 +576,8 @@ function Invoke-Setup {
571
576
  Write-Host " Please run 'claude' in a terminal to complete the authentication process." -ForegroundColor Yellow
572
577
  Write-Host ""
573
578
 
574
- # Step 4: Create dedicated 'minion' service account
575
- Write-Step 4 $totalSteps "Creating dedicated service account..."
576
- $MinionSvcUser = 'minion'
577
- $MinionSvcUserFull = ".\$MinionSvcUser"
578
- $minionUserExists = [bool](Get-LocalUser -Name $MinionSvcUser -ErrorAction SilentlyContinue)
579
- if ($minionUserExists) {
580
- Write-Detail "Service account '$MinionSvcUser' already exists"
581
- } else {
582
- # Generate a random password (service account — not used interactively)
583
- $svcPassword = [System.Web.Security.Membership]::GeneratePassword(24, 4)
584
- $securePassword = ConvertTo-SecureString $svcPassword -AsPlainText -Force
585
- New-LocalUser -Name $MinionSvcUser -Password $securePassword -Description 'Minion Agent Service Account' -PasswordNeverExpires -UserMayNotChangePassword -AccountNeverExpires | Out-Null
586
- # Deny interactive/remote logon (service-only account)
587
- & net localgroup "Users" $MinionSvcUser /delete 2>$null
588
- Write-Detail "Service account '$MinionSvcUser' created (non-interactive)"
589
- }
590
- # Store password for NSSM ObjectName configuration
591
- if (-not $minionUserExists) {
592
- # Save password to a protected file for NSSM service registration
593
- $svcPasswordFile = Join-Path $DataDir '.svc-password'
594
- New-Item -Path (Split-Path $svcPasswordFile) -ItemType Directory -Force | Out-Null
595
- [System.IO.File]::WriteAllText($svcPasswordFile, $svcPassword)
596
- # Restrict file access to current user only
597
- $acl = Get-Acl $svcPasswordFile
598
- $acl.SetAccessRuleProtection($true, $false)
599
- $adminRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
600
- [System.Security.Principal.WindowsIdentity]::GetCurrent().Name, 'FullControl', 'Allow')
601
- $acl.AddAccessRule($adminRule)
602
- Set-Acl $svcPasswordFile $acl
603
- Write-Detail "Service account credentials stored"
604
- } else {
605
- $svcPasswordFile = Join-Path $DataDir '.svc-password'
606
- if (Test-Path $svcPasswordFile) {
607
- $svcPassword = [System.IO.File]::ReadAllText($svcPasswordFile).Trim()
608
- } else {
609
- # Re-generate password for existing account (reset)
610
- $svcPassword = [System.Web.Security.Membership]::GeneratePassword(24, 4)
611
- $securePassword = ConvertTo-SecureString $svcPassword -AsPlainText -Force
612
- Set-LocalUser -Name $MinionSvcUser -Password $securePassword
613
- New-Item -Path (Split-Path $svcPasswordFile) -ItemType Directory -Force | Out-Null
614
- [System.IO.File]::WriteAllText($svcPasswordFile, $svcPassword)
615
- $acl = Get-Acl $svcPasswordFile
616
- $acl.SetAccessRuleProtection($true, $false)
617
- $adminRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
618
- [System.Security.Principal.WindowsIdentity]::GetCurrent().Name, 'FullControl', 'Allow')
619
- $acl.AddAccessRule($adminRule)
620
- Set-Acl $svcPasswordFile $acl
621
- Write-Detail "Service account password reset and stored"
622
- }
623
- }
624
- # Grant 'Log on as a service' right to the minion user
625
- $tempCfg = Join-Path $env:TEMP 'minion-secedit.cfg'
626
- $tempDb = Join-Path $env:TEMP 'minion-secedit.sdb'
627
- & secedit /export /cfg $tempCfg /areas USER_RIGHTS 2>$null
628
- $cfgContent = Get-Content $tempCfg -Raw
629
- if ($cfgContent -match 'SeServiceLogonRight\s*=\s*(.*)') {
630
- $existing = $Matches[1]
631
- if ($existing -notmatch $MinionSvcUser) {
632
- $cfgContent = $cfgContent -replace "(SeServiceLogonRight\s*=\s*)(.*)", "`$1`$2,$MinionSvcUser"
633
- [System.IO.File]::WriteAllText($tempCfg, $cfgContent)
634
- & secedit /configure /db $tempDb /cfg $tempCfg /areas USER_RIGHTS 2>$null
635
- Write-Detail "Granted 'Log on as a service' right to '$MinionSvcUser'"
636
- }
637
- }
638
- Remove-Item $tempCfg, $tempDb -Force -ErrorAction SilentlyContinue
639
-
640
- # Step 5: Create config directory and default .env
641
- Write-Step 5 $totalSteps "Creating config directory..."
579
+ # Step 4: Create config directory and default .env
580
+ Write-Step 4 $totalSteps "Creating config directory..."
642
581
  New-Item -Path $DataDir -ItemType Directory -Force | Out-Null
643
582
  New-Item -Path $LogDir -ItemType Directory -Force | Out-Null
644
583
  if (-not (Test-Path $EnvFile)) {
@@ -653,16 +592,8 @@ function Invoke-Setup {
653
592
  Write-Detail "$EnvFile already exists, preserving"
654
593
  }
655
594
 
656
- # Grant minion service account read/write access to data directory
657
- $minionAcl = Get-Acl $DataDir
658
- $minionRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
659
- $MinionSvcUser, 'Modify', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
660
- $minionAcl.AddAccessRule($minionRule)
661
- Set-Acl $DataDir $minionAcl
662
- Write-Detail "Granted '$MinionSvcUser' access to $DataDir"
663
-
664
- # Step 6: Install node-pty (required for Windows terminal management)
665
- Write-Step 6 $totalSteps "Installing terminal support (node-pty)..."
595
+ # Step 5: Install node-pty (required for Windows terminal management)
596
+ Write-Step 5 $totalSteps "Installing terminal support (node-pty)..."
666
597
  $minionPkgDir = $CliDir
667
598
  if (Test-Path $minionPkgDir) {
668
599
  Push-Location $minionPkgDir
@@ -696,22 +627,22 @@ function Invoke-Setup {
696
627
  }
697
628
  else {
698
629
  Write-Warn "Minion package not found at $minionPkgDir"
699
- Write-Host " Please run: npm install -g @geekbeer/minion"
630
+ Write-Host " Please run (admin PowerShell): npm install -g @geekbeer/minion"
700
631
  }
701
632
 
702
633
  # Step 7: Verify NSSM
703
- Write-Step 7 $totalSteps "Verifying NSSM..."
634
+ Write-Step 6 $totalSteps "Verifying NSSM..."
704
635
  Assert-NssmAvailable
705
636
  $nssmVersion = Invoke-Nssm version
706
637
  Write-Detail "NSSM available: $NssmPath ($nssmVersion)"
707
638
 
708
639
  # Step 8: Register Windows Services via NSSM
709
- Write-Step 8 $totalSteps "Registering Windows Services..."
640
+ Write-Step 7 $totalSteps "Registering Windows Services..."
710
641
 
711
642
  $serverJs = Join-Path $minionPkgDir 'win\server.js'
712
643
  if (-not (Test-Path $serverJs)) {
713
644
  Write-Error "server.js not found at $serverJs"
714
- Write-Host " Please run: npm install -g @geekbeer/minion"
645
+ Write-Host " Please run (admin PowerShell): npm install -g @geekbeer/minion"
715
646
  exit 1
716
647
  }
717
648
  $nodePath = (Get-Command node).Source
@@ -743,13 +674,15 @@ function Invoke-Setup {
743
674
  Invoke-Nssm set minion-agent Start SERVICE_AUTO_START
744
675
  Invoke-Nssm set minion-agent DisplayName "Minion Agent"
745
676
  Invoke-Nssm set minion-agent Description "GeekBeer Minion AI Agent Service"
746
- # Run as dedicated minion service account (not LocalSystem)
747
- Invoke-Nssm set minion-agent ObjectName $MinionSvcUserFull $svcPassword
677
+ # Runs as LocalSystem (NSSM default). USERPROFILE/HOME env vars point to target user's profile.
748
678
  Grant-ServiceControlToUser 'minion-agent' $setupUserSid
749
- Write-Detail "minion-agent service registered (runs as '$MinionSvcUser')"
679
+ if ($targetUserSid -and $targetUserSid -ne $setupUserSid) {
680
+ Grant-ServiceControlToUser 'minion-agent' $targetUserSid
681
+ }
682
+ Write-Detail "minion-agent service registered (runs as LocalSystem)"
750
683
 
751
- # Step 9: Install and configure TightVNC (runs as LocalSystem for desktop capture)
752
- Write-Step 9 $totalSteps "Setting up TightVNC Server..."
684
+ # Step 9: Install and configure TightVNC (runs as logon task in user session for desktop capture)
685
+ Write-Step 8 $totalSteps "Setting up TightVNC Server..."
753
686
  $vncSystemPath = 'C:\Program Files\TightVNC\tvnserver.exe'
754
687
  $vncPortableDir = Join-Path $DataDir 'tightvnc'
755
688
  $vncPortablePath = Join-Path $vncPortableDir 'PFiles\TightVNC\tvnserver.exe'
@@ -798,31 +731,32 @@ function Invoke-Setup {
798
731
  }
799
732
 
800
733
  # Configure TightVNC registry (localhost-only, no VNC auth)
801
- $vncRegPath = 'HKCU:\Software\TightVNC\Server'
734
+ # Write to both HKCU (for user-session -run mode) and HKLM (fallback)
802
735
  if ($vncExePath) {
803
- if (-not (Test-Path $vncRegPath)) {
804
- New-Item -Path $vncRegPath -Force | Out-Null
736
+ foreach ($vncRegPath in @('HKCU:\Software\TightVNC\Server', 'HKLM:\Software\TightVNC\Server')) {
737
+ if (-not (Test-Path $vncRegPath)) {
738
+ New-Item -Path $vncRegPath -Force | Out-Null
739
+ }
740
+ Set-ItemProperty -Path $vncRegPath -Name 'LoopbackOnly' -Value 1 -Type DWord
741
+ Set-ItemProperty -Path $vncRegPath -Name 'AllowLoopback' -Value 1 -Type DWord
742
+ Set-ItemProperty -Path $vncRegPath -Name 'UseVncAuthentication' -Value 0 -Type DWord
743
+ Set-ItemProperty -Path $vncRegPath -Name 'UseControlAuthentication' -Value 0 -Type DWord
744
+ Set-ItemProperty -Path $vncRegPath -Name 'RfbPort' -Value 5900 -Type DWord
805
745
  }
806
- Set-ItemProperty -Path $vncRegPath -Name 'LoopbackOnly' -Value 1 -Type DWord
807
- Set-ItemProperty -Path $vncRegPath -Name 'AllowLoopback' -Value 1 -Type DWord
808
- Set-ItemProperty -Path $vncRegPath -Name 'UseVncAuthentication' -Value 0 -Type DWord
809
- Set-ItemProperty -Path $vncRegPath -Name 'UseControlAuthentication' -Value 0 -Type DWord
810
- Set-ItemProperty -Path $vncRegPath -Name 'RfbPort' -Value 5900 -Type DWord
746
+ Write-Detail "TightVNC registry configured (HKCU + HKLM)"
811
747
 
812
- # Register VNC as NSSM service (application mode, not TightVNC's own service)
748
+ # Remove legacy NSSM service if present (VNC now runs as logon task)
813
749
  Invoke-Nssm stop minion-vnc
814
750
  Invoke-Nssm remove minion-vnc confirm
815
- Invoke-Nssm install minion-vnc $vncExePath '-run'
816
- Invoke-Nssm set minion-vnc Start SERVICE_AUTO_START
817
- Invoke-Nssm set minion-vnc DisplayName "Minion VNC Server"
818
- Invoke-Nssm set minion-vnc Description "TightVNC for Minion remote desktop"
819
- Invoke-Nssm set minion-vnc AppRestartDelay 3000
820
- Grant-ServiceControlToUser 'minion-vnc' $setupUserSid
821
- Write-Detail "minion-vnc service registered"
751
+
752
+ # Register VNC as logon task (must run in user session for desktop capture)
753
+ schtasks /Delete /TN "MinionVNC" /F 2>$null
754
+ schtasks /Create /TN "MinionVNC" /TR "'$vncExePath' -run" /SC ONLOGON /RL HIGHEST /F | Out-Null
755
+ Write-Detail "TightVNC registered as logon task (user session, not service)"
822
756
  }
823
757
 
824
758
  # Step 10: Setup websockify (runs as LocalSystem, paired with VNC)
825
- Write-Step 10 $totalSteps "Setting up websockify..."
759
+ Write-Step 9 $totalSteps "Setting up websockify..."
826
760
  [array]$wsCmd = Get-WebsockifyCommand
827
761
  if (-not $wsCmd) {
828
762
  # Ensure Python is installed
@@ -878,7 +812,7 @@ function Invoke-Setup {
878
812
  }
879
813
 
880
814
  if ($wsCmd -and $vncExePath) {
881
- # Register websockify as NSSM service
815
+ # Register websockify as NSSM service (no dependency on minion-vnc — VNC runs as logon task)
882
816
  Invoke-Nssm stop minion-websockify
883
817
  Invoke-Nssm remove minion-websockify confirm
884
818
  if ($wsCmd.Count -eq 1) {
@@ -888,19 +822,21 @@ function Invoke-Setup {
888
822
  $wsArgs = ($wsCmd[1..($wsCmd.Count-1)] + @('6080', 'localhost:5900')) -join ' '
889
823
  Invoke-Nssm install minion-websockify $wsCmd[0] $wsArgs
890
824
  }
891
- Invoke-Nssm set minion-websockify DependOnService minion-vnc
892
825
  Invoke-Nssm set minion-websockify Start SERVICE_AUTO_START
893
826
  Invoke-Nssm set minion-websockify DisplayName "Minion Websockify"
894
827
  Invoke-Nssm set minion-websockify Description "WebSocket proxy for VNC (6080 -> 5900)"
895
828
  Invoke-Nssm set minion-websockify AppRestartDelay 3000
896
829
  Grant-ServiceControlToUser 'minion-websockify' $setupUserSid
897
- Write-Detail "minion-websockify service registered (depends on minion-vnc)"
830
+ if ($targetUserSid -and $targetUserSid -ne $setupUserSid) {
831
+ Grant-ServiceControlToUser 'minion-websockify' $targetUserSid
832
+ }
833
+ Write-Detail "minion-websockify service registered"
898
834
  } else {
899
835
  Write-Warn "websockify not available, VNC WebSocket proxy will not be registered"
900
836
  }
901
837
 
902
838
  # Step 11: Disable screensaver, lock screen, and sleep
903
- Write-Step 11 $totalSteps "Disabling screensaver, lock screen, and sleep..."
839
+ Write-Step 10 $totalSteps "Disabling screensaver, lock screen, and sleep..."
904
840
  Set-ItemProperty -Path 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive -Value '0'
905
841
  Set-ItemProperty -Path 'HKCU:\Control Panel\Desktop' -Name ScreenSaveTimeOut -Value '0'
906
842
  Set-ItemProperty -Path 'HKCU:\Control Panel\Desktop' -Name SCRNSAVE.EXE -Value ''
@@ -914,7 +850,7 @@ function Invoke-Setup {
914
850
  Write-Detail "Sleep and monitor timeout disabled"
915
851
 
916
852
  # Configure firewall rules
917
- Write-Step 10 $totalSteps "Configuring firewall rules..."
853
+ Write-Step 11 $totalSteps "Configuring firewall rules..."
918
854
  $fwRules = @(
919
855
  @{ Name = 'Minion Agent'; Port = 8080 },
920
856
  @{ Name = 'Minion Terminal'; Port = 7681 },
@@ -930,25 +866,73 @@ function Invoke-Setup {
930
866
  }
931
867
  }
932
868
 
933
- # Grant minion service account access to .claude directory (skills, rules, settings)
934
- $claudeDir = Join-Path $TargetUserProfile '.claude'
935
- New-Item -Path $claudeDir -ItemType Directory -Force | Out-Null
936
- $claudeAcl = Get-Acl $claudeDir
937
- $claudeRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
938
- $MinionSvcUser, 'Modify', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
939
- $claudeAcl.AddAccessRule($claudeRule)
940
- Set-Acl $claudeDir $claudeAcl
941
- Write-Detail "Granted '$MinionSvcUser' access to $claudeDir"
869
+ # Grant target user access to admin's minion package and bin links (so target user can run minion-cli-win)
870
+ $adminNpmBin = Split-Path (Get-Command minion-cli-win -ErrorAction SilentlyContinue).Source -ErrorAction SilentlyContinue
871
+ if (-not $adminNpmBin) {
872
+ $adminNpmBin = & npm config get prefix 2>$null
873
+ }
874
+ if ($adminNpmBin -and ($adminNpmBin -ne (Join-Path $TargetUserProfile 'AppData\Roaming\npm'))) {
875
+ $targetUserName = Split-Path $TargetUserProfile -Leaf
876
+
877
+ # Grant ReadAndExecute on bin links (minion-cli-win.cmd, hq-win.cmd, etc.)
878
+ $binFiles = Get-ChildItem -Path $adminNpmBin -Filter '*minion*' -ErrorAction SilentlyContinue
879
+ $binFiles += Get-ChildItem -Path $adminNpmBin -Filter '*hq-win*' -ErrorAction SilentlyContinue
880
+ foreach ($f in $binFiles) {
881
+ icacls $f.FullName /grant "${targetUserName}:(RX)" /Q 2>$null | Out-Null
882
+ }
883
+ # Grant ReadAndExecute on the @geekbeer/minion package directory (recursive)
884
+ $minionPkgDir = Join-Path (Join-Path $adminNpmBin 'node_modules') '@geekbeer\minion'
885
+ if (Test-Path $minionPkgDir) {
886
+ icacls $minionPkgDir /grant "${targetUserName}:(OI)(CI)RX" /T /Q 2>$null | Out-Null
887
+ Write-Detail "Granted target user read access to $minionPkgDir"
888
+ }
889
+ # Grant traverse access on ancestor directories so the path is reachable
890
+ # e.g., C:\Users\yunoda -> AppData -> Roaming -> npm -> node_modules -> @geekbeer
891
+ $traverseDirs = @(
892
+ $adminNpmBin,
893
+ (Join-Path $adminNpmBin 'node_modules'),
894
+ (Join-Path $adminNpmBin 'node_modules\@geekbeer')
895
+ )
896
+ # Walk up from npm bin dir to drive root to grant traverse (list+read) on each
897
+ $walkDir = $adminNpmBin
898
+ while ($walkDir) {
899
+ $parent = Split-Path $walkDir -Parent
900
+ if (-not $parent -or $parent -eq $walkDir) { break }
901
+ $traverseDirs += $parent
902
+ $walkDir = $parent
903
+ # Stop at drive root
904
+ if ($parent.Length -le 3) { break }
905
+ }
906
+ foreach ($dir in ($traverseDirs | Select-Object -Unique)) {
907
+ if (Test-Path $dir) {
908
+ # Grant only traverse + list (no recursive, no inherit)
909
+ icacls $dir /grant "${targetUserName}:(RX)" /Q 2>$null | Out-Null
910
+ }
911
+ }
942
912
 
943
- # Grant minion service account access to npm global directory (for package access)
944
- $npmGlobalDir = Split-Path $CliDir
945
- if ($npmGlobalDir -and (Test-Path $npmGlobalDir)) {
946
- $npmAcl = Get-Acl $npmGlobalDir
947
- $npmRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
948
- $MinionSvcUser, 'ReadAndExecute', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
949
- $npmAcl.AddAccessRule($npmRule)
950
- Set-Acl $npmGlobalDir $npmAcl
951
- Write-Detail "Granted '$MinionSvcUser' read access to npm global"
913
+ # Add admin's npm bin to target user's PATH
914
+ $targetUserSid = $null
915
+ try {
916
+ $targetUserSid = (New-Object System.Security.Principal.NTAccount($targetUserName)).Translate(
917
+ [System.Security.Principal.SecurityIdentifier]).Value
918
+ } catch {}
919
+ if ($targetUserSid) {
920
+ $regPath = "Registry::HKEY_USERS\$targetUserSid\Environment"
921
+ if (Test-Path $regPath) {
922
+ $currentPath = (Get-ItemProperty -Path $regPath -Name PATH -ErrorAction SilentlyContinue).PATH
923
+ if ($currentPath -and $currentPath -notlike "*$adminNpmBin*") {
924
+ Set-ItemProperty -Path $regPath -Name PATH -Value "$currentPath;$adminNpmBin"
925
+ Write-Detail "Added $adminNpmBin to target user's PATH"
926
+ } elseif (-not $currentPath) {
927
+ Set-ItemProperty -Path $regPath -Name PATH -Value $adminNpmBin
928
+ Write-Detail "Set target user's PATH to $adminNpmBin"
929
+ } else {
930
+ Write-Detail "Target user's PATH already contains $adminNpmBin"
931
+ }
932
+ } else {
933
+ Write-Warn "Target user's registry not loaded. User must log in and re-run setup, or manually add $adminNpmBin to PATH."
934
+ }
935
+ }
952
936
  }
953
937
 
954
938
  Write-Host ""
@@ -958,8 +942,8 @@ function Invoke-Setup {
958
942
  Write-Host ""
959
943
  Write-Host "Services registered (not yet started):"
960
944
  Write-Host " minion-agent - AI Agent (port 8080)"
961
- Write-Host " minion-vnc - TightVNC Server (port 5900)"
962
945
  Write-Host " minion-websockify - WebSocket proxy (port 6080)"
946
+ Write-Host " MinionVNC (task) - TightVNC (port 5900, starts at logon)"
963
947
  Write-Host ""
964
948
  Write-Host "Next step: Connect to HQ (run as regular user):" -ForegroundColor Yellow
965
949
  Write-Host " minion-cli-win configure ``"
@@ -1001,12 +985,12 @@ function Invoke-Uninstall {
1001
985
  }
1002
986
  Write-Host ""
1003
987
 
1004
- $totalSteps = 7
988
+ $totalSteps = 6
1005
989
 
1006
990
  # Step 1: Stop and remove all NSSM services
1007
991
  Write-Step 1 $totalSteps "Stopping and removing services..."
1008
992
  if ($NssmPath -and (Test-Path $NssmPath)) {
1009
- foreach ($svc in @('minion-cloudflared', 'minion-websockify', 'minion-vnc', 'minion-agent')) {
993
+ foreach ($svc in @('minion-cloudflared', 'minion-websockify', 'minion-agent')) {
1010
994
  $status = Invoke-Nssm status $svc
1011
995
  if ($status) {
1012
996
  Invoke-Nssm stop $svc
@@ -1016,6 +1000,12 @@ function Invoke-Uninstall {
1016
1000
  }
1017
1001
  }
1018
1002
 
1003
+ # Remove VNC logon task and legacy NSSM service
1004
+ schtasks /Delete /TN "MinionVNC" /F 2>$null
1005
+ Invoke-Nssm stop minion-vnc
1006
+ Invoke-Nssm remove minion-vnc confirm
1007
+ Write-Detail "VNC logon task and legacy service removed"
1008
+
1019
1009
  # Also stop legacy processes
1020
1010
  Stop-Process -Name tvnserver -Force -ErrorAction SilentlyContinue
1021
1011
  Stop-Process -Name websockify -Force -ErrorAction SilentlyContinue
@@ -1024,7 +1014,7 @@ function Invoke-Uninstall {
1024
1014
 
1025
1015
  # Step 2: Remove firewall rules
1026
1016
  Write-Step 2 $totalSteps "Removing firewall rules..."
1027
- foreach ($ruleName in @('Minion Agent', 'Minion VNC')) {
1017
+ foreach ($ruleName in @('Minion Agent', 'Minion Terminal', 'Minion VNC')) {
1028
1018
  $existing = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
1029
1019
  if ($existing) {
1030
1020
  Remove-NetFirewallRule -DisplayName $ruleName
@@ -1095,24 +1085,15 @@ function Invoke-Uninstall {
1095
1085
  Write-Detail "Removed rules: core.md"
1096
1086
  }
1097
1087
 
1098
- # Step 6: Remove minion service account
1099
- Write-Step 6 $totalSteps "Removing service account..."
1100
- $MinionSvcUser = 'minion'
1101
- if (Get-LocalUser -Name $MinionSvcUser -ErrorAction SilentlyContinue) {
1102
- Remove-LocalUser -Name $MinionSvcUser
1103
- Write-Detail "Removed local user '$MinionSvcUser'"
1104
- } else {
1105
- Write-Detail "Service account '$MinionSvcUser' not found, skipping"
1106
- }
1107
- # Remove stored service password
1088
+ # Clean up legacy service account and password file (from v3.1.0-v3.4.x)
1108
1089
  $svcPasswordFile = Join-Path $DataDir '.svc-password'
1109
1090
  if (Test-Path $svcPasswordFile) {
1110
1091
  Remove-Item $svcPasswordFile -Force
1111
- Write-Detail "Removed service credentials file"
1092
+ Write-Detail "Removed legacy service credentials file"
1112
1093
  }
1113
1094
 
1114
- # Step 7: Remove Cloudflare Tunnel configuration
1115
- Write-Step 7 $totalSteps "Removing Cloudflare Tunnel configuration..."
1095
+ # Step 6: Remove Cloudflare Tunnel configuration
1096
+ Write-Step 6 $totalSteps "Removing Cloudflare Tunnel configuration..."
1116
1097
  $cfConfigDir = Join-Path $TargetUserProfile '.cloudflared'
1117
1098
  if (Test-Path $cfConfigDir) {
1118
1099
  Remove-Item $cfConfigDir -Recurse -Force
@@ -1280,13 +1261,24 @@ function Invoke-Configure {
1280
1261
  # Start services (uses sc.exe — works without admin via SDDL)
1281
1262
  $startStep = if ($SetupTunnel) { $totalSteps - 2 } else { $totalSteps - 2 }
1282
1263
  Write-Step ($totalSteps - 1) $totalSteps "Starting services..."
1283
- # Start VNC/websockify if registered
1284
- foreach ($svc in @('minion-vnc', 'minion-websockify')) {
1285
- $svcState = Get-ServiceState $svc
1286
- if ($svcState -and $svcState -ne 'RUNNING') {
1287
- sc.exe start $svc 2>&1 | Out-Null
1288
- Write-Detail "$svc started"
1264
+ # Start VNC (logon task — runs in user session for desktop capture)
1265
+ $vncProcess = Get-Process -Name tvnserver -ErrorAction SilentlyContinue
1266
+ if (-not $vncProcess) {
1267
+ $vncSystemPath = 'C:\Program Files\TightVNC\tvnserver.exe'
1268
+ $vncPortablePath = Join-Path $DataDir 'tightvnc\PFiles\TightVNC\tvnserver.exe'
1269
+ $vncExe = if (Test-Path $vncSystemPath) { $vncSystemPath } elseif (Test-Path $vncPortablePath) { $vncPortablePath } else { $null }
1270
+ if ($vncExe) {
1271
+ Start-Process -FilePath $vncExe -ArgumentList '-run'
1272
+ Write-Detail "TightVNC started (user session)"
1289
1273
  }
1274
+ } else {
1275
+ Write-Detail "TightVNC already running"
1276
+ }
1277
+ # Start websockify service
1278
+ $wsState = Get-ServiceState 'minion-websockify'
1279
+ if ($wsState -and $wsState -ne 'RUNNING') {
1280
+ sc.exe start minion-websockify 2>&1 | Out-Null
1281
+ Write-Detail "minion-websockify started"
1290
1282
  }
1291
1283
  Start-MinionService
1292
1284
 
@@ -1343,7 +1335,7 @@ function Show-Status {
1343
1335
  }
1344
1336
 
1345
1337
  function Show-Daemons {
1346
- foreach ($svc in @('minion-agent', 'minion-vnc', 'minion-websockify', 'minion-cloudflared')) {
1338
+ foreach ($svc in @('minion-agent', 'minion-websockify', 'minion-cloudflared')) {
1347
1339
  $state = Get-ServiceState $svc
1348
1340
  if ($state) {
1349
1341
  Write-Host "${svc}: $state"
@@ -1351,6 +1343,13 @@ function Show-Daemons {
1351
1343
  Write-Host "${svc}: not installed"
1352
1344
  }
1353
1345
  }
1346
+ # VNC runs as logon task, not NSSM service
1347
+ $vncProc = Get-Process -Name tvnserver -ErrorAction SilentlyContinue
1348
+ if ($vncProc) {
1349
+ Write-Host "vnc (task): RUNNING (PID $($vncProc[0].Id))"
1350
+ } else {
1351
+ Write-Host "vnc (task): not running"
1352
+ }
1354
1353
  }
1355
1354
 
1356
1355
  function Show-Health {
@@ -1383,13 +1382,7 @@ function Show-Diagnose {
1383
1382
  Write-Host ""
1384
1383
 
1385
1384
  Write-Host "Service Account:" -ForegroundColor Yellow
1386
- $MinionSvcUser = 'minion'
1387
- $svcUser = Get-LocalUser -Name $MinionSvcUser -ErrorAction SilentlyContinue
1388
- if ($svcUser) {
1389
- Write-Host " User: $MinionSvcUser (Enabled: $($svcUser.Enabled))"
1390
- } else {
1391
- Write-Host " User: NOT FOUND (services run as LocalSystem)" -ForegroundColor Yellow
1392
- }
1385
+ Write-Host " Runs as: LocalSystem"
1393
1386
  Write-Host ""
1394
1387
 
1395
1388
  Write-Host "NSSM:" -ForegroundColor Yellow