@geekbeer/minion 3.17.0 → 3.23.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/db.js CHANGED
@@ -259,12 +259,15 @@ function initSchema(db) {
259
259
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
260
260
  updated_at TEXT NOT NULL DEFAULT (datetime('now')),
261
261
  completed_at TEXT,
262
- data TEXT
262
+ data TEXT,
263
+ session_id TEXT,
264
+ injection_count INTEGER NOT NULL DEFAULT 0
263
265
  );
264
266
 
265
267
  CREATE INDEX IF NOT EXISTS idx_todos_status ON todos(status);
266
268
  CREATE INDEX IF NOT EXISTS idx_todos_project ON todos(project_id);
267
269
  CREATE INDEX IF NOT EXISTS idx_todos_priority ON todos(priority);
270
+ CREATE INDEX IF NOT EXISTS idx_todos_session ON todos(session_id);
268
271
 
269
272
  -- ==================== emails ====================
270
273
  CREATE TABLE IF NOT EXISTS emails (
@@ -473,6 +476,26 @@ function migrateSchema(db) {
473
476
  try { db.exec("INSERT OR IGNORE INTO schema_version (version) VALUES (4)") } catch {}
474
477
  }
475
478
  }
479
+
480
+ if (currentVersion < 5) {
481
+ try {
482
+ console.log('[DB] Migration 5: Adding session_id / injection_count to todos...')
483
+
484
+ db.exec(`
485
+ ALTER TABLE todos ADD COLUMN session_id TEXT;
486
+ ALTER TABLE todos ADD COLUMN injection_count INTEGER NOT NULL DEFAULT 0;
487
+
488
+ CREATE INDEX IF NOT EXISTS idx_todos_session ON todos(session_id);
489
+
490
+ INSERT INTO schema_version (version) VALUES (5);
491
+ `)
492
+
493
+ console.log('[DB] Migration 5 complete: todos.session_id / injection_count added')
494
+ } catch (err) {
495
+ console.warn(`[DB] Migration 5 skipped: ${err.message}`)
496
+ try { db.exec("INSERT OR IGNORE INTO schema_version (version) VALUES (5)") } catch {}
497
+ }
498
+ }
476
499
  }
477
500
 
478
501
  /**
@@ -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 skillDir = path.join(config.HOME_DIR || process.env.HOME, '.claude', 'skills', ephemeralName)
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
- await fs.mkdir(skillDir, { recursive: true })
282
- await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillContent, 'utf-8')
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
 
@@ -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, '.config', 'gemini'),
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
- const stat = fs.statSync(p)
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 skillsDir = path.join(config.HOME_DIR, '.claude', 'skills')
50
+ const { getActiveSkillDirs } = require('../llm-plugins/lib/skill-dirs')
51
+ const roots = getActiveSkillDirs()
49
52
 
50
- for (const name of skillNames) {
51
- const skillMdPath = path.join(skillsDir, name, 'SKILL.md')
52
- try {
53
- const original = await fs.readFile(skillMdPath, 'utf-8')
54
- const expanded = expandTemplateVars(original, minionVars)
55
- if (expanded !== original) {
56
- originals.set(name, original)
57
- await fs.writeFile(skillMdPath, expanded, 'utf-8')
58
- console.log(`[TemplateExpander] Expanded {{VAR}} in skill: ${name}`)
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 skillsDir = path.join(config.HOME_DIR, '.claude', 'skills')
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, content, 'utf-8')
81
+ await fs.writeFile(skillMdPath, original, 'utf-8')
79
82
  } catch (err) {
80
- console.error(`[TemplateExpander] Failed to restore ${name}: ${err.message}`)
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 }