@geekbeer/minion 2.5.0 → 2.6.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/minion-cli.sh +142 -4
- package/package.json +3 -1
- package/routes/files.js +21 -7
- package/routes/index.js +10 -0
- package/routes/routines.js +260 -0
- package/routes/workflows.js +14 -0
- package/routine-runner.js +404 -0
- package/routine-store.js +116 -0
- package/rules/minion.md +194 -52
- package/server.js +15 -1
- package/workflow-runner.js +1 -0
- package/workflow-store.js +5 -1
package/minion-cli.sh
CHANGED
|
@@ -286,11 +286,11 @@ do_setup() {
|
|
|
286
286
|
case "$PROC_MGR" in
|
|
287
287
|
systemd)
|
|
288
288
|
SUDOERS_CONTENT+="\n${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/systemctl restart minion-agent"
|
|
289
|
-
SUDOERS_CONTENT+="\n${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/systemctl restart xvfb fluxbox
|
|
289
|
+
SUDOERS_CONTENT+="\n${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/systemctl restart xvfb fluxbox autocutsel vnc novnc"
|
|
290
290
|
;;
|
|
291
291
|
supervisord)
|
|
292
292
|
SUDOERS_CONTENT+="\n${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/supervisorctl restart minion-agent"
|
|
293
|
-
SUDOERS_CONTENT+="\n${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/supervisorctl restart xvfb fluxbox
|
|
293
|
+
SUDOERS_CONTENT+="\n${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/supervisorctl restart xvfb fluxbox autocutsel vnc novnc"
|
|
294
294
|
SUDOERS_CONTENT+="\n${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/supervisorctl status"
|
|
295
295
|
;;
|
|
296
296
|
esac
|
|
@@ -354,6 +354,135 @@ Environment=MINION_USER=${TARGET_USER}
|
|
|
354
354
|
WantedBy=multi-user.target
|
|
355
355
|
SVCEOF
|
|
356
356
|
echo " -> /etc/systemd/system/minion-agent.service created"
|
|
357
|
+
|
|
358
|
+
# VNC stack services (only if Xvfb is installed)
|
|
359
|
+
if command -v Xvfb &>/dev/null; then
|
|
360
|
+
echo " Creating VNC stack services..."
|
|
361
|
+
|
|
362
|
+
# xvfb: virtual display (-ac disables access control so any user can connect)
|
|
363
|
+
$SUDO tee /etc/systemd/system/xvfb.service > /dev/null <<XVFBEOF
|
|
364
|
+
[Unit]
|
|
365
|
+
Description=Xvfb virtual framebuffer
|
|
366
|
+
After=network.target
|
|
367
|
+
|
|
368
|
+
[Service]
|
|
369
|
+
Type=simple
|
|
370
|
+
ExecStart=/usr/bin/Xvfb :99 -screen 0 1920x1080x24 -ac
|
|
371
|
+
Restart=always
|
|
372
|
+
RestartSec=5
|
|
373
|
+
|
|
374
|
+
[Install]
|
|
375
|
+
WantedBy=multi-user.target
|
|
376
|
+
XVFBEOF
|
|
377
|
+
echo " -> /etc/systemd/system/xvfb.service created"
|
|
378
|
+
|
|
379
|
+
# fluxbox: window manager (runs as TARGET_USER so terminals open as that user)
|
|
380
|
+
$SUDO tee /etc/systemd/system/fluxbox.service > /dev/null <<FBEOF
|
|
381
|
+
[Unit]
|
|
382
|
+
Description=Fluxbox window manager
|
|
383
|
+
After=xvfb.service
|
|
384
|
+
Requires=xvfb.service
|
|
385
|
+
|
|
386
|
+
[Service]
|
|
387
|
+
Type=simple
|
|
388
|
+
User=${TARGET_USER}
|
|
389
|
+
Environment=DISPLAY=:99
|
|
390
|
+
ExecStart=/usr/bin/fluxbox
|
|
391
|
+
Restart=always
|
|
392
|
+
RestartSec=5
|
|
393
|
+
|
|
394
|
+
[Install]
|
|
395
|
+
WantedBy=multi-user.target
|
|
396
|
+
FBEOF
|
|
397
|
+
echo " -> /etc/systemd/system/fluxbox.service created (User=${TARGET_USER})"
|
|
398
|
+
|
|
399
|
+
# autocutsel: clipboard sync (runs as TARGET_USER to match desktop session)
|
|
400
|
+
if command -v autocutsel &>/dev/null; then
|
|
401
|
+
$SUDO tee /etc/systemd/system/autocutsel.service > /dev/null <<ACEOF
|
|
402
|
+
[Unit]
|
|
403
|
+
Description=X clipboard synchronization
|
|
404
|
+
After=xvfb.service
|
|
405
|
+
Requires=xvfb.service
|
|
406
|
+
|
|
407
|
+
[Service]
|
|
408
|
+
Type=forking
|
|
409
|
+
User=${TARGET_USER}
|
|
410
|
+
Environment=DISPLAY=:99
|
|
411
|
+
ExecStart=/usr/bin/autocutsel -fork
|
|
412
|
+
Restart=always
|
|
413
|
+
RestartSec=5
|
|
414
|
+
|
|
415
|
+
[Install]
|
|
416
|
+
WantedBy=multi-user.target
|
|
417
|
+
ACEOF
|
|
418
|
+
echo " -> /etc/systemd/system/autocutsel.service created (User=${TARGET_USER})"
|
|
419
|
+
fi
|
|
420
|
+
|
|
421
|
+
# vnc: VNC server (detect x0vncserver or x11vnc)
|
|
422
|
+
if command -v x0vncserver &>/dev/null; then
|
|
423
|
+
$SUDO tee /etc/systemd/system/vnc.service > /dev/null <<VNCEOF
|
|
424
|
+
[Unit]
|
|
425
|
+
Description=TigerVNC scraping server
|
|
426
|
+
After=fluxbox.service
|
|
427
|
+
Requires=xvfb.service
|
|
428
|
+
|
|
429
|
+
[Service]
|
|
430
|
+
Type=simple
|
|
431
|
+
Environment=DISPLAY=:99
|
|
432
|
+
ExecStart=/usr/bin/x0vncserver -display :99 -rfbport 5900 -SecurityTypes None -fg
|
|
433
|
+
Restart=always
|
|
434
|
+
RestartSec=5
|
|
435
|
+
|
|
436
|
+
[Install]
|
|
437
|
+
WantedBy=multi-user.target
|
|
438
|
+
VNCEOF
|
|
439
|
+
echo " -> /etc/systemd/system/vnc.service created (x0vncserver)"
|
|
440
|
+
elif command -v x11vnc &>/dev/null; then
|
|
441
|
+
$SUDO tee /etc/systemd/system/vnc.service > /dev/null <<VNCEOF
|
|
442
|
+
[Unit]
|
|
443
|
+
Description=x11vnc VNC server
|
|
444
|
+
After=fluxbox.service
|
|
445
|
+
Requires=xvfb.service
|
|
446
|
+
|
|
447
|
+
[Service]
|
|
448
|
+
Type=simple
|
|
449
|
+
Environment=DISPLAY=:99
|
|
450
|
+
ExecStart=/usr/bin/x11vnc -display :99 -rfbport 5900 -nopw -forever -shared
|
|
451
|
+
Restart=always
|
|
452
|
+
RestartSec=5
|
|
453
|
+
|
|
454
|
+
[Install]
|
|
455
|
+
WantedBy=multi-user.target
|
|
456
|
+
VNCEOF
|
|
457
|
+
echo " -> /etc/systemd/system/vnc.service created (x11vnc)"
|
|
458
|
+
else
|
|
459
|
+
echo " WARNING: No VNC server found (x0vncserver or x11vnc)"
|
|
460
|
+
fi
|
|
461
|
+
|
|
462
|
+
# novnc: WebSocket proxy for browser access
|
|
463
|
+
if command -v websockify &>/dev/null; then
|
|
464
|
+
local NOVNC_WEB="/usr/share/novnc"
|
|
465
|
+
if [ ! -d "$NOVNC_WEB" ]; then
|
|
466
|
+
NOVNC_WEB="/usr/share/novnc/utils/../"
|
|
467
|
+
fi
|
|
468
|
+
$SUDO tee /etc/systemd/system/novnc.service > /dev/null <<NVNCEOF
|
|
469
|
+
[Unit]
|
|
470
|
+
Description=noVNC WebSocket proxy
|
|
471
|
+
After=vnc.service
|
|
472
|
+
Requires=vnc.service
|
|
473
|
+
|
|
474
|
+
[Service]
|
|
475
|
+
Type=simple
|
|
476
|
+
ExecStart=/usr/bin/websockify --web=${NOVNC_WEB} 6080 localhost:5900
|
|
477
|
+
Restart=always
|
|
478
|
+
RestartSec=5
|
|
479
|
+
|
|
480
|
+
[Install]
|
|
481
|
+
WantedBy=multi-user.target
|
|
482
|
+
NVNCEOF
|
|
483
|
+
echo " -> /etc/systemd/system/novnc.service created"
|
|
484
|
+
fi
|
|
485
|
+
fi
|
|
357
486
|
;;
|
|
358
487
|
|
|
359
488
|
supervisord)
|
|
@@ -394,11 +523,20 @@ SUPEOF
|
|
|
394
523
|
;;
|
|
395
524
|
esac
|
|
396
525
|
|
|
397
|
-
# Step 7: Enable and start
|
|
398
|
-
echo "[7/${TOTAL_STEPS}] Starting
|
|
526
|
+
# Step 7: Enable and start services
|
|
527
|
+
echo "[7/${TOTAL_STEPS}] Starting services..."
|
|
399
528
|
case "$PROC_MGR" in
|
|
400
529
|
systemd)
|
|
401
530
|
$SUDO systemctl daemon-reload
|
|
531
|
+
# Enable and start VNC stack (if service files were created)
|
|
532
|
+
for svc in xvfb fluxbox autocutsel vnc novnc; do
|
|
533
|
+
if [ -f "/etc/systemd/system/${svc}.service" ]; then
|
|
534
|
+
$SUDO systemctl enable "$svc"
|
|
535
|
+
$SUDO systemctl restart "$svc"
|
|
536
|
+
echo " -> ${svc} started"
|
|
537
|
+
fi
|
|
538
|
+
done
|
|
539
|
+
# Enable and start agent services
|
|
402
540
|
$SUDO systemctl enable tmux-init
|
|
403
541
|
$SUDO systemctl start tmux-init
|
|
404
542
|
$SUDO systemctl enable minion-agent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekbeer/minion",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "AI Agent runtime for Minion - manages status and skill deployment on VPS",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"terminal-proxy.js",
|
|
14
14
|
"workflow-runner.js",
|
|
15
15
|
"workflow-store.js",
|
|
16
|
+
"routine-runner.js",
|
|
17
|
+
"routine-store.js",
|
|
16
18
|
"execution-store.js",
|
|
17
19
|
"lib/",
|
|
18
20
|
"routes/",
|
package/routes/files.js
CHANGED
|
@@ -15,6 +15,7 @@ const fs = require('fs').promises
|
|
|
15
15
|
const fsSync = require('fs')
|
|
16
16
|
const path = require('path')
|
|
17
17
|
const os = require('os')
|
|
18
|
+
const { spawn } = require('child_process')
|
|
18
19
|
|
|
19
20
|
const { verifyToken } = require('../lib/auth')
|
|
20
21
|
|
|
@@ -124,7 +125,7 @@ async function fileRoutes(fastify) {
|
|
|
124
125
|
}
|
|
125
126
|
})
|
|
126
127
|
|
|
127
|
-
// GET /api/files/* - Download a file
|
|
128
|
+
// GET /api/files/* - Download a file or directory (with ?format=tar.gz for directories)
|
|
128
129
|
fastify.get('/api/files/*', async (request, reply) => {
|
|
129
130
|
if (!verifyToken(request)) {
|
|
130
131
|
reply.code(401)
|
|
@@ -146,12 +147,6 @@ async function fileRoutes(fastify) {
|
|
|
146
147
|
try {
|
|
147
148
|
const stat = await fs.lstat(resolved)
|
|
148
149
|
|
|
149
|
-
// Don't allow downloading directories
|
|
150
|
-
if (stat.isDirectory()) {
|
|
151
|
-
reply.code(400)
|
|
152
|
-
return { success: false, error: 'Cannot download a directory' }
|
|
153
|
-
}
|
|
154
|
-
|
|
155
150
|
// Check symlink safety
|
|
156
151
|
if (stat.isSymbolicLink()) {
|
|
157
152
|
const realPath = await fs.realpath(resolved)
|
|
@@ -161,6 +156,25 @@ async function fileRoutes(fastify) {
|
|
|
161
156
|
}
|
|
162
157
|
}
|
|
163
158
|
|
|
159
|
+
// Directory download as tar.gz
|
|
160
|
+
if (stat.isDirectory()) {
|
|
161
|
+
const format = request.query.format
|
|
162
|
+
if (format !== 'tar.gz') {
|
|
163
|
+
reply.code(400)
|
|
164
|
+
return { success: false, error: 'Cannot download a directory. Use ?format=tar.gz' }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const dirName = path.basename(resolved)
|
|
168
|
+
const parentDir = path.dirname(resolved)
|
|
169
|
+
|
|
170
|
+
reply
|
|
171
|
+
.type('application/gzip')
|
|
172
|
+
.header('Content-Disposition', `attachment; filename="${encodeURIComponent(dirName)}.tar.gz"`)
|
|
173
|
+
|
|
174
|
+
const tar = spawn('tar', ['-czf', '-', '-C', parentDir, dirName])
|
|
175
|
+
return reply.send(tar.stdout)
|
|
176
|
+
}
|
|
177
|
+
|
|
164
178
|
const filename = path.basename(resolved)
|
|
165
179
|
reply
|
|
166
180
|
.type('application/octet-stream')
|
package/routes/index.js
CHANGED
|
@@ -35,6 +35,14 @@
|
|
|
35
35
|
* POST /api/terminal/send - Send keys to session (auth required)
|
|
36
36
|
* POST /api/terminal/kill - Kill a session (auth required)
|
|
37
37
|
*
|
|
38
|
+
* Routines (routes/routines.js)
|
|
39
|
+
* GET /api/routines - List all routines with status (auth required)
|
|
40
|
+
* POST /api/routines - Receive routines from HQ (auth required)
|
|
41
|
+
* POST /api/routines/sync - Pull routines from HQ (auth required)
|
|
42
|
+
* PUT /api/routines/:id/schedule - Update routine schedule (auth required)
|
|
43
|
+
* DELETE /api/routines/:id - Remove a routine (auth required)
|
|
44
|
+
* POST /api/routines/trigger - Manual trigger for a routine (auth required)
|
|
45
|
+
*
|
|
38
46
|
* Files (routes/files.js)
|
|
39
47
|
* GET /api/files - List files in directory (auth required)
|
|
40
48
|
* GET /api/files/* - Download a file (auth required)
|
|
@@ -47,6 +55,7 @@ const { healthRoutes, setOffline } = require('./health')
|
|
|
47
55
|
const { commandRoutes, getProcessManager, getAllowedCommands } = require('./commands')
|
|
48
56
|
const { skillRoutes } = require('./skills')
|
|
49
57
|
const { workflowRoutes } = require('./workflows')
|
|
58
|
+
const { routineRoutes } = require('./routines')
|
|
50
59
|
const { terminalRoutes } = require('./terminal')
|
|
51
60
|
const { fileRoutes } = require('./files')
|
|
52
61
|
|
|
@@ -59,6 +68,7 @@ async function registerRoutes(fastify) {
|
|
|
59
68
|
await fastify.register(commandRoutes)
|
|
60
69
|
await fastify.register(skillRoutes)
|
|
61
70
|
await fastify.register(workflowRoutes)
|
|
71
|
+
await fastify.register(routineRoutes)
|
|
62
72
|
await fastify.register(terminalRoutes)
|
|
63
73
|
await fastify.register(fileRoutes)
|
|
64
74
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routine management endpoints
|
|
3
|
+
*
|
|
4
|
+
* Endpoints:
|
|
5
|
+
* - GET /api/routines - List all routines with status
|
|
6
|
+
* - POST /api/routines - Receive routines from HQ (upsert/additive)
|
|
7
|
+
* - POST /api/routines/sync - Pull routines from HQ and sync locally
|
|
8
|
+
* - PUT /api/routines/:id/schedule - Update a routine schedule (cron_expression, is_active)
|
|
9
|
+
* - DELETE /api/routines/:id - Remove a routine
|
|
10
|
+
* - POST /api/routines/trigger - Manual trigger for a routine
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { verifyToken } = require('../lib/auth')
|
|
14
|
+
const routineRunner = require('../routine-runner')
|
|
15
|
+
const routineStore = require('../routine-store')
|
|
16
|
+
const api = require('../api')
|
|
17
|
+
const { isHqConfigured } = require('../config')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Register routine routes as Fastify plugin
|
|
21
|
+
* @param {import('fastify').FastifyInstance} fastify
|
|
22
|
+
*/
|
|
23
|
+
async function routineRoutes(fastify) {
|
|
24
|
+
// Receive routines from HQ (upsert: add new, update existing)
|
|
25
|
+
fastify.post('/api/routines', async (request, reply) => {
|
|
26
|
+
if (!verifyToken(request)) {
|
|
27
|
+
reply.code(401)
|
|
28
|
+
return { success: false, error: 'Unauthorized' }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { routines: incomingRoutines } = request.body || {}
|
|
32
|
+
|
|
33
|
+
if (!Array.isArray(incomingRoutines)) {
|
|
34
|
+
reply.code(400)
|
|
35
|
+
return { success: false, error: 'routines array is required' }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(`[Routines] Receiving ${incomingRoutines.length} routines from HQ`)
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Load existing routines
|
|
42
|
+
const existingRoutines = await routineStore.load()
|
|
43
|
+
|
|
44
|
+
// Merge: update existing or add new (upsert by id)
|
|
45
|
+
const routineMap = new Map(existingRoutines.map(r => [r.id, r]))
|
|
46
|
+
for (const incoming of incomingRoutines) {
|
|
47
|
+
routineMap.set(incoming.id, incoming)
|
|
48
|
+
}
|
|
49
|
+
const mergedRoutines = Array.from(routineMap.values())
|
|
50
|
+
|
|
51
|
+
// Save to local store
|
|
52
|
+
await routineStore.save(mergedRoutines)
|
|
53
|
+
|
|
54
|
+
// Reload cron jobs
|
|
55
|
+
routineRunner.loadRoutines(mergedRoutines)
|
|
56
|
+
|
|
57
|
+
const activeCount = mergedRoutines.filter(r => r.is_active).length
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
success: true,
|
|
61
|
+
message: `${incomingRoutines.length} routines upserted (total: ${mergedRoutines.length})`,
|
|
62
|
+
active: activeCount,
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error(`[Routines] Failed to load routines: ${error.message}`)
|
|
66
|
+
reply.code(500)
|
|
67
|
+
return { success: false, error: error.message }
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// List all routines with their current status
|
|
72
|
+
fastify.get('/api/routines', async (request, reply) => {
|
|
73
|
+
if (!verifyToken(request)) {
|
|
74
|
+
reply.code(401)
|
|
75
|
+
return { success: false, error: 'Unauthorized' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const routines = await routineStore.load()
|
|
79
|
+
const status = routineRunner.getStatus()
|
|
80
|
+
|
|
81
|
+
// Merge routine data with runtime status
|
|
82
|
+
const result = routines.map(r => {
|
|
83
|
+
const runtimeInfo = status.routines.find(rr => rr.id === r.id)
|
|
84
|
+
return {
|
|
85
|
+
...r,
|
|
86
|
+
next_run: runtimeInfo?.next_run || null,
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
return { routines: result }
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Pull routines from HQ and sync locally
|
|
94
|
+
fastify.post('/api/routines/sync', async (request, reply) => {
|
|
95
|
+
if (!verifyToken(request)) {
|
|
96
|
+
reply.code(401)
|
|
97
|
+
return { success: false, error: 'Unauthorized' }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!isHqConfigured()) {
|
|
101
|
+
reply.code(400)
|
|
102
|
+
return { success: false, error: 'HQ not configured' }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log('[Routines] Syncing routines from HQ')
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const result = await api.request('/routines')
|
|
109
|
+
const hqRoutines = result.routines || []
|
|
110
|
+
|
|
111
|
+
console.log(`[Routines] Received ${hqRoutines.length} routines from HQ`)
|
|
112
|
+
|
|
113
|
+
// Upsert each routine from HQ
|
|
114
|
+
let allRoutines = []
|
|
115
|
+
for (const routine of hqRoutines) {
|
|
116
|
+
allRoutines = await routineStore.upsertFromHQ(routine)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// If no routines from HQ, just load existing
|
|
120
|
+
if (hqRoutines.length === 0) {
|
|
121
|
+
allRoutines = await routineStore.load()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Reload cron jobs
|
|
125
|
+
routineRunner.loadRoutines(allRoutines)
|
|
126
|
+
|
|
127
|
+
const activeCount = allRoutines.filter(r => r.is_active).length
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
success: true,
|
|
131
|
+
message: `Synced ${hqRoutines.length} routines from HQ (total: ${allRoutines.length})`,
|
|
132
|
+
active: activeCount,
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error(`[Routines] Failed to sync routines: ${error.message}`)
|
|
136
|
+
reply.code(error.statusCode || 500)
|
|
137
|
+
return { success: false, error: error.message }
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// Update a routine schedule (cron_expression, is_active)
|
|
142
|
+
fastify.put('/api/routines/:id/schedule', async (request, reply) => {
|
|
143
|
+
if (!verifyToken(request)) {
|
|
144
|
+
reply.code(401)
|
|
145
|
+
return { success: false, error: 'Unauthorized' }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const { id } = request.params
|
|
149
|
+
const updates = request.body || {}
|
|
150
|
+
|
|
151
|
+
console.log(`[Routines] Updating routine schedule: ${id}`)
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const routines = await routineStore.load()
|
|
155
|
+
const index = routines.findIndex(r => r.id === id)
|
|
156
|
+
|
|
157
|
+
if (index < 0) {
|
|
158
|
+
reply.code(404)
|
|
159
|
+
return { success: false, error: 'Routine not found' }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if ('cron_expression' in updates) {
|
|
163
|
+
routines[index].cron_expression = updates.cron_expression
|
|
164
|
+
}
|
|
165
|
+
if ('is_active' in updates) {
|
|
166
|
+
routines[index].is_active = updates.is_active
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Save and reload
|
|
170
|
+
await routineStore.save(routines)
|
|
171
|
+
routineRunner.loadRoutines(routines)
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
success: true,
|
|
175
|
+
routine: routines[index],
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error(`[Routines] Failed to update routine: ${error.message}`)
|
|
179
|
+
reply.code(500)
|
|
180
|
+
return { success: false, error: error.message }
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// Delete a routine
|
|
185
|
+
fastify.delete('/api/routines/:id', async (request, reply) => {
|
|
186
|
+
if (!verifyToken(request)) {
|
|
187
|
+
reply.code(401)
|
|
188
|
+
return { success: false, error: 'Unauthorized' }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const { id } = request.params
|
|
192
|
+
|
|
193
|
+
console.log(`[Routines] Deleting routine: ${id}`)
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const routines = await routineStore.load()
|
|
197
|
+
const index = routines.findIndex(r => r.id === id)
|
|
198
|
+
|
|
199
|
+
if (index < 0) {
|
|
200
|
+
reply.code(404)
|
|
201
|
+
return { success: false, error: 'Routine not found' }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const removed = routines.splice(index, 1)[0]
|
|
205
|
+
|
|
206
|
+
// Save and reload
|
|
207
|
+
await routineStore.save(routines)
|
|
208
|
+
routineRunner.loadRoutines(routines)
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
success: true,
|
|
212
|
+
message: `Routine "${removed.name}" removed`,
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error(`[Routines] Failed to delete routine: ${error.message}`)
|
|
216
|
+
reply.code(500)
|
|
217
|
+
return { success: false, error: error.message }
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// Manual trigger: run a routine immediately
|
|
222
|
+
fastify.post('/api/routines/trigger', async (request, reply) => {
|
|
223
|
+
if (!verifyToken(request)) {
|
|
224
|
+
reply.code(401)
|
|
225
|
+
return { success: false, error: 'Unauthorized' }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const { routine_id } = request.body || {}
|
|
229
|
+
|
|
230
|
+
if (!routine_id) {
|
|
231
|
+
reply.code(400)
|
|
232
|
+
return { success: false, error: 'routine_id is required' }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Find the routine in local store
|
|
236
|
+
const routines = await routineStore.load()
|
|
237
|
+
const routine = routines.find(r => r.id === routine_id)
|
|
238
|
+
|
|
239
|
+
if (!routine) {
|
|
240
|
+
reply.code(404)
|
|
241
|
+
return { success: false, error: 'Routine not found' }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
console.log(`[Routines] Manual trigger for: ${routine.name}`)
|
|
245
|
+
|
|
246
|
+
// Run asynchronously — respond immediately
|
|
247
|
+
const executionPromise = routineRunner.runRoutine(routine)
|
|
248
|
+
executionPromise.catch(err => {
|
|
249
|
+
console.error(`[Routines] Manual trigger failed for ${routine.name}: ${err.message}`)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
success: true,
|
|
254
|
+
name: routine.name,
|
|
255
|
+
message: 'Execution triggered',
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = { routineRoutes }
|
package/routes/workflows.js
CHANGED
|
@@ -156,12 +156,25 @@ async function workflowRoutes(fastify) {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// project_id: from request body, or from local store
|
|
160
|
+
const { project_id } = request.body || {}
|
|
161
|
+
const effectiveProjectId = project_id || workflow.project_id
|
|
162
|
+
|
|
163
|
+
if (!effectiveProjectId) {
|
|
164
|
+
reply.code(400)
|
|
165
|
+
return {
|
|
166
|
+
success: false,
|
|
167
|
+
error: 'project_id is required. Provide it in the request body or ensure the workflow has a project_id.',
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
159
171
|
const result = await api.request('/workflows', {
|
|
160
172
|
method: 'POST',
|
|
161
173
|
body: JSON.stringify({
|
|
162
174
|
name: workflow.name,
|
|
163
175
|
pipeline_skill_names: pipelineSkillNames,
|
|
164
176
|
content: workflow.content || '',
|
|
177
|
+
project_id: effectiveProjectId,
|
|
165
178
|
}),
|
|
166
179
|
})
|
|
167
180
|
|
|
@@ -231,6 +244,7 @@ async function workflowRoutes(fastify) {
|
|
|
231
244
|
name: workflow.name,
|
|
232
245
|
pipeline_skill_names: workflow.pipeline_skill_names,
|
|
233
246
|
content: workflow.content || '',
|
|
247
|
+
project_id: workflow.project_id || null,
|
|
234
248
|
})
|
|
235
249
|
|
|
236
250
|
// 4. Reload cron jobs
|