alegra-data-agent 0.3.1 → 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.
- package/dist/autoupdate.js +57 -2
- package/dist/commands/install.js +1 -1
- package/dist/hermes-plugin.js +83 -0
- package/dist/installer.js +176 -5
- package/dist/lib.js +22 -0
- package/dist/render.js +33 -0
- package/dist/targets.js +12 -1
- package/dist/ui.js +2 -2
- package/package.json +1 -1
package/dist/autoupdate.js
CHANGED
|
@@ -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 {
|
|
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() {
|
package/dist/commands/install.js
CHANGED
|
@@ -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
|
|
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
|
-
:
|
|
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:
|
|
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:
|
|
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
|