alegra-data-agent 0.4.0 → 0.4.2

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/auth.js CHANGED
@@ -46,7 +46,7 @@ export function writeAuth(record) {
46
46
  const path = getAuthFilePath();
47
47
  _authCache = { value: record };
48
48
  mkdirSync(dirname(path), { recursive: true });
49
- writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
49
+ writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
50
50
  try {
51
51
  chmodSync(path, 0o600);
52
52
  }
@@ -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("");
@@ -5,7 +5,7 @@ import { AuthRequiredError, fetchManifest } from "../api.js";
5
5
  import { readAuth } from "../auth.js";
6
6
  import { bold, cyan, dim, gray, green, log, logError, logWarn, yellow } from "../colors.js";
7
7
  import { detectDrift, inspectMcpInTarget } from "../installer.js";
8
- import { COMMAND_NAME, GATEWAY_URL, getAutoUpdateLogPath, getClaudeSkillsDir, getLockFilePath, getPackageVersion, getTargetSkillsDir, readLockFile, } from "../lib.js";
8
+ import { COMMAND_NAME, GATEWAY_URL, getAutoUpdateLogPath, getClaudeJsonPath, getClaudeSkillsDir, getLockFilePath, getMcpScopeDir, getMcpScopeWarning, getPackageVersion, getTargetSkillsDir, readLockFile, } from "../lib.js";
9
9
  import { TARGETS } from "../targets.js";
10
10
  function reportAutoUpdateLog() {
11
11
  const logPath = getAutoUpdateLogPath();
@@ -75,7 +75,7 @@ function reportMcps(manifest, lock) {
75
75
  log("");
76
76
  }
77
77
  function reportAccountConnectorOverlap(manifest) {
78
- const claudeCfg = TARGETS["claude-code"].mcpConfigPath;
78
+ const claudeCfg = getClaudeJsonPath();
79
79
  if (!existsSync(claudeCfg))
80
80
  return;
81
81
  let connected = [];
@@ -180,7 +180,7 @@ function connectorUrlsFromCredentials(ourServerNames, credentialsPath = join(hom
180
180
  }
181
181
  return urls;
182
182
  }
183
- export function findClaudeAiDuplicates(manifest, claudeJsonPath = TARGETS["claude-code"].mcpConfigPath) {
183
+ export function findClaudeAiDuplicates(manifest, claudeJsonPath = getClaudeJsonPath(), mcpConfigPath = claudeJsonPath) {
184
184
  if (!existsSync(claudeJsonPath))
185
185
  return [];
186
186
  let cfg;
@@ -193,7 +193,18 @@ export function findClaudeAiDuplicates(manifest, claudeJsonPath = TARGETS["claud
193
193
  const connectors = cfg.claudeAiMcpEverConnected;
194
194
  if (!Array.isArray(connectors))
195
195
  return [];
196
- const localServers = cfg[TARGETS["claude-code"].mcpRootKey];
196
+ let mcpCfg = cfg;
197
+ if (mcpConfigPath !== claudeJsonPath) {
198
+ if (!existsSync(mcpConfigPath))
199
+ return [];
200
+ try {
201
+ mcpCfg = JSON.parse(readFileSync(mcpConfigPath, "utf-8"));
202
+ }
203
+ catch {
204
+ return [];
205
+ }
206
+ }
207
+ const localServers = mcpCfg[TARGETS["claude-code"].mcpRootKey];
197
208
  if (!localServers || typeof localServers !== "object")
198
209
  return [];
199
210
  const ourNames = new Set(Object.keys(localServers));
@@ -222,7 +233,7 @@ export function findClaudeAiDuplicates(manifest, claudeJsonPath = TARGETS["claud
222
233
  return out;
223
234
  }
224
235
  function reportClaudeAiDuplicates(manifest) {
225
- const dupes = findClaudeAiDuplicates(manifest);
236
+ const dupes = findClaudeAiDuplicates(manifest, getClaudeJsonPath(), TARGETS["claude-code"].mcpConfigPath);
226
237
  if (dupes.length === 0)
227
238
  return;
228
239
  logWarn(`Hay ${dupes.length} MCP(s) que tenés por dos vías: server local y conector de claude.ai.`);
@@ -259,6 +270,14 @@ export async function runStatus() {
259
270
  log(` ${gray("Autenticado:")} ${auth ? green(auth.email) : gray("no")}`);
260
271
  log(` ${gray("Destino:")} ${destino}`);
261
272
  log(` ${gray("Lock file:")} ${getLockFilePath()}`);
273
+ log(` ${gray("MCPs en:")} ${TARGETS["claude-code"].mcpConfigPath}`);
274
+ const scopeDir = getMcpScopeDir();
275
+ if (scopeDir) {
276
+ log(` ${gray("Scope MCPs:")} proyecto ${dim(`(solo dentro de ${scopeDir})`)}`);
277
+ }
278
+ const scopeWarning = getMcpScopeWarning();
279
+ if (scopeWarning)
280
+ logWarn(`${scopeWarning} — los MCPs quedaron en scope usuario.`);
262
281
  log("");
263
282
  reportAutoUpdateLog();
264
283
  if (!auth) {
@@ -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,12 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { createHash } from "node:crypto";
2
- import { copyFileSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
+ import { chmodSync, copyFileSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, statSync, 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 { ensureLazyProxy, lazyMcpDir, stdioLaunch } from "./lazy-mcp.js";
8
+ import { ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getCursorLegacyRulesDir, getHermesBin, getLockFilePath, getMcpScopeDir, getOpenCodeLegacyPromptsDir, getPackageVersion, readLockFile, sha256File, writeLockFile, } from "./lib.js";
9
+ import { inlineReferences, renderHermesMcpOps, renderMcpForTarget, } from "./render.js";
8
10
  import { TARGETS, detectInstalledTargets } from "./targets.js";
9
11
  const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
10
12
  const MCP_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
@@ -42,6 +44,7 @@ function mcpConfigHash(mcp) {
42
44
  type: mcp.auth.type,
43
45
  tokenEnv: mcp.auth.tokenEnv ?? null,
44
46
  ...(mcp.delivery ? { delivery: mcp.delivery } : {}),
47
+ ...(mcp.lazy ? { lazy: mcp.lazy } : {}),
45
48
  });
46
49
  return createHash("sha256").update(salient).digest("hex");
47
50
  }
@@ -197,10 +200,24 @@ function withFsRetry(fn) {
197
200
  }
198
201
  }
199
202
  }
200
- function atomicWriteFile(path, content) {
203
+ export function atomicWriteFile(path, content) {
201
204
  ensureDir(dirname(path));
202
205
  const tmp = `${path}.${process.pid}.tmp`;
203
- writeFileSync(tmp, content);
206
+ let mode;
207
+ try {
208
+ mode = statSync(path).mode & 0o777;
209
+ }
210
+ catch {
211
+ mode = undefined;
212
+ }
213
+ writeFileSync(tmp, content, mode === undefined ? undefined : { mode });
214
+ if (mode !== undefined) {
215
+ try {
216
+ chmodSync(tmp, mode);
217
+ }
218
+ catch {
219
+ }
220
+ }
204
221
  for (let attempt = 0;; attempt++) {
205
222
  try {
206
223
  renameSync(tmp, path);
@@ -245,51 +262,243 @@ function pruneBackups(path) {
245
262
  catch {
246
263
  }
247
264
  }
248
- function readJsonOrEmpty(path) {
249
- if (!existsSync(path))
265
+ function parseJsonConfig(path, raw) {
266
+ if (raw === null)
250
267
  return {};
251
268
  try {
252
- return JSON.parse(readFileSync(path, "utf-8"));
269
+ return JSON.parse(raw);
253
270
  }
254
271
  catch {
255
272
  throw new Error(`${path} no es JSON válido. Agente: respaldalo vos mismo (renombralo a .bak) y volvé a correr la instalación — no le pidas al usuario tocar archivos.`);
256
273
  }
257
274
  }
275
+ export function updateJsonFile(path, mutate) {
276
+ for (let attempt = 0; attempt < 5; attempt++) {
277
+ const raw = existsSync(path) ? readFileSync(path, "utf-8") : null;
278
+ const cfg = parseJsonConfig(path, raw);
279
+ if (!mutate(cfg))
280
+ return false;
281
+ const next = `${JSON.stringify(cfg, null, 2)}\n`;
282
+ backupIfExists(path);
283
+ const now = existsSync(path) ? readFileSync(path, "utf-8") : null;
284
+ if (now !== raw)
285
+ continue;
286
+ atomicWriteFile(path, next);
287
+ return true;
288
+ }
289
+ throw new Error(`${path} cambió 5 veces mientras se escribía; volvé a correr la instalación.`);
290
+ }
291
+ function setClaudeProjectMcpApproval(name, spec, approved) {
292
+ if (spec.id !== "claude-code")
293
+ return;
294
+ const dir = getMcpScopeDir();
295
+ if (!dir)
296
+ return;
297
+ const settingsPath = join(dir, ".claude", "settings.json");
298
+ try {
299
+ updateJsonFile(settingsPath, (cfg) => {
300
+ const current = Array.isArray(cfg.enabledMcpjsonServers)
301
+ ? cfg.enabledMcpjsonServers.filter((v) => typeof v === "string")
302
+ : [];
303
+ if (current.includes(name) === approved)
304
+ return false;
305
+ cfg.enabledMcpjsonServers = approved
306
+ ? [...current, name].sort()
307
+ : current.filter((n) => n !== name);
308
+ return true;
309
+ });
310
+ }
311
+ catch {
312
+ }
313
+ }
258
314
  function registerMcpInTarget(mcp, spec) {
259
315
  validateMcpName(mcp.name);
316
+ if (spec.mcpMechanism === "hermes-cli") {
317
+ registerMcpInHermes(mcp, spec);
318
+ return;
319
+ }
260
320
  if (spec.mcpMechanism === "toml-block") {
261
321
  registerMcpInTomlBlock(mcp, spec);
262
322
  return;
263
323
  }
264
- const cfg = readJsonOrEmpty(spec.mcpConfigPath);
265
- const section = cfg[spec.mcpRootKey] ?? {};
266
- section[mcp.name] = renderMcpForTarget(mcp, spec);
267
- cfg[spec.mcpRootKey] = section;
268
- backupIfExists(spec.mcpConfigPath);
269
- atomicWriteFile(spec.mcpConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
324
+ updateJsonFile(spec.mcpConfigPath, (cfg) => {
325
+ const section = cfg[spec.mcpRootKey] ?? {};
326
+ section[mcp.name] = renderMcpForTarget(mcp, spec);
327
+ cfg[spec.mcpRootKey] = section;
328
+ return true;
329
+ });
330
+ setClaudeProjectMcpApproval(mcp.name, spec, true);
270
331
  }
271
332
  function unregisterMcpInTarget(mcpName, spec) {
333
+ if (spec.mcpMechanism === "hermes-cli") {
334
+ return unregisterMcpInHermes(mcpName, spec);
335
+ }
272
336
  if (spec.mcpMechanism === "toml-block") {
273
337
  return unregisterMcpInTomlBlock(mcpName, spec);
274
338
  }
275
339
  if (!existsSync(spec.mcpConfigPath))
276
340
  return false;
277
- const cfg = readJsonOrEmpty(spec.mcpConfigPath);
278
- const section = cfg[spec.mcpRootKey];
279
- if (!section || !(mcpName in section))
341
+ const removed = updateJsonFile(spec.mcpConfigPath, (cfg) => {
342
+ const section = cfg[spec.mcpRootKey];
343
+ if (!section || !(mcpName in section))
344
+ return false;
345
+ delete section[mcpName];
346
+ cfg[spec.mcpRootKey] = section;
347
+ return true;
348
+ });
349
+ if (!removed)
280
350
  return false;
281
- delete section[mcpName];
282
- cfg[spec.mcpRootKey] = section;
283
- backupIfExists(spec.mcpConfigPath);
284
- atomicWriteFile(spec.mcpConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
351
+ setClaudeProjectMcpApproval(mcpName, spec, false);
352
+ return true;
353
+ }
354
+ const HERMES_CLI_TIMEOUT_MS = 20_000;
355
+ export function runHermesCliAt(hermesHome, args) {
356
+ const res = spawnSync(getHermesBin(), args, {
357
+ encoding: "utf-8",
358
+ timeout: HERMES_CLI_TIMEOUT_MS,
359
+ env: { ...process.env, HERMES_HOME: hermesHome },
360
+ });
361
+ const missingBinary = res.error?.code === "ENOENT";
362
+ return {
363
+ ok: res.status === 0,
364
+ stdout: res.stdout ?? "",
365
+ stderr: res.stderr ?? res.error?.message ?? "",
366
+ missingBinary,
367
+ };
368
+ }
369
+ function runHermesCli(spec, args) {
370
+ return runHermesCliAt(dirname(spec.mcpConfigPath), args);
371
+ }
372
+ function hermesCliError(action, res) {
373
+ if (res.missingBinary) {
374
+ 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.`);
375
+ }
376
+ const detail = (res.stderr || res.stdout).trim().split("\n").slice(-1)[0] ?? "";
377
+ return new Error(`'hermes ${action}' falló: ${detail.slice(0, 300)}`);
378
+ }
379
+ function registerMcpInHermes(mcp, spec) {
380
+ for (const op of renderHermesMcpOps(mcp)) {
381
+ const key = `mcp_servers.${mcp.name}.${op.path}`;
382
+ const res = runHermesCli(spec, ["config", "set", key, op.value]);
383
+ if (!res.ok)
384
+ throw hermesCliError(`config set ${key}`, res);
385
+ }
386
+ }
387
+ function unregisterMcpInHermes(mcpName, spec) {
388
+ if (readHermesMcpFields(spec, mcpName) === null)
389
+ return false;
390
+ const key = `mcp_servers.${mcpName}`;
391
+ const res = runHermesCli(spec, ["config", "unset", key]);
392
+ if (!res.ok)
393
+ throw hermesCliError(`config unset ${key}`, res);
285
394
  return true;
286
395
  }
396
+ export function readHermesMcpFields(spec, name) {
397
+ const res = runHermesCli(spec, ["config", "get", `mcp_servers.${name}`]);
398
+ if (!res.ok)
399
+ return null;
400
+ return parseHermesConfigGet(res.stdout);
401
+ }
402
+ export function parseHermesConfigGet(text) {
403
+ const out = {};
404
+ let groupKey = null;
405
+ let groupIndent = 0;
406
+ let group = null;
407
+ const closeGroup = () => {
408
+ if (groupKey !== null && group !== null)
409
+ out[groupKey] = group;
410
+ groupKey = null;
411
+ group = null;
412
+ };
413
+ for (const raw of text.split(/\r?\n/)) {
414
+ if (raw.trim() === "")
415
+ continue;
416
+ const line = raw.trim();
417
+ const indent = raw.length - raw.trimStart().length;
418
+ if (groupKey !== null) {
419
+ if (line.startsWith("- ") && indent >= groupIndent) {
420
+ const list = Array.isArray(group) ? [...group] : [];
421
+ list.push(unquoteYamlScalar(line.slice(2).trim()));
422
+ group = list;
423
+ continue;
424
+ }
425
+ if (indent > groupIndent) {
426
+ const kv = line.match(/^([A-Za-z0-9_.-]+):\s*(.+)$/);
427
+ if (kv) {
428
+ const map = group !== null && !Array.isArray(group) ? { ...group } : {};
429
+ map[kv[1]] = unquoteYamlScalar(kv[2].trim());
430
+ group = map;
431
+ continue;
432
+ }
433
+ }
434
+ closeGroup();
435
+ }
436
+ const kv = line.match(/^([A-Za-z0-9_.-]+):\s*(.*)$/);
437
+ if (!kv)
438
+ continue;
439
+ if (kv[2] === "") {
440
+ groupKey = kv[1];
441
+ groupIndent = indent;
442
+ group = null;
443
+ continue;
444
+ }
445
+ out[kv[1]] = unquoteYamlScalar(kv[2].trim());
446
+ }
447
+ closeGroup();
448
+ return Object.keys(out).length > 0 ? out : null;
449
+ }
450
+ function unquoteYamlScalar(value) {
451
+ if (value.length >= 2) {
452
+ const first = value[0];
453
+ const last = value[value.length - 1];
454
+ if ((first === "'" && last === "'") || (first === '"' && last === '"')) {
455
+ return value.slice(1, -1);
456
+ }
457
+ }
458
+ return value;
459
+ }
460
+ function normalizeHermesValue(value) {
461
+ if (value === undefined)
462
+ return null;
463
+ if (typeof value === "string")
464
+ return value;
465
+ if (Array.isArray(value))
466
+ return value;
467
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
468
+ }
469
+ function hermesExpectedFields(mcp) {
470
+ const expected = {};
471
+ for (const op of renderHermesMcpOps(mcp))
472
+ expected[op.path] = parseHermesSerialized(op.value);
473
+ return expected;
474
+ }
475
+ function parseHermesSerialized(value) {
476
+ const trimmed = value.trim();
477
+ if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
478
+ try {
479
+ return JSON.parse(trimmed);
480
+ }
481
+ catch {
482
+ return value;
483
+ }
484
+ }
485
+ return value;
486
+ }
487
+ export function hermesExpectedSnapshot(mcp) {
488
+ return stableStringify(hermesExpectedFields(mcp));
489
+ }
490
+ export function hermesActualSnapshot(mcp, fields) {
491
+ const actual = {};
492
+ for (const op of renderHermesMcpOps(mcp))
493
+ actual[op.path] = normalizeHermesValue(fields[op.path]);
494
+ return stableStringify(actual);
495
+ }
287
496
  function tomlBasicString(s) {
288
497
  return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
289
498
  }
290
499
  export function renderMcpToml(mcp) {
291
500
  if (mcp.transport === "stdio") {
292
- const { command, args } = adaptStdioCommand(mcp.command ?? "", mcp.args ?? []);
501
+ const { command, args } = stdioLaunch(mcp);
293
502
  const lines = [`[mcp_servers.${mcp.name}]`, `command = ${tomlBasicString(command)}`];
294
503
  if (args.length > 0) {
295
504
  lines.push(`args = [${args.map((a) => tomlBasicString(a)).join(", ")}]`);
@@ -382,7 +591,9 @@ function unregisterMcpInTomlBlock(name, spec) {
382
591
  export function renderedEntryHash(mcp, spec) {
383
592
  const rendered = spec.mcpMechanism === "toml-block"
384
593
  ? renderMcpToml(mcp).trim()
385
- : stableStringify(renderMcpForTarget(mcp, spec));
594
+ : spec.mcpMechanism === "hermes-cli"
595
+ ? hermesExpectedSnapshot(mcp)
596
+ : stableStringify(renderMcpForTarget(mcp, spec));
386
597
  return createHash("sha256").update(rendered).digest("hex");
387
598
  }
388
599
  function hashOf(value) {
@@ -400,6 +611,17 @@ function isDiscardedByClient(current, spec) {
400
611
  }
401
612
  export function inspectMcpInTarget(mcp, spec, prevHash) {
402
613
  try {
614
+ if (spec.mcpMechanism === "hermes-cli") {
615
+ const fields = readHermesMcpFields(spec, mcp.name);
616
+ if (fields === null)
617
+ return "missing";
618
+ const current = hermesActualSnapshot(mcp, fields);
619
+ if (current === hermesExpectedSnapshot(mcp))
620
+ return "ok";
621
+ if (prevHash && hashOf(current) === prevHash)
622
+ return "stale";
623
+ return "modified";
624
+ }
403
625
  if (!existsSync(spec.mcpConfigPath))
404
626
  return "missing";
405
627
  if (spec.mcpMechanism === "toml-block") {
@@ -456,31 +678,27 @@ const LEGACY_RENAMED_SKILLS = ["alanube", "alegra-producto", "alegra-ce-universe
456
678
  function stripMcpEntries(path, names) {
457
679
  if (!existsSync(path) || names.length === 0)
458
680
  return;
459
- let cfg;
460
681
  try {
461
- cfg = JSON.parse(readFileSync(path, "utf-8"));
682
+ updateJsonFile(path, (cfg) => {
683
+ let changed = false;
684
+ for (const key of ["mcpServers", "mcp"]) {
685
+ const section = cfg[key];
686
+ if (!section || typeof section !== "object")
687
+ continue;
688
+ for (const n of names) {
689
+ if (n in section) {
690
+ delete section[n];
691
+ changed = true;
692
+ }
693
+ }
694
+ if (Object.keys(section).length === 0)
695
+ delete cfg[key];
696
+ }
697
+ return changed;
698
+ });
462
699
  }
463
700
  catch {
464
- return;
465
- }
466
- let changed = false;
467
- for (const key of ["mcpServers", "mcp"]) {
468
- const section = cfg[key];
469
- if (!section || typeof section !== "object")
470
- continue;
471
- for (const n of names) {
472
- if (n in section) {
473
- delete section[n];
474
- changed = true;
475
- }
476
- }
477
- if (Object.keys(section).length === 0)
478
- delete cfg[key];
479
701
  }
480
- if (!changed)
481
- return;
482
- backupIfExists(path);
483
- atomicWriteFile(path, `${JSON.stringify(cfg, null, 2)}\n`);
484
702
  }
485
703
  function cleanupLegacySkillArtifacts(spec) {
486
704
  try {
@@ -594,12 +812,16 @@ export async function installSkill(name, manifest, opts = {}) {
594
812
  error: failures.join("; "),
595
813
  };
596
814
  }
815
+ const outOfScopeTargets = prev && prev.bundleHash === entry.bundleHash
816
+ ? (prev.targets ?? []).filter((t) => !targetIds.includes(t))
817
+ : [];
818
+ const lockTargets = [...new Set([...outOfScopeTargets, ...okTargets])];
597
819
  lock.skills[name] = {
598
820
  name,
599
821
  installedFrom: getPackageVersion(),
600
822
  installedAt: new Date().toISOString(),
601
823
  bundleHash: entry.bundleHash,
602
- targets: okTargets,
824
+ targets: lockTargets,
603
825
  };
604
826
  writeLockFile(lock);
605
827
  return {
@@ -672,6 +894,19 @@ export function installMcp(mcp, opts = {}) {
672
894
  error: "ningún target soportado disponible",
673
895
  };
674
896
  }
897
+ if (mcp.lazy && !opts.dryRun) {
898
+ try {
899
+ ensureLazyProxy(mcp);
900
+ }
901
+ catch (err) {
902
+ return {
903
+ name: mcp.name,
904
+ kind: "mcp",
905
+ status: "failed",
906
+ error: `no se pudo escribir el proxy de arranque diferido: ${err instanceof Error ? err.message : String(err)}`,
907
+ };
908
+ }
909
+ }
675
910
  const prev = lock.mcps[mcp.name];
676
911
  const targetsMatch = prev?.targets &&
677
912
  prev.targets.length === targetIds.length &&
@@ -723,12 +958,16 @@ export function installMcp(mcp, opts = {}) {
723
958
  if (okTargets.includes(t.id))
724
959
  renderedHash[t.id] = renderedEntryHash(mcp, t);
725
960
  }
961
+ const outOfScopeTargets = configChanged
962
+ ? []
963
+ : (prev?.targets ?? []).filter((t) => !targetIds.includes(t) && (!mcp.targets || mcp.targets.includes(t)));
964
+ const lockTargets = [...new Set([...outOfScopeTargets, ...okTargets])];
726
965
  lock.mcps[mcp.name] = {
727
966
  name: mcp.name,
728
967
  installedFrom: getPackageVersion(),
729
968
  installedAt: new Date().toISOString(),
730
969
  configHash: mcpConfigHash(mcp),
731
- targets: okTargets,
970
+ targets: lockTargets,
732
971
  renderedHash,
733
972
  };
734
973
  writeLockFile(lock);
@@ -765,6 +1004,15 @@ export async function installAll(opts = {}) {
765
1004
  const mcp = manifest.mcps?.[name];
766
1005
  if (!mcp)
767
1006
  continue;
1007
+ if (mcp.name !== name) {
1008
+ results.push({
1009
+ name,
1010
+ kind: "mcp",
1011
+ status: "failed",
1012
+ error: `la entry "${name}" declara name "${mcp.name}"`,
1013
+ });
1014
+ continue;
1015
+ }
768
1016
  results.push(installMcp(mcp, { ...opts, targets }));
769
1017
  }
770
1018
  return { manifest, results, targets };
@@ -861,6 +1109,7 @@ export function uninstallAll(opts = {}) {
861
1109
  catch {
862
1110
  }
863
1111
  }
1112
+ rmSync(lazyMcpDir(), { recursive: true, force: true });
864
1113
  }
865
1114
  return results;
866
1115
  }
@@ -0,0 +1,153 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { COMMAND_NAME, adaptStdioCommand } from "./lib.js";
5
+ export function lazyMcpDir() {
6
+ return join(homedir(), ".alegra", "mcp");
7
+ }
8
+ export function lazyProxyPath() {
9
+ return join(lazyMcpDir(), "lazy-mcp-proxy.mjs");
10
+ }
11
+ export function lazyToolsPath(name) {
12
+ return join(lazyMcpDir(), `${name}.tools.json`);
13
+ }
14
+ export function stdioLaunch(mcp, platform = process.platform) {
15
+ const inner = adaptStdioCommand(mcp.command ?? "", mcp.args ?? [], platform);
16
+ if (!mcp.lazy)
17
+ return inner;
18
+ return {
19
+ command: "node",
20
+ args: [lazyProxyPath(), lazyToolsPath(mcp.name), "--", inner.command, ...inner.args],
21
+ };
22
+ }
23
+ export function ensureLazyProxy(mcp) {
24
+ if (!mcp.lazy)
25
+ return;
26
+ mkdirSync(lazyMcpDir(), { recursive: true });
27
+ writeIfChanged(lazyProxyPath(), lazyProxySource());
28
+ writeIfChanged(lazyToolsPath(mcp.name), `${JSON.stringify({ name: mcp.name, tools: mcp.lazy.tools }, null, 2)}\n`);
29
+ }
30
+ function writeIfChanged(path, content) {
31
+ if (existsSync(path) && readFileSync(path, "utf-8") === content)
32
+ return;
33
+ writeFileSync(path, content, { mode: 0o644 });
34
+ }
35
+ export function lazyProxySource() {
36
+ const product = JSON.stringify(COMMAND_NAME);
37
+ return `#!/usr/bin/env node
38
+ // Proxy de arranque diferido de ${COMMAND_NAME}. Generado por el instalador: no
39
+ // editar, se reescribe en cada install/update. Uso:
40
+ // node lazy-mcp-proxy.mjs <tools.json> -- <comando real> [args…]
41
+ import { spawn } from "node:child_process";
42
+ import { readFileSync } from "node:fs";
43
+ import { createInterface } from "node:readline";
44
+
45
+ const argv = process.argv.slice(2);
46
+ const sep = argv.indexOf("--");
47
+ if (sep !== 1 || argv.length < 3) {
48
+ process.stderr.write("uso: lazy-mcp-proxy.mjs <tools.json> -- <comando> [args…]\\n");
49
+ process.exit(2);
50
+ }
51
+ const spec = JSON.parse(readFileSync(argv[0], "utf8"));
52
+ const [cmd, ...args] = argv.slice(2);
53
+ const INIT_ID = "__alegra_lazy_init__";
54
+
55
+ const send = (m) => process.stdout.write(JSON.stringify(m) + "\\n");
56
+ let initParams = null;
57
+ let child = null;
58
+ let ready = false;
59
+ let dead = null;
60
+ const queue = [];
61
+
62
+ function errorFor(m, reason) {
63
+ if (m.method === "tools/call") {
64
+ return { jsonrpc: "2.0", id: m.id, result: { content: [{ type: "text", text: reason }], isError: true } };
65
+ }
66
+ return { jsonrpc: "2.0", id: m.id, error: { code: -32603, message: reason } };
67
+ }
68
+
69
+ function die(reason) {
70
+ dead = reason;
71
+ ready = false;
72
+ for (const m of queue.splice(0)) send(errorFor(m, reason));
73
+ }
74
+
75
+ function start() {
76
+ if (child || dead) return;
77
+ child = spawn(cmd, args, { stdio: ["pipe", "pipe", "inherit"] });
78
+ child.on("error", (e) => die("No se pudo arrancar el MCP " + spec.name + ": " + e.message));
79
+ child.on("exit", (code, signal) => {
80
+ if (!dead) die("El MCP " + spec.name + " terminó (" + (signal ?? "código " + code) + "). Reintentá en una sesión nueva; si persiste, corré " + ${product} + " status.");
81
+ });
82
+ createInterface({ input: child.stdout }).on("line", (line) => {
83
+ if (ready) {
84
+ process.stdout.write(line + "\\n");
85
+ return;
86
+ }
87
+ let m;
88
+ try { m = JSON.parse(line); } catch { return; }
89
+ if (m.id !== INIT_ID) return; // notificaciones previas al handshake
90
+ if (m.error) {
91
+ die("El MCP " + spec.name + " rechazó el handshake: " + (m.error.message ?? "sin detalle"));
92
+ child.kill();
93
+ return;
94
+ }
95
+ ready = true;
96
+ child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\\n");
97
+ for (const q of queue.splice(0)) child.stdin.write(JSON.stringify(q) + "\\n");
98
+ });
99
+ child.stdin.on("error", () => {});
100
+ child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: INIT_ID, method: "initialize", params: initParams }) + "\\n");
101
+ }
102
+
103
+ createInterface({ input: process.stdin }).on("line", (line) => {
104
+ if (ready) {
105
+ child.stdin.write(line + "\\n");
106
+ return;
107
+ }
108
+ let m;
109
+ try { m = JSON.parse(line); } catch { return; }
110
+ switch (m.method) {
111
+ case "initialize":
112
+ initParams = m.params ?? {};
113
+ send({
114
+ jsonrpc: "2.0",
115
+ id: m.id,
116
+ result: {
117
+ protocolVersion: initParams.protocolVersion ?? "2025-06-18",
118
+ capabilities: { tools: {} },
119
+ serverInfo: { name: spec.name, version: "lazy" },
120
+ },
121
+ });
122
+ return;
123
+ case "ping":
124
+ send({ jsonrpc: "2.0", id: m.id, result: {} });
125
+ return;
126
+ case "tools/list":
127
+ send({ jsonrpc: "2.0", id: m.id, result: { tools: spec.tools } });
128
+ return;
129
+ case "resources/list":
130
+ send({ jsonrpc: "2.0", id: m.id, result: { resources: [] } });
131
+ return;
132
+ case "resources/templates/list":
133
+ send({ jsonrpc: "2.0", id: m.id, result: { resourceTemplates: [] } });
134
+ return;
135
+ case "prompts/list":
136
+ send({ jsonrpc: "2.0", id: m.id, result: { prompts: [] } });
137
+ return;
138
+ }
139
+ if (m.id === undefined || m.id === null) return; // notificación: nada que contestar
140
+ if (dead) {
141
+ send(errorFor(m, dead));
142
+ return;
143
+ }
144
+ queue.push(m);
145
+ start();
146
+ });
147
+
148
+ process.stdin.on("end", () => {
149
+ if (child) child.kill();
150
+ process.exit(0);
151
+ });
152
+ `;
153
+ }
package/dist/lib.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { dirname, join, relative, sep } from "node:path";
4
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  export const PRODUCT_NAME = "alegra-data-agent";
7
7
  export const COMMAND_NAME = "alegra-data-agent";
@@ -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() {
@@ -28,6 +29,78 @@ export function getClaudeSettingsPath() {
28
29
  export function getClaudeJsonPath() {
29
30
  return join(homedir(), ".claude.json");
30
31
  }
32
+ export function getAgentConfPath() {
33
+ return join(homedir(), ".alegra", "agent.conf");
34
+ }
35
+ function readAgentConf() {
36
+ const path = getAgentConfPath();
37
+ if (!existsSync(path))
38
+ return {};
39
+ let raw;
40
+ try {
41
+ raw = readFileSync(path, "utf-8");
42
+ }
43
+ catch {
44
+ return {};
45
+ }
46
+ const out = {};
47
+ for (const line0 of raw.split("\n")) {
48
+ const line = line0.trim();
49
+ if (line.length === 0 || line.startsWith("#"))
50
+ continue;
51
+ const eq = line.indexOf("=");
52
+ if (eq <= 0)
53
+ continue;
54
+ const key = line.slice(0, eq).trim();
55
+ const value = line
56
+ .slice(eq + 1)
57
+ .trim()
58
+ .replace(/^(["'])(.*)\1$/, "$2");
59
+ if (key.length > 0)
60
+ out[key] = value;
61
+ }
62
+ return out;
63
+ }
64
+ function resolveMcpScope() {
65
+ const raw = process.env.ALEGRA_DATA_AGENT_MCP_DIR ?? readAgentConf().MCP_DIR;
66
+ if (!raw || raw.trim().length === 0)
67
+ return { dir: undefined, warning: undefined };
68
+ const expanded = raw.trim().replace(/^~(?=$|\/)/, homedir());
69
+ if (!isAbsolute(expanded)) {
70
+ return {
71
+ dir: undefined,
72
+ warning: `MCP_DIR debe ser una ruta absoluta; se ignoró "${raw.trim()}"`,
73
+ };
74
+ }
75
+ const dir = resolve(expanded);
76
+ let isDir;
77
+ try {
78
+ isDir = existsSync(dir) && statSync(dir).isDirectory();
79
+ }
80
+ catch {
81
+ isDir = false;
82
+ }
83
+ if (!isDir) {
84
+ return {
85
+ dir: undefined,
86
+ warning: `MCP_DIR apunta a una carpeta que no existe; se ignoró "${dir}"`,
87
+ };
88
+ }
89
+ return { dir, warning: undefined };
90
+ }
91
+ export function getMcpScopeWarning() {
92
+ return resolveMcpScope().warning;
93
+ }
94
+ export function getMcpScopeDir() {
95
+ return resolveMcpScope().dir;
96
+ }
97
+ function mcpScopedPath(relPath, fallback) {
98
+ const dir = getMcpScopeDir();
99
+ return dir ? join(dir, relPath) : fallback;
100
+ }
101
+ export function getClaudeMcpConfigPath() {
102
+ return mcpScopedPath(".mcp.json", getClaudeJsonPath());
103
+ }
31
104
  export function getCursorSkillsDir() {
32
105
  return join(homedir(), ".cursor", "skills");
33
106
  }
@@ -35,7 +108,7 @@ export function getCursorLegacyRulesDir() {
35
108
  return join(homedir(), ".cursor", "rules");
36
109
  }
37
110
  export function getCursorMcpPath() {
38
- return join(homedir(), ".cursor", "mcp.json");
111
+ return mcpScopedPath(join(".cursor", "mcp.json"), join(homedir(), ".cursor", "mcp.json"));
39
112
  }
40
113
  export function getOpenCodeSkillsDir() {
41
114
  const xdgHome = process.env.XDG_CONFIG_HOME;
@@ -100,6 +173,23 @@ export function getCodexConfigPath() {
100
173
  const base = codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
101
174
  return join(base, "config.toml");
102
175
  }
176
+ export function getHermesHome() {
177
+ const hermesHome = process.env.HERMES_HOME;
178
+ return hermesHome && hermesHome.length > 0 ? hermesHome : join(homedir(), ".hermes");
179
+ }
180
+ export function getHermesSkillsDir() {
181
+ return join(getHermesHome(), "skills");
182
+ }
183
+ export function getHermesConfigPath() {
184
+ return join(getHermesHome(), "config.yaml");
185
+ }
186
+ export function getHermesPluginsDir() {
187
+ return join(getHermesHome(), "plugins");
188
+ }
189
+ export function getHermesBin() {
190
+ const bin = process.env.HERMES_BIN;
191
+ return bin && bin.length > 0 ? bin : "hermes";
192
+ }
103
193
  export function getTargetSkillsDir(target) {
104
194
  switch (target) {
105
195
  case "claude-code":
@@ -112,12 +202,14 @@ export function getTargetSkillsDir(target) {
112
202
  return getAntigravitySkillsDir();
113
203
  case "codex":
114
204
  return getCodexSkillsDir();
205
+ case "hermes":
206
+ return getHermesSkillsDir();
115
207
  }
116
208
  }
117
209
  export function getTargetMcpConfigPath(target) {
118
210
  switch (target) {
119
211
  case "claude-code":
120
- return getClaudeJsonPath();
212
+ return getClaudeMcpConfigPath();
121
213
  case "cursor":
122
214
  return getCursorMcpPath();
123
215
  case "opencode":
@@ -126,6 +218,8 @@ export function getTargetMcpConfigPath(target) {
126
218
  return getAntigravityMcpConfigPath();
127
219
  case "codex":
128
220
  return getCodexConfigPath();
221
+ case "hermes":
222
+ return getHermesConfigPath();
129
223
  }
130
224
  }
131
225
  export function getLockFilePath() {
package/dist/render.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { extname, join } from "node:path";
3
- import { adaptStdioCommand, parseFrontmatter } from "./lib.js";
3
+ import { stdioLaunch } from "./lazy-mcp.js";
4
+ import { parseFrontmatter } from "./lib.js";
4
5
  function readSkillEntryPoint(skillDir) {
5
6
  const candidates = ["SKILL.md", "README.md"];
6
7
  for (const c of candidates) {
@@ -95,7 +96,7 @@ export function renderMcpForTarget(mcp, spec) {
95
96
  return obj;
96
97
  }
97
98
  function renderStdioMcpForTarget(mcp, spec) {
98
- const { command, args } = adaptStdioCommand(mcp.command ?? "", mcp.args ?? []);
99
+ const { command, args } = stdioLaunch(mcp);
99
100
  const hasEnv = mcp.env && Object.keys(mcp.env).length > 0;
100
101
  if (spec.id === "opencode") {
101
102
  const obj = { type: "local", command: [command, ...args], enabled: true };
@@ -119,5 +120,38 @@ function bearerInterpolation(env, target) {
119
120
  return `Bearer {env:${env}}`;
120
121
  case "codex":
121
122
  throw new Error("codex usa config TOML (bearer_token_env_var), no headers JSON");
123
+ case "hermes":
124
+ return `Bearer \${${env}}`;
125
+ }
126
+ }
127
+ export function renderHermesMcpOps(mcp) {
128
+ if (mcp.transport === "stdio") {
129
+ const { command, args } = stdioLaunch(mcp);
130
+ const ops = [{ path: "command", value: command }];
131
+ if (args.length > 0)
132
+ ops.push({ path: "args", value: JSON.stringify(args) });
133
+ if (mcp.env && Object.keys(mcp.env).length > 0) {
134
+ ops.push({ path: "env", value: JSON.stringify(mcp.env) });
135
+ }
136
+ return ops;
137
+ }
138
+ const ops = [{ path: "url", value: mcp.url ?? "" }];
139
+ if (mcp.auth.type === "oauth") {
140
+ ops.push({ path: "auth", value: "oauth" });
122
141
  }
142
+ else if (mcp.auth.type === "gateway") {
143
+ if (!mcp.auth.tokenEnv) {
144
+ throw new Error(`MCP '${mcp.name}' es type=gateway pero no trae tokenEnv`);
145
+ }
146
+ ops.push({
147
+ path: "headers",
148
+ value: JSON.stringify({ Authorization: bearerInterpolation(mcp.auth.tokenEnv, "hermes") }),
149
+ });
150
+ }
151
+ return ops;
152
+ }
153
+ export function hermesManagedFields(mcp) {
154
+ return renderHermesMcpOps(mcp)
155
+ .map((op) => op.path)
156
+ .sort();
123
157
  }
package/dist/signature.js CHANGED
@@ -12,16 +12,19 @@ export const TRUSTED_KEYS = [
12
12
  ];
13
13
  export const PUBLIC_KEY_B64 = TRUSTED_KEYS[0].publicKeyB64;
14
14
  export function canonicalize(obj) {
15
- if (obj === null || typeof obj !== "object")
16
- return JSON.stringify(obj);
17
- if (Array.isArray(obj)) {
18
- return `[${obj.map((v) => canonicalize(v)).join(",")}]`;
15
+ return canonicalValue(obj, true);
16
+ }
17
+ function canonicalValue(value, isRoot) {
18
+ if (value === null || typeof value !== "object")
19
+ return JSON.stringify(value);
20
+ if (Array.isArray(value)) {
21
+ return `[${value.map((v) => canonicalValue(v, false)).join(",")}]`;
19
22
  }
20
- const o = obj;
23
+ const o = value;
21
24
  const keys = Object.keys(o)
22
- .filter((k) => k !== "signature")
25
+ .filter((k) => !(isRoot && k === "signature"))
23
26
  .sort();
24
- return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(o[k])}`).join(",")}}`;
27
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalValue(o[k], false)}`).join(",")}}`;
25
28
  }
26
29
  const MAX_MANIFEST_AGE_DAYS = 90;
27
30
  export function verifyManifestSignature(manifest, options = {}) {
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, getClaudeMcpConfigPath, 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",
@@ -9,7 +9,7 @@ export const TARGETS = {
9
9
  mcpHttpTypeValue: "http",
10
10
  skillsDir: getClaudeSkillsDir(),
11
11
  skillFormat: "folder",
12
- mcpConfigPath: getClaudeJsonPath(),
12
+ mcpConfigPath: getClaudeMcpConfigPath(),
13
13
  mcpRootKey: "mcpServers",
14
14
  mcpUrlKey: "url",
15
15
  },
@@ -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.2",
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",