@openagents-org/agent-launcher 0.2.111 → 0.2.112
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/package.json +1 -1
- package/registry.json +32 -0
- package/src/adapters/hermes.js +360 -0
- package/src/adapters/index.js +4 -1
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -535,5 +535,37 @@
|
|
|
535
535
|
"linux": "pip install sweagent",
|
|
536
536
|
"windows": "pip install sweagent"
|
|
537
537
|
}
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
"name": "hermes",
|
|
541
|
+
"label": "Hermes Agent",
|
|
542
|
+
"description": "Nous Hermes Agent — self-improving AI with tools, profiles, memory, and messaging",
|
|
543
|
+
"homepage": "https://github.com/NousResearch/hermes-agent",
|
|
544
|
+
"tags": [
|
|
545
|
+
"coding",
|
|
546
|
+
"tools",
|
|
547
|
+
"orchestration",
|
|
548
|
+
"profiles"
|
|
549
|
+
],
|
|
550
|
+
"featured": true,
|
|
551
|
+
"order": 6,
|
|
552
|
+
"support": { "install": true, "workspace": true, "collaboration": true },
|
|
553
|
+
"builtin": true,
|
|
554
|
+
"install": {
|
|
555
|
+
"binary": "hermes",
|
|
556
|
+
"requires": ["python3"],
|
|
557
|
+
"macos": "curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash",
|
|
558
|
+
"linux": "curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash",
|
|
559
|
+
"windows": "echo 'Hermes requires WSL2 on Windows — see https://github.com/NousResearch/hermes-agent'"
|
|
560
|
+
},
|
|
561
|
+
"launch": {
|
|
562
|
+
"args": []
|
|
563
|
+
},
|
|
564
|
+
"check_ready": {
|
|
565
|
+
"creds_file": "~/.hermes/config.yaml",
|
|
566
|
+
"status_command": "hermes status",
|
|
567
|
+
"login_command": "hermes setup",
|
|
568
|
+
"not_ready_message": "Hermes not configured — run: hermes setup"
|
|
569
|
+
}
|
|
538
570
|
}
|
|
539
571
|
]
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermes adapter for OpenAgents workspace.
|
|
3
|
+
*
|
|
4
|
+
* Bridges Nous Research's Hermes Agent CLI (https://github.com/NousResearch/hermes-agent)
|
|
5
|
+
* to an OpenAgents workspace by spawning `hermes chat -q <prompt> -Q` per
|
|
6
|
+
* incoming message and posting the response back to the workspace channel.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors the Python adapter at sdk/src/openagents/adapters/hermes.py:
|
|
9
|
+
* - per-channel Hermes session IDs persisted to ~/.openagents/sessions/
|
|
10
|
+
* - profile auto-detection from the agent name (falls back to 'default')
|
|
11
|
+
* - workspace context injection (identity + recent history + agent roster)
|
|
12
|
+
* - subprocess isolation (hermes manages its own HERMES_HOME per profile)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { execSync, spawn } = require('child_process');
|
|
21
|
+
|
|
22
|
+
const BaseAdapter = require('./base');
|
|
23
|
+
const { buildOpenclawSystemPrompt } = require('./workspace-prompt');
|
|
24
|
+
|
|
25
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
26
|
+
const SESSION_ID_RE = /session_id:\s*(\S+)/;
|
|
27
|
+
const MAX_HISTORY_ENTRIES = 12;
|
|
28
|
+
|
|
29
|
+
class HermesAdapter extends BaseAdapter {
|
|
30
|
+
/**
|
|
31
|
+
* @param {object} opts - BaseAdapter opts plus:
|
|
32
|
+
* @param {string} [opts.hermesProfile] - explicit Hermes profile, or 'auto'
|
|
33
|
+
* @param {string} [opts.hermesSource] - `--source` label (default: 'tool')
|
|
34
|
+
* @param {number} [opts.maxTurns] - `--max-turns` value
|
|
35
|
+
* @param {boolean} [opts.yolo] - pass `--yolo` to skip prompts
|
|
36
|
+
* @param {Set} [opts.disabledModules]
|
|
37
|
+
*/
|
|
38
|
+
constructor(opts) {
|
|
39
|
+
super(opts);
|
|
40
|
+
this.disabledModules = opts.disabledModules || new Set();
|
|
41
|
+
this.hermesProfile = this._resolveProfile(opts.hermesProfile, this.agentName);
|
|
42
|
+
this.hermesSource = opts.hermesSource || 'tool';
|
|
43
|
+
this.maxTurns = Number.isInteger(opts.maxTurns) ? opts.maxTurns : 60;
|
|
44
|
+
this.yolo = !!opts.yolo;
|
|
45
|
+
|
|
46
|
+
this._channelSessions = {};
|
|
47
|
+
this._channelProcesses = {};
|
|
48
|
+
this._sessionsFile = path.join(
|
|
49
|
+
os.homedir(), '.openagents', 'sessions',
|
|
50
|
+
`${this.workspaceId}_${this.agentName}_hermes.json`,
|
|
51
|
+
);
|
|
52
|
+
this._loadSessions();
|
|
53
|
+
|
|
54
|
+
this._hermesBin = this._findHermesBinary();
|
|
55
|
+
if (this._hermesBin) {
|
|
56
|
+
this._log(`Using Hermes binary: ${this._hermesBin} (profile=${this.hermesProfile})`);
|
|
57
|
+
} else {
|
|
58
|
+
this._log('Warning: hermes CLI not found. Install: curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ------------------------------------------------------------------
|
|
63
|
+
// Binary discovery (multi-tier, matching codex/claude pattern)
|
|
64
|
+
// ------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
_findHermesBinary() {
|
|
67
|
+
const home = os.homedir();
|
|
68
|
+
|
|
69
|
+
// Tier 1: PATH
|
|
70
|
+
try {
|
|
71
|
+
if (IS_WINDOWS) {
|
|
72
|
+
// Native Windows unsupported upstream — we try anyway for WSL cases
|
|
73
|
+
const r = execSync('where hermes 2>nul', { encoding: 'utf-8', timeout: 5000 });
|
|
74
|
+
const found = r.split(/\r?\n/)[0].trim();
|
|
75
|
+
if (found) return found;
|
|
76
|
+
} else {
|
|
77
|
+
const found = execSync('which hermes', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
78
|
+
if (found) return found;
|
|
79
|
+
}
|
|
80
|
+
} catch {}
|
|
81
|
+
|
|
82
|
+
// Tier 2: Common install locations (hermes installer uses ~/.local/bin)
|
|
83
|
+
const candidates = IS_WINDOWS ? [] : [
|
|
84
|
+
path.join(home, '.local', 'bin', 'hermes'),
|
|
85
|
+
'/opt/homebrew/bin/hermes',
|
|
86
|
+
'/usr/local/bin/hermes',
|
|
87
|
+
];
|
|
88
|
+
for (const c of candidates) {
|
|
89
|
+
if (fs.existsSync(c)) return c;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_resolveProfile(explicit, agentName) {
|
|
96
|
+
if (explicit && explicit !== '' && explicit !== 'auto') return explicit;
|
|
97
|
+
// Match agent name to an existing ~/.hermes/profiles/<name> if present
|
|
98
|
+
try {
|
|
99
|
+
const profileDir = path.join(os.homedir(), '.hermes', 'profiles', agentName);
|
|
100
|
+
if (fs.existsSync(profileDir)) return agentName;
|
|
101
|
+
} catch {}
|
|
102
|
+
return 'default';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ------------------------------------------------------------------
|
|
106
|
+
// Session persistence (per-channel Hermes session IDs)
|
|
107
|
+
// ------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
_loadSessions() {
|
|
110
|
+
try {
|
|
111
|
+
if (fs.existsSync(this._sessionsFile)) {
|
|
112
|
+
const data = JSON.parse(fs.readFileSync(this._sessionsFile, 'utf-8'));
|
|
113
|
+
if (data && typeof data === 'object') {
|
|
114
|
+
Object.assign(this._channelSessions, data);
|
|
115
|
+
this._log(`Loaded ${Object.keys(data).length} Hermes session(s)`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
this._log('Could not load Hermes sessions file, starting fresh');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_saveSessions() {
|
|
124
|
+
try {
|
|
125
|
+
fs.mkdirSync(path.dirname(this._sessionsFile), { recursive: true });
|
|
126
|
+
fs.writeFileSync(this._sessionsFile, JSON.stringify(this._channelSessions));
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ------------------------------------------------------------------
|
|
131
|
+
// Prompt assembly
|
|
132
|
+
// ------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
async _getAgentsText() {
|
|
135
|
+
try {
|
|
136
|
+
const agents = await this.client.getAgents(this.workspaceId, this.token);
|
|
137
|
+
if (!Array.isArray(agents) || agents.length === 0) return '';
|
|
138
|
+
const lines = agents
|
|
139
|
+
.map((a) => {
|
|
140
|
+
const name = a.agentName || a.agent_name || a.name;
|
|
141
|
+
if (!name) return null;
|
|
142
|
+
const role = a.role || 'member';
|
|
143
|
+
const status = a.status || 'unknown';
|
|
144
|
+
return `- ${name} (${role}, ${status})`;
|
|
145
|
+
})
|
|
146
|
+
.filter(Boolean);
|
|
147
|
+
return lines.length ? `## Available Workspace Agents\n${lines.join('\n')}` : '';
|
|
148
|
+
} catch {
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async _getRecentHistoryText(channelName) {
|
|
154
|
+
try {
|
|
155
|
+
const messages = await this.client.pollMessages({
|
|
156
|
+
workspaceId: this.workspaceId,
|
|
157
|
+
channelName,
|
|
158
|
+
token: this.token,
|
|
159
|
+
limit: MAX_HISTORY_ENTRIES,
|
|
160
|
+
});
|
|
161
|
+
if (!Array.isArray(messages) || messages.length === 0) return '';
|
|
162
|
+
const lines = messages
|
|
163
|
+
.filter((m) => m.messageType !== 'status')
|
|
164
|
+
.map((m) => {
|
|
165
|
+
const sender = m.senderName || m.senderType || 'unknown';
|
|
166
|
+
const content = (m.content || '').trim();
|
|
167
|
+
if (!content) return null;
|
|
168
|
+
return `- ${sender}: ${content.slice(0, 400)}`;
|
|
169
|
+
})
|
|
170
|
+
.filter(Boolean);
|
|
171
|
+
return lines.length ? `## Recent Workspace Messages\n${lines.join('\n')}` : '';
|
|
172
|
+
} catch {
|
|
173
|
+
return '';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async _buildContextPrefix(channelName) {
|
|
178
|
+
const parts = [
|
|
179
|
+
buildOpenclawSystemPrompt({
|
|
180
|
+
agentName: this.agentName,
|
|
181
|
+
workspaceId: this.workspaceId,
|
|
182
|
+
channelName,
|
|
183
|
+
endpoint: this.endpoint,
|
|
184
|
+
token: this.token,
|
|
185
|
+
mode: this._mode,
|
|
186
|
+
disabledModules: this.disabledModules,
|
|
187
|
+
}),
|
|
188
|
+
'\n## OpenAgents-specific Rules',
|
|
189
|
+
'- Your final text response is posted back to the workspace automatically.',
|
|
190
|
+
'- If you need to ask the user something, ask in normal text. Do not try to open an interactive prompt.',
|
|
191
|
+
'- Do not reveal secrets, tokens, raw auth headers, or internal command lines.',
|
|
192
|
+
'- Keep status concise. Focus on useful output over theatre.',
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
const [agentsText, historyText] = await Promise.all([
|
|
196
|
+
this._getAgentsText(),
|
|
197
|
+
this._getRecentHistoryText(channelName),
|
|
198
|
+
]);
|
|
199
|
+
if (agentsText) parts.push('\n' + agentsText);
|
|
200
|
+
if (historyText) parts.push('\n' + historyText);
|
|
201
|
+
return parts.join('\n').trim();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ------------------------------------------------------------------
|
|
205
|
+
// Output parsing
|
|
206
|
+
// ------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
_parseHermesOutput(raw) {
|
|
209
|
+
let sessionId = null;
|
|
210
|
+
let body = raw;
|
|
211
|
+
|
|
212
|
+
const m = SESSION_ID_RE.exec(body);
|
|
213
|
+
if (m) {
|
|
214
|
+
sessionId = m[1];
|
|
215
|
+
body = body.replace(SESSION_ID_RE, '');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const lines = [];
|
|
219
|
+
for (const line of body.split(/\r?\n/)) {
|
|
220
|
+
const stripped = line.trim();
|
|
221
|
+
if (!stripped) continue;
|
|
222
|
+
if (stripped.startsWith('↻ Resumed session ')) continue;
|
|
223
|
+
lines.push(line);
|
|
224
|
+
}
|
|
225
|
+
return { text: lines.join('\n').trim(), sessionId };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ------------------------------------------------------------------
|
|
229
|
+
// Subprocess lifecycle
|
|
230
|
+
// ------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
_buildHermesCmd(prompt, resumeSessionId) {
|
|
233
|
+
if (!this._hermesBin) {
|
|
234
|
+
throw new Error('hermes CLI not found. Install with: curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash');
|
|
235
|
+
}
|
|
236
|
+
const args = [];
|
|
237
|
+
if (this.hermesProfile && this.hermesProfile !== 'default') {
|
|
238
|
+
args.push('-p', this.hermesProfile);
|
|
239
|
+
}
|
|
240
|
+
args.push(
|
|
241
|
+
'chat',
|
|
242
|
+
'-q', prompt,
|
|
243
|
+
'-Q',
|
|
244
|
+
'--source', this.hermesSource,
|
|
245
|
+
'--max-turns', String(this.maxTurns),
|
|
246
|
+
);
|
|
247
|
+
if (resumeSessionId) args.push('--resume', resumeSessionId);
|
|
248
|
+
if (this.yolo) args.push('--yolo');
|
|
249
|
+
return args;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async _runHermes(prompt, channelName) {
|
|
253
|
+
const resumeId = this._channelSessions[channelName];
|
|
254
|
+
const args = this._buildHermesCmd(prompt, resumeId);
|
|
255
|
+
this._log(`Running hermes (profile=${this.hermesProfile}, channel=${channelName}, resume=${!!resumeId})`);
|
|
256
|
+
|
|
257
|
+
const env = { ...(this.agentEnv || process.env) };
|
|
258
|
+
const proc = spawn(this._hermesBin, args, {
|
|
259
|
+
env,
|
|
260
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
261
|
+
detached: !IS_WINDOWS,
|
|
262
|
+
});
|
|
263
|
+
this._channelProcesses[channelName] = proc;
|
|
264
|
+
|
|
265
|
+
let stdout = '';
|
|
266
|
+
let stderr = '';
|
|
267
|
+
proc.stdout.on('data', (d) => { stdout += d.toString('utf-8'); });
|
|
268
|
+
proc.stderr.on('data', (d) => { stderr += d.toString('utf-8'); });
|
|
269
|
+
|
|
270
|
+
const exitCode = await new Promise((resolve) => {
|
|
271
|
+
proc.on('exit', resolve);
|
|
272
|
+
proc.on('error', () => resolve(-1));
|
|
273
|
+
});
|
|
274
|
+
delete this._channelProcesses[channelName];
|
|
275
|
+
|
|
276
|
+
if (exitCode !== 0) {
|
|
277
|
+
if (resumeId) {
|
|
278
|
+
// Resume may have failed because the session was deleted — drop it and retry fresh
|
|
279
|
+
this._log(`Hermes resume failed (code=${exitCode}), retrying without resume`);
|
|
280
|
+
delete this._channelSessions[channelName];
|
|
281
|
+
this._saveSessions();
|
|
282
|
+
return this._runHermes(prompt, channelName);
|
|
283
|
+
}
|
|
284
|
+
const detail = (stderr || stdout).trim().slice(0, 600);
|
|
285
|
+
throw new Error(`hermes exited with code ${exitCode}: ${detail}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const { text, sessionId } = this._parseHermesOutput(stdout);
|
|
289
|
+
if (sessionId) {
|
|
290
|
+
this._channelSessions[channelName] = sessionId;
|
|
291
|
+
this._saveSessions();
|
|
292
|
+
}
|
|
293
|
+
return text;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async _stopProcess(proc) {
|
|
297
|
+
if (!proc || proc.exitCode !== null) return;
|
|
298
|
+
try {
|
|
299
|
+
if (IS_WINDOWS) {
|
|
300
|
+
try { execSync(`taskkill /F /T /PID ${proc.pid}`, { timeout: 5000 }); } catch {}
|
|
301
|
+
} else {
|
|
302
|
+
try { process.kill(-proc.pid, 'SIGTERM'); } catch {
|
|
303
|
+
proc.kill('SIGTERM');
|
|
304
|
+
}
|
|
305
|
+
await new Promise((resolve) => {
|
|
306
|
+
const timeout = setTimeout(() => {
|
|
307
|
+
try { process.kill(-proc.pid, 'SIGKILL'); } catch {
|
|
308
|
+
proc.kill('SIGKILL');
|
|
309
|
+
}
|
|
310
|
+
resolve();
|
|
311
|
+
}, 5000);
|
|
312
|
+
proc.on('exit', () => { clearTimeout(timeout); resolve(); });
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
} catch {}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async _onControlAction(action, _payload) {
|
|
319
|
+
if (action === 'stop') {
|
|
320
|
+
for (const [channel, proc] of Object.entries(this._channelProcesses)) {
|
|
321
|
+
await this._stopProcess(proc);
|
|
322
|
+
delete this._channelProcesses[channel];
|
|
323
|
+
try { await this.sendStatus(channel, 'Execution stopped by user'); } catch {}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ------------------------------------------------------------------
|
|
329
|
+
// Message handler
|
|
330
|
+
// ------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
async _handleMessage(msg) {
|
|
333
|
+
const content = (msg.content || '').trim();
|
|
334
|
+
if (!content) return;
|
|
335
|
+
|
|
336
|
+
const msgChannel = msg.sessionId || this.channelName;
|
|
337
|
+
const sender = msg.senderName || msg.senderType || 'user';
|
|
338
|
+
this._log(`Processing workspace message from ${sender} in ${msgChannel}`);
|
|
339
|
+
|
|
340
|
+
await this._autoTitleChannel(msgChannel, content);
|
|
341
|
+
await this.sendStatus(msgChannel, 'thinking...');
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const context = await this._buildContextPrefix(msgChannel);
|
|
345
|
+
const prompt = context ? `${context}\n\n---\n\nUser message:\n${content}` : content;
|
|
346
|
+
const responseText = await this._runHermes(prompt, msgChannel);
|
|
347
|
+
|
|
348
|
+
if (responseText) {
|
|
349
|
+
await this.sendResponse(msgChannel, responseText);
|
|
350
|
+
} else {
|
|
351
|
+
await this.sendResponse(msgChannel, 'No response generated. Please try again.');
|
|
352
|
+
}
|
|
353
|
+
} catch (e) {
|
|
354
|
+
this._log(`Hermes adapter error: ${e.message}`);
|
|
355
|
+
await this.sendError(msgChannel, `Error processing message: ${e.message}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
module.exports = HermesAdapter;
|
package/src/adapters/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const CodexAdapter = require('./codex');
|
|
|
11
11
|
const OpenCodeAdapter = require('./opencode');
|
|
12
12
|
const NanoClawAdapter = require('./nanoclaw');
|
|
13
13
|
const CursorAdapter = require('./cursor');
|
|
14
|
+
const HermesAdapter = require('./hermes');
|
|
14
15
|
|
|
15
16
|
const ADAPTER_MAP = {
|
|
16
17
|
openclaw: OpenClawAdapter,
|
|
@@ -19,11 +20,12 @@ const ADAPTER_MAP = {
|
|
|
19
20
|
opencode: OpenCodeAdapter,
|
|
20
21
|
nanoclaw: NanoClawAdapter,
|
|
21
22
|
cursor: CursorAdapter,
|
|
23
|
+
hermes: HermesAdapter,
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Create an adapter instance for the given agent type.
|
|
26
|
-
* @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor)
|
|
28
|
+
* @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes)
|
|
27
29
|
* @param {object} opts - Adapter constructor options
|
|
28
30
|
* @returns {BaseAdapter}
|
|
29
31
|
*/
|
|
@@ -43,6 +45,7 @@ module.exports = {
|
|
|
43
45
|
OpenCodeAdapter,
|
|
44
46
|
NanoClawAdapter,
|
|
45
47
|
CursorAdapter,
|
|
48
|
+
HermesAdapter,
|
|
46
49
|
createAdapter,
|
|
47
50
|
ADAPTER_MAP,
|
|
48
51
|
};
|