@saleso.innovations/bridge 0.1.0 → 0.1.4
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 +10 -1
- package/dist/cli.js +8 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +5 -3
- package/dist/ensureHermesApi.d.ts +7 -0
- package/dist/ensureHermesApi.d.ts.map +1 -0
- package/dist/ensureHermesApi.js +158 -0
- package/dist/hermesForwarder.js +1 -1
- package/dist/normalizePairingCode.d.ts +3 -0
- package/dist/normalizePairingCode.d.ts.map +1 -0
- package/dist/normalizePairingCode.js +4 -0
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,10 +13,19 @@ curl -fsSL https://amicable-elephant-407.convex.site/install-bridge.sh | bash -s
|
|
|
13
13
|
Requirements:
|
|
14
14
|
|
|
15
15
|
- Node.js 20+
|
|
16
|
-
- Hermes
|
|
16
|
+
- Hermes installed on the same machine (`hermes gateway` — the install script configures the API automatically)
|
|
17
17
|
|
|
18
18
|
## Manual usage
|
|
19
19
|
|
|
20
|
+
The install script puts the bridge in `~/.cleos/bridge` (no PATH setup needed). To run manually:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
node ~/.cleos/bridge/node_modules/@saleso.innovations/bridge/dist/cli.js pair ABCD1234
|
|
24
|
+
node ~/.cleos/bridge/node_modules/@saleso.innovations/bridge/dist/cli.js start
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or install globally if you prefer:
|
|
28
|
+
|
|
20
29
|
```bash
|
|
21
30
|
npm install -g @saleso.innovations/bridge
|
|
22
31
|
cleos-bridge connect ABCD1234 # first-time pairing
|
package/dist/cli.js
CHANGED
|
@@ -2,18 +2,23 @@
|
|
|
2
2
|
import { credentialsPathForDisplay, loadCredentials } from "./credentials.js";
|
|
3
3
|
import { connectHermesAgent, pairCleosAgent, reconnectHermesAgent } from "./client.js";
|
|
4
4
|
import { runBridgeDaemon } from "./daemon.js";
|
|
5
|
+
import { prepareHermesForBridge } from "./ensureHermesApi.js";
|
|
5
6
|
import { createHermesMessageHandler } from "./hermesForwarder.js";
|
|
6
7
|
import { convexSiteUrlFromEnv } from "./resolve.js";
|
|
8
|
+
import { normalizePairingCode } from "./normalizePairingCode.js";
|
|
7
9
|
async function main() {
|
|
8
10
|
const [, , command, codeArg] = process.argv;
|
|
9
11
|
const onUserMessage = createHermesMessageHandler();
|
|
12
|
+
if (command === "pair" || command === "connect" || command === "start" || command === "reconnect") {
|
|
13
|
+
await prepareHermesForBridge();
|
|
14
|
+
}
|
|
10
15
|
if (command === "pair") {
|
|
11
16
|
if (!codeArg) {
|
|
12
17
|
printUsage();
|
|
13
18
|
process.exit(1);
|
|
14
19
|
}
|
|
15
20
|
const payload = await pairCleosAgent({
|
|
16
|
-
code: codeArg
|
|
21
|
+
code: normalizePairingCode(codeArg),
|
|
17
22
|
convexSiteUrl: convexSiteUrlFromEnv(),
|
|
18
23
|
});
|
|
19
24
|
console.log(`Paired agent ${payload.agentId}.`);
|
|
@@ -26,7 +31,7 @@ async function main() {
|
|
|
26
31
|
process.exit(1);
|
|
27
32
|
}
|
|
28
33
|
const session = await connectHermesAgent({
|
|
29
|
-
code: codeArg
|
|
34
|
+
code: normalizePairingCode(codeArg),
|
|
30
35
|
convexSiteUrl: convexSiteUrlFromEnv(),
|
|
31
36
|
onUserMessage,
|
|
32
37
|
});
|
|
@@ -45,7 +50,7 @@ async function main() {
|
|
|
45
50
|
console.error("No saved credentials. Run: cleos-bridge connect <CODE>");
|
|
46
51
|
process.exit(1);
|
|
47
52
|
}
|
|
48
|
-
await runBridgeDaemon({ code: codeArg
|
|
53
|
+
await runBridgeDaemon({ code: normalizePairingCode(codeArg), convexSiteUrl: convexSiteUrlFromEnv() });
|
|
49
54
|
return;
|
|
50
55
|
}
|
|
51
56
|
if (command === "reconnect") {
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoC,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoC,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAIhG,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACrG,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACpD,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB,CAAC;AA8HF,wBAAsB,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,eAAe,CAAC,GAAG,OAAO,CAAC;IAC5F,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC,CA0BD;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAcxF;AAED,wBAAsB,oBAAoB,CAAC,OAAO,GAAE;IAClD,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,cAAc,CAAC,eAAe,CAAC,CAAC;CAC5C,GAAG,OAAO,CAAC,aAAa,CAAC,CAc9B"}
|
package/dist/client.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import WebSocket from "ws";
|
|
3
3
|
import { saveCredentials, loadCredentials } from "./credentials.js";
|
|
4
4
|
import { convexSiteUrlFromEnv, resolvePairingCode } from "./resolve.js";
|
|
5
|
+
import { normalizePairingCode } from "./normalizePairingCode.js";
|
|
5
6
|
function createReplySender(ws, agentId, conversationId, messageId) {
|
|
6
7
|
return {
|
|
7
8
|
delta(text, sequence) {
|
|
@@ -82,17 +83,18 @@ async function openAgentConnection(options) {
|
|
|
82
83
|
};
|
|
83
84
|
}
|
|
84
85
|
async function resolveRelayTargets(options) {
|
|
86
|
+
const normalizedCode = normalizePairingCode(options.code);
|
|
85
87
|
if (options.relayHttpBaseUrl && options.relayWsUrl) {
|
|
86
88
|
return {
|
|
87
|
-
code:
|
|
89
|
+
code: normalizedCode,
|
|
88
90
|
expiresAt: Date.now() + 60_000,
|
|
89
91
|
relayHttpUrl: options.relayHttpBaseUrl,
|
|
90
92
|
relayWsUrl: options.relayWsUrl,
|
|
91
|
-
connectCommand: `cleos-bridge connect ${
|
|
93
|
+
connectCommand: `cleos-bridge connect ${normalizedCode}`,
|
|
92
94
|
};
|
|
93
95
|
}
|
|
94
96
|
const convexSiteUrl = options.convexSiteUrl ?? convexSiteUrlFromEnv();
|
|
95
|
-
return await resolvePairingCode(convexSiteUrl,
|
|
97
|
+
return await resolvePairingCode(convexSiteUrl, normalizedCode);
|
|
96
98
|
}
|
|
97
99
|
export async function pairCleosAgent(options) {
|
|
98
100
|
const pairing = await resolveRelayTargets(options);
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { execFile, spawn } from "node:child_process";
|
|
3
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
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
|
+
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
|
+
function parseEnvLine(line) {
|
|
14
|
+
const trimmed = line.trim();
|
|
15
|
+
if (!trimmed)
|
|
16
|
+
return { kind: "blank" };
|
|
17
|
+
if (trimmed.startsWith("#"))
|
|
18
|
+
return { kind: "comment", text: line };
|
|
19
|
+
const eq = line.indexOf("=");
|
|
20
|
+
if (eq <= 0)
|
|
21
|
+
return { kind: "comment", text: line };
|
|
22
|
+
const key = line.slice(0, eq).trim();
|
|
23
|
+
const value = line.slice(eq + 1).trim();
|
|
24
|
+
return { kind: "assignment", key, value, raw: line };
|
|
25
|
+
}
|
|
26
|
+
function serializeEnvLine(line) {
|
|
27
|
+
if (line.kind === "blank")
|
|
28
|
+
return "";
|
|
29
|
+
if (line.kind === "comment")
|
|
30
|
+
return line.text;
|
|
31
|
+
return `${line.key}=${line.value}`;
|
|
32
|
+
}
|
|
33
|
+
function generateApiKey() {
|
|
34
|
+
return randomBytes(24).toString("base64url");
|
|
35
|
+
}
|
|
36
|
+
async function readHermesEnvLines() {
|
|
37
|
+
try {
|
|
38
|
+
await access(HERMES_ENV_PATH);
|
|
39
|
+
const contents = await readFile(HERMES_ENV_PATH, "utf8");
|
|
40
|
+
return contents.split("\n").map(parseEnvLine);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function getAssignment(lines, key) {
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
if (line.kind === "assignment" && line.key === key) {
|
|
49
|
+
return line.value.replace(/^["']|["']$/g, "");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
function upsertAssignment(lines, key, value) {
|
|
55
|
+
let found = false;
|
|
56
|
+
const next = lines.map((line) => {
|
|
57
|
+
if (line.kind !== "assignment" || line.key !== key)
|
|
58
|
+
return line;
|
|
59
|
+
found = true;
|
|
60
|
+
return { kind: "assignment", key, value, raw: `${key}=${value}` };
|
|
61
|
+
});
|
|
62
|
+
if (!found) {
|
|
63
|
+
if (next.length > 0 && next[next.length - 1]?.kind !== "blank") {
|
|
64
|
+
next.push({ kind: "blank" });
|
|
65
|
+
}
|
|
66
|
+
next.push({ kind: "assignment", key, value, raw: `${key}=${value}` });
|
|
67
|
+
}
|
|
68
|
+
return next;
|
|
69
|
+
}
|
|
70
|
+
export async function ensureHermesApiConfigured() {
|
|
71
|
+
await mkdir(join(homedir(), ".hermes"), { recursive: true });
|
|
72
|
+
const before = await readHermesEnvLines();
|
|
73
|
+
const enabled = getAssignment(before, "API_SERVER_ENABLED");
|
|
74
|
+
const existingKey = getAssignment(before, "API_SERVER_KEY");
|
|
75
|
+
const apiKey = existingKey && existingKey.length >= 8 ? existingKey : generateApiKey();
|
|
76
|
+
let lines = before;
|
|
77
|
+
let changed = false;
|
|
78
|
+
if (enabled !== "true") {
|
|
79
|
+
lines = upsertAssignment(lines, "API_SERVER_ENABLED", "true");
|
|
80
|
+
changed = true;
|
|
81
|
+
}
|
|
82
|
+
if (!existingKey || existingKey.length < 8) {
|
|
83
|
+
lines = upsertAssignment(lines, "API_SERVER_KEY", apiKey);
|
|
84
|
+
changed = true;
|
|
85
|
+
}
|
|
86
|
+
if (changed || before.length === 0) {
|
|
87
|
+
const body = lines.map(serializeEnvLine).join("\n");
|
|
88
|
+
await writeFile(HERMES_ENV_PATH, body.endsWith("\n") ? body : `${body}\n`, "utf8");
|
|
89
|
+
}
|
|
90
|
+
return { envPath: HERMES_ENV_PATH, changed, apiKey };
|
|
91
|
+
}
|
|
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
|
+
export async function prepareHermesForBridge() {
|
|
141
|
+
const { envPath, changed, apiKey } = await ensureHermesApiConfigured();
|
|
142
|
+
if (changed) {
|
|
143
|
+
console.log(`Configured Hermes API in ${envPath}`);
|
|
144
|
+
}
|
|
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))
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
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
|
+
}
|
package/dist/hermesForwarder.js
CHANGED
|
@@ -43,7 +43,7 @@ async function ensureHermesReachable(apiUrl, apiKey) {
|
|
|
43
43
|
}
|
|
44
44
|
catch (error) {
|
|
45
45
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
46
|
-
throw new Error(`Hermes is not reachable at ${healthUrl}.
|
|
46
|
+
throw new Error(`Hermes is not reachable at ${healthUrl}. Ensure Hermes is installed and run \`hermes gateway\` on this machine. ${message}`);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
function parseSseDataLines(buffer) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalizePairingCode.d.ts","sourceRoot":"","sources":["../src/normalizePairingCode.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzD"}
|
package/dist/resolve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,wBAAgB,oBAAoB,IAAI,MAAM,CAM7C;AAED,wBAAsB,kBAAkB,CAAC,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAQlG"}
|
package/dist/resolve.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_CLEOS_CONVEX_SITE_URL } from "./constants.js";
|
|
2
|
+
import { normalizePairingCode } from "./normalizePairingCode.js";
|
|
2
3
|
export function convexSiteUrlFromEnv() {
|
|
3
4
|
const configured = process.env.CLEOS_CONVEX_SITE_URL?.trim() ||
|
|
4
5
|
process.env.CONVEX_SITE_URL?.trim() ||
|
|
@@ -6,7 +7,7 @@ export function convexSiteUrlFromEnv() {
|
|
|
6
7
|
return configured.replace(/\/$/, "");
|
|
7
8
|
}
|
|
8
9
|
export async function resolvePairingCode(convexSiteUrl, code) {
|
|
9
|
-
const normalized = code
|
|
10
|
+
const normalized = normalizePairingCode(code);
|
|
10
11
|
const response = await fetch(`${convexSiteUrl.replace(/\/$/, "")}/pairing/${normalized}`);
|
|
11
12
|
if (!response.ok) {
|
|
12
13
|
const body = await response.text();
|