@agentworkspaceos/hermes 0.1.0

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/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @agentworkspaceos/hermes
2
+
3
+ AgentOS connector CLI for pairing a local Hermes runtime with an AgentOS workspace.
4
+
5
+ ## Usage
6
+
7
+ Run this command from the machine where Hermes is installed:
8
+
9
+ ```sh
10
+ npx --yes @agentworkspaceos/hermes@latest connect \
11
+ --mode plugin \
12
+ --pair AGOS-XXXX-XXXX \
13
+ --agentos-url https://agentos-local.rewardsbunny.com \
14
+ --hermes-url http://127.0.0.1:8642 \
15
+ --dashboard-url http://127.0.0.1:9119
16
+ ```
17
+
18
+ The connector sends safe runtime metadata and heartbeat status to AgentOS. Secrets stay on the local machine.
19
+
20
+ ## Options
21
+
22
+ ```txt
23
+ agentos-hermes connect --pair <code> [options]
24
+
25
+ --agentos-url <url> AgentOS web app URL
26
+ --hermes-url <url> Local Hermes gateway URL
27
+ --dashboard-url <url> Optional Hermes dashboard URL
28
+ --mode <mode> plugin, sidecar, or direct-url
29
+ --config-dir <path> Local config directory
30
+ --hermes-command <cmd> Hermes CLI executable
31
+ --interval-ms <ms> Heartbeat interval
32
+ --skip-inventory Send basic heartbeat metadata only
33
+ --once Send one heartbeat and exit
34
+ ```
35
+
36
+ ## Publish
37
+
38
+ The package is configured for public scoped publishing:
39
+
40
+ ```sh
41
+ npm login
42
+ npm publish --access public
43
+ ```
44
+
45
+ You must own or have publish access to the `@agentos` npm scope before publishing.
@@ -0,0 +1,1187 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
+ import { homedir, hostname } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+
7
+ const VERSION = "0.1.0";
8
+ const DEFAULT_AGENTOS_URL = "http://localhost:3000";
9
+ const DEFAULT_HERMES_URL = "http://127.0.0.1:8642";
10
+ const DEFAULT_MODE = "plugin";
11
+ const DEFAULT_HERMES_COMMAND = "hermes";
12
+ const HERMES_COMMAND_TIMEOUT_MS = 2500;
13
+ const PARENT_AGENT_NAME = "Hermes Main Orchestrator";
14
+
15
+ async function main(argv) {
16
+ const [command, ...args] = argv;
17
+
18
+ if (!command || command === "--help" || command === "-h") {
19
+ printHelp();
20
+ return;
21
+ }
22
+
23
+ if (command === "--version" || command === "-v") {
24
+ console.log(VERSION);
25
+ return;
26
+ }
27
+
28
+ if (command !== "connect") {
29
+ throw new Error(`Unknown command "${command}". Run agentos-hermes --help.`);
30
+ }
31
+
32
+ await connect(args);
33
+ }
34
+
35
+ async function connect(args) {
36
+ const options = parseConnectArgs(args);
37
+ const agentosUrl = normalizeBaseUrl(options.agentosUrl ?? process.env.AGENTOS_URL ?? DEFAULT_AGENTOS_URL);
38
+ const hermesUrl = normalizeBaseUrl(options.hermesUrl ?? process.env.HERMES_BASE_URL ?? DEFAULT_HERMES_URL);
39
+ const dashboardUrl = options.dashboardUrl ? normalizeBaseUrl(options.dashboardUrl) : undefined;
40
+ const mode = options.mode ?? DEFAULT_MODE;
41
+ const pairingCode = options.pair;
42
+ const hermesCommand = options.hermesCommand ?? process.env.HERMES_CLI_COMMAND ?? DEFAULT_HERMES_COMMAND;
43
+
44
+ if (!pairingCode) {
45
+ throw new Error("Missing --pair <code>.");
46
+ }
47
+
48
+ if (!["plugin", "sidecar", "direct-url"].includes(mode)) {
49
+ throw new Error('--mode must be "plugin", "sidecar", or "direct-url".');
50
+ }
51
+
52
+ const hermesHealth = await detectHermes(hermesUrl);
53
+ const metadata = options.skipInventory
54
+ ? createFallbackMetadata(hermesHealth)
55
+ : await collectHermesInventory({ hermesCommand, hermesHealth });
56
+ const configPath = await persistLocalConfig({
57
+ agentosUrl,
58
+ configDir: options.configDir,
59
+ dashboardUrl,
60
+ hermesUrl,
61
+ mode,
62
+ pairingCode,
63
+ });
64
+ const payload = {
65
+ connectorMode: mode,
66
+ connectorVersion: VERSION,
67
+ hermes: {
68
+ baseUrl: hermesUrl,
69
+ dashboardUrl,
70
+ detected: hermesHealth.detected,
71
+ platform: hermesHealth.platform,
72
+ status: hermesHealth.status,
73
+ },
74
+ machineLabel: hostname(),
75
+ metadata,
76
+ };
77
+
78
+ const sendHeartbeat = async () => postHeartbeat(agentosUrl, pairingCode, payload);
79
+ const session = await sendHeartbeat();
80
+
81
+ console.log("AgentOS Hermes connector registered");
82
+ console.log(`Workspace: ${session.workspaceSlug}`);
83
+ console.log(`Pairing code: ${session.pairingCode}`);
84
+ console.log(`Status: ${session.status}`);
85
+ console.log(`Mode: ${mode}`);
86
+ console.log(`Hermes detected: ${hermesHealth.detected ? "yes" : "no"}`);
87
+ console.log(
88
+ `Inventory: ${metadata.skills?.length ?? 0} skills, ${metadata.toolsets?.length ?? 0} enabled toolsets, ${
89
+ metadata.mcpServers?.length ?? 0
90
+ } MCP servers, ${metadata.identityFiles?.length ?? 0} identity files, ${metadata.childAgents?.length ?? 0} child agents`,
91
+ );
92
+ console.log(`Local config: ${configPath}`);
93
+ console.log("Secrets stay local. Full skill docs, identity files, and redacted config were sent.");
94
+ if (session.status === "pending_confirmation") {
95
+ console.log("Confirm this Hermes runtime in AgentOS before tasks are sent.");
96
+ }
97
+
98
+ if (options.once) {
99
+ return;
100
+ }
101
+
102
+ const intervalMs = options.intervalMs ?? 15000;
103
+
104
+ console.log(`Sending heartbeat every ${Math.round(intervalMs / 1000)}s. Press Ctrl+C to stop.`);
105
+
106
+ await runHeartbeatLoop(sendHeartbeat, intervalMs);
107
+ }
108
+
109
+ function parseConnectArgs(args) {
110
+ const options = {};
111
+
112
+ for (let index = 0; index < args.length; index += 1) {
113
+ const arg = args[index];
114
+ const next = args[index + 1];
115
+
116
+ if (arg === "--pair") {
117
+ options.pair = requireValue(arg, next);
118
+ index += 1;
119
+ } else if (arg === "--agentos-url") {
120
+ options.agentosUrl = requireValue(arg, next);
121
+ index += 1;
122
+ } else if (arg === "--hermes-url") {
123
+ options.hermesUrl = requireValue(arg, next);
124
+ index += 1;
125
+ } else if (arg === "--dashboard-url") {
126
+ options.dashboardUrl = requireValue(arg, next);
127
+ index += 1;
128
+ } else if (arg === "--mode") {
129
+ options.mode = requireValue(arg, next);
130
+ index += 1;
131
+ } else if (arg === "--config-dir") {
132
+ options.configDir = requireValue(arg, next);
133
+ index += 1;
134
+ } else if (arg === "--hermes-command") {
135
+ options.hermesCommand = requireValue(arg, next);
136
+ index += 1;
137
+ } else if (arg === "--interval-ms") {
138
+ options.intervalMs = Number(requireValue(arg, next));
139
+ index += 1;
140
+ } else if (arg === "--once") {
141
+ options.once = true;
142
+ } else if (arg === "--skip-inventory") {
143
+ options.skipInventory = true;
144
+ } else if (arg === "--help" || arg === "-h") {
145
+ printConnectHelp();
146
+ process.exit(0);
147
+ } else {
148
+ throw new Error(`Unknown option "${arg}".`);
149
+ }
150
+ }
151
+
152
+ return options;
153
+ }
154
+
155
+ function requireValue(name, value) {
156
+ if (!value || value.startsWith("--")) {
157
+ throw new Error(`Missing value for ${name}.`);
158
+ }
159
+
160
+ return value;
161
+ }
162
+
163
+ async function detectHermes(hermesUrl) {
164
+ try {
165
+ const response = await fetch(`${hermesUrl}/health`, {
166
+ headers: {
167
+ accept: "application/json",
168
+ },
169
+ method: "GET",
170
+ });
171
+ const text = await response.text();
172
+ const body = text ? JSON.parse(text) : {};
173
+
174
+ return {
175
+ detected: response.ok,
176
+ platform: typeof body.platform === "string" ? body.platform : null,
177
+ status: typeof body.status === "string" ? body.status : response.ok ? "ok" : "unavailable",
178
+ };
179
+ } catch {
180
+ return {
181
+ detected: false,
182
+ platform: null,
183
+ status: "unavailable",
184
+ };
185
+ }
186
+ }
187
+
188
+ async function collectHermesInventory({ hermesCommand, hermesHealth }) {
189
+ const [version, status, tools, skills, mcp, profiles, plugins, cron, insights] = await Promise.all([
190
+ runHermesCommand(hermesCommand, ["version"]),
191
+ runHermesCommand(hermesCommand, ["status"]),
192
+ runHermesCommand(hermesCommand, ["tools", "list"]),
193
+ runHermesCommand(hermesCommand, ["skills", "list"]),
194
+ runHermesCommand(hermesCommand, ["mcp", "list"]),
195
+ runHermesCommand(hermesCommand, ["profile", "list"]),
196
+ runHermesCommand(hermesCommand, ["plugins", "list"]),
197
+ runHermesCommand(hermesCommand, ["cron", "list"]),
198
+ runHermesCommand(hermesCommand, ["insights", "--days", "30"]),
199
+ ]);
200
+
201
+ const versionMetadata = parseVersionOutput(version.stdout);
202
+ const projectPath = parseProjectPath(version.stdout);
203
+ const statusMetadata = parseStatusOutput(status.stdout);
204
+ const toolsetsDetailed = parseToolsetsOutput(tools.stdout);
205
+ const skillCatalog = projectPath ? await readSkillCatalog(projectPath) : [];
206
+ const skillsDetailed = mergeSkillDetails(parseSkillsOutput(skills.stdout), skillCatalog);
207
+ const mcpServersDetailed = parseMcpOutput(mcp.stdout);
208
+ const profilesDetailed = parseProfilesOutput(profiles.stdout);
209
+ const pluginsDetailed = parsePluginsOutput(plugins.stdout);
210
+ const cronJobs = parseCronOutput(cron.stdout);
211
+ const usageInsights = parseInsightsOutput(insights.stdout);
212
+ const configFiles = await readHermesConfigFiles(hermesCommand);
213
+ const identityFiles = await readHermesIdentityFiles(hermesCommand, projectPath);
214
+ const activeProfile = profilesDetailed.find((profile) => profile.active) ?? profilesDetailed[0];
215
+
216
+ return compactObject({
217
+ ...createFallbackMetadata(hermesHealth),
218
+ ...versionMetadata,
219
+ ...statusMetadata,
220
+ configFiles: configFiles.length ? configFiles : undefined,
221
+ cronJobs: cronJobs.length ? cronJobs : undefined,
222
+ identityFiles: identityFiles.length ? identityFiles : undefined,
223
+ insights: usageInsights,
224
+ mcpServers: mcpServersDetailed.map((server) => server.name),
225
+ mcpServersDetailed: mcpServersDetailed.length ? mcpServersDetailed : undefined,
226
+ plugins: pluginsDetailed.length ? pluginsDetailed : undefined,
227
+ profile: activeProfile?.name ?? statusMetadata.profile ?? "default",
228
+ profiles: profilesDetailed.length ? profilesDetailed : undefined,
229
+ skills: skillsDetailed.map((skill) => skill.name),
230
+ skillsDetailed: skillsDetailed.length ? skillsDetailed : undefined,
231
+ toolsets: toolsetsDetailed.filter((toolset) => toolset.status === "enabled").map((toolset) => toolset.name),
232
+ toolsetsDetailed: toolsetsDetailed.length ? toolsetsDetailed : undefined,
233
+ });
234
+ }
235
+
236
+ function parseInsightsOutput(output) {
237
+ if (!output || !/Hermes Insights|Overview|Sessions:/i.test(output)) {
238
+ return undefined;
239
+ }
240
+
241
+ const clean = stripAnsi(output);
242
+ const period = clean.match(/^\s*Period:\s+(.+)$/m)?.[1]?.trim();
243
+ const insights = compactObject({
244
+ period: sanitizeDisplayString(period),
245
+ ...parseOverviewMetrics(clean),
246
+ models: parseInsightsTable(clean, "Models Used", "Platforms", parseModelsRow),
247
+ platforms: parseInsightsTable(clean, "Platforms", "Top Tools", parsePlatformsRow),
248
+ topTools: parseInsightsTable(clean, "Top Tools", "Top Skills", parseToolRow),
249
+ topSkills: parseInsightsTable(clean, "Top Skills", "Activity Patterns", parseSkillRow),
250
+ weekdayActivity: parseWeekdayActivity(clean),
251
+ ...parseActivitySummary(clean),
252
+ notableSessions: parseInsightsTable(clean, "Notable Sessions", undefined, parseNotableSessionRow),
253
+ });
254
+
255
+ return Object.keys(insights).length ? insights : undefined;
256
+ }
257
+
258
+ function parseOverviewMetrics(output) {
259
+ return compactObject({
260
+ activeTime: output.match(/Active time:\s+([^\n]+?)\s{2,}Avg session:/)?.[1]?.trim(),
261
+ avgMessagesPerSession: parseNumber(output.match(/Avg msgs\/session:\s+([\d,.]+)/)?.[1]),
262
+ avgSession: output.match(/Avg session:\s+([^\n]+)/)?.[1]?.trim(),
263
+ inputTokens: parseNumber(output.match(/Input tokens:\s+([\d,]+)/)?.[1]),
264
+ messages: parseNumber(output.match(/Messages:\s+([\d,]+)/)?.[1]),
265
+ outputTokens: parseNumber(output.match(/Output tokens:\s+([\d,]+)/)?.[1]),
266
+ sessions: parseNumber(output.match(/Sessions:\s+([\d,]+)/)?.[1]),
267
+ totalTokens: parseNumber(output.match(/Total tokens:\s+([\d,]+)/)?.[1]),
268
+ toolCalls: parseNumber(output.match(/Tool calls:\s+([\d,]+)/)?.[1]),
269
+ userMessages: parseNumber(output.match(/User messages:\s+([\d,]+)/)?.[1]),
270
+ });
271
+ }
272
+
273
+ function parseInsightsTable(output, startLabel, endLabel, rowParser) {
274
+ const section = sliceInsightsSection(output, startLabel, endLabel);
275
+
276
+ if (!section) {
277
+ return undefined;
278
+ }
279
+
280
+ const rows = section
281
+ .split("\n")
282
+ .map((line) => rowParser(line.trim()))
283
+ .filter(Boolean);
284
+
285
+ return rows.length ? rows : undefined;
286
+ }
287
+
288
+ function sliceInsightsSection(output, startLabel, endLabel) {
289
+ const startIndex = output.search(new RegExp(escapeRegExp(startLabel), "i"));
290
+
291
+ if (startIndex < 0) {
292
+ return undefined;
293
+ }
294
+
295
+ const sliced = output.slice(startIndex + startLabel.length);
296
+ const endIndex = endLabel ? sliced.search(new RegExp(escapeRegExp(endLabel), "i")) : -1;
297
+
298
+ return (endIndex >= 0 ? sliced.slice(0, endIndex) : sliced)
299
+ .split("\n")
300
+ .filter((line) => !/^[\s─]+$/.test(line))
301
+ .join("\n");
302
+ }
303
+
304
+ function parseModelsRow(line) {
305
+ const match = line.match(/^(.+?)\s+([\d,]+)\s+([\d,]+)$/);
306
+
307
+ return match
308
+ ? compactObject({
309
+ label: sanitizeDisplayString(match[1]),
310
+ primary: `${parseNumber(match[2]) ?? 0} sessions`,
311
+ secondary: `${formatCompactNumber(parseNumber(match[3]) ?? 0)} tokens`,
312
+ value: parseNumber(match[3]),
313
+ })
314
+ : undefined;
315
+ }
316
+
317
+ function parsePlatformsRow(line) {
318
+ const match = line.match(/^(.+?)\s+([\d,]+)\s+([\d,]+)\s+([\d,]+)$/);
319
+
320
+ return match
321
+ ? compactObject({
322
+ label: sanitizeDisplayString(match[1]),
323
+ primary: `${parseNumber(match[2]) ?? 0} sessions`,
324
+ secondary: `${formatCompactNumber(parseNumber(match[4]) ?? 0)} tokens`,
325
+ value: parseNumber(match[2]),
326
+ })
327
+ : undefined;
328
+ }
329
+
330
+ function parseToolRow(line) {
331
+ const match = line.match(/^(.+?)\s+([\d,]+)\s+([\d.]+%)$/);
332
+
333
+ return match
334
+ ? compactObject({
335
+ label: sanitizeDisplayString(match[1]),
336
+ primary: `${parseNumber(match[2]) ?? 0} calls`,
337
+ secondary: match[3],
338
+ value: parseNumber(match[2]),
339
+ })
340
+ : undefined;
341
+ }
342
+
343
+ function parseSkillRow(line) {
344
+ const match = line.match(/^(.+?)\s+([\d,]+)\s+([\d,]+)\s+(.+)$/);
345
+
346
+ return match
347
+ ? compactObject({
348
+ label: sanitizeDisplayString(match[1]),
349
+ primary: `${parseNumber(match[2]) ?? 0} loads`,
350
+ secondary: `${parseNumber(match[3]) ?? 0} edits, last used ${sanitizeDisplayString(match[4])}`,
351
+ value: parseNumber(match[2]),
352
+ })
353
+ : undefined;
354
+ }
355
+
356
+ function parseNotableSessionRow(line) {
357
+ const match = line.match(/^(.+?)\s{2,}(.+?)\s{2,}\((.+)\)$/);
358
+
359
+ return match
360
+ ? compactObject({
361
+ label: sanitizeDisplayString(match[1]),
362
+ primary: sanitizeDisplayString(match[2]),
363
+ secondary: sanitizeDisplayString(match[3]),
364
+ })
365
+ : undefined;
366
+ }
367
+
368
+ function parseWeekdayActivity(output) {
369
+ const rows = [...output.matchAll(/^\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+[█\s]+\s+([\d,]+)$/gm)].map((match) =>
370
+ compactObject({
371
+ label: match[1],
372
+ value: parseNumber(match[2]),
373
+ }),
374
+ );
375
+
376
+ return rows.length ? rows : undefined;
377
+ }
378
+
379
+ function parseActivitySummary(output) {
380
+ return compactObject({
381
+ activeDays: parseNumber(output.match(/Active days:\s+([\d,]+)/)?.[1]),
382
+ bestStreak: output.match(/Best streak:\s+(.+)$/m)?.[1]?.trim(),
383
+ });
384
+ }
385
+
386
+ function createFallbackMetadata(hermesHealth) {
387
+ return {
388
+ agentName: PARENT_AGENT_NAME,
389
+ mcpServers: [],
390
+ profile: "default",
391
+ runtimeType: "hermes",
392
+ skills: [],
393
+ status: hermesHealth.detected ? "online" : "offline",
394
+ toolsets: [],
395
+ };
396
+ }
397
+
398
+ function runHermesCommand(hermesCommand, args) {
399
+ return runCommand(hermesCommand, args);
400
+ }
401
+
402
+ function runCommand(command, args, options = {}) {
403
+ return new Promise((resolve) => {
404
+ const child = spawn(command, args, {
405
+ cwd: options.cwd,
406
+ env: {
407
+ ...process.env,
408
+ COLUMNS: "220",
409
+ NO_COLOR: "1",
410
+ TERM: "dumb",
411
+ },
412
+ stdio: ["ignore", "pipe", "pipe"],
413
+ });
414
+ let settled = false;
415
+ let stdout = "";
416
+ let stderr = "";
417
+
418
+ const timeout = setTimeout(() => {
419
+ if (settled) {
420
+ return;
421
+ }
422
+
423
+ settled = true;
424
+ child.kill("SIGTERM");
425
+ resolve({ ok: false, stdout, stderr: `${stderr}\nTimed out running hermes ${args.join(" ")}`.trim() });
426
+ }, options.timeoutMs ?? HERMES_COMMAND_TIMEOUT_MS);
427
+
428
+ child.stdout.setEncoding("utf8");
429
+ child.stderr.setEncoding("utf8");
430
+ child.stdout.on("data", (chunk) => {
431
+ stdout += chunk;
432
+ });
433
+ child.stderr.on("data", (chunk) => {
434
+ stderr += chunk;
435
+ });
436
+ child.on("error", (error) => {
437
+ if (settled) {
438
+ return;
439
+ }
440
+
441
+ settled = true;
442
+ clearTimeout(timeout);
443
+ resolve({ ok: false, stdout: "", stderr: error.message });
444
+ });
445
+ child.on("close", (status) => {
446
+ if (settled) {
447
+ return;
448
+ }
449
+
450
+ settled = true;
451
+ clearTimeout(timeout);
452
+ resolve({ ok: status === 0, stdout: stripAnsi(stdout), stderr: stripAnsi(stderr) });
453
+ });
454
+ });
455
+ }
456
+
457
+ function parseVersionOutput(output) {
458
+ const metadata = {};
459
+ const version = output.match(/Hermes Agent v([^\s]+)/);
460
+ const pythonVersion = output.match(/^Python:\s+(.+)$/m);
461
+ const openAiSdkVersion = output.match(/^OpenAI SDK:\s+(.+)$/m);
462
+
463
+ if (version) {
464
+ metadata.version = version[1];
465
+ }
466
+
467
+ if (pythonVersion) {
468
+ metadata.pythonVersion = pythonVersion[1].trim();
469
+ }
470
+
471
+ if (openAiSdkVersion) {
472
+ metadata.openAiSdkVersion = openAiSdkVersion[1].trim();
473
+ }
474
+
475
+ return metadata;
476
+ }
477
+
478
+ function parseProjectPath(output) {
479
+ const project = output.match(/^Project:\s+(.+)$/m);
480
+
481
+ return project ? project[1].trim() : undefined;
482
+ }
483
+
484
+ async function readSkillCatalog(projectPath) {
485
+ const python = join(projectPath, "venv", "bin", "python");
486
+ const script = `
487
+ import json
488
+ from agent.skill_utils import get_external_skills_dirs, iter_skill_index_files
489
+ from tools.skills_tool import (
490
+ SKILLS_DIR,
491
+ _EXCLUDED_SKILL_DIRS,
492
+ _get_category_from_path,
493
+ _parse_frontmatter,
494
+ skill_matches_platform,
495
+ )
496
+ skills = []
497
+ seen = set()
498
+ dirs = []
499
+ if SKILLS_DIR.exists():
500
+ dirs.append(SKILLS_DIR)
501
+ dirs.extend(get_external_skills_dirs())
502
+ for scan_dir in dirs:
503
+ for skill_md in iter_skill_index_files(scan_dir, "SKILL.md"):
504
+ if any(part in _EXCLUDED_SKILL_DIRS for part in skill_md.parts):
505
+ continue
506
+ try:
507
+ content = skill_md.read_text(encoding="utf-8")
508
+ frontmatter, body = _parse_frontmatter(content)
509
+ if not skill_matches_platform(frontmatter):
510
+ continue
511
+ name = str(frontmatter.get("name") or skill_md.parent.name)[:64]
512
+ if not name or name in seen:
513
+ continue
514
+ description = frontmatter.get("description", "")
515
+ if not description:
516
+ for line in body.strip().split("\\n"):
517
+ line = line.strip()
518
+ if line and not line.startswith("#"):
519
+ description = line
520
+ break
521
+ skills.append({
522
+ "name": name,
523
+ "category": _get_category_from_path(skill_md),
524
+ "description": description,
525
+ "path": str(skill_md),
526
+ "content": content,
527
+ })
528
+ seen.add(name)
529
+ except Exception:
530
+ continue
531
+ print(json.dumps(skills))
532
+ `.trim();
533
+ const result = await runCommand(python, ["-c", script], { cwd: projectPath, timeoutMs: 8000 });
534
+
535
+ if (!result.ok || !result.stdout) {
536
+ return [];
537
+ }
538
+
539
+ try {
540
+ const parsed = JSON.parse(result.stdout);
541
+
542
+ if (!Array.isArray(parsed)) {
543
+ return [];
544
+ }
545
+
546
+ return parsed
547
+ .map((skill) =>
548
+ compactObject({
549
+ category: sanitizeDisplayString(skill.category),
550
+ content: redactSensitiveContent(String(skill.content ?? "")),
551
+ contentLength: typeof skill.content === "string" ? skill.content.length : undefined,
552
+ contentRedacted: true,
553
+ description: sanitizeDisplayString(skill.description, 420),
554
+ name: sanitizeDisplayString(skill.name),
555
+ path: redactLocalPath(sanitizeDisplayString(skill.path, 420)),
556
+ }),
557
+ )
558
+ .filter((skill) => skill.name);
559
+ } catch {
560
+ return [];
561
+ }
562
+ }
563
+
564
+ function mergeSkillDetails(listRows, catalogRows) {
565
+ const catalogByName = new Map(catalogRows.map((skill) => [skill.name, skill]));
566
+ const merged = listRows.map((skill) => compactObject({ ...catalogByName.get(skill.name), ...skill }));
567
+ const seen = new Set(merged.map((skill) => skill.name));
568
+
569
+ for (const skill of catalogRows) {
570
+ if (!seen.has(skill.name)) {
571
+ merged.push(skill);
572
+ }
573
+ }
574
+
575
+ return merged;
576
+ }
577
+
578
+ async function readHermesConfigFiles(hermesCommand) {
579
+ const configPathResult = await runHermesCommand(hermesCommand, ["config", "path"]);
580
+ const configPath = configPathResult.stdout.trim();
581
+
582
+ if (!configPath) {
583
+ return [];
584
+ }
585
+
586
+ const files = [];
587
+ const hermesHome = dirname(configPath);
588
+
589
+ await addConfigFile(files, "Default config", configPath);
590
+
591
+ try {
592
+ const profilesDir = join(hermesHome, "profiles");
593
+ const profiles = await readdir(profilesDir, { withFileTypes: true });
594
+
595
+ for (const profile of profiles) {
596
+ if (!profile.isDirectory()) {
597
+ continue;
598
+ }
599
+
600
+ await addConfigFile(files, `Profile config: ${profile.name}`, join(profilesDir, profile.name, "config.yaml"));
601
+ }
602
+ } catch {
603
+ // Profiles are optional.
604
+ }
605
+
606
+ return files;
607
+ }
608
+
609
+ async function readHermesIdentityFiles(hermesCommand, projectPath) {
610
+ const files = [];
611
+ const seen = new Set();
612
+ const configPathResult = await runHermesCommand(hermesCommand, ["config", "path"]);
613
+ const configPath = configPathResult.stdout.trim();
614
+
615
+ if (configPath) {
616
+ const hermesHome = dirname(configPath);
617
+ const profilesDir = join(hermesHome, "profiles");
618
+
619
+ await addIdentityFile(files, seen, {
620
+ label: "Default SOUL.md",
621
+ path: join(hermesHome, "SOUL.md"),
622
+ profile: "default",
623
+ type: "soul",
624
+ });
625
+ await addIdentityFile(files, seen, {
626
+ label: "Default AGENTS.md",
627
+ path: join(hermesHome, "AGENTS.md"),
628
+ profile: "default",
629
+ type: "agent-instructions",
630
+ });
631
+ await addIdentityFile(files, seen, {
632
+ label: "Default MEMORY.md",
633
+ path: join(hermesHome, "MEMORY.md"),
634
+ profile: "default",
635
+ type: "memory",
636
+ });
637
+ await addIdentityFile(files, seen, {
638
+ label: "Default .cursorrules",
639
+ path: join(hermesHome, ".cursorrules"),
640
+ profile: "default",
641
+ type: "rules",
642
+ });
643
+
644
+ try {
645
+ const profiles = await readdir(profilesDir, { withFileTypes: true });
646
+
647
+ for (const profile of profiles) {
648
+ if (!profile.isDirectory()) {
649
+ continue;
650
+ }
651
+
652
+ await addProfileIdentityFiles(files, seen, profilesDir, profile.name);
653
+ }
654
+ } catch {
655
+ // Profiles are optional.
656
+ }
657
+ }
658
+
659
+ if (projectPath) {
660
+ await addIdentityFile(files, seen, {
661
+ label: "Hermes project AGENTS.md",
662
+ path: join(projectPath, "AGENTS.md"),
663
+ type: "project-instructions",
664
+ });
665
+ await addIdentityFile(files, seen, {
666
+ label: "Hermes project SOUL.md",
667
+ path: join(projectPath, "SOUL.md"),
668
+ type: "project-soul",
669
+ });
670
+ await addIdentityFile(files, seen, {
671
+ label: "Docker SOUL.md",
672
+ path: join(projectPath, "docker", "SOUL.md"),
673
+ type: "docker-soul",
674
+ });
675
+ }
676
+
677
+ return files;
678
+ }
679
+
680
+ async function addProfileIdentityFiles(files, seen, profilesDir, profileName) {
681
+ const profileDir = join(profilesDir, profileName);
682
+
683
+ await addIdentityFile(files, seen, {
684
+ label: `Profile SOUL.md: ${profileName}`,
685
+ path: join(profileDir, "SOUL.md"),
686
+ profile: profileName,
687
+ type: "profile-soul",
688
+ });
689
+ await addIdentityFile(files, seen, {
690
+ label: `Profile AGENTS.md: ${profileName}`,
691
+ path: join(profileDir, "AGENTS.md"),
692
+ profile: profileName,
693
+ type: "agent-instructions",
694
+ });
695
+ await addIdentityFile(files, seen, {
696
+ label: `Profile MEMORY.md: ${profileName}`,
697
+ path: join(profileDir, "MEMORY.md"),
698
+ profile: profileName,
699
+ type: "memory",
700
+ });
701
+ await addIdentityFile(files, seen, {
702
+ label: `Profile .cursorrules: ${profileName}`,
703
+ path: join(profileDir, ".cursorrules"),
704
+ profile: profileName,
705
+ type: "rules",
706
+ });
707
+ await addIdentityFile(files, seen, {
708
+ label: `Profile memory SOUL.md: ${profileName}`,
709
+ path: join(profileDir, "memory", "SOUL.md"),
710
+ profile: profileName,
711
+ type: "memory-soul",
712
+ });
713
+ await addIdentityFile(files, seen, {
714
+ label: `Profile memory AGENTS.md: ${profileName}`,
715
+ path: join(profileDir, "memory", "AGENTS.md"),
716
+ profile: profileName,
717
+ type: "memory-instructions",
718
+ });
719
+ await addIdentityFile(files, seen, {
720
+ label: `Profile memory MEMORY.md: ${profileName}`,
721
+ path: join(profileDir, "memory", "MEMORY.md"),
722
+ profile: profileName,
723
+ type: "memory",
724
+ });
725
+ }
726
+
727
+ async function addConfigFile(files, label, path) {
728
+ try {
729
+ const content = await readFile(path, "utf8");
730
+
731
+ files.push({
732
+ content: redactSensitiveContent(content),
733
+ label,
734
+ path: redactLocalPath(path),
735
+ redacted: true,
736
+ });
737
+ } catch {
738
+ // Missing or unreadable config files are not fatal to pairing.
739
+ }
740
+ }
741
+
742
+ async function addIdentityFile(files, seen, { label, path, profile, type }) {
743
+ if (seen.has(path)) {
744
+ return;
745
+ }
746
+
747
+ seen.add(path);
748
+
749
+ try {
750
+ const content = await readFile(path, "utf8");
751
+
752
+ files.push(
753
+ compactObject({
754
+ content: redactSensitiveContent(content),
755
+ label,
756
+ path: redactLocalPath(path),
757
+ profile,
758
+ redacted: true,
759
+ type,
760
+ }),
761
+ );
762
+ } catch {
763
+ // Missing identity files are expected across Hermes installs.
764
+ }
765
+ }
766
+
767
+ function parseStatusOutput(output) {
768
+ const metadata = {};
769
+ const model = output.match(/^\s*Model:\s+(.+)$/m);
770
+ const provider = output.match(/^\s*Provider:\s+(.+)$/m);
771
+ const terminalBackend = output.match(/^\s*Backend:\s+(.+)$/m);
772
+ const sudo = output.match(/^\s*Sudo:\s+(.+)$/m);
773
+ const gatewayStatus = output.match(/\u25c6 Gateway Service[\s\S]*?^\s*Status:\s+(?:\u2713\s*)?(.+)$/m);
774
+ const scheduledJobs = output.match(/^\s*Jobs:\s+(\d+)\s+active,\s+(\d+)\s+total$/m);
775
+ const activeSessions = output.match(/^\s*Active:\s+(\d+)\s+session\(s\)$/m);
776
+
777
+ if (model) {
778
+ metadata.model = model[1].trim();
779
+ }
780
+
781
+ if (provider) {
782
+ metadata.provider = provider[1].trim();
783
+ }
784
+
785
+ if (terminalBackend) {
786
+ metadata.terminalBackend = terminalBackend[1].trim();
787
+ }
788
+
789
+ if (sudo) {
790
+ metadata.sudoEnabled = /enabled/i.test(sudo[1]);
791
+ }
792
+
793
+ if (gatewayStatus) {
794
+ metadata.gatewayStatus = gatewayStatus[1].trim();
795
+ }
796
+
797
+ if (scheduledJobs) {
798
+ metadata.scheduledJobs = Number(scheduledJobs[2]);
799
+ }
800
+
801
+ if (activeSessions) {
802
+ metadata.activeSessions = Number(activeSessions[1]);
803
+ }
804
+
805
+ return metadata;
806
+ }
807
+
808
+ function parseToolsetsOutput(output) {
809
+ return output
810
+ .split("\n")
811
+ .map((line) => line.match(/^\s*[\u2713\u2717]\s+(enabled|disabled)\s+(\S+)\s+(.+)$/))
812
+ .filter(Boolean)
813
+ .map((match) => ({
814
+ label: sanitizeDisplayString(match[3]),
815
+ name: sanitizeDisplayString(match[2]),
816
+ status: match[1],
817
+ }))
818
+ .filter((toolset) => toolset.name);
819
+ }
820
+
821
+ function parseSkillsOutput(output) {
822
+ return parseBoxRows(output)
823
+ .map((columns) => {
824
+ const [name, category, source, trust, status] = columns;
825
+
826
+ return compactObject({
827
+ category: sanitizeDisplayString(category),
828
+ name: sanitizeDisplayString(name),
829
+ source: sanitizeDisplayString(source),
830
+ status: sanitizeDisplayString(status),
831
+ trust: sanitizeDisplayString(trust),
832
+ });
833
+ })
834
+ .filter((skill) => skill.name && skill.name !== "Name");
835
+ }
836
+
837
+ function parseMcpOutput(output) {
838
+ if (/No MCP servers configured/i.test(output)) {
839
+ return [];
840
+ }
841
+
842
+ return parseBoxRows(output)
843
+ .map((columns) => compactObject({ name: sanitizeDisplayString(columns[0]), status: sanitizeDisplayString(columns[1]), type: sanitizeDisplayString(columns[2]) }))
844
+ .filter((server) => server.name && server.name !== "Name");
845
+ }
846
+
847
+ function parseProfilesOutput(output) {
848
+ return output
849
+ .split("\n")
850
+ .map((line) => {
851
+ const trimmed = line.trim();
852
+
853
+ if (!trimmed || /^Profile\s+Model\s+Gateway\s+Alias/i.test(trimmed) || /^[\u2500\s]+$/.test(trimmed)) {
854
+ return undefined;
855
+ }
856
+
857
+ const active = trimmed.startsWith("\u25c6");
858
+ const normalized = trimmed.replace(/^\u25c6/, "").trim();
859
+ const columns = normalized.split(/\s{2,}/).map((column) => sanitizeDisplayString(column));
860
+
861
+ if (!columns[0]) {
862
+ return undefined;
863
+ }
864
+
865
+ return compactObject({
866
+ active,
867
+ alias: columns[3] === "\u2014" ? undefined : columns[3],
868
+ gateway: columns[2] === "\u2014" ? undefined : columns[2],
869
+ model: columns[1] === "\u2014" ? undefined : columns[1],
870
+ name: columns[0],
871
+ });
872
+ })
873
+ .filter(Boolean);
874
+ }
875
+
876
+ function parsePluginsOutput(output) {
877
+ const plugins = [];
878
+
879
+ for (const columns of parseBoxRows(output)) {
880
+ const [name, status, version, description, source] = columns.map((column) => sanitizeDisplayString(column));
881
+
882
+ if (name && name !== "Name") {
883
+ plugins.push(
884
+ compactObject({
885
+ description: sanitizeDisplayString(description, 420),
886
+ name,
887
+ source,
888
+ status,
889
+ version,
890
+ }),
891
+ );
892
+ }
893
+ }
894
+
895
+ return plugins;
896
+ }
897
+
898
+ function parseCronOutput(output) {
899
+ const jobs = [];
900
+ let current;
901
+
902
+ for (const line of output.split("\n")) {
903
+ const header = line.match(/^\s*([a-f0-9]{12})\s+\[([^\]]+)\]/i);
904
+
905
+ if (header) {
906
+ current = {
907
+ id: sanitizeDisplayString(header[1]),
908
+ name: sanitizeDisplayString(header[1]),
909
+ status: sanitizeDisplayString(header[2]),
910
+ };
911
+ jobs.push(current);
912
+ continue;
913
+ }
914
+
915
+ if (!current) {
916
+ continue;
917
+ }
918
+
919
+ const field = line.match(/^\s*(Name|Schedule|Next run|Deliver|Script|Last run):\s+(.+)$/);
920
+ const repeatField = line.match(/^\s*Repeat:\s+(.+)$/);
921
+ const skillsField = line.match(/^\s*Skills:\s+(.+)$/);
922
+
923
+ if (repeatField) {
924
+ current.repeat = sanitizeDisplayString(repeatField[1]);
925
+ continue;
926
+ }
927
+
928
+ if (skillsField) {
929
+ current.skills = skillsField[1]
930
+ .split(",")
931
+ .map((skill) => sanitizeDisplayString(skill))
932
+ .filter(Boolean);
933
+ continue;
934
+ }
935
+
936
+ if (!field) {
937
+ continue;
938
+ }
939
+
940
+ const key = field[1];
941
+ const value = sanitizeDisplayString(field[2]);
942
+
943
+ if (key === "Name") {
944
+ current.name = value;
945
+ } else if (key === "Schedule") {
946
+ current.schedule = value;
947
+ } else if (key === "Next run") {
948
+ current.nextRun = value;
949
+ } else if (key === "Deliver") {
950
+ current.deliverTo = sanitizeDeliveryTarget(value);
951
+ } else if (key === "Script") {
952
+ current.script = value;
953
+ } else if (key === "Last run") {
954
+ current.lastRun = value;
955
+ }
956
+ }
957
+
958
+ return jobs.filter((job) => job.id && job.name);
959
+ }
960
+
961
+ function parseBoxRows(output) {
962
+ return output
963
+ .split("\n")
964
+ .filter((line) => line.trim().startsWith("\u2502"))
965
+ .map((line) =>
966
+ line
967
+ .slice(1, -1)
968
+ .split("\u2502")
969
+ .map((column) => sanitizeDisplayString(column)),
970
+ )
971
+ .filter((columns) => columns.some(Boolean));
972
+ }
973
+
974
+ async function persistLocalConfig({ agentosUrl, configDir, dashboardUrl, hermesUrl, mode, pairingCode }) {
975
+ const targetDir = configDir ?? join(homedir(), ".agentos", "hermes");
976
+ const targetPath = join(targetDir, "connection.json");
977
+ const body = {
978
+ agentosUrl,
979
+ createdAt: new Date().toISOString(),
980
+ dashboardUrl,
981
+ hermesUrl,
982
+ mode,
983
+ pairingCode,
984
+ };
985
+
986
+ await mkdir(targetDir, { recursive: true });
987
+ await writeFile(targetPath, `${JSON.stringify(body, null, 2)}\n`, { mode: 0o600 });
988
+
989
+ return targetPath;
990
+ }
991
+
992
+ async function postHeartbeat(agentosUrl, pairingCode, payload) {
993
+ const response = await fetch(
994
+ `${agentosUrl}/api/hermes/pairing-sessions/${encodeURIComponent(pairingCode)}/heartbeat`,
995
+ {
996
+ body: JSON.stringify(payload),
997
+ headers: {
998
+ "content-type": "application/json",
999
+ accept: "application/json",
1000
+ },
1001
+ method: "POST",
1002
+ },
1003
+ );
1004
+ const text = await response.text();
1005
+ const body = text ? JSON.parse(text) : {};
1006
+
1007
+ if (!response.ok) {
1008
+ const message = typeof body.error === "string" ? body.error : `AgentOS returned ${response.status}.`;
1009
+ throw new Error(message);
1010
+ }
1011
+
1012
+ return body;
1013
+ }
1014
+
1015
+ async function runHeartbeatLoop(sendHeartbeat, intervalMs) {
1016
+ if (!Number.isFinite(intervalMs) || intervalMs < 1000) {
1017
+ throw new Error("--interval-ms must be at least 1000.");
1018
+ }
1019
+
1020
+ let stopping = false;
1021
+ let interval;
1022
+
1023
+ await new Promise((resolve, reject) => {
1024
+ const stop = () => {
1025
+ stopping = true;
1026
+ clearInterval(interval);
1027
+ resolve();
1028
+ };
1029
+
1030
+ process.once("SIGINT", stop);
1031
+ process.once("SIGTERM", stop);
1032
+
1033
+ interval = setInterval(() => {
1034
+ void sendHeartbeat()
1035
+ .then((session) => {
1036
+ if (!stopping) {
1037
+ console.log(`Heartbeat accepted at ${session.lastHeartbeatAt ?? new Date().toISOString()}`);
1038
+ }
1039
+ })
1040
+ .catch((error) => {
1041
+ clearInterval(interval);
1042
+ reject(error);
1043
+ });
1044
+ }, intervalMs);
1045
+ });
1046
+ }
1047
+
1048
+ function normalizeBaseUrl(value) {
1049
+ return value.replace(/\/$/, "");
1050
+ }
1051
+
1052
+ function stripAnsi(value) {
1053
+ return value.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "");
1054
+ }
1055
+
1056
+ function sanitizeDisplayString(value, maxLength = 220) {
1057
+ if (typeof value !== "string") {
1058
+ return undefined;
1059
+ }
1060
+
1061
+ const compacted = value.replace(/\s+/g, " ").trim();
1062
+
1063
+ if (!compacted || compacted === "\u2014") {
1064
+ return undefined;
1065
+ }
1066
+
1067
+ return compacted.slice(0, maxLength);
1068
+ }
1069
+
1070
+ function parseNumber(value) {
1071
+ if (!value) {
1072
+ return undefined;
1073
+ }
1074
+
1075
+ const parsed = Number(String(value).replaceAll(",", ""));
1076
+
1077
+ return Number.isFinite(parsed) ? parsed : undefined;
1078
+ }
1079
+
1080
+ function formatCompactNumber(value) {
1081
+ if (!Number.isFinite(value)) {
1082
+ return "0";
1083
+ }
1084
+
1085
+ if (value >= 1_000_000) {
1086
+ return `${(value / 1_000_000).toFixed(value >= 10_000_000 ? 1 : 2)}M`;
1087
+ }
1088
+
1089
+ if (value >= 1_000) {
1090
+ return `${(value / 1_000).toFixed(value >= 10_000 ? 1 : 2)}K`;
1091
+ }
1092
+
1093
+ return String(value);
1094
+ }
1095
+
1096
+ function escapeRegExp(value) {
1097
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1098
+ }
1099
+
1100
+ function sanitizeDeliveryTarget(value) {
1101
+ const safeValue = sanitizeDisplayString(value);
1102
+
1103
+ if (!safeValue) {
1104
+ return undefined;
1105
+ }
1106
+
1107
+ if (safeValue.includes(":")) {
1108
+ return safeValue.split(":")[0];
1109
+ }
1110
+
1111
+ return safeValue;
1112
+ }
1113
+
1114
+ function redactLocalPath(value) {
1115
+ if (!value) {
1116
+ return undefined;
1117
+ }
1118
+
1119
+ return value.replaceAll(homedir(), "~");
1120
+ }
1121
+
1122
+ function redactSensitiveContent(value) {
1123
+ if (!value) {
1124
+ return "";
1125
+ }
1126
+
1127
+ return value
1128
+ .replaceAll(homedir(), "~")
1129
+ .replace(
1130
+ /^(\s*[\w.-]*(?:api[_-]?key|token|secret|password|credential|private[_-]?key|auth|cookie|session)[\w.-]*\s*[:=]\s*).+$/gim,
1131
+ "$1[redacted]",
1132
+ )
1133
+ .replace(/\bsk-[A-Za-z0-9_-]{16,}\b/g, "[redacted-openai-key]")
1134
+ .replace(/\bgh[pousr]_[A-Za-z0-9_]{16,}\b/g, "[redacted-github-token]")
1135
+ .replace(/\bxox[baprs]-[A-Za-z0-9-]{16,}\b/g, "[redacted-slack-token]")
1136
+ .replace(/\bAKIA[0-9A-Z]{16}\b/g, "[redacted-aws-key]")
1137
+ .replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[redacted-private-key]");
1138
+ }
1139
+
1140
+ function compactObject(value) {
1141
+ return Object.fromEntries(
1142
+ Object.entries(value).filter(([, entry]) => {
1143
+ if (entry === undefined || entry === null) {
1144
+ return false;
1145
+ }
1146
+
1147
+ if (Array.isArray(entry)) {
1148
+ return entry.length > 0;
1149
+ }
1150
+
1151
+ return true;
1152
+ }),
1153
+ );
1154
+ }
1155
+
1156
+ function printHelp() {
1157
+ console.log(`AgentOS Hermes connector ${VERSION}
1158
+
1159
+ Usage:
1160
+ agentos-hermes connect --pair <code> [options]
1161
+
1162
+ Commands:
1163
+ connect Pair the local Hermes agent with AgentOS
1164
+ `);
1165
+ }
1166
+
1167
+ function printConnectHelp() {
1168
+ console.log(`Usage:
1169
+ agentos-hermes connect --pair <code> [options]
1170
+
1171
+ Options:
1172
+ --agentos-url <url> AgentOS web app URL. Defaults to ${DEFAULT_AGENTOS_URL}
1173
+ --hermes-url <url> Local Hermes gateway URL. Defaults to ${DEFAULT_HERMES_URL}
1174
+ --dashboard-url <url> Optional Hermes dashboard URL, often http://127.0.0.1:9119
1175
+ --mode <mode> plugin, sidecar, or direct-url. Defaults to ${DEFAULT_MODE}
1176
+ --config-dir <path> Local config directory. Defaults to ~/.agentos/hermes
1177
+ --hermes-command <cmd> Hermes CLI executable. Defaults to ${DEFAULT_HERMES_COMMAND}
1178
+ --interval-ms <ms> Heartbeat interval. Defaults to 15000
1179
+ --skip-inventory Send basic heartbeat metadata only
1180
+ --once Send one heartbeat and exit, useful for CI and smoke tests
1181
+ `);
1182
+ }
1183
+
1184
+ main(process.argv.slice(2)).catch((error) => {
1185
+ console.error(error instanceof Error ? error.message : error);
1186
+ process.exitCode = 1;
1187
+ });
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@agentworkspaceos/hermes",
3
+ "version": "0.1.0",
4
+ "description": "AgentOS connector CLI for pairing a local Hermes runtime with an AgentOS workspace.",
5
+ "type": "module",
6
+ "bin": {
7
+ "agentos-hermes": "bin/agentos-hermes.js"
8
+ },
9
+ "files": [
10
+ "bin/agentos-hermes.js",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "cli": "node bin/agentos-hermes.js",
15
+ "test": "node --test tests/**/*.test.js",
16
+ "pack:dry-run": "npm pack --dry-run",
17
+ "publish:public": "npm publish --access public"
18
+ },
19
+ "keywords": [
20
+ "agentos",
21
+ "hermes",
22
+ "connector",
23
+ "cli"
24
+ ],
25
+ "engines": {
26
+ "node": ">=22.0.0"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
31
+ }