@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.
- package/dist/hermesCommands.d.ts +1 -1
- package/dist/hermesCommands.d.ts.map +1 -1
- package/dist/hermesCommands.js +16 -1
- package/dist/hermesCronJobs.d.ts +4 -0
- package/dist/hermesCronJobs.d.ts.map +1 -0
- package/dist/hermesCronJobs.js +32 -0
- package/dist/hermesFileCommands.d.ts +8 -0
- package/dist/hermesFileCommands.d.ts.map +1 -1
- package/dist/hermesFileCommands.js +15 -1
- package/dist/hermesFiles.d.ts +22 -0
- package/dist/hermesFiles.d.ts.map +1 -1
- package/dist/hermesFiles.js +172 -15
- package/dist/hermesFiles.test.d.ts +2 -0
- package/dist/hermesFiles.test.d.ts.map +1 -0
- package/dist/hermesFiles.test.js +72 -0
- package/package.json +2 -2
package/dist/hermesCommands.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|
package/dist/hermesCommands.js
CHANGED
|
@@ -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 @@
|
|
|
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":"
|
|
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;
|
package/dist/hermesFiles.d.ts
CHANGED
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"hermesFiles.d.ts","sourceRoot":"","sources":["../src/hermesFiles.ts"],"names":[],"mappings":"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"}
|
package/dist/hermesFiles.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 @@
|
|
|
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.
|
|
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": "
|
|
39
|
+
"test": "node --import tsx --test src/hermesFiles.test.ts"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"better-sqlite3": "^11.10.0",
|