@saleso.innovations/bridge 0.1.5 → 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
@@ -53,6 +53,38 @@ export async function resumeCleosConnection() {
53
53
  - Relay routing (Railway) between iOS app and Hermes WebSocket
54
54
  - Message history in Convex
55
55
 
56
+ ## Cron job delivery
57
+
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:
74
+
75
+ ```typescript
76
+ import { reconnectHermesAgent } from "@saleso.innovations/bridge";
77
+
78
+ const connection = await reconnectHermesAgent({ onUserMessage: handleCleosMessage });
79
+
80
+ connection.deliverCronResult(finalText, {
81
+ conversationId: "<convex-conversation-id>",
82
+ jobId: job.id,
83
+ jobName: job.name,
84
+ runAt: Date.now(),
85
+ });
86
+ ```
87
+
56
88
  ## What Hermes handles
57
89
 
58
90
  - Pairing UI (code entry)
@@ -72,7 +104,7 @@ Codes expire after 10 minutes. Each code links **one** Hermes instance to **one*
72
104
 
73
105
  ## Credentials
74
106
 
75
- 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.
76
108
 
77
109
  ## Self-hosting Cleos
78
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);
package/dist/client.d.ts CHANGED
@@ -8,20 +8,35 @@ export type ConnectOptions = {
8
8
  capabilities?: string[];
9
9
  onUserMessage?: (content: string, meta: UserMessageMeta, reply: AgentReply) => Promise<void> | void;
10
10
  };
11
+ export type UserMessageAttachment = {
12
+ storageId: string;
13
+ mimeType: string;
14
+ fileName: string;
15
+ size?: number;
16
+ url?: string;
17
+ };
11
18
  export type UserMessageMeta = {
12
19
  agentId: string;
13
20
  conversationId: string;
14
21
  messageId: string;
22
+ attachments?: UserMessageAttachment[];
15
23
  };
16
24
  export type AgentReply = {
17
25
  delta: (text: string, sequence: number) => void;
18
26
  complete: (text: string, sequence: number) => void;
19
27
  };
28
+ export type CronDeliveryMeta = {
29
+ conversationId: string;
30
+ jobId: string;
31
+ jobName: string;
32
+ runAt?: number;
33
+ };
20
34
  export type ConnectResult = {
21
35
  agentId: string;
22
36
  agentToken: string;
23
37
  close: () => void;
24
38
  closed: Promise<void>;
39
+ deliverCronResult: (content: string, meta: CronDeliveryMeta) => void;
25
40
  };
26
41
  export declare function pairCleosAgent(options: Omit<ConnectOptions, "onUserMessage">): Promise<{
27
42
  agentId: string;
@@ -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,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACpD,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB,CAAC;AA8HF,wBAAsB,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,eAAe,CAAC,GAAG,OAAO,CAAC;IAC5F,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC,CA0BD;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAcxF;AAED,wBAAsB,oBAAoB,CAAC,OAAO,GAAE;IAClD,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,cAAc,CAAC,eAAe,CAAC,CAAC;CAC5C,GAAG,OAAO,CAAC,aAAa,CAAC,CAc9B"}
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,8 +1,50 @@
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";
7
+ function parseUserMessageAttachments(raw) {
8
+ if (!Array.isArray(raw))
9
+ return undefined;
10
+ const attachments = [];
11
+ for (const item of raw) {
12
+ if (!item || typeof item !== "object")
13
+ continue;
14
+ const record = item;
15
+ const storageId = typeof record.storageId === "string" ? record.storageId : "";
16
+ const mimeType = typeof record.mimeType === "string" ? record.mimeType : "";
17
+ const fileName = typeof record.fileName === "string" ? record.fileName : "";
18
+ if (!storageId || !mimeType || !fileName)
19
+ continue;
20
+ attachments.push({
21
+ storageId,
22
+ mimeType,
23
+ fileName,
24
+ size: typeof record.size === "number" ? record.size : undefined,
25
+ url: typeof record.url === "string" ? record.url : undefined,
26
+ });
27
+ }
28
+ return attachments.length > 0 ? attachments : undefined;
29
+ }
30
+ function sendCronResult(ws, agentId, content, meta) {
31
+ ws.send(JSON.stringify({
32
+ type: "agent.message",
33
+ agentId,
34
+ conversationId: meta.conversationId,
35
+ messageId: randomUUID(),
36
+ content,
37
+ sequence: 0,
38
+ final: true,
39
+ metadata: {
40
+ cron: {
41
+ jobId: meta.jobId,
42
+ jobName: meta.jobName,
43
+ runAt: meta.runAt,
44
+ },
45
+ },
46
+ }));
47
+ }
6
48
  function createReplySender(ws, agentId, conversationId, messageId) {
7
49
  return {
8
50
  delta(text, sequence) {
@@ -52,10 +94,14 @@ async function openAgentConnection(options) {
52
94
  const content = typeof envelope.content === "string" ? envelope.content : "";
53
95
  const agentId = typeof envelope.agentId === "string" ? envelope.agentId : options.agentId;
54
96
  const conversationId = typeof envelope.conversationId === "string" ? envelope.conversationId : "unknown";
97
+ if (conversationId !== "unknown") {
98
+ rememberConversationId(conversationId);
99
+ }
100
+ const attachments = parseUserMessageAttachments(envelope.attachments);
55
101
  const replyMessageId = randomUUID();
56
102
  const reply = createReplySender(ws, agentId, conversationId, replyMessageId);
57
103
  if (options.onUserMessage) {
58
- await options.onUserMessage(content, { agentId, conversationId, messageId: replyMessageId }, reply);
104
+ await options.onUserMessage(content, { agentId, conversationId, messageId: replyMessageId, attachments }, reply);
59
105
  return;
60
106
  }
61
107
  const echo = `Echo: ${content}`;
@@ -80,6 +126,12 @@ async function openAgentConnection(options) {
80
126
  ws.close();
81
127
  },
82
128
  closed,
129
+ deliverCronResult(content, meta) {
130
+ if (ws.readyState !== WebSocket.OPEN) {
131
+ throw new Error("Cleos relay connection is not open");
132
+ }
133
+ sendCronResult(ws, options.agentId, content, meta);
134
+ },
83
135
  };
84
136
  }
85
137
  async function resolveRelayTargets(options) {
@@ -116,6 +168,7 @@ export async function pairCleosAgent(options) {
116
168
  agentToken: payload.agentToken,
117
169
  relayHttpUrl: pairing.relayHttpUrl,
118
170
  relayWsUrl: pairing.relayWsUrl,
171
+ convexSiteUrl: options.convexSiteUrl ?? convexSiteUrlFromEnv(),
119
172
  agentName: options.agentName ?? "Hermes",
120
173
  savedAt: Date.now(),
121
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"hermesForwarder.d.ts","sourceRoot":"","sources":["../src/hermesForwarder.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG/D,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAkBF,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,sBAA2B,GAAG;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;CACf,CAUA;AAqCD,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,eAAe,EACrB,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,IAAI,CAAC,CAwEf;AAED,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,sBAA2B,IAC/D,SAAS,MAAM,EAAE,MAAM,eAAe,EAAE,OAAO,UAAU,KAAG,OAAO,CAAC,IAAI,CAAC,CAMxF"}
1
+ {"version":3,"file":"hermesForwarder.d.ts","sourceRoot":"","sources":["../src/hermesForwarder.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAyB,eAAe,EAAE,MAAM,aAAa,CAAC;AAGtF,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAkBF,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,sBAA2B,GAAG;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;CACf,CAUA;AA2ED,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,eAAe,EACrB,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,IAAI,CAAC,CAyEf;AAED,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,sBAA2B,IAC/D,SAAS,MAAM,EAAE,MAAM,eAAe,EAAE,OAAO,UAAU,KAAG,OAAO,CAAC,IAAI,CAAC,CAMxF"}
@@ -64,6 +64,32 @@ function extractDeltaFromChunk(payload) {
64
64
  const content = delta.content;
65
65
  return typeof content === "string" ? content : null;
66
66
  }
67
+ function buildHermesUserContent(text, attachments) {
68
+ const trimmed = text.trim();
69
+ const imageParts = (attachments ?? [])
70
+ .filter((attachment) => attachment.mimeType.startsWith("image/") && attachment.url)
71
+ .map((attachment) => ({
72
+ type: "image_url",
73
+ image_url: { url: attachment.url },
74
+ }));
75
+ const fileNames = (attachments ?? [])
76
+ .filter((attachment) => !attachment.mimeType.startsWith("image/"))
77
+ .map((attachment) => attachment.fileName);
78
+ let prose = trimmed;
79
+ if (fileNames.length > 0) {
80
+ const fileNote = `[Attached files: ${fileNames.join(", ")}]`;
81
+ prose = prose.length === 0 ? fileNote : `${prose}\n\n${fileNote}`;
82
+ }
83
+ if (imageParts.length === 0) {
84
+ return prose.length === 0 ? "(attachment)" : prose;
85
+ }
86
+ const parts = [];
87
+ if (prose.length > 0) {
88
+ parts.push({ type: "text", text: prose });
89
+ }
90
+ parts.push(...imageParts);
91
+ return parts;
92
+ }
67
93
  export async function forwardToHermes(content, meta, reply, options = {}) {
68
94
  const { apiUrl, apiKey, model } = resolveHermesApiConfig(options);
69
95
  await ensureHermesReachable(apiUrl, apiKey);
@@ -73,10 +99,11 @@ export async function forwardToHermes(content, meta, reply, options = {}) {
73
99
  if (apiKey)
74
100
  headers.authorization = `Bearer ${apiKey}`;
75
101
  const conversationKey = options.conversationId ?? meta.conversationId;
102
+ const userContent = buildHermesUserContent(content, meta.attachments);
76
103
  const body = {
77
104
  model,
78
105
  stream: true,
79
- messages: [{ role: "user", content }],
106
+ messages: [{ role: "user", content: userContent }],
80
107
  user: conversationKey,
81
108
  };
82
109
  const response = await fetch(apiUrl, {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { connectHermesAgent, pairCleosAgent, reconnectHermesAgent } from "./client.js";
2
- export type { AgentReply, ConnectOptions, ConnectResult, UserMessageMeta } from "./client.js";
2
+ export type { AgentReply, ConnectOptions, ConnectResult, CronDeliveryMeta, UserMessageMeta } from "./client.js";
3
3
  export { loadCredentials, saveCredentials, credentialsPathForDisplay } from "./credentials.js";
4
4
  export type { SavedAgentCredentials } from "./credentials.js";
5
5
  export { resolvePairingCode, convexSiteUrlFromEnv } from "./resolve.js";
@@ -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,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9F,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.5",
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",