@saleso.innovations/bridge 0.1.15 → 0.1.17

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.
Files changed (40) hide show
  1. package/INTEGRATION.md +11 -0
  2. package/README.md +8 -1
  3. package/dist/cli.js +25 -0
  4. package/dist/client.d.ts +3 -2
  5. package/dist/client.d.ts.map +1 -1
  6. package/dist/client.js +6 -2
  7. package/dist/convexRelay.d.ts +24 -0
  8. package/dist/convexRelay.d.ts.map +1 -0
  9. package/dist/convexRelay.js +70 -0
  10. package/dist/cronBackfill.d.ts +16 -0
  11. package/dist/cronBackfill.d.ts.map +1 -0
  12. package/dist/cronBackfill.js +14 -0
  13. package/dist/cronWatcher.d.ts +14 -0
  14. package/dist/cronWatcher.d.ts.map +1 -1
  15. package/dist/cronWatcher.js +85 -41
  16. package/dist/hermesCommands.d.ts +1 -1
  17. package/dist/hermesCommands.d.ts.map +1 -1
  18. package/dist/hermesCommands.js +41 -0
  19. package/dist/hermesFileCommands.d.ts +19 -0
  20. package/dist/hermesFileCommands.d.ts.map +1 -0
  21. package/dist/hermesFileCommands.js +61 -0
  22. package/dist/hermesFiles.d.ts +60 -0
  23. package/dist/hermesFiles.d.ts.map +1 -0
  24. package/dist/hermesFiles.js +266 -0
  25. package/dist/hermesForwarder.d.ts +4 -1
  26. package/dist/hermesForwarder.d.ts.map +1 -1
  27. package/dist/hermesForwarder.js +80 -3
  28. package/dist/hermesSessionDb.d.ts +21 -0
  29. package/dist/hermesSessionDb.d.ts.map +1 -0
  30. package/dist/hermesSessionDb.js +174 -0
  31. package/dist/index.d.ts +5 -2
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3 -1
  34. package/dist/runtimeVersion.d.ts +14 -0
  35. package/dist/runtimeVersion.d.ts.map +1 -0
  36. package/dist/runtimeVersion.js +105 -0
  37. package/dist/skillsList.d.ts +9 -0
  38. package/dist/skillsList.d.ts.map +1 -0
  39. package/dist/skillsList.js +187 -0
  40. package/package.json +14 -3
@@ -0,0 +1,61 @@
1
+ import { DEFAULT_FILES_READ_MAX_BYTES, listHermesFiles, readHermesFileBytes, } from "./hermesFiles.js";
2
+ import { uploadFileToConvex } from "./convexRelay.js";
3
+ import { loadCredentials } from "./credentials.js";
4
+ function optionalNumber(args, key) {
5
+ const value = args[key];
6
+ if (typeof value === "number" && Number.isFinite(value))
7
+ return value;
8
+ if (typeof value === "string" && value.trim().length > 0) {
9
+ const parsed = Number.parseInt(value, 10);
10
+ if (Number.isFinite(parsed))
11
+ return parsed;
12
+ }
13
+ return undefined;
14
+ }
15
+ function optionalCategory(args) {
16
+ const value = args.category;
17
+ if (typeof value !== "string")
18
+ return "all";
19
+ const normalized = value.trim().toLowerCase();
20
+ if (normalized === "image" || normalized === "video" || normalized === "document") {
21
+ return normalized;
22
+ }
23
+ return "all";
24
+ }
25
+ export async function executeFilesList(args) {
26
+ const limit = optionalNumber(args, "limit");
27
+ const offset = optionalNumber(args, "offset");
28
+ const category = optionalCategory(args);
29
+ return listHermesFiles({ limit, offset, category });
30
+ }
31
+ export async function executeFilesRead(args) {
32
+ const path = typeof args.path === "string" ? args.path.trim() : "";
33
+ const maxBytes = optionalNumber(args, "maxBytes") ?? DEFAULT_FILES_READ_MAX_BYTES;
34
+ const { bytes, mimeType, fileName, size } = readHermesFileBytes(path);
35
+ if (size <= maxBytes) {
36
+ return {
37
+ fileName,
38
+ mimeType,
39
+ size,
40
+ contentBase64: bytes.toString("base64"),
41
+ };
42
+ }
43
+ const credentials = loadCredentials();
44
+ const agentId = credentials?.agentId;
45
+ if (!agentId) {
46
+ throw new Error("Large files require bridge credentials with agentId for Convex upload.");
47
+ }
48
+ const uploaded = await uploadFileToConvex({
49
+ agentId,
50
+ fileName,
51
+ mimeType,
52
+ contentBase64: bytes.toString("base64"),
53
+ });
54
+ return {
55
+ fileName,
56
+ mimeType,
57
+ size,
58
+ storageId: uploaded.storageId,
59
+ url: uploaded.url ?? undefined,
60
+ };
61
+ }
@@ -0,0 +1,60 @@
1
+ export declare const MAX_RELAY_UPLOAD_BYTES: number;
2
+ export declare const DEFAULT_FILES_READ_MAX_BYTES: number;
3
+ export type HermesFileCategory = "image" | "video" | "document" | "other";
4
+ export type HermesFileEntry = {
5
+ relativePath: string;
6
+ fileName: string;
7
+ size: number;
8
+ mtime: number;
9
+ mimeType: string;
10
+ category: HermesFileCategory;
11
+ };
12
+ export type HermesFileReadResult = {
13
+ fileName: string;
14
+ mimeType: string;
15
+ size: number;
16
+ contentBase64?: string;
17
+ storageId?: string;
18
+ url?: string;
19
+ };
20
+ export type MediaReference = {
21
+ kind: "local";
22
+ absolutePath: string;
23
+ raw: string;
24
+ } | {
25
+ kind: "url";
26
+ url: string;
27
+ raw: string;
28
+ };
29
+ export declare function resolveHermesHome(): string;
30
+ export declare function inferMimeType(fileName: string): string;
31
+ export declare function inferCategory(fileName: string): HermesFileCategory;
32
+ export declare function resolveSandboxedPath(inputPath: string): string;
33
+ export declare function listHermesFiles(options?: {
34
+ limit?: number;
35
+ offset?: number;
36
+ category?: HermesFileCategory | "all";
37
+ }): {
38
+ files: HermesFileEntry[];
39
+ };
40
+ export declare function readHermesFileBytes(inputPath: string): {
41
+ bytes: Buffer;
42
+ mimeType: string;
43
+ fileName: string;
44
+ size: number;
45
+ };
46
+ export declare function parseMediaReferences(text: string): MediaReference[];
47
+ export declare function stripMediaReferences(text: string, refs: MediaReference[]): string;
48
+ export declare function downloadUrlToBytes(url: string, timeoutMs?: number): Promise<{
49
+ bytes: Buffer;
50
+ mimeType: string;
51
+ fileName: string;
52
+ size: number;
53
+ }>;
54
+ export declare function resolveMediaReferenceBytes(ref: MediaReference): Promise<{
55
+ bytes: Buffer;
56
+ mimeType: string;
57
+ fileName: string;
58
+ size: number;
59
+ } | null>;
60
+ //# sourceMappingURL=hermesFiles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hermesFiles.d.ts","sourceRoot":"","sources":["../src/hermesFiles.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,QAAmB,CAAC;AACvD,eAAO,MAAM,4BAA4B,QAAa,CAAC;AAgCvD,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,GAAG,OAAO,CAAC;AAE1E,MAAM,MAAM,eAAe,GAAG;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,kBAAkB,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiCtD;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,kBAAkB,CAMlE;AAQD,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAe9D;AA8DD,wBAAgB,eAAe,CAAC,OAAO,GAAE;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,kBAAkB,GAAG,KAAK,CAAC;CAClC,GAAG;IAAE,KAAK,EAAE,eAAe,EAAE,CAAA;CAAE,CAepC;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAe1H;AAMD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,EAAE,CA6BnE;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,MAAM,CAMjF;AAED,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,SAAS,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAqBtJ;AAED,wBAAsB,0BAA0B,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CASzJ"}
@@ -0,0 +1,266 @@
1
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, join, relative, resolve, sep } from "node:path";
4
+ export const MAX_RELAY_UPLOAD_BYTES = 25 * 1024 * 1024;
5
+ export const DEFAULT_FILES_READ_MAX_BYTES = 512 * 1024;
6
+ const HIDDEN_DIR_NAMES = new Set([".git", ".hub"]);
7
+ const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "webp", "bmp", "tiff", "svg"]);
8
+ const VIDEO_EXTENSIONS = new Set(["mp4", "mov", "webm", "mkv", "avi"]);
9
+ const DOCUMENT_EXTENSIONS = new Set([
10
+ "pdf",
11
+ "txt",
12
+ "md",
13
+ "csv",
14
+ "json",
15
+ "xml",
16
+ "html",
17
+ "yaml",
18
+ "yml",
19
+ "log",
20
+ "docx",
21
+ "xlsx",
22
+ "pptx",
23
+ "odt",
24
+ "ods",
25
+ "odp",
26
+ "zip",
27
+ "rar",
28
+ "7z",
29
+ "tar",
30
+ "gz",
31
+ "bz2",
32
+ "epub",
33
+ ]);
34
+ export function resolveHermesHome() {
35
+ return process.env.HERMES_HOME?.trim() || join(homedir(), ".hermes");
36
+ }
37
+ export function inferMimeType(fileName) {
38
+ const ext = fileName.split(".").pop()?.toLowerCase() ?? "";
39
+ const map = {
40
+ png: "image/png",
41
+ jpg: "image/jpeg",
42
+ jpeg: "image/jpeg",
43
+ gif: "image/gif",
44
+ webp: "image/webp",
45
+ bmp: "image/bmp",
46
+ tiff: "image/tiff",
47
+ svg: "image/svg+xml",
48
+ mp4: "video/mp4",
49
+ mov: "video/quicktime",
50
+ webm: "video/webm",
51
+ mkv: "video/x-matroska",
52
+ avi: "video/x-msvideo",
53
+ pdf: "application/pdf",
54
+ txt: "text/plain",
55
+ md: "text/markdown",
56
+ csv: "text/csv",
57
+ json: "application/json",
58
+ xml: "application/xml",
59
+ html: "text/html",
60
+ yaml: "text/yaml",
61
+ yml: "text/yaml",
62
+ log: "text/plain",
63
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
64
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
65
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
66
+ zip: "application/zip",
67
+ epub: "application/epub+zip",
68
+ };
69
+ return map[ext] ?? "application/octet-stream";
70
+ }
71
+ export function inferCategory(fileName) {
72
+ const ext = fileName.split(".").pop()?.toLowerCase() ?? "";
73
+ if (IMAGE_EXTENSIONS.has(ext))
74
+ return "image";
75
+ if (VIDEO_EXTENSIONS.has(ext))
76
+ return "video";
77
+ if (DOCUMENT_EXTENSIONS.has(ext))
78
+ return "document";
79
+ return "other";
80
+ }
81
+ function isPathInsideRoot(root, candidate) {
82
+ const normalizedRoot = resolve(root);
83
+ const normalizedCandidate = resolve(candidate);
84
+ return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}${sep}`);
85
+ }
86
+ export function resolveSandboxedPath(inputPath) {
87
+ const home = resolveHermesHome();
88
+ const candidate = inputPath.startsWith("/") ? resolve(inputPath) : resolve(home, inputPath);
89
+ let resolved = candidate;
90
+ try {
91
+ if (existsSync(candidate)) {
92
+ resolved = realpathSync(candidate);
93
+ }
94
+ }
95
+ catch {
96
+ // Fall back to unresolved candidate for not-yet-existing paths.
97
+ }
98
+ if (!isPathInsideRoot(home, resolved)) {
99
+ throw new Error(`Path is outside Hermes home: ${inputPath}`);
100
+ }
101
+ return resolved;
102
+ }
103
+ function artifactScanRoots(home) {
104
+ const roots = [];
105
+ const direct = [
106
+ join(home, "cache", "images"),
107
+ join(home, "cache", "documents"),
108
+ join(home, "cache", "remote-syncs"),
109
+ join(home, "browser_recordings"),
110
+ ];
111
+ for (const dir of direct) {
112
+ if (existsSync(dir))
113
+ roots.push(dir);
114
+ }
115
+ const profilesDir = join(home, "profiles");
116
+ if (existsSync(profilesDir)) {
117
+ for (const profileName of readdirSync(profilesDir)) {
118
+ if (profileName.startsWith("."))
119
+ continue;
120
+ const imageCache = join(profilesDir, profileName, "image_cache");
121
+ if (existsSync(imageCache))
122
+ roots.push(imageCache);
123
+ }
124
+ }
125
+ return roots;
126
+ }
127
+ function walkFiles(rootDir, home, files) {
128
+ let names;
129
+ try {
130
+ names = readdirSync(rootDir);
131
+ }
132
+ catch {
133
+ return;
134
+ }
135
+ for (const name of names) {
136
+ if (name.startsWith(".") && HIDDEN_DIR_NAMES.has(name))
137
+ continue;
138
+ const fullPath = join(rootDir, name);
139
+ let stats;
140
+ try {
141
+ stats = statSync(fullPath);
142
+ }
143
+ catch {
144
+ continue;
145
+ }
146
+ if (stats.isDirectory()) {
147
+ walkFiles(fullPath, home, files);
148
+ continue;
149
+ }
150
+ if (!stats.isFile())
151
+ continue;
152
+ const relativePath = relative(home, fullPath).replace(/\\/g, "/");
153
+ const fileName = basename(fullPath);
154
+ const mimeType = inferMimeType(fileName);
155
+ files.push({
156
+ relativePath,
157
+ fileName,
158
+ size: stats.size,
159
+ mtime: stats.mtimeMs,
160
+ mimeType,
161
+ category: inferCategory(fileName),
162
+ });
163
+ }
164
+ }
165
+ export function listHermesFiles(options = {}) {
166
+ const home = resolveHermesHome();
167
+ const limit = options.limit ?? 200;
168
+ const offset = options.offset ?? 0;
169
+ const category = options.category ?? "all";
170
+ const all = [];
171
+ for (const root of artifactScanRoots(home)) {
172
+ walkFiles(root, home, all);
173
+ }
174
+ const filtered = category === "all" ? all : all.filter((file) => file.category === category);
175
+ filtered.sort((left, right) => right.mtime - left.mtime);
176
+ return { files: filtered.slice(offset, offset + limit) };
177
+ }
178
+ export function readHermesFileBytes(inputPath) {
179
+ const resolved = resolveSandboxedPath(inputPath);
180
+ if (!existsSync(resolved)) {
181
+ throw new Error(`File not found: ${inputPath}`);
182
+ }
183
+ const stats = statSync(resolved);
184
+ if (!stats.isFile()) {
185
+ throw new Error(`Not a file: ${inputPath}`);
186
+ }
187
+ if (stats.size > MAX_RELAY_UPLOAD_BYTES) {
188
+ throw new Error(`File exceeds maximum size (${MAX_RELAY_UPLOAD_BYTES} bytes)`);
189
+ }
190
+ const bytes = readFileSync(resolved);
191
+ const fileName = basename(resolved);
192
+ return { bytes, mimeType: inferMimeType(fileName), fileName, size: bytes.length };
193
+ }
194
+ const MEDIA_TAG_PATTERN = /MEDIA:(\/[^\s\n\r]+)/g;
195
+ const MARKDOWN_IMAGE_URL_PATTERN = /\[Image:[^\]|]*\|\s*(https?:\/\/[^\]\s]+)\]/gi;
196
+ const BARE_MEDIA_URL_PATTERN = /(https?:\/\/[^\s\n\r]+\.(?:png|jpe?g|gif|webp|mp4|mov|webm|pdf|docx?))/gi;
197
+ export function parseMediaReferences(text) {
198
+ const refs = [];
199
+ const seen = new Set();
200
+ for (const match of text.matchAll(MEDIA_TAG_PATTERN)) {
201
+ const absolutePath = match[1]?.trim();
202
+ const raw = match[0];
203
+ if (!absolutePath || seen.has(raw))
204
+ continue;
205
+ seen.add(raw);
206
+ refs.push({ kind: "local", absolutePath, raw });
207
+ }
208
+ for (const match of text.matchAll(MARKDOWN_IMAGE_URL_PATTERN)) {
209
+ const url = match[1]?.trim();
210
+ const raw = match[0];
211
+ if (!url || seen.has(raw))
212
+ continue;
213
+ seen.add(raw);
214
+ refs.push({ kind: "url", url, raw });
215
+ }
216
+ for (const match of text.matchAll(BARE_MEDIA_URL_PATTERN)) {
217
+ const url = match[1]?.trim();
218
+ const raw = match[0];
219
+ if (!url || seen.has(raw))
220
+ continue;
221
+ seen.add(raw);
222
+ refs.push({ kind: "url", url, raw });
223
+ }
224
+ return refs;
225
+ }
226
+ export function stripMediaReferences(text, refs) {
227
+ let cleaned = text;
228
+ for (const ref of refs) {
229
+ cleaned = cleaned.replace(ref.raw, "");
230
+ }
231
+ return cleaned.replace(/\n{3,}/g, "\n\n").trim();
232
+ }
233
+ export async function downloadUrlToBytes(url, timeoutMs = 30_000) {
234
+ const controller = new AbortController();
235
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
236
+ try {
237
+ const response = await fetch(url, { signal: controller.signal });
238
+ if (!response.ok) {
239
+ throw new Error(`Failed to download media (${response.status})`);
240
+ }
241
+ const arrayBuffer = await response.arrayBuffer();
242
+ const bytes = Buffer.from(arrayBuffer);
243
+ if (bytes.length > MAX_RELAY_UPLOAD_BYTES) {
244
+ throw new Error(`Downloaded file exceeds maximum size (${MAX_RELAY_UPLOAD_BYTES} bytes)`);
245
+ }
246
+ const contentType = response.headers.get("content-type")?.split(";")[0]?.trim();
247
+ const urlPath = new URL(url).pathname;
248
+ const fileName = basename(urlPath) || "download";
249
+ const mimeType = contentType && contentType !== "application/octet-stream" ? contentType : inferMimeType(fileName);
250
+ return { bytes, mimeType, fileName, size: bytes.length };
251
+ }
252
+ finally {
253
+ clearTimeout(timer);
254
+ }
255
+ }
256
+ export async function resolveMediaReferenceBytes(ref) {
257
+ try {
258
+ if (ref.kind === "local") {
259
+ return readHermesFileBytes(ref.absolutePath);
260
+ }
261
+ return await downloadUrlToBytes(ref.url);
262
+ }
263
+ catch {
264
+ return null;
265
+ }
266
+ }
@@ -5,11 +5,14 @@ export type HermesForwarderOptions = {
5
5
  model?: string;
6
6
  conversationId?: string;
7
7
  };
8
+ export type HermesForwardResult = {
9
+ sessionId: string;
10
+ };
8
11
  export declare function resolveHermesApiConfig(options?: HermesForwarderOptions): {
9
12
  apiUrl: string;
10
13
  apiKey: string | undefined;
11
14
  model: string;
12
15
  };
13
- export declare function forwardToHermes(content: string, meta: UserMessageMeta, reply: AgentReply, options?: HermesForwarderOptions): Promise<void>;
16
+ export declare function forwardToHermes(content: string, meta: UserMessageMeta, reply: AgentReply, options?: HermesForwarderOptions): Promise<HermesForwardResult>;
14
17
  export declare function createHermesMessageHandler(options?: HermesForwarderOptions): (content: string, meta: UserMessageMeta, reply: AgentReply) => Promise<void>;
15
18
  //# sourceMappingURL=hermesForwarder.d.ts.map
@@ -1 +1 @@
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;AAyID,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,eAAe,EACrB,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,IAAI,CAAC,CA4Df;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;AAWtF,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;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAC;CACnB,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;AA+MD,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,eAAe,EACrB,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,mBAAmB,CAAC,CAuE9B;AAED,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,sBAA2B,IAC/D,SAAS,MAAM,EAAE,MAAM,eAAe,EAAE,OAAO,UAAU,KAAG,OAAO,CAAC,IAAI,CAAC,CAMxF"}
@@ -1,7 +1,10 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { linkHermesMessageIds, uploadFileToConvex } from "./convexRelay.js";
4
5
  import { DEFAULT_HERMES_API_URL } from "./constants.js";
6
+ import { getLatestTurnMessageIds, hermesStateDbExists } from "./hermesSessionDb.js";
7
+ import { parseMediaReferences, resolveMediaReferenceBytes, stripMediaReferences, } from "./hermesFiles.js";
5
8
  function readHermesApiKeyFromEnvFile() {
6
9
  const envPath = join(homedir(), ".hermes", ".env");
7
10
  try {
@@ -136,21 +139,89 @@ function buildHermesUserContent(text, attachments) {
136
139
  parts.push(...imageParts);
137
140
  return parts;
138
141
  }
142
+ async function uploadMediaAttachments(agentId, refs) {
143
+ const attachments = [];
144
+ for (const ref of refs) {
145
+ const resolved = await resolveMediaReferenceBytes(ref);
146
+ if (!resolved)
147
+ continue;
148
+ try {
149
+ const uploaded = await uploadFileToConvex({
150
+ agentId,
151
+ fileName: resolved.fileName,
152
+ mimeType: resolved.mimeType,
153
+ contentBase64: resolved.bytes.toString("base64"),
154
+ });
155
+ attachments.push({
156
+ storageId: uploaded.storageId,
157
+ mimeType: uploaded.mimeType,
158
+ fileName: uploaded.fileName,
159
+ size: uploaded.size,
160
+ url: uploaded.url ?? undefined,
161
+ });
162
+ }
163
+ catch (error) {
164
+ const message = error instanceof Error ? error.message : String(error);
165
+ console.error(JSON.stringify({ event: "cleos-bridge.media_upload_failed", message }));
166
+ }
167
+ }
168
+ return attachments;
169
+ }
170
+ async function linkTurnToConvex(meta, sessionId) {
171
+ if (!hermesStateDbExists())
172
+ return;
173
+ const { userMessageId, assistantMessageId } = getLatestTurnMessageIds(sessionId);
174
+ const links = [];
175
+ if (userMessageId !== undefined) {
176
+ links.push({
177
+ clientMessageId: meta.clientMessageId,
178
+ hermesMessageId: String(userMessageId),
179
+ });
180
+ }
181
+ if (assistantMessageId !== undefined) {
182
+ links.push({
183
+ externalMessageId: meta.replyMessageId,
184
+ hermesMessageId: String(assistantMessageId),
185
+ });
186
+ }
187
+ if (links.length === 0)
188
+ return;
189
+ for (let attempt = 0; attempt < 5; attempt += 1) {
190
+ try {
191
+ await linkHermesMessageIds({
192
+ agentId: meta.agentId,
193
+ conversationId: meta.conversationId,
194
+ links,
195
+ });
196
+ return;
197
+ }
198
+ catch (error) {
199
+ const message = error instanceof Error ? error.message : String(error);
200
+ if (attempt === 4) {
201
+ console.error(JSON.stringify({ event: "cleos-bridge.hermes_link_failed", message }));
202
+ return;
203
+ }
204
+ await new Promise((resolve) => setTimeout(resolve, 250 * (attempt + 1)));
205
+ }
206
+ }
207
+ }
139
208
  export async function forwardToHermes(content, meta, reply, options = {}) {
140
209
  const { apiUrl, apiKey, model } = resolveHermesApiConfig(options);
141
210
  await ensureHermesReachable(apiUrl, apiKey);
211
+ const sessionId = options.conversationId ?? meta.conversationId;
142
212
  const headers = {
143
213
  "content-type": "application/json",
144
214
  };
145
215
  if (apiKey)
146
216
  headers.authorization = `Bearer ${apiKey}`;
147
- const conversationKey = options.conversationId ?? meta.conversationId;
217
+ if (sessionId)
218
+ headers["X-Hermes-Session-Id"] = sessionId;
148
219
  const userContent = buildHermesUserContent(content, meta.attachments);
149
220
  const body = {
150
221
  model,
151
222
  stream: true,
152
223
  messages: [{ role: "user", content: userContent }],
153
- user: conversationKey,
224
+ user: sessionId,
154
225
  };
155
226
  const response = await fetch(apiUrl, {
156
227
  method: "POST",
@@ -161,6 +232,7 @@ export async function forwardToHermes(content, meta, reply, options = {}) {
161
232
  const text = await response.text();
162
233
  throw new Error(`Hermes request failed (${response.status}): ${text}`);
163
234
  }
235
+ const resolvedSessionId = response.headers.get("X-Hermes-Session-Id")?.trim() || sessionId;
164
236
  if (!response.body) {
165
237
  throw new Error("Hermes returned an empty streaming body");
166
238
  }
@@ -187,7 +259,12 @@ export async function forwardToHermes(content, meta, reply, options = {}) {
187
259
  reply.delta(fullText, counters.textSequence);
188
260
  counters.textSequence += 1;
189
261
  }
190
- reply.complete(fullText, counters.textSequence);
262
+ const mediaRefs = parseMediaReferences(fullText);
263
+ const attachments = mediaRefs.length > 0 ? await uploadMediaAttachments(meta.agentId, mediaRefs) : [];
264
+ const displayText = attachments.length > 0 ? stripMediaReferences(fullText, mediaRefs) : fullText.trim() || fullText;
265
+ reply.complete(displayText, counters.textSequence, attachments.length > 0 ? attachments : undefined);
266
+ await linkTurnToConvex(meta, resolvedSessionId);
267
+ return { sessionId: resolvedSessionId };
191
268
  }
192
269
  export function createHermesMessageHandler(options = {}) {
193
270
  return async (content, meta, reply) => {
@@ -0,0 +1,21 @@
1
+ export type HermesSessionMessage = {
2
+ id: number;
3
+ sessionId: string;
4
+ role: string;
5
+ content: string;
6
+ timestamp: number;
7
+ };
8
+ export declare function listSessionMessages(sessionId: string, options?: {
9
+ limit?: number;
10
+ offset?: number;
11
+ offsetFromEnd?: number;
12
+ }): HermesSessionMessage[];
13
+ export declare function getLatestTurnMessageIds(sessionId: string): {
14
+ userMessageId?: number;
15
+ assistantMessageId?: number;
16
+ };
17
+ export declare function hermesStateDbExists(): boolean;
18
+ export declare function countUserMessagesSent(): {
19
+ count: number;
20
+ };
21
+ //# sourceMappingURL=hermesSessionDb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hermesSessionDb.d.ts","sourceRoot":"","sources":["../src/hermesSessionDb.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAgGF,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO,GACxE,oBAAoB,EAAE,CAwCxB;AAED,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,GAChB;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE,CAmCzD;AAED,wBAAgB,mBAAmB,IAAI,OAAO,CAO7C;AAED,wBAAgB,qBAAqB,IAAI;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAgBzD"}