@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.
- package/docs/api-reference.md +27 -2
- package/docs/environment-setup.md +9 -0
- package/package.json +1 -1
- package/rules/core.md +24 -0
- package/rules/windows.md +72 -0
- package/win/routes/terminal.js +14 -1
- package/win/server.js +6 -4
- package/win/wsl-session-server.js +45 -0
package/docs/api-reference.md
CHANGED
|
@@ -54,9 +54,9 @@ Outcome values: `success`, `failure`, `partial`
|
|
|
54
54
|
|
|
55
55
|
| Method | Endpoint | Description |
|
|
56
56
|
|--------|----------|-------------|
|
|
57
|
-
| GET | `/api/terminal/sessions` | List
|
|
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
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
|
package/rules/windows.md
ADDED
|
@@ -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` の場合、ターゲットユーザーがログインしていない可能性がある。
|
package/win/routes/terminal.js
CHANGED
|
@@ -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
|
|
148
|
-
|
|
149
|
-
fs.
|
|
150
|
-
|
|
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
|
|