@geekbeer/minion 3.17.0 → 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 +22 -13
- 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
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
* - Supports concurrent node execution (up to MAX_CONCURRENT)
|
|
8
8
|
* - Reports completion to /api/dag/minion/node-complete
|
|
9
9
|
* - Injects input_data into skill context
|
|
10
|
-
*
|
|
11
|
-
* Feature flag: only starts when DAG_ENGINE_ENABLED=true
|
|
12
10
|
*/
|
|
13
11
|
|
|
14
12
|
const { config, isHqConfigured } = require('../config')
|
|
@@ -259,7 +257,12 @@ async function executeTransformNode(node) {
|
|
|
259
257
|
const ephemeralName = `_dag_transform_${node_execution_id.substring(0, 8)}`
|
|
260
258
|
const fs = require('fs').promises
|
|
261
259
|
const path = require('path')
|
|
262
|
-
const
|
|
260
|
+
const { getActiveSkillDirs } = require('../llm-plugins/lib/skill-dirs')
|
|
261
|
+
const skillRoots = getActiveSkillDirs()
|
|
262
|
+
// Write the ephemeral skill to every active plugin's dir so any Primary
|
|
263
|
+
// can find it. Cleanup at the end sweeps the same set.
|
|
264
|
+
const ephemeralSkillDirs = skillRoots.map(root => path.join(root, ephemeralName))
|
|
265
|
+
const skillDir = ephemeralSkillDirs[0] // canonical for downstream paths
|
|
263
266
|
|
|
264
267
|
try {
|
|
265
268
|
// 1. Claim the node
|
|
@@ -276,10 +279,13 @@ async function executeTransformNode(node) {
|
|
|
276
279
|
throw claimErr
|
|
277
280
|
}
|
|
278
281
|
|
|
279
|
-
// 2. Create ephemeral skill from transform_instruction
|
|
282
|
+
// 2. Create ephemeral skill from transform_instruction.
|
|
283
|
+
// Write to every active plugin's skill dir so any Primary can find it.
|
|
280
284
|
const skillContent = buildTransformSkillContent(transform_instruction, input_data)
|
|
281
|
-
|
|
282
|
-
|
|
285
|
+
for (const dir of ephemeralSkillDirs) {
|
|
286
|
+
await fs.mkdir(dir, { recursive: true })
|
|
287
|
+
await fs.writeFile(path.join(dir, 'SKILL.md'), skillContent, 'utf-8')
|
|
288
|
+
}
|
|
283
289
|
|
|
284
290
|
// 3. Run the ephemeral skill
|
|
285
291
|
const runPayload = {
|
|
@@ -322,18 +328,21 @@ async function executeTransformNode(node) {
|
|
|
322
328
|
|
|
323
329
|
} catch (err) {
|
|
324
330
|
console.error(`[DagPoller] Failed to execute transform node ${node_id}: ${err.message}`)
|
|
331
|
+
// Best-effort cleanup on synchronous failure (before skill hand-off).
|
|
332
|
+
// On the success path, cleanup is performed by the post-execution hook
|
|
333
|
+
// in skills.js once the session actually finishes.
|
|
334
|
+
for (const dir of ephemeralSkillDirs) {
|
|
335
|
+
try {
|
|
336
|
+
await fs.rm(dir, { recursive: true, force: true })
|
|
337
|
+
} catch {
|
|
338
|
+
// best-effort
|
|
339
|
+
}
|
|
340
|
+
}
|
|
325
341
|
try {
|
|
326
342
|
await reportNodeComplete(node_execution_id, 'failed', null, err.message)
|
|
327
343
|
} catch {
|
|
328
344
|
// best-effort
|
|
329
345
|
}
|
|
330
|
-
} finally {
|
|
331
|
-
// 4. Clean up ephemeral skill directory
|
|
332
|
-
try {
|
|
333
|
-
await fs.rm(skillDir, { recursive: true, force: true })
|
|
334
|
-
} catch {
|
|
335
|
-
// best-effort cleanup
|
|
336
|
-
}
|
|
337
346
|
}
|
|
338
347
|
}
|
|
339
348
|
|
package/core/lib/llm-checker.js
CHANGED
|
@@ -69,8 +69,10 @@ function isClaudeAuthenticated() {
|
|
|
69
69
|
function isGeminiAuthenticated() {
|
|
70
70
|
if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) return true
|
|
71
71
|
|
|
72
|
+
// Official @google/gemini-cli stores OAuth credentials at ~/.gemini/oauth_creds.json
|
|
73
|
+
// after `gemini` login. Fall back to ADC locations for gcloud-based setups.
|
|
72
74
|
const possiblePaths = [
|
|
73
|
-
path.join(config.HOME_DIR, '.
|
|
75
|
+
path.join(config.HOME_DIR, '.gemini', 'oauth_creds.json'),
|
|
74
76
|
path.join(config.HOME_DIR, '.config', 'gcloud', 'application_default_credentials.json'),
|
|
75
77
|
// Windows-specific locations
|
|
76
78
|
...(IS_WINDOWS ? [
|
|
@@ -80,12 +82,7 @@ function isGeminiAuthenticated() {
|
|
|
80
82
|
for (const p of possiblePaths) {
|
|
81
83
|
try {
|
|
82
84
|
if (!fs.existsSync(p)) continue
|
|
83
|
-
|
|
84
|
-
if (stat.isDirectory()) {
|
|
85
|
-
if (fs.readdirSync(p).length > 0) return true
|
|
86
|
-
} else {
|
|
87
|
-
if (fs.readFileSync(p, 'utf-8').trim().length > 0) return true
|
|
88
|
-
}
|
|
85
|
+
if (fs.readFileSync(p, 'utf-8').trim().length > 0) return true
|
|
89
86
|
} catch {
|
|
90
87
|
// Ignore
|
|
91
88
|
}
|
|
@@ -44,21 +44,26 @@ async function expandSkillTemplates(skillNames) {
|
|
|
44
44
|
// If no variables defined, skip
|
|
45
45
|
if (Object.keys(minionVars).length === 0) return new Map()
|
|
46
46
|
|
|
47
|
+
// originals key: `${root}::${name}` so we restore the right copy in each
|
|
48
|
+
// plugin's skill dir (multi-plugin distribution).
|
|
47
49
|
const originals = new Map()
|
|
48
|
-
const
|
|
50
|
+
const { getActiveSkillDirs } = require('../llm-plugins/lib/skill-dirs')
|
|
51
|
+
const roots = getActiveSkillDirs()
|
|
49
52
|
|
|
50
|
-
for (const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
for (const root of roots) {
|
|
54
|
+
for (const name of skillNames) {
|
|
55
|
+
const skillMdPath = path.join(root, name, 'SKILL.md')
|
|
56
|
+
try {
|
|
57
|
+
const original = await fs.readFile(skillMdPath, 'utf-8')
|
|
58
|
+
const expanded = expandTemplateVars(original, minionVars)
|
|
59
|
+
if (expanded !== original) {
|
|
60
|
+
originals.set(`${root}::${name}`, { skillMdPath, original })
|
|
61
|
+
await fs.writeFile(skillMdPath, expanded, 'utf-8')
|
|
62
|
+
console.log(`[TemplateExpander] Expanded {{VAR}} in skill: ${name} (${root})`)
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Skill missing in this dir — skip
|
|
59
66
|
}
|
|
60
|
-
} catch (err) {
|
|
61
|
-
console.error(`[TemplateExpander] Failed to expand ${name}: ${err.message}`)
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
|
|
@@ -68,16 +73,14 @@ async function expandSkillTemplates(skillNames) {
|
|
|
68
73
|
/**
|
|
69
74
|
* Restore original SKILL.md contents after execution.
|
|
70
75
|
*
|
|
71
|
-
* @param {Map<string, string>} originals - Map from expandSkillTemplates
|
|
76
|
+
* @param {Map<string, {skillMdPath: string, original: string}>} originals - Map from expandSkillTemplates
|
|
72
77
|
*/
|
|
73
78
|
async function restoreSkillTemplates(originals) {
|
|
74
|
-
const
|
|
75
|
-
for (const [name, content] of originals) {
|
|
76
|
-
const skillMdPath = path.join(skillsDir, name, 'SKILL.md')
|
|
79
|
+
for (const { skillMdPath, original } of originals.values()) {
|
|
77
80
|
try {
|
|
78
|
-
await fs.writeFile(skillMdPath,
|
|
81
|
+
await fs.writeFile(skillMdPath, original, 'utf-8')
|
|
79
82
|
} catch (err) {
|
|
80
|
-
console.error(`[TemplateExpander] Failed to restore ${
|
|
83
|
+
console.error(`[TemplateExpander] Failed to restore ${skillMdPath}: ${err.message}`)
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Dispatch MCP server (stdio).
|
|
4
|
+
*
|
|
5
|
+
* Primary LLM clients (Claude Code, Codex, etc.) launch this as a child
|
|
6
|
+
* process via ~/.mcp.json. It exposes one MCP tool per enabled child plugin:
|
|
7
|
+
*
|
|
8
|
+
* dispatch_to_<plugin>(prompt, opts)
|
|
9
|
+
*
|
|
10
|
+
* When invoked, the server acquires a slot from the child session pool,
|
|
11
|
+
* calls plugin.invoke() (or plugin.resume() when a prior slot is reused),
|
|
12
|
+
* and returns the standardized LlmOutput as an MCP tool result.
|
|
13
|
+
*
|
|
14
|
+
* Parallel tool calls from the Primary are supported: each concurrent call
|
|
15
|
+
* gets its own slot. The server's lifetime equals the Primary's lifetime
|
|
16
|
+
* (when the Primary exits, the MCP transport closes and this process dies,
|
|
17
|
+
* disposing the pool).
|
|
18
|
+
*
|
|
19
|
+
* A parent_session_id is generated on startup (random UUID). It represents
|
|
20
|
+
* "this Primary session" and scopes the pool.
|
|
21
|
+
*
|
|
22
|
+
* Protocol: JSON-RPC 2.0 over stdio, one message per line.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const readline = require('readline')
|
|
26
|
+
const crypto = require('crypto')
|
|
27
|
+
const { loadAllPlugins, readConfig } = require('../llm-plugins/registry')
|
|
28
|
+
const { SessionPool } = require('./session-pool')
|
|
29
|
+
|
|
30
|
+
const PARENT_SESSION_ID = crypto.randomUUID()
|
|
31
|
+
const pool = new SessionPool()
|
|
32
|
+
|
|
33
|
+
const plugins = loadAllPlugins()
|
|
34
|
+
const cfg = readConfig()
|
|
35
|
+
|
|
36
|
+
/** Enabled child plugins (exclude the primary itself to avoid self-dispatch loops). */
|
|
37
|
+
function enabledChildren() {
|
|
38
|
+
const enabled = new Set(cfg.enabled)
|
|
39
|
+
const out = []
|
|
40
|
+
for (const p of plugins.values()) {
|
|
41
|
+
if (!enabled.has(p.name)) continue
|
|
42
|
+
if (p.name === cfg.primary) continue
|
|
43
|
+
out.push(p)
|
|
44
|
+
}
|
|
45
|
+
return out
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toolNameFor(pluginName) {
|
|
49
|
+
return `dispatch_to_${pluginName}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toolsList() {
|
|
53
|
+
return enabledChildren().map(p => ({
|
|
54
|
+
name: toolNameFor(p.name),
|
|
55
|
+
description: `Dispatch a prompt to ${p.displayName}. Returns standardized LlmOutput { text, files, metadata }.`,
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
prompt: { type: 'string', description: 'Prompt text to send to the child LLM.' },
|
|
60
|
+
systemPrompt: { type: 'string', description: 'Optional system prompt.' },
|
|
61
|
+
model: { type: 'string', description: 'Optional model identifier.' },
|
|
62
|
+
continue: {
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
description: 'Reuse the existing child session if one exists. Default true.',
|
|
65
|
+
default: true,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
required: ['prompt'],
|
|
69
|
+
},
|
|
70
|
+
}))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function handleToolCall(name, args) {
|
|
74
|
+
const children = enabledChildren()
|
|
75
|
+
const plugin = children.find(p => toolNameFor(p.name) === name)
|
|
76
|
+
if (!plugin) throw new Error(`Unknown tool: ${name}`)
|
|
77
|
+
|
|
78
|
+
const input = {
|
|
79
|
+
prompt: String(args.prompt || ''),
|
|
80
|
+
systemPrompt: args.systemPrompt,
|
|
81
|
+
model: args.model,
|
|
82
|
+
}
|
|
83
|
+
const continueSession = args.continue !== false
|
|
84
|
+
|
|
85
|
+
const forceFresh = !continueSession || !plugin.capabilities.sessionResume
|
|
86
|
+
const { slot } = pool.acquire(PARENT_SESSION_ID, plugin.name, { forceFresh })
|
|
87
|
+
|
|
88
|
+
let output
|
|
89
|
+
try {
|
|
90
|
+
if (slot.sessionId && continueSession && typeof plugin.resume === 'function') {
|
|
91
|
+
output = await plugin.resume(slot.sessionId, input)
|
|
92
|
+
} else {
|
|
93
|
+
output = await plugin.invoke(input)
|
|
94
|
+
}
|
|
95
|
+
} catch (err) {
|
|
96
|
+
pool.drop(PARENT_SESSION_ID, slot)
|
|
97
|
+
throw err
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
pool.release(slot, output?.metadata?.sessionId)
|
|
101
|
+
return output
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- JSON-RPC transport ------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
function send(msg) {
|
|
107
|
+
process.stdout.write(JSON.stringify(msg) + '\n')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sendResult(id, result) {
|
|
111
|
+
send({ jsonrpc: '2.0', id, result })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function sendError(id, code, message) {
|
|
115
|
+
send({ jsonrpc: '2.0', id, error: { code, message } })
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function dispatch(msg) {
|
|
119
|
+
const { id, method, params } = msg
|
|
120
|
+
try {
|
|
121
|
+
switch (method) {
|
|
122
|
+
case 'initialize':
|
|
123
|
+
sendResult(id, {
|
|
124
|
+
protocolVersion: '2024-11-05',
|
|
125
|
+
serverInfo: { name: 'minion-llm-dispatch', version: '1.0.0' },
|
|
126
|
+
capabilities: { tools: {} },
|
|
127
|
+
})
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
case 'notifications/initialized':
|
|
131
|
+
// Notification: no response
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
case 'tools/list':
|
|
135
|
+
sendResult(id, { tools: toolsList() })
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
case 'tools/call': {
|
|
139
|
+
const { name, arguments: args } = params || {}
|
|
140
|
+
const output = await handleToolCall(name, args || {})
|
|
141
|
+
sendResult(id, {
|
|
142
|
+
content: [{ type: 'text', text: JSON.stringify(output) }],
|
|
143
|
+
isError: !!output?.error,
|
|
144
|
+
})
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
case 'ping':
|
|
149
|
+
sendResult(id, {})
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
default:
|
|
153
|
+
if (id !== undefined) sendError(id, -32601, `Method not found: ${method}`)
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (id !== undefined) sendError(id, -32000, err.message || String(err))
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function start() {
|
|
162
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false })
|
|
163
|
+
rl.on('line', line => {
|
|
164
|
+
const trimmed = line.trim()
|
|
165
|
+
if (!trimmed) return
|
|
166
|
+
let msg
|
|
167
|
+
try {
|
|
168
|
+
msg = JSON.parse(trimmed)
|
|
169
|
+
} catch {
|
|
170
|
+
// Ignore malformed lines
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
dispatch(msg)
|
|
174
|
+
})
|
|
175
|
+
rl.on('close', () => {
|
|
176
|
+
pool.disposeParent(PARENT_SESSION_ID)
|
|
177
|
+
process.exit(0)
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (require.main === module) {
|
|
182
|
+
start()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { start, toolsList, handleToolCall, pool, PARENT_SESSION_ID }
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child session pool for dispatch.
|
|
3
|
+
*
|
|
4
|
+
* Keyed by (parentSessionId, pluginName). When a dispatch arrives while the
|
|
5
|
+
* existing pool entry is in-flight (parallel dispatch), a new slot is created
|
|
6
|
+
* so that the concurrent calls use independent child sessions.
|
|
7
|
+
*
|
|
8
|
+
* Parent termination disposes all slots at once.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} Slot
|
|
13
|
+
* @property {string} pluginName
|
|
14
|
+
* @property {string|null} sessionId plugin-reported sessionId (for resume)
|
|
15
|
+
* @property {boolean} inFlight
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
class SessionPool {
|
|
19
|
+
constructor() {
|
|
20
|
+
/** @type {Map<string, Slot[]>} parentSessionId -> slots */
|
|
21
|
+
this._slots = new Map()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_key(parentSessionId) {
|
|
25
|
+
return String(parentSessionId)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Acquire a slot for (parent, plugin). Reuses an idle slot if available,
|
|
30
|
+
* otherwise creates a new slot. Marks the slot in-flight.
|
|
31
|
+
* @param {string} parentSessionId
|
|
32
|
+
* @param {string} pluginName
|
|
33
|
+
* @param {{ forceFresh?: boolean }} [opts]
|
|
34
|
+
* @returns {{ slot: Slot, slotIndex: number, slots: Slot[] }}
|
|
35
|
+
*/
|
|
36
|
+
acquire(parentSessionId, pluginName, opts = {}) {
|
|
37
|
+
const key = this._key(parentSessionId)
|
|
38
|
+
let slots = this._slots.get(key)
|
|
39
|
+
if (!slots) {
|
|
40
|
+
slots = []
|
|
41
|
+
this._slots.set(key, slots)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!opts.forceFresh) {
|
|
45
|
+
const idle = slots.find(s => s.pluginName === pluginName && !s.inFlight)
|
|
46
|
+
if (idle) {
|
|
47
|
+
idle.inFlight = true
|
|
48
|
+
return { slot: idle, slotIndex: slots.indexOf(idle), slots }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const slot = { pluginName, sessionId: null, inFlight: true }
|
|
53
|
+
slots.push(slot)
|
|
54
|
+
return { slot, slotIndex: slots.length - 1, slots }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Release a slot after a dispatch completes. Stores sessionId (if returned
|
|
59
|
+
* by the plugin) for subsequent reuse.
|
|
60
|
+
*/
|
|
61
|
+
release(slot, sessionId) {
|
|
62
|
+
slot.inFlight = false
|
|
63
|
+
if (sessionId) slot.sessionId = sessionId
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Drop a slot entirely (e.g. plugin reported hard failure or caller asked
|
|
68
|
+
* to reset the session).
|
|
69
|
+
*/
|
|
70
|
+
drop(parentSessionId, slot) {
|
|
71
|
+
const key = this._key(parentSessionId)
|
|
72
|
+
const slots = this._slots.get(key)
|
|
73
|
+
if (!slots) return
|
|
74
|
+
const idx = slots.indexOf(slot)
|
|
75
|
+
if (idx >= 0) slots.splice(idx, 1)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Dispose all slots for a parent session (call on parent termination). */
|
|
79
|
+
disposeParent(parentSessionId) {
|
|
80
|
+
this._slots.delete(this._key(parentSessionId))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Inspection helper (for diagnostics). */
|
|
84
|
+
snapshot() {
|
|
85
|
+
const out = {}
|
|
86
|
+
for (const [parent, slots] of this._slots.entries()) {
|
|
87
|
+
out[parent] = slots.map(s => ({
|
|
88
|
+
pluginName: s.pluginName,
|
|
89
|
+
sessionId: s.sessionId,
|
|
90
|
+
inFlight: s.inFlight,
|
|
91
|
+
}))
|
|
92
|
+
}
|
|
93
|
+
return out
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { SessionPool }
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builtin plugin: Claude Code CLI
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
const { execSync } = require('child_process')
|
|
8
|
+
const { config } = require('../../config')
|
|
9
|
+
const { IS_WINDOWS, buildExtendedPath } = require('../../lib/platform')
|
|
10
|
+
const { resolveBinary, spawnWithStdin } = require('../lib/spawn-helper')
|
|
11
|
+
const { streamClaude } = require('./stream')
|
|
12
|
+
|
|
13
|
+
const BINARY = 'claude'
|
|
14
|
+
|
|
15
|
+
async function isAuthenticated() {
|
|
16
|
+
const candidates = [
|
|
17
|
+
path.join(config.HOME_DIR, '.claude', '.credentials.json'),
|
|
18
|
+
path.join(config.HOME_DIR, '.claude', 'credentials.json'),
|
|
19
|
+
]
|
|
20
|
+
for (const p of candidates) {
|
|
21
|
+
try {
|
|
22
|
+
if (fs.existsSync(p)) {
|
|
23
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf-8'))
|
|
24
|
+
if (parsed && Object.keys(parsed).length > 0) return true
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// continue
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const bin = resolveBinary(BINARY, config.HOME_DIR)
|
|
32
|
+
execSync(`${bin} auth whoami`, {
|
|
33
|
+
encoding: 'utf-8',
|
|
34
|
+
timeout: 5000,
|
|
35
|
+
stdio: 'pipe',
|
|
36
|
+
env: {
|
|
37
|
+
...process.env,
|
|
38
|
+
HOME: config.HOME_DIR,
|
|
39
|
+
...(IS_WINDOWS && { USERPROFILE: config.HOME_DIR }),
|
|
40
|
+
PATH: buildExtendedPath(config.HOME_DIR),
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
return true
|
|
44
|
+
} catch {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildArgs(input, { resume } = {}) {
|
|
50
|
+
const args = ['-p']
|
|
51
|
+
if (input.model) args.push('--model', input.model)
|
|
52
|
+
if (resume) args.push('--resume', resume)
|
|
53
|
+
return args
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseFinalSessionId(stdout) {
|
|
57
|
+
// Claude prints session info on --verbose; in -p mode we don't force verbose.
|
|
58
|
+
// Leave sessionId undefined unless we parse it from known markers in future.
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function invoke(input) {
|
|
63
|
+
const bin = resolveBinary(BINARY, config.HOME_DIR)
|
|
64
|
+
const args = buildArgs(input)
|
|
65
|
+
const { stdout, stderr, code, durationMs } = await spawnWithStdin({
|
|
66
|
+
binary: bin,
|
|
67
|
+
args,
|
|
68
|
+
prompt: input.prompt,
|
|
69
|
+
homeDir: config.HOME_DIR,
|
|
70
|
+
timeoutMs: input.timeoutMs,
|
|
71
|
+
})
|
|
72
|
+
if (code !== 0) {
|
|
73
|
+
return {
|
|
74
|
+
text: stdout,
|
|
75
|
+
files: [],
|
|
76
|
+
metadata: { durationMs },
|
|
77
|
+
error: { code: `exit_${code}`, message: stderr.slice(0, 2000) || 'claude exited non-zero' },
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
text: stdout.trim(),
|
|
82
|
+
files: [],
|
|
83
|
+
metadata: { durationMs, sessionId: parseFinalSessionId(stdout) },
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function resume(sessionId, input) {
|
|
88
|
+
const bin = resolveBinary(BINARY, config.HOME_DIR)
|
|
89
|
+
const args = buildArgs(input, { resume: sessionId })
|
|
90
|
+
const { stdout, stderr, code, durationMs } = await spawnWithStdin({
|
|
91
|
+
binary: bin,
|
|
92
|
+
args,
|
|
93
|
+
prompt: input.prompt,
|
|
94
|
+
homeDir: config.HOME_DIR,
|
|
95
|
+
timeoutMs: input.timeoutMs,
|
|
96
|
+
})
|
|
97
|
+
if (code !== 0) {
|
|
98
|
+
return {
|
|
99
|
+
text: stdout,
|
|
100
|
+
files: [],
|
|
101
|
+
metadata: { durationMs, sessionId },
|
|
102
|
+
error: { code: `exit_${code}`, message: stderr.slice(0, 2000) || 'claude exited non-zero' },
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
text: stdout.trim(),
|
|
107
|
+
files: [],
|
|
108
|
+
metadata: { durationMs, sessionId },
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function stream(input, onEvent, ctx) {
|
|
113
|
+
return streamClaude(input, onEvent, ctx)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildShellInvocation({ promptFile, model }) {
|
|
117
|
+
const bin = resolveBinary(BINARY, config.HOME_DIR)
|
|
118
|
+
const modelArg = model ? ` --model ${model}` : ''
|
|
119
|
+
return `${bin} -p${modelArg} < ${promptFile}`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getSkillsDir() {
|
|
123
|
+
return path.join(config.HOME_DIR, '.claude', 'skills')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatSkillInvocation(skillName, args = []) {
|
|
127
|
+
const argStr = args.length ? ' ' + args.join(' ') : ''
|
|
128
|
+
return `/${skillName}${argStr}`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @type {import('../types').LlmPlugin} */
|
|
132
|
+
const plugin = {
|
|
133
|
+
name: 'claude',
|
|
134
|
+
displayName: 'Claude Code',
|
|
135
|
+
capabilities: {
|
|
136
|
+
toolUse: true,
|
|
137
|
+
streaming: true,
|
|
138
|
+
vision: true,
|
|
139
|
+
imageGeneration: false,
|
|
140
|
+
sessionResume: true,
|
|
141
|
+
},
|
|
142
|
+
isAuthenticated,
|
|
143
|
+
invoke,
|
|
144
|
+
stream,
|
|
145
|
+
resume,
|
|
146
|
+
buildShellInvocation,
|
|
147
|
+
getSkillsDir,
|
|
148
|
+
formatSkillInvocation,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { plugin }
|