@geekbeer/minion 3.16.1 → 3.22.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/core/lib/dag-step-poller.js +158 -6
- package/core/lib/llm-checker.js +4 -7
- package/core/lib/template-expander.js +21 -18
- package/core/llm-dispatch/mcp-server.js +185 -0
- package/core/llm-dispatch/session-pool.js +97 -0
- package/core/llm-plugins/claude/index.js +151 -0
- package/core/llm-plugins/claude/stream.js +166 -0
- package/core/llm-plugins/codex/index.js +161 -0
- package/core/llm-plugins/gemini/index.js +104 -0
- package/core/llm-plugins/lib/active.js +23 -0
- package/core/llm-plugins/lib/mcp-registration.js +132 -0
- package/core/llm-plugins/lib/skill-dirs.js +67 -0
- package/core/llm-plugins/lib/spawn-helper.js +88 -0
- package/core/llm-plugins/registry.js +168 -0
- package/core/llm-plugins/types.js +85 -0
- package/core/routes/llm.js +89 -0
- package/core/routes/skills.js +112 -57
- package/docs/api-reference.md +439 -0
- package/docs/task-guides.md +220 -0
- package/linux/bin/hq +168 -15
- package/linux/minion-cli.sh +14 -0
- package/linux/routes/chat.js +121 -2
- package/linux/routine-runner.js +22 -7
- package/linux/server.js +2 -0
- package/linux/workflow-runner.js +26 -8
- package/package.json +1 -1
- package/rules/core.md +13 -7
- package/win/bin/hq.ps1 +155 -27
- package/win/routes/chat.js +115 -2
- package/win/routine-runner.js +20 -6
- package/win/server.js +2 -0
- package/win/workflow-runner.js +20 -7
package/core/routes/skills.js
CHANGED
|
@@ -19,6 +19,8 @@ const api = require('../api')
|
|
|
19
19
|
const { config, isHqConfigured } = require('../config')
|
|
20
20
|
const executionStore = require('../stores/execution-store')
|
|
21
21
|
const logManager = require('../lib/log-manager')
|
|
22
|
+
const { reportDagNodeComplete } = require('../lib/dag-node-executor')
|
|
23
|
+
const { getActiveSkillDirs, getPrimarySkillDir } = require('../llm-plugins/lib/skill-dirs')
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* Parse YAML frontmatter from SKILL.md content
|
|
@@ -40,8 +42,12 @@ function parseFrontmatter(content) {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
|
-
* Write a skill to the local
|
|
44
|
-
*
|
|
45
|
+
* Write a skill to the local skill directories of all enabled LLM plugins.
|
|
46
|
+
*
|
|
47
|
+
* Multi-plugin distribution: each LLM CLI loads skills from its own dir
|
|
48
|
+
* (e.g. ~/.claude/skills/, ~/.gemini/skills/). The same skill content is
|
|
49
|
+
* written to every enabled plugin's directory so switching Primary doesn't
|
|
50
|
+
* break skill availability.
|
|
45
51
|
*
|
|
46
52
|
* @param {string} name - Skill slug (e.g. "execution-report")
|
|
47
53
|
* @param {object} opts
|
|
@@ -49,15 +55,9 @@ function parseFrontmatter(content) {
|
|
|
49
55
|
* @param {string} [opts.description] - Skill description for frontmatter
|
|
50
56
|
* @param {string} [opts.display_name] - Display name for frontmatter
|
|
51
57
|
* @param {Array<{path: string, content?: string}>} [opts.files] - Skill files from HQ storage
|
|
52
|
-
* @returns {Promise<{path: string, files_count: number}>}
|
|
58
|
+
* @returns {Promise<{path: string, paths: string[], files_count: number}>}
|
|
53
59
|
*/
|
|
54
60
|
async function writeSkillToLocal(name, { content, description, display_name, type, files = [] }) {
|
|
55
|
-
const skillDir = path.join(config.HOME_DIR, '.claude', 'skills', name)
|
|
56
|
-
const filesDir = path.join(skillDir, 'files')
|
|
57
|
-
|
|
58
|
-
await fs.mkdir(skillDir, { recursive: true })
|
|
59
|
-
await fs.mkdir(filesDir, { recursive: true })
|
|
60
|
-
|
|
61
61
|
// If content already has frontmatter, write as-is; otherwise build frontmatter
|
|
62
62
|
const hasFrontmatter = content.startsWith('---\n')
|
|
63
63
|
let fileContent
|
|
@@ -73,21 +73,30 @@ async function writeSkillToLocal(name, { content, description, display_name, typ
|
|
|
73
73
|
fileContent = `---\n${frontmatterLines}\n---\n\n${content}`
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
76
|
+
const targetRoots = getActiveSkillDirs()
|
|
77
|
+
const writtenPaths = []
|
|
78
|
+
|
|
79
|
+
for (const root of targetRoots) {
|
|
80
|
+
const skillDir = path.join(root, name)
|
|
81
|
+
const filesDir = path.join(skillDir, 'files')
|
|
82
|
+
await fs.mkdir(skillDir, { recursive: true })
|
|
83
|
+
await fs.mkdir(filesDir, { recursive: true })
|
|
84
|
+
await fs.writeFile(path.join(skillDir, 'SKILL.md'), fileContent, 'utf-8')
|
|
85
|
+
|
|
86
|
+
for (const file of files) {
|
|
87
|
+
if (file.path && file.content) {
|
|
88
|
+
const safeFilename = path.basename(file.path)
|
|
89
|
+
await fs.writeFile(path.join(filesDir, safeFilename), file.content, 'utf-8')
|
|
90
|
+
}
|
|
87
91
|
}
|
|
92
|
+
writtenPaths.push(skillDir)
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
return {
|
|
95
|
+
return {
|
|
96
|
+
path: writtenPaths[0],
|
|
97
|
+
paths: writtenPaths,
|
|
98
|
+
files_count: files.length,
|
|
99
|
+
}
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
/**
|
|
@@ -99,8 +108,10 @@ async function writeSkillToLocal(name, { content, description, display_name, typ
|
|
|
99
108
|
* @throws {Error} If skill not found locally or HQ rejects
|
|
100
109
|
*/
|
|
101
110
|
async function pushSkillToHQ(name) {
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
// Use the Primary plugin's skill directory as the canonical read source.
|
|
112
|
+
// Skills are written identically to every enabled plugin dir, so reading
|
|
113
|
+
// from any one of them yields the same content.
|
|
114
|
+
const skillDir = path.join(getPrimarySkillDir(), name)
|
|
104
115
|
const skillMdPath = path.join(skillDir, 'SKILL.md')
|
|
105
116
|
|
|
106
117
|
const rawContent = await fs.readFile(skillMdPath, 'utf-8')
|
|
@@ -158,34 +169,29 @@ async function skillRoutes(fastify, opts) {
|
|
|
158
169
|
console.log('[Skills] Listing deployed skills')
|
|
159
170
|
|
|
160
171
|
try {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const entries = await fs.readdir(skillsDir, { withFileTypes: true })
|
|
175
|
-
|
|
176
|
-
// Filter to only directories that contain SKILL.md
|
|
177
|
-
const skills = []
|
|
178
|
-
for (const entry of entries) {
|
|
179
|
-
if (entry.isDirectory()) {
|
|
172
|
+
// Union of skills across all active plugin skill dirs.
|
|
173
|
+
// Same skill name may exist in multiple dirs (multi-plugin distribution);
|
|
174
|
+
// dedupe via Set.
|
|
175
|
+
const skillsSet = new Set()
|
|
176
|
+
for (const skillsDir of getActiveSkillDirs()) {
|
|
177
|
+
try {
|
|
178
|
+
await fs.access(skillsDir)
|
|
179
|
+
} catch {
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true })
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
if (!entry.isDirectory()) continue
|
|
180
185
|
const skillMdPath = path.join(skillsDir, entry.name, 'SKILL.md')
|
|
181
186
|
try {
|
|
182
187
|
await fs.access(skillMdPath)
|
|
183
|
-
|
|
188
|
+
skillsSet.add(entry.name)
|
|
184
189
|
} catch {
|
|
185
|
-
// SKILL.md
|
|
190
|
+
// SKILL.md missing, skip
|
|
186
191
|
}
|
|
187
192
|
}
|
|
188
193
|
}
|
|
194
|
+
const skills = [...skillsSet]
|
|
189
195
|
|
|
190
196
|
console.log(`[Skills] Found ${skills.length} deployed skills: ${skills.join(', ') || '(none)'}`)
|
|
191
197
|
|
|
@@ -311,16 +317,23 @@ async function skillRoutes(fastify, opts) {
|
|
|
311
317
|
return { success: false, error: 'Unauthorized' }
|
|
312
318
|
}
|
|
313
319
|
|
|
314
|
-
const {
|
|
320
|
+
const {
|
|
321
|
+
skill_name,
|
|
322
|
+
execution_id,
|
|
323
|
+
step_index,
|
|
324
|
+
workflow_name,
|
|
325
|
+
role,
|
|
326
|
+
revision_feedback,
|
|
327
|
+
dag_node_execution_id,
|
|
328
|
+
} = request.body || {}
|
|
315
329
|
|
|
316
330
|
if (!skill_name) {
|
|
317
331
|
reply.code(400)
|
|
318
332
|
return { success: false, error: 'skill_name is required' }
|
|
319
333
|
}
|
|
320
334
|
|
|
321
|
-
// Verify skill exists locally
|
|
322
|
-
const
|
|
323
|
-
const skillMdPath = path.join(homeDir, '.claude', 'skills', skill_name, 'SKILL.md')
|
|
335
|
+
// Verify skill exists locally (any plugin's dir is sufficient)
|
|
336
|
+
const skillMdPath = path.join(getPrimarySkillDir(), skill_name, 'SKILL.md')
|
|
324
337
|
try {
|
|
325
338
|
await fs.access(skillMdPath)
|
|
326
339
|
} catch {
|
|
@@ -419,6 +432,44 @@ async function skillRoutes(fastify, opts) {
|
|
|
419
432
|
console.error(`[Skills] Failed to report step completion: ${hookErr.message}`)
|
|
420
433
|
}
|
|
421
434
|
}
|
|
435
|
+
|
|
436
|
+
// Post-execution hook: report DAG node completion to HQ
|
|
437
|
+
if (dag_node_execution_id) {
|
|
438
|
+
try {
|
|
439
|
+
let summary = null
|
|
440
|
+
let details = null
|
|
441
|
+
try {
|
|
442
|
+
const execRecord = await executionStore.getById(effectiveExecutionId)
|
|
443
|
+
if (execRecord) {
|
|
444
|
+
summary = execRecord.summary || null
|
|
445
|
+
details = execRecord.details || null
|
|
446
|
+
}
|
|
447
|
+
} catch (readErr) {
|
|
448
|
+
console.error(`[Skills] Failed to read execution report for DAG node: ${readErr.message}`)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
await reportDagNodeComplete(
|
|
452
|
+
dag_node_execution_id,
|
|
453
|
+
success ? 'completed' : 'failed',
|
|
454
|
+
summary,
|
|
455
|
+
details
|
|
456
|
+
)
|
|
457
|
+
console.log(`[Skills] DAG node completion reported: ${dag_node_execution_id} → ${success ? 'completed' : 'failed'}`)
|
|
458
|
+
} catch (hookErr) {
|
|
459
|
+
console.error(`[Skills] Failed to report DAG node completion: ${hookErr.message}`)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Clean up ephemeral transform skill directory (created by dag-step-poller)
|
|
463
|
+
if (skill_name.startsWith('_dag_transform_')) {
|
|
464
|
+
for (const root of getActiveSkillDirs()) {
|
|
465
|
+
try {
|
|
466
|
+
await fs.rm(path.join(root, skill_name), { recursive: true, force: true })
|
|
467
|
+
} catch {
|
|
468
|
+
// best-effort cleanup
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
422
473
|
}
|
|
423
474
|
})()
|
|
424
475
|
|
|
@@ -458,20 +509,24 @@ async function skillRoutes(fastify, opts) {
|
|
|
458
509
|
console.log(`[Skills] Deleting skill: ${name}`)
|
|
459
510
|
|
|
460
511
|
try {
|
|
461
|
-
|
|
462
|
-
|
|
512
|
+
// Delete from every active plugin's skill dir
|
|
513
|
+
let foundAny = false
|
|
514
|
+
for (const root of getActiveSkillDirs()) {
|
|
515
|
+
const skillDir = path.join(root, name)
|
|
516
|
+
try {
|
|
517
|
+
await fs.access(skillDir)
|
|
518
|
+
await fs.rm(skillDir, { recursive: true, force: true })
|
|
519
|
+
foundAny = true
|
|
520
|
+
} catch {
|
|
521
|
+
// not present in this dir
|
|
522
|
+
}
|
|
523
|
+
}
|
|
463
524
|
|
|
464
|
-
|
|
465
|
-
try {
|
|
466
|
-
await fs.access(skillDir)
|
|
467
|
-
} catch {
|
|
525
|
+
if (!foundAny) {
|
|
468
526
|
reply.code(404)
|
|
469
527
|
return { success: false, error: 'Skill not found' }
|
|
470
528
|
}
|
|
471
529
|
|
|
472
|
-
// Recursively delete the skill directory
|
|
473
|
-
await fs.rm(skillDir, { recursive: true, force: true })
|
|
474
|
-
|
|
475
530
|
console.log(`[Skills] Skill deleted successfully: ${name}`)
|
|
476
531
|
|
|
477
532
|
return {
|