@saleso.innovations/bridge 0.1.22 → 0.1.23

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.
@@ -1,4 +1,4 @@
1
- export declare const HERMES_COMMAND_NAMES: readonly ["runtime.health", "runtime.detailedHealth", "runtime.version", "runtime.capabilities", "models.list", "model.set", "responses.create", "runs.create", "runs.status", "runs.stop", "jobs.list", "jobs.get", "jobs.create", "jobs.update", "jobs.pause", "jobs.resume", "jobs.runNow", "jobs.delete", "profiles.list", "profiles.create", "gateway.start", "gateway.stop", "gateway.restart", "hermes.update", "sessions.messages.list", "sessions.messages.countSent", "skills.list", "files.list", "files.read"];
1
+ export declare const HERMES_COMMAND_NAMES: readonly ["runtime.health", "runtime.detailedHealth", "runtime.version", "runtime.capabilities", "models.list", "model.set", "responses.create", "runs.create", "runs.status", "runs.stop", "jobs.list", "jobs.get", "jobs.create", "jobs.update", "jobs.pause", "jobs.resume", "jobs.runNow", "jobs.delete", "profiles.list", "profiles.create", "gateway.start", "gateway.stop", "gateway.restart", "hermes.update", "sessions.messages.list", "sessions.messages.countSent", "skills.list", "files.list", "files.read", "files.write", "memories.list"];
2
2
  export type HermesCommandName = (typeof HERMES_COMMAND_NAMES)[number];
3
3
  export declare function isHermesCommandName(value: string): value is HermesCommandName;
4
4
  export type HermesCommandErrorCode = "command_unsupported" | "hermes_unreachable" | "hermes_request_failed" | "invalid_command_args" | "unsupported_by_http";
@@ -1 +1 @@
1
- {"version":3,"file":"hermesCommands.d.ts","sourceRoot":"","sources":["../src/hermesCommands.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,oBAAoB,4fA8BvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,iBAAiB,CAE7E;AAED,MAAM,MAAM,sBAAsB,GAC9B,qBAAqB,GACrB,oBAAoB,GACpB,uBAAuB,GACvB,sBAAsB,GACtB,qBAAqB,CAAC;AAE1B,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;gBAE1B,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM;CAI1D;AAgGD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GACjE,OAAO,CAAC,OAAO,CAAC,CAwLlB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAetG"}
1
+ {"version":3,"file":"hermesCommands.d.ts","sourceRoot":"","sources":["../src/hermesCommands.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,oBAAoB,4hBAgCvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,iBAAiB,CAE7E;AAED,MAAM,MAAM,sBAAsB,GAC9B,qBAAqB,GACrB,oBAAoB,GACpB,uBAAuB,GACvB,sBAAsB,GACtB,qBAAqB,CAAC;AAE1B,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;gBAE1B,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM;CAI1D;AAgGD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GACjE,OAAO,CAAC,OAAO,CAAC,CAqMlB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAetG"}
@@ -4,7 +4,7 @@ import { listHermesCronJobs } from "./cronList.js";
4
4
  import { listSessionMessages, countUserMessagesSent } from "./hermesSessionDb.js";
5
5
  import { listHermesSkills } from "./skillsList.js";
6
6
  import { runHermesUpdate } from "./hermesUpdate.js";
7
- import { executeFilesList, executeFilesRead } from "./hermesFileCommands.js";
7
+ import { executeFilesList, executeFilesRead, executeFilesWrite, executeMemoriesList, } from "./hermesFileCommands.js";
8
8
  import { fetchHermesRuntimeVersion } from "./runtimeVersion.js";
9
9
  export const HERMES_COMMAND_NAMES = [
10
10
  "runtime.health",
@@ -36,6 +36,8 @@ export const HERMES_COMMAND_NAMES = [
36
36
  "skills.list",
37
37
  "files.list",
38
38
  "files.read",
39
+ "files.write",
40
+ "memories.list",
39
41
  ];
40
42
  export function isHermesCommandName(value) {
41
43
  return HERMES_COMMAND_NAMES.includes(value);
@@ -297,6 +299,19 @@ export async function executeHermesCommand(command, args, options = {}) {
297
299
  }
298
300
  return await executeFilesRead({ ...args, path });
299
301
  }
302
+ case "files.write": {
303
+ const path = optionalString(args, "path");
304
+ if (!path) {
305
+ throw new HermesCommandError("invalid_command_args", 'Missing "path"');
306
+ }
307
+ const content = args.content;
308
+ if (typeof content !== "string") {
309
+ throw new HermesCommandError("invalid_command_args", 'Missing "content"');
310
+ }
311
+ return await executeFilesWrite({ path, content });
312
+ }
313
+ case "memories.list":
314
+ return await executeMemoriesList();
300
315
  default: {
301
316
  const _exhaustive = command;
302
317
  throw new HermesCommandError("command_unsupported", `Unsupported command: ${String(_exhaustive)}`);
@@ -0,0 +1,4 @@
1
+ export declare function resolveHermesHomeForCron(): string;
2
+ export declare function hermesCronJobsFilePath(home?: string): string;
3
+ export declare function loadHermesCronJobNames(home?: string): Map<string, string>;
4
+ //# sourceMappingURL=hermesCronJobs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hermesCronJobs.d.ts","sourceRoot":"","sources":["../src/hermesCronJobs.ts"],"names":[],"mappings":"AASA,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED,wBAAgB,sBAAsB,CAAC,IAAI,GAAE,MAAmC,GAAG,MAAM,CAExF;AAED,wBAAgB,sBAAsB,CAAC,IAAI,GAAE,MAAmC,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAsBrG"}
@@ -0,0 +1,32 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ export function resolveHermesHomeForCron() {
5
+ return process.env.HERMES_HOME?.trim() || join(homedir(), ".hermes");
6
+ }
7
+ export function hermesCronJobsFilePath(home = resolveHermesHomeForCron()) {
8
+ return join(home, "cron", "jobs.json");
9
+ }
10
+ export function loadHermesCronJobNames(home = resolveHermesHomeForCron()) {
11
+ const names = new Map();
12
+ const jobsFile = hermesCronJobsFilePath(home);
13
+ if (!existsSync(jobsFile))
14
+ return names;
15
+ try {
16
+ const parsed = JSON.parse(readFileSync(jobsFile, "utf8"));
17
+ const jobs = Array.isArray(parsed)
18
+ ? parsed
19
+ : parsed && typeof parsed === "object" && Array.isArray(parsed.jobs)
20
+ ? (parsed.jobs ?? [])
21
+ : [];
22
+ for (const job of jobs) {
23
+ if (!job?.id)
24
+ continue;
25
+ names.set(job.id, job.name?.trim() || job.id);
26
+ }
27
+ }
28
+ catch {
29
+ // Ignore malformed jobs file; fall back to job id as name.
30
+ }
31
+ return names;
32
+ }
@@ -1,6 +1,14 @@
1
1
  export declare function executeFilesList(args: Record<string, unknown>): Promise<{
2
2
  files: import("./hermesFiles.js").HermesFileEntry[];
3
3
  }>;
4
+ export declare function executeFilesWrite(args: Record<string, unknown>): Promise<{
5
+ ok: true;
6
+ relativePath: string;
7
+ size: number;
8
+ }>;
9
+ export declare function executeMemoriesList(): Promise<{
10
+ files: import("./hermesFiles.js").HermesFileEntry[];
11
+ }>;
4
12
  export declare function executeFilesRead(args: Record<string, unknown>): Promise<{
5
13
  fileName: string;
6
14
  mimeType: string;
@@ -1 +1 @@
1
- {"version":3,"file":"hermesFileCommands.d.ts","sourceRoot":"","sources":["../src/hermesFileCommands.ts"],"names":[],"mappings":"AA6BA,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;GAKnE;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;;;;;;;;;GAkCnE"}
1
+ {"version":3,"file":"hermesFileCommands.d.ts","sourceRoot":"","sources":["../src/hermesFileCommands.ts"],"names":[],"mappings":"AA+BA,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;GAKnE;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;GAUpE;AAED,wBAAsB,mBAAmB;;GAExC;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;;;;;;;;;GAkCnE"}
@@ -1,4 +1,4 @@
1
- import { DEFAULT_FILES_READ_MAX_BYTES, listHermesFiles, readHermesFileBytes, } from "./hermesFiles.js";
1
+ import { DEFAULT_FILES_READ_MAX_BYTES, listHermesFiles, listHermesMemoryFiles, readHermesFileBytes, writeHermesFile, } from "./hermesFiles.js";
2
2
  import { uploadFileToConvex } from "./convexRelay.js";
3
3
  import { loadCredentials } from "./credentials.js";
4
4
  function optionalNumber(args, key) {
@@ -28,6 +28,20 @@ export async function executeFilesList(args) {
28
28
  const category = optionalCategory(args);
29
29
  return listHermesFiles({ limit, offset, category });
30
30
  }
31
+ export async function executeFilesWrite(args) {
32
+ const path = typeof args.path === "string" ? args.path.trim() : "";
33
+ if (!path) {
34
+ throw new Error('Missing "path"');
35
+ }
36
+ const content = typeof args.content === "string" ? args.content : undefined;
37
+ if (content === undefined) {
38
+ throw new Error('Missing "content"');
39
+ }
40
+ return writeHermesFile(path, content);
41
+ }
42
+ export async function executeMemoriesList() {
43
+ return listHermesMemoryFiles();
44
+ }
31
45
  export async function executeFilesRead(args) {
32
46
  const path = typeof args.path === "string" ? args.path.trim() : "";
33
47
  const maxBytes = optionalNumber(args, "maxBytes") ?? DEFAULT_FILES_READ_MAX_BYTES;
@@ -1,6 +1,13 @@
1
1
  export declare const MAX_RELAY_UPLOAD_BYTES: number;
2
2
  export declare const DEFAULT_FILES_READ_MAX_BYTES: number;
3
+ export declare const SOUL_RELATIVE_PATH = "SOUL.md";
4
+ export declare const MEMORIES_DIR = "memories";
5
+ export declare const MEMORY_MD_RELATIVE_PATH = "memories/MEMORY.md";
6
+ export declare const USER_MD_RELATIVE_PATH = "memories/USER.md";
7
+ export declare const MEMORY_MD_CHAR_LIMIT = 2200;
8
+ export declare const USER_MD_CHAR_LIMIT = 1375;
3
9
  export type HermesFileCategory = "image" | "video" | "document" | "other";
10
+ export type HermesFileSource = "cron" | "artifact";
4
11
  export type HermesFileEntry = {
5
12
  relativePath: string;
6
13
  fileName: string;
@@ -8,7 +15,12 @@ export type HermesFileEntry = {
8
15
  mtime: number;
9
16
  mimeType: string;
10
17
  category: HermesFileCategory;
18
+ source?: HermesFileSource;
19
+ sourceKey?: string;
20
+ hermesJobId?: string;
21
+ hermesJobName?: string;
11
22
  };
23
+ export declare function cronOutputSourceKey(relativePath: string): string | undefined;
12
24
  export type HermesFileReadResult = {
13
25
  fileName: string;
14
26
  mimeType: string;
@@ -37,6 +49,16 @@ export declare function listHermesFiles(options?: {
37
49
  }): {
38
50
  files: HermesFileEntry[];
39
51
  };
52
+ export declare function normalizeHermesRelativePath(inputPath: string): string;
53
+ export declare function assertWritableHermesPath(inputPath: string): string;
54
+ export declare function writeHermesFile(inputPath: string, content: string): {
55
+ ok: true;
56
+ relativePath: string;
57
+ size: number;
58
+ };
59
+ export declare function listHermesMemoryFiles(): {
60
+ files: HermesFileEntry[];
61
+ };
40
62
  export declare function readHermesFileBytes(inputPath: string): {
41
63
  bytes: Buffer;
42
64
  mimeType: string;
@@ -1 +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"}
1
+ {"version":3,"file":"hermesFiles.d.ts","sourceRoot":"","sources":["../src/hermesFiles.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,16 @@
1
- import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync, } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { basename, join, relative, resolve, sep } from "node:path";
3
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
4
+ import { loadHermesCronJobNames } from "./hermesCronJobs.js";
4
5
  export const MAX_RELAY_UPLOAD_BYTES = 25 * 1024 * 1024;
5
6
  export const DEFAULT_FILES_READ_MAX_BYTES = 512 * 1024;
7
+ export const SOUL_RELATIVE_PATH = "SOUL.md";
8
+ export const MEMORIES_DIR = "memories";
9
+ export const MEMORY_MD_RELATIVE_PATH = "memories/MEMORY.md";
10
+ export const USER_MD_RELATIVE_PATH = "memories/USER.md";
11
+ export const MEMORY_MD_CHAR_LIMIT = 2200;
12
+ export const USER_MD_CHAR_LIMIT = 1375;
13
+ const KNOWN_MEMORY_FILES = ["MEMORY.md", "USER.md"];
6
14
  const HIDDEN_DIR_NAMES = new Set([".git", ".hub"]);
7
15
  const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "webp", "bmp", "tiff", "svg"]);
8
16
  const VIDEO_EXTENSIONS = new Set(["mp4", "mov", "webm", "mkv", "avi"]);
@@ -31,6 +39,13 @@ const DOCUMENT_EXTENSIONS = new Set([
31
39
  "bz2",
32
40
  "epub",
33
41
  ]);
42
+ const CRON_OUTPUT_PREFIX = "cron/output/";
43
+ export function cronOutputSourceKey(relativePath) {
44
+ if (!relativePath.startsWith(CRON_OUTPUT_PREFIX))
45
+ return undefined;
46
+ const key = relativePath.slice(CRON_OUTPUT_PREFIX.length);
47
+ return key.length > 0 ? key : undefined;
48
+ }
34
49
  export function resolveHermesHome() {
35
50
  return process.env.HERMES_HOME?.trim() || join(homedir(), ".hermes");
36
51
  }
@@ -78,27 +93,57 @@ export function inferCategory(fileName) {
78
93
  return "document";
79
94
  return "other";
80
95
  }
96
+ function normalizePathForSandbox(root, candidate) {
97
+ let normalizedRoot = resolve(root);
98
+ let normalizedCandidate = resolve(candidate);
99
+ try {
100
+ if (existsSync(normalizedRoot)) {
101
+ normalizedRoot = realpathSync(normalizedRoot);
102
+ }
103
+ }
104
+ catch {
105
+ // Keep resolved path.
106
+ }
107
+ try {
108
+ if (existsSync(normalizedCandidate)) {
109
+ normalizedCandidate = realpathSync(normalizedCandidate);
110
+ }
111
+ else {
112
+ let parent = normalizedCandidate;
113
+ while (parent !== dirname(parent)) {
114
+ parent = dirname(parent);
115
+ if (existsSync(parent)) {
116
+ const resolvedParent = realpathSync(parent);
117
+ normalizedCandidate = join(resolvedParent, relative(parent, normalizedCandidate));
118
+ break;
119
+ }
120
+ }
121
+ }
122
+ }
123
+ catch {
124
+ // Keep resolved path.
125
+ }
126
+ return { root: normalizedRoot, candidate: normalizedCandidate };
127
+ }
81
128
  function isPathInsideRoot(root, candidate) {
82
- const normalizedRoot = resolve(root);
83
- const normalizedCandidate = resolve(candidate);
129
+ const { root: normalizedRoot, candidate: normalizedCandidate } = normalizePathForSandbox(root, candidate);
84
130
  return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}${sep}`);
85
131
  }
86
132
  export function resolveSandboxedPath(inputPath) {
87
133
  const home = resolveHermesHome();
88
134
  const candidate = inputPath.startsWith("/") ? resolve(inputPath) : resolve(home, inputPath);
89
- let resolved = candidate;
135
+ if (!isPathInsideRoot(home, candidate)) {
136
+ throw new Error(`Path is outside Hermes home: ${inputPath}`);
137
+ }
90
138
  try {
91
139
  if (existsSync(candidate)) {
92
- resolved = realpathSync(candidate);
140
+ return realpathSync(candidate);
93
141
  }
94
142
  }
95
143
  catch {
96
144
  // Fall back to unresolved candidate for not-yet-existing paths.
97
145
  }
98
- if (!isPathInsideRoot(home, resolved)) {
99
- throw new Error(`Path is outside Hermes home: ${inputPath}`);
100
- }
101
- return resolved;
146
+ return candidate;
102
147
  }
103
148
  function artifactScanRoots(home) {
104
149
  const roots = [];
@@ -107,6 +152,7 @@ function artifactScanRoots(home) {
107
152
  join(home, "cache", "documents"),
108
153
  join(home, "cache", "remote-syncs"),
109
154
  join(home, "browser_recordings"),
155
+ join(home, "cron", "output"),
110
156
  ];
111
157
  for (const dir of direct) {
112
158
  if (existsSync(dir))
@@ -124,7 +170,22 @@ function artifactScanRoots(home) {
124
170
  }
125
171
  return roots;
126
172
  }
127
- function walkFiles(rootDir, home, files) {
173
+ function attachSourceMetadata(entry, jobNames) {
174
+ const sourceKey = cronOutputSourceKey(entry.relativePath);
175
+ if (!sourceKey) {
176
+ return { ...entry, source: "artifact" };
177
+ }
178
+ const hermesJobId = sourceKey.split("/")[0] ?? undefined;
179
+ const hermesJobName = hermesJobId ? jobNames.get(hermesJobId) ?? hermesJobId : undefined;
180
+ return {
181
+ ...entry,
182
+ source: "cron",
183
+ sourceKey,
184
+ hermesJobId,
185
+ hermesJobName,
186
+ };
187
+ }
188
+ function walkFiles(rootDir, home, files, jobNames) {
128
189
  let names;
129
190
  try {
130
191
  names = readdirSync(rootDir);
@@ -144,7 +205,7 @@ function walkFiles(rootDir, home, files) {
144
205
  continue;
145
206
  }
146
207
  if (stats.isDirectory()) {
147
- walkFiles(fullPath, home, files);
208
+ walkFiles(fullPath, home, files, jobNames);
148
209
  continue;
149
210
  }
150
211
  if (!stats.isFile())
@@ -152,14 +213,15 @@ function walkFiles(rootDir, home, files) {
152
213
  const relativePath = relative(home, fullPath).replace(/\\/g, "/");
153
214
  const fileName = basename(fullPath);
154
215
  const mimeType = inferMimeType(fileName);
155
- files.push({
216
+ const entry = attachSourceMetadata({
156
217
  relativePath,
157
218
  fileName,
158
219
  size: stats.size,
159
220
  mtime: stats.mtimeMs,
160
221
  mimeType,
161
222
  category: inferCategory(fileName),
162
- });
223
+ }, jobNames);
224
+ files.push(entry);
163
225
  }
164
226
  }
165
227
  export function listHermesFiles(options = {}) {
@@ -167,14 +229,109 @@ export function listHermesFiles(options = {}) {
167
229
  const limit = options.limit ?? 200;
168
230
  const offset = options.offset ?? 0;
169
231
  const category = options.category ?? "all";
232
+ const jobNames = loadHermesCronJobNames(home);
170
233
  const all = [];
171
234
  for (const root of artifactScanRoots(home)) {
172
- walkFiles(root, home, all);
235
+ walkFiles(root, home, all, jobNames);
173
236
  }
174
237
  const filtered = category === "all" ? all : all.filter((file) => file.category === category);
175
238
  filtered.sort((left, right) => right.mtime - left.mtime);
176
239
  return { files: filtered.slice(offset, offset + limit) };
177
240
  }
241
+ export function normalizeHermesRelativePath(inputPath) {
242
+ return inputPath.trim().replace(/\\/g, "/").replace(/^\/+/, "");
243
+ }
244
+ export function assertWritableHermesPath(inputPath) {
245
+ const relativePath = normalizeHermesRelativePath(inputPath);
246
+ if (!relativePath) {
247
+ throw new Error("Path is required");
248
+ }
249
+ if (relativePath === SOUL_RELATIVE_PATH) {
250
+ return relativePath;
251
+ }
252
+ const memoriesPrefix = `${MEMORIES_DIR}/`;
253
+ if (!relativePath.startsWith(memoriesPrefix)) {
254
+ throw new Error(`Path is not writable: ${inputPath}`);
255
+ }
256
+ const remainder = relativePath.slice(memoriesPrefix.length);
257
+ if (!remainder || remainder.includes("/") || remainder.includes("..")) {
258
+ throw new Error(`Path is not writable: ${inputPath}`);
259
+ }
260
+ if (!remainder.endsWith(".md")) {
261
+ throw new Error(`Path is not writable: ${inputPath}`);
262
+ }
263
+ return relativePath;
264
+ }
265
+ function memoryCharLimitForPath(relativePath) {
266
+ if (relativePath === MEMORY_MD_RELATIVE_PATH)
267
+ return MEMORY_MD_CHAR_LIMIT;
268
+ if (relativePath === USER_MD_RELATIVE_PATH)
269
+ return USER_MD_CHAR_LIMIT;
270
+ return undefined;
271
+ }
272
+ export function writeHermesFile(inputPath, content) {
273
+ const relativePath = assertWritableHermesPath(inputPath);
274
+ const charLimit = memoryCharLimitForPath(relativePath);
275
+ if (charLimit !== undefined && content.length > charLimit) {
276
+ throw new Error(`Content exceeds ${charLimit} character limit for ${relativePath} (${content.length} chars)`);
277
+ }
278
+ const home = resolveHermesHome();
279
+ const resolved = resolveSandboxedPath(relativePath);
280
+ if (relativePath.startsWith(`${MEMORIES_DIR}/`)) {
281
+ mkdirSync(join(home, MEMORIES_DIR), { recursive: true });
282
+ }
283
+ const bytes = Buffer.from(content, "utf8");
284
+ writeFileSync(resolved, bytes);
285
+ return { ok: true, relativePath, size: bytes.length };
286
+ }
287
+ function memoryFileEntry(home, fileName) {
288
+ const relativePath = `${MEMORIES_DIR}/${fileName}`;
289
+ const fullPath = join(home, MEMORIES_DIR, fileName);
290
+ let size = 0;
291
+ let mtime = 0;
292
+ if (existsSync(fullPath)) {
293
+ try {
294
+ const stats = statSync(fullPath);
295
+ if (stats.isFile()) {
296
+ size = stats.size;
297
+ mtime = stats.mtimeMs;
298
+ }
299
+ }
300
+ catch {
301
+ // Keep defaults for unreadable files.
302
+ }
303
+ }
304
+ return {
305
+ relativePath,
306
+ fileName,
307
+ size,
308
+ mtime,
309
+ mimeType: inferMimeType(fileName),
310
+ category: "document",
311
+ source: "artifact",
312
+ };
313
+ }
314
+ export function listHermesMemoryFiles() {
315
+ const home = resolveHermesHome();
316
+ const memoriesRoot = join(home, MEMORIES_DIR);
317
+ const discovered = new Set(KNOWN_MEMORY_FILES);
318
+ if (existsSync(memoriesRoot)) {
319
+ try {
320
+ for (const name of readdirSync(memoriesRoot)) {
321
+ if (name.startsWith(".") || !name.endsWith(".md"))
322
+ continue;
323
+ discovered.add(name);
324
+ }
325
+ }
326
+ catch {
327
+ // Fall back to known files only.
328
+ }
329
+ }
330
+ const files = [...discovered]
331
+ .sort((left, right) => left.localeCompare(right))
332
+ .map((fileName) => memoryFileEntry(home, fileName));
333
+ return { files };
334
+ }
178
335
  export function readHermesFileBytes(inputPath) {
179
336
  const resolved = resolveSandboxedPath(inputPath);
180
337
  if (!existsSync(resolved)) {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=hermesFiles.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hermesFiles.test.d.ts","sourceRoot":"","sources":["../src/hermesFiles.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,72 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import { assertWritableHermesPath, cronOutputSourceKey, listHermesFiles, listHermesMemoryFiles, MEMORY_MD_CHAR_LIMIT, MEMORY_MD_RELATIVE_PATH, readHermesFileBytes, SOUL_RELATIVE_PATH, USER_MD_CHAR_LIMIT, writeHermesFile, } from "./hermesFiles.js";
7
+ test("cronOutputSourceKey extracts jobId/filename from cron output paths", () => {
8
+ assert.equal(cronOutputSourceKey("cron/output/abc123/2026-05-25_06-30-00.md"), "abc123/2026-05-25_06-30-00.md");
9
+ assert.equal(cronOutputSourceKey("cache/documents/report.pdf"), undefined);
10
+ });
11
+ test("listHermesFiles includes cron output with source metadata", () => {
12
+ const home = mkdtempSync(join(tmpdir(), "hermes-files-test-"));
13
+ const previousHome = process.env.HERMES_HOME;
14
+ process.env.HERMES_HOME = home;
15
+ try {
16
+ const jobId = "job-alpha";
17
+ const fileName = "2026-05-25_06-30-00.md";
18
+ const outputDir = join(home, "cron", "output", jobId);
19
+ mkdirSync(outputDir, { recursive: true });
20
+ writeFileSync(join(outputDir, fileName), "# Cron output\n", "utf8");
21
+ writeFileSync(join(home, "cron", "jobs.json"), JSON.stringify([{ id: jobId, name: "AI News Daily Brief" }]), "utf8");
22
+ const { files } = listHermesFiles({ limit: 50 });
23
+ const cronFile = files.find((file) => file.sourceKey === `${jobId}/${fileName}`);
24
+ assert.ok(cronFile, "expected cron output file in list");
25
+ assert.equal(cronFile?.source, "cron");
26
+ assert.equal(cronFile?.hermesJobId, jobId);
27
+ assert.equal(cronFile?.hermesJobName, "AI News Daily Brief");
28
+ assert.equal(cronFile?.sourceKey, `${jobId}/${fileName}`);
29
+ }
30
+ finally {
31
+ if (previousHome === undefined) {
32
+ delete process.env.HERMES_HOME;
33
+ }
34
+ else {
35
+ process.env.HERMES_HOME = previousHome;
36
+ }
37
+ }
38
+ });
39
+ test("assertWritableHermesPath allows SOUL.md and memories markdown only", () => {
40
+ assert.equal(assertWritableHermesPath("SOUL.md"), SOUL_RELATIVE_PATH);
41
+ assert.equal(assertWritableHermesPath("memories/MEMORY.md"), MEMORY_MD_RELATIVE_PATH);
42
+ assert.throws(() => assertWritableHermesPath("cron/output/x.md"));
43
+ assert.throws(() => assertWritableHermesPath("memories/nested/x.md"));
44
+ assert.throws(() => assertWritableHermesPath("../SOUL.md"));
45
+ });
46
+ test("writeHermesFile enforces memory char limits and round-trips", () => {
47
+ const home = mkdtempSync(join(tmpdir(), "hermes-write-test-"));
48
+ const previousHome = process.env.HERMES_HOME;
49
+ process.env.HERMES_HOME = home;
50
+ try {
51
+ writeHermesFile(SOUL_RELATIVE_PATH, "# My soul\n");
52
+ const soul = readHermesFileBytes(SOUL_RELATIVE_PATH);
53
+ assert.equal(soul.bytes.toString("utf8"), "# My soul\n");
54
+ const withinLimit = "x".repeat(MEMORY_MD_CHAR_LIMIT);
55
+ writeHermesFile(MEMORY_MD_RELATIVE_PATH, withinLimit);
56
+ assert.throws(() => writeHermesFile(MEMORY_MD_RELATIVE_PATH, "x".repeat(MEMORY_MD_CHAR_LIMIT + 1)), /exceeds 2200/);
57
+ const userOver = "y".repeat(USER_MD_CHAR_LIMIT + 1);
58
+ assert.throws(() => writeHermesFile("memories/USER.md", userOver), /exceeds 1375/);
59
+ const { files } = listHermesMemoryFiles();
60
+ assert.equal(files.length, 2);
61
+ assert.ok(files.some((file) => file.fileName === "MEMORY.md"));
62
+ assert.ok(files.some((file) => file.fileName === "USER.md"));
63
+ }
64
+ finally {
65
+ if (previousHome === undefined) {
66
+ delete process.env.HERMES_HOME;
67
+ }
68
+ else {
69
+ process.env.HERMES_HOME = previousHome;
70
+ }
71
+ }
72
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saleso.innovations/bridge",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "Connect your Hermes agent to the Cleos iOS app via pairing code.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@
36
36
  "lint": "eslint . --max-warnings 0",
37
37
  "check-types": "tsc --noEmit",
38
38
  "prepublishOnly": "npm run build",
39
- "test": "echo 'No @saleso.innovations/bridge tests configured'"
39
+ "test": "node --import tsx --test src/hermesFiles.test.ts"
40
40
  },
41
41
  "dependencies": {
42
42
  "better-sqlite3": "^11.10.0",