@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 +17 -7
- package/README.md +2 -0
- package/dist/activeConversation.d.ts +4 -0
- package/dist/activeConversation.d.ts.map +1 -0
- package/dist/activeConversation.js +34 -0
- package/dist/cli.js +6 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +5 -0
- package/dist/credentials.d.ts +3 -0
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +8 -0
- package/dist/cronWatcher.d.ts +13 -0
- package/dist/cronWatcher.d.ts.map +1 -0
- package/dist/cronWatcher.js +165 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +8 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/token.d.ts +2 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +4 -0
- package/package.json +1 -1
package/INTEGRATION.md
CHANGED
|
@@ -55,14 +55,28 @@ export async function resumeCleosConnection() {
|
|
|
55
55
|
|
|
56
56
|
## Cron job delivery
|
|
57
57
|
|
|
58
|
-
|
|
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);
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoC,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoC,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;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
|
});
|
package/dist/credentials.d.ts
CHANGED
|
@@ -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"}
|
package/dist/credentials.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/daemon.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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";
|
package/dist/token.d.ts
ADDED
|
@@ -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