@geekbeer/minion 2.53.3 → 2.54.1

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.
@@ -1,22 +1,19 @@
1
1
  /**
2
2
  * Capability checker
3
3
  *
4
- * Detects MCP servers configured in ~/.mcp.json and available CLI tools.
5
- * Results are cached in memory for 5 minutes to avoid excessive filesystem/process checks.
6
- * Follows the same caching pattern as llm-checker.js.
4
+ * Detects MCP servers, installed packages, and environment variables.
5
+ * Always returns fresh results called on-demand, not on every heartbeat.
6
+ *
7
+ * - MCP servers: parsed from ~/.mcp.json
8
+ * - Packages: scanned from package managers (apt, npm, pip, etc.)
9
+ * - Env var keys: collected from minion-local variables/secrets stores
7
10
  */
8
11
 
9
12
  const fs = require('fs')
10
13
  const path = require('path')
11
- const { execSync } = require('child_process')
12
14
  const { config } = require('../config')
13
- const { IS_WINDOWS, buildExtendedPath } = require('./platform')
14
15
  const variableStore = require('../stores/variable-store')
15
-
16
- const CACHE_TTL_MS = 300000 // 5 minutes
17
-
18
- let cachedResult = null
19
- let cachedAt = 0
16
+ const { scanAllPackages, checkPackages } = require('./package-scanner')
20
17
 
21
18
  /**
22
19
  * Detect MCP servers from ~/.mcp.json.
@@ -39,46 +36,6 @@ function getMcpServers() {
39
36
  }
40
37
  }
41
38
 
42
- /**
43
- * Check if a CLI tool is available and get its version.
44
- * @param {string} name - Tool name (e.g., 'git', 'node')
45
- * @returns {{ name: string, available: boolean, version?: string }}
46
- */
47
- function checkTool(name) {
48
- const extendedPath = buildExtendedPath(config.HOME_DIR)
49
- const env = {
50
- ...process.env,
51
- HOME: config.HOME_DIR,
52
- ...(IS_WINDOWS && { USERPROFILE: config.HOME_DIR }),
53
- PATH: extendedPath,
54
- }
55
- const execOpts = { encoding: 'utf-8', timeout: 5000, stdio: 'pipe', env }
56
-
57
- try {
58
- // Check existence
59
- const whichCmd = IS_WINDOWS ? 'where' : 'which'
60
- execSync(`${whichCmd} ${name}`, execOpts)
61
-
62
- // Get version
63
- let version = null
64
- try {
65
- const out = execSync(`${name} --version`, execOpts).trim()
66
- // Extract version number from output (e.g., "git version 2.43.0" → "2.43.0")
67
- const match = out.match(/(\d+\.\d+[\.\d]*)/)
68
- if (match) version = match[1]
69
- } catch {
70
- // Tool exists but --version failed, that's ok
71
- }
72
-
73
- return { name, available: true, ...(version && { version }) }
74
- } catch {
75
- return { name, available: false }
76
- }
77
- }
78
-
79
- /** CLI tools to detect */
80
- const TOOL_NAMES = ['git', 'node', 'npx', 'claude', 'docker', 'tmux']
81
-
82
39
  /**
83
40
  * Get all available environment variable keys (variables + secrets).
84
41
  * Returns deduplicated key names without values (secrets stay hidden).
@@ -91,38 +48,51 @@ function getEnvVarKeys() {
91
48
  }
92
49
 
93
50
  /**
94
- * Get all capabilities (MCP servers + CLI tools + env var keys), cached for 5 minutes.
95
- * @returns {{ mcp_servers: { name: string, configured: boolean }[], cli_tools: { name: string, available: boolean, version?: string }[], env_var_keys: string[] }}
51
+ * Get all capabilities (MCP servers + installed packages + env var keys).
52
+ * Always returns fresh results.
53
+ * @returns {{ mcp_servers: { name: string, configured: boolean }[], packages: Record<string, { available: boolean, items: { name: string, version: string }[] }>, env_var_keys: string[] }}
96
54
  */
97
55
  function getCapabilities() {
98
- const now = Date.now()
99
- if (cachedResult && (now - cachedAt) < CACHE_TTL_MS) {
100
- return cachedResult
101
- }
102
-
103
56
  const mcp_servers = getMcpServers()
104
- const cli_tools = TOOL_NAMES.map(name => checkTool(name))
57
+ const packages = scanAllPackages()
105
58
  const env_var_keys = getEnvVarKeys()
106
59
 
107
- cachedResult = { mcp_servers, cli_tools, env_var_keys }
108
- cachedAt = now
109
- return cachedResult
110
- }
111
-
112
- /** Clear the cached capability status */
113
- function clearCapabilityCache() {
114
- cachedResult = null
115
- cachedAt = 0
60
+ return { mcp_servers, packages, env_var_keys }
116
61
  }
117
62
 
118
63
  /**
119
- * Check arbitrary tool names on-demand (not cached).
120
- * Used by readiness checks to verify skill-declared cli_tools.
121
- * @param {string[]} names - Tool names to check
122
- * @returns {{ name: string, available: boolean, version?: string }[]}
64
+ * Check requirements against current capabilities.
65
+ * @param {{ mcp_servers?: string[], packages?: Record<string, string[]>, env_vars?: string[] }} requires
66
+ * @returns {{ satisfied: boolean, missing: { mcp_servers: string[], packages: Record<string, string[]>, env_vars: string[] } }}
123
67
  */
124
- function checkTools(names) {
125
- return names.map(name => checkTool(name))
68
+ function checkRequirements(requires) {
69
+ const missing = { mcp_servers: [], packages: {}, env_vars: [] }
70
+ let satisfied = true
71
+
72
+ // Check MCP servers
73
+ if (requires.mcp_servers && requires.mcp_servers.length > 0) {
74
+ const configuredServers = getMcpServers()
75
+ const configuredNames = new Set(configuredServers.map(s => s.name))
76
+ missing.mcp_servers = requires.mcp_servers.filter(s => !configuredNames.has(s))
77
+ if (missing.mcp_servers.length > 0) satisfied = false
78
+ }
79
+
80
+ // Check packages
81
+ if (requires.packages && Object.keys(requires.packages).length > 0) {
82
+ const installed = scanAllPackages()
83
+ const result = checkPackages(requires.packages, installed)
84
+ missing.packages = result.missing
85
+ if (!result.satisfied) satisfied = false
86
+ }
87
+
88
+ // Check env vars
89
+ if (requires.env_vars && requires.env_vars.length > 0) {
90
+ const envVarKeys = new Set(getEnvVarKeys())
91
+ missing.env_vars = requires.env_vars.filter(v => !envVarKeys.has(v))
92
+ if (missing.env_vars.length > 0) satisfied = false
93
+ }
94
+
95
+ return { satisfied, missing }
126
96
  }
127
97
 
128
- module.exports = { getCapabilities, clearCapabilityCache, checkTools }
98
+ module.exports = { getCapabilities, checkRequirements }
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Package scanner
3
+ *
4
+ * Scans installed packages from various package managers.
5
+ * Each scanner checks if the package manager is available, then lists installed packages.
6
+ * Returns results grouped by package manager with availability status.
7
+ *
8
+ * Supported package managers:
9
+ * Linux: apt, npm, pnpm, bun, pip, uv, pipx, cargo
10
+ * macOS: brew, npm, pnpm, bun, pip, uv, pipx, cargo
11
+ * Windows: winget, scoop, npm, pnpm, bun, pip, uv, pipx, cargo
12
+ */
13
+
14
+ const { execSync } = require('child_process')
15
+ const { IS_WINDOWS, buildExtendedPath } = require('./platform')
16
+ const { config } = require('../config')
17
+
18
+ /**
19
+ * Build exec options with extended PATH.
20
+ * @returns {object}
21
+ */
22
+ function execOpts() {
23
+ const extendedPath = buildExtendedPath(config.HOME_DIR)
24
+ return {
25
+ encoding: 'utf-8',
26
+ timeout: 30000,
27
+ stdio: 'pipe',
28
+ env: {
29
+ ...process.env,
30
+ HOME: config.HOME_DIR,
31
+ ...(IS_WINDOWS && { USERPROFILE: config.HOME_DIR }),
32
+ PATH: extendedPath,
33
+ },
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Check if a command exists.
39
+ * @param {string} cmd
40
+ * @returns {boolean}
41
+ */
42
+ function commandExists(cmd) {
43
+ try {
44
+ const whichCmd = IS_WINDOWS ? 'where' : 'which'
45
+ execSync(`${whichCmd} ${cmd}`, execOpts())
46
+ return true
47
+ } catch {
48
+ return false
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Run a command and return stdout, or null on failure.
54
+ * @param {string} cmd
55
+ * @returns {string | null}
56
+ */
57
+ function run(cmd) {
58
+ try {
59
+ return execSync(cmd, execOpts()).trim()
60
+ } catch {
61
+ return null
62
+ }
63
+ }
64
+
65
+ /**
66
+ * @typedef {{ name: string, version: string }} PackageItem
67
+ * @typedef {{ available: boolean, items: PackageItem[] }} ScanResult
68
+ */
69
+
70
+ // --- Individual scanners ---
71
+
72
+ /** @returns {ScanResult} */
73
+ function scanApt() {
74
+ if (IS_WINDOWS || process.platform === 'darwin') return { available: false, items: [] }
75
+ if (!commandExists('dpkg-query')) return { available: false, items: [] }
76
+
77
+ const out = run("dpkg-query -W -f '${Package}\\t${Version}\\n'")
78
+ if (!out) return { available: true, items: [] }
79
+
80
+ const items = out.split('\n').filter(Boolean).map(line => {
81
+ const [name, version] = line.split('\t')
82
+ return { name: name || '', version: version || '' }
83
+ }).filter(i => i.name)
84
+
85
+ return { available: true, items }
86
+ }
87
+
88
+ /** @returns {ScanResult} */
89
+ function scanBrew() {
90
+ if (process.platform !== 'darwin') return { available: false, items: [] }
91
+ if (!commandExists('brew')) return { available: false, items: [] }
92
+
93
+ const out = run('brew list --versions')
94
+ if (!out) return { available: true, items: [] }
95
+
96
+ const items = out.split('\n').filter(Boolean).map(line => {
97
+ const parts = line.split(/\s+/)
98
+ return { name: parts[0] || '', version: parts.slice(1).join(' ') || '' }
99
+ }).filter(i => i.name)
100
+
101
+ return { available: true, items }
102
+ }
103
+
104
+ /** @returns {ScanResult} */
105
+ function scanWinget() {
106
+ if (!IS_WINDOWS) return { available: false, items: [] }
107
+ if (!commandExists('winget')) return { available: false, items: [] }
108
+
109
+ const out = run('winget list --disable-interactivity --accept-source-agreements 2>nul')
110
+ if (!out) return { available: true, items: [] }
111
+
112
+ // winget list output is table-formatted; parse lines after the header separator
113
+ const lines = out.split('\n')
114
+ const sepIndex = lines.findIndex(l => /^-{2,}/.test(l.trim()))
115
+ if (sepIndex < 0) return { available: true, items: [] }
116
+
117
+ const items = []
118
+ for (let i = sepIndex + 1; i < lines.length; i++) {
119
+ const line = lines[i].trim()
120
+ if (!line) continue
121
+ // Columns are variable-width; extract name and version heuristically
122
+ const parts = line.split(/\s{2,}/)
123
+ if (parts.length >= 2) {
124
+ items.push({ name: parts[0], version: parts[1] || '' })
125
+ }
126
+ }
127
+
128
+ return { available: true, items }
129
+ }
130
+
131
+ /** @returns {ScanResult} */
132
+ function scanScoop() {
133
+ if (!IS_WINDOWS) return { available: false, items: [] }
134
+ if (!commandExists('scoop')) return { available: false, items: [] }
135
+
136
+ const out = run('scoop list 2>nul')
137
+ if (!out) return { available: true, items: [] }
138
+
139
+ // scoop list output: " name version source ..."
140
+ const lines = out.split('\n')
141
+ const sepIndex = lines.findIndex(l => /^-{2,}/.test(l.trim()))
142
+
143
+ const items = []
144
+ const startIdx = sepIndex >= 0 ? sepIndex + 1 : 0
145
+ for (let i = startIdx; i < lines.length; i++) {
146
+ const line = lines[i].trim()
147
+ if (!line || line.startsWith('-')) continue
148
+ const parts = line.split(/\s+/)
149
+ if (parts.length >= 2) {
150
+ items.push({ name: parts[0], version: parts[1] || '' })
151
+ }
152
+ }
153
+
154
+ return { available: true, items }
155
+ }
156
+
157
+ /** @returns {ScanResult} */
158
+ function scanNpm() {
159
+ if (!commandExists('npm')) return { available: false, items: [] }
160
+
161
+ const out = run('npm list -g --depth=0 --json')
162
+ if (!out) return { available: true, items: [] }
163
+
164
+ try {
165
+ const data = JSON.parse(out)
166
+ const deps = data.dependencies || {}
167
+ const items = Object.entries(deps).map(([name, info]) => ({
168
+ name,
169
+ version: (typeof info === 'object' && info !== null ? info.version : '') || '',
170
+ }))
171
+ return { available: true, items }
172
+ } catch {
173
+ return { available: true, items: [] }
174
+ }
175
+ }
176
+
177
+ /** @returns {ScanResult} */
178
+ function scanPnpm() {
179
+ if (!commandExists('pnpm')) return { available: false, items: [] }
180
+
181
+ const out = run('pnpm list -g --json')
182
+ if (!out) return { available: true, items: [] }
183
+
184
+ try {
185
+ const data = JSON.parse(out)
186
+ // pnpm list -g --json returns an array
187
+ const list = Array.isArray(data) ? data : [data]
188
+ const items = []
189
+ for (const entry of list) {
190
+ const deps = { ...(entry.dependencies || {}), ...(entry.devDependencies || {}) }
191
+ for (const [name, info] of Object.entries(deps)) {
192
+ items.push({
193
+ name,
194
+ version: (typeof info === 'object' && info !== null ? info.version : '') || '',
195
+ })
196
+ }
197
+ }
198
+ return { available: true, items }
199
+ } catch {
200
+ return { available: true, items: [] }
201
+ }
202
+ }
203
+
204
+ /** @returns {ScanResult} */
205
+ function scanBun() {
206
+ if (!commandExists('bun')) return { available: false, items: [] }
207
+
208
+ // bun pm ls -g outputs text format
209
+ const out = run('bun pm ls -g 2>/dev/null || bun pm ls -g 2>nul')
210
+ if (!out) return { available: true, items: [] }
211
+
212
+ const items = []
213
+ for (const line of out.split('\n')) {
214
+ // Lines like: "├── package-name@1.0.0"
215
+ const match = line.match(/[├└─│\s]*(.+?)@(\S+)/)
216
+ if (match) {
217
+ items.push({ name: match[1], version: match[2] })
218
+ }
219
+ }
220
+
221
+ return { available: true, items }
222
+ }
223
+
224
+ /** @returns {ScanResult} */
225
+ function scanPip() {
226
+ // Try pip3 first, then pip
227
+ const pipCmd = commandExists('pip3') ? 'pip3' : commandExists('pip') ? 'pip' : null
228
+ if (!pipCmd) return { available: false, items: [] }
229
+
230
+ const out = run(`${pipCmd} list --format=json`)
231
+ if (!out) return { available: true, items: [] }
232
+
233
+ try {
234
+ const data = JSON.parse(out)
235
+ const items = data.map(pkg => ({
236
+ name: pkg.name || '',
237
+ version: pkg.version || '',
238
+ })).filter(i => i.name)
239
+ return { available: true, items }
240
+ } catch {
241
+ return { available: true, items: [] }
242
+ }
243
+ }
244
+
245
+ /** @returns {ScanResult} */
246
+ function scanUv() {
247
+ if (!commandExists('uv')) return { available: false, items: [] }
248
+
249
+ const out = run('uv pip list --format=json 2>/dev/null || uv pip list --format=json 2>nul')
250
+ if (!out) return { available: true, items: [] }
251
+
252
+ try {
253
+ const data = JSON.parse(out)
254
+ const items = data.map(pkg => ({
255
+ name: pkg.name || '',
256
+ version: pkg.version || '',
257
+ })).filter(i => i.name)
258
+ return { available: true, items }
259
+ } catch {
260
+ return { available: true, items: [] }
261
+ }
262
+ }
263
+
264
+ /** @returns {ScanResult} */
265
+ function scanPipx() {
266
+ if (!commandExists('pipx')) return { available: false, items: [] }
267
+
268
+ const out = run('pipx list --json')
269
+ if (!out) return { available: true, items: [] }
270
+
271
+ try {
272
+ const data = JSON.parse(out)
273
+ const venvs = data.venvs || {}
274
+ const items = Object.entries(venvs).map(([name, info]) => ({
275
+ name,
276
+ version: (info && info.metadata && info.metadata.main_package && info.metadata.main_package.package_version) || '',
277
+ }))
278
+ return { available: true, items }
279
+ } catch {
280
+ return { available: true, items: [] }
281
+ }
282
+ }
283
+
284
+ /** @returns {ScanResult} */
285
+ function scanCargo() {
286
+ if (!commandExists('cargo')) return { available: false, items: [] }
287
+
288
+ const out = run('cargo install --list')
289
+ if (!out) return { available: true, items: [] }
290
+
291
+ // Lines like: "ripgrep v14.1.0:" (top-level, no indent = package name)
292
+ const items = []
293
+ for (const line of out.split('\n')) {
294
+ if (line.startsWith(' ') || line.startsWith('\t') || !line.trim()) continue
295
+ const match = line.match(/^(\S+)\s+v?(\S+):?/)
296
+ if (match) {
297
+ items.push({ name: match[1], version: match[2].replace(/:$/, '') })
298
+ }
299
+ }
300
+
301
+ return { available: true, items }
302
+ }
303
+
304
+ // --- Scanner registry ---
305
+
306
+ /**
307
+ * All supported package manager scanners.
308
+ * Each scanner handles its own platform check internally.
309
+ * @type {Record<string, () => ScanResult>}
310
+ */
311
+ const SCANNERS = {
312
+ apt: scanApt,
313
+ brew: scanBrew,
314
+ winget: scanWinget,
315
+ scoop: scanScoop,
316
+ npm: scanNpm,
317
+ pnpm: scanPnpm,
318
+ bun: scanBun,
319
+ pip: scanPip,
320
+ uv: scanUv,
321
+ pipx: scanPipx,
322
+ cargo: scanCargo,
323
+ }
324
+
325
+ /**
326
+ * Scan all package managers and return installed packages.
327
+ * Skips package managers not available on the current platform.
328
+ * @returns {Record<string, ScanResult>}
329
+ */
330
+ function scanAllPackages() {
331
+ const results = {}
332
+ for (const [name, scanner] of Object.entries(SCANNERS)) {
333
+ results[name] = scanner()
334
+ }
335
+ return results
336
+ }
337
+
338
+ /**
339
+ * Check if specific packages are installed for given package managers.
340
+ * @param {Record<string, string[]>} required - e.g. { apt: ['ffmpeg'], npm: ['@anthropic-ai/claude-code'] }
341
+ * @param {Record<string, ScanResult>} installed - result from scanAllPackages()
342
+ * @returns {{ satisfied: boolean, missing: Record<string, string[]> }}
343
+ */
344
+ function checkPackages(required, installed) {
345
+ const missing = {}
346
+ let satisfied = true
347
+
348
+ for (const [manager, packages] of Object.entries(required)) {
349
+ const scan = installed[manager]
350
+ if (!scan || !scan.available) {
351
+ // Package manager not available — all packages are missing
352
+ if (packages.length > 0) {
353
+ missing[manager] = [...packages]
354
+ satisfied = false
355
+ }
356
+ continue
357
+ }
358
+
359
+ const installedNames = new Set(scan.items.map(i => i.name))
360
+ const missingPkgs = packages.filter(p => !installedNames.has(p))
361
+ if (missingPkgs.length > 0) {
362
+ missing[manager] = missingPkgs
363
+ satisfied = false
364
+ }
365
+ }
366
+
367
+ return { satisfied, missing }
368
+ }
369
+
370
+ module.exports = { scanAllPackages, checkPackages, SCANNERS }
@@ -5,14 +5,15 @@
5
5
  * - GET /api/health - Health check
6
6
  * - GET /api/status - Get current status
7
7
  * - POST /api/status - Update status
8
- * - POST /api/capabilities/check-tools - Check arbitrary CLI tools on-demand
8
+ * - GET /api/capabilities - Get current capabilities (MCP servers, packages, env vars)
9
+ * - POST /api/capabilities/check - Check requirements (MCP servers, packages, env vars)
9
10
  */
10
11
 
11
12
  const { version } = require('../../package.json')
12
13
  const { config, isHqConfigured } = require('../config')
13
14
  const { sendHeartbeat } = require('../api')
14
15
  const { getLlmServices, isLlmCommandConfigured } = require('../lib/llm-checker')
15
- const { getCapabilities, checkTools } = require('../lib/capability-checker')
16
+ const { getCapabilities, checkRequirements } = require('../lib/capability-checker')
16
17
 
17
18
  function maskToken(token) {
18
19
  if (!token || token.length < 8) return token ? '***' : ''
@@ -64,7 +65,6 @@ async function healthRoutes(fastify) {
64
65
  timestamp: new Date().toISOString(),
65
66
  llm_services: getLlmServices(),
66
67
  llm_command_configured: isLlmCommandConfigured(),
67
- capabilities: getCapabilities(),
68
68
  env: {
69
69
  HQ_URL: config.HQ_URL || '',
70
70
  MINION_ID: config.MINION_ID || '',
@@ -107,16 +107,21 @@ async function healthRoutes(fastify) {
107
107
  return { success: true }
108
108
  })
109
109
 
110
- // Check arbitrary CLI tools on-demand
111
- fastify.post('/api/capabilities/check-tools', async (request, reply) => {
112
- const { tools } = request.body || {}
113
- if (!Array.isArray(tools) || tools.length === 0) {
110
+ // Get current capabilities (MCP servers, installed packages, env var keys)
111
+ fastify.get('/api/capabilities', async () => {
112
+ return getCapabilities()
113
+ })
114
+
115
+ // Check requirements against current capabilities
116
+ fastify.post('/api/capabilities/check', async (request, reply) => {
117
+ const { mcp_servers, packages, env_vars } = request.body || {}
118
+
119
+ if (!mcp_servers && !packages && !env_vars) {
114
120
  reply.code(400)
115
- return { error: 'tools must be a non-empty array of tool names' }
121
+ return { error: 'At least one of mcp_servers, packages, or env_vars must be provided' }
116
122
  }
117
- // Limit to 20 tools per request to avoid abuse
118
- const limited = tools.slice(0, 20).filter(t => typeof t === 'string' && t.length > 0)
119
- return { cli_tools: checkTools(limited) }
123
+
124
+ return checkRequirements({ mcp_servers, packages, env_vars })
120
125
  })
121
126
  }
122
127
 
@@ -11,13 +11,16 @@
11
11
  ```yaml
12
12
  requires:
13
13
  mcp_servers: [playwright]
14
- cli_tools: [jq, imagemagick]
14
+ packages:
15
+ apt: [jq, imagemagick]
16
+ env_vars: [API_KEY]
15
17
  ```
16
18
 
17
19
  | 種別 | 検出方法 | チェック対象 |
18
20
  |------|---------|-------------|
19
21
  | `mcp_servers` | `~/.mcp.json` を読み取り、`mcpServers` キーにサーバー名が存在するか確認 | 設定ファイルの有無のみ(実際の起動テストはしない) |
20
- | `cli_tools` | `which <tool>` でパスが通っているか確認 | コマンドの存在(バージョンも取得を試みる) |
22
+ | `packages` | 各パッケージマネージャーの list コマンドでインストール済みパッケージを取得し、突合 | パッケージの存在 |
23
+ | `env_vars` | ミニオンローカル変数/シークレット + HQ注入変数 | キーの存在 |
21
24
 
22
25
  **重要**: MCP サーバーは `~/.mcp.json` に設定しないと検出されない。npm パッケージをインストールしただけでは不十分。
23
26
 
@@ -91,9 +94,9 @@ URL ベースの MCP サーバーは `url` フィールドで指定する(`com
91
94
 
92
95
  ---
93
96
 
94
- ## CLI ツールのインストール
97
+ ## パッケージのインストール
95
98
 
96
- スキルが `requires.cli_tools` で宣言したツールは、`which` コマンドでパスが通っていれば検出される。
99
+ スキルが `requires.packages` で宣言したパッケージは、対応するパッケージマネージャーの list コマンドで検出される。
97
100
 
98
101
  ### パッケージマネージャーの使い分け
99
102
 
@@ -162,7 +165,7 @@ pip3 install --user yq
162
165
 
163
166
  ### npx(一時実行)
164
167
 
165
- `npx` はインストールせずに npm パッケージを一時実行する。`cli_tools` の事前チェックでは検出されないため、恒久的に使うツールには `npm install -g` を使うこと。
168
+ `npx` はインストールせずに npm パッケージを一時実行する。`packages` の事前チェックでは検出されないため、恒久的に使うツールには `npm install -g` を使うこと。
166
169
 
167
170
  ```bash
168
171
  # 一時的な利用(事前チェックでは検出されない)
@@ -23,7 +23,10 @@ name: my-skill
23
23
  description: What this skill does
24
24
  requires:
25
25
  mcp_servers: [playwright, supabase]
26
- cli_tools: [git, node]
26
+ packages:
27
+ apt: [ffmpeg]
28
+ npm: ["@anthropic-ai/claude-code"]
29
+ env_vars: [API_KEY]
27
30
  ---
28
31
 
29
32
  Skill instructions here...
@@ -37,7 +40,8 @@ Skill instructions here...
37
40
  | `description` | Yes | スキルの説明 |
38
41
  | `requires` | No | 実行に必要な依存関係 |
39
42
  | `requires.mcp_servers` | No | 必要な MCP サーバー名のリスト |
40
- | `requires.cli_tools` | No | 必要な CLI ツール名のリスト |
43
+ | `requires.packages` | No | 必要なパッケージ(パッケージマネージャー別に指定) |
44
+ | `requires.env_vars` | No | 必要な環境変数キーのリスト |
41
45
 
42
46
  `requires` を宣言すると、ワークフロー実行前にミニオンの環境と照合する事前チェック(readiness check)が行われる。
43
47
  未宣言の場合は常に実行可能と見なされる。
package/linux/server.js CHANGED
@@ -35,7 +35,7 @@ const PACKAGE_ROOT = path.join(__dirname, '..')
35
35
  const { config, validate, isHqConfigured } = require('../core/config')
36
36
  const { sendHeartbeat } = require('../core/api')
37
37
  const { version } = require('../package.json')
38
- const { getCapabilities } = require('../core/lib/capability-checker')
38
+
39
39
  const workflowStore = require('../core/stores/workflow-store')
40
40
  const routineStore = require('../core/stores/routine-store')
41
41
 
@@ -96,7 +96,7 @@ async function shutdown(signal) {
96
96
  if (isHqConfigured()) {
97
97
  try {
98
98
  await Promise.race([
99
- sendHeartbeat({ status: 'offline', version, capabilities: getCapabilities() }),
99
+ sendHeartbeat({ status: 'offline', version }),
100
100
  new Promise(resolve => setTimeout(resolve, 3000)),
101
101
  ])
102
102
  } catch {
@@ -331,14 +331,14 @@ async function start() {
331
331
  // Send initial online heartbeat
332
332
  const { getStatus } = require('../core/routes/health')
333
333
  const { currentTask } = getStatus()
334
- sendHeartbeat({ status: 'online', current_task: currentTask, version, capabilities: getCapabilities() }).catch(err => {
334
+ sendHeartbeat({ status: 'online', current_task: currentTask, version }).catch(err => {
335
335
  console.error('[Heartbeat] Initial heartbeat failed:', err.message)
336
336
  })
337
337
 
338
338
  // Start periodic heartbeat
339
339
  heartbeatTimer = setInterval(() => {
340
340
  const { currentStatus, currentTask } = getStatus()
341
- sendHeartbeat({ status: currentStatus, current_task: currentTask, version, capabilities: getCapabilities() }).catch(err => {
341
+ sendHeartbeat({ status: currentStatus, current_task: currentTask, version }).catch(err => {
342
342
  console.error('[Heartbeat] Periodic heartbeat failed:', err.message)
343
343
  })
344
344
  }, HEARTBEAT_INTERVAL_MS)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "2.53.3",
3
+ "version": "2.54.1",
4
4
  "description": "AI Agent runtime for Minion - manages status and skill deployment on VPS",
5
5
  "main": "linux/server.js",
6
6
  "bin": {
@@ -60,6 +60,14 @@ function buildUpdateScript(npmInstallCmd, stopCmd, startCmd) {
60
60
  const logPath = path.join(dataDir, 'update-agent.log')
61
61
 
62
62
  // PowerShell script content: stop → install → start, with logging
63
+ // The stopCmd kills all child processes of the agent. Since this script
64
+ // itself is a child of the agent (spawn from Node), we must exclude our
65
+ // own PID ($PID) to avoid the update script killing itself.
66
+ const safeStopCmd = stopCmd.replace(
67
+ /ForEach-Object\s*\{\s*Stop-Process/,
68
+ 'Where-Object { $_.ProcessId -ne $PID } | ForEach-Object { Stop-Process'
69
+ )
70
+
63
71
  const ps1 = [
64
72
  `$ErrorActionPreference = 'Stop'`,
65
73
  `$logFile = '${logPath.replace(/\\/g, '\\\\')}'`,
@@ -67,7 +75,7 @@ function buildUpdateScript(npmInstallCmd, stopCmd, startCmd) {
67
75
  `Log 'Update started'`,
68
76
  `try {`,
69
77
  ` Log 'Stopping agent...'`,
70
- ` ${stopCmd}`,
78
+ ` ${safeStopCmd}`,
71
79
  ` Start-Sleep -Seconds 3`,
72
80
  ` Log 'Installing package...'`,
73
81
  ` $out = & cmd /c "${npmInstallCmd} 2>&1"`,
package/win/server.js CHANGED
@@ -16,7 +16,7 @@ const fastify = require('fastify')({ logger: true })
16
16
  const { config, validate, isHqConfigured } = require('../core/config')
17
17
  const { sendHeartbeat } = require('../core/api')
18
18
  const { version } = require('../package.json')
19
- const { getCapabilities } = require('../core/lib/capability-checker')
19
+
20
20
  const workflowStore = require('../core/stores/workflow-store')
21
21
  const routineStore = require('../core/stores/routine-store')
22
22
 
@@ -74,7 +74,7 @@ async function shutdown(signal) {
74
74
  if (isHqConfigured()) {
75
75
  try {
76
76
  await Promise.race([
77
- sendHeartbeat({ status: 'offline', version, capabilities: getCapabilities() }),
77
+ sendHeartbeat({ status: 'offline', version }),
78
78
  new Promise(resolve => setTimeout(resolve, 3000)),
79
79
  ])
80
80
  } catch {
@@ -267,14 +267,14 @@ async function start() {
267
267
  // Send initial online heartbeat
268
268
  const { getStatus } = require('../core/routes/health')
269
269
  const { currentTask } = getStatus()
270
- sendHeartbeat({ status: 'online', current_task: currentTask, version, capabilities: getCapabilities() }).catch(err => {
270
+ sendHeartbeat({ status: 'online', current_task: currentTask, version }).catch(err => {
271
271
  console.error('[Heartbeat] Initial heartbeat failed:', err.message)
272
272
  })
273
273
 
274
274
  // Start periodic heartbeat
275
275
  heartbeatTimer = setInterval(() => {
276
276
  const { currentStatus, currentTask } = getStatus()
277
- sendHeartbeat({ status: currentStatus, current_task: currentTask, version, capabilities: getCapabilities() }).catch(err => {
277
+ sendHeartbeat({ status: currentStatus, current_task: currentTask, version }).catch(err => {
278
278
  console.error('[Heartbeat] Periodic heartbeat failed:', err.message)
279
279
  })
280
280
  }, HEARTBEAT_INTERVAL_MS)