@robbiesrobotics/alice-agents 1.5.9 → 1.5.10
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/README.md +152 -132
- package/bin/alice-install.mjs +27 -35
- package/lib/hermes-agent.mjs +449 -0
- package/lib/hermes-installer.mjs +338 -0
- package/lib/installer.mjs +254 -19
- package/lib/skills.mjs +128 -4
- package/package.json +3 -3
- package/templates/workspaces/_shared/AGENTS-hermes.md +54 -0
- package/templates/workspaces/_shared/AGENTS.md +25 -0
- package/templates/workspaces/_shared/SOUL-hermes.md +35 -0
- package/templates/workspaces/_shared/hermes-agent-skill.md +40 -0
- package/templates/workspaces/_shared/hermes-orchestrator-skill.md +150 -0
- package/templates/workspaces/_shared/hermes-specialist-skill.md +109 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
// lib/hermes-agent.mjs
|
|
2
|
+
// HermesAgentManager — creates, spawns, and manages Hermes agents as A.L.I.C.E. team members
|
|
3
|
+
|
|
4
|
+
import { execSync, spawn } from 'node:child_process';
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync } from 'node:fs';
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const HERMES_DIR = join(homedir(), '.hermes');
|
|
12
|
+
const OPENCLAW_DIR = join(homedir(), '.openclaw');
|
|
13
|
+
const SKILLS_PARENT = join(HERMES_DIR, 'skills'); // skills live at ~/.hermes/skills/<category>/<id>/SKILL.md
|
|
14
|
+
const HERMES_AGENTS_JSON = join(OPENCLAW_DIR, 'hermes-agents.json');
|
|
15
|
+
const TEMPLATES_DIR = join(__dirname, '..', 'templates', 'workspaces', '_shared');
|
|
16
|
+
|
|
17
|
+
// ── Hermes CLI detection ──────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function findHermesBinary() {
|
|
20
|
+
const candidates = [
|
|
21
|
+
join(HERMES_DIR, 'bin', 'hermes'),
|
|
22
|
+
join(homedir(), '.local', 'bin', 'hermes'),
|
|
23
|
+
'hermes', // PATH
|
|
24
|
+
];
|
|
25
|
+
for (const candidate of candidates) {
|
|
26
|
+
try {
|
|
27
|
+
execSync(`${process.platform === 'win32' ? 'where' : 'which'} ${candidate.replace(/\\/g, '')}`, { stdio: 'pipe' });
|
|
28
|
+
return candidate;
|
|
29
|
+
} catch {
|
|
30
|
+
// try next
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Fallback: trust `hermes` is in PATH
|
|
34
|
+
return 'hermes';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const HERMES_BIN = findHermesBinary();
|
|
38
|
+
|
|
39
|
+
// ── HermesAgentManager ─────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export class HermesAgentManager {
|
|
42
|
+
/**
|
|
43
|
+
* @param {Object} options
|
|
44
|
+
* @param {string} [options.skillsCategory='alice'] — Hermes skill category for A.L.I.C.E. Hermes agents
|
|
45
|
+
* @param {string} [options.defaultToolsets='terminal,file,web'] — default toolsets for spawned agents
|
|
46
|
+
*/
|
|
47
|
+
constructor({ skillsCategory = 'alice', defaultToolsets = 'terminal,file,web' } = {}) {
|
|
48
|
+
this.skillsCategory = skillsCategory;
|
|
49
|
+
this.defaultToolsets = defaultToolsets;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Registry ─────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
_readRegistry() {
|
|
55
|
+
if (!existsSync(HERMES_AGENTS_JSON)) return { hermesAgents: [] };
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(readFileSync(HERMES_AGENTS_JSON, 'utf8'));
|
|
58
|
+
} catch {
|
|
59
|
+
return { hermesAgents: [] };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_writeRegistry(registry) {
|
|
64
|
+
mkdirSync(dirname(HERMES_AGENTS_JSON), { recursive: true });
|
|
65
|
+
writeFileSync(HERMES_AGENTS_JSON, JSON.stringify(registry, null, 2) + '\n', 'utf8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* List all registered Hermes agents.
|
|
70
|
+
* @returns {{ id: string, agentId: string, domain: string, description: string, createdAt: string }[]}
|
|
71
|
+
*/
|
|
72
|
+
listAgents() {
|
|
73
|
+
return this._readRegistry().hermesAgents || [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if a Hermes agent is registered.
|
|
78
|
+
* @param {string} agentId
|
|
79
|
+
* @returns {boolean}
|
|
80
|
+
*/
|
|
81
|
+
hasAgent(agentId) {
|
|
82
|
+
return this.listAgents().some(a => a.agentId === agentId);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Skill directory helpers ───────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Path to the SKILL.md for a given agent.
|
|
89
|
+
* @param {string} agentId
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
_skillPath(agentId) {
|
|
93
|
+
return join(SKILLS_PARENT, this.skillsCategory, agentId, 'SKILL.md');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Ensure the Hermes skills directory exists for an agent.
|
|
98
|
+
* @param {string} agentId
|
|
99
|
+
*/
|
|
100
|
+
_ensureSkillDir(agentId) {
|
|
101
|
+
const dir = join(SKILLS_PARENT, this.skillsCategory, agentId);
|
|
102
|
+
mkdirSync(dir, { recursive: true });
|
|
103
|
+
return dir;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Model / provider detection ────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Detect the default model and provider from OpenClaw config.
|
|
110
|
+
* Returns null if OpenClaw config can't be read.
|
|
111
|
+
*
|
|
112
|
+
* @returns {{ primary: string, provider: string, model: string } | null}
|
|
113
|
+
*/
|
|
114
|
+
static detectOpenClawModel() {
|
|
115
|
+
const configPath = join(OPENCLAW_DIR, 'openclaw.json');
|
|
116
|
+
if (!existsSync(configPath)) return null;
|
|
117
|
+
try {
|
|
118
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
119
|
+
const agentDefaults = config?.agents?.defaults?.model || {};
|
|
120
|
+
const globalModel = config?.model;
|
|
121
|
+
const primary = agentDefaults.primary || globalModel || null;
|
|
122
|
+
if (!primary || typeof primary !== 'string') return null;
|
|
123
|
+
const [provider, ...rest] = primary.split('/');
|
|
124
|
+
const model = rest.join('/'); // handles models with slashes like 'anthropic/claude-sonnet-4-6'
|
|
125
|
+
return { primary, provider, model };
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Build the hermes CLI args for model/provider from OpenClaw detected config.
|
|
133
|
+
* Returns [] if nothing detected.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} [fallbackProvider='minimax']
|
|
136
|
+
* @param {string} [fallbackModel]
|
|
137
|
+
* @returns {string[]}
|
|
138
|
+
*/
|
|
139
|
+
buildHermesModelArgs(fallbackProvider = 'minimax', fallbackModel = '') {
|
|
140
|
+
const detected = HermesAgentManager.detectOpenClawModel();
|
|
141
|
+
if (detected?.provider && detected?.model) {
|
|
142
|
+
return ['--provider', detected.provider, '--model', detected.model];
|
|
143
|
+
}
|
|
144
|
+
// Fallback: use minimax (common default)
|
|
145
|
+
const args = ['--provider', fallbackProvider];
|
|
146
|
+
if (fallbackModel) args.push('--model', fallbackModel);
|
|
147
|
+
return args;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Create ───────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Create a new Hermes agent for the A.L.I.C.E. team.
|
|
154
|
+
*
|
|
155
|
+
* Creates:
|
|
156
|
+
* - ~/.hermes/skills/<category>/<agentId>/SKILL.md
|
|
157
|
+
* - Registers in ~/.openclaw/hermes-agents.json
|
|
158
|
+
*
|
|
159
|
+
* @param {string} agentId — unique ID, e.g. 'researcher' or 'analyst'
|
|
160
|
+
* @param {string} domain — domain label, e.g. 'Research' or 'Data Analysis'
|
|
161
|
+
* @param {string} description — one-line description of what this agent does
|
|
162
|
+
* @param {string} [persona] — optional custom persona instructions override
|
|
163
|
+
* @param {string[]} [skills] — list of hermes skill names to preload (e.g. ['web-search', 'codex'])
|
|
164
|
+
* @param {string} [toolsets] — comma-separated toolsets to enable (default: 'terminal,file,web')
|
|
165
|
+
* @returns {{ status: 'created' | 'exists', agentId: string, skillPath: string }}
|
|
166
|
+
*/
|
|
167
|
+
createAgent(agentId, domain, description, persona = '', skills = [], toolsets = null) {
|
|
168
|
+
// Check if already exists
|
|
169
|
+
if (this.hasAgent(agentId)) {
|
|
170
|
+
return { status: 'exists', agentId, skillPath: this._skillPath(agentId) };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Ensure skill directory
|
|
174
|
+
const skillDir = this._ensureSkillDir(agentId);
|
|
175
|
+
const skillPath = join(skillDir, 'SKILL.md');
|
|
176
|
+
|
|
177
|
+
// Build skill content
|
|
178
|
+
const skillContent = this._buildSkillContent(agentId, domain, description, persona, skills);
|
|
179
|
+
writeFileSync(skillPath, skillContent, 'utf8');
|
|
180
|
+
|
|
181
|
+
// Register
|
|
182
|
+
const registry = this._readRegistry();
|
|
183
|
+
registry.hermesAgents.push({
|
|
184
|
+
id: `hermes-${Date.now()}`,
|
|
185
|
+
agentId,
|
|
186
|
+
domain,
|
|
187
|
+
description,
|
|
188
|
+
skillsCategory: this.skillsCategory,
|
|
189
|
+
toolsets: toolsets || this.defaultToolsets,
|
|
190
|
+
preloadSkills: skills,
|
|
191
|
+
createdAt: new Date().toISOString(),
|
|
192
|
+
});
|
|
193
|
+
this._writeRegistry(registry);
|
|
194
|
+
|
|
195
|
+
return { status: 'created', agentId, skillPath };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Build SKILL.md content for a Hermes agent.
|
|
200
|
+
* @private
|
|
201
|
+
*/
|
|
202
|
+
_buildSkillContent(agentId, domain, description, persona, skills) {
|
|
203
|
+
const skillLines = [
|
|
204
|
+
'---',
|
|
205
|
+
`name: ${agentId}`,
|
|
206
|
+
`description: ${description} — managed by A.L.I.C.E. on OpenClaw`,
|
|
207
|
+
'---',
|
|
208
|
+
'',
|
|
209
|
+
`# ${agentId} (${domain})`,
|
|
210
|
+
'',
|
|
211
|
+
`> Managed by A.L.I.C.E. team — do not edit manually.`,
|
|
212
|
+
'',
|
|
213
|
+
`**Domain:** ${domain}`,
|
|
214
|
+
'',
|
|
215
|
+
`**Role:** ${description}`,
|
|
216
|
+
'',
|
|
217
|
+
'## Persona',
|
|
218
|
+
'',
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
if (persona) {
|
|
222
|
+
skillLines.push(persona, '');
|
|
223
|
+
} else {
|
|
224
|
+
skillLines.push(
|
|
225
|
+
`You are ${agentId}, a ${domain} specialist working on the A.L.I.C.E. team.`,
|
|
226
|
+
`A.L.I.C.E. (orchestrated by OpenClaw) assigns you tasks via this skill interface.`,
|
|
227
|
+
`Your role is ${description}.`,
|
|
228
|
+
'',
|
|
229
|
+
'## Working with A.L.I.C.E.',
|
|
230
|
+
'',
|
|
231
|
+
'- A.L.I.C.E. will send you a task description',
|
|
232
|
+
'- Complete the task using your tools and skills',
|
|
233
|
+
'- Return your findings/results clearly and concisely',
|
|
234
|
+
'- Do not add unnecessary commentary',
|
|
235
|
+
'',
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (skills.length > 0) {
|
|
240
|
+
skillLines.push(
|
|
241
|
+
'## Preloaded Skills',
|
|
242
|
+
'',
|
|
243
|
+
`This agent has the following skills available:`,
|
|
244
|
+
...skills.map(s => ` - \`${s}\``),
|
|
245
|
+
'',
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
skillLines.push(
|
|
250
|
+
'## Notes',
|
|
251
|
+
'',
|
|
252
|
+
'- Skills are managed by A.L.I.C.E. — see `~/.openclaw/hermes-agents.json` for configuration',
|
|
253
|
+
'- Model is inherited from OpenClaw default config',
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return skillLines.join('\n');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── Remove ───────────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Remove a Hermes agent (deletes skill dir + unregisters).
|
|
263
|
+
*
|
|
264
|
+
* @param {string} agentId
|
|
265
|
+
* @returns {{ status: 'removed' | 'not_found' }}
|
|
266
|
+
*/
|
|
267
|
+
removeAgent(agentId) {
|
|
268
|
+
const registry = this._readRegistry();
|
|
269
|
+
const before = registry.hermesAgents.length;
|
|
270
|
+
registry.hermesAgents = registry.hermesAgents.filter(a => a.agentId !== agentId);
|
|
271
|
+
|
|
272
|
+
if (registry.hermesAgents.length === before) {
|
|
273
|
+
return { status: 'not_found' };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this._writeRegistry(registry);
|
|
277
|
+
|
|
278
|
+
// Remove skill directory
|
|
279
|
+
const skillDir = join(SKILLS_PARENT, this.skillsCategory, agentId);
|
|
280
|
+
if (existsSync(skillDir)) {
|
|
281
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { status: 'removed' };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── Spawn ───────────────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Spawn a Hermes agent to execute a single task.
|
|
291
|
+
* Uses `hermes chat -q "<task>"` in quiet mode.
|
|
292
|
+
*
|
|
293
|
+
* @param {string} agentId — registered Hermes agent ID
|
|
294
|
+
* @param {string} task — task description (will be wrapped in persona context)
|
|
295
|
+
* @param {{ timeout?: number, toolsets?: string, skills?: string[], context?: string }} [options]
|
|
296
|
+
* @returns {Promise<{ stdout: string, stderr: string, exitCode: number }>}
|
|
297
|
+
*/
|
|
298
|
+
spawnAgent(agentId, task, options = {}) {
|
|
299
|
+
const { timeout = 300, toolsets: overrideToolsets, skills: overrideSkills, context = '' } = options;
|
|
300
|
+
|
|
301
|
+
const registry = this._readRegistry();
|
|
302
|
+
const agent = registry.hermesAgents.find(a => a.agentId === agentId);
|
|
303
|
+
|
|
304
|
+
const toolsets = overrideToolsets || agent?.toolsets || this.defaultToolsets;
|
|
305
|
+
const preloadSkills = overrideSkills || agent?.preloadSkills || [];
|
|
306
|
+
|
|
307
|
+
// Build the prompt — wrap task in agent context
|
|
308
|
+
const prompt = this._buildPrompt(agentId, task, context);
|
|
309
|
+
|
|
310
|
+
// Build CLI args
|
|
311
|
+
const args = [
|
|
312
|
+
'chat', '-q', prompt,
|
|
313
|
+
'-s', preloadSkills.join(','),
|
|
314
|
+
'-t', toolsets,
|
|
315
|
+
'-Q', // quiet mode — programmatic output only
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
// Add model/provider from OpenClaw config
|
|
319
|
+
const modelArgs = this.buildHermesModelArgs();
|
|
320
|
+
args.push(...modelArgs);
|
|
321
|
+
|
|
322
|
+
return new Promise((resolve, reject) => {
|
|
323
|
+
const child = spawn(HERMES_BIN, args, {
|
|
324
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
325
|
+
timeout: timeout * 1000,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
let stdout = '';
|
|
329
|
+
let stderr = '';
|
|
330
|
+
|
|
331
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
332
|
+
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
333
|
+
|
|
334
|
+
child.on('error', reject);
|
|
335
|
+
|
|
336
|
+
child.on('close', (code) => {
|
|
337
|
+
resolve({ stdout, stderr, exitCode: code ?? 0 });
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Handle timeout
|
|
341
|
+
const timer = setTimeout(() => {
|
|
342
|
+
child.kill('SIGTERM');
|
|
343
|
+
reject(new Error(`Hermes agent "${agentId}" timed out after ${timeout}s`));
|
|
344
|
+
}, timeout * 1000);
|
|
345
|
+
|
|
346
|
+
child.on('close', () => clearTimeout(timer));
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Build the prompt passed to hermes chat -q.
|
|
352
|
+
* Wraps the task in agent context so Hermes knows who it is.
|
|
353
|
+
* @private
|
|
354
|
+
*/
|
|
355
|
+
_buildPrompt(agentId, task, context) {
|
|
356
|
+
const lines = [
|
|
357
|
+
`[A.L.I.C.E. Team — ${agentId}]`,
|
|
358
|
+
'',
|
|
359
|
+
context ? `${context}\n` : '',
|
|
360
|
+
`Task: ${task}`,
|
|
361
|
+
'',
|
|
362
|
+
'Instructions: You are a specialist on the A.L.I.C.E. team. Complete the task above.',
|
|
363
|
+
'Return your results clearly. Do not add meta-commentary about being an AI.',
|
|
364
|
+
];
|
|
365
|
+
return lines.join('\n');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── Quick spawn (one-off, no registration required) ────────────────────
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Spawn a quick one-off Hermes agent without registering it.
|
|
372
|
+
* Useful for tasks that don't need persistent memory.
|
|
373
|
+
*
|
|
374
|
+
* @param {string} task
|
|
375
|
+
* @param {{ domain?: string, skills?: string[], toolsets?: string, timeout?: number }} [options]
|
|
376
|
+
* @returns {Promise<{ stdout: string, stderr: string, exitCode: number }>}
|
|
377
|
+
*/
|
|
378
|
+
quickSpawn(task, options = {}) {
|
|
379
|
+
const {
|
|
380
|
+
domain = 'Specialist',
|
|
381
|
+
skills = [],
|
|
382
|
+
toolsets = 'terminal,file,web',
|
|
383
|
+
timeout = 300,
|
|
384
|
+
} = options;
|
|
385
|
+
|
|
386
|
+
return this.spawnAgent(`__quick__-${Date.now()}`, task, {
|
|
387
|
+
...options,
|
|
388
|
+
// For quick spawn, we pass skills/toolsets directly but don't register
|
|
389
|
+
toolsets,
|
|
390
|
+
skills,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ── OpenClaw bridge setup ───────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Register the A.L.I.C.E. OpenClaw bridge — sets up the hermes-agents.json
|
|
398
|
+
* and verifies Hermes is reachable.
|
|
399
|
+
*
|
|
400
|
+
* @returns {{ status: 'ok', hermesVersion: string } | { status: 'error', reason: string }}
|
|
401
|
+
*/
|
|
402
|
+
setupBridge() {
|
|
403
|
+
try {
|
|
404
|
+
// Verify Hermes is installed
|
|
405
|
+
const version = execSync(`${HERMES_BIN} version 2>&1`, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
406
|
+
|
|
407
|
+
// Ensure registry exists
|
|
408
|
+
if (!existsSync(HERMES_AGENTS_JSON)) {
|
|
409
|
+
this._writeRegistry({ hermesAgents: [] });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return { status: 'ok', hermesVersion: version };
|
|
413
|
+
} catch (err) {
|
|
414
|
+
return { status: 'error', reason: `Hermes not found or not working: ${err.message}` };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ── Inspect ─────────────────────────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Get full details for a registered Hermes agent.
|
|
422
|
+
* @param {string} agentId
|
|
423
|
+
* @returns {Object | null}
|
|
424
|
+
*/
|
|
425
|
+
getAgent(agentId) {
|
|
426
|
+
return this.listAgents().find(a => a.agentId === agentId) || null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Get the path to an agent's SKILL.md.
|
|
431
|
+
* @param {string} agentId
|
|
432
|
+
* @returns {string | null}
|
|
433
|
+
*/
|
|
434
|
+
getSkillPath(agentId) {
|
|
435
|
+
const p = this._skillPath(agentId);
|
|
436
|
+
return existsSync(p) ? p : null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── Export singleton factory ───────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
let _manager = null;
|
|
443
|
+
|
|
444
|
+
export function getHermesAgentManager(options) {
|
|
445
|
+
if (!_manager) {
|
|
446
|
+
_manager = new HermesAgentManager(options);
|
|
447
|
+
}
|
|
448
|
+
return _manager;
|
|
449
|
+
}
|