@geekbeer/minion 2.25.0 → 2.33.4
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/{config.js → core/config.js} +4 -4
- package/{lib → core/lib}/llm-checker.js +9 -16
- package/{lib → core/lib}/log-manager.js +3 -2
- package/core/lib/platform.js +117 -0
- package/{routes → core/routes}/health.js +1 -1
- package/{routes → core/routes}/routines.js +3 -3
- package/{routes → core/routes}/skills.js +3 -3
- package/{routes → core/routes}/workflows.js +4 -4
- package/{chat-store.js → core/stores/chat-store.js} +5 -5
- package/{execution-store.js → core/stores/execution-store.js} +5 -5
- package/{routine-store.js → core/stores/routine-store.js} +6 -6
- package/{workflow-store.js → core/stores/workflow-store.js} +6 -7
- package/{minion-cli.sh → linux/minion-cli.sh} +63 -6
- package/{routes → linux/routes}/chat.js +3 -3
- package/{routes → linux/routes}/commands.js +1 -1
- package/{routes → linux/routes}/config.js +3 -3
- package/{routes → linux/routes}/directives.js +5 -5
- package/{routes → linux/routes}/files.js +2 -2
- package/{routes → linux/routes}/terminal.js +2 -2
- package/{routine-runner.js → linux/routine-runner.js} +4 -4
- package/{server.js → linux/server.js} +71 -36
- package/{workflow-runner.js → linux/workflow-runner.js} +4 -4
- package/package.json +16 -20
- package/win/bin/hq-win.js +18 -0
- package/win/bin/hq.ps1 +108 -0
- package/win/bin/minion-cli-win.js +20 -0
- package/win/lib/process-manager.js +112 -0
- package/win/minion-cli.ps1 +877 -0
- package/win/routes/chat.js +280 -0
- package/win/routes/commands.js +101 -0
- package/win/routes/config.js +227 -0
- package/win/routes/directives.js +136 -0
- package/win/routes/files.js +283 -0
- package/win/routes/terminal.js +335 -0
- package/win/routine-runner.js +324 -0
- package/win/server.js +230 -0
- package/win/terminal-server.js +242 -0
- package/win/workflow-runner.js +380 -0
- package/routes/index.js +0 -106
- /package/{api.js → core/api.js} +0 -0
- /package/{lib → core/lib}/auth.js +0 -0
- /package/{routes → core/routes}/auth.js +0 -0
- /package/{bin → linux/bin}/hq +0 -0
- /package/{lib → linux/lib}/process-manager.js +0 -0
- /package/{terminal-proxy.js → linux/terminal-proxy.js} +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows Directive endpoints
|
|
3
|
+
*
|
|
4
|
+
* Same as routes/directives.js but uses win/workflow-runner.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs').promises
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const crypto = require('crypto')
|
|
10
|
+
|
|
11
|
+
const { verifyToken } = require('../../core/lib/auth')
|
|
12
|
+
const { config } = require('../../core/config')
|
|
13
|
+
const { writeSkillToLocal } = require('../../core/routes/skills')
|
|
14
|
+
const workflowRunner = require('../workflow-runner')
|
|
15
|
+
const executionStore = require('../../core/stores/execution-store')
|
|
16
|
+
const logManager = require('../../core/lib/log-manager')
|
|
17
|
+
|
|
18
|
+
function parseFrontmatter(content) {
|
|
19
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
|
|
20
|
+
if (!match) return { metadata: {}, body: content }
|
|
21
|
+
const metadata = {}
|
|
22
|
+
for (const line of match[1].split('\n')) {
|
|
23
|
+
const [key, ...rest] = line.split(':')
|
|
24
|
+
if (key && rest.length) {
|
|
25
|
+
metadata[key.trim()] = rest.join(':').trim()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { metadata, body: match[2].trimStart() }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function directiveRoutes(fastify) {
|
|
32
|
+
fastify.post('/api/directive', async (request, reply) => {
|
|
33
|
+
if (!verifyToken(request)) {
|
|
34
|
+
reply.code(401)
|
|
35
|
+
return { success: false, error: 'Unauthorized' }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { skill_name, skill_content, execution_id, context } = request.body || {}
|
|
39
|
+
|
|
40
|
+
if (!skill_name || !skill_content) {
|
|
41
|
+
reply.code(400)
|
|
42
|
+
return { success: false, error: 'skill_name and skill_content are required' }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!skill_name.startsWith('__')) {
|
|
46
|
+
reply.code(400)
|
|
47
|
+
return { success: false, error: 'Directive skill names must start with __' }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const effectiveExecutionId = execution_id || crypto.randomUUID()
|
|
51
|
+
const sessionName = `dir-${effectiveExecutionId.substring(0, 8)}-${effectiveExecutionId.substring(8, 12)}`
|
|
52
|
+
|
|
53
|
+
console.log(`[Directive] Received directive: ${skill_name} (execution: ${effectiveExecutionId})`)
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const homeDir = config.HOME_DIR
|
|
57
|
+
const skillDir = path.join(homeDir, '.claude', 'skills', skill_name)
|
|
58
|
+
await fs.mkdir(skillDir, { recursive: true })
|
|
59
|
+
await fs.writeFile(path.join(skillDir, 'SKILL.md'), skill_content, 'utf-8')
|
|
60
|
+
console.log(`[Directive] Temp skill written: ${skillDir}`)
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error(`[Directive] Failed to write temp skill: ${err.message}`)
|
|
63
|
+
reply.code(500)
|
|
64
|
+
return { success: false, error: `Failed to write temp skill: ${err.message}` }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const startedAt = new Date().toISOString()
|
|
68
|
+
const logFile = logManager.getLogPath(effectiveExecutionId)
|
|
69
|
+
const workflowName = context?.workflow_name || skill_name
|
|
70
|
+
|
|
71
|
+
await executionStore.save({
|
|
72
|
+
id: effectiveExecutionId,
|
|
73
|
+
skill_name,
|
|
74
|
+
workflow_id: null,
|
|
75
|
+
workflow_name: workflowName,
|
|
76
|
+
status: 'running',
|
|
77
|
+
outcome: null,
|
|
78
|
+
started_at: startedAt,
|
|
79
|
+
completed_at: null,
|
|
80
|
+
parent_execution_id: null,
|
|
81
|
+
error_message: null,
|
|
82
|
+
log_file: logFile,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const executionPromise = (async () => {
|
|
86
|
+
const homeDir = config.HOME_DIR
|
|
87
|
+
const skillDir = path.join(homeDir, '.claude', 'skills', skill_name)
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const result = await workflowRunner.runWorkflow({
|
|
91
|
+
id: effectiveExecutionId,
|
|
92
|
+
name: workflowName,
|
|
93
|
+
pipeline_skill_names: [skill_name],
|
|
94
|
+
}, { skipExecutionReport: true })
|
|
95
|
+
|
|
96
|
+
console.log(`[Directive] Execution completed: ${skill_name} (success: ${result.execution_id ? 'yes' : 'no'})`)
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(`[Directive] Execution failed: ${err.message}`)
|
|
99
|
+
await executionStore.save({
|
|
100
|
+
id: effectiveExecutionId,
|
|
101
|
+
skill_name,
|
|
102
|
+
workflow_id: null,
|
|
103
|
+
workflow_name: workflowName,
|
|
104
|
+
status: 'failed',
|
|
105
|
+
outcome: 'failure',
|
|
106
|
+
started_at: startedAt,
|
|
107
|
+
completed_at: new Date().toISOString(),
|
|
108
|
+
parent_execution_id: null,
|
|
109
|
+
error_message: err.message,
|
|
110
|
+
log_file: logFile,
|
|
111
|
+
})
|
|
112
|
+
} finally {
|
|
113
|
+
try {
|
|
114
|
+
await fs.rm(skillDir, { recursive: true, force: true })
|
|
115
|
+
console.log(`[Directive] Temp skill cleaned up: ${skillDir}`)
|
|
116
|
+
} catch (cleanupErr) {
|
|
117
|
+
console.error(`[Directive] Failed to cleanup temp skill: ${cleanupErr.message}`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
})()
|
|
121
|
+
|
|
122
|
+
executionPromise.catch(err => {
|
|
123
|
+
console.error(`[Directive] Unhandled error: ${err.message}`)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
reply.code(202)
|
|
127
|
+
return {
|
|
128
|
+
success: true,
|
|
129
|
+
session_name: sessionName,
|
|
130
|
+
execution_id: effectiveExecutionId,
|
|
131
|
+
message: 'Directive accepted',
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { directiveRoutes }
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows File management endpoints
|
|
3
|
+
*
|
|
4
|
+
* Same as routes/files.js but uses Node.js archiver for directory downloads
|
|
5
|
+
* instead of tar command. Single-file operations are identical.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs').promises
|
|
9
|
+
const fsSync = require('fs')
|
|
10
|
+
const path = require('path')
|
|
11
|
+
const zlib = require('zlib')
|
|
12
|
+
|
|
13
|
+
const { verifyToken } = require('../../core/lib/auth')
|
|
14
|
+
const { config } = require('../../core/config')
|
|
15
|
+
|
|
16
|
+
const FILES_DIR = path.join(config.HOME_DIR, 'files')
|
|
17
|
+
const MAX_UPLOAD_SIZE = 50 * 1024 * 1024
|
|
18
|
+
|
|
19
|
+
function safePath(basedir, userPath) {
|
|
20
|
+
const decoded = decodeURIComponent(userPath)
|
|
21
|
+
if (decoded.includes('\0')) return null
|
|
22
|
+
const resolved = path.resolve(basedir, decoded)
|
|
23
|
+
if (resolved === basedir) return resolved
|
|
24
|
+
if (!resolved.startsWith(basedir + path.sep)) return null
|
|
25
|
+
return resolved
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a gzip-compressed tar-like stream from a directory using pure Node.js.
|
|
30
|
+
* Returns a JSON manifest of files (same format as config-routes backup).
|
|
31
|
+
*/
|
|
32
|
+
function createDirArchive(dirPath) {
|
|
33
|
+
const files = []
|
|
34
|
+
const baseName = path.basename(dirPath)
|
|
35
|
+
|
|
36
|
+
function collect(dir, prefix) {
|
|
37
|
+
const entries = fsSync.readdirSync(dir, { withFileTypes: true })
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const fullPath = path.join(dir, entry.name)
|
|
40
|
+
const relativePath = path.join(prefix, entry.name)
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
collect(fullPath, relativePath)
|
|
43
|
+
} else {
|
|
44
|
+
try {
|
|
45
|
+
const content = fsSync.readFileSync(fullPath)
|
|
46
|
+
files.push({ path: relativePath, content: content.toString('base64'), encoding: 'base64' })
|
|
47
|
+
} catch { /* skip unreadable */ }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
collect(dirPath, baseName)
|
|
53
|
+
const json = JSON.stringify(files)
|
|
54
|
+
return zlib.gzipSync(Buffer.from(json))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function fileRoutes(fastify) {
|
|
58
|
+
fastify.addContentTypeParser('application/octet-stream', function (request, payload, done) {
|
|
59
|
+
const chunks = []
|
|
60
|
+
let totalSize = 0
|
|
61
|
+
payload.on('data', chunk => {
|
|
62
|
+
totalSize += chunk.length
|
|
63
|
+
if (totalSize > MAX_UPLOAD_SIZE) {
|
|
64
|
+
done(new Error('File too large (max 50MB)'))
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
chunks.push(chunk)
|
|
68
|
+
})
|
|
69
|
+
payload.on('end', () => done(null, Buffer.concat(chunks)))
|
|
70
|
+
payload.on('error', done)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// List files
|
|
74
|
+
fastify.get('/api/files', async (request, reply) => {
|
|
75
|
+
if (!verifyToken(request)) {
|
|
76
|
+
reply.code(401)
|
|
77
|
+
return { success: false, error: 'Unauthorized' }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const subPath = request.query.path || ''
|
|
81
|
+
try {
|
|
82
|
+
let targetDir = FILES_DIR
|
|
83
|
+
if (subPath) {
|
|
84
|
+
targetDir = safePath(FILES_DIR, subPath)
|
|
85
|
+
if (!targetDir) {
|
|
86
|
+
reply.code(403)
|
|
87
|
+
return { success: false, error: 'Invalid path' }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await fs.mkdir(targetDir, { recursive: true })
|
|
92
|
+
const entries = await fs.readdir(targetDir, { withFileTypes: true })
|
|
93
|
+
const files = []
|
|
94
|
+
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const entryPath = path.join(targetDir, entry.name)
|
|
97
|
+
try {
|
|
98
|
+
const stat = await fs.lstat(entryPath)
|
|
99
|
+
if (stat.isSymbolicLink()) {
|
|
100
|
+
const realPath = await fs.realpath(entryPath)
|
|
101
|
+
if (!realPath.startsWith(FILES_DIR)) continue
|
|
102
|
+
}
|
|
103
|
+
const relativePath = path.relative(FILES_DIR, entryPath)
|
|
104
|
+
files.push({
|
|
105
|
+
name: entry.name,
|
|
106
|
+
path: relativePath,
|
|
107
|
+
size: stat.size,
|
|
108
|
+
modified: stat.mtime.toISOString(),
|
|
109
|
+
is_directory: entry.isDirectory(),
|
|
110
|
+
})
|
|
111
|
+
} catch { /* skip */ }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
files.sort((a, b) => {
|
|
115
|
+
if (a.is_directory !== b.is_directory) return a.is_directory ? -1 : 1
|
|
116
|
+
return a.name.localeCompare(b.name)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
return { success: true, files, current_path: subPath || '' }
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(`[Files] List error: ${error.message}`)
|
|
122
|
+
reply.code(500)
|
|
123
|
+
return { success: false, error: error.message }
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Download file or directory
|
|
128
|
+
fastify.get('/api/files/*', async (request, reply) => {
|
|
129
|
+
if (!verifyToken(request)) {
|
|
130
|
+
reply.code(401)
|
|
131
|
+
return { success: false, error: 'Unauthorized' }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const filePath = request.params['*']
|
|
135
|
+
if (!filePath) {
|
|
136
|
+
reply.code(400)
|
|
137
|
+
return { success: false, error: 'File path is required' }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const resolved = safePath(FILES_DIR, filePath)
|
|
141
|
+
if (!resolved) {
|
|
142
|
+
reply.code(403)
|
|
143
|
+
return { success: false, error: 'Invalid path' }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const stat = await fs.lstat(resolved)
|
|
148
|
+
|
|
149
|
+
if (stat.isSymbolicLink()) {
|
|
150
|
+
const realPath = await fs.realpath(resolved)
|
|
151
|
+
if (!realPath.startsWith(FILES_DIR)) {
|
|
152
|
+
reply.code(403)
|
|
153
|
+
return { success: false, error: 'Invalid path' }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Directory download as gzip
|
|
158
|
+
if (stat.isDirectory()) {
|
|
159
|
+
const format = request.query.format
|
|
160
|
+
if (format !== 'tar.gz') {
|
|
161
|
+
reply.code(400)
|
|
162
|
+
return { success: false, error: 'Cannot download a directory. Use ?format=tar.gz' }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const dirName = path.basename(resolved)
|
|
166
|
+
const compressed = createDirArchive(resolved)
|
|
167
|
+
|
|
168
|
+
reply
|
|
169
|
+
.type('application/gzip')
|
|
170
|
+
.header('Content-Disposition', `attachment; filename="${encodeURIComponent(dirName)}.tar.gz"`)
|
|
171
|
+
|
|
172
|
+
return reply.send(compressed)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const filename = path.basename(resolved)
|
|
176
|
+
reply
|
|
177
|
+
.type('application/octet-stream')
|
|
178
|
+
.header('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`)
|
|
179
|
+
.header('Content-Length', stat.size)
|
|
180
|
+
|
|
181
|
+
return reply.send(fsSync.createReadStream(resolved))
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error.code === 'ENOENT') {
|
|
184
|
+
reply.code(404)
|
|
185
|
+
return { success: false, error: 'File not found' }
|
|
186
|
+
}
|
|
187
|
+
console.error(`[Files] Download error: ${error.message}`)
|
|
188
|
+
reply.code(500)
|
|
189
|
+
return { success: false, error: error.message }
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// Upload file
|
|
194
|
+
fastify.post('/api/files/*', { bodyLimit: MAX_UPLOAD_SIZE }, async (request, reply) => {
|
|
195
|
+
if (!verifyToken(request)) {
|
|
196
|
+
reply.code(401)
|
|
197
|
+
return { success: false, error: 'Unauthorized' }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const filePath = request.params['*']
|
|
201
|
+
if (!filePath) {
|
|
202
|
+
reply.code(400)
|
|
203
|
+
return { success: false, error: 'File path is required' }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const resolved = safePath(FILES_DIR, filePath)
|
|
207
|
+
if (!resolved) {
|
|
208
|
+
reply.code(403)
|
|
209
|
+
return { success: false, error: 'Invalid path' }
|
|
210
|
+
}
|
|
211
|
+
if (resolved === FILES_DIR) {
|
|
212
|
+
reply.code(400)
|
|
213
|
+
return { success: false, error: 'Invalid file path' }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const body = request.body
|
|
218
|
+
if (!Buffer.isBuffer(body)) {
|
|
219
|
+
reply.code(400)
|
|
220
|
+
return { success: false, error: 'Expected binary body (application/octet-stream)' }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await fs.mkdir(path.dirname(resolved), { recursive: true })
|
|
224
|
+
await fs.writeFile(resolved, body)
|
|
225
|
+
|
|
226
|
+
const stat = await fs.stat(resolved)
|
|
227
|
+
const relativePath = path.relative(FILES_DIR, resolved)
|
|
228
|
+
|
|
229
|
+
console.log(`[Files] Uploaded: ${relativePath} (${stat.size} bytes)`)
|
|
230
|
+
return {
|
|
231
|
+
success: true,
|
|
232
|
+
file: { name: path.basename(resolved), path: relativePath, size: stat.size },
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error(`[Files] Upload error: ${error.message}`)
|
|
236
|
+
reply.code(500)
|
|
237
|
+
return { success: false, error: error.message }
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// Delete file
|
|
242
|
+
fastify.delete('/api/files/*', async (request, reply) => {
|
|
243
|
+
if (!verifyToken(request)) {
|
|
244
|
+
reply.code(401)
|
|
245
|
+
return { success: false, error: 'Unauthorized' }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const filePath = request.params['*']
|
|
249
|
+
if (!filePath) {
|
|
250
|
+
reply.code(400)
|
|
251
|
+
return { success: false, error: 'File path is required' }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const resolved = safePath(FILES_DIR, filePath)
|
|
255
|
+
if (!resolved) {
|
|
256
|
+
reply.code(403)
|
|
257
|
+
return { success: false, error: 'Invalid path' }
|
|
258
|
+
}
|
|
259
|
+
if (resolved === FILES_DIR) {
|
|
260
|
+
reply.code(400)
|
|
261
|
+
return { success: false, error: 'Cannot delete root files directory' }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try { await fs.access(resolved) } catch {
|
|
265
|
+
reply.code(404)
|
|
266
|
+
return { success: false, error: 'File not found' }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const stat = await fs.lstat(resolved)
|
|
271
|
+
await fs.rm(resolved, { recursive: stat.isDirectory(), force: true })
|
|
272
|
+
const relativePath = path.relative(FILES_DIR, resolved)
|
|
273
|
+
console.log(`[Files] Deleted: ${relativePath}`)
|
|
274
|
+
return { success: true, message: `Deleted ${relativePath}` }
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.error(`[Files] Delete error: ${error.message}`)
|
|
277
|
+
reply.code(500)
|
|
278
|
+
return { success: false, error: error.message }
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = { fileRoutes }
|