@saleso.innovations/bridge 0.1.11 → 0.1.12

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 CHANGED
@@ -34,7 +34,7 @@ This installs the latest package, refreshes the CLI symlink, and restarts `cleos
34
34
  To pin a specific release (e.g. after a Cleos update):
35
35
 
36
36
  ```bash
37
- curl -fsSL https://amicable-elephant-407.convex.site/update-bridge.sh | bash -s -- 0.1.11
37
+ curl -fsSL https://amicable-elephant-407.convex.site/update-bridge.sh | bash -s -- 0.1.12
38
38
  ```
39
39
 
40
40
  ## Manual usage
@@ -1 +1 @@
1
- {"version":3,"file":"ensureHermesApi.d.ts","sourceRoot":"","sources":["../src/ensureHermesApi.ts"],"names":[],"mappings":"AA8EA,wBAAsB,yBAAyB,IAAI,OAAO,CAAC;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC,CA0BD;AAkDD,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAsB5D"}
1
+ {"version":3,"file":"ensureHermesApi.d.ts","sourceRoot":"","sources":["../src/ensureHermesApi.ts"],"names":[],"mappings":"AAsEA,wBAAsB,yBAAyB,IAAI,OAAO,CAAC;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC,CA0BD;AAED,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAsB5D"}
@@ -1,15 +1,8 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { execFile, spawn } from "node:child_process";
3
2
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
4
3
  import { homedir } from "node:os";
5
4
  import { join } from "node:path";
6
- import { promisify } from "node:util";
7
- import { DEFAULT_HERMES_API_URL } from "./constants.js";
8
- const execFileAsync = promisify(execFile);
9
5
  const HERMES_ENV_PATH = join(homedir(), ".hermes", ".env");
10
- const HEALTH_URL = DEFAULT_HERMES_API_URL.replace(/\/v1\/chat\/completions\/?$/, "/health");
11
- const HEALTH_POLL_MS = 1_000;
12
- const HEALTH_TIMEOUT_MS = 30_000;
13
6
  function parseEnvLine(line) {
14
7
  const trimmed = line.trim();
15
8
  if (!trimmed)
@@ -89,70 +82,26 @@ export async function ensureHermesApiConfigured() {
89
82
  }
90
83
  return { envPath: HERMES_ENV_PATH, changed, apiKey };
91
84
  }
92
- async function fetchHermesHealth(apiKey) {
93
- const headers = {};
94
- if (apiKey)
95
- headers.authorization = `Bearer ${apiKey}`;
96
- try {
97
- const response = await fetch(HEALTH_URL, { headers, signal: AbortSignal.timeout(2_000) });
98
- return response.ok;
99
- }
100
- catch {
101
- return false;
102
- }
103
- }
104
- async function tryRestartHermesGateway() {
105
- const units = ["hermes-gateway.service", "hermes.service", "hermes-gateway", "hermes"];
106
- for (const unit of units) {
107
- try {
108
- await execFileAsync("systemctl", ["try-restart", unit], { timeout: 10_000 });
109
- }
110
- catch {
111
- // Unit may not exist — keep trying other names.
112
- }
113
- }
114
- }
115
- async function tryStartHermesGateway() {
116
- await new Promise((resolve) => {
117
- try {
118
- const child = spawn("hermes", ["gateway"], {
119
- detached: true,
120
- stdio: "ignore",
121
- });
122
- child.on("error", () => resolve());
123
- child.unref();
124
- resolve();
125
- }
126
- catch {
127
- resolve();
128
- }
129
- });
130
- }
131
- async function waitForHermesHealth(apiKey) {
132
- const deadline = Date.now() + HEALTH_TIMEOUT_MS;
133
- while (Date.now() < deadline) {
134
- if (await fetchHermesHealth(apiKey))
135
- return true;
136
- await new Promise((resolve) => setTimeout(resolve, HEALTH_POLL_MS));
137
- }
138
- return false;
139
- }
140
85
  export async function prepareHermesForBridge() {
141
- const { envPath, changed, apiKey } = await ensureHermesApiConfigured();
86
+ const { envPath, changed } = await ensureHermesApiConfigured();
87
+ const { startHermesGateway } = await import("./gatewayControl.js");
142
88
  if (changed) {
143
89
  console.log(`Configured Hermes API in ${envPath}`);
144
90
  }
145
- if (await fetchHermesHealth(apiKey))
146
- return;
147
- if (changed) {
148
- console.log("Restarting Hermes gateway to apply API settings...");
149
- await tryRestartHermesGateway();
150
- if (await waitForHermesHealth(apiKey))
91
+ try {
92
+ const result = await startHermesGateway();
93
+ if (result.alreadyRunning && !changed) {
151
94
  return;
95
+ }
96
+ if (changed) {
97
+ console.log("Restarting Hermes gateway to apply API settings...");
98
+ }
99
+ else if (!result.alreadyRunning) {
100
+ console.log("Hermes gateway not detected — starting `hermes gateway`...");
101
+ }
102
+ }
103
+ catch (error) {
104
+ const message = error instanceof Error ? error.message : String(error);
105
+ throw new Error(message);
152
106
  }
153
- console.log("Hermes gateway not detected — starting `hermes gateway`...");
154
- await tryStartHermesGateway();
155
- if (await waitForHermesHealth(apiKey))
156
- return;
157
- throw new Error("Hermes is not running. Install Hermes, then run `hermes gateway` once on this machine and retry.");
158
107
  }
@@ -0,0 +1,12 @@
1
+ export declare function startHermesGateway(): Promise<{
2
+ ok: true;
3
+ alreadyRunning: boolean;
4
+ }>;
5
+ export declare function stopHermesGateway(): Promise<{
6
+ ok: true;
7
+ wasRunning: boolean;
8
+ }>;
9
+ export declare function restartHermesGateway(): Promise<{
10
+ ok: true;
11
+ }>;
12
+ //# sourceMappingURL=gatewayControl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gatewayControl.d.ts","sourceRoot":"","sources":["../src/gatewayControl.ts"],"names":[],"mappings":"AA8EA,wBAAsB,kBAAkB,IAAI,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,cAAc,EAAE,OAAO,CAAA;CAAE,CAAC,CAezF;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAAC,CAiBpF;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAAC,CAyBlE"}
@@ -0,0 +1,119 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { DEFAULT_HERMES_API_URL } from "./constants.js";
4
+ import { ensureHermesApiConfigured } from "./ensureHermesApi.js";
5
+ const execFileAsync = promisify(execFile);
6
+ const HEALTH_URL = DEFAULT_HERMES_API_URL.replace(/\/v1\/chat\/completions\/?$/, "/health");
7
+ const HEALTH_POLL_MS = 1_000;
8
+ const HEALTH_TIMEOUT_MS = 30_000;
9
+ const GATEWAY_UNITS = ["hermes-gateway.service", "hermes.service", "hermes-gateway", "hermes"];
10
+ async function fetchHermesHealth(apiKey) {
11
+ const headers = {};
12
+ if (apiKey)
13
+ headers.authorization = `Bearer ${apiKey}`;
14
+ try {
15
+ const response = await fetch(HEALTH_URL, { headers, signal: AbortSignal.timeout(2_000) });
16
+ return response.ok;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ async function waitForHermesHealth(apiKey) {
23
+ const deadline = Date.now() + HEALTH_TIMEOUT_MS;
24
+ while (Date.now() < deadline) {
25
+ if (await fetchHermesHealth(apiKey))
26
+ return true;
27
+ await new Promise((resolve) => setTimeout(resolve, HEALTH_POLL_MS));
28
+ }
29
+ return false;
30
+ }
31
+ async function tryRestartHermesGatewayUnits() {
32
+ for (const unit of GATEWAY_UNITS) {
33
+ try {
34
+ await execFileAsync("systemctl", ["try-restart", unit], { timeout: 10_000 });
35
+ }
36
+ catch {
37
+ // Unit may not exist — keep trying other names.
38
+ }
39
+ }
40
+ }
41
+ async function tryStopHermesGatewayUnits() {
42
+ for (const unit of GATEWAY_UNITS) {
43
+ try {
44
+ await execFileAsync("systemctl", ["stop", unit], { timeout: 10_000 });
45
+ }
46
+ catch {
47
+ // Unit may not exist — keep trying other names.
48
+ }
49
+ }
50
+ }
51
+ async function tryKillHermesGatewayProcess() {
52
+ try {
53
+ await execFileAsync("pkill", ["-f", "hermes gateway"], { timeout: 5_000 });
54
+ }
55
+ catch {
56
+ // No matching process — that's fine.
57
+ }
58
+ }
59
+ async function spawnHermesGateway() {
60
+ await new Promise((resolve) => {
61
+ try {
62
+ const child = spawn("hermes", ["gateway"], {
63
+ detached: true,
64
+ stdio: "ignore",
65
+ });
66
+ child.on("error", () => resolve());
67
+ child.unref();
68
+ resolve();
69
+ }
70
+ catch {
71
+ resolve();
72
+ }
73
+ });
74
+ }
75
+ export async function startHermesGateway() {
76
+ const { apiKey } = await ensureHermesApiConfigured();
77
+ if (await fetchHermesHealth(apiKey)) {
78
+ return { ok: true, alreadyRunning: true };
79
+ }
80
+ await spawnHermesGateway();
81
+ if (await waitForHermesHealth(apiKey)) {
82
+ return { ok: true, alreadyRunning: false };
83
+ }
84
+ throw new Error("Hermes gateway did not become healthy. Install Hermes, then run `hermes gateway` on this machine and retry.");
85
+ }
86
+ export async function stopHermesGateway() {
87
+ const { apiKey } = await ensureHermesApiConfigured();
88
+ const wasRunning = await fetchHermesHealth(apiKey);
89
+ await tryStopHermesGatewayUnits();
90
+ await tryKillHermesGatewayProcess();
91
+ if (wasRunning) {
92
+ await new Promise((resolve) => setTimeout(resolve, HEALTH_POLL_MS));
93
+ }
94
+ const stillRunning = await fetchHermesHealth(apiKey);
95
+ if (stillRunning) {
96
+ throw new Error("Hermes gateway is still running. Stop it manually on the agent machine.");
97
+ }
98
+ return { ok: true, wasRunning };
99
+ }
100
+ export async function restartHermesGateway() {
101
+ const { changed, apiKey } = await ensureHermesApiConfigured();
102
+ if (changed) {
103
+ await tryRestartHermesGatewayUnits();
104
+ if (await waitForHermesHealth(apiKey)) {
105
+ return { ok: true };
106
+ }
107
+ }
108
+ if (await fetchHermesHealth(apiKey)) {
109
+ await tryRestartHermesGatewayUnits();
110
+ if (await waitForHermesHealth(apiKey)) {
111
+ return { ok: true };
112
+ }
113
+ }
114
+ await spawnHermesGateway();
115
+ if (await waitForHermesHealth(apiKey)) {
116
+ return { ok: true };
117
+ }
118
+ throw new Error("Hermes gateway did not restart successfully. Run `hermes gateway` on this machine and retry.");
119
+ }
@@ -1,4 +1,4 @@
1
- export declare const HERMES_COMMAND_NAMES: readonly ["runtime.health", "runtime.detailedHealth", "runtime.capabilities", "models.list", "model.set", "responses.create", "runs.create", "runs.status", "runs.stop", "jobs.list", "jobs.get", "jobs.create", "jobs.update", "jobs.pause", "jobs.resume", "jobs.runNow", "jobs.delete", "profiles.list", "profiles.create"];
1
+ export declare const HERMES_COMMAND_NAMES: readonly ["runtime.health", "runtime.detailedHealth", "runtime.capabilities", "models.list", "model.set", "responses.create", "runs.create", "runs.status", "runs.stop", "jobs.list", "jobs.get", "jobs.create", "jobs.update", "jobs.pause", "jobs.resume", "jobs.runNow", "jobs.delete", "profiles.list", "profiles.create", "gateway.start", "gateway.stop", "gateway.restart"];
2
2
  export type HermesCommandName = (typeof HERMES_COMMAND_NAMES)[number];
3
3
  export declare function isHermesCommandName(value: string): value is HermesCommandName;
4
4
  export type HermesCommandErrorCode = "command_unsupported" | "hermes_unreachable" | "hermes_request_failed" | "invalid_command_args" | "unsupported_by_http";
@@ -1 +1 @@
1
- {"version":3,"file":"hermesCommands.d.ts","sourceRoot":"","sources":["../src/hermesCommands.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,oBAAoB,gUAoBvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,iBAAiB,CAE7E;AAED,MAAM,MAAM,sBAAsB,GAC9B,qBAAqB,GACrB,oBAAoB,GACpB,uBAAuB,GACvB,sBAAsB,GACtB,qBAAqB,CAAC;AAE1B,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;gBAE1B,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM;CAI1D;AAuFD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GACjE,OAAO,CAAC,OAAO,CAAC,CAmJlB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAetG"}
1
+ {"version":3,"file":"hermesCommands.d.ts","sourceRoot":"","sources":["../src/hermesCommands.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,oXAuBvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,iBAAiB,CAE7E;AAED,MAAM,MAAM,sBAAsB,GAC9B,qBAAqB,GACrB,oBAAoB,GACpB,uBAAuB,GACvB,sBAAsB,GACtB,qBAAqB,CAAC;AAE1B,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;gBAE1B,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM;CAI1D;AAuFD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GACjE,OAAO,CAAC,OAAO,CAAC,CAyJlB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAetG"}
@@ -1,4 +1,5 @@
1
1
  import { resolveHermesApiConfig } from "./hermesForwarder.js";
2
+ import { restartHermesGateway, startHermesGateway, stopHermesGateway } from "./gatewayControl.js";
2
3
  export const HERMES_COMMAND_NAMES = [
3
4
  "runtime.health",
4
5
  "runtime.detailedHealth",
@@ -19,6 +20,9 @@ export const HERMES_COMMAND_NAMES = [
19
20
  "jobs.delete",
20
21
  "profiles.list",
21
22
  "profiles.create",
23
+ "gateway.start",
24
+ "gateway.stop",
25
+ "gateway.restart",
22
26
  ];
23
27
  export function isHermesCommandName(value) {
24
28
  return HERMES_COMMAND_NAMES.includes(value);
@@ -234,6 +238,12 @@ export async function executeHermesCommand(command, args, options = {}) {
234
238
  throw new HermesCommandError("unsupported_by_http", "Listing Hermes profiles is not exposed over the HTTP API.");
235
239
  case "profiles.create":
236
240
  throw new HermesCommandError("unsupported_by_http", "Creating Hermes profiles is not exposed over the HTTP API. Run `hermes profile create` on the agent machine.");
241
+ case "gateway.start":
242
+ return await startHermesGateway();
243
+ case "gateway.stop":
244
+ return await stopHermesGateway();
245
+ case "gateway.restart":
246
+ return await restartHermesGateway();
237
247
  default: {
238
248
  const _exhaustive = command;
239
249
  throw new HermesCommandError("command_unsupported", `Unsupported command: ${String(_exhaustive)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saleso.innovations/bridge",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Connect your Hermes agent to the Cleos iOS app via pairing code.",
5
5
  "type": "module",
6
6
  "license": "MIT",