@geekbeer/minion 2.33.4 → 2.42.5
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/.env.example +0 -3
- package/README.md +0 -1
- package/core/api.js +13 -0
- package/core/config.js +46 -1
- package/core/lib/log-manager.js +4 -1
- package/core/lib/platform.js +8 -13
- package/core/lib/revision-watcher.js +252 -0
- package/core/lib/step-poller.js +222 -0
- package/core/lib/strip-ansi.js +18 -0
- package/core/lib/workflow-orchestrator.js +382 -0
- package/core/routes/diagnose.js +296 -0
- package/core/routes/health.js +27 -0
- package/core/routes/routines.js +15 -10
- package/core/routes/skills.js +4 -1
- package/core/routes/workflows.js +49 -2
- package/core/stores/chat-store.js +8 -1
- package/core/stores/routine-store.js +2 -2
- package/linux/lib/process-manager.js +14 -0
- package/linux/minion-cli.sh +57 -16
- package/linux/routes/chat.js +182 -20
- package/linux/routes/config.js +8 -12
- package/linux/routine-runner.js +5 -4
- package/linux/server.js +53 -1
- package/linux/workflow-runner.js +25 -61
- package/package.json +1 -1
- package/roles/pm.md +11 -12
- package/win/lib/process-manager.js +15 -0
- package/win/minion-cli.ps1 +122 -27
- package/win/routes/chat.js +178 -14
- package/win/routes/config.js +6 -2
- package/win/routine-runner.js +4 -2
- package/win/server.js +53 -0
- package/win/workflow-runner.js +31 -43
- package/skills/execution-report/SKILL.md +0 -106
package/core/routes/health.js
CHANGED
|
@@ -8,8 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const { version } = require('../../package.json')
|
|
11
|
+
const { config, isHqConfigured } = require('../config')
|
|
12
|
+
const { sendHeartbeat } = require('../api')
|
|
11
13
|
const { getLlmServices, isLlmCommandConfigured } = require('../lib/llm-checker')
|
|
12
14
|
|
|
15
|
+
function maskToken(token) {
|
|
16
|
+
if (!token || token.length < 8) return token ? '***' : ''
|
|
17
|
+
return token.slice(0, 3) + '...' + token.slice(-3)
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
// Shared status state
|
|
14
21
|
let currentStatus = 'online'
|
|
15
22
|
let currentTask = null
|
|
@@ -51,6 +58,15 @@ async function healthRoutes(fastify) {
|
|
|
51
58
|
timestamp: new Date().toISOString(),
|
|
52
59
|
llm_services: getLlmServices(),
|
|
53
60
|
llm_command_configured: isLlmCommandConfigured(),
|
|
61
|
+
env: {
|
|
62
|
+
HQ_URL: config.HQ_URL || '',
|
|
63
|
+
MINION_ID: config.MINION_ID || '',
|
|
64
|
+
MINION_USER: process.env.MINION_USER || '',
|
|
65
|
+
AGENT_PORT: config.AGENT_PORT,
|
|
66
|
+
API_TOKEN: maskToken(config.API_TOKEN),
|
|
67
|
+
LLM_COMMAND: config.LLM_COMMAND || '',
|
|
68
|
+
HEARTBEAT_INTERVAL: process.env.HEARTBEAT_INTERVAL || '',
|
|
69
|
+
},
|
|
54
70
|
}
|
|
55
71
|
})
|
|
56
72
|
|
|
@@ -58,18 +74,29 @@ async function healthRoutes(fastify) {
|
|
|
58
74
|
fastify.post('/api/status', async (request, reply) => {
|
|
59
75
|
const { status, current_task } = request.body || {}
|
|
60
76
|
|
|
77
|
+
let changed = false
|
|
78
|
+
|
|
61
79
|
if (status) {
|
|
62
80
|
if (!['online', 'offline', 'busy'].includes(status)) {
|
|
63
81
|
reply.code(400)
|
|
64
82
|
return { success: false, error: `Invalid status: ${status}` }
|
|
65
83
|
}
|
|
84
|
+
if (currentStatus !== status) changed = true
|
|
66
85
|
currentStatus = status
|
|
67
86
|
}
|
|
68
87
|
|
|
69
88
|
if (current_task !== undefined) {
|
|
89
|
+
if (currentTask !== (current_task || null)) changed = true
|
|
70
90
|
currentTask = current_task || null
|
|
71
91
|
}
|
|
72
92
|
|
|
93
|
+
// Push status change to HQ immediately via heartbeat
|
|
94
|
+
if (changed && isHqConfigured()) {
|
|
95
|
+
sendHeartbeat({ status: currentStatus, current_task: currentTask, version }).catch(err => {
|
|
96
|
+
console.error('[Heartbeat] Status-change heartbeat failed:', err.message)
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
73
100
|
return { success: true }
|
|
74
101
|
})
|
|
75
102
|
}
|
package/core/routes/routines.js
CHANGED
|
@@ -81,7 +81,7 @@ async function routineRoutes(fastify, opts) {
|
|
|
81
81
|
|
|
82
82
|
// Merge routine data with runtime status
|
|
83
83
|
const result = routines.map(r => {
|
|
84
|
-
const runtimeInfo = status.routines.find(rr => rr.id === r.id)
|
|
84
|
+
const runtimeInfo = status.routines.find(rr => String(rr.id) === String(r.id))
|
|
85
85
|
return {
|
|
86
86
|
...r,
|
|
87
87
|
next_run: runtimeInfo?.next_run || null,
|
|
@@ -149,13 +149,15 @@ async function routineRoutes(fastify, opts) {
|
|
|
149
149
|
const { id } = request.params
|
|
150
150
|
const updates = request.body || {}
|
|
151
151
|
|
|
152
|
-
console.log(`[Routines] Updating routine schedule: ${id}`)
|
|
152
|
+
console.log(`[Routines] Updating routine schedule: ${id} (type: ${typeof id})`)
|
|
153
153
|
|
|
154
154
|
try {
|
|
155
155
|
const routines = await routineStore.load()
|
|
156
|
-
const index = routines.findIndex(r => r.id === id)
|
|
156
|
+
const index = routines.findIndex(r => String(r.id) === String(id))
|
|
157
157
|
|
|
158
158
|
if (index < 0) {
|
|
159
|
+
const storedIds = routines.map(r => `${r.name}(id=${r.id}, type=${typeof r.id})`).join(', ')
|
|
160
|
+
console.error(`[Routines] Schedule update: routine not found. Requested id=${id}. Stored: [${storedIds}]`)
|
|
159
161
|
reply.code(404)
|
|
160
162
|
return { success: false, error: 'Routine not found' }
|
|
161
163
|
}
|
|
@@ -195,7 +197,7 @@ async function routineRoutes(fastify, opts) {
|
|
|
195
197
|
|
|
196
198
|
try {
|
|
197
199
|
const routines = await routineStore.load()
|
|
198
|
-
const index = routines.findIndex(r => r.id === id)
|
|
200
|
+
const index = routines.findIndex(r => String(r.id) === String(id))
|
|
199
201
|
|
|
200
202
|
if (index < 0) {
|
|
201
203
|
reply.code(404)
|
|
@@ -265,20 +267,23 @@ async function routineRoutes(fastify, opts) {
|
|
|
265
267
|
return { success: false, error: 'Unauthorized' }
|
|
266
268
|
}
|
|
267
269
|
|
|
268
|
-
const { routine_id } = request.body || {}
|
|
270
|
+
const { routine_id, routine_name } = request.body || {}
|
|
269
271
|
|
|
270
|
-
if (!routine_id) {
|
|
272
|
+
if (!routine_id && !routine_name) {
|
|
271
273
|
reply.code(400)
|
|
272
|
-
return { success: false, error: 'routine_id is required' }
|
|
274
|
+
return { success: false, error: 'routine_id or routine_name is required' }
|
|
273
275
|
}
|
|
274
276
|
|
|
275
|
-
// Find the routine in local store
|
|
277
|
+
// Find the routine in local store (try id first, then name fallback)
|
|
276
278
|
const routines = await routineStore.load()
|
|
277
|
-
const routine = routines.find(r => r.id === routine_id)
|
|
279
|
+
const routine = routines.find(r => routine_id != null && String(r.id) === String(routine_id))
|
|
280
|
+
|| (routine_name && routines.find(r => r.name === routine_name))
|
|
278
281
|
|
|
279
282
|
if (!routine) {
|
|
283
|
+
const storedIds = routines.map(r => `${r.name}(${r.id || 'no-id'})`).join(', ')
|
|
284
|
+
console.error(`[Routines] Trigger: routine not found. Requested id=${routine_id}, name=${routine_name}. Stored: [${storedIds}]`)
|
|
280
285
|
reply.code(404)
|
|
281
|
-
return { success: false, error:
|
|
286
|
+
return { success: false, error: `Routine not found (id=${routine_id}). Available: [${storedIds}]` }
|
|
282
287
|
}
|
|
283
288
|
|
|
284
289
|
console.log(`[Routines] Manual trigger for: ${routine.name}`)
|
package/core/routes/skills.js
CHANGED
|
@@ -51,7 +51,7 @@ function parseFrontmatter(content) {
|
|
|
51
51
|
* @param {Array<{filename: string, content: string}>} [opts.references] - Reference files
|
|
52
52
|
* @returns {Promise<{path: string, references_count: number}>}
|
|
53
53
|
*/
|
|
54
|
-
async function writeSkillToLocal(name, { content, description, display_name, references = [] }) {
|
|
54
|
+
async function writeSkillToLocal(name, { content, description, display_name, type, references = [] }) {
|
|
55
55
|
const skillDir = path.join(config.HOME_DIR, '.claude', 'skills', name)
|
|
56
56
|
const referencesDir = path.join(skillDir, 'references')
|
|
57
57
|
|
|
@@ -62,6 +62,7 @@ async function writeSkillToLocal(name, { content, description, display_name, ref
|
|
|
62
62
|
const frontmatterLines = [
|
|
63
63
|
`name: ${name}`,
|
|
64
64
|
display_name ? `display_name: ${display_name}` : null,
|
|
65
|
+
type ? `type: ${type}` : null,
|
|
65
66
|
`description: ${description || ''}`,
|
|
66
67
|
].filter(Boolean).join('\n')
|
|
67
68
|
|
|
@@ -118,6 +119,7 @@ async function pushSkillToHQ(name) {
|
|
|
118
119
|
display_name: metadata.display_name || metadata.name || name,
|
|
119
120
|
description: metadata.description || '',
|
|
120
121
|
content: body,
|
|
122
|
+
type: metadata.type || 'workflow',
|
|
121
123
|
references,
|
|
122
124
|
}),
|
|
123
125
|
})
|
|
@@ -240,6 +242,7 @@ async function skillRoutes(fastify, opts) {
|
|
|
240
242
|
content: skill.content,
|
|
241
243
|
description: skill.description,
|
|
242
244
|
display_name: skill.display_name,
|
|
245
|
+
type: skill.type,
|
|
243
246
|
references: skill.references || [],
|
|
244
247
|
})
|
|
245
248
|
|
package/core/routes/workflows.js
CHANGED
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
* - GET /api/executions/:id - Get single execution details
|
|
15
15
|
* - GET /api/executions/:id/log - Get execution log file content
|
|
16
16
|
* - POST /api/executions/:id/outcome - Update execution outcome (no auth, local)
|
|
17
|
+
* - POST /api/workflows/orchestrate - Start multi-minion orchestration (from HQ)
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
const fs = require('fs').promises
|
|
20
21
|
const path = require('path')
|
|
22
|
+
const { orchestrate } = require('../lib/workflow-orchestrator')
|
|
21
23
|
|
|
22
24
|
const { verifyToken } = require('../lib/auth')
|
|
23
25
|
const workflowStore = require('../stores/workflow-store')
|
|
@@ -493,8 +495,53 @@ async function workflowRoutes(fastify, opts) {
|
|
|
493
495
|
}
|
|
494
496
|
})
|
|
495
497
|
|
|
496
|
-
//
|
|
497
|
-
|
|
498
|
+
// Orchestrate a multi-minion workflow execution (called by HQ)
|
|
499
|
+
fastify.post('/api/workflows/orchestrate', async (request, reply) => {
|
|
500
|
+
if (!verifyToken(request)) {
|
|
501
|
+
reply.code(401)
|
|
502
|
+
return { success: false, error: 'Unauthorized' }
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const { execution_id, steps, hq_url } = request.body || {}
|
|
506
|
+
|
|
507
|
+
if (!execution_id || !Array.isArray(steps)) {
|
|
508
|
+
reply.code(400)
|
|
509
|
+
return { success: false, error: 'execution_id and steps[] are required' }
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
console.log(`[Workflows] Starting orchestration for execution: ${execution_id} (${steps.length} steps)`)
|
|
513
|
+
|
|
514
|
+
// Load optional PM revision policy
|
|
515
|
+
let revisionPolicy = ''
|
|
516
|
+
try {
|
|
517
|
+
const policyPath = path.join(config.HOME_DIR, '.minion', 'revision-policy.md')
|
|
518
|
+
revisionPolicy = await fs.readFile(policyPath, 'utf-8')
|
|
519
|
+
} catch {
|
|
520
|
+
// No custom policy — use defaults
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Run orchestration asynchronously
|
|
524
|
+
orchestrate({ executionId: execution_id, steps, hqUrl: hq_url, revisionPolicy })
|
|
525
|
+
.then((result) => {
|
|
526
|
+
if (result.success) {
|
|
527
|
+
console.log(`[Workflows] Orchestration completed: ${execution_id}`)
|
|
528
|
+
} else {
|
|
529
|
+
console.error(`[Workflows] Orchestration failed: ${execution_id} — ${result.error}`)
|
|
530
|
+
}
|
|
531
|
+
})
|
|
532
|
+
.catch((err) => {
|
|
533
|
+
console.error(`[Workflows] Orchestration error: ${execution_id} — ${err.message}`)
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
success: true,
|
|
538
|
+
message: 'Orchestration started',
|
|
539
|
+
execution_id,
|
|
540
|
+
}
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
// Update execution outcome (called by workflow runner after completion)
|
|
544
|
+
// This endpoint does NOT require auth - runs in the same environment
|
|
498
545
|
fastify.post('/api/executions/:id/outcome', async (request, reply) => {
|
|
499
546
|
const { id } = request.params
|
|
500
547
|
const { outcome, summary, details } = request.body || {}
|
|
@@ -61,8 +61,9 @@ async function save(session) {
|
|
|
61
61
|
* Creates a new session if none exists
|
|
62
62
|
* @param {string} sessionId - Claude CLI session ID
|
|
63
63
|
* @param {{ role: string, content: string }} msg - Message to add
|
|
64
|
+
* @param {number} [turnCount] - Optional turn count to update session with
|
|
64
65
|
*/
|
|
65
|
-
async function addMessage(sessionId, msg) {
|
|
66
|
+
async function addMessage(sessionId, msg, turnCount) {
|
|
66
67
|
let session = await load()
|
|
67
68
|
|
|
68
69
|
// If session_id changed, start a new session
|
|
@@ -70,6 +71,7 @@ async function addMessage(sessionId, msg) {
|
|
|
70
71
|
session = {
|
|
71
72
|
session_id: sessionId,
|
|
72
73
|
messages: [],
|
|
74
|
+
turn_count: 0,
|
|
73
75
|
created_at: Date.now(),
|
|
74
76
|
updated_at: Date.now(),
|
|
75
77
|
}
|
|
@@ -82,6 +84,11 @@ async function addMessage(sessionId, msg) {
|
|
|
82
84
|
})
|
|
83
85
|
session.updated_at = Date.now()
|
|
84
86
|
|
|
87
|
+
// Update turn count if provided
|
|
88
|
+
if (typeof turnCount === 'number' && turnCount > 0) {
|
|
89
|
+
session.turn_count = (session.turn_count || 0) + turnCount
|
|
90
|
+
}
|
|
91
|
+
|
|
85
92
|
// Prune old messages
|
|
86
93
|
if (session.messages.length > MAX_MESSAGES) {
|
|
87
94
|
session.messages = session.messages.slice(-MAX_MESSAGES)
|
|
@@ -60,7 +60,7 @@ async function save(routines) {
|
|
|
60
60
|
*/
|
|
61
61
|
async function updateLastRun(routineId) {
|
|
62
62
|
const routines = await load()
|
|
63
|
-
const routine = routines.find(r => r.id === routineId)
|
|
63
|
+
const routine = routines.find(r => String(r.id) === String(routineId))
|
|
64
64
|
if (routine) {
|
|
65
65
|
routine.last_run = new Date().toISOString()
|
|
66
66
|
await save(routines)
|
|
@@ -85,7 +85,7 @@ async function findByName(name) {
|
|
|
85
85
|
*/
|
|
86
86
|
async function upsertFromHQ(routineData) {
|
|
87
87
|
const routines = await load()
|
|
88
|
-
const index = routines.findIndex(r => r.id === routineData.id)
|
|
88
|
+
const index = routines.findIndex(r => String(r.id) === String(routineData.id))
|
|
89
89
|
|
|
90
90
|
if (index >= 0) {
|
|
91
91
|
// Update from HQ (preserve local-only fields if any)
|
|
@@ -54,6 +54,11 @@ function buildAllowedCommands(procMgr) {
|
|
|
54
54
|
command: `${SUDO}npm install -g @geekbeer/minion@latest && ${SUDO}systemctl restart minion-agent`,
|
|
55
55
|
deferred: true,
|
|
56
56
|
}
|
|
57
|
+
commands['update-agent-dev'] = {
|
|
58
|
+
description: 'Update @geekbeer/minion from Verdaccio (dev) and restart',
|
|
59
|
+
command: `${SUDO}npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873 && ${SUDO}systemctl restart minion-agent`,
|
|
60
|
+
deferred: true,
|
|
61
|
+
}
|
|
57
62
|
commands['restart-display'] = {
|
|
58
63
|
description: 'Restart Xvfb, Fluxbox, x11vnc and noVNC services',
|
|
59
64
|
command: `${SUDO}systemctl restart xvfb fluxbox x11vnc novnc`,
|
|
@@ -73,6 +78,11 @@ function buildAllowedCommands(procMgr) {
|
|
|
73
78
|
command: `${SUDO}npm install -g @geekbeer/minion@latest && ${SUDO}supervisorctl restart minion-agent`,
|
|
74
79
|
deferred: true,
|
|
75
80
|
}
|
|
81
|
+
commands['update-agent-dev'] = {
|
|
82
|
+
description: 'Update @geekbeer/minion from Verdaccio (dev) and restart',
|
|
83
|
+
command: `${SUDO}npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873 && ${SUDO}supervisorctl restart minion-agent`,
|
|
84
|
+
deferred: true,
|
|
85
|
+
}
|
|
76
86
|
commands['restart-display'] = {
|
|
77
87
|
description: 'Restart Xvfb, x11vnc and noVNC services',
|
|
78
88
|
command: `${SUDO}supervisorctl restart xvfb fluxbox x11vnc novnc`,
|
|
@@ -87,6 +97,10 @@ function buildAllowedCommands(procMgr) {
|
|
|
87
97
|
description: 'Update @geekbeer/minion to latest version',
|
|
88
98
|
command: `${SUDO}npm install -g @geekbeer/minion@latest`,
|
|
89
99
|
}
|
|
100
|
+
commands['update-agent-dev'] = {
|
|
101
|
+
description: 'Update @geekbeer/minion from Verdaccio (dev)',
|
|
102
|
+
command: `${SUDO}npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873`,
|
|
103
|
+
}
|
|
90
104
|
commands['status-services'] = {
|
|
91
105
|
description: 'Show agent process info',
|
|
92
106
|
command: 'echo "Process Manager: standalone (no systemd/supervisord)" && echo "Agent PID: $$" && echo "Node version: $(node -v)" && echo "Uptime: $(ps -o etime= -p $$)"',
|
package/linux/minion-cli.sh
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
# sudo minion-cli restart # Restart agent service (root)
|
|
12
12
|
# minion-cli status # Get current status
|
|
13
13
|
# minion-cli health # Health check
|
|
14
|
+
# minion-cli diagnose # Run full service diagnostics
|
|
14
15
|
# minion-cli set-status busy "Running X" # Set status and task
|
|
15
16
|
# minion-cli set-status online # Set status only
|
|
16
17
|
#
|
|
@@ -186,8 +187,8 @@ do_setup() {
|
|
|
186
187
|
fi
|
|
187
188
|
elif [ "$(id -u)" -eq 0 ]; then
|
|
188
189
|
# Running as root without --user
|
|
189
|
-
if [ "$PROC_MGR" = "supervisord" ]; then
|
|
190
|
-
# Container environment (supervisord) - allow root for backward compatibility
|
|
190
|
+
if [ "$PROC_MGR" = "supervisord" ] && [ -f /.dockerenv ]; then
|
|
191
|
+
# Container environment (supervisord + Docker) - allow root for backward compatibility
|
|
191
192
|
echo " NOTE: Running as root in container environment (supervisord detected)"
|
|
192
193
|
TARGET_USER="root"
|
|
193
194
|
TARGET_HOME="$HOME"
|
|
@@ -303,7 +304,6 @@ do_setup() {
|
|
|
303
304
|
fi
|
|
304
305
|
|
|
305
306
|
ENV_CONTENT+="AGENT_PORT=8080\n"
|
|
306
|
-
ENV_CONTENT+="HEARTBEAT_INTERVAL=30\n"
|
|
307
307
|
ENV_CONTENT+="MINION_USER=${TARGET_USER}\n"
|
|
308
308
|
|
|
309
309
|
echo -e "$ENV_CONTENT" | $SUDO tee /opt/minion-agent/.env > /dev/null
|
|
@@ -316,21 +316,23 @@ do_setup() {
|
|
|
316
316
|
local NPM_BIN
|
|
317
317
|
NPM_BIN="$(which npm)"
|
|
318
318
|
local SUDOERS_FILE="/etc/sudoers.d/minion-agent"
|
|
319
|
-
local SUDOERS_CONTENT="${TARGET_USER} ALL=(root) NOPASSWD: ${NPM_BIN} install -g @geekbeer/minion@latest"
|
|
320
319
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
SUDOERS_CONTENT+="\n${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/systemctl restart xvfb fluxbox autocutsel vnc novnc"
|
|
325
|
-
;;
|
|
326
|
-
supervisord)
|
|
327
|
-
SUDOERS_CONTENT+="\n${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/supervisorctl restart minion-agent"
|
|
328
|
-
SUDOERS_CONTENT+="\n${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/supervisorctl restart xvfb fluxbox autocutsel vnc novnc"
|
|
329
|
-
SUDOERS_CONTENT+="\n${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/supervisorctl status"
|
|
330
|
-
;;
|
|
331
|
-
esac
|
|
320
|
+
{
|
|
321
|
+
echo "${TARGET_USER} ALL=(root) NOPASSWD: ${NPM_BIN} install -g @geekbeer/minion@latest"
|
|
322
|
+
echo "${TARGET_USER} ALL=(root) NOPASSWD: ${NPM_BIN} install -g @geekbeer/minion@latest --registry *"
|
|
332
323
|
|
|
333
|
-
|
|
324
|
+
case "$PROC_MGR" in
|
|
325
|
+
systemd)
|
|
326
|
+
echo "${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/systemctl restart minion-agent"
|
|
327
|
+
echo "${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/systemctl restart xvfb fluxbox autocutsel vnc novnc"
|
|
328
|
+
;;
|
|
329
|
+
supervisord)
|
|
330
|
+
echo "${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/supervisorctl restart minion-agent"
|
|
331
|
+
echo "${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/supervisorctl restart xvfb fluxbox autocutsel vnc novnc"
|
|
332
|
+
echo "${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/supervisorctl status"
|
|
333
|
+
;;
|
|
334
|
+
esac
|
|
335
|
+
} | $SUDO tee "$SUDOERS_FILE" > /dev/null
|
|
334
336
|
$SUDO chmod 440 "$SUDOERS_FILE"
|
|
335
337
|
echo " -> $SUDOERS_FILE created"
|
|
336
338
|
else
|
|
@@ -1034,6 +1036,44 @@ case "${1:-}" in
|
|
|
1034
1036
|
curl -s "$AGENT_URL/api/health" | jq .
|
|
1035
1037
|
;;
|
|
1036
1038
|
|
|
1039
|
+
diagnose)
|
|
1040
|
+
echo "Running diagnostics..."
|
|
1041
|
+
echo ""
|
|
1042
|
+
RESULT=$(curl -s --max-time 15 "$AGENT_URL/api/diagnose" 2>/dev/null) || {
|
|
1043
|
+
echo "FAIL: Cannot reach minion agent at $AGENT_URL"
|
|
1044
|
+
echo " Is the agent running? Try: sudo minion-cli start"
|
|
1045
|
+
exit 1
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
# Parse and display results with color coding
|
|
1049
|
+
SUMMARY=$(echo "$RESULT" | jq -r '.summary // "UNKNOWN"')
|
|
1050
|
+
VERSION=$(echo "$RESULT" | jq -r '.version // "?"')
|
|
1051
|
+
PLATFORM=$(echo "$RESULT" | jq -r '.platform // "?"')
|
|
1052
|
+
|
|
1053
|
+
echo "=== Minion Diagnostics (v${VERSION}, ${PLATFORM}) ==="
|
|
1054
|
+
echo ""
|
|
1055
|
+
|
|
1056
|
+
# Display each check with pass/fail indicator
|
|
1057
|
+
for CHECK in agent hq tunnel vnc terminal llm env; do
|
|
1058
|
+
OK=$(echo "$RESULT" | jq -r ".checks.${CHECK}.ok")
|
|
1059
|
+
DETAILS=$(echo "$RESULT" | jq -r ".checks.${CHECK}.details // \"\"")
|
|
1060
|
+
CHECK_UPPER=$(echo "$CHECK" | tr '[:lower:]' '[:upper:]')
|
|
1061
|
+
|
|
1062
|
+
if [ "$OK" = "true" ]; then
|
|
1063
|
+
printf " \033[32m[PASS]\033[0m %-10s %s\n" "$CHECK_UPPER" "$DETAILS"
|
|
1064
|
+
else
|
|
1065
|
+
printf " \033[31m[FAIL]\033[0m %-10s %s\n" "$CHECK_UPPER" "$DETAILS"
|
|
1066
|
+
fi
|
|
1067
|
+
done
|
|
1068
|
+
|
|
1069
|
+
echo ""
|
|
1070
|
+
if [ "$SUMMARY" = "ALL OK" ]; then
|
|
1071
|
+
echo -e "\033[32m$SUMMARY\033[0m"
|
|
1072
|
+
else
|
|
1073
|
+
echo -e "\033[33m$SUMMARY\033[0m"
|
|
1074
|
+
fi
|
|
1075
|
+
;;
|
|
1076
|
+
|
|
1037
1077
|
start)
|
|
1038
1078
|
require_root start
|
|
1039
1079
|
svc_control start
|
|
@@ -1107,6 +1147,7 @@ case "${1:-}" in
|
|
|
1107
1147
|
echo " sudo minion-cli restart # Restart agent service (root)"
|
|
1108
1148
|
echo " minion-cli status # Get current status"
|
|
1109
1149
|
echo " minion-cli health # Health check"
|
|
1150
|
+
echo " minion-cli diagnose # Run full service diagnostics"
|
|
1110
1151
|
echo " minion-cli set-status <status> [task] # Set status and optional task"
|
|
1111
1152
|
echo " minion-cli skill <push|fetch|list> # Manage skills with HQ"
|
|
1112
1153
|
echo " minion-cli --version # Show version"
|