athena-agent-launcher 0.1.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.
Files changed (3) hide show
  1. package/README.md +31 -0
  2. package/bin/launcher.mjs +338 -0
  3. package/package.json +31 -0
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # athena-agent-launcher
2
+
3
+ Runs an agent configured on the Athena control plane locally on the user's machine.
4
+
5
+ ```
6
+ # One-time setup (saves credentials to ~/.athena/credentials.json)
7
+ npx athena-agent-launcher setup
8
+
9
+ # Run an agent
10
+ npx athena-agent-launcher run <agent-id>
11
+ ```
12
+
13
+ For `hermes` runtime agents, the launcher will auto-install the Hermes binary
14
+ silently on first run (via `curl ... | bash -s -- --skip-setup`), skipping Nous
15
+ Research's setup wizard. The agent uses Athena's pre-configured YAML, an
16
+ isolated `HOME` at `~/.athena/hermes/<agent-id>/`, and the Athena
17
+ Anthropic-compatible proxy for inference.
18
+
19
+ ## Auth
20
+
21
+ Per-agent API key (`ak_...`) issued via the Athena agents table. Supplied via one of:
22
+
23
+ - `--api-key ak_...`
24
+ - `ATHENA_API_KEY` env var
25
+ - `~/.athena/credentials.json` → `{"api_key": "ak_...", "base_url": "..."}`
26
+
27
+ ## Flow
28
+
29
+ 1. Fetch `GET {base_url}/api/agents/{agentId}/launcher-manifest` with `Authorization: Bearer ak_...`.
30
+ 2. Manifest declares `runtime.kind` (`anthropic_sdk` | `openclaw` | `hermes`) and `runtime.version`.
31
+ 3. Launcher spawns `npx -y <entrypoint>` with manifest env injected. The spawned runtime resolves Anthropic / Cortex credentials via `ATHENA_GATEWAY_URL`; raw secrets never touch the launcher or the local filesystem.
@@ -0,0 +1,338 @@
1
+ #!/usr/bin/env node
2
+ // athena-agent-launcher
3
+ //
4
+ // Usage:
5
+ // npx athena-agent-launcher run <agent-id> [--base-url URL] [--api-key ak_...]
6
+ //
7
+ // Reads runtime config from Athena's control plane and spawns the
8
+ // configured runtime (anthropic_sdk | openclaw | hermes). Secrets are
9
+ // resolved at runtime by the spawned process via ATHENA_GATEWAY_URL — the
10
+ // launcher itself never sees raw Anthropic or Cortex credentials.
11
+
12
+ import { spawn } from "node:child_process";
13
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { join, dirname } from "node:path";
16
+ import readline from "node:readline";
17
+
18
+ const DEFAULT_BASE_URL = "https://athena-v2-pi.vercel.app";
19
+
20
+ function parseArgs(argv) {
21
+ const args = { _: [] };
22
+ for (let i = 0; i < argv.length; i++) {
23
+ const a = argv[i];
24
+ if (a === "--base-url") args.baseUrl = argv[++i];
25
+ else if (a === "--api-key") args.apiKey = argv[++i];
26
+ else if (a === "--runtime-cmd") args.runtimeCmd = argv[++i];
27
+ else if (a === "--manifest-only") args.manifestOnly = true;
28
+ else if (a === "--help" || a === "-h") args.help = true;
29
+ else args._.push(a);
30
+ }
31
+ return args;
32
+ }
33
+
34
+ function loadCredsFile() {
35
+ const p = join(homedir(), ".athena", "credentials.json");
36
+ if (!existsSync(p)) return {};
37
+ try {
38
+ return JSON.parse(readFileSync(p, "utf8"));
39
+ } catch {
40
+ return {};
41
+ }
42
+ }
43
+
44
+ function usage() {
45
+ process.stderr.write(
46
+ [
47
+ "Athena Agent Launcher — Sonance",
48
+ "",
49
+ "Usage:",
50
+ " athena-agent setup One-time setup (prompts for creds)",
51
+ " athena-agent run <agent-id> [options] Run an agent locally",
52
+ "",
53
+ "Options for `run`:",
54
+ " --base-url URL Athena control-plane base URL",
55
+ ` (default: ${DEFAULT_BASE_URL} or $ATHENA_BASE_URL)`,
56
+ " --api-key ak_... Per-agent API key",
57
+ " (default: $ATHENA_API_KEY or ~/.athena/credentials.json)",
58
+ " --manifest-only Print the launcher manifest and exit (no spawn)",
59
+ " --runtime-cmd Override the runtime command (smoke testing)",
60
+ "",
61
+ ].join("\n"),
62
+ );
63
+ }
64
+
65
+ function prompt(question, { mask = false } = {}) {
66
+ return new Promise((resolve) => {
67
+ const rl = readline.createInterface({
68
+ input: process.stdin,
69
+ output: process.stdout,
70
+ terminal: true,
71
+ });
72
+ if (mask) {
73
+ // Hide typed input for secrets.
74
+ const onData = (char) => {
75
+ const s = char.toString();
76
+ if (s === "\n" || s === "\r" || s === "\r\n") {
77
+ process.stdin.removeListener("data", onData);
78
+ } else {
79
+ process.stdout.write("\x1b[2K\r" + question + "*".repeat(rl.line.length));
80
+ }
81
+ };
82
+ process.stdin.on("data", onData);
83
+ }
84
+ rl.question(question, (answer) => {
85
+ rl.close();
86
+ if (mask) process.stdout.write("\n");
87
+ resolve(answer.trim());
88
+ });
89
+ });
90
+ }
91
+
92
+ async function runSetup() {
93
+ process.stdout.write(
94
+ [
95
+ "",
96
+ "╭──────────────────────────────────────────────╮",
97
+ "│ Athena Agent Launcher — Sonance Setup │",
98
+ "╰──────────────────────────────────────────────╯",
99
+ "",
100
+ "This will save your Athena credentials to ~/.athena/credentials.json",
101
+ "so you can run agents without passing flags every time.",
102
+ "",
103
+ ].join("\n"),
104
+ );
105
+ const existing = loadCredsFile();
106
+ const baseUrl =
107
+ (await prompt(`Athena base URL [${existing.base_url || DEFAULT_BASE_URL}]: `)) ||
108
+ existing.base_url ||
109
+ DEFAULT_BASE_URL;
110
+ const email =
111
+ (await prompt(`Your Sonance email${existing.email ? ` [${existing.email}]` : ""}: `)) ||
112
+ existing.email ||
113
+ "";
114
+ if (!email) {
115
+ process.stderr.write("Email is required.\n");
116
+ process.exit(2);
117
+ }
118
+ process.stdout.write(
119
+ "\nPaste a per-agent API key (ak_...) issued from the agent's Permissions tab.\n" +
120
+ "You can have multiple keys; this one will be the default for `athena-agent run`.\n",
121
+ );
122
+ const apiKey =
123
+ (await prompt("API key (ak_...): ", { mask: true })) || existing.api_key || "";
124
+ if (!apiKey || !apiKey.startsWith("ak_")) {
125
+ process.stderr.write("Invalid API key. Must start with ak_.\n");
126
+ process.exit(2);
127
+ }
128
+
129
+ const credsPath = join(homedir(), ".athena", "credentials.json");
130
+ mkdirSync(dirname(credsPath), { recursive: true });
131
+ writeFileSync(
132
+ credsPath,
133
+ JSON.stringify({ base_url: baseUrl, email, api_key: apiKey }, null, 2),
134
+ "utf8",
135
+ );
136
+ chmodSync(credsPath, 0o600);
137
+ process.stdout.write(`\n✓ Saved to ${credsPath}\n`);
138
+ process.stdout.write(" Run an agent with: athena-agent run <agent-id>\n\n");
139
+ }
140
+
141
+ // Return the absolute path to `hermes` if it's findable on PATH, in
142
+ // ~/.local/bin, or in a few other common locations. Returns null if not found.
143
+ async function locateHermes() {
144
+ const userLocalBin = join(homedir(), ".local", "bin", "hermes");
145
+ const candidates = [userLocalBin, "/usr/local/bin/hermes", "/opt/homebrew/bin/hermes"];
146
+ for (const p of candidates) {
147
+ if (existsSync(p)) return p;
148
+ }
149
+ const which = await new Promise((resolve) => {
150
+ const c = spawn("which", ["hermes"], { stdio: ["ignore", "pipe", "ignore"] });
151
+ let out = "";
152
+ c.stdout.on("data", (d) => (out += d.toString()));
153
+ c.on("exit", () => resolve(out.trim()));
154
+ });
155
+ return which || null;
156
+ }
157
+
158
+ async function ensureHermesInstalled() {
159
+ const found = await locateHermes();
160
+ if (found) return found;
161
+
162
+ process.stderr.write(
163
+ "[athena-agent] Hermes is not installed. Installing silently (this is a one-time setup)…\n",
164
+ );
165
+ // `bash -s -- --skip-setup` passes the flag to the install script and
166
+ // suppresses its post-install wizard, so the user only ever sees Athena's UX.
167
+ const code = await new Promise((resolve) => {
168
+ const c = spawn(
169
+ "bash",
170
+ ["-c", "curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --skip-setup"],
171
+ { stdio: "inherit" },
172
+ );
173
+ c.on("exit", (code) => resolve(code ?? 1));
174
+ });
175
+ if (code !== 0) {
176
+ process.stderr.write("[athena-agent] Hermes install failed. See output above.\n");
177
+ return null;
178
+ }
179
+ process.stderr.write("[athena-agent] Hermes installed.\n");
180
+ return await locateHermes();
181
+ }
182
+
183
+ async function fetchManifest({ baseUrl, agentId, apiKey }) {
184
+ const res = await fetch(`${baseUrl}/api/agents/${agentId}/launcher-manifest`, {
185
+ headers: { authorization: `Bearer ${apiKey}` },
186
+ });
187
+ if (!res.ok) {
188
+ const body = await res.text();
189
+ throw new Error(`Manifest fetch failed (${res.status}): ${body}`);
190
+ }
191
+ return res.json();
192
+ }
193
+
194
+ function resolveCommand(manifest, override) {
195
+ // Local override: `--runtime-cmd "node /path/to/run.mjs"` or
196
+ // $ATHENA_RUNTIME_CMD. Used for smoke-testing before the runtime
197
+ // packages are published to npm.
198
+ const ov = override || process.env.ATHENA_RUNTIME_CMD;
199
+ if (ov) {
200
+ const [cmd, ...args] = ov.split(/\s+/).filter(Boolean);
201
+ return { cmd, args };
202
+ }
203
+ const { kind, entrypoint } = manifest.runtime;
204
+ switch (kind) {
205
+ case "anthropic_sdk":
206
+ case "openclaw":
207
+ // npm-distributed runtimes — spawn via npx.
208
+ return { cmd: "npx", args: ["-y", entrypoint] };
209
+ case "hermes":
210
+ // Hermes Agent (Nous Research) is distributed via
211
+ // `curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash`,
212
+ // not npm. The entrypoint is just the binary name on $PATH (`hermes`
213
+ // by default). If the user customized entrypoint to a path, honor that.
214
+ return { cmd: entrypoint || "hermes", args: [] };
215
+ default:
216
+ throw new Error(`Unsupported runtime kind: ${kind}`);
217
+ }
218
+ }
219
+
220
+ async function main() {
221
+ const args = parseArgs(process.argv.slice(2));
222
+
223
+ if (args._[0] === "setup") {
224
+ await runSetup();
225
+ return;
226
+ }
227
+
228
+ if (args.help || args._[0] !== "run" || !args._[1]) {
229
+ usage();
230
+ process.exit(args.help ? 0 : 2);
231
+ }
232
+
233
+ const agentId = args._[1];
234
+ const creds = loadCredsFile();
235
+ const baseUrl =
236
+ args.baseUrl || process.env.ATHENA_BASE_URL || creds.base_url || DEFAULT_BASE_URL;
237
+ const apiKey = args.apiKey || process.env.ATHENA_API_KEY || creds.api_key;
238
+
239
+ if (!apiKey) {
240
+ process.stderr.write(
241
+ "Error: no API key. Pass --api-key ak_..., set $ATHENA_API_KEY, " +
242
+ "or write {\"api_key\": \"ak_...\"} to ~/.athena/credentials.json\n",
243
+ );
244
+ process.exit(2);
245
+ }
246
+
247
+ let manifest;
248
+ try {
249
+ manifest = await fetchManifest({ baseUrl, agentId, apiKey });
250
+ } catch (err) {
251
+ process.stderr.write(`${err.message}\n`);
252
+ process.exit(1);
253
+ }
254
+
255
+ if (args.manifestOnly) {
256
+ process.stdout.write(JSON.stringify(manifest, null, 2) + "\n");
257
+ process.exit(0);
258
+ }
259
+
260
+ let { cmd, args: cmdArgs } = resolveCommand(manifest, args.runtimeCmd);
261
+ // Prepend ~/.local/bin to PATH so newly-installed Hermes (and other tools
262
+ // that drop binaries there) are findable in the spawned child without
263
+ // requiring the user to reload their shell.
264
+ const userLocalBin = join(homedir(), ".local", "bin");
265
+ const augmentedPath = `${userLocalBin}:${process.env.PATH || ""}`;
266
+ let env = {
267
+ ...process.env,
268
+ ...manifest.env,
269
+ PATH: augmentedPath,
270
+ ATHENA_API_KEY: apiKey,
271
+ };
272
+
273
+ // Hermes-specific preflight + pre-configuration:
274
+ // 1. Verify `hermes` is on PATH (give a clear install hint if not).
275
+ // 2. Fetch the rendered cli-config.yaml from Athena and write it to a
276
+ // per-agent state dir.
277
+ // 3. Override HOME to that state dir so Hermes reads our config instead
278
+ // of any pre-existing user config — and skills/memory persist there.
279
+ // 4. Set ANTHROPIC_BASE_URL + ANTHROPIC_API_KEY to the Athena proxy +
280
+ // the per-agent ak_ key. Hermes's anthropic provider picks these up.
281
+ if (manifest.runtime.kind === "hermes" && cmd === "hermes") {
282
+ const hermesPath = await ensureHermesInstalled();
283
+ if (!hermesPath) process.exit(127);
284
+ // Use the absolute path we resolved so spawn() doesn't fail with ENOENT
285
+ // when ~/.local/bin isn't on the parent's PATH yet.
286
+ cmd = hermesPath;
287
+
288
+ // Hermes data directory — respects $HERMES_HOME, defaults to ~/.hermes.
289
+ // We give each Athena agent its own HERMES_HOME so configs, skills, and
290
+ // memory stay isolated. The python code path (~/.hermes/hermes-agent) is
291
+ // resolved by the launcher script at install time, so HERMES_HOME only
292
+ // affects where Hermes looks for runtime data — not its own code.
293
+ const hermesHome = join(homedir(), ".athena", "hermes", agentId);
294
+ mkdirSync(hermesHome, { recursive: true });
295
+
296
+ const cfgRes = await fetch(
297
+ `${baseUrl}/api/agents/${agentId}/hermes-config`,
298
+ { headers: { authorization: `Bearer ${apiKey}` } },
299
+ );
300
+ if (!cfgRes.ok) {
301
+ process.stderr.write(
302
+ `[athena-agent] failed to fetch hermes config (${cfgRes.status}): ${await cfgRes.text()}\n`,
303
+ );
304
+ process.exit(1);
305
+ }
306
+ const yaml = await cfgRes.text();
307
+ // Hermes reads config from $HERMES_HOME/config.yaml at runtime.
308
+ // The .example file in the repo is named cli-config.yaml.example but gets
309
+ // installed as config.yaml — we have to match that.
310
+ writeFileSync(join(hermesHome, "config.yaml"), yaml, "utf8");
311
+ process.stderr.write(`[athena-agent] wrote hermes config to ${hermesHome}/config.yaml\n`);
312
+
313
+ const anthropicBase =
314
+ manifest.env?.ATHENA_ANTHROPIC_URL || manifest.gateway_url;
315
+ env = {
316
+ ...env,
317
+ HERMES_HOME: hermesHome,
318
+ ANTHROPIC_BASE_URL: anthropicBase,
319
+ ANTHROPIC_API_KEY: apiKey,
320
+ };
321
+ }
322
+
323
+ process.stderr.write(
324
+ `[athena-agent] spawning ${manifest.runtime.kind}@${manifest.runtime.version} ` +
325
+ `for agent ${agentId}\n`,
326
+ );
327
+
328
+ const child = spawn(cmd, cmdArgs, { stdio: "inherit", env });
329
+ child.on("exit", (code, signal) => {
330
+ if (signal) process.kill(process.pid, signal);
331
+ else process.exit(code ?? 0);
332
+ });
333
+ }
334
+
335
+ main().catch((err) => {
336
+ process.stderr.write(`${err.stack || err.message}\n`);
337
+ process.exit(1);
338
+ });
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "athena-agent-launcher",
3
+ "version": "0.1.0",
4
+ "description": "Run an Athena-configured agent locally. Resolves runtime + bindings from the Athena control plane and spawns the correct agent binary (Anthropic SDK, OpenClaw, or Hermes).",
5
+ "type": "module",
6
+ "bin": {
7
+ "athena-agent": "bin/launcher.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/sonance-tech/prometheus.git"
19
+ },
20
+ "keywords": [
21
+ "athena",
22
+ "sonance",
23
+ "ai-agent",
24
+ "anthropic",
25
+ "hermes"
26
+ ],
27
+ "license": "MIT",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
31
+ }