@saleso.innovations/bridge 0.1.6 → 0.1.7

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
@@ -34,6 +34,8 @@ cleos-bridge start # reconnecting daemon (systemd)
34
34
 
35
35
  Credentials are saved to `~/.cleos/agent.json`.
36
36
 
37
+ The daemon watches `~/.hermes/cron/output/` and forwards new cron results to the Cleos Jobs tab automatically.
38
+
37
39
  ## Hermes API
38
40
 
39
41
  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
+ }
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
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";
5
6
  import { prepareHermesForBridge } from "./ensureHermesApi.js";
6
7
  import { createHermesMessageHandler } from "./hermesForwarder.js";
@@ -38,7 +39,7 @@ async function main() {
38
39
  console.log(`Connected agent ${session.agentId}.`);
39
40
  console.log(`Credentials saved to ${credentialsPathForDisplay()}`);
40
41
  console.log("Press Ctrl+C to exit.");
41
- await waitForExit(session);
42
+ await waitForExit(session, () => startCronWatcher({ session }));
42
43
  return;
43
44
  }
44
45
  if (command === "start") {
@@ -61,7 +62,7 @@ async function main() {
61
62
  });
62
63
  console.log(`Reconnected agent ${session.agentId}.`);
63
64
  console.log("Press Ctrl+C to exit.");
64
- await waitForExit(session);
65
+ await waitForExit(session, () => startCronWatcher({ session }));
65
66
  return;
66
67
  }
67
68
  printUsage();
@@ -74,9 +75,11 @@ function printUsage() {
74
75
  console.error(" cleos-bridge start [CODE] Run as a reconnecting daemon (systemd)");
75
76
  console.error(" cleos-bridge reconnect Reconnect once using saved credentials");
76
77
  }
77
- async function waitForExit(session) {
78
+ async function waitForExit(session, onStart) {
79
+ const stop = onStart?.();
78
80
  await new Promise((resolve) => {
79
81
  const onSignal = () => {
82
+ stop?.();
80
83
  session.close();
81
84
  resolve();
82
85
  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
  });
@@ -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,6 +7,8 @@ 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
14
  export { DEFAULT_BRIDGE_INSTALL_URL, DEFAULT_CLEOS_CONVEX_SITE_URL, DEFAULT_HERMES_API_URL, bridgeInstallCommand, } from "./constants.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,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,6BAA6B,EAC7B,sBAAsB,EACtB,oBAAoB,GACrB,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
9
  export { DEFAULT_BRIDGE_INSTALL_URL, DEFAULT_CLEOS_CONVEX_SITE_URL, DEFAULT_HERMES_API_URL, bridgeInstallCommand, } 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
+ }
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.7",
4
4
  "description": "Connect your Hermes agent to the Cleos iOS app via pairing code.",
5
5
  "type": "module",
6
6
  "license": "MIT",