alegra-data-agent 0.4.0 → 0.4.1

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,8 +1,10 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
- import { dirname } from "node:path";
2
+ import { dirname, join } from "node:path";
3
3
  import { logWarn } from "./colors.js";
4
4
  import { hookCommandFor, runHookInstall, runHookUninstall, } from "./commands/hook.js";
5
- import { COMMAND_NAME, getClaudeSettingsPath, getCodexHooksPath, getCursorHooksPath, getGeminiSettingsPath, getOpenCodePluginPath, readLockFile, } from "./lib.js";
5
+ import { HERMES_PLUGIN_NAME, hermesPluginManifest, hermesPluginModule } from "./hermes-plugin.js";
6
+ import { runHermesCliAt } from "./installer.js";
7
+ import { COMMAND_NAME, getClaudeSettingsPath, getCodexHooksPath, getCursorHooksPath, getGeminiSettingsPath, getHermesHome, getHermesPluginsDir, getOpenCodePluginPath, readLockFile, } from "./lib.js";
6
8
  function readJsonSafe(path) {
7
9
  if (!existsSync(path))
8
10
  return {};
@@ -111,6 +113,49 @@ export const AlegraDataAgentAutoUpdate = async ({ $ }) => {
111
113
  `;
112
114
  writeFileSync(path, plugin, "utf-8");
113
115
  }
116
+ function installHermes(command) {
117
+ const dir = join(getHermesPluginsDir(), HERMES_PLUGIN_NAME);
118
+ mkdirSync(dir, { recursive: true });
119
+ writeFileSync(join(dir, "plugin.yaml"), hermesPluginManifest(), "utf-8");
120
+ writeFileSync(join(dir, "__init__.py"), hermesPluginModule(command), "utf-8");
121
+ enableHermesPlugin();
122
+ }
123
+ function enableHermesPlugin() {
124
+ const res = runHermesCliAt(getHermesHome(), ["plugins", "enable", HERMES_PLUGIN_NAME]);
125
+ if (res.ok)
126
+ return;
127
+ logWarn(res.missingBinary
128
+ ? `No encontré el binario 'hermes' en el PATH: el plugin de auto-update quedó escrito pero sin habilitar (corré 'hermes plugins enable ${HERMES_PLUGIN_NAME}').`
129
+ : `No pude habilitar el plugin de auto-update de Hermes: ${(res.stderr || res.stdout).trim().slice(0, 200)}`);
130
+ }
131
+ function removeHermesPlugin() {
132
+ const dir = join(getHermesPluginsDir(), HERMES_PLUGIN_NAME);
133
+ if (!existsSync(dir))
134
+ return;
135
+ const home = getHermesHome();
136
+ runHermesCliAt(home, ["plugins", "disable", HERMES_PLUGIN_NAME]);
137
+ rmSync(dir, { recursive: true, force: true });
138
+ dropHermesPluginFromList(home, "enabled");
139
+ dropHermesPluginFromList(home, "disabled");
140
+ runHermesCliAt(home, ["config", "unset", `plugins.entries.${HERMES_PLUGIN_NAME}`]);
141
+ }
142
+ function dropHermesPluginFromList(home, key) {
143
+ const res = runHermesCliAt(home, ["config", "get", `plugins.${key}`]);
144
+ if (!res.ok)
145
+ return;
146
+ const names = res.stdout
147
+ .split(/\r?\n/)
148
+ .map((line) => line.trim())
149
+ .filter((line) => line.startsWith("- "))
150
+ .map((line) => line
151
+ .slice(2)
152
+ .trim()
153
+ .replace(/^['"]|['"]$/g, ""));
154
+ if (!names.includes(HERMES_PLUGIN_NAME))
155
+ return;
156
+ const kept = names.filter((n) => n !== HERMES_PLUGIN_NAME);
157
+ runHermesCliAt(home, ["config", "set", `plugins.${key}`, JSON.stringify(kept)]);
158
+ }
114
159
  function removeJsonHook(path, key) {
115
160
  const cfg = readJsonSafe(path);
116
161
  if (!cfg)
@@ -147,6 +192,9 @@ export function installAutoUpdate(target, mode = "auto") {
147
192
  case "opencode":
148
193
  installOpenCode(hookCommandFor(effective));
149
194
  break;
195
+ case "hermes":
196
+ installHermes(hookCommandFor(effective));
197
+ break;
150
198
  }
151
199
  }
152
200
  catch (err) {
@@ -174,6 +222,9 @@ export function removeAutoUpdate(target) {
174
222
  rmSync(p, { force: true });
175
223
  break;
176
224
  }
225
+ case "hermes":
226
+ removeHermesPlugin();
227
+ break;
177
228
  }
178
229
  }
179
230
  catch (err) {
@@ -199,6 +250,10 @@ export function hasAutoUpdate(target) {
199
250
  return jsonHookHasOurs(getGeminiSettingsPath(), "SessionStart");
200
251
  case "opencode":
201
252
  return existsSync(getOpenCodePluginPath());
253
+ case "hermes": {
254
+ const manifest = join(getHermesPluginsDir(), HERMES_PLUGIN_NAME, "plugin.yaml");
255
+ return existsSync(manifest);
256
+ }
202
257
  }
203
258
  }
204
259
  export function refreshAutoUpdate() {
@@ -124,7 +124,7 @@ function printSummary(results, manifest, targets) {
124
124
  }
125
125
  function printNextSteps(noHook) {
126
126
  section("Ya estás listo");
127
- log(` ${bold(yellow("1."))} ${bold("Reiniciá tu cliente")} ${gray("(Claude Code, Cursor, OpenCode, Antigravity o Codex)")}.`);
127
+ log(` ${bold(yellow("1."))} ${bold("Reiniciá tu cliente")} ${gray("(Claude Code, Cursor, OpenCode, Antigravity, Codex o Hermes)")}.`);
128
128
  log(` ${bold(yellow("2."))} ${bold("Hacele tu pregunta en español.")}`);
129
129
  log(` ${gray("Ejemplo:")} ${cyan('"cuántos nuevos clientes en mayo en Colombia?"')}`);
130
130
  log("");
@@ -0,0 +1,83 @@
1
+ import { COMMAND_NAME } from "./lib.js";
2
+ export const HERMES_PLUGIN_NAME = "alegra-data-agent-autoupdate";
3
+ export function hermesPluginManifest() {
4
+ return `# Generado por ${COMMAND_NAME}. Borrar esta carpeta desactiva el auto-update.
5
+ name: ${HERMES_PLUGIN_NAME}
6
+ version: 1.0.0
7
+ description: Mantiene al día las skills y MCPs de ${COMMAND_NAME} al iniciar una sesión de Hermes.
8
+ provides_hooks:
9
+ - on_session_start
10
+ `;
11
+ }
12
+ export function hermesPluginModule(command) {
13
+ const commandLiteral = JSON.stringify(command);
14
+ const productLiteral = JSON.stringify(COMMAND_NAME);
15
+ return `"""Auto-update de ${COMMAND_NAME} al iniciar una sesión de Hermes.
16
+
17
+ Generado por el instalador — no editar a mano. Borrar esta carpeta (o correr
18
+ \`${COMMAND_NAME} uninstall\`) desactiva el auto-update.
19
+ """
20
+
21
+ import logging
22
+ import os
23
+ import shlex
24
+ import shutil
25
+ import subprocess
26
+ from pathlib import Path
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ COMMAND = ${commandLiteral}
31
+
32
+ # Hermes puede arrancar sin el PATH del shell del usuario (Desktop, servicios):
33
+ # ahí \`pnpm\` no resuelve y el auto-update moriría en silencio.
34
+ EXTRA_PATH_DIRS = (
35
+ os.environ.get("PNPM_HOME", ""),
36
+ os.path.expanduser("~/.local/share/pnpm"),
37
+ os.path.expanduser("~/Library/pnpm"),
38
+ "/opt/homebrew/bin",
39
+ "/usr/local/bin",
40
+ )
41
+
42
+
43
+ def _log_skip(reason: str) -> None:
44
+ """Deja constancia en autoupdate.log — el mismo archivo que reporta status."""
45
+ try:
46
+ base = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config")
47
+ log_file = Path(base) / ${productLiteral} / "autoupdate.log"
48
+ log_file.parent.mkdir(parents=True, exist_ok=True)
49
+ with log_file.open("a", encoding="utf-8") as fh:
50
+ fh.write(f"[hermes] {reason}\\n")
51
+ except Exception:
52
+ pass
53
+
54
+
55
+ def _run_update(**_kwargs) -> None:
56
+ """Lanza el update y vuelve: nunca bloquea ni rompe el arranque."""
57
+ try:
58
+ argv = shlex.split(COMMAND)
59
+ if not argv:
60
+ return
61
+ env = dict(os.environ)
62
+ extra = [d for d in EXTRA_PATH_DIRS if d and os.path.isdir(d)]
63
+ if extra:
64
+ env["PATH"] = os.pathsep.join(extra + [env.get("PATH", "")])
65
+ if shutil.which(argv[0], path=env.get("PATH")) is None:
66
+ _log_skip(f"'{argv[0]}' no esta en el PATH; auto-update omitido")
67
+ return
68
+ subprocess.Popen(
69
+ argv,
70
+ env=env,
71
+ stdin=subprocess.DEVNULL,
72
+ stdout=subprocess.DEVNULL,
73
+ stderr=subprocess.DEVNULL,
74
+ start_new_session=True,
75
+ )
76
+ except Exception as exc:
77
+ logger.debug("%s: auto-update no se pudo lanzar: %s", ${productLiteral}, exc)
78
+
79
+
80
+ def register(ctx):
81
+ ctx.register_hook("on_session_start", _run_update)
82
+ `;
83
+ }
package/dist/installer.js CHANGED
@@ -1,10 +1,11 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { createHash } from "node:crypto";
2
3
  import { copyFileSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
4
  import { basename, dirname, join, resolve, sep } from "node:path";
4
5
  import { AuthRequiredError, fetchManifest, fetchSkillFile } from "./api.js";
5
6
  import { dim, gray, green, logError, red, yellow } from "./colors.js";
6
- import { adaptStdioCommand, ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getCursorLegacyRulesDir, getLockFilePath, getOpenCodeLegacyPromptsDir, getPackageVersion, readLockFile, sha256File, writeLockFile, } from "./lib.js";
7
- import { inlineReferences, renderMcpForTarget } from "./render.js";
7
+ import { adaptStdioCommand, ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getCursorLegacyRulesDir, getHermesBin, getLockFilePath, getOpenCodeLegacyPromptsDir, getPackageVersion, readLockFile, sha256File, writeLockFile, } from "./lib.js";
8
+ import { inlineReferences, renderHermesMcpOps, renderMcpForTarget, } from "./render.js";
8
9
  import { TARGETS, detectInstalledTargets } from "./targets.js";
9
10
  const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
10
11
  const MCP_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
@@ -257,6 +258,10 @@ function readJsonOrEmpty(path) {
257
258
  }
258
259
  function registerMcpInTarget(mcp, spec) {
259
260
  validateMcpName(mcp.name);
261
+ if (spec.mcpMechanism === "hermes-cli") {
262
+ registerMcpInHermes(mcp, spec);
263
+ return;
264
+ }
260
265
  if (spec.mcpMechanism === "toml-block") {
261
266
  registerMcpInTomlBlock(mcp, spec);
262
267
  return;
@@ -269,6 +274,9 @@ function registerMcpInTarget(mcp, spec) {
269
274
  atomicWriteFile(spec.mcpConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
270
275
  }
271
276
  function unregisterMcpInTarget(mcpName, spec) {
277
+ if (spec.mcpMechanism === "hermes-cli") {
278
+ return unregisterMcpInHermes(mcpName, spec);
279
+ }
272
280
  if (spec.mcpMechanism === "toml-block") {
273
281
  return unregisterMcpInTomlBlock(mcpName, spec);
274
282
  }
@@ -284,6 +292,148 @@ function unregisterMcpInTarget(mcpName, spec) {
284
292
  atomicWriteFile(spec.mcpConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
285
293
  return true;
286
294
  }
295
+ const HERMES_CLI_TIMEOUT_MS = 20_000;
296
+ export function runHermesCliAt(hermesHome, args) {
297
+ const res = spawnSync(getHermesBin(), args, {
298
+ encoding: "utf-8",
299
+ timeout: HERMES_CLI_TIMEOUT_MS,
300
+ env: { ...process.env, HERMES_HOME: hermesHome },
301
+ });
302
+ const missingBinary = res.error?.code === "ENOENT";
303
+ return {
304
+ ok: res.status === 0,
305
+ stdout: res.stdout ?? "",
306
+ stderr: res.stderr ?? res.error?.message ?? "",
307
+ missingBinary,
308
+ };
309
+ }
310
+ function runHermesCli(spec, args) {
311
+ return runHermesCliAt(dirname(spec.mcpConfigPath), args);
312
+ }
313
+ function hermesCliError(action, res) {
314
+ if (res.missingBinary) {
315
+ return new Error(`no encontré el binario '${getHermesBin()}' en el PATH, necesario para registrar MCPs en Hermes. Instalá Hermes Agent (curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash) o apuntá HERMES_BIN al binario.`);
316
+ }
317
+ const detail = (res.stderr || res.stdout).trim().split("\n").slice(-1)[0] ?? "";
318
+ return new Error(`'hermes ${action}' falló: ${detail.slice(0, 300)}`);
319
+ }
320
+ function registerMcpInHermes(mcp, spec) {
321
+ for (const op of renderHermesMcpOps(mcp)) {
322
+ const key = `mcp_servers.${mcp.name}.${op.path}`;
323
+ const res = runHermesCli(spec, ["config", "set", key, op.value]);
324
+ if (!res.ok)
325
+ throw hermesCliError(`config set ${key}`, res);
326
+ }
327
+ }
328
+ function unregisterMcpInHermes(mcpName, spec) {
329
+ if (readHermesMcpFields(spec, mcpName) === null)
330
+ return false;
331
+ const key = `mcp_servers.${mcpName}`;
332
+ const res = runHermesCli(spec, ["config", "unset", key]);
333
+ if (!res.ok)
334
+ throw hermesCliError(`config unset ${key}`, res);
335
+ return true;
336
+ }
337
+ export function readHermesMcpFields(spec, name) {
338
+ const res = runHermesCli(spec, ["config", "get", `mcp_servers.${name}`]);
339
+ if (!res.ok)
340
+ return null;
341
+ return parseHermesConfigGet(res.stdout);
342
+ }
343
+ export function parseHermesConfigGet(text) {
344
+ const out = {};
345
+ let groupKey = null;
346
+ let groupIndent = 0;
347
+ let group = null;
348
+ const closeGroup = () => {
349
+ if (groupKey !== null && group !== null)
350
+ out[groupKey] = group;
351
+ groupKey = null;
352
+ group = null;
353
+ };
354
+ for (const raw of text.split(/\r?\n/)) {
355
+ if (raw.trim() === "")
356
+ continue;
357
+ const line = raw.trim();
358
+ const indent = raw.length - raw.trimStart().length;
359
+ if (groupKey !== null) {
360
+ if (line.startsWith("- ") && indent >= groupIndent) {
361
+ const list = Array.isArray(group) ? [...group] : [];
362
+ list.push(unquoteYamlScalar(line.slice(2).trim()));
363
+ group = list;
364
+ continue;
365
+ }
366
+ if (indent > groupIndent) {
367
+ const kv = line.match(/^([A-Za-z0-9_.-]+):\s*(.+)$/);
368
+ if (kv) {
369
+ const map = group !== null && !Array.isArray(group) ? { ...group } : {};
370
+ map[kv[1]] = unquoteYamlScalar(kv[2].trim());
371
+ group = map;
372
+ continue;
373
+ }
374
+ }
375
+ closeGroup();
376
+ }
377
+ const kv = line.match(/^([A-Za-z0-9_.-]+):\s*(.*)$/);
378
+ if (!kv)
379
+ continue;
380
+ if (kv[2] === "") {
381
+ groupKey = kv[1];
382
+ groupIndent = indent;
383
+ group = null;
384
+ continue;
385
+ }
386
+ out[kv[1]] = unquoteYamlScalar(kv[2].trim());
387
+ }
388
+ closeGroup();
389
+ return Object.keys(out).length > 0 ? out : null;
390
+ }
391
+ function unquoteYamlScalar(value) {
392
+ if (value.length >= 2) {
393
+ const first = value[0];
394
+ const last = value[value.length - 1];
395
+ if ((first === "'" && last === "'") || (first === '"' && last === '"')) {
396
+ return value.slice(1, -1);
397
+ }
398
+ }
399
+ return value;
400
+ }
401
+ function normalizeHermesValue(value) {
402
+ if (value === undefined)
403
+ return null;
404
+ if (typeof value === "string")
405
+ return value;
406
+ if (Array.isArray(value))
407
+ return value;
408
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
409
+ }
410
+ function hermesExpectedFields(mcp) {
411
+ const expected = {};
412
+ for (const op of renderHermesMcpOps(mcp))
413
+ expected[op.path] = parseHermesSerialized(op.value);
414
+ return expected;
415
+ }
416
+ function parseHermesSerialized(value) {
417
+ const trimmed = value.trim();
418
+ if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
419
+ try {
420
+ return JSON.parse(trimmed);
421
+ }
422
+ catch {
423
+ return value;
424
+ }
425
+ }
426
+ return value;
427
+ }
428
+ export function hermesExpectedSnapshot(mcp) {
429
+ return stableStringify(hermesExpectedFields(mcp));
430
+ }
431
+ export function hermesActualSnapshot(mcp, fields) {
432
+ const actual = {};
433
+ for (const op of renderHermesMcpOps(mcp))
434
+ actual[op.path] = normalizeHermesValue(fields[op.path]);
435
+ return stableStringify(actual);
436
+ }
287
437
  function tomlBasicString(s) {
288
438
  return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
289
439
  }
@@ -382,7 +532,9 @@ function unregisterMcpInTomlBlock(name, spec) {
382
532
  export function renderedEntryHash(mcp, spec) {
383
533
  const rendered = spec.mcpMechanism === "toml-block"
384
534
  ? renderMcpToml(mcp).trim()
385
- : stableStringify(renderMcpForTarget(mcp, spec));
535
+ : spec.mcpMechanism === "hermes-cli"
536
+ ? hermesExpectedSnapshot(mcp)
537
+ : stableStringify(renderMcpForTarget(mcp, spec));
386
538
  return createHash("sha256").update(rendered).digest("hex");
387
539
  }
388
540
  function hashOf(value) {
@@ -400,6 +552,17 @@ function isDiscardedByClient(current, spec) {
400
552
  }
401
553
  export function inspectMcpInTarget(mcp, spec, prevHash) {
402
554
  try {
555
+ if (spec.mcpMechanism === "hermes-cli") {
556
+ const fields = readHermesMcpFields(spec, mcp.name);
557
+ if (fields === null)
558
+ return "missing";
559
+ const current = hermesActualSnapshot(mcp, fields);
560
+ if (current === hermesExpectedSnapshot(mcp))
561
+ return "ok";
562
+ if (prevHash && hashOf(current) === prevHash)
563
+ return "stale";
564
+ return "modified";
565
+ }
403
566
  if (!existsSync(spec.mcpConfigPath))
404
567
  return "missing";
405
568
  if (spec.mcpMechanism === "toml-block") {
@@ -594,12 +757,16 @@ export async function installSkill(name, manifest, opts = {}) {
594
757
  error: failures.join("; "),
595
758
  };
596
759
  }
760
+ const outOfScopeTargets = prev && prev.bundleHash === entry.bundleHash
761
+ ? (prev.targets ?? []).filter((t) => !targetIds.includes(t))
762
+ : [];
763
+ const lockTargets = [...new Set([...outOfScopeTargets, ...okTargets])];
597
764
  lock.skills[name] = {
598
765
  name,
599
766
  installedFrom: getPackageVersion(),
600
767
  installedAt: new Date().toISOString(),
601
768
  bundleHash: entry.bundleHash,
602
- targets: okTargets,
769
+ targets: lockTargets,
603
770
  };
604
771
  writeLockFile(lock);
605
772
  return {
@@ -723,12 +890,16 @@ export function installMcp(mcp, opts = {}) {
723
890
  if (okTargets.includes(t.id))
724
891
  renderedHash[t.id] = renderedEntryHash(mcp, t);
725
892
  }
893
+ const outOfScopeTargets = configChanged
894
+ ? []
895
+ : (prev?.targets ?? []).filter((t) => !targetIds.includes(t) && (!mcp.targets || mcp.targets.includes(t)));
896
+ const lockTargets = [...new Set([...outOfScopeTargets, ...okTargets])];
726
897
  lock.mcps[mcp.name] = {
727
898
  name: mcp.name,
728
899
  installedFrom: getPackageVersion(),
729
900
  installedAt: new Date().toISOString(),
730
901
  configHash: mcpConfigHash(mcp),
731
- targets: okTargets,
902
+ targets: lockTargets,
732
903
  renderedHash,
733
904
  };
734
905
  writeLockFile(lock);
package/dist/lib.js CHANGED
@@ -14,6 +14,7 @@ export const ALL_TARGETS = [
14
14
  "opencode",
15
15
  "antigravity",
16
16
  "codex",
17
+ "hermes",
17
18
  ];
18
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
20
  export function getPackageRoot() {
@@ -100,6 +101,23 @@ export function getCodexConfigPath() {
100
101
  const base = codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
101
102
  return join(base, "config.toml");
102
103
  }
104
+ export function getHermesHome() {
105
+ const hermesHome = process.env.HERMES_HOME;
106
+ return hermesHome && hermesHome.length > 0 ? hermesHome : join(homedir(), ".hermes");
107
+ }
108
+ export function getHermesSkillsDir() {
109
+ return join(getHermesHome(), "skills");
110
+ }
111
+ export function getHermesConfigPath() {
112
+ return join(getHermesHome(), "config.yaml");
113
+ }
114
+ export function getHermesPluginsDir() {
115
+ return join(getHermesHome(), "plugins");
116
+ }
117
+ export function getHermesBin() {
118
+ const bin = process.env.HERMES_BIN;
119
+ return bin && bin.length > 0 ? bin : "hermes";
120
+ }
103
121
  export function getTargetSkillsDir(target) {
104
122
  switch (target) {
105
123
  case "claude-code":
@@ -112,6 +130,8 @@ export function getTargetSkillsDir(target) {
112
130
  return getAntigravitySkillsDir();
113
131
  case "codex":
114
132
  return getCodexSkillsDir();
133
+ case "hermes":
134
+ return getHermesSkillsDir();
115
135
  }
116
136
  }
117
137
  export function getTargetMcpConfigPath(target) {
@@ -126,6 +146,8 @@ export function getTargetMcpConfigPath(target) {
126
146
  return getAntigravityMcpConfigPath();
127
147
  case "codex":
128
148
  return getCodexConfigPath();
149
+ case "hermes":
150
+ return getHermesConfigPath();
129
151
  }
130
152
  }
131
153
  export function getLockFilePath() {
package/dist/render.js CHANGED
@@ -119,5 +119,38 @@ function bearerInterpolation(env, target) {
119
119
  return `Bearer {env:${env}}`;
120
120
  case "codex":
121
121
  throw new Error("codex usa config TOML (bearer_token_env_var), no headers JSON");
122
+ case "hermes":
123
+ return `Bearer \${${env}}`;
124
+ }
125
+ }
126
+ export function renderHermesMcpOps(mcp) {
127
+ if (mcp.transport === "stdio") {
128
+ const { command, args } = adaptStdioCommand(mcp.command ?? "", mcp.args ?? []);
129
+ const ops = [{ path: "command", value: command }];
130
+ if (args.length > 0)
131
+ ops.push({ path: "args", value: JSON.stringify(args) });
132
+ if (mcp.env && Object.keys(mcp.env).length > 0) {
133
+ ops.push({ path: "env", value: JSON.stringify(mcp.env) });
134
+ }
135
+ return ops;
136
+ }
137
+ const ops = [{ path: "url", value: mcp.url ?? "" }];
138
+ if (mcp.auth.type === "oauth") {
139
+ ops.push({ path: "auth", value: "oauth" });
122
140
  }
141
+ else if (mcp.auth.type === "gateway") {
142
+ if (!mcp.auth.tokenEnv) {
143
+ throw new Error(`MCP '${mcp.name}' es type=gateway pero no trae tokenEnv`);
144
+ }
145
+ ops.push({
146
+ path: "headers",
147
+ value: JSON.stringify({ Authorization: bearerInterpolation(mcp.auth.tokenEnv, "hermes") }),
148
+ });
149
+ }
150
+ return ops;
151
+ }
152
+ export function hermesManagedFields(mcp) {
153
+ return renderHermesMcpOps(mcp)
154
+ .map((op) => op.path)
155
+ .sort();
123
156
  }
package/dist/targets.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { getAntigravityGeminiSkillsDir, getAntigravityMcpConfigPath, getAntigravitySkillsDir, getClaudeJsonPath, getClaudeSkillsDir, getCodexConfigPath, getCodexSkillsDir, getCursorMcpPath, getCursorSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
4
+ import { getAntigravityGeminiSkillsDir, getAntigravityMcpConfigPath, getAntigravitySkillsDir, getClaudeJsonPath, getClaudeSkillsDir, getCodexConfigPath, getCodexSkillsDir, getCursorMcpPath, getCursorSkillsDir, getHermesConfigPath, getHermesHome, getHermesSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
5
5
  export const TARGETS = {
6
6
  "claude-code": {
7
7
  id: "claude-code",
@@ -53,6 +53,16 @@ export const TARGETS = {
53
53
  mcpUrlKey: "url",
54
54
  mcpMechanism: "toml-block",
55
55
  },
56
+ hermes: {
57
+ id: "hermes",
58
+ label: "Hermes",
59
+ skillsDir: getHermesSkillsDir(),
60
+ skillFormat: "folder",
61
+ mcpConfigPath: getHermesConfigPath(),
62
+ mcpRootKey: "mcp_servers",
63
+ mcpUrlKey: "url",
64
+ mcpMechanism: "hermes-cli",
65
+ },
56
66
  };
57
67
  export function detectInstalledTargets() {
58
68
  const home = homedir();
@@ -68,6 +78,7 @@ export function detectInstalledTargets() {
68
78
  opencode: [join(xdgConfig, "opencode")],
69
79
  antigravity: [join(home, ".gemini", "antigravity-cli"), join(home, ".gemini", "antigravity")],
70
80
  codex: [codexHome],
81
+ hermes: [getHermesHome()],
71
82
  };
72
83
  const out = [];
73
84
  for (const id of Object.keys(sentinels)) {
package/dist/ui.js CHANGED
@@ -44,7 +44,7 @@ ${bold("Comandos:")}
44
44
  login Autenticarte con tu cuenta @alegra.com
45
45
  logout Cerrar sesión (borra el token local)
46
46
  whoami Mostrar la cuenta actualmente autenticada
47
- install Instala skills + MCPs en tus clientes (Claude Code, Cursor, OpenCode, Antigravity, Codex)
47
+ install Instala skills + MCPs en tus clientes (Claude Code, Cursor, OpenCode, Antigravity, Codex, Hermes)
48
48
  update Re-descarga skills/MCPs que cambiaron
49
49
  list Lista skills y MCPs disponibles en el gateway
50
50
  status Muestra estado: auth, instalado, drift
@@ -59,7 +59,7 @@ ${bold("Opciones:")}
59
59
  -y, --yes No pedir confirmación — solo uninstall pregunta
60
60
  --dry-run Mostrar qué se haría, sin tocar archivos
61
61
  --only=a,b,c Instalar solo esas skills/MCPs
62
- --target=NAME Restringir al cliente NAME (claude-code|cursor|opencode|antigravity|codex)
62
+ --target=NAME Restringir al cliente NAME (claude-code|cursor|opencode|antigravity|codex|hermes)
63
63
  --no-hook No instalar el hook de SessionStart
64
64
  --no-bounce No saltar a la última versión publicada (la usa el hook)
65
65
  --auto-update Aceptado por compatibilidad — el hook ya es auto-update por default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alegra-data-agent",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "description": "Agente de Data interno de Alegra. Instala skills + MCPs en tu cliente de IA con un comando.",
6
6
  "license": "UNLICENSED",