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 +1 -1
- package/dist/autoupdate.js +57 -2
- package/dist/commands/install.js +1 -1
- package/dist/commands/status.js +24 -5
- package/dist/hermes-plugin.js +83 -0
- package/dist/installer.js +295 -46
- package/dist/lazy-mcp.js +153 -0
- package/dist/lib.js +97 -3
- package/dist/render.js +36 -2
- package/dist/signature.js +10 -7
- package/dist/targets.js +13 -2
- package/dist/ui.js +2 -2
- package/package.json +1 -1
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
|
}
|
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("");
|
package/dist/commands/status.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
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 {
|
|
7
|
-
import {
|
|
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
|
-
|
|
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
|
|
249
|
-
if (
|
|
265
|
+
function parseJsonConfig(path, raw) {
|
|
266
|
+
if (raw === null)
|
|
250
267
|
return {};
|
|
251
268
|
try {
|
|
252
|
-
return JSON.parse(
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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 } =
|
|
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
|
-
:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
}
|
package/dist/lazy-mcp.js
ADDED
|
@@ -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
|
|
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 {
|
|
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 } =
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 =
|
|
23
|
+
const o = value;
|
|
21
24
|
const keys = Object.keys(o)
|
|
22
|
-
.filter((k) => k
|
|
25
|
+
.filter((k) => !(isRoot && k === "signature"))
|
|
23
26
|
.sort();
|
|
24
|
-
return `{${keys.map((k) => `${JSON.stringify(k)}:${
|
|
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,
|
|
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:
|
|
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
|