@agentworkspaceos/hermes 0.1.0 → 0.1.1
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 +6 -6
- package/bin/agentos-hermes.js +110 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,20 +10,20 @@ Run this command from the machine where Hermes is installed:
|
|
|
10
10
|
npx --yes @agentworkspaceos/hermes@latest connect \
|
|
11
11
|
--mode plugin \
|
|
12
12
|
--pair AGOS-XXXX-XXXX \
|
|
13
|
-
--agentos-url
|
|
14
|
-
--hermes-url http://127.0.0.1:8642 \
|
|
15
|
-
--dashboard-url http://127.0.0.1:9119
|
|
13
|
+
--agentos-url http://127.0.0.1:3002
|
|
16
14
|
```
|
|
17
15
|
|
|
18
|
-
The connector sends safe runtime metadata and heartbeat status to AgentOS. Secrets stay on the local machine.
|
|
16
|
+
The connector sends safe runtime metadata and heartbeat status to AgentOS. Secrets stay on the local machine. If the dashboard is opened through a browser-only or Cloudflare-protected URL, keep `--agentos-url` pointed at the direct AgentOS API base URL.
|
|
17
|
+
|
|
18
|
+
When `--hermes-url` is omitted, the CLI probes common local Hermes gateway ports (`8642` through `8645`) and falls back to `http://127.0.0.1:8642`. Use `--hermes-url` when Hermes is running on an unusual local port or a reachable remote gateway. `--dashboard-url` is optional and can be omitted when the Hermes dashboard is not running.
|
|
19
19
|
|
|
20
20
|
## Options
|
|
21
21
|
|
|
22
22
|
```txt
|
|
23
23
|
agentos-hermes connect --pair <code> [options]
|
|
24
24
|
|
|
25
|
-
--agentos-url <url> AgentOS
|
|
26
|
-
--hermes-url <url> Local Hermes gateway URL
|
|
25
|
+
--agentos-url <url> AgentOS API base URL
|
|
26
|
+
--hermes-url <url> Local Hermes gateway URL; omitted means auto-detect local Hermes
|
|
27
27
|
--dashboard-url <url> Optional Hermes dashboard URL
|
|
28
28
|
--mode <mode> plugin, sidecar, or direct-url
|
|
29
29
|
--config-dir <path> Local config directory
|
package/bin/agentos-hermes.js
CHANGED
|
@@ -4,12 +4,15 @@ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { homedir, hostname } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
|
|
7
|
-
const VERSION = "0.1.
|
|
7
|
+
const VERSION = "0.1.1";
|
|
8
8
|
const DEFAULT_AGENTOS_URL = "http://localhost:3000";
|
|
9
9
|
const DEFAULT_HERMES_URL = "http://127.0.0.1:8642";
|
|
10
|
+
const DEFAULT_HERMES_DISCOVERY_PORTS = [8642, 8643, 8644, 8645];
|
|
10
11
|
const DEFAULT_MODE = "plugin";
|
|
11
12
|
const DEFAULT_HERMES_COMMAND = "hermes";
|
|
12
13
|
const HERMES_COMMAND_TIMEOUT_MS = 2500;
|
|
14
|
+
const HERMES_HEALTH_TIMEOUT_MS = 1000;
|
|
15
|
+
const HERMES_DISCOVERY_TIMEOUT_MS = 350;
|
|
13
16
|
const PARENT_AGENT_NAME = "Hermes Main Orchestrator";
|
|
14
17
|
|
|
15
18
|
async function main(argv) {
|
|
@@ -35,7 +38,6 @@ async function main(argv) {
|
|
|
35
38
|
async function connect(args) {
|
|
36
39
|
const options = parseConnectArgs(args);
|
|
37
40
|
const agentosUrl = normalizeBaseUrl(options.agentosUrl ?? process.env.AGENTOS_URL ?? DEFAULT_AGENTOS_URL);
|
|
38
|
-
const hermesUrl = normalizeBaseUrl(options.hermesUrl ?? process.env.HERMES_BASE_URL ?? DEFAULT_HERMES_URL);
|
|
39
41
|
const dashboardUrl = options.dashboardUrl ? normalizeBaseUrl(options.dashboardUrl) : undefined;
|
|
40
42
|
const mode = options.mode ?? DEFAULT_MODE;
|
|
41
43
|
const pairingCode = options.pair;
|
|
@@ -49,7 +51,9 @@ async function connect(args) {
|
|
|
49
51
|
throw new Error('--mode must be "plugin", "sidecar", or "direct-url".');
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
const
|
|
54
|
+
const hermesConnection = await resolveHermesConnection(options);
|
|
55
|
+
const hermesUrl = hermesConnection.hermesUrl;
|
|
56
|
+
const hermesHealth = hermesConnection.hermesHealth;
|
|
53
57
|
const metadata = options.skipInventory
|
|
54
58
|
? createFallbackMetadata(hermesHealth)
|
|
55
59
|
: await collectHermesInventory({ hermesCommand, hermesHealth });
|
|
@@ -83,6 +87,7 @@ async function connect(args) {
|
|
|
83
87
|
console.log(`Pairing code: ${session.pairingCode}`);
|
|
84
88
|
console.log(`Status: ${session.status}`);
|
|
85
89
|
console.log(`Mode: ${mode}`);
|
|
90
|
+
console.log(`Hermes URL: ${hermesUrl}${hermesConnection.autodetected ? " (auto-detected)" : ""}`);
|
|
86
91
|
console.log(`Hermes detected: ${hermesHealth.detected ? "yes" : "no"}`);
|
|
87
92
|
console.log(
|
|
88
93
|
`Inventory: ${metadata.skills?.length ?? 0} skills, ${metadata.toolsets?.length ?? 0} enabled toolsets, ${
|
|
@@ -160,13 +165,75 @@ function requireValue(name, value) {
|
|
|
160
165
|
return value;
|
|
161
166
|
}
|
|
162
167
|
|
|
163
|
-
async function
|
|
168
|
+
async function resolveHermesConnection(options) {
|
|
169
|
+
const configuredHermesUrl = options.hermesUrl ?? process.env.HERMES_BASE_URL;
|
|
170
|
+
|
|
171
|
+
if (configuredHermesUrl) {
|
|
172
|
+
const hermesUrl = normalizeBaseUrl(configuredHermesUrl);
|
|
173
|
+
return {
|
|
174
|
+
autodetected: false,
|
|
175
|
+
hermesHealth: await detectHermes(hermesUrl),
|
|
176
|
+
hermesUrl,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const candidates = createHermesDiscoveryUrls();
|
|
181
|
+
const checks = await Promise.all(
|
|
182
|
+
candidates.map(async (hermesUrl) => ({
|
|
183
|
+
hermesHealth: await detectHermes(hermesUrl, { timeoutMs: HERMES_DISCOVERY_TIMEOUT_MS }),
|
|
184
|
+
hermesUrl,
|
|
185
|
+
})),
|
|
186
|
+
);
|
|
187
|
+
const detected = checks.find((check) => check.hermesHealth.detected);
|
|
188
|
+
|
|
189
|
+
if (detected) {
|
|
190
|
+
return {
|
|
191
|
+
autodetected: true,
|
|
192
|
+
...detected,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const fallbackUrl = normalizeBaseUrl(DEFAULT_HERMES_URL);
|
|
197
|
+
const fallback = checks.find((check) => check.hermesUrl === fallbackUrl);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
autodetected: false,
|
|
201
|
+
hermesHealth: fallback?.hermesHealth ?? (await detectHermes(fallbackUrl)),
|
|
202
|
+
hermesUrl: fallbackUrl,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function createHermesDiscoveryUrls() {
|
|
207
|
+
const configuredUrls = splitEnvList(process.env.HERMES_DISCOVERY_URLS).map((url) => normalizeBaseUrl(url));
|
|
208
|
+
const configuredPorts = splitEnvList(process.env.HERMES_DISCOVERY_PORTS)
|
|
209
|
+
.map((port) => Number(port))
|
|
210
|
+
.filter((port) => Number.isInteger(port) && port > 0 && port < 65536);
|
|
211
|
+
const ports = configuredPorts.length ? configuredPorts : DEFAULT_HERMES_DISCOVERY_PORTS;
|
|
212
|
+
const localUrls = ports.flatMap((port) => [`http://127.0.0.1:${port}`, `http://localhost:${port}`]);
|
|
213
|
+
|
|
214
|
+
return [...new Set([...configuredUrls, ...localUrls].map((url) => normalizeBaseUrl(url)))];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function splitEnvList(value) {
|
|
218
|
+
return value
|
|
219
|
+
? value
|
|
220
|
+
.split(",")
|
|
221
|
+
.map((item) => item.trim())
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
: [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function detectHermes(hermesUrl, options = {}) {
|
|
227
|
+
const controller = new AbortController();
|
|
228
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? HERMES_HEALTH_TIMEOUT_MS);
|
|
229
|
+
|
|
164
230
|
try {
|
|
165
231
|
const response = await fetch(`${hermesUrl}/health`, {
|
|
166
232
|
headers: {
|
|
167
233
|
accept: "application/json",
|
|
168
234
|
},
|
|
169
235
|
method: "GET",
|
|
236
|
+
signal: controller.signal,
|
|
170
237
|
});
|
|
171
238
|
const text = await response.text();
|
|
172
239
|
const body = text ? JSON.parse(text) : {};
|
|
@@ -182,6 +249,8 @@ async function detectHermes(hermesUrl) {
|
|
|
182
249
|
platform: null,
|
|
183
250
|
status: "unavailable",
|
|
184
251
|
};
|
|
252
|
+
} finally {
|
|
253
|
+
clearTimeout(timeout);
|
|
185
254
|
}
|
|
186
255
|
}
|
|
187
256
|
|
|
@@ -1001,8 +1070,7 @@ async function postHeartbeat(agentosUrl, pairingCode, payload) {
|
|
|
1001
1070
|
method: "POST",
|
|
1002
1071
|
},
|
|
1003
1072
|
);
|
|
1004
|
-
const
|
|
1005
|
-
const body = text ? JSON.parse(text) : {};
|
|
1073
|
+
const body = await parseJsonResponse(response, "AgentOS heartbeat endpoint");
|
|
1006
1074
|
|
|
1007
1075
|
if (!response.ok) {
|
|
1008
1076
|
const message = typeof body.error === "string" ? body.error : `AgentOS returned ${response.status}.`;
|
|
@@ -1012,6 +1080,39 @@ async function postHeartbeat(agentosUrl, pairingCode, payload) {
|
|
|
1012
1080
|
return body;
|
|
1013
1081
|
}
|
|
1014
1082
|
|
|
1083
|
+
async function parseJsonResponse(response, context) {
|
|
1084
|
+
const text = await response.text();
|
|
1085
|
+
|
|
1086
|
+
if (!text) {
|
|
1087
|
+
return {};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
try {
|
|
1091
|
+
return JSON.parse(text);
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
const contentType = response.headers.get("content-type") ?? "unknown content type";
|
|
1094
|
+
const status = [response.status, response.statusText].filter(Boolean).join(" ");
|
|
1095
|
+
|
|
1096
|
+
if (isHtmlResponse(text, contentType)) {
|
|
1097
|
+
const isCloudflareChallenge = response.headers.get("cf-mitigated") === "challenge" || text.includes("challenge-platform");
|
|
1098
|
+
const hint = isCloudflareChallenge
|
|
1099
|
+
? " Cloudflare is challenging the CLI request; use a direct local API URL for --agentos-url, such as http://127.0.0.1:3002, or exempt /api/hermes/* from browser challenges."
|
|
1100
|
+
: " Check that --agentos-url points to the AgentOS API server, not a browser-only app shell or proxy fallback. For local AgentOS, use http://127.0.0.1:3002.";
|
|
1101
|
+
|
|
1102
|
+
throw new Error(context + " returned HTML instead of JSON (" + (status || contentType) + ")." + hint);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1106
|
+
throw new Error(context + " returned invalid JSON (" + (status || contentType) + "): " + message);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function isHtmlResponse(text, contentType) {
|
|
1111
|
+
const trimmed = text.trimStart().toLowerCase();
|
|
1112
|
+
|
|
1113
|
+
return contentType.toLowerCase().includes("text/html") || trimmed.startsWith("<!doctype") || trimmed.startsWith("<html");
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1015
1116
|
async function runHeartbeatLoop(sendHeartbeat, intervalMs) {
|
|
1016
1117
|
if (!Number.isFinite(intervalMs) || intervalMs < 1000) {
|
|
1017
1118
|
throw new Error("--interval-ms must be at least 1000.");
|
|
@@ -1169,9 +1270,9 @@ function printConnectHelp() {
|
|
|
1169
1270
|
agentos-hermes connect --pair <code> [options]
|
|
1170
1271
|
|
|
1171
1272
|
Options:
|
|
1172
|
-
--agentos-url <url> AgentOS
|
|
1173
|
-
--hermes-url <url> Local Hermes gateway URL. Defaults to ${DEFAULT_HERMES_URL}
|
|
1174
|
-
--dashboard-url <url> Optional Hermes dashboard URL
|
|
1273
|
+
--agentos-url <url> AgentOS API base URL. Defaults to ${DEFAULT_AGENTOS_URL}
|
|
1274
|
+
--hermes-url <url> Local Hermes gateway URL. Defaults to auto-detection, then ${DEFAULT_HERMES_URL}
|
|
1275
|
+
--dashboard-url <url> Optional Hermes dashboard URL. Omit it when the dashboard is not running
|
|
1175
1276
|
--mode <mode> plugin, sidecar, or direct-url. Defaults to ${DEFAULT_MODE}
|
|
1176
1277
|
--config-dir <path> Local config directory. Defaults to ~/.agentos/hermes
|
|
1177
1278
|
--hermes-command <cmd> Hermes CLI executable. Defaults to ${DEFAULT_HERMES_COMMAND}
|