@saleso.innovations/bridge 0.1.28 → 0.1.30

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,10 @@
1
+ export type BridgeRuntimeVersionInfo = {
2
+ installed: string | null;
3
+ latest: string | null;
4
+ updateAvailable: boolean | null;
5
+ checkError?: string;
6
+ };
7
+ export declare function fetchInstalledBridgeVersion(): string | null;
8
+ export declare function fetchLatestBridgeVersionFromNpm(): Promise<string | null>;
9
+ export declare function fetchBridgeRuntimeVersion(): Promise<BridgeRuntimeVersionInfo>;
10
+ //# sourceMappingURL=bridgeVersion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridgeVersion.d.ts","sourceRoot":"","sources":["../src/bridgeVersion.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,wBAAwB,GAAG;IACrC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,eAAe,EAAE,OAAO,GAAG,IAAI,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,wBAAgB,2BAA2B,IAAI,MAAM,GAAG,IAAI,CAE3D;AAED,wBAAsB,+BAA+B,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoB9E;AAiBD,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAsBnF"}
@@ -0,0 +1,63 @@
1
+ import { BRIDGE_PACKAGE, readInstalledBridgeVersion, resolveBridgeInstallDir } from "./bridgePaths.js";
2
+ import { NPM_PUBLIC_REGISTRY } from "./constants.js";
3
+ const LATEST_CHECK_TIMEOUT_MS = 30_000;
4
+ export function fetchInstalledBridgeVersion() {
5
+ return readInstalledBridgeVersion(resolveBridgeInstallDir());
6
+ }
7
+ export async function fetchLatestBridgeVersionFromNpm() {
8
+ const url = `${NPM_PUBLIC_REGISTRY}${BRIDGE_PACKAGE.replace("/", "%2F")}`;
9
+ const controller = new AbortController();
10
+ const timeout = setTimeout(() => controller.abort(), LATEST_CHECK_TIMEOUT_MS);
11
+ try {
12
+ const response = await fetch(url, {
13
+ headers: { accept: "application/vnd.npm.install-v1+json" },
14
+ signal: controller.signal,
15
+ });
16
+ if (!response.ok) {
17
+ return null;
18
+ }
19
+ const body = (await response.json());
20
+ const latest = body["dist-tags"]?.latest;
21
+ return typeof latest === "string" && latest.trim().length > 0 ? latest.trim() : null;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ finally {
27
+ clearTimeout(timeout);
28
+ }
29
+ }
30
+ function compareSemver(a, b) {
31
+ const parse = (value) => value
32
+ .split(".")
33
+ .map((part) => Number.parseInt(part, 10))
34
+ .map((part) => (Number.isFinite(part) ? part : 0));
35
+ const left = parse(a);
36
+ const right = parse(b);
37
+ for (let i = 0; i < Math.max(left.length, right.length); i++) {
38
+ const diff = (left[i] ?? 0) - (right[i] ?? 0);
39
+ if (diff !== 0)
40
+ return diff > 0 ? 1 : -1;
41
+ }
42
+ return 0;
43
+ }
44
+ export async function fetchBridgeRuntimeVersion() {
45
+ const installed = fetchInstalledBridgeVersion();
46
+ const latest = await fetchLatestBridgeVersionFromNpm();
47
+ if (latest === null) {
48
+ return {
49
+ installed,
50
+ latest: null,
51
+ updateAvailable: null,
52
+ checkError: "Could not reach the npm registry to check the latest cleos-bridge version.",
53
+ };
54
+ }
55
+ if (installed === null) {
56
+ return { installed: null, latest, updateAvailable: null };
57
+ }
58
+ return {
59
+ installed,
60
+ latest,
61
+ updateAvailable: compareSemver(installed, latest) < 0,
62
+ };
63
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=bridgeVersion.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridgeVersion.test.d.ts","sourceRoot":"","sources":["../src/bridgeVersion.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,33 @@
1
+ import assert from "node:assert/strict";
2
+ import test, { afterEach } from "node:test";
3
+ import { fetchBridgeRuntimeVersion, fetchLatestBridgeVersionFromNpm } from "./bridgeVersion.js";
4
+ const realFetch = globalThis.fetch;
5
+ function mockFetch(impl) {
6
+ globalThis.fetch = impl;
7
+ }
8
+ afterEach(() => {
9
+ globalThis.fetch = realFetch;
10
+ });
11
+ test("fetchLatestBridgeVersionFromNpm reads dist-tags.latest", async () => {
12
+ mockFetch(async () => new Response(JSON.stringify({ "dist-tags": { latest: "0.1.31" } }), { status: 200 }));
13
+ assert.equal(await fetchLatestBridgeVersionFromNpm(), "0.1.31");
14
+ });
15
+ test("fetchLatestBridgeVersionFromNpm returns null on non-200", async () => {
16
+ mockFetch(async () => new Response("nope", { status: 503 }));
17
+ assert.equal(await fetchLatestBridgeVersionFromNpm(), null);
18
+ });
19
+ test("fetchLatestBridgeVersionFromNpm returns null when fetch throws", async () => {
20
+ mockFetch(async () => {
21
+ throw new Error("network down");
22
+ });
23
+ assert.equal(await fetchLatestBridgeVersionFromNpm(), null);
24
+ });
25
+ test("fetchBridgeRuntimeVersion reports checkError when npm is unreachable", async () => {
26
+ mockFetch(async () => {
27
+ throw new Error("network down");
28
+ });
29
+ const info = await fetchBridgeRuntimeVersion();
30
+ assert.equal(info.latest, null);
31
+ assert.equal(info.updateAvailable, null);
32
+ assert.ok(info.checkError);
33
+ });
@@ -1,5 +1,5 @@
1
1
  import { type ShellDeltaEmitter } from "./shellSession.js";
2
- export declare const HERMES_COMMAND_NAMES: readonly ["runtime.health", "runtime.detailedHealth", "runtime.version", "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", "hermes.update", "sessions.messages.list", "sessions.messages.countSent", "sessions.titles.resolve", "sessions.usage.get", "sessions.list", "skills.list", "files.list", "files.read", "files.write", "files.delete", "memories.list", "shell.exec", "shell.session.reset"];
2
+ export declare const HERMES_COMMAND_NAMES: readonly ["runtime.health", "runtime.detailedHealth", "runtime.version", "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", "hermes.update", "bridge.version", "bridge.update", "sessions.messages.list", "sessions.messages.countSent", "sessions.titles.resolve", "sessions.usage.get", "sessions.list", "skills.list", "tools.list", "tools.set", "mcp.list", "mcp.add", "mcp.remove", "files.list", "files.read", "files.write", "files.delete", "memories.list", "shell.exec", "shell.session.reset"];
3
3
  export type HermesCommandName = (typeof HERMES_COMMAND_NAMES)[number];
4
4
  export declare function isHermesCommandName(value: string): value is HermesCommandName;
5
5
  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":"AAmBA,OAAO,EAAuC,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGhG,eAAO,MAAM,oBAAoB,mpBAsCvB,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;AA6GD,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,CAAC;IAAC,OAAO,CAAC,EAAE,iBAAiB,CAAA;CAAO,GAC9F,OAAO,CAAC,OAAO,CAAC,CAiOlB;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":"AAqBA,OAAO,EAAuC,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAMhG,eAAO,MAAM,oBAAoB,svBA6CvB,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;AAkID,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,CAAC;IAAC,OAAO,CAAC,EAAE,iBAAiB,CAAA;CAAO,GAC9F,OAAO,CAAC,OAAO,CAAC,CAkQlB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAetG"}
@@ -3,10 +3,15 @@ import { restartHermesGateway, startHermesGateway, stopHermesGateway } from "./g
3
3
  import { listHermesCronJobs } from "./cronList.js";
4
4
  import { listHermesSessions, listSessionMessages, countUserMessagesSent, getSessionUsage, resolveSessionTitles, } from "./hermesSessionDb.js";
5
5
  import { listHermesSkills } from "./skillsList.js";
6
+ import { listHermesTools, setHermesToolEnabled } from "./toolsList.js";
7
+ import { addHermesMcpServer, listHermesMcpServers, removeHermesMcpServer } from "./mcpList.js";
6
8
  import { runHermesUpdate } from "./hermesUpdate.js";
7
9
  import { executeFilesList, executeFilesRead, executeFilesWrite, executeFilesDelete, executeMemoriesList, } from "./hermesFileCommands.js";
8
10
  import { executeShellExec, resetShellSession } from "./shellSession.js";
9
11
  import { fetchHermesRuntimeVersion } from "./runtimeVersion.js";
12
+ import { fetchBridgeRuntimeVersion, fetchInstalledBridgeVersion } from "./bridgeVersion.js";
13
+ import { runBridgeUpdate } from "./update.js";
14
+ import { spawn } from "node:child_process";
10
15
  export const HERMES_COMMAND_NAMES = [
11
16
  "runtime.health",
12
17
  "runtime.detailedHealth",
@@ -32,12 +37,19 @@ export const HERMES_COMMAND_NAMES = [
32
37
  "gateway.stop",
33
38
  "gateway.restart",
34
39
  "hermes.update",
40
+ "bridge.version",
41
+ "bridge.update",
35
42
  "sessions.messages.list",
36
43
  "sessions.messages.countSent",
37
44
  "sessions.titles.resolve",
38
45
  "sessions.usage.get",
39
46
  "sessions.list",
40
47
  "skills.list",
48
+ "tools.list",
49
+ "tools.set",
50
+ "mcp.list",
51
+ "mcp.add",
52
+ "mcp.remove",
41
53
  "files.list",
42
54
  "files.read",
43
55
  "files.write",
@@ -143,6 +155,23 @@ async function hermesFetchVoid(config, path, init = {}) {
143
155
  await hermesFetchJson(config, path, init);
144
156
  return { ok: true };
145
157
  }
158
+ /**
159
+ * Restart the cleos-bridge systemd service out-of-band so the freshly installed package is
160
+ * loaded. Detached + delayed so the in-flight `bridge.update` result can be flushed to the app
161
+ * before this process is replaced. No-op when the service unit is absent.
162
+ */
163
+ function scheduleDetachedBridgeRestart() {
164
+ try {
165
+ const child = spawn("bash", [
166
+ "-c",
167
+ "sleep 3; systemctl restart cleos-bridge.service || sudo systemctl restart cleos-bridge.service",
168
+ ], { detached: true, stdio: "ignore" });
169
+ child.unref();
170
+ }
171
+ catch {
172
+ // Service not managed by systemd; the new package loads on the next manual restart.
173
+ }
174
+ }
146
175
  export async function executeHermesCommand(command, args, options = {}) {
147
176
  const config = resolveHermesApiConfig(options);
148
177
  const { onDelta } = options;
@@ -300,6 +329,19 @@ export async function executeHermesCommand(command, args, options = {}) {
300
329
  const restartGateway = args.restartGateway === true || args.restart_gateway === true;
301
330
  return await runHermesUpdate({ restartGateway });
302
331
  }
332
+ case "bridge.version":
333
+ return await fetchBridgeRuntimeVersion();
334
+ case "bridge.update": {
335
+ const tag = optionalString(args, "tag") ?? "latest";
336
+ const before = fetchInstalledBridgeVersion();
337
+ // Install without restarting inline: restarting the service here would kill this
338
+ // process before the result reaches the app. We schedule a detached restart so the
339
+ // new package is picked up shortly after the result is delivered.
340
+ runBridgeUpdate({ tag, skipRestart: true });
341
+ const after = fetchInstalledBridgeVersion();
342
+ scheduleDetachedBridgeRestart();
343
+ return { ok: true, before, after };
344
+ }
303
345
  case "sessions.messages.list": {
304
346
  const sessionId = requireString(args, "sessionId");
305
347
  const limit = optionalNumber(args, "limit");
@@ -323,6 +365,26 @@ export async function executeHermesCommand(command, args, options = {}) {
323
365
  }
324
366
  case "skills.list":
325
367
  return await listHermesSkills();
368
+ case "tools.list": {
369
+ const platform = optionalString(args, "platform");
370
+ return await listHermesTools(platform);
371
+ }
372
+ case "tools.set": {
373
+ const name = requireString(args, "name");
374
+ const enabled = args.enabled === true;
375
+ return await setHermesToolEnabled(name, enabled);
376
+ }
377
+ case "mcp.list":
378
+ return await listHermesMcpServers();
379
+ case "mcp.add": {
380
+ const name = optionalString(args, "name");
381
+ const json = requireString(args, "json");
382
+ return await addHermesMcpServer({ name, json });
383
+ }
384
+ case "mcp.remove": {
385
+ const name = requireString(args, "name");
386
+ return await removeHermesMcpServer(name);
387
+ }
326
388
  case "files.list":
327
389
  return await executeFilesList(args);
328
390
  case "files.read": {
package/dist/index.d.ts CHANGED
@@ -12,6 +12,8 @@ export type { HermesSessionListEntry, HermesSessionUsage } from "./hermesSession
12
12
  export type { HermesSessionMessage } from "./hermesSessionDb.js";
13
13
  export { executeHermesCommand, HERMES_COMMAND_NAMES, isHermesCommandName, userSafeCommandError, } from "./hermesCommands.js";
14
14
  export type { HermesCommandErrorCode, HermesCommandName } from "./hermesCommands.js";
15
+ export { fetchBridgeRuntimeVersion, fetchInstalledBridgeVersion, fetchLatestBridgeVersionFromNpm, } from "./bridgeVersion.js";
16
+ export type { BridgeRuntimeVersionInfo } from "./bridgeVersion.js";
15
17
  export { startCronWatcher, listPendingCronFiles, clearDeliveredIndex } from "./cronWatcher.js";
16
18
  export { backfillCronDeliveries, describeCronDeliveryState } from "./cronBackfill.js";
17
19
  export { resolveActiveConversationId, rememberConversationId } from "./activeConversation.js";
@@ -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,EACV,oBAAoB,EACpB,kBAAkB,EAClB,UAAU,EACV,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,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,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACvF,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AACtF,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,2BAA2B,EAC3B,0BAA0B,EAC1B,gCAAgC,EAChC,+BAA+B,EAC/B,oBAAoB,EACpB,mBAAmB,GACpB,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,EACV,oBAAoB,EACpB,kBAAkB,EAClB,UAAU,EACV,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,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,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACvF,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,+BAA+B,GAChC,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AACtF,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,2BAA2B,EAC3B,0BAA0B,EAC1B,gCAAgC,EAChC,+BAA+B,EAC/B,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export { pairWithCleos } from "./pairWithCleos.js";
5
5
  export { createHermesMessageHandler, forwardToHermes, resolveHermesApiConfig } from "./hermesForwarder.js";
6
6
  export { getSessionUsage, listHermesSessions, listSessionMessages, resolveSessionTitle, resolveSessionTitles, } from "./hermesSessionDb.js";
7
7
  export { executeHermesCommand, HERMES_COMMAND_NAMES, isHermesCommandName, userSafeCommandError, } from "./hermesCommands.js";
8
+ export { fetchBridgeRuntimeVersion, fetchInstalledBridgeVersion, fetchLatestBridgeVersionFromNpm, } from "./bridgeVersion.js";
8
9
  export { startCronWatcher, listPendingCronFiles, clearDeliveredIndex } from "./cronWatcher.js";
9
10
  export { backfillCronDeliveries, describeCronDeliveryState } from "./cronBackfill.js";
10
11
  export { resolveActiveConversationId, rememberConversationId } from "./activeConversation.js";
@@ -0,0 +1,23 @@
1
+ export type HermesMcpServerEntry = {
2
+ name: string;
3
+ transport?: string;
4
+ tools?: string;
5
+ status?: string;
6
+ enabled: boolean;
7
+ };
8
+ export declare function parseMcpListOutput(stdout: string): HermesMcpServerEntry[];
9
+ export declare function listHermesMcpServers(): Promise<{
10
+ servers: HermesMcpServerEntry[];
11
+ }>;
12
+ export declare function addHermesMcpServer(input: {
13
+ name?: string;
14
+ json: string;
15
+ }): Promise<{
16
+ ok: true;
17
+ name: string;
18
+ }>;
19
+ export declare function removeHermesMcpServer(name: string): Promise<{
20
+ ok: true;
21
+ name: string;
22
+ }>;
23
+ //# sourceMappingURL=mcpList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpList.d.ts","sourceRoot":"","sources":["../src/mcpList.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAqBF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,oBAAoB,EAAE,CAwEzE;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC;IAAE,OAAO,EAAE,oBAAoB,EAAE,CAAA;CAAE,CAAC,CAgBzF;AAuLD,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAYtC;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAO7F"}
@@ -0,0 +1,275 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ const LIST_TIMEOUT_MS = 30_000;
5
+ const ADD_TIMEOUT_MS = 120_000;
6
+ const REMOVE_TIMEOUT_MS = 30_000;
7
+ const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
8
+ const HEADER_KEYS = ["Name", "Transport", "Tools", "Status"];
9
+ function isSeparatorLine(line) {
10
+ const trimmed = line.trim();
11
+ if (trimmed.length === 0) {
12
+ return true;
13
+ }
14
+ return /^[\s─_=-]+$/u.test(trimmed);
15
+ }
16
+ function stripLeadingGlyph(value) {
17
+ const trimmed = value.trim();
18
+ const match = trimmed.match(/^([^\p{L}\p{N}]+)\s*(.*)$/u);
19
+ if (match && match[2] && match[2].trim().length > 0) {
20
+ return match[2].trim();
21
+ }
22
+ return trimmed;
23
+ }
24
+ export function parseMcpListOutput(stdout) {
25
+ const lines = stdout.split(/\r?\n/);
26
+ let headerIndex = -1;
27
+ for (let i = 0; i < lines.length; i += 1) {
28
+ const line = lines[i] ?? "";
29
+ if (line.includes("Name") && line.includes("Status")) {
30
+ headerIndex = i;
31
+ break;
32
+ }
33
+ }
34
+ if (headerIndex < 0) {
35
+ return [];
36
+ }
37
+ const headerLine = lines[headerIndex] ?? "";
38
+ const columns = [];
39
+ for (const key of HEADER_KEYS) {
40
+ const start = headerLine.indexOf(key);
41
+ if (start >= 0) {
42
+ columns.push({ key, start });
43
+ }
44
+ }
45
+ columns.sort((a, b) => a.start - b.start);
46
+ if (columns.length === 0) {
47
+ return [];
48
+ }
49
+ const servers = [];
50
+ const seen = new Set();
51
+ for (let i = headerIndex + 1; i < lines.length; i += 1) {
52
+ const line = lines[i] ?? "";
53
+ if (isSeparatorLine(line)) {
54
+ continue;
55
+ }
56
+ const cells = {};
57
+ for (let c = 0; c < columns.length; c += 1) {
58
+ const column = columns[c];
59
+ if (!column) {
60
+ continue;
61
+ }
62
+ const end = c + 1 < columns.length ? columns[c + 1]?.start ?? line.length : line.length;
63
+ cells[column.key] = line.slice(column.start, end).trim();
64
+ }
65
+ const name = (cells.Name ?? "").trim();
66
+ if (name.length === 0 || seen.has(name)) {
67
+ continue;
68
+ }
69
+ const status = cells.Status ? stripLeadingGlyph(cells.Status) : undefined;
70
+ const entry = {
71
+ name,
72
+ enabled: (status ?? "").toLowerCase().includes("enabled") && !(status ?? "").toLowerCase().includes("disabled"),
73
+ };
74
+ if (cells.Transport && cells.Transport.length > 0) {
75
+ entry.transport = cells.Transport;
76
+ }
77
+ if (cells.Tools && cells.Tools.length > 0) {
78
+ entry.tools = cells.Tools;
79
+ }
80
+ if (status && status.length > 0) {
81
+ entry.status = status;
82
+ }
83
+ servers.push(entry);
84
+ seen.add(name);
85
+ }
86
+ return servers;
87
+ }
88
+ export async function listHermesMcpServers() {
89
+ try {
90
+ const { stdout } = await execFileAsync("hermes", ["mcp", "list"], {
91
+ timeout: LIST_TIMEOUT_MS,
92
+ maxBuffer: MAX_OUTPUT_BYTES,
93
+ env: process.env,
94
+ });
95
+ return { servers: parseMcpListOutput(stdout) };
96
+ }
97
+ catch (error) {
98
+ // `hermes mcp list` exits non-zero when no servers are configured on some builds.
99
+ const stdout = readExecStdout(error);
100
+ if (stdout != null) {
101
+ return { servers: parseMcpListOutput(stdout) };
102
+ }
103
+ throw error;
104
+ }
105
+ }
106
+ function readExecStdout(error) {
107
+ if (error && typeof error === "object" && "stdout" in error) {
108
+ const stdout = error.stdout;
109
+ if (typeof stdout === "string") {
110
+ return stdout;
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+ function asStringArray(value) {
116
+ if (!Array.isArray(value)) {
117
+ return undefined;
118
+ }
119
+ const items = value
120
+ .filter((entry) => typeof entry === "string" || typeof entry === "number")
121
+ .map((entry) => String(entry));
122
+ return items;
123
+ }
124
+ function asStringRecord(value) {
125
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
126
+ return undefined;
127
+ }
128
+ const record = {};
129
+ for (const [key, raw] of Object.entries(value)) {
130
+ if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
131
+ record[key] = String(raw);
132
+ }
133
+ }
134
+ return Object.keys(record).length > 0 ? record : undefined;
135
+ }
136
+ function pickConfigString(record, keys) {
137
+ for (const key of keys) {
138
+ const value = record[key];
139
+ if (typeof value === "string") {
140
+ const trimmed = value.trim();
141
+ if (trimmed.length > 0) {
142
+ return trimmed;
143
+ }
144
+ }
145
+ }
146
+ return undefined;
147
+ }
148
+ function toConfig(value) {
149
+ const config = {};
150
+ const command = pickConfigString(value, ["command", "cmd"]);
151
+ if (command) {
152
+ config.command = command;
153
+ }
154
+ const args = asStringArray(value.args);
155
+ if (args && args.length > 0) {
156
+ config.args = args;
157
+ }
158
+ const env = asStringRecord(value.env);
159
+ if (env) {
160
+ config.env = env;
161
+ }
162
+ const url = pickConfigString(value, ["url", "endpoint"]);
163
+ if (url) {
164
+ config.url = url;
165
+ }
166
+ const auth = pickConfigString(value, ["auth"]);
167
+ if (auth === "oauth" || auth === "header") {
168
+ config.auth = auth;
169
+ }
170
+ const preset = pickConfigString(value, ["preset"]);
171
+ if (preset) {
172
+ config.preset = preset;
173
+ }
174
+ return config;
175
+ }
176
+ function looksLikeConfig(value) {
177
+ return (typeof value.command === "string" ||
178
+ typeof value.url === "string" ||
179
+ typeof value.preset === "string" ||
180
+ Array.isArray(value.args) ||
181
+ (typeof value.env === "object" && value.env != null));
182
+ }
183
+ function normalizeAddInput(name, json) {
184
+ let parsed;
185
+ try {
186
+ parsed = JSON.parse(json);
187
+ }
188
+ catch {
189
+ throw new Error("MCP configuration is not valid JSON.");
190
+ }
191
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
192
+ throw new Error("MCP configuration must be a JSON object.");
193
+ }
194
+ const record = parsed;
195
+ // Shape 1: { "mcpServers": { name: cfg } }
196
+ const mcpServers = record.mcpServers ?? record.servers;
197
+ if (mcpServers && typeof mcpServers === "object" && !Array.isArray(mcpServers)) {
198
+ const entries = Object.entries(mcpServers);
199
+ const entry = entries[0];
200
+ if (entry && entry[1] && typeof entry[1] === "object") {
201
+ return {
202
+ name: (name && name.trim().length > 0 ? name.trim() : entry[0]).trim(),
203
+ config: toConfig(entry[1]),
204
+ };
205
+ }
206
+ }
207
+ // Shape 2: bare config object { command/url/args/env/... }
208
+ if (looksLikeConfig(record)) {
209
+ const resolvedName = (name ?? "").trim();
210
+ if (resolvedName.length === 0) {
211
+ throw new Error('Missing server "name".');
212
+ }
213
+ return { name: resolvedName, config: toConfig(record) };
214
+ }
215
+ // Shape 3: { name: cfg } single-entry map
216
+ const entries = Object.entries(record);
217
+ const entry = entries[0];
218
+ if (entries.length >= 1 && entry && entry[1] && typeof entry[1] === "object" && !Array.isArray(entry[1])) {
219
+ return {
220
+ name: (name && name.trim().length > 0 ? name.trim() : entry[0]).trim(),
221
+ config: toConfig(entry[1]),
222
+ };
223
+ }
224
+ throw new Error("Could not parse MCP server configuration from the provided JSON.");
225
+ }
226
+ function buildAddArgs(input) {
227
+ const { name, config } = input;
228
+ const argv = ["mcp", "add", name];
229
+ if (config.url) {
230
+ argv.push("--url", config.url);
231
+ }
232
+ if (config.command) {
233
+ argv.push("--command", config.command);
234
+ }
235
+ if (config.args && config.args.length > 0) {
236
+ argv.push("--args", ...config.args);
237
+ }
238
+ if (config.env) {
239
+ const envPairs = Object.entries(config.env).map(([key, value]) => `${key}=${value}`);
240
+ if (envPairs.length > 0) {
241
+ argv.push("--env", ...envPairs);
242
+ }
243
+ }
244
+ if (config.auth) {
245
+ argv.push("--auth", config.auth);
246
+ }
247
+ if (config.preset) {
248
+ argv.push("--preset", config.preset);
249
+ }
250
+ if (!config.url && !config.command && !config.preset) {
251
+ throw new Error('MCP configuration needs a "command", "url", or "preset".');
252
+ }
253
+ return argv;
254
+ }
255
+ export async function addHermesMcpServer(input) {
256
+ const normalized = normalizeAddInput(input.name, input.json);
257
+ if (normalized.name.length === 0) {
258
+ throw new Error('Missing server "name".');
259
+ }
260
+ const argv = buildAddArgs(normalized);
261
+ await execFileAsync("hermes", argv, {
262
+ timeout: ADD_TIMEOUT_MS,
263
+ maxBuffer: MAX_OUTPUT_BYTES,
264
+ env: process.env,
265
+ });
266
+ return { ok: true, name: normalized.name };
267
+ }
268
+ export async function removeHermesMcpServer(name) {
269
+ await execFileAsync("hermes", ["mcp", "remove", name], {
270
+ timeout: REMOVE_TIMEOUT_MS,
271
+ maxBuffer: MAX_OUTPUT_BYTES,
272
+ env: process.env,
273
+ });
274
+ return { ok: true, name };
275
+ }
@@ -0,0 +1,16 @@
1
+ export type HermesToolEntry = {
2
+ name: string;
3
+ description: string;
4
+ enabled: boolean;
5
+ category?: string;
6
+ };
7
+ export declare function parseToolsListOutput(stdout: string): HermesToolEntry[];
8
+ export declare function listHermesTools(platform?: string): Promise<{
9
+ tools: HermesToolEntry[];
10
+ }>;
11
+ export declare function setHermesToolEnabled(name: string, enabled: boolean): Promise<{
12
+ ok: true;
13
+ name: string;
14
+ enabled: boolean;
15
+ }>;
16
+ //# sourceMappingURL=toolsList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolsList.d.ts","sourceRoot":"","sources":["../src/toolsList.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AA+BF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,EAAE,CAqCtE;AAED,wBAAsB,eAAe,CACnC,QAAQ,GAAE,MAAyB,GAClC,OAAO,CAAC;IAAE,KAAK,EAAE,eAAe,EAAE,CAAA;CAAE,CAAC,CAYvC;AAED,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,GACf,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAQvD"}
@@ -0,0 +1,84 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ const LIST_TIMEOUT_MS = 30_000;
5
+ const SET_TIMEOUT_MS = 30_000;
6
+ const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
7
+ const DEFAULT_PLATFORM = "cli";
8
+ const ROW_PATTERN = /^\s*(?:✓|✗|×|x|☑|☒|•|-)?\s*(enabled|disabled)\b\s+(\S+)\s*(.*)$/i;
9
+ function stripLeadingIcon(value) {
10
+ // Hermes prints an emoji/icon before the human label (e.g. "🔍 Web Search").
11
+ // Drop a single leading non-alphanumeric, non-ASCII glyph plus following spaces.
12
+ const trimmed = value.trim();
13
+ const match = trimmed.match(/^([^\p{L}\p{N}(]+)\s*(.*)$/u);
14
+ if (match && match[2] && match[2].trim().length > 0) {
15
+ return match[2].trim();
16
+ }
17
+ return trimmed;
18
+ }
19
+ function isSectionHeader(line) {
20
+ const trimmed = line.trim();
21
+ if (trimmed.length === 0) {
22
+ return undefined;
23
+ }
24
+ if (!trimmed.endsWith(":")) {
25
+ return undefined;
26
+ }
27
+ if (ROW_PATTERN.test(line)) {
28
+ return undefined;
29
+ }
30
+ // Normalize "Built-in toolsets (cli):" -> "Built-in toolsets".
31
+ const label = trimmed.replace(/:\s*$/, "").replace(/\s*\([^)]*\)\s*$/, "").trim();
32
+ return label.length > 0 ? label : undefined;
33
+ }
34
+ export function parseToolsListOutput(stdout) {
35
+ const tools = [];
36
+ const seen = new Set();
37
+ let currentCategory;
38
+ for (const rawLine of stdout.split(/\r?\n/)) {
39
+ const header = isSectionHeader(rawLine);
40
+ if (header) {
41
+ currentCategory = header;
42
+ continue;
43
+ }
44
+ const match = rawLine.match(ROW_PATTERN);
45
+ if (!match) {
46
+ continue;
47
+ }
48
+ const statusToken = (match[1] ?? "").toLowerCase();
49
+ const name = (match[2] ?? "").trim();
50
+ if (name.length === 0 || seen.has(name)) {
51
+ continue;
52
+ }
53
+ const description = stripLeadingIcon(match[3] ?? "");
54
+ const entry = {
55
+ name,
56
+ description,
57
+ enabled: statusToken === "enabled",
58
+ };
59
+ if (currentCategory) {
60
+ entry.category = currentCategory;
61
+ }
62
+ tools.push(entry);
63
+ seen.add(name);
64
+ }
65
+ return tools;
66
+ }
67
+ export async function listHermesTools(platform = DEFAULT_PLATFORM) {
68
+ const resolvedPlatform = platform.trim().length > 0 ? platform.trim() : DEFAULT_PLATFORM;
69
+ const { stdout } = await execFileAsync("hermes", ["tools", "list", "--platform", resolvedPlatform], {
70
+ timeout: LIST_TIMEOUT_MS,
71
+ maxBuffer: MAX_OUTPUT_BYTES,
72
+ env: process.env,
73
+ });
74
+ return { tools: parseToolsListOutput(stdout) };
75
+ }
76
+ export async function setHermesToolEnabled(name, enabled) {
77
+ const subcommand = enabled ? "enable" : "disable";
78
+ await execFileAsync("hermes", ["tools", subcommand, name], {
79
+ timeout: SET_TIMEOUT_MS,
80
+ maxBuffer: MAX_OUTPUT_BYTES,
81
+ env: process.env,
82
+ });
83
+ return { ok: true, name, enabled };
84
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saleso.innovations/bridge",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Connect your Hermes agent to the Cleos iOS app via pairing code.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@
36
36
  "lint": "eslint . --max-warnings 0",
37
37
  "check-types": "tsc --noEmit",
38
38
  "prepublishOnly": "npm run build",
39
- "test": "node --import tsx --test src/hermesFiles.test.ts src/hermesSessionDb.test.ts src/shellSession.test.ts"
39
+ "test": "node --import tsx --test src/hermesFiles.test.ts src/hermesSessionDb.test.ts src/shellSession.test.ts src/bridgeVersion.test.ts"
40
40
  },
41
41
  "dependencies": {
42
42
  "better-sqlite3": "^11.10.0",