@saleso.innovations/bridge 0.1.6 → 0.1.8

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/INTEGRATION.md CHANGED
@@ -55,14 +55,28 @@ export async function resumeCleosConnection() {
55
55
 
56
56
  ## Cron job delivery
57
57
 
58
- When a Hermes cron job completes and should appear in the Cleos **Jobs** tab (not chat), deliver the result over the existing agent WebSocket:
58
+ Hermes writes every cron run to `~/.hermes/cron/output/{job_id}/{timestamp}.md`. The bridge watches that folder automatically and forwards new results to Cleos as Jobs (not chat).
59
+
60
+ No Hermes changes are required. Keep `cleos-bridge start` running on the VPS.
61
+
62
+ When a cron job completes:
63
+
64
+ 1. Hermes saves output under `~/.hermes/cron/output/`
65
+ 2. The bridge detects the new file
66
+ 3. The bridge sends `agent.message` with `metadata.cron` over the Cleos WebSocket
67
+ 4. Cleos routes it to the Jobs tab
68
+
69
+ The bridge resolves the Cleos `conversationId` from saved credentials or Convex (`/bridge/active-conversation`).
70
+
71
+ ### Manual delivery (optional)
72
+
73
+ For custom integrations you can still call:
59
74
 
60
75
  ```typescript
61
76
  import { reconnectHermesAgent } from "@saleso.innovations/bridge";
62
77
 
63
78
  const connection = await reconnectHermesAgent({ onUserMessage: handleCleosMessage });
64
79
 
65
- // On cron completion — conversationId is the active Cleos conversation for this agent.
66
80
  connection.deliverCronResult(finalText, {
67
81
  conversationId: "<convex-conversation-id>",
68
82
  jobId: job.id,
@@ -71,10 +85,6 @@ connection.deliverCronResult(finalText, {
71
85
  });
72
86
  ```
73
87
 
74
- Cleos routes messages with `metadata.cron` directly to the Jobs inbox. Hermes default cron wrappers (`Cronjob Response: ...`) are also detected automatically.
75
-
76
- Store the Cleos `conversationId` when pairing (fetch via Convex `conversations:getActive` for the new `agentId`) so cron delivery has a valid target.
77
-
78
88
  ## What Hermes handles
79
89
 
80
90
  - Pairing UI (code entry)
@@ -94,7 +104,7 @@ Codes expire after 10 minutes. Each code links **one** Hermes instance to **one*
94
104
 
95
105
  ## Credentials
96
106
 
97
- After first pairing, credentials are saved to `~/.cleos/agent.json`. Use `reconnectHermesAgent()` on restart — no new code required.
107
+ After first pairing, credentials are saved to `~/.cleos/agent.json`. Delivered cron files are tracked in `~/.cleos/cron-delivered.json` so restarts do not duplicate Jobs. Use `reconnectHermesAgent()` on restart — no new pairing code required.
98
108
 
99
109
  ## Self-hosting Cleos
100
110
 
package/README.md CHANGED
@@ -15,9 +15,25 @@ Requirements:
15
15
  - Node.js 20+
16
16
  - Hermes installed on the same machine (`hermes gateway` — the install script configures the API automatically)
17
17
 
18
+ ## Update
19
+
20
+ After the first install, updates are one command:
21
+
22
+ ```bash
23
+ cleos-bridge update
24
+ ```
25
+
26
+ The install script links `cleos-bridge` to `/usr/local/bin` (root) or `~/.local/bin` (non-root). If that command is not found yet, use:
27
+
28
+ ```bash
29
+ curl -fsSL https://amicable-elephant-407.convex.site/update-bridge.sh | bash
30
+ ```
31
+
32
+ This installs the latest package, refreshes the CLI symlink, and restarts `cleos-bridge.service` when present.
33
+
18
34
  ## Manual usage
19
35
 
20
- The install script puts the bridge in `~/.cleos/bridge` (no PATH setup needed). To run manually:
36
+ The install script puts the bridge in `~/.cleos/bridge`. To run without the symlink:
21
37
 
22
38
  ```bash
23
39
  node ~/.cleos/bridge/node_modules/@saleso.innovations/bridge/dist/cli.js pair ABCD1234
@@ -34,6 +50,8 @@ cleos-bridge start # reconnecting daemon (systemd)
34
50
 
35
51
  Credentials are saved to `~/.cleos/agent.json`.
36
52
 
53
+ The daemon watches `~/.hermes/cron/output/` and forwards new cron results to the Cleos Jobs tab automatically.
54
+
37
55
  ## Hermes API
38
56
 
39
57
  By default the bridge forwards chat to Hermes at `http://127.0.0.1:8642/v1/chat/completions`.
@@ -0,0 +1,4 @@
1
+ import { type SavedAgentCredentials } from "./credentials.js";
2
+ export declare function resolveActiveConversationId(credentials?: SavedAgentCredentials): Promise<string>;
3
+ export declare function rememberConversationId(conversationId: string): void;
4
+ //# sourceMappingURL=activeConversation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activeConversation.d.ts","sourceRoot":"","sources":["../src/activeConversation.ts"],"names":[],"mappings":"AAEA,OAAO,EAAqC,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAEjG,wBAAsB,2BAA2B,CAC/C,WAAW,CAAC,EAAE,qBAAqB,GAClC,OAAO,CAAC,MAAM,CAAC,CA+BjB;AAED,wBAAgB,sBAAsB,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAEnE"}
@@ -0,0 +1,34 @@
1
+ import { convexSiteUrlFromEnv } from "./resolve.js";
2
+ import { hashToken } from "./token.js";
3
+ import { loadCredentials, patchCredentials } from "./credentials.js";
4
+ export async function resolveActiveConversationId(credentials) {
5
+ const resolved = credentials ?? loadCredentials();
6
+ if (!resolved) {
7
+ throw new Error("No saved Cleos agent credentials");
8
+ }
9
+ if (resolved.conversationId) {
10
+ return resolved.conversationId;
11
+ }
12
+ const convexSiteUrl = resolved.convexSiteUrl ?? convexSiteUrlFromEnv();
13
+ const response = await fetch(`${convexSiteUrl.replace(/\/$/, "")}/bridge/active-conversation`, {
14
+ method: "POST",
15
+ headers: { "content-type": "application/json" },
16
+ body: JSON.stringify({
17
+ agentId: resolved.agentId,
18
+ tokenHash: hashToken(resolved.agentToken),
19
+ }),
20
+ });
21
+ if (!response.ok) {
22
+ const body = await response.text();
23
+ throw new Error(`Active conversation lookup failed (${response.status}): ${body}`);
24
+ }
25
+ const payload = (await response.json());
26
+ if (!payload.conversationId) {
27
+ throw new Error("Active conversation lookup returned no conversationId");
28
+ }
29
+ patchCredentials({ conversationId: payload.conversationId });
30
+ return payload.conversationId;
31
+ }
32
+ export function rememberConversationId(conversationId) {
33
+ patchCredentials({ conversationId });
34
+ }
@@ -0,0 +1,7 @@
1
+ export declare const BRIDGE_PACKAGE = "@saleso.innovations/bridge";
2
+ export declare function defaultBridgeInstallDir(): string;
3
+ export declare function bridgeCliPath(bridgeDir: string): string;
4
+ /** Directory used by `npm install --prefix` (usually ~/.cleos/bridge). */
5
+ export declare function resolveBridgeInstallDir(): string;
6
+ export declare function readInstalledBridgeVersion(bridgeDir: string): string | null;
7
+ //# sourceMappingURL=bridgePaths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridgePaths.d.ts","sourceRoot":"","sources":["../src/bridgePaths.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,cAAc,+BAA+B,CAAC;AAE3D,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEvD;AAwBD,0EAA0E;AAC1E,wBAAgB,uBAAuB,IAAI,MAAM,CAMhD;AAED,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ3E"}
@@ -0,0 +1,51 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, sep } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ export const BRIDGE_PACKAGE = "@saleso.innovations/bridge";
7
+ export function defaultBridgeInstallDir() {
8
+ return join(homedir(), ".cleos", "bridge");
9
+ }
10
+ export function bridgeCliPath(bridgeDir) {
11
+ return join(bridgeDir, "node_modules", BRIDGE_PACKAGE, "dist", "cli.js");
12
+ }
13
+ function inferBridgeDirFromCliPath(cliPath) {
14
+ const marker = `${sep}node_modules${sep}${BRIDGE_PACKAGE.replace("/", sep)}${sep}`;
15
+ const idx = cliPath.indexOf(marker);
16
+ if (idx < 0)
17
+ return null;
18
+ const nodeModulesDir = join(cliPath.slice(0, idx), "node_modules");
19
+ return dirname(nodeModulesDir);
20
+ }
21
+ function readBridgeDirFromSystemd() {
22
+ try {
23
+ const unit = execFileSync("systemctl", ["cat", "cleos-bridge.service"], {
24
+ encoding: "utf8",
25
+ stdio: ["ignore", "pipe", "ignore"],
26
+ });
27
+ const match = unit.match(/ExecStart=\S+\s+(\S+\/dist\/cli\.js)/);
28
+ if (!match?.[1])
29
+ return null;
30
+ return inferBridgeDirFromCliPath(match[1]);
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ /** Directory used by `npm install --prefix` (usually ~/.cleos/bridge). */
37
+ export function resolveBridgeInstallDir() {
38
+ return (readBridgeDirFromSystemd() ??
39
+ inferBridgeDirFromCliPath(fileURLToPath(import.meta.url)) ??
40
+ defaultBridgeInstallDir());
41
+ }
42
+ export function readInstalledBridgeVersion(bridgeDir) {
43
+ const pkgJson = join(bridgeDir, "node_modules", BRIDGE_PACKAGE, "package.json");
44
+ try {
45
+ const parsed = JSON.parse(readFileSync(pkgJson, "utf8"));
46
+ return parsed.version ?? null;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
package/dist/cli.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { credentialsPathForDisplay, loadCredentials } from "./credentials.js";
3
3
  import { connectHermesAgent, pairCleosAgent, reconnectHermesAgent } from "./client.js";
4
+ import { startCronWatcher } from "./cronWatcher.js";
4
5
  import { runBridgeDaemon } from "./daemon.js";
6
+ import { runBridgeUpdate } from "./update.js";
5
7
  import { prepareHermesForBridge } from "./ensureHermesApi.js";
6
8
  import { createHermesMessageHandler } from "./hermesForwarder.js";
7
9
  import { convexSiteUrlFromEnv } from "./resolve.js";
@@ -38,7 +40,7 @@ async function main() {
38
40
  console.log(`Connected agent ${session.agentId}.`);
39
41
  console.log(`Credentials saved to ${credentialsPathForDisplay()}`);
40
42
  console.log("Press Ctrl+C to exit.");
41
- await waitForExit(session);
43
+ await waitForExit(session, () => startCronWatcher({ session }));
42
44
  return;
43
45
  }
44
46
  if (command === "start") {
@@ -61,7 +63,11 @@ async function main() {
61
63
  });
62
64
  console.log(`Reconnected agent ${session.agentId}.`);
63
65
  console.log("Press Ctrl+C to exit.");
64
- await waitForExit(session);
66
+ await waitForExit(session, () => startCronWatcher({ session }));
67
+ return;
68
+ }
69
+ if (command === "update") {
70
+ runBridgeUpdate({ tag: codeArg || "latest" });
65
71
  return;
66
72
  }
67
73
  printUsage();
@@ -73,10 +79,13 @@ function printUsage() {
73
79
  console.error(" cleos-bridge connect <CODE> Pair and stay connected in the foreground");
74
80
  console.error(" cleos-bridge start [CODE] Run as a reconnecting daemon (systemd)");
75
81
  console.error(" cleos-bridge reconnect Reconnect once using saved credentials");
82
+ console.error(" cleos-bridge update [TAG] Update package and restart systemd service");
76
83
  }
77
- async function waitForExit(session) {
84
+ async function waitForExit(session, onStart) {
85
+ const stop = onStart?.();
78
86
  await new Promise((resolve) => {
79
87
  const onSignal = () => {
88
+ stop?.();
80
89
  session.close();
81
90
  resolve();
82
91
  process.exit(0);
@@ -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;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,qBAAqB,GAAG;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,qBAAqB,EAAE,CAAC;CACvC,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,gBAAgB,GAAG;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,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;IACtB,iBAAiB,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACtE,CAAC;AAwLF,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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoC,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAKhG,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,qBAAqB,GAAG;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,qBAAqB,EAAE,CAAC;CACvC,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,gBAAgB,GAAG;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,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;IACtB,iBAAiB,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACtE,CAAC;AA2LF,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,CA2BD;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
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import WebSocket from "ws";
3
3
  import { saveCredentials, loadCredentials } from "./credentials.js";
4
+ import { rememberConversationId } from "./activeConversation.js";
4
5
  import { convexSiteUrlFromEnv, resolvePairingCode } from "./resolve.js";
5
6
  import { normalizePairingCode } from "./normalizePairingCode.js";
6
7
  function parseUserMessageAttachments(raw) {
@@ -93,6 +94,9 @@ async function openAgentConnection(options) {
93
94
  const content = typeof envelope.content === "string" ? envelope.content : "";
94
95
  const agentId = typeof envelope.agentId === "string" ? envelope.agentId : options.agentId;
95
96
  const conversationId = typeof envelope.conversationId === "string" ? envelope.conversationId : "unknown";
97
+ if (conversationId !== "unknown") {
98
+ rememberConversationId(conversationId);
99
+ }
96
100
  const attachments = parseUserMessageAttachments(envelope.attachments);
97
101
  const replyMessageId = randomUUID();
98
102
  const reply = createReplySender(ws, agentId, conversationId, replyMessageId);
@@ -164,6 +168,7 @@ export async function pairCleosAgent(options) {
164
168
  agentToken: payload.agentToken,
165
169
  relayHttpUrl: pairing.relayHttpUrl,
166
170
  relayWsUrl: pairing.relayWsUrl,
171
+ convexSiteUrl: options.convexSiteUrl ?? convexSiteUrlFromEnv(),
167
172
  agentName: options.agentName ?? "Hermes",
168
173
  savedAt: Date.now(),
169
174
  });
@@ -4,5 +4,8 @@ export declare const DEFAULT_CLEOS_CONVEX_SITE_URL = "https://amicable-elephant-
4
4
  export declare const DEFAULT_HERMES_API_URL = "http://127.0.0.1:8642/v1/chat/completions";
5
5
  /** VPS one-line installer (served from public Convex HTTP). */
6
6
  export declare const DEFAULT_BRIDGE_INSTALL_URL = "https://amicable-elephant-407.convex.site/install-bridge.sh";
7
+ /** VPS one-line updater (served from public Convex HTTP). */
8
+ export declare const DEFAULT_BRIDGE_UPDATE_URL = "https://amicable-elephant-407.convex.site/update-bridge.sh";
7
9
  export declare function bridgeInstallCommand(code: string): string;
10
+ export declare function bridgeUpdateCommand(): string;
8
11
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,eAAO,MAAM,6BAA6B,8CAA8C,CAAC;AAEzF,6FAA6F;AAC7F,eAAO,MAAM,sBAAsB,8CAA8C,CAAC;AAElF,+DAA+D;AAC/D,eAAO,MAAM,0BAA0B,gEACwB,CAAC;AAEhE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzD"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,eAAO,MAAM,6BAA6B,8CAA8C,CAAC;AAEzF,6FAA6F;AAC7F,eAAO,MAAM,sBAAsB,8CAA8C,CAAC;AAElF,+DAA+D;AAC/D,eAAO,MAAM,0BAA0B,gEACwB,CAAC;AAEhE,6DAA6D;AAC7D,eAAO,MAAM,yBAAyB,+DACwB,CAAC;AAE/D,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C"}
package/dist/constants.js CHANGED
@@ -4,6 +4,11 @@ export const DEFAULT_CLEOS_CONVEX_SITE_URL = "https://amicable-elephant-407.conv
4
4
  export const DEFAULT_HERMES_API_URL = "http://127.0.0.1:8642/v1/chat/completions";
5
5
  /** VPS one-line installer (served from public Convex HTTP). */
6
6
  export const DEFAULT_BRIDGE_INSTALL_URL = "https://amicable-elephant-407.convex.site/install-bridge.sh";
7
+ /** VPS one-line updater (served from public Convex HTTP). */
8
+ export const DEFAULT_BRIDGE_UPDATE_URL = "https://amicable-elephant-407.convex.site/update-bridge.sh";
7
9
  export function bridgeInstallCommand(code) {
8
10
  return `curl -fsSL ${DEFAULT_BRIDGE_INSTALL_URL} | bash -s -- ${code}`;
9
11
  }
12
+ export function bridgeUpdateCommand() {
13
+ return `curl -fsSL ${DEFAULT_BRIDGE_UPDATE_URL} | bash`;
14
+ }
@@ -3,10 +3,13 @@ export type SavedAgentCredentials = {
3
3
  agentToken: string;
4
4
  relayHttpUrl: string;
5
5
  relayWsUrl: string;
6
+ convexSiteUrl?: string;
7
+ conversationId?: string;
6
8
  agentName?: string;
7
9
  savedAt: number;
8
10
  };
9
11
  export declare function loadCredentials(): SavedAgentCredentials | null;
10
12
  export declare function saveCredentials(credentials: SavedAgentCredentials): void;
13
+ export declare function patchCredentials(patch: Partial<SavedAgentCredentials>): SavedAgentCredentials | null;
11
14
  export declare function credentialsPathForDisplay(): string;
12
15
  //# sourceMappingURL=credentials.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAIF,wBAAgB,eAAe,IAAI,qBAAqB,GAAG,IAAI,CAS9D;AAED,wBAAgB,eAAe,CAAC,WAAW,EAAE,qBAAqB,GAAG,IAAI,CAGxE;AAED,wBAAgB,yBAAyB,IAAI,MAAM,CAElD"}
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAIF,wBAAgB,eAAe,IAAI,qBAAqB,GAAG,IAAI,CAS9D;AAED,wBAAgB,eAAe,CAAC,WAAW,EAAE,qBAAqB,GAAG,IAAI,CAGxE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,qBAAqB,CAAC,GAAG,qBAAqB,GAAG,IAAI,CAMpG;AAED,wBAAgB,yBAAyB,IAAI,MAAM,CAElD"}
@@ -18,6 +18,14 @@ export function saveCredentials(credentials) {
18
18
  mkdirSync(dirname(credentialsPath), { recursive: true });
19
19
  writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2));
20
20
  }
21
+ export function patchCredentials(patch) {
22
+ const existing = loadCredentials();
23
+ if (!existing)
24
+ return null;
25
+ const next = { ...existing, ...patch, savedAt: Date.now() };
26
+ saveCredentials(next);
27
+ return next;
28
+ }
21
29
  export function credentialsPathForDisplay() {
22
30
  return credentialsPath;
23
31
  }
@@ -0,0 +1,13 @@
1
+ import type { ConnectResult } from "./client.js";
2
+ type CronWatcherOptions = {
3
+ session: ConnectResult;
4
+ onDelivered?: (info: {
5
+ jobId: string;
6
+ jobName: string;
7
+ filePath: string;
8
+ }) => void;
9
+ onError?: (message: string) => void;
10
+ };
11
+ export declare function startCronWatcher(options: CronWatcherOptions): () => void;
12
+ export {};
13
+ //# sourceMappingURL=cronWatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cronWatcher.d.ts","sourceRoot":"","sources":["../src/cronWatcher.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAejD,KAAK,kBAAkB,GAAG;IACxB,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACnF,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC,CAAC;AAiGF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,IAAI,CAoExE"}
@@ -0,0 +1,165 @@
1
+ import { mkdirSync, readFileSync, readdirSync, statSync, existsSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, relative } from "node:path";
4
+ import { resolveActiveConversationId } from "./activeConversation.js";
5
+ import { loadCredentials } from "./credentials.js";
6
+ const HERMES_CRON_OUTPUT_DIR = join(homedir(), ".hermes", "cron", "output");
7
+ const HERMES_CRON_JOBS_FILE = join(homedir(), ".hermes", "cron", "jobs.json");
8
+ const DELIVERED_INDEX_PATH = join(homedir(), ".cleos", "cron-delivered.json");
9
+ const POLL_INTERVAL_MS = 5_000;
10
+ const FILE_SETTLE_MS = 750;
11
+ function readDeliveredIndex() {
12
+ try {
13
+ const parsed = JSON.parse(readFileSync(DELIVERED_INDEX_PATH, "utf8"));
14
+ if (!Array.isArray(parsed))
15
+ return new Set();
16
+ return new Set(parsed.filter((item) => typeof item === "string"));
17
+ }
18
+ catch {
19
+ return new Set();
20
+ }
21
+ }
22
+ function writeDeliveredIndex(delivered) {
23
+ mkdirSync(dirname(DELIVERED_INDEX_PATH), { recursive: true });
24
+ writeFileSync(DELIVERED_INDEX_PATH, JSON.stringify([...delivered].sort(), null, 2));
25
+ }
26
+ function listCronOutputFiles(rootDir) {
27
+ if (!existsSync(rootDir))
28
+ return [];
29
+ const files = [];
30
+ for (const jobId of readdirSync(rootDir)) {
31
+ const jobDir = join(rootDir, jobId);
32
+ let stat;
33
+ try {
34
+ stat = statSync(jobDir);
35
+ }
36
+ catch {
37
+ continue;
38
+ }
39
+ if (!stat.isDirectory())
40
+ continue;
41
+ for (const fileName of readdirSync(jobDir)) {
42
+ if (!fileName.endsWith(".md"))
43
+ continue;
44
+ files.push(join(jobDir, fileName));
45
+ }
46
+ }
47
+ return files.sort();
48
+ }
49
+ function loadHermesCronJobs() {
50
+ const names = new Map();
51
+ if (!existsSync(HERMES_CRON_JOBS_FILE))
52
+ return names;
53
+ try {
54
+ const parsed = JSON.parse(readFileSync(HERMES_CRON_JOBS_FILE, "utf8"));
55
+ const jobs = Array.isArray(parsed)
56
+ ? parsed
57
+ : parsed && typeof parsed === "object" && Array.isArray(parsed.jobs)
58
+ ? (parsed.jobs ?? [])
59
+ : [];
60
+ for (const job of jobs) {
61
+ if (!job?.id)
62
+ continue;
63
+ names.set(job.id, job.name?.trim() || job.id);
64
+ }
65
+ }
66
+ catch {
67
+ // Ignore malformed jobs file; fall back to job id as name.
68
+ }
69
+ return names;
70
+ }
71
+ function relativeOutputKey(filePath) {
72
+ return relative(HERMES_CRON_OUTPUT_DIR, filePath).replace(/\\/g, "/");
73
+ }
74
+ function parseRunAtFromFileName(filePath) {
75
+ const base = filePath.split("/").pop()?.replace(/\.md$/i, "") ?? "";
76
+ if (!base)
77
+ return undefined;
78
+ const asNumber = Number(base);
79
+ if (Number.isFinite(asNumber) && asNumber > 0)
80
+ return asNumber;
81
+ const asDate = Date.parse(base);
82
+ if (Number.isFinite(asDate))
83
+ return asDate;
84
+ try {
85
+ return statSync(filePath).mtimeMs;
86
+ }
87
+ catch {
88
+ return undefined;
89
+ }
90
+ }
91
+ async function readFileWhenStable(filePath) {
92
+ let lastSize = -1;
93
+ for (let attempt = 0; attempt < 4; attempt += 1) {
94
+ const content = readFileSync(filePath, "utf8");
95
+ if (content.length === lastSize) {
96
+ return content.trim();
97
+ }
98
+ lastSize = content.length;
99
+ await sleep(FILE_SETTLE_MS);
100
+ }
101
+ return readFileSync(filePath, "utf8").trim();
102
+ }
103
+ function sleep(ms) {
104
+ return new Promise((resolve) => setTimeout(resolve, ms));
105
+ }
106
+ export function startCronWatcher(options) {
107
+ const delivered = readDeliveredIndex();
108
+ let conversationId = loadCredentials()?.conversationId ?? null;
109
+ let stopped = false;
110
+ for (const filePath of listCronOutputFiles(HERMES_CRON_OUTPUT_DIR)) {
111
+ delivered.add(relativeOutputKey(filePath));
112
+ }
113
+ writeDeliveredIndex(delivered);
114
+ const tick = () => {
115
+ void (async () => {
116
+ if (stopped)
117
+ return;
118
+ try {
119
+ if (!conversationId) {
120
+ conversationId = await resolveActiveConversationId();
121
+ }
122
+ const jobNames = loadHermesCronJobs();
123
+ for (const filePath of listCronOutputFiles(HERMES_CRON_OUTPUT_DIR)) {
124
+ const key = relativeOutputKey(filePath);
125
+ if (delivered.has(key))
126
+ continue;
127
+ const parts = key.split("/");
128
+ const jobId = parts[0] ?? "unknown";
129
+ const jobName = jobNames.get(jobId) ?? jobId;
130
+ const content = await readFileWhenStable(filePath);
131
+ if (!content) {
132
+ delivered.add(key);
133
+ continue;
134
+ }
135
+ options.session.deliverCronResult(content, {
136
+ conversationId: conversationId,
137
+ jobId,
138
+ jobName,
139
+ runAt: parseRunAtFromFileName(filePath),
140
+ });
141
+ delivered.add(key);
142
+ writeDeliveredIndex(delivered);
143
+ options.onDelivered?.({ jobId, jobName, filePath });
144
+ console.log(JSON.stringify({
145
+ event: "cleos-bridge.cron-delivered",
146
+ jobId,
147
+ jobName,
148
+ file: key,
149
+ }));
150
+ }
151
+ }
152
+ catch (error) {
153
+ const message = error instanceof Error ? error.message : String(error);
154
+ options.onError?.(message);
155
+ console.error(JSON.stringify({ event: "cleos-bridge.cron-error", message }));
156
+ }
157
+ })();
158
+ };
159
+ const timer = setInterval(tick, POLL_INTERVAL_MS);
160
+ tick();
161
+ return () => {
162
+ stopped = true;
163
+ clearInterval(timer);
164
+ };
165
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,KAAK,CAAC,CA8BpF"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,KAAK,CAAC,CAmCpF"}
package/dist/daemon.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { connectHermesAgent, reconnectHermesAgent } from "./client.js";
2
+ import { startCronWatcher } from "./cronWatcher.js";
2
3
  import { createHermesMessageHandler } from "./hermesForwarder.js";
3
4
  const RECONNECT_DELAY_MS = 5_000;
4
5
  export async function runBridgeDaemon(options = {}) {
@@ -20,7 +21,13 @@ export async function runBridgeDaemon(options = {}) {
20
21
  session = await reconnectHermesAgent({ onUserMessage });
21
22
  console.log(JSON.stringify({ event: "cleos-bridge.connected", agentId: session.agentId, mode: "reconnect" }));
22
23
  }
23
- await session.closed;
24
+ const stopCronWatcher = startCronWatcher({ session });
25
+ try {
26
+ await session.closed;
27
+ }
28
+ finally {
29
+ stopCronWatcher();
30
+ }
24
31
  session.close();
25
32
  console.log(JSON.stringify({ event: "cleos-bridge.disconnected", retryInMs: RECONNECT_DELAY_MS }));
26
33
  }
package/dist/index.d.ts CHANGED
@@ -7,7 +7,9 @@ export type { PairingInfo } from "./resolve.js";
7
7
  export { pairWithCleos } from "./pairWithCleos.js";
8
8
  export { createHermesMessageHandler, forwardToHermes, resolveHermesApiConfig } from "./hermesForwarder.js";
9
9
  export type { HermesForwarderOptions } from "./hermesForwarder.js";
10
+ export { startCronWatcher } from "./cronWatcher.js";
11
+ export { resolveActiveConversationId, rememberConversationId } from "./activeConversation.js";
10
12
  export { runBridgeDaemon } from "./daemon.js";
11
13
  export type { RunBridgeOptions } from "./daemon.js";
12
- export { DEFAULT_BRIDGE_INSTALL_URL, DEFAULT_CLEOS_CONVEX_SITE_URL, DEFAULT_HERMES_API_URL, bridgeInstallCommand, } from "./constants.js";
14
+ export { DEFAULT_BRIDGE_INSTALL_URL, DEFAULT_BRIDGE_UPDATE_URL, DEFAULT_CLEOS_CONVEX_SITE_URL, DEFAULT_HERMES_API_URL, bridgeInstallCommand, bridgeUpdateCommand, } from "./constants.js";
13
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACvF,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAChH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACxE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC3G,YAAY,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EACL,0BAA0B,EAC1B,6BAA6B,EAC7B,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACvF,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAChH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACxE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC3G,YAAY,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,2BAA2B,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAC9F,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EACL,0BAA0B,EAC1B,yBAAyB,EACzB,6BAA6B,EAC7B,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -3,5 +3,7 @@ export { loadCredentials, saveCredentials, credentialsPathForDisplay } from "./c
3
3
  export { resolvePairingCode, convexSiteUrlFromEnv } from "./resolve.js";
4
4
  export { pairWithCleos } from "./pairWithCleos.js";
5
5
  export { createHermesMessageHandler, forwardToHermes, resolveHermesApiConfig } from "./hermesForwarder.js";
6
+ export { startCronWatcher } from "./cronWatcher.js";
7
+ export { resolveActiveConversationId, rememberConversationId } from "./activeConversation.js";
6
8
  export { runBridgeDaemon } from "./daemon.js";
7
- export { DEFAULT_BRIDGE_INSTALL_URL, DEFAULT_CLEOS_CONVEX_SITE_URL, DEFAULT_HERMES_API_URL, bridgeInstallCommand, } from "./constants.js";
9
+ export { DEFAULT_BRIDGE_INSTALL_URL, DEFAULT_BRIDGE_UPDATE_URL, DEFAULT_CLEOS_CONVEX_SITE_URL, DEFAULT_HERMES_API_URL, bridgeInstallCommand, bridgeUpdateCommand, } from "./constants.js";
@@ -0,0 +1,2 @@
1
+ export declare function hashToken(token: string): string;
2
+ //# sourceMappingURL=token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAEA,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C"}
package/dist/token.js ADDED
@@ -0,0 +1,4 @@
1
+ import { createHash } from "node:crypto";
2
+ export function hashToken(token) {
3
+ return createHash("sha256").update(token).digest("hex");
4
+ }
@@ -0,0 +1,8 @@
1
+ export type RunBridgeUpdateOptions = {
2
+ /** npm dist-tag or semver, default `latest`. */
3
+ tag?: string;
4
+ /** Skip systemd / service restart after install. */
5
+ skipRestart?: boolean;
6
+ };
7
+ export declare function runBridgeUpdate(options?: RunBridgeUpdateOptions): void;
8
+ //# sourceMappingURL=update.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../src/update.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,sBAAsB,GAAG;IACnC,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,IAAI,CAsC1E"}
package/dist/update.js ADDED
@@ -0,0 +1,80 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { BRIDGE_PACKAGE, bridgeCliPath, readInstalledBridgeVersion, resolveBridgeInstallDir, } from "./bridgePaths.js";
6
+ export function runBridgeUpdate(options = {}) {
7
+ const tag = options.tag ?? "latest";
8
+ const bridgeDir = resolveBridgeInstallDir();
9
+ const beforeVersion = readInstalledBridgeVersion(bridgeDir);
10
+ mkdirSync(bridgeDir, { recursive: true });
11
+ console.log(`Updating ${BRIDGE_PACKAGE}@${tag} in ${bridgeDir}...`);
12
+ const npm = spawnSync("npm", [
13
+ "install",
14
+ "--prefix",
15
+ bridgeDir,
16
+ `${BRIDGE_PACKAGE}@${tag}`,
17
+ "--no-fund",
18
+ "--no-audit",
19
+ "--omit=dev",
20
+ ], { stdio: "inherit", env: process.env });
21
+ if (npm.status !== 0) {
22
+ throw new Error("npm install failed");
23
+ }
24
+ const cliPath = bridgeCliPath(bridgeDir);
25
+ linkBridgeCli(cliPath);
26
+ const afterVersion = readInstalledBridgeVersion(bridgeDir);
27
+ if (beforeVersion && afterVersion && beforeVersion === afterVersion) {
28
+ console.log(`Already on ${afterVersion}.`);
29
+ }
30
+ else {
31
+ console.log(`Updated ${beforeVersion ?? "unknown"} → ${afterVersion ?? "unknown"}.`);
32
+ }
33
+ if (!options.skipRestart) {
34
+ restartBridgeService();
35
+ }
36
+ }
37
+ function linkBridgeCli(cliPath) {
38
+ const nodeBin = process.execPath;
39
+ const wrapper = `#!/usr/bin/env bash\nexec "${nodeBin}" "${cliPath}" "$@"\n`;
40
+ const targets = process.getuid?.() === 0
41
+ ? ["/usr/local/bin/cleos-bridge"]
42
+ : [join(homedir(), ".local", "bin", "cleos-bridge")];
43
+ for (const target of targets) {
44
+ try {
45
+ mkdirSync(join(target, ".."), { recursive: true });
46
+ writeFileSync(target, wrapper, { mode: 0o755 });
47
+ chmodSync(target, 0o755);
48
+ console.log(`Linked ${target}`);
49
+ return;
50
+ }
51
+ catch {
52
+ // try next target
53
+ }
54
+ }
55
+ }
56
+ function restartBridgeService() {
57
+ const unitExists = spawnSync("systemctl", ["cat", "cleos-bridge.service"], {
58
+ encoding: "utf8",
59
+ stdio: ["ignore", "pipe", "ignore"],
60
+ });
61
+ if (unitExists.status !== 0) {
62
+ console.log("No cleos-bridge.service found. Restart manually if the bridge is running:");
63
+ console.log(" cleos-bridge start");
64
+ return;
65
+ }
66
+ console.log("Restarting cleos-bridge.service...");
67
+ let restart = spawnSync("systemctl", ["restart", "cleos-bridge.service"], { stdio: "inherit" });
68
+ if (restart.status === 0) {
69
+ console.log("cleos-bridge.service restarted.");
70
+ return;
71
+ }
72
+ try {
73
+ execFileSync("sudo", ["systemctl", "restart", "cleos-bridge.service"], { stdio: "inherit" });
74
+ console.log("cleos-bridge.service restarted (via sudo).");
75
+ }
76
+ catch {
77
+ console.log("Could not restart the service. Run:");
78
+ console.log(" sudo systemctl restart cleos-bridge");
79
+ }
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saleso.innovations/bridge",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Connect your Hermes agent to the Cleos iOS app via pairing code.",
5
5
  "type": "module",
6
6
  "license": "MIT",