@geekbeer/minion 3.4.7 → 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.
package/core/api.js CHANGED
@@ -87,8 +87,8 @@ async function sendHeartbeat(data) {
87
87
  }
88
88
 
89
89
  /**
90
- * Create a project thread (help or discussion) on HQ.
91
- * @param {object} data - { project_id, title, content, thread_type?, mentions?, context? }
90
+ * Create a thread (project-scoped or workspace-scoped) on HQ.
91
+ * @param {object} data - { project_id?, scope?, category?, title, content, thread_type?, mentions?, context? }
92
92
  * @returns {Promise<{ thread: object }>}
93
93
  */
94
94
  async function createThread(data) {
@@ -99,11 +99,12 @@ async function createThread(data) {
99
99
  }
100
100
 
101
101
  /**
102
- * Get open threads in this minion's projects.
102
+ * Get open threads accessible to this minion.
103
+ * @param {'project' | 'workspace' | 'all'} [scope='all'] - Filter by scope
103
104
  * @returns {Promise<{ threads: object[] }>}
104
105
  */
105
- async function getOpenThreads() {
106
- return request('/threads/open')
106
+ async function getOpenThreads(scope = 'all') {
107
+ return request(`/threads/open?scope=${scope}`)
107
108
  }
108
109
 
109
110
  /**
@@ -130,9 +130,15 @@ async function pollOnce() {
130
130
  * Process a single thread: check for new activity and evaluate if needed.
131
131
  */
132
132
  async function processThread(thread, myProjects, now) {
133
- // Find my role in this project
134
- const myProject = myProjects.find(p => p.id === thread.project_id)
135
- if (!myProject) return // Not a member
133
+ // For workspace threads, create a synthetic project context
134
+ // For project threads, find role via membership
135
+ let myProject
136
+ if (thread.scope === 'workspace') {
137
+ myProject = { id: null, name: 'Workspace', role: 'engineer' }
138
+ } else {
139
+ myProject = myProjects.find(p => p.id === thread.project_id)
140
+ if (!myProject) return // Not a member
141
+ }
136
142
 
137
143
  const state = readState.get(thread.id) || { lastMessageCount: 0, lastEvalAt: 0 }
138
144
 
@@ -212,7 +218,12 @@ async function evaluateWithLlm(threadSummary, threadDetail, allMessages, newMess
212
218
  ctx.attempted_resolution ? `試行済み: ${ctx.attempted_resolution}` : '',
213
219
  ].filter(Boolean).join('\n')
214
220
 
215
- const prompt = `あなたはプロジェクト「${myProject.name}」のチームメンバー(ロール: ${myProject.role}、ID: ${config.MINION_ID})です。
221
+ const isWorkspace = threadDetail.scope === 'workspace'
222
+ const scopeContext = isWorkspace
223
+ ? `あなたはワークスペースのメンバー(ID: ${config.MINION_ID})です。\nこれはプロジェクトに属さないワークスペーススレッドです(カテゴリ: ${threadDetail.category || 'general'})。`
224
+ : `あなたはプロジェクト「${myProject.name}」のチームメンバー(ロール: ${myProject.role}、ID: ${config.MINION_ID})です。`
225
+
226
+ const prompt = `${scopeContext}
216
227
  以下のスレッドに対して、あなたが返信すべきかどうかを判断し、返信する場合はその内容を生成してください。
217
228
 
218
229
  スレッドタイプ: ${threadType}
@@ -235,7 +246,7 @@ ${messageHistory || '(メッセージなし)'}
235
246
  判断基準:
236
247
  - 自分が起票したスレッドの場合、他のメンバーの回答を待つべき(追加情報がある場合を除く)
237
248
  - メンション対象が特定のロールやミニオンに限定されている場合、自分が対象でなければ静観する
238
- - 自分のロール(${myProject.role})に関連する話題か
249
+ ${isWorkspace ? '- ワークスペーススレッドではすべてのミニオンが参加可能。自分が貢献できる場合は積極的に参加する' : `- 自分のロール(${myProject.role})に関連する話題か`}
239
250
  - 自分が貢献できる知見や意見があるか
240
251
  - 既に十分な回答がある場合は重複を避ける
241
252
  - 人間に聞くべき場合は @user メンションを含めて返信する`
@@ -281,6 +292,9 @@ ${messageHistory || '(メッセージなし)'}
281
292
  */
282
293
  async function fallbackMemoryMatch(thread) {
283
294
  try {
295
+ // Workspace threads have no project_id — skip memory matching
296
+ if (!thread.project_id) return
297
+
284
298
  const ctx = thread.context || {}
285
299
  const category = ctx.category || ''
286
300
  const searchParam = category ? `&category=${category}` : ''
@@ -274,26 +274,30 @@ GET `/api/daemons/status` response:
274
274
  |--------|------|
275
275
  | `step_poller` | ワークフローステップの取得・実行(30秒間隔) |
276
276
  | `revision_watcher` | リビジョン要求の検知(30秒間隔、PMのみ) |
277
- | `thread_watcher` | プロジェクトスレッドの監視・LLM評価(15秒間隔) |
277
+ | `thread_watcher` | プロジェクト・ワークスペーススレッドの監視・LLM評価(15秒間隔) |
278
278
  | `reflection_scheduler` | 1日1回の振り返り(cron) |
279
279
  | `heartbeat` | HQへのハートビート(30秒間隔) |
280
280
 
281
- ### Project Threads (プロジェクトスレッド)
281
+ ### Threads (スレッド)
282
282
 
283
- プロジェクト内のコミュニケーションチャネル。ブロッカー共有やチーム議論に使う。
283
+ プロジェクト内またはワークスペースレベルのコミュニケーションチャネル。ブロッカー共有やチーム議論に使う。
284
284
  リクエストは HQ にプロキシされる。
285
285
 
286
+ **2つのスコープ:**
287
+ - **Project** (`scope: "project"`): プロジェクトに紐づくスレッド。`project_id` 必須。プロジェクトメンバーが参加。
288
+ - **Workspace** (`scope: "workspace"`): プロジェクトに属さないスレッド。ルーティンのブロッカー、全体連絡等。オーナーの全ミニオンが参加。
289
+
286
290
  | Method | Endpoint | Description |
287
291
  |--------|----------|-------------|
288
- | GET | `/api/threads` | 参加プロジェクトのオープンスレッド一覧 |
292
+ | GET | `/api/threads` | オープンスレッド一覧(プロジェクト+ワークスペース) |
289
293
  | POST | `/api/threads` | スレッドを起票(初回メッセージを同時作成) |
290
294
  | GET | `/api/threads/:id` | スレッド詳細 + メッセージ一覧 |
291
295
  | POST | `/api/threads/:id/messages` | スレッドにメッセージを投稿 |
292
296
  | POST | `/api/threads/:id/resolve` | スレッドを解決済みにする |
293
297
  | POST | `/api/threads/:id/cancel` | スレッドをキャンセル |
294
- | DELETE | `/api/threads/:id` | スレッドを完全削除(PMのみ) |
298
+ | DELETE | `/api/threads/:id` | スレッドを完全削除(project: PMのみ、workspace: オーナーの全ミニオン) |
295
299
 
296
- POST `/api/threads` body (ヘルプスレッド起票):
300
+ POST `/api/threads` body (プロジェクトスレッド — ヘルプ):
297
301
  ```json
298
302
  {
299
303
  "project_id": "uuid",
@@ -308,7 +312,23 @@ POST `/api/threads` body (ヘルプスレッド起票):
308
312
  }
309
313
  ```
310
314
 
311
- POST `/api/threads` body (ディスカッションスレッド起票):
315
+ POST `/api/threads` body (ワークスペーススレッド — ルーティンのブロッカー):
316
+ ```json
317
+ {
318
+ "scope": "workspace",
319
+ "category": "general",
320
+ "thread_type": "help",
321
+ "title": "朝作業ルーティン: APIキーの期限切れ",
322
+ "content": "外部サービスのAPIキーが期限切れでアクセスできない。更新が必要。",
323
+ "mentions": ["user"],
324
+ "context": {
325
+ "category": "auth",
326
+ "attempted_resolution": "既存のAPIキーで再試行済み、401エラー"
327
+ }
328
+ }
329
+ ```
330
+
331
+ POST `/api/threads` body (プロジェクトスレッド — ディスカッション):
312
332
  ```json
313
333
  {
314
334
  "project_id": "uuid",
@@ -321,13 +341,20 @@ POST `/api/threads` body (ディスカッションスレッド起票):
321
341
 
322
342
  | Field | Type | Required | Description |
323
343
  |-------|------|----------|-------------|
324
- | `project_id` | string | Yes | プロジェクト UUID |
344
+ | `scope` | string | No | `project`(デフォルト)or `workspace` |
345
+ | `project_id` | string | scope=project時 Yes | プロジェクト UUID |
346
+ | `category` | string | No | ワークスペーススレッドのカテゴリ: `general`(デフォルト), `ops`, `standup` |
325
347
  | `thread_type` | string | No | `help`(デフォルト)or `discussion` |
326
348
  | `title` | string | Yes | スレッドの要約 |
327
349
  | `content` | string | Yes | スレッド本文(thread_messagesの最初のメッセージとして保存) |
328
350
  | `mentions` | string[] | No | メンション対象。形式: `role:engineer`, `role:pm`, `minion:<minion_id>`, `user` |
329
351
  | `context` | object | No | 任意のメタデータ(category, urgency, workflow_execution_id等) |
330
352
 
353
+ **scope の使い分け:**
354
+ - ワークフロー実行中(プロジェクトあり)→ `scope: "project"` + `project_id`
355
+ - ルーティン実行中(プロジェクトなし)→ `scope: "workspace"`
356
+ - プロジェクト外の一般的な質問・報告 → `scope: "workspace"` + `category`
357
+
331
358
  **thread_type の違い:**
332
359
  - `help`: ブロッカー解決。`resolve` で解決
333
360
  - `discussion`: チーム内ディスカッション。`close` で完了
@@ -411,10 +438,10 @@ POST `/api/project-memories` body:
411
438
  | `source_thread_id` | string | No | 知見の出典ヘルプスレッド UUID |
412
439
 
413
440
  **推奨ワークフロー:**
414
- 1. ブロッカー発生 → `GET /api/project-memories?project_id=...&category=auth&search=2fa` で既知の知見を検索
441
+ 1. ブロッカー発生 → プロジェクトコンテキストの場合は `GET /api/project-memories?project_id=...&category=auth&search=2fa` で既知の知見を検索
415
442
  2. 該当あり → 知見に基づいて自己解決 or 即エスカレーション
416
- 3. 該当なし → `POST /api/threads` でブロッカー起票
417
- 4. 解決後 → `POST /api/project-memories` で知見を蓄積
443
+ 3. 該当なし → `POST /api/threads` でブロッカー起票(プロジェクトなら `scope: "project"` + `project_id`、ルーティンなら `scope: "workspace"`)
444
+ 4. 解決後 → プロジェクトスレッドの場合は `POST /api/project-memories` で知見を蓄積
418
445
 
419
446
  ### Commands
420
447
 
@@ -774,20 +801,20 @@ Response:
774
801
 
775
802
  スキルはバージョン管理される。push ごとに新バージョンが作成され、ファイルは Supabase Storage に保存される。
776
803
 
777
- ### Project Threads (HQ)
804
+ ### Threads (HQ)
778
805
 
779
806
  | Method | Endpoint | Description |
780
807
  |--------|----------|-------------|
781
- | POST | `/api/minion/threads` | スレッドを起票(初回メッセージを同時作成) |
782
- | GET | `/api/minion/threads/open` | 参加プロジェクトの未解決スレッド一覧 |
808
+ | POST | `/api/minion/threads` | スレッドを起票(scope, category対応。初回メッセージを同時作成) |
809
+ | GET | `/api/minion/threads/open` | 未解決スレッド一覧(`?scope=all\|project\|workspace`) |
783
810
  | GET | `/api/minion/threads/:id` | スレッド詳細 + メッセージ一覧 |
784
811
  | POST | `/api/minion/threads/:id/messages` | スレッドにメッセージを投稿 |
785
812
  | PATCH | `/api/minion/threads/:id/resolve` | スレッドを解決済みにする。Body: `{resolution}` |
786
813
  | PATCH | `/api/minion/threads/:id/cancel` | スレッドをキャンセル。Body: `{reason?}` |
787
- | DELETE | `/api/minion/threads/:id` | スレッドを完全削除(PMのみ)。メッセージもCASCADE削除 |
814
+ | DELETE | `/api/minion/threads/:id` | スレッドを完全削除(project: PMのみ、workspace: オーナーの全ミニオン)。メッセージもCASCADE削除 |
788
815
 
789
816
  ローカルエージェントの `/api/threads` は上記 HQ API へのプロキシ。
790
- 詳細なリクエスト/レスポンス仕様はローカル API セクションの「Project Threads」を参照。
817
+ 詳細なリクエスト/レスポンス仕様はローカル API セクションの「Threads」を参照。
791
818
 
792
819
  ### Project Memories (HQ)
793
820
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "3.4.7",
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": {
package/rules/core.md CHANGED
@@ -174,12 +174,15 @@ Routine 実行中は以下もtmuxセッション環境で利用可能:
174
174
  ### 対処フロー
175
175
 
176
176
  ```
177
- 1. プロジェクトメモリーを検索(過去に同じ問題が解決済みか確認)
177
+ 1. プロジェクトコンテキストの場合:
178
+ プロジェクトメモリーを検索(過去に同じ問題が解決済みか確認)
178
179
  GET /api/project-memories?project_id=...&category=...&search=キーワード
179
180
 
180
181
  2-a. 該当する知見あり → 知見に基づいて自己解決を試みる
181
182
 
182
183
  2-b. 該当なし or 自己解決不可 → ヘルプスレッドを起票
184
+
185
+ ■ ワークフロー実行中(プロジェクトあり)→ プロジェクトスレッド:
183
186
  POST /api/threads
184
187
  {
185
188
  "project_id": "...",
@@ -193,9 +196,24 @@ Routine 実行中は以下もtmuxセッション環境で利用可能:
193
196
  }
194
197
  }
195
198
 
199
+ ■ ルーティン実行中(プロジェクトなし)→ ワークスペーススレッド:
200
+ POST /api/threads
201
+ {
202
+ "scope": "workspace",
203
+ "category": "general",
204
+ "thread_type": "help",
205
+ "title": "問題の要約(1行)",
206
+ "content": "状況の詳細説明",
207
+ "mentions": ["user"],
208
+ "context": {
209
+ "category": "auth|environment|external-service|information|approval",
210
+ "attempted_resolution": "試行した内容"
211
+ }
212
+ }
213
+
196
214
  3. スレッドの返信を待つ(thread_watcher が自動監視)
197
215
 
198
- 4. 解決後 → プロジェクトメモリーに知見を保存
216
+ 4. 解決後 → プロジェクトスレッドの場合はプロジェクトメモリーに知見を保存
199
217
  POST /api/project-memories
200
218
  {
201
219
  "project_id": "...",
@@ -206,6 +224,14 @@ Routine 実行中は以下もtmuxセッション環境で利用可能:
206
224
  }
207
225
  ```
208
226
 
227
+ ### スレッドスコープの使い分け
228
+
229
+ | 状況 | スコープ | 説明 |
230
+ |------|---------|------|
231
+ | ワークフロー実行中のブロッカー | `scope: "project"` (デフォルト) | `project_id` 必須。プロジェクトメンバーが参加 |
232
+ | ルーティン実行中のブロッカー | `scope: "workspace"` | `project_id` 不要。オーナーの全ミニオンが参加 |
233
+ | プロジェクト外の一般的な質問・報告 | `scope: "workspace"` | カテゴリ: `general`, `ops`, `standup` |
234
+
209
235
  ### メンションの使い分け
210
236
 
211
237
  | 状況 | メンション |
@@ -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