@perkos/perkos-a2a 0.8.35 → 0.9.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.
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * install-hermes — managed-service install helper for the
4
+ * Hermes-flavoured PerkOS A2A bridge.
5
+ *
6
+ * Goal: parity with `openclaw plugins install @perkos/perkos-a2a`. The
7
+ * OpenClaw flow drops a plugin into the runtime and the runtime
8
+ * supervises it. Hermes has no plugin host, so the equivalent is a
9
+ * systemd unit (Linux) or a launchd plist (macOS) that supervises the
10
+ * `perkos-a2a-hermes` CLI alongside the Hermes API Server.
11
+ *
12
+ * Two modes:
13
+ * --emit Print the unit/plist to stdout. Use when you want to pipe
14
+ * into your own deploy tooling.
15
+ * --write Write to the conventional system location. Requires root
16
+ * on Linux; on macOS writes to ~/Library/LaunchAgents.
17
+ *
18
+ * Required:
19
+ * --agent-name <name>
20
+ *
21
+ * Common:
22
+ * --relay-url <wss> Transport relay URL
23
+ * --relay-key <key> Transport relay API key
24
+ * --hermes-url <url> Hermes API base URL (default http://127.0.0.1:8642)
25
+ * --port <n> Local A2A port (default 5060)
26
+ * --user <name> systemd User= (default current $USER)
27
+ *
28
+ * Pre-flight: confirms the Hermes API responds before emitting the
29
+ * unit. Skip with --skip-preflight.
30
+ */
31
+
32
+ import { writeFileSync, mkdirSync, existsSync, statSync, chmodSync } from "node:fs";
33
+ import { homedir, platform, userInfo } from "node:os";
34
+ import { join, resolve } from "node:path";
35
+
36
+ function argValue(flag) {
37
+ const idx = process.argv.indexOf(flag);
38
+ if (idx === -1) return undefined;
39
+ return process.argv[idx + 1];
40
+ }
41
+ function hasFlag(flag) { return process.argv.includes(flag); }
42
+
43
+ function fail(msg, code = 1) {
44
+ console.error(`install-hermes: ${msg}`);
45
+ process.exit(code);
46
+ }
47
+
48
+ function help() {
49
+ console.log(`install-hermes — managed-service install for perkos-a2a-hermes.
50
+
51
+ Usage:
52
+ install-hermes --emit --agent-name Apollo [...]
53
+ install-hermes --write --agent-name Apollo [...]
54
+
55
+ Required:
56
+ --agent-name <name>
57
+
58
+ Common:
59
+ --relay-url <wss> Transport relay URL (or A2A_RELAY_URL)
60
+ --relay-key <key> Transport relay API key (or A2A_RELAY_API_KEY)
61
+ --hermes-url <url> Hermes API base URL (default http://127.0.0.1:8642)
62
+ --hermes-token <token> Hermes API bearer token (or HERMES_API_KEY)
63
+ --port <n> Local A2A port (default 5060)
64
+ --user <name> systemd User= (default \$USER)
65
+ --skip-preflight Skip the Hermes API reachability check
66
+
67
+ Output:
68
+ Linux: systemd unit at /etc/systemd/system/perkos-a2a-hermes.service
69
+ macOS: launchd plist at ~/Library/LaunchAgents/xyz.perkos.a2a-hermes.plist
70
+ `);
71
+ }
72
+
73
+ if (hasFlag("-h") || hasFlag("--help")) { help(); process.exit(0); }
74
+ const mode = hasFlag("--emit") ? "emit" : hasFlag("--write") ? "write" : null;
75
+ if (!mode) fail("specify --emit or --write (see --help)", 2);
76
+
77
+ const agentName = argValue("--agent-name");
78
+ if (!agentName) fail("--agent-name is required", 2);
79
+ const port = Number(argValue("--port") || 5060);
80
+ if (!Number.isFinite(port) || port <= 0) fail(`invalid --port ${argValue("--port")}`, 2);
81
+ const relayUrl = argValue("--relay-url") || process.env.A2A_RELAY_URL;
82
+ const relayKey = argValue("--relay-key") || process.env.A2A_RELAY_API_KEY;
83
+ const hermesUrl = argValue("--hermes-url") || process.env.HERMES_API_URL || "http://127.0.0.1:8642";
84
+ const hermesToken = argValue("--hermes-token") || process.env.HERMES_API_KEY;
85
+ const user = argValue("--user") || userInfo().username;
86
+
87
+ async function preflight() {
88
+ if (hasFlag("--skip-preflight")) return;
89
+ let res;
90
+ try {
91
+ res = await fetch(`${hermesUrl.replace(/\/+$/, "")}/v1/responses`, {
92
+ method: "OPTIONS",
93
+ });
94
+ } catch (err) {
95
+ fail(`Hermes API not reachable at ${hermesUrl} (${err.message}). Re-run with --skip-preflight if intentional.`);
96
+ }
97
+ // Any response — including 405 Method Not Allowed — proves something is listening.
98
+ if (!res) fail(`Hermes API check returned no response. Re-run with --skip-preflight if intentional.`);
99
+ }
100
+
101
+ function unitLinux() {
102
+ const env = [
103
+ `Environment=A2A_AGENT_NAME=${agentName}`,
104
+ `Environment=A2A_PORT=${port}`,
105
+ `Environment=HERMES_API_URL=${hermesUrl}`,
106
+ ];
107
+ if (relayUrl) env.push(`Environment=A2A_RELAY_URL=${relayUrl}`);
108
+ if (relayKey) env.push(`Environment=A2A_RELAY_API_KEY=${relayKey}`);
109
+ if (hermesToken) env.push(`Environment=HERMES_API_KEY=${hermesToken}`);
110
+
111
+ return `[Unit]
112
+ Description=PerkOS A2A bridge (Hermes runtime)
113
+ After=network-online.target
114
+ Wants=network-online.target
115
+
116
+ [Service]
117
+ Type=simple
118
+ User=${user}
119
+ ExecStart=${process.execPath} ${resolve(process.argv[1], "..", "..", "..", "dist", "hermes-cli.js")}
120
+ Restart=on-failure
121
+ RestartSec=5
122
+ ${env.join("\n")}
123
+
124
+ [Install]
125
+ WantedBy=default.target
126
+ `;
127
+ }
128
+
129
+ function plistMac() {
130
+ const envPairs = [
131
+ ["A2A_AGENT_NAME", agentName],
132
+ ["A2A_PORT", String(port)],
133
+ ["HERMES_API_URL", hermesUrl],
134
+ ];
135
+ if (relayUrl) envPairs.push(["A2A_RELAY_URL", relayUrl]);
136
+ if (relayKey) envPairs.push(["A2A_RELAY_API_KEY", relayKey]);
137
+ if (hermesToken) envPairs.push(["HERMES_API_KEY", hermesToken]);
138
+
139
+ const envXml = envPairs
140
+ .map(([k, v]) => ` <key>${k}</key>\n <string>${escapeXml(v)}</string>`)
141
+ .join("\n");
142
+
143
+ const cliPath = resolve(process.argv[1], "..", "..", "..", "dist", "hermes-cli.js");
144
+
145
+ return `<?xml version="1.0" encoding="UTF-8"?>
146
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
147
+ <plist version="1.0">
148
+ <dict>
149
+ <key>Label</key>
150
+ <string>xyz.perkos.a2a-hermes</string>
151
+ <key>ProgramArguments</key>
152
+ <array>
153
+ <string>${process.execPath}</string>
154
+ <string>${cliPath}</string>
155
+ </array>
156
+ <key>RunAtLoad</key>
157
+ <true/>
158
+ <key>KeepAlive</key>
159
+ <true/>
160
+ <key>EnvironmentVariables</key>
161
+ <dict>
162
+ ${envXml}
163
+ </dict>
164
+ <key>StandardOutPath</key>
165
+ <string>${homedir()}/Library/Logs/perkos-a2a-hermes.log</string>
166
+ <key>StandardErrorPath</key>
167
+ <string>${homedir()}/Library/Logs/perkos-a2a-hermes.err.log</string>
168
+ </dict>
169
+ </plist>
170
+ `;
171
+ }
172
+
173
+ function escapeXml(s) {
174
+ return String(s)
175
+ .replace(/&/g, "&amp;")
176
+ .replace(/</g, "&lt;")
177
+ .replace(/>/g, "&gt;")
178
+ .replace(/"/g, "&quot;")
179
+ .replace(/'/g, "&apos;");
180
+ }
181
+
182
+ async function main() {
183
+ await preflight();
184
+ const os = platform();
185
+ const content = os === "darwin" ? plistMac() : unitLinux();
186
+ if (mode === "emit") {
187
+ process.stdout.write(content);
188
+ return;
189
+ }
190
+ // mode === "write"
191
+ let target;
192
+ if (os === "darwin") {
193
+ const dir = join(homedir(), "Library", "LaunchAgents");
194
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
195
+ target = join(dir, "xyz.perkos.a2a-hermes.plist");
196
+ writeFileSync(target, content, { mode: 0o644 });
197
+ console.log(`wrote ${target}`);
198
+ console.log("activate: launchctl load -w " + target);
199
+ } else {
200
+ target = "/etc/systemd/system/perkos-a2a-hermes.service";
201
+ try {
202
+ writeFileSync(target, content, { mode: 0o644 });
203
+ console.log(`wrote ${target}`);
204
+ console.log("activate: sudo systemctl daemon-reload && sudo systemctl enable --now perkos-a2a-hermes");
205
+ } catch (err) {
206
+ fail(`cannot write ${target} (${err.message}). Re-run with sudo, or use --emit and pipe to sudo tee.`);
207
+ }
208
+ }
209
+ }
210
+
211
+ main().catch((err) => fail(err.message ?? String(err)));