@geekbeer/minion 3.13.0 → 3.14.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.
@@ -54,9 +54,9 @@ Outcome values: `success`, `failure`, `partial`
54
54
 
55
55
  | Method | Endpoint | Description |
56
56
  |--------|----------|-------------|
57
- | GET | `/api/terminal/sessions` | List tmux sessions |
57
+ | GET | `/api/terminal/sessions` | List sessions (CMD + WSL merged) |
58
58
  | POST | `/api/terminal/send` | Send keys. Body: `{session, input?, enter?, special?}` |
59
- | POST | `/api/terminal/create` | Create session. Body: `{name?, command?}` |
59
+ | POST | `/api/terminal/create` | Create session. Body: `{name?, command?, type?, get_or_create?}` |
60
60
  | POST | `/api/terminal/kill` | Kill session. Body: `{session}` |
61
61
  | GET | `/api/terminal/capture` | Capture pane content. Query: `?session=&lines=100` |
62
62
  | GET | `/api/terminal/ttyd/status` | ttyd process status |
@@ -64,6 +64,31 @@ Outcome values: `success`, `failure`, `partial`
64
64
  | POST | `/api/terminal/ttyd/stop` | Stop ttyd for session. Body: `{session}` |
65
65
  | POST | `/api/terminal/ttyd/stop-all` | Stop all ttyd processes |
66
66
 
67
+ #### WSL セッション(Windows ミニオン限定)
68
+
69
+ WSL 内でコマンドを実行するには `type: "wsl"` を指定する。セッション名は `wsl-` prefix が自動付与される。
70
+
71
+ ```bash
72
+ # WSL セッション作成(既存があれば再利用)
73
+ curl -X POST http://localhost:8080/api/terminal/create \
74
+ -H "Authorization: Bearer $API_TOKEN" \
75
+ -H "Content-Type: application/json" \
76
+ -d '{"name": "wsl-dev", "type": "wsl", "get_or_create": true}'
77
+ ```
78
+
79
+ | パラメータ | 型 | 説明 |
80
+ |-----------|-----|------|
81
+ | `type` | string | `"wsl"` で WSL セッションを作成 |
82
+ | `get_or_create` | boolean | `true` の場合、同名の既存セッションがあれば再利用(レスポンスに `reused: true`) |
83
+
84
+ WSL セッションの上限は **5つ**。上限に達すると `429` を返す。不要なセッションを `kill` してから再作成すること。
85
+
86
+ ```bash
87
+ # WSL サーバー稼働状態の確認
88
+ curl http://localhost:8080/api/terminal/wsl/status \
89
+ -H "Authorization: Bearer $API_TOKEN"
90
+ ```
91
+
67
92
  ### Files
68
93
 
69
94
  Files are stored in `~/files/`. Max upload size: 50MB.
@@ -232,6 +232,14 @@ curl -X POST http://localhost:8080/api/chat \
232
232
  | 7682 | WSL session server (HTTP API) | localhost のみ |
233
233
  | 7683 | WSL session server (WebSocket) | localhost のみ、ttyd プロトコル |
234
234
 
235
+ ### セッション管理
236
+
237
+ - **セッション再利用**: `get_or_create: true` を指定すると、同名の既存セッションがあれば再利用される。毎回新しいセッションを作らないこと。
238
+ - **固定名**: `wsl-dev`, `wsl-build` など目的を示す名前を使う。名前を省略するとタイムスタンプ名が生成され再利用できない。
239
+ - **上限**: WSL セッションは最大 5 つ。上限に達すると作成が拒否される。
240
+ - **自動クリーンアップ**: 完了済みセッションは 5 分後に自動削除される。
241
+ - **手動クリーンアップ**: 不要なセッションは `POST /api/terminal/kill` で終了する。
242
+
235
243
  ### トラブルシューティング
236
244
 
237
245
  | 問題 | 原因 | 対処 |
@@ -239,6 +247,7 @@ curl -X POST http://localhost:8080/api/chat \
239
247
  | WSL session server is not running | ユーザーが未ログイン | RDP/コンソールでログイン |
240
248
  | WSL not detected during setup | WSL 未インストール | `wsl --install` を実行後 `minion-cli setup` を再実行 |
241
249
  | Connection refused on port 7682 | サーバー異常終了 | `schtasks /Run /TN "MinionWSL"` で再起動 |
250
+ | WSL session limit reached | 5 セッション上限 | `GET /api/terminal/sessions` で確認し不要なセッションを `kill` |
242
251
 
243
252
  ---
244
253
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "3.13.0",
3
+ "version": "3.14.0",
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
@@ -40,6 +40,30 @@ Minion
40
40
 
41
41
  API の詳細仕様は `~/.minion/docs/api-reference.md` の「Email」セクションを参照。
42
42
 
43
+ ## Terminal Session Management
44
+
45
+ ターミナルセッション(`/api/terminal/*`)を使用する際は以下のルールに従うこと。
46
+
47
+ ### セッション再利用の原則
48
+
49
+ - **新しいセッションを作る前に、既存セッションを確認する。**
50
+ ```bash
51
+ curl -H "Authorization: Bearer $API_TOKEN" http://localhost:8080/api/terminal/sessions
52
+ ```
53
+ - 目的に合う既存セッション(未完了で再利用可能なもの)があれば、そのセッションに `send` でコマンドを送る。
54
+ - 新規作成が必要な場合は、**目的を示す固定名**を付ける(例: `dev`, `build`, `test`)。タイムスタンプ名(`session-1234567890`)は避ける。
55
+
56
+ ### クリーンアップ
57
+
58
+ - 作業が完了したセッションは `POST /api/terminal/kill` で終了する。
59
+ - 一時的なコマンド実行(`command` 引数付きで作成したセッション)は、完了確認後に必ず kill する。
60
+ - セッションを放置しない。使い終わったら片付ける。
61
+
62
+ ### セッション数の制限
63
+
64
+ - 同時に保持するセッションは **最大3つ** を目安にする。
65
+ - それ以上必要な場合は、不要なセッションを先に kill してから作成する。
66
+
43
67
  ## Available Tools
44
68
 
45
69
  ### CLI
@@ -0,0 +1,72 @@
1
+ # Windows Minion
2
+
3
+ Windows ミニオン固有のルール。
4
+
5
+ ## WSL セッション管理
6
+
7
+ Windows ミニオンでは WSL(Windows Subsystem for Linux)内の Docker やリポジトリ操作のために WSL セッションを使用できる。
8
+
9
+ ### WSL セッションの使い方
10
+
11
+ #### ターミナルセッション
12
+
13
+ WSL 内でコマンドを実行するには `type: "wsl"` を指定してセッションを作成する:
14
+
15
+ ```bash
16
+ # WSL セッション作成(固定名を付けること)
17
+ curl -X POST http://localhost:8080/api/terminal/create \
18
+ -H "Authorization: Bearer $API_TOKEN" \
19
+ -H "Content-Type: application/json" \
20
+ -d '{"name": "wsl-dev", "type": "wsl"}'
21
+
22
+ # コマンド送信
23
+ curl -X POST http://localhost:8080/api/terminal/send \
24
+ -H "Authorization: Bearer $API_TOKEN" \
25
+ -H "Content-Type: application/json" \
26
+ -d '{"session": "wsl-dev", "input": "docker compose up -d", "enter": true}'
27
+
28
+ # 出力確認
29
+ curl "http://localhost:8080/api/terminal/capture?session=wsl-dev&lines=50" \
30
+ -H "Authorization: Bearer $API_TOKEN"
31
+ ```
32
+
33
+ #### チャット(WSL モード)
34
+
35
+ チャット API に `wsl_mode: true` を追加すると、Claude Code CLI がユーザーセッションで実行され WSL コマンドを直接使用できる:
36
+
37
+ ```bash
38
+ curl -X POST http://localhost:8080/api/chat \
39
+ -H "Authorization: Bearer $API_TOKEN" \
40
+ -H "Content-Type: application/json" \
41
+ -d '{"message": "WSL内でリポジトリをクローンしてDockerを起動して", "wsl_mode": true}'
42
+ ```
43
+
44
+ ### セッション管理ルール
45
+
46
+ #### 再利用を徹底する
47
+
48
+ - **WSL セッションの新規作成前に、必ず既存セッションを確認する。**
49
+ ```bash
50
+ curl -H "Authorization: Bearer $API_TOKEN" http://localhost:8080/api/terminal/sessions
51
+ ```
52
+ レスポンスの `type: "wsl"` かつ `completed: false` のセッションがあれば再利用する。
53
+ - **固定名を使う。** `wsl-dev`, `wsl-build`, `wsl-docker` など目的を示す名前を付ける。
54
+ 名前を省略すると `wsl-session-{timestamp}` が自動生成され、再利用できなくなる。
55
+ - **1つの WSL セッションで複数コマンドを順次実行する。** コマンドごとに新しいセッションを作らない。
56
+
57
+ #### クリーンアップ
58
+
59
+ - WSL 作業が完了したら `POST /api/terminal/kill` でセッションを終了する。
60
+ - WSL セッションは最大 **5つ** まで。上限に達すると新規作成が拒否される。
61
+ - 完了済み(`completed: true`)のセッションは自動的にクリーンアップされる。
62
+
63
+ #### WSL セッションサーバーの確認
64
+
65
+ WSL セッションが使えない場合は、まずサーバーの稼働状態を確認する:
66
+
67
+ ```bash
68
+ curl http://localhost:8080/api/terminal/wsl/status \
69
+ -H "Authorization: Bearer $API_TOKEN"
70
+ ```
71
+
72
+ `running: false` の場合、ターゲットユーザーがログインしていない可能性がある。
@@ -316,13 +316,26 @@ async function terminalRoutes(fastify) {
316
316
  if (type === 'wsl') {
317
317
  const sessionName = name || `wsl-session-${Date.now()}`
318
318
  const wslName = sessionName.startsWith('wsl-') ? sessionName : `wsl-${sessionName}`
319
+
320
+ // get_or_create: return existing session if it's still alive
321
+ if (request.body.get_or_create) {
322
+ const sessions = await proxyToWsl('GET', '/api/wsl/sessions')
323
+ if (sessions && sessions.success && sessions.sessions) {
324
+ const existing = sessions.sessions.find(s => s.name === wslName && !s.completed)
325
+ if (existing) {
326
+ console.log(`[Terminal] Reusing existing WSL session '${wslName}'`)
327
+ return { success: true, session: wslName, message: `Reusing existing WSL session '${wslName}'`, reused: true }
328
+ }
329
+ }
330
+ }
331
+
319
332
  console.log(`[Terminal] Creating WSL session '${wslName}' — proxying to WSL server`)
320
333
  const result = await proxyToWsl('POST', '/api/wsl/create', { name: wslName, command })
321
334
  if (!result) {
322
335
  reply.code(503)
323
336
  return { success: false, error: 'WSL session server is not running. The target user must be logged in for WSL sessions.' }
324
337
  }
325
- reply.code(result.success ? 200 : 500)
338
+ reply.code(result.success ? 200 : (result.error?.includes('limit') ? 429 : 500))
326
339
  return result
327
340
  }
328
341
 
package/win/server.js CHANGED
@@ -144,10 +144,12 @@ function syncBundledRules() {
144
144
  try {
145
145
  if (!fs.existsSync(bundledRulesDir)) return
146
146
  fs.mkdirSync(targetRulesDir, { recursive: true })
147
- const coreSrc = path.join(bundledRulesDir, 'core.md')
148
- if (fs.existsSync(coreSrc)) {
149
- fs.copyFileSync(coreSrc, path.join(targetRulesDir, 'core.md'))
150
- console.log('[Rules] Synced: core.md')
147
+ for (const ruleFile of ['core.md', 'windows.md']) {
148
+ const src = path.join(bundledRulesDir, ruleFile)
149
+ if (fs.existsSync(src)) {
150
+ fs.copyFileSync(src, path.join(targetRulesDir, ruleFile))
151
+ console.log(`[Rules] Synced: ${ruleFile}`)
152
+ }
151
153
  }
152
154
  for (const legacy of ['minion.md', 'role-pm.md', 'role-engineer.md']) {
153
155
  const legacyPath = path.join(targetRulesDir, legacy)
@@ -29,6 +29,9 @@ const DATA_DIR = path.join(HOME_DIR, '.minion')
29
29
  const TOKEN_FILE = path.join(DATA_DIR, '.wsl-session-token')
30
30
  const PID_FILE = path.join(DATA_DIR, '.wsl-session.pid')
31
31
 
32
+ const MAX_WSL_SESSIONS = 5
33
+ const COMPLETED_SESSION_TTL_MS = 5 * 60 * 1000 // 5 minutes
34
+
32
35
  let AUTH_TOKEN = ''
33
36
  try {
34
37
  AUTH_TOKEN = fs.readFileSync(TOKEN_FILE, 'utf-8').trim()
@@ -109,6 +112,7 @@ function createWslSession(sessionName, command) {
109
112
  ptyProcess.onExit(({ exitCode }) => {
110
113
  session.completed = true
111
114
  session.exitCode = exitCode
115
+ session.completedAt = Date.now()
112
116
  for (const ws of session.wsClients) {
113
117
  try { ws.close() } catch {}
114
118
  }
@@ -119,6 +123,30 @@ function createWslSession(sessionName, command) {
119
123
  return session
120
124
  }
121
125
 
126
+ /**
127
+ * Remove completed sessions that have been idle for COMPLETED_SESSION_TTL_MS.
128
+ */
129
+ function reapCompletedSessions() {
130
+ const now = Date.now()
131
+ for (const [name, session] of activeSessions) {
132
+ if (session.completed && session.completedAt && now - session.completedAt > COMPLETED_SESSION_TTL_MS) {
133
+ activeSessions.delete(name)
134
+ console.log(`[WSL] Reaped completed session '${name}'`)
135
+ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Count active (non-completed) sessions.
141
+ */
142
+ function activeSessionCount() {
143
+ let count = 0
144
+ for (const [, session] of activeSessions) {
145
+ if (!session.completed) count++
146
+ }
147
+ return count
148
+ }
149
+
122
150
  const specialKeyMap = {
123
151
  'Enter': '\r', 'Escape': '\x1b', 'Tab': '\t',
124
152
  'C-c': '\x03', 'C-d': '\x04', 'C-z': '\x1a',
@@ -249,10 +277,24 @@ async function startServer() {
249
277
  if (!/^[\w-]+$/.test(sessionName)) {
250
278
  reply.code(400); return { success: false, error: 'Invalid session name' }
251
279
  }
280
+
281
+ // Reap completed sessions before checking limits
282
+ reapCompletedSessions()
283
+
252
284
  if (activeSessions.has(sessionName)) {
253
285
  reply.code(409); return { success: false, error: `Session '${sessionName}' already exists` }
254
286
  }
255
287
 
288
+ if (activeSessionCount() >= MAX_WSL_SESSIONS) {
289
+ reply.code(429)
290
+ return {
291
+ success: false,
292
+ error: `WSL session limit reached (max ${MAX_WSL_SESSIONS}). Kill unused sessions before creating new ones.`,
293
+ active_sessions: activeSessionCount(),
294
+ max_sessions: MAX_WSL_SESSIONS,
295
+ }
296
+ }
297
+
256
298
  try {
257
299
  createWslSession(sessionName, command)
258
300
  return { success: true, session: sessionName, message: `WSL session '${sessionName}' created` }
@@ -373,6 +415,9 @@ async function startServer() {
373
415
  fs.writeFileSync(PID_FILE, String(process.pid))
374
416
  } catch {}
375
417
 
418
+ // Periodically reap completed sessions
419
+ setInterval(reapCompletedSessions, 60 * 1000)
420
+
376
421
  return fastify
377
422
  }
378
423