@geekbeer/minion 1.6.1 → 2.4.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.
- package/api.js +3 -1
- package/lib/process-manager.js +3 -3
- package/minion-cli.sh +55 -0
- package/package.json +3 -1
- package/routes/files.js +288 -0
- package/routes/index.js +8 -0
- package/routes/skills.js +191 -49
- package/routes/terminal.js +12 -0
- package/routes/workflows.js +192 -11
- package/rules/minion.md +195 -0
- package/server.js +91 -0
- package/settings/permissions.json +17 -0
- package/settings/tmux.conf +16 -0
- package/workflow-store.js +45 -1
package/routes/skills.js
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* Skill management endpoints
|
|
3
3
|
*
|
|
4
4
|
* Endpoints:
|
|
5
|
-
* - GET /api/list-skills
|
|
6
|
-
* -
|
|
5
|
+
* - GET /api/list-skills - List locally deployed skills
|
|
6
|
+
* - DELETE /api/skills/:name - Delete a local skill
|
|
7
|
+
* - POST /api/skills/push/:name - Push local skill to HQ
|
|
8
|
+
* - POST /api/skills/fetch/:name - Fetch skill from HQ and deploy locally
|
|
9
|
+
* - GET /api/skills/remote - List skills on HQ
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
const fs = require('fs').promises
|
|
@@ -11,6 +14,111 @@ const path = require('path')
|
|
|
11
14
|
const os = require('os')
|
|
12
15
|
|
|
13
16
|
const { verifyToken } = require('../lib/auth')
|
|
17
|
+
const api = require('../api')
|
|
18
|
+
const { isHqConfigured } = require('../config')
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse YAML frontmatter from SKILL.md content
|
|
22
|
+
* @param {string} content - Raw file content
|
|
23
|
+
* @returns {{ metadata: object, body: string }}
|
|
24
|
+
*/
|
|
25
|
+
function parseFrontmatter(content) {
|
|
26
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
|
|
27
|
+
if (!match) return { metadata: {}, body: content }
|
|
28
|
+
|
|
29
|
+
const metadata = {}
|
|
30
|
+
for (const line of match[1].split('\n')) {
|
|
31
|
+
const [key, ...rest] = line.split(':')
|
|
32
|
+
if (key && rest.length) {
|
|
33
|
+
metadata[key.trim()] = rest.join(':').trim()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { metadata, body: match[2].trimStart() }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Write a skill to the local ~/.claude/skills/ directory.
|
|
41
|
+
* Shared by deploy-skill and skills/fetch endpoints.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} name - Skill slug (e.g. "execution-report")
|
|
44
|
+
* @param {object} opts
|
|
45
|
+
* @param {string} opts.content - Skill body (markdown without frontmatter)
|
|
46
|
+
* @param {string} [opts.description] - Skill description for frontmatter
|
|
47
|
+
* @param {string} [opts.display_name] - Display name for frontmatter
|
|
48
|
+
* @param {Array<{filename: string, content: string}>} [opts.references] - Reference files
|
|
49
|
+
* @returns {Promise<{path: string, references_count: number}>}
|
|
50
|
+
*/
|
|
51
|
+
async function writeSkillToLocal(name, { content, description, display_name, references = [] }) {
|
|
52
|
+
const skillDir = path.join(os.homedir(), '.claude', 'skills', name)
|
|
53
|
+
const referencesDir = path.join(skillDir, 'references')
|
|
54
|
+
|
|
55
|
+
await fs.mkdir(skillDir, { recursive: true })
|
|
56
|
+
await fs.mkdir(referencesDir, { recursive: true })
|
|
57
|
+
|
|
58
|
+
// Build frontmatter with all available metadata
|
|
59
|
+
const frontmatterLines = [
|
|
60
|
+
`name: ${name}`,
|
|
61
|
+
display_name ? `display_name: ${display_name}` : null,
|
|
62
|
+
`description: ${description || ''}`,
|
|
63
|
+
].filter(Boolean).join('\n')
|
|
64
|
+
|
|
65
|
+
await fs.writeFile(
|
|
66
|
+
path.join(skillDir, 'SKILL.md'),
|
|
67
|
+
`---\n${frontmatterLines}\n---\n\n${content}`,
|
|
68
|
+
'utf-8'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
// Write reference files
|
|
72
|
+
for (const ref of references) {
|
|
73
|
+
if (ref.filename && ref.content) {
|
|
74
|
+
const safeFilename = path.basename(ref.filename)
|
|
75
|
+
await fs.writeFile(path.join(referencesDir, safeFilename), ref.content, 'utf-8')
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { path: skillDir, references_count: references.length }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read a local skill and push it to HQ.
|
|
84
|
+
* Reusable by workflow push to auto-sync pipeline skills.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} name - Skill slug (e.g. "execution-report")
|
|
87
|
+
* @returns {Promise<object>} HQ response
|
|
88
|
+
* @throws {Error} If skill not found locally or HQ rejects
|
|
89
|
+
*/
|
|
90
|
+
async function pushSkillToHQ(name) {
|
|
91
|
+
const homeDir = os.homedir()
|
|
92
|
+
const skillDir = path.join(homeDir, '.claude', 'skills', name)
|
|
93
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md')
|
|
94
|
+
|
|
95
|
+
const rawContent = await fs.readFile(skillMdPath, 'utf-8')
|
|
96
|
+
const { metadata, body } = parseFrontmatter(rawContent)
|
|
97
|
+
|
|
98
|
+
// Read references
|
|
99
|
+
const referencesDir = path.join(skillDir, 'references')
|
|
100
|
+
const references = []
|
|
101
|
+
try {
|
|
102
|
+
const refEntries = await fs.readdir(referencesDir)
|
|
103
|
+
for (const filename of refEntries) {
|
|
104
|
+
const refContent = await fs.readFile(path.join(referencesDir, filename), 'utf-8')
|
|
105
|
+
references.push({ filename, content: refContent })
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// No references directory
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return api.request('/skills', {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
name: metadata.name || name,
|
|
115
|
+
display_name: metadata.display_name || metadata.name || name,
|
|
116
|
+
description: metadata.description || '',
|
|
117
|
+
content: body,
|
|
118
|
+
references,
|
|
119
|
+
}),
|
|
120
|
+
})
|
|
121
|
+
}
|
|
14
122
|
|
|
15
123
|
/**
|
|
16
124
|
* Register skill routes as Fastify plugin
|
|
@@ -66,71 +174,105 @@ async function skillRoutes(fastify) {
|
|
|
66
174
|
}
|
|
67
175
|
})
|
|
68
176
|
|
|
69
|
-
//
|
|
70
|
-
fastify.post('/api/
|
|
177
|
+
// Push local skill to HQ
|
|
178
|
+
fastify.post('/api/skills/push/:name', async (request, reply) => {
|
|
71
179
|
if (!verifyToken(request)) {
|
|
72
180
|
reply.code(401)
|
|
73
181
|
return { success: false, error: 'Unauthorized' }
|
|
74
182
|
}
|
|
75
183
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!name || !content) {
|
|
184
|
+
if (!isHqConfigured()) {
|
|
79
185
|
reply.code(400)
|
|
80
|
-
return { success: false, error: '
|
|
186
|
+
return { success: false, error: 'HQ not configured' }
|
|
81
187
|
}
|
|
82
188
|
|
|
83
|
-
|
|
84
|
-
|
|
189
|
+
const { name } = request.params
|
|
190
|
+
|
|
191
|
+
if (!name || !/^[a-z0-9-]+$/.test(name)) {
|
|
85
192
|
reply.code(400)
|
|
86
|
-
return { success: false, error: '
|
|
193
|
+
return { success: false, error: 'Invalid skill name' }
|
|
87
194
|
}
|
|
88
195
|
|
|
89
|
-
console.log(`[
|
|
196
|
+
console.log(`[Skills] Pushing skill to HQ: ${name}`)
|
|
90
197
|
|
|
91
198
|
try {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// Write SKILL.md with frontmatter
|
|
102
|
-
const skillContent = `---
|
|
103
|
-
name: ${name}
|
|
104
|
-
description: Deployed from HQ
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
${content}`
|
|
108
|
-
await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillContent, 'utf-8')
|
|
109
|
-
|
|
110
|
-
// Write reference files
|
|
111
|
-
for (const ref of references) {
|
|
112
|
-
if (ref.filename && ref.content) {
|
|
113
|
-
// Sanitize filename to prevent directory traversal
|
|
114
|
-
const safeFilename = path.basename(ref.filename)
|
|
115
|
-
await fs.writeFile(path.join(referencesDir, safeFilename), ref.content, 'utf-8')
|
|
116
|
-
}
|
|
117
|
-
}
|
|
199
|
+
const result = await pushSkillToHQ(name)
|
|
200
|
+
console.log(`[Skills] Skill pushed to HQ: ${name}`)
|
|
201
|
+
return { success: true, ...result }
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error(`[Skills] Failed to push skill: ${error.message}`)
|
|
204
|
+
reply.code(error.statusCode || 500)
|
|
205
|
+
return { success: false, error: error.message }
|
|
206
|
+
}
|
|
207
|
+
})
|
|
118
208
|
|
|
119
|
-
|
|
209
|
+
// Fetch skill from HQ and deploy locally
|
|
210
|
+
fastify.post('/api/skills/fetch/:name', async (request, reply) => {
|
|
211
|
+
if (!verifyToken(request)) {
|
|
212
|
+
reply.code(401)
|
|
213
|
+
return { success: false, error: 'Unauthorized' }
|
|
214
|
+
}
|
|
120
215
|
|
|
216
|
+
if (!isHqConfigured()) {
|
|
217
|
+
reply.code(400)
|
|
218
|
+
return { success: false, error: 'HQ not configured' }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const { name } = request.params
|
|
222
|
+
|
|
223
|
+
if (!name || !/^[a-z0-9-]+$/.test(name)) {
|
|
224
|
+
reply.code(400)
|
|
225
|
+
return { success: false, error: 'Invalid skill name' }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log(`[Skills] Fetching skill from HQ: ${name}`)
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
// Fetch from HQ
|
|
232
|
+
const skill = await api.request(`/skills/${encodeURIComponent(name)}`)
|
|
233
|
+
|
|
234
|
+
// Write to local filesystem using shared helper
|
|
235
|
+
const result = await writeSkillToLocal(name, {
|
|
236
|
+
content: skill.content,
|
|
237
|
+
description: skill.description,
|
|
238
|
+
display_name: skill.display_name,
|
|
239
|
+
references: skill.references || [],
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
console.log(`[Skills] Skill fetched and deployed: ${name}`)
|
|
121
243
|
return {
|
|
122
244
|
success: true,
|
|
123
|
-
message: `Skill "${name}"
|
|
124
|
-
|
|
125
|
-
references_count: references.length,
|
|
245
|
+
message: `Skill "${name}" fetched from HQ`,
|
|
246
|
+
...result,
|
|
126
247
|
}
|
|
127
248
|
} catch (error) {
|
|
128
|
-
console.error(`[
|
|
129
|
-
reply.code(500)
|
|
130
|
-
return {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
249
|
+
console.error(`[Skills] Failed to fetch skill: ${error.message}`)
|
|
250
|
+
reply.code(error.statusCode || 500)
|
|
251
|
+
return { success: false, error: error.message }
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
// List skills on HQ (remote)
|
|
256
|
+
fastify.get('/api/skills/remote', async (request, reply) => {
|
|
257
|
+
if (!verifyToken(request)) {
|
|
258
|
+
reply.code(401)
|
|
259
|
+
return { success: false, error: 'Unauthorized' }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!isHqConfigured()) {
|
|
263
|
+
reply.code(400)
|
|
264
|
+
return { success: false, error: 'HQ not configured' }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
console.log('[Skills] Listing remote skills from HQ')
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const result = await api.request('/skills')
|
|
271
|
+
return result
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.error(`[Skills] Failed to list remote skills: ${error.message}`)
|
|
274
|
+
reply.code(error.statusCode || 500)
|
|
275
|
+
return { success: false, error: error.message }
|
|
134
276
|
}
|
|
135
277
|
})
|
|
136
278
|
|
|
@@ -185,4 +327,4 @@ ${content}`
|
|
|
185
327
|
})
|
|
186
328
|
}
|
|
187
329
|
|
|
188
|
-
module.exports = { skillRoutes }
|
|
330
|
+
module.exports = { skillRoutes, writeSkillToLocal, pushSkillToHQ }
|
package/routes/terminal.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
const { exec, spawn } = require('child_process')
|
|
18
18
|
const { promisify } = require('util')
|
|
19
|
+
const path = require('path')
|
|
19
20
|
const net = require('net')
|
|
20
21
|
const os = require('os')
|
|
21
22
|
const execAsync = promisify(exec)
|
|
@@ -122,6 +123,17 @@ async function startTtydForSession(session) {
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
// Reload tmux.conf to ensure mouse/scroll settings are active
|
|
127
|
+
const tmuxConf = path.join(homeDir, '.tmux.conf')
|
|
128
|
+
try {
|
|
129
|
+
await execAsync(`tmux source-file "${tmuxConf}"`, {
|
|
130
|
+
timeout: 5000,
|
|
131
|
+
env: { ...process.env, HOME: homeDir },
|
|
132
|
+
})
|
|
133
|
+
} catch {
|
|
134
|
+
// Ignore: .tmux.conf may not exist yet on first run
|
|
135
|
+
}
|
|
136
|
+
|
|
125
137
|
const port = await findAvailablePort()
|
|
126
138
|
|
|
127
139
|
const proc = spawn('ttyd', [
|
package/routes/workflows.js
CHANGED
|
@@ -2,22 +2,32 @@
|
|
|
2
2
|
* Workflow management endpoints
|
|
3
3
|
*
|
|
4
4
|
* Endpoints:
|
|
5
|
-
* - GET /api/workflows
|
|
6
|
-
* - POST /api/workflows
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
5
|
+
* - GET /api/workflows - List all workflows with status
|
|
6
|
+
* - POST /api/workflows - Receive workflows from HQ (upsert/additive)
|
|
7
|
+
* - POST /api/workflows/push/:name - Push local workflow to HQ
|
|
8
|
+
* - POST /api/workflows/fetch/:name - Fetch workflow from HQ and deploy locally
|
|
9
|
+
* - GET /api/workflows/remote - List workflows on HQ
|
|
10
|
+
* - PUT /api/workflows/:id/schedule - Update a workflow schedule (cron_expression, is_active)
|
|
11
|
+
* - DELETE /api/workflows/:id - Remove a workflow
|
|
12
|
+
* - POST /api/workflows/trigger - Manual trigger for a workflow
|
|
13
|
+
* - GET /api/executions - List execution history
|
|
14
|
+
* - GET /api/executions/:id - Get single execution details
|
|
15
|
+
* - GET /api/executions/:id/log - Get execution log file content
|
|
13
16
|
* - POST /api/executions/:id/outcome - Update execution outcome (no auth, local)
|
|
14
17
|
*/
|
|
15
18
|
|
|
19
|
+
const fs = require('fs').promises
|
|
20
|
+
const path = require('path')
|
|
21
|
+
const os = require('os')
|
|
22
|
+
|
|
16
23
|
const { verifyToken } = require('../lib/auth')
|
|
17
24
|
const workflowRunner = require('../workflow-runner')
|
|
18
25
|
const workflowStore = require('../workflow-store')
|
|
19
26
|
const executionStore = require('../execution-store')
|
|
20
27
|
const logManager = require('../lib/log-manager')
|
|
28
|
+
const api = require('../api')
|
|
29
|
+
const { isHqConfigured } = require('../config')
|
|
30
|
+
const { writeSkillToLocal, pushSkillToHQ } = require('./skills')
|
|
21
31
|
|
|
22
32
|
/**
|
|
23
33
|
* Register workflow routes as Fastify plugin
|
|
@@ -93,8 +103,179 @@ async function workflowRoutes(fastify) {
|
|
|
93
103
|
return { workflows: result }
|
|
94
104
|
})
|
|
95
105
|
|
|
96
|
-
//
|
|
97
|
-
fastify.
|
|
106
|
+
// Push local workflow to HQ
|
|
107
|
+
fastify.post('/api/workflows/push/:name', async (request, reply) => {
|
|
108
|
+
if (!verifyToken(request)) {
|
|
109
|
+
reply.code(401)
|
|
110
|
+
return { success: false, error: 'Unauthorized' }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!isHqConfigured()) {
|
|
114
|
+
reply.code(400)
|
|
115
|
+
return { success: false, error: 'HQ not configured' }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { name } = request.params
|
|
119
|
+
|
|
120
|
+
if (!name || !/^[a-z0-9-]+$/.test(name)) {
|
|
121
|
+
reply.code(400)
|
|
122
|
+
return { success: false, error: 'Invalid workflow name' }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(`[Workflows] Pushing workflow to HQ: ${name}`)
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const workflow = await workflowStore.findByName(name)
|
|
129
|
+
if (!workflow) {
|
|
130
|
+
reply.code(404)
|
|
131
|
+
return { success: false, error: `Local workflow not found: ${name}` }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const pipelineSkillNames = workflow.pipeline_skill_names || []
|
|
135
|
+
|
|
136
|
+
// Auto-push pipeline skills to HQ (ensures they exist before workflow push)
|
|
137
|
+
const skillsPushed = []
|
|
138
|
+
const skillsFailed = []
|
|
139
|
+
for (const skillName of pipelineSkillNames) {
|
|
140
|
+
try {
|
|
141
|
+
await pushSkillToHQ(skillName)
|
|
142
|
+
skillsPushed.push(skillName)
|
|
143
|
+
console.log(`[Workflows] Auto-pushed pipeline skill: ${skillName}`)
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.log(`[Workflows] Skill "${skillName}" not auto-pushed: ${err.message}`)
|
|
146
|
+
skillsFailed.push(skillName)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (skillsFailed.length > 0) {
|
|
151
|
+
reply.code(400)
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
error: `Pipeline skills not found locally: ${skillsFailed.join(', ')}. Deploy them first with: minion-cli skill fetch <name>`,
|
|
155
|
+
skills_pushed: skillsPushed,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const result = await api.request('/workflows', {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
name: workflow.name,
|
|
163
|
+
pipeline_skill_names: pipelineSkillNames,
|
|
164
|
+
content: workflow.content || '',
|
|
165
|
+
}),
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
console.log(`[Workflows] Workflow pushed to HQ: ${name}`)
|
|
169
|
+
return { success: true, skills_pushed: skillsPushed, ...result }
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error(`[Workflows] Failed to push workflow: ${error.message}`)
|
|
172
|
+
reply.code(error.statusCode || 500)
|
|
173
|
+
return { success: false, error: error.message }
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Fetch workflow from HQ and deploy locally
|
|
178
|
+
fastify.post('/api/workflows/fetch/:name', async (request, reply) => {
|
|
179
|
+
if (!verifyToken(request)) {
|
|
180
|
+
reply.code(401)
|
|
181
|
+
return { success: false, error: 'Unauthorized' }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!isHqConfigured()) {
|
|
185
|
+
reply.code(400)
|
|
186
|
+
return { success: false, error: 'HQ not configured' }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { name } = request.params
|
|
190
|
+
|
|
191
|
+
if (!name || !/^[a-z0-9-]+$/.test(name)) {
|
|
192
|
+
reply.code(400)
|
|
193
|
+
return { success: false, error: 'Invalid workflow name' }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(`[Workflows] Fetching workflow from HQ: ${name}`)
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
// 1. Fetch workflow definition from HQ
|
|
200
|
+
const workflow = await api.request(`/workflows/${encodeURIComponent(name)}`)
|
|
201
|
+
|
|
202
|
+
// 2. Fetch pipeline skills that are not deployed locally
|
|
203
|
+
const fetchedSkills = []
|
|
204
|
+
const homeDir = os.homedir()
|
|
205
|
+
|
|
206
|
+
for (const skillName of workflow.pipeline_skill_names || []) {
|
|
207
|
+
const skillMdPath = path.join(homeDir, '.claude', 'skills', skillName, 'SKILL.md')
|
|
208
|
+
let exists = false
|
|
209
|
+
try {
|
|
210
|
+
await fs.access(skillMdPath)
|
|
211
|
+
exists = true
|
|
212
|
+
} catch {
|
|
213
|
+
// Skill not deployed locally
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!exists) {
|
|
217
|
+
console.log(`[Workflows] Fetching pipeline skill: ${skillName}`)
|
|
218
|
+
const skill = await api.request(`/skills/${encodeURIComponent(skillName)}`)
|
|
219
|
+
await writeSkillToLocal(skillName, {
|
|
220
|
+
content: skill.content,
|
|
221
|
+
description: skill.description,
|
|
222
|
+
display_name: skill.display_name,
|
|
223
|
+
references: skill.references || [],
|
|
224
|
+
})
|
|
225
|
+
fetchedSkills.push(skillName)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 3. Save workflow to local store (upsert by name)
|
|
230
|
+
const updatedWorkflows = await workflowStore.upsertByName({
|
|
231
|
+
name: workflow.name,
|
|
232
|
+
pipeline_skill_names: workflow.pipeline_skill_names,
|
|
233
|
+
content: workflow.content || '',
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// 4. Reload cron jobs
|
|
237
|
+
workflowRunner.loadWorkflows(updatedWorkflows)
|
|
238
|
+
|
|
239
|
+
console.log(`[Workflows] Workflow fetched: ${name} (${fetchedSkills.length} skills fetched)`)
|
|
240
|
+
return {
|
|
241
|
+
success: true,
|
|
242
|
+
message: `Workflow "${name}" fetched from HQ`,
|
|
243
|
+
skills_fetched: fetchedSkills,
|
|
244
|
+
pipeline: workflow.pipeline_skill_names,
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error(`[Workflows] Failed to fetch workflow: ${error.message}`)
|
|
248
|
+
reply.code(error.statusCode || 500)
|
|
249
|
+
return { success: false, error: error.message }
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// List workflows on HQ (remote)
|
|
254
|
+
fastify.get('/api/workflows/remote', async (request, reply) => {
|
|
255
|
+
if (!verifyToken(request)) {
|
|
256
|
+
reply.code(401)
|
|
257
|
+
return { success: false, error: 'Unauthorized' }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!isHqConfigured()) {
|
|
261
|
+
reply.code(400)
|
|
262
|
+
return { success: false, error: 'HQ not configured' }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log('[Workflows] Listing remote workflows from HQ')
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const result = await api.request('/workflows')
|
|
269
|
+
return result
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error(`[Workflows] Failed to list remote workflows: ${error.message}`)
|
|
272
|
+
reply.code(error.statusCode || 500)
|
|
273
|
+
return { success: false, error: error.message }
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
// Update a workflow schedule (cron_expression, is_active)
|
|
278
|
+
fastify.put('/api/workflows/:id/schedule', async (request, reply) => {
|
|
98
279
|
if (!verifyToken(request)) {
|
|
99
280
|
reply.code(401)
|
|
100
281
|
return { success: false, error: 'Unauthorized' }
|
|
@@ -103,7 +284,7 @@ async function workflowRoutes(fastify) {
|
|
|
103
284
|
const { id } = request.params
|
|
104
285
|
const updates = request.body || {}
|
|
105
286
|
|
|
106
|
-
console.log(`[Workflows] Updating workflow: ${id}`)
|
|
287
|
+
console.log(`[Workflows] Updating workflow schedule: ${id}`)
|
|
107
288
|
|
|
108
289
|
try {
|
|
109
290
|
const workflows = await workflowStore.load()
|