@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.
Files changed (45) hide show
  1. package/{config.js → core/config.js} +4 -4
  2. package/{lib → core/lib}/llm-checker.js +9 -16
  3. package/{lib → core/lib}/log-manager.js +3 -2
  4. package/core/lib/platform.js +117 -0
  5. package/{routes → core/routes}/health.js +1 -1
  6. package/{routes → core/routes}/routines.js +3 -3
  7. package/{routes → core/routes}/skills.js +3 -3
  8. package/{routes → core/routes}/workflows.js +4 -4
  9. package/{chat-store.js → core/stores/chat-store.js} +5 -5
  10. package/{execution-store.js → core/stores/execution-store.js} +5 -5
  11. package/{routine-store.js → core/stores/routine-store.js} +6 -6
  12. package/{workflow-store.js → core/stores/workflow-store.js} +6 -7
  13. package/{minion-cli.sh → linux/minion-cli.sh} +63 -6
  14. package/{routes → linux/routes}/chat.js +3 -3
  15. package/{routes → linux/routes}/commands.js +1 -1
  16. package/{routes → linux/routes}/config.js +3 -3
  17. package/{routes → linux/routes}/directives.js +5 -5
  18. package/{routes → linux/routes}/files.js +2 -2
  19. package/{routes → linux/routes}/terminal.js +2 -2
  20. package/{routine-runner.js → linux/routine-runner.js} +4 -4
  21. package/{server.js → linux/server.js} +71 -36
  22. package/{workflow-runner.js → linux/workflow-runner.js} +4 -4
  23. package/package.json +16 -20
  24. package/win/bin/hq-win.js +18 -0
  25. package/win/bin/hq.ps1 +108 -0
  26. package/win/bin/minion-cli-win.js +20 -0
  27. package/win/lib/process-manager.js +112 -0
  28. package/win/minion-cli.ps1 +877 -0
  29. package/win/routes/chat.js +280 -0
  30. package/win/routes/commands.js +101 -0
  31. package/win/routes/config.js +227 -0
  32. package/win/routes/directives.js +136 -0
  33. package/win/routes/files.js +283 -0
  34. package/win/routes/terminal.js +335 -0
  35. package/win/routine-runner.js +324 -0
  36. package/win/server.js +230 -0
  37. package/win/terminal-server.js +242 -0
  38. package/win/workflow-runner.js +380 -0
  39. package/routes/index.js +0 -106
  40. /package/{api.js → core/api.js} +0 -0
  41. /package/{lib → core/lib}/auth.js +0 -0
  42. /package/{routes → core/routes}/auth.js +0 -0
  43. /package/{bin → linux/bin}/hq +0 -0
  44. /package/{lib → linux/lib}/process-manager.js +0 -0
  45. /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 }