alegra-data-agent 0.4.1 → 0.4.3
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/commands/status.js +34 -6
- package/dist/commands/update.js +3 -1
- package/dist/installer.js +329 -49
- package/dist/lazy-mcp.js +198 -0
- package/dist/lib.js +90 -2
- package/dist/render.js +4 -3
- package/dist/signature.js +10 -7
- package/dist/targets.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/commands/status.js
CHANGED
|
@@ -4,8 +4,8 @@ import { join } from "node:path";
|
|
|
4
4
|
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
|
-
import { detectDrift, inspectMcpInTarget } from "../installer.js";
|
|
8
|
-
import { COMMAND_NAME, GATEWAY_URL, getAutoUpdateLogPath, getClaudeSkillsDir, getLockFilePath, getPackageVersion, getTargetSkillsDir, readLockFile, } from "../lib.js";
|
|
7
|
+
import { detectDrift, findMisplacedMcps, inspectMcpInTarget } from "../installer.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 = [];
|
|
@@ -145,6 +145,14 @@ export function findRogueMetabase(manifest, specs = Object.values(TARGETS)) {
|
|
|
145
145
|
}
|
|
146
146
|
return found;
|
|
147
147
|
}
|
|
148
|
+
function reportMisplacedMcps(manifest) {
|
|
149
|
+
const found = findMisplacedMcps(manifest);
|
|
150
|
+
if (found.length === 0)
|
|
151
|
+
return;
|
|
152
|
+
logWarn(`Hay ${found.length} MCP(s) de Alegra fuera de su lugar: ${found.join(" · ")}`);
|
|
153
|
+
log(gray(` El próximo arranque los retira solo. Para hacerlo ya: ${COMMAND_NAME} update`));
|
|
154
|
+
log("");
|
|
155
|
+
}
|
|
148
156
|
function reportRogueMetabase(manifest) {
|
|
149
157
|
const official = manifest.mcps?.metabase?.url;
|
|
150
158
|
if (!official)
|
|
@@ -180,7 +188,7 @@ function connectorUrlsFromCredentials(ourServerNames, credentialsPath = join(hom
|
|
|
180
188
|
}
|
|
181
189
|
return urls;
|
|
182
190
|
}
|
|
183
|
-
export function findClaudeAiDuplicates(manifest, claudeJsonPath =
|
|
191
|
+
export function findClaudeAiDuplicates(manifest, claudeJsonPath = getClaudeJsonPath(), mcpConfigPath = claudeJsonPath) {
|
|
184
192
|
if (!existsSync(claudeJsonPath))
|
|
185
193
|
return [];
|
|
186
194
|
let cfg;
|
|
@@ -193,7 +201,18 @@ export function findClaudeAiDuplicates(manifest, claudeJsonPath = TARGETS["claud
|
|
|
193
201
|
const connectors = cfg.claudeAiMcpEverConnected;
|
|
194
202
|
if (!Array.isArray(connectors))
|
|
195
203
|
return [];
|
|
196
|
-
|
|
204
|
+
let mcpCfg = cfg;
|
|
205
|
+
if (mcpConfigPath !== claudeJsonPath) {
|
|
206
|
+
if (!existsSync(mcpConfigPath))
|
|
207
|
+
return [];
|
|
208
|
+
try {
|
|
209
|
+
mcpCfg = JSON.parse(readFileSync(mcpConfigPath, "utf-8"));
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const localServers = mcpCfg[TARGETS["claude-code"].mcpRootKey];
|
|
197
216
|
if (!localServers || typeof localServers !== "object")
|
|
198
217
|
return [];
|
|
199
218
|
const ourNames = new Set(Object.keys(localServers));
|
|
@@ -222,7 +241,7 @@ export function findClaudeAiDuplicates(manifest, claudeJsonPath = TARGETS["claud
|
|
|
222
241
|
return out;
|
|
223
242
|
}
|
|
224
243
|
function reportClaudeAiDuplicates(manifest) {
|
|
225
|
-
const dupes = findClaudeAiDuplicates(manifest);
|
|
244
|
+
const dupes = findClaudeAiDuplicates(manifest, getClaudeJsonPath(), TARGETS["claude-code"].mcpConfigPath);
|
|
226
245
|
if (dupes.length === 0)
|
|
227
246
|
return;
|
|
228
247
|
logWarn(`Hay ${dupes.length} MCP(s) que tenés por dos vías: server local y conector de claude.ai.`);
|
|
@@ -259,6 +278,14 @@ export async function runStatus() {
|
|
|
259
278
|
log(` ${gray("Autenticado:")} ${auth ? green(auth.email) : gray("no")}`);
|
|
260
279
|
log(` ${gray("Destino:")} ${destino}`);
|
|
261
280
|
log(` ${gray("Lock file:")} ${getLockFilePath()}`);
|
|
281
|
+
log(` ${gray("MCPs en:")} ${TARGETS["claude-code"].mcpConfigPath}`);
|
|
282
|
+
const scopeDir = getMcpScopeDir();
|
|
283
|
+
if (scopeDir) {
|
|
284
|
+
log(` ${gray("Scope MCPs:")} proyecto ${dim(`(solo dentro de ${scopeDir})`)}`);
|
|
285
|
+
}
|
|
286
|
+
const scopeWarning = getMcpScopeWarning();
|
|
287
|
+
if (scopeWarning)
|
|
288
|
+
logWarn(`${scopeWarning} — los MCPs quedaron en scope usuario.`);
|
|
262
289
|
log("");
|
|
263
290
|
reportAutoUpdateLog();
|
|
264
291
|
if (!auth) {
|
|
@@ -301,6 +328,7 @@ export async function runStatus() {
|
|
|
301
328
|
log("");
|
|
302
329
|
reportMcps(manifest, lock);
|
|
303
330
|
reportRogueMetabase(manifest);
|
|
331
|
+
reportMisplacedMcps(manifest);
|
|
304
332
|
reportClaudeAiDuplicates(manifest);
|
|
305
333
|
reportAccountConnectorOverlap(manifest);
|
|
306
334
|
const total = drift.toInstall.length +
|
package/dist/commands/update.js
CHANGED
|
@@ -97,7 +97,9 @@ async function runUpdateOnce(opts) {
|
|
|
97
97
|
for (const name of drift.mcps.toUpdate) {
|
|
98
98
|
const motivo = drift.mcps.missing.includes(name)
|
|
99
99
|
? "(MCP ausente o descartado por el cliente — se repone)"
|
|
100
|
-
:
|
|
100
|
+
: drift.mcps.outOfPlace.includes(name)
|
|
101
|
+
? "(copia fuera de lugar o proxy ausente — se corrige)"
|
|
102
|
+
: "(MCP actualizado)";
|
|
101
103
|
log(` ${yellow("↻")} ${name} ${gray(motivo)}`);
|
|
102
104
|
}
|
|
103
105
|
log("");
|
package/dist/installer.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
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";
|
|
4
4
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
5
5
|
import { AuthRequiredError, fetchManifest, fetchSkillFile } from "./api.js";
|
|
6
6
|
import { dim, gray, green, logError, red, yellow } from "./colors.js";
|
|
7
|
-
import {
|
|
7
|
+
import { ensureLazyProxy, lazyMcpDir, lazyProxyPath, lazyToolsPath, stdioLaunch, } from "./lazy-mcp.js";
|
|
8
|
+
import { ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getCursorLegacyRulesDir, getHermesBin, getLockFilePath, getMcpScopeDir, getMcpScopeWarning, getOpenCodeLegacyPromptsDir, getPackageVersion, getUserScopeMcpConfigPath, isMcpScopeEnvOverride, readLockFile, sha256File, writeLockFile, } from "./lib.js";
|
|
8
9
|
import { inlineReferences, renderHermesMcpOps, renderMcpForTarget, } from "./render.js";
|
|
9
10
|
import { TARGETS, detectInstalledTargets } from "./targets.js";
|
|
10
11
|
const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
@@ -43,6 +44,7 @@ function mcpConfigHash(mcp) {
|
|
|
43
44
|
type: mcp.auth.type,
|
|
44
45
|
tokenEnv: mcp.auth.tokenEnv ?? null,
|
|
45
46
|
...(mcp.delivery ? { delivery: mcp.delivery } : {}),
|
|
47
|
+
...(mcp.lazy ? { lazy: mcp.lazy } : {}),
|
|
46
48
|
});
|
|
47
49
|
return createHash("sha256").update(salient).digest("hex");
|
|
48
50
|
}
|
|
@@ -198,10 +200,24 @@ function withFsRetry(fn) {
|
|
|
198
200
|
}
|
|
199
201
|
}
|
|
200
202
|
}
|
|
201
|
-
function atomicWriteFile(path, content) {
|
|
203
|
+
export function atomicWriteFile(path, content) {
|
|
202
204
|
ensureDir(dirname(path));
|
|
203
205
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
204
|
-
|
|
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
|
+
}
|
|
205
221
|
for (let attempt = 0;; attempt++) {
|
|
206
222
|
try {
|
|
207
223
|
renameSync(tmp, path);
|
|
@@ -246,16 +262,182 @@ function pruneBackups(path) {
|
|
|
246
262
|
catch {
|
|
247
263
|
}
|
|
248
264
|
}
|
|
249
|
-
function
|
|
250
|
-
if (
|
|
265
|
+
function parseJsonConfig(path, raw) {
|
|
266
|
+
if (raw === null)
|
|
251
267
|
return {};
|
|
252
268
|
try {
|
|
253
|
-
return JSON.parse(
|
|
269
|
+
return JSON.parse(raw);
|
|
254
270
|
}
|
|
255
271
|
catch {
|
|
256
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.`);
|
|
257
273
|
}
|
|
258
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, dir = getMcpScopeDir()) {
|
|
292
|
+
if (spec.id !== "claude-code")
|
|
293
|
+
return;
|
|
294
|
+
if (!dir)
|
|
295
|
+
return;
|
|
296
|
+
const settingsPath = join(dir, ".claude", "settings.json");
|
|
297
|
+
try {
|
|
298
|
+
updateJsonFile(settingsPath, (cfg) => {
|
|
299
|
+
const current = Array.isArray(cfg.enabledMcpjsonServers)
|
|
300
|
+
? cfg.enabledMcpjsonServers.filter((v) => typeof v === "string")
|
|
301
|
+
: [];
|
|
302
|
+
if (current.includes(name) === approved)
|
|
303
|
+
return false;
|
|
304
|
+
cfg.enabledMcpjsonServers = approved
|
|
305
|
+
? [...current, name].sort()
|
|
306
|
+
: current.filter((n) => n !== name);
|
|
307
|
+
return true;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
export function isOwnMcpEntry(entry, mcp, spec, recordedHash) {
|
|
314
|
+
if (!entry || typeof entry !== "object")
|
|
315
|
+
return false;
|
|
316
|
+
const got = stableStringify(entry);
|
|
317
|
+
if (recordedHash && createHash("sha256").update(got).digest("hex") === recordedHash)
|
|
318
|
+
return true;
|
|
319
|
+
const shapes = [mcp, { ...mcp, lazy: undefined }];
|
|
320
|
+
return shapes.some((m) => stableStringify(renderMcpForTarget(m, spec)) === got);
|
|
321
|
+
}
|
|
322
|
+
function removeMcpEntryAt(path, rootKey, name, isOurs) {
|
|
323
|
+
if (!existsSync(path))
|
|
324
|
+
return false;
|
|
325
|
+
return updateJsonFile(path, (cfg) => {
|
|
326
|
+
const section = cfg[rootKey];
|
|
327
|
+
if (!section || !(name in section) || !isOurs(section[name]))
|
|
328
|
+
return false;
|
|
329
|
+
delete section[name];
|
|
330
|
+
return true;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
function canReconcileScope() {
|
|
334
|
+
return !getMcpScopeWarning() && !isMcpScopeEnvOverride();
|
|
335
|
+
}
|
|
336
|
+
function staleMcpConfigPaths(spec, prev) {
|
|
337
|
+
if (!canReconcileScope())
|
|
338
|
+
return [];
|
|
339
|
+
const out = new Set();
|
|
340
|
+
const recorded = prev?.configPaths?.[spec.id];
|
|
341
|
+
if (recorded)
|
|
342
|
+
out.add(recorded);
|
|
343
|
+
const user = getUserScopeMcpConfigPath(spec.id);
|
|
344
|
+
if (user && getMcpScopeDir())
|
|
345
|
+
out.add(user);
|
|
346
|
+
out.delete(spec.mcpConfigPath);
|
|
347
|
+
return [...out];
|
|
348
|
+
}
|
|
349
|
+
function retireStaleMcpCopies(mcp, specs, prev) {
|
|
350
|
+
const touched = [];
|
|
351
|
+
for (const spec of specs) {
|
|
352
|
+
if (spec.mcpMechanism === "hermes-cli" || spec.mcpMechanism === "toml-block")
|
|
353
|
+
continue;
|
|
354
|
+
for (const path of staleMcpConfigPaths(spec, prev)) {
|
|
355
|
+
try {
|
|
356
|
+
const isOurs = (e) => isOwnMcpEntry(e, mcp, spec, prev?.renderedHash?.[spec.id]);
|
|
357
|
+
if (removeMcpEntryAt(path, spec.mcpRootKey, mcp.name, isOurs)) {
|
|
358
|
+
touched.push(path);
|
|
359
|
+
if (spec.id === "claude-code" && basename(path) === ".mcp.json") {
|
|
360
|
+
setClaudeProjectMcpApproval(mcp.name, spec, false, dirname(path));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return touched;
|
|
369
|
+
}
|
|
370
|
+
function retireFromUnscopableTargets(mcp, specs, prev) {
|
|
371
|
+
const out = [];
|
|
372
|
+
for (const spec of specs) {
|
|
373
|
+
try {
|
|
374
|
+
if (spec.mcpMechanism === "hermes-cli" || spec.mcpMechanism === "toml-block") {
|
|
375
|
+
if (prev?.targets?.includes(spec.id) && unregisterMcpInTarget(mcp.name, spec))
|
|
376
|
+
out.push(spec.id);
|
|
377
|
+
}
|
|
378
|
+
else if (removeMcpEntryAt(spec.mcpConfigPath, spec.mcpRootKey, mcp.name, (e) => isOwnMcpEntry(e, mcp, spec, prev?.renderedHash?.[spec.id]))) {
|
|
379
|
+
out.push(spec.id);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return out;
|
|
386
|
+
}
|
|
387
|
+
export function mcpOutOfPlace(mcp, prev) {
|
|
388
|
+
const found = [];
|
|
389
|
+
if (mcp.delivery === "connector")
|
|
390
|
+
return found;
|
|
391
|
+
if (mcp.lazy && prev && (!existsSync(lazyProxyPath()) || !existsSync(lazyToolsPath(mcp.name)))) {
|
|
392
|
+
found.push("falta el proxy de arranque diferido");
|
|
393
|
+
}
|
|
394
|
+
const scoped = Boolean(getMcpScopeDir()) && canReconcileScope();
|
|
395
|
+
for (const spec of Object.values(TARGETS)) {
|
|
396
|
+
if (mcp.targets && !mcp.targets.includes(spec.id))
|
|
397
|
+
continue;
|
|
398
|
+
if (spec.mcpMechanism === "hermes-cli" || spec.mcpMechanism === "toml-block") {
|
|
399
|
+
if (scoped && prev?.targets?.includes(spec.id))
|
|
400
|
+
found.push(`copia en ${spec.id}`);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const candidates = getUserScopeMcpConfigPath(spec.id)
|
|
404
|
+
? staleMcpConfigPaths(spec, prev)
|
|
405
|
+
: scoped
|
|
406
|
+
? [spec.mcpConfigPath]
|
|
407
|
+
: [];
|
|
408
|
+
for (const path of candidates) {
|
|
409
|
+
try {
|
|
410
|
+
if (!existsSync(path))
|
|
411
|
+
continue;
|
|
412
|
+
const cfg = JSON.parse(readFileSync(path, "utf-8"));
|
|
413
|
+
const entry = cfg[spec.mcpRootKey]?.[mcp.name];
|
|
414
|
+
if (entry && isOwnMcpEntry(entry, mcp, spec, prev?.renderedHash?.[spec.id])) {
|
|
415
|
+
found.push(`copia en ${path}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return found;
|
|
423
|
+
}
|
|
424
|
+
export function findMisplacedMcps(manifest) {
|
|
425
|
+
const lock = readLockFile();
|
|
426
|
+
const found = [];
|
|
427
|
+
for (const mcp of Object.values(manifest.mcps ?? {})) {
|
|
428
|
+
for (const what of mcpOutOfPlace(mcp, lock.mcps?.[mcp.name]))
|
|
429
|
+
found.push(`${mcp.name}: ${what}`);
|
|
430
|
+
}
|
|
431
|
+
return found;
|
|
432
|
+
}
|
|
433
|
+
function scopedConfigPaths(targetIds) {
|
|
434
|
+
const out = {};
|
|
435
|
+
for (const id of targetIds) {
|
|
436
|
+
if (getUserScopeMcpConfigPath(id))
|
|
437
|
+
out[id] = TARGETS[id].mcpConfigPath;
|
|
438
|
+
}
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
259
441
|
function registerMcpInTarget(mcp, spec) {
|
|
260
442
|
validateMcpName(mcp.name);
|
|
261
443
|
if (spec.mcpMechanism === "hermes-cli") {
|
|
@@ -266,12 +448,13 @@ function registerMcpInTarget(mcp, spec) {
|
|
|
266
448
|
registerMcpInTomlBlock(mcp, spec);
|
|
267
449
|
return;
|
|
268
450
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
451
|
+
updateJsonFile(spec.mcpConfigPath, (cfg) => {
|
|
452
|
+
const section = cfg[spec.mcpRootKey] ?? {};
|
|
453
|
+
section[mcp.name] = renderMcpForTarget(mcp, spec);
|
|
454
|
+
cfg[spec.mcpRootKey] = section;
|
|
455
|
+
return true;
|
|
456
|
+
});
|
|
457
|
+
setClaudeProjectMcpApproval(mcp.name, spec, true);
|
|
275
458
|
}
|
|
276
459
|
function unregisterMcpInTarget(mcpName, spec) {
|
|
277
460
|
if (spec.mcpMechanism === "hermes-cli") {
|
|
@@ -282,14 +465,17 @@ function unregisterMcpInTarget(mcpName, spec) {
|
|
|
282
465
|
}
|
|
283
466
|
if (!existsSync(spec.mcpConfigPath))
|
|
284
467
|
return false;
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
468
|
+
const removed = updateJsonFile(spec.mcpConfigPath, (cfg) => {
|
|
469
|
+
const section = cfg[spec.mcpRootKey];
|
|
470
|
+
if (!section || !(mcpName in section))
|
|
471
|
+
return false;
|
|
472
|
+
delete section[mcpName];
|
|
473
|
+
cfg[spec.mcpRootKey] = section;
|
|
474
|
+
return true;
|
|
475
|
+
});
|
|
476
|
+
if (!removed)
|
|
288
477
|
return false;
|
|
289
|
-
|
|
290
|
-
cfg[spec.mcpRootKey] = section;
|
|
291
|
-
backupIfExists(spec.mcpConfigPath);
|
|
292
|
-
atomicWriteFile(spec.mcpConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
478
|
+
setClaudeProjectMcpApproval(mcpName, spec, false);
|
|
293
479
|
return true;
|
|
294
480
|
}
|
|
295
481
|
const HERMES_CLI_TIMEOUT_MS = 20_000;
|
|
@@ -439,7 +625,7 @@ function tomlBasicString(s) {
|
|
|
439
625
|
}
|
|
440
626
|
export function renderMcpToml(mcp) {
|
|
441
627
|
if (mcp.transport === "stdio") {
|
|
442
|
-
const { command, args } =
|
|
628
|
+
const { command, args } = stdioLaunch(mcp);
|
|
443
629
|
const lines = [`[mcp_servers.${mcp.name}]`, `command = ${tomlBasicString(command)}`];
|
|
444
630
|
if (args.length > 0) {
|
|
445
631
|
lines.push(`args = [${args.map((a) => tomlBasicString(a)).join(", ")}]`);
|
|
@@ -619,31 +805,27 @@ const LEGACY_RENAMED_SKILLS = ["alanube", "alegra-producto", "alegra-ce-universe
|
|
|
619
805
|
function stripMcpEntries(path, names) {
|
|
620
806
|
if (!existsSync(path) || names.length === 0)
|
|
621
807
|
return;
|
|
622
|
-
let cfg;
|
|
623
808
|
try {
|
|
624
|
-
|
|
809
|
+
updateJsonFile(path, (cfg) => {
|
|
810
|
+
let changed = false;
|
|
811
|
+
for (const key of ["mcpServers", "mcp"]) {
|
|
812
|
+
const section = cfg[key];
|
|
813
|
+
if (!section || typeof section !== "object")
|
|
814
|
+
continue;
|
|
815
|
+
for (const n of names) {
|
|
816
|
+
if (n in section) {
|
|
817
|
+
delete section[n];
|
|
818
|
+
changed = true;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (Object.keys(section).length === 0)
|
|
822
|
+
delete cfg[key];
|
|
823
|
+
}
|
|
824
|
+
return changed;
|
|
825
|
+
});
|
|
625
826
|
}
|
|
626
827
|
catch {
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
let changed = false;
|
|
630
|
-
for (const key of ["mcpServers", "mcp"]) {
|
|
631
|
-
const section = cfg[key];
|
|
632
|
-
if (!section || typeof section !== "object")
|
|
633
|
-
continue;
|
|
634
|
-
for (const n of names) {
|
|
635
|
-
if (n in section) {
|
|
636
|
-
delete section[n];
|
|
637
|
-
changed = true;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
if (Object.keys(section).length === 0)
|
|
641
|
-
delete cfg[key];
|
|
642
828
|
}
|
|
643
|
-
if (!changed)
|
|
644
|
-
return;
|
|
645
|
-
backupIfExists(path);
|
|
646
|
-
atomicWriteFile(path, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
647
829
|
}
|
|
648
830
|
function cleanupLegacySkillArtifacts(spec) {
|
|
649
831
|
try {
|
|
@@ -829,16 +1011,48 @@ export function installMcp(mcp, opts = {}) {
|
|
|
829
1011
|
error: `entry local retirada — ${hint}`,
|
|
830
1012
|
};
|
|
831
1013
|
}
|
|
832
|
-
const
|
|
1014
|
+
const selected = resolveTargets(opts).filter((t) => !mcp.targets || mcp.targets.includes(t.id));
|
|
1015
|
+
const unscopable = getMcpScopeDir()
|
|
1016
|
+
? selected.filter((t) => !getUserScopeMcpConfigPath(t.id))
|
|
1017
|
+
: [];
|
|
1018
|
+
const wantedTargets = selected.filter((t) => !unscopable.includes(t));
|
|
833
1019
|
const targetIds = wantedTargets.map((t) => t.id);
|
|
1020
|
+
const retiredUnscoped = opts.dryRun || unscopable.length === 0
|
|
1021
|
+
? []
|
|
1022
|
+
: retireFromUnscopableTargets(mcp, unscopable, lock.mcps[mcp.name]);
|
|
1023
|
+
const unscopedNote = unscopable.length > 0
|
|
1024
|
+
? `con MCP_DIR solo va a Claude Code/Cursor; no se instala en ${unscopable.map((t) => t.id).join(", ")}${retiredUnscoped.length > 0 ? ` (retirado de ${retiredUnscoped.join(", ")})` : ""}`
|
|
1025
|
+
: undefined;
|
|
834
1026
|
if (targetIds.length === 0) {
|
|
1027
|
+
const prevEntry = lock.mcps[mcp.name];
|
|
1028
|
+
if (prevEntry && retiredUnscoped.length > 0) {
|
|
1029
|
+
const left = (prevEntry.targets ?? []).filter((t) => !retiredUnscoped.includes(t));
|
|
1030
|
+
if (left.length > 0)
|
|
1031
|
+
lock.mcps[mcp.name] = { ...prevEntry, targets: left };
|
|
1032
|
+
else
|
|
1033
|
+
delete lock.mcps[mcp.name];
|
|
1034
|
+
writeLockFile(lock);
|
|
1035
|
+
}
|
|
835
1036
|
return {
|
|
836
1037
|
name: mcp.name,
|
|
837
1038
|
kind: "mcp",
|
|
838
|
-
status: "skipped",
|
|
839
|
-
error: "ningún target soportado disponible",
|
|
1039
|
+
status: retiredUnscoped.length > 0 ? "updated" : "skipped",
|
|
1040
|
+
error: unscopedNote ?? "ningún target soportado disponible",
|
|
840
1041
|
};
|
|
841
1042
|
}
|
|
1043
|
+
if (mcp.lazy && !opts.dryRun) {
|
|
1044
|
+
try {
|
|
1045
|
+
ensureLazyProxy(mcp);
|
|
1046
|
+
}
|
|
1047
|
+
catch (err) {
|
|
1048
|
+
return {
|
|
1049
|
+
name: mcp.name,
|
|
1050
|
+
kind: "mcp",
|
|
1051
|
+
status: "failed",
|
|
1052
|
+
error: `no se pudo escribir el proxy de arranque diferido: ${err instanceof Error ? err.message : String(err)}`,
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
842
1056
|
const prev = lock.mcps[mcp.name];
|
|
843
1057
|
const targetsMatch = prev?.targets &&
|
|
844
1058
|
prev.targets.length === targetIds.length &&
|
|
@@ -854,6 +1068,27 @@ export function installMcp(mcp, opts = {}) {
|
|
|
854
1068
|
.filter((t) => !writeTargets.includes(t) && !needsWrite(t))
|
|
855
1069
|
.map((t) => t.id);
|
|
856
1070
|
if (writeTargets.length === 0 && targetsMatch) {
|
|
1071
|
+
const retired = opts.dryRun ? [] : retireStaleMcpCopies(mcp, wantedTargets, prev);
|
|
1072
|
+
const configPaths = { ...(prev?.configPaths ?? {}), ...scopedConfigPaths(targetIds) };
|
|
1073
|
+
if (prev &&
|
|
1074
|
+
(retired.length > 0 ||
|
|
1075
|
+
stableStringify(configPaths) !== stableStringify(prev.configPaths ?? {}))) {
|
|
1076
|
+
lock.mcps[mcp.name] = { ...prev, configPaths };
|
|
1077
|
+
writeLockFile(lock);
|
|
1078
|
+
}
|
|
1079
|
+
if (retired.length > 0 || retiredUnscoped.length > 0) {
|
|
1080
|
+
const notes = [
|
|
1081
|
+
retired.length > 0 ? `copia de otro scope retirada de ${retired.join(", ")}` : "",
|
|
1082
|
+
retiredUnscoped.length > 0 ? (unscopedNote ?? "") : "",
|
|
1083
|
+
].filter(Boolean);
|
|
1084
|
+
return {
|
|
1085
|
+
name: mcp.name,
|
|
1086
|
+
kind: "mcp",
|
|
1087
|
+
status: "updated",
|
|
1088
|
+
targets: targetIds,
|
|
1089
|
+
error: notes.join("; "),
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
857
1092
|
return { name: mcp.name, kind: "mcp", status: "unchanged", targets: targetIds };
|
|
858
1093
|
}
|
|
859
1094
|
if (opts.dryRun) {
|
|
@@ -885,6 +1120,7 @@ export function installMcp(mcp, opts = {}) {
|
|
|
885
1120
|
error: failures.join("; "),
|
|
886
1121
|
};
|
|
887
1122
|
}
|
|
1123
|
+
const retired = retireStaleMcpCopies(mcp, okTargets.map((id) => TARGETS[id]), prev);
|
|
888
1124
|
const renderedHash = { ...(prev?.renderedHash ?? {}) };
|
|
889
1125
|
for (const t of writeTargets) {
|
|
890
1126
|
if (okTargets.includes(t.id))
|
|
@@ -892,7 +1128,10 @@ export function installMcp(mcp, opts = {}) {
|
|
|
892
1128
|
}
|
|
893
1129
|
const outOfScopeTargets = configChanged
|
|
894
1130
|
? []
|
|
895
|
-
: (prev?.targets ?? []).filter((t) => !targetIds.includes(t) &&
|
|
1131
|
+
: (prev?.targets ?? []).filter((t) => !targetIds.includes(t) &&
|
|
1132
|
+
(!mcp.targets || mcp.targets.includes(t)) &&
|
|
1133
|
+
!retiredUnscoped.includes(t) &&
|
|
1134
|
+
!(getMcpScopeDir() && !getUserScopeMcpConfigPath(t)));
|
|
896
1135
|
const lockTargets = [...new Set([...outOfScopeTargets, ...okTargets])];
|
|
897
1136
|
lock.mcps[mcp.name] = {
|
|
898
1137
|
name: mcp.name,
|
|
@@ -901,6 +1140,7 @@ export function installMcp(mcp, opts = {}) {
|
|
|
901
1140
|
configHash: mcpConfigHash(mcp),
|
|
902
1141
|
targets: lockTargets,
|
|
903
1142
|
renderedHash,
|
|
1143
|
+
configPaths: { ...(prev?.configPaths ?? {}), ...scopedConfigPaths(okTargets) },
|
|
904
1144
|
};
|
|
905
1145
|
writeLockFile(lock);
|
|
906
1146
|
return {
|
|
@@ -908,7 +1148,13 @@ export function installMcp(mcp, opts = {}) {
|
|
|
908
1148
|
kind: "mcp",
|
|
909
1149
|
status: prev ? "updated" : "installed",
|
|
910
1150
|
targets: okTargets,
|
|
911
|
-
error:
|
|
1151
|
+
error: [
|
|
1152
|
+
failures.length > 0 ? `targets con fallo — ${failures.join("; ")}` : "",
|
|
1153
|
+
retired.length > 0 ? `copia de otro scope retirada de ${retired.join(", ")}` : "",
|
|
1154
|
+
unscopedNote ?? "",
|
|
1155
|
+
]
|
|
1156
|
+
.filter(Boolean)
|
|
1157
|
+
.join("; ") || undefined,
|
|
912
1158
|
};
|
|
913
1159
|
}
|
|
914
1160
|
export async function installAll(opts = {}) {
|
|
@@ -936,6 +1182,15 @@ export async function installAll(opts = {}) {
|
|
|
936
1182
|
const mcp = manifest.mcps?.[name];
|
|
937
1183
|
if (!mcp)
|
|
938
1184
|
continue;
|
|
1185
|
+
if (mcp.name !== name) {
|
|
1186
|
+
results.push({
|
|
1187
|
+
name,
|
|
1188
|
+
kind: "mcp",
|
|
1189
|
+
status: "failed",
|
|
1190
|
+
error: `la entry "${name}" declara name "${mcp.name}"`,
|
|
1191
|
+
});
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
939
1194
|
results.push(installMcp(mcp, { ...opts, targets }));
|
|
940
1195
|
}
|
|
941
1196
|
return { manifest, results, targets };
|
|
@@ -1002,6 +1257,18 @@ export function uninstallMcp(name, opts = {}) {
|
|
|
1002
1257
|
try {
|
|
1003
1258
|
for (const tId of targetIds) {
|
|
1004
1259
|
unregisterMcpInTarget(name, TARGETS[tId]);
|
|
1260
|
+
const recorded = prev.configPaths?.[tId];
|
|
1261
|
+
if (recorded && recorded !== TARGETS[tId].mcpConfigPath) {
|
|
1262
|
+
try {
|
|
1263
|
+
if (removeMcpEntryAt(recorded, TARGETS[tId].mcpRootKey, name, () => true)) {
|
|
1264
|
+
if (tId === "claude-code" && basename(recorded) === ".mcp.json") {
|
|
1265
|
+
setClaudeProjectMcpApproval(name, TARGETS[tId], false, dirname(recorded));
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
catch {
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1005
1272
|
}
|
|
1006
1273
|
}
|
|
1007
1274
|
catch (err) {
|
|
@@ -1014,6 +1281,7 @@ export function uninstallMcp(name, opts = {}) {
|
|
|
1014
1281
|
}
|
|
1015
1282
|
delete lock.mcps[name];
|
|
1016
1283
|
writeLockFile(lock);
|
|
1284
|
+
rmSync(lazyToolsPath(name), { force: true });
|
|
1017
1285
|
}
|
|
1018
1286
|
return { name, kind: "mcp", status: "installed", targets: targetIds };
|
|
1019
1287
|
}
|
|
@@ -1032,6 +1300,13 @@ export function uninstallAll(opts = {}) {
|
|
|
1032
1300
|
catch {
|
|
1033
1301
|
}
|
|
1034
1302
|
}
|
|
1303
|
+
if (Object.keys(readLockFile().mcps ?? {}).length === 0) {
|
|
1304
|
+
try {
|
|
1305
|
+
rmSync(lazyMcpDir(), { recursive: true, force: true });
|
|
1306
|
+
}
|
|
1307
|
+
catch {
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1035
1310
|
}
|
|
1036
1311
|
return results;
|
|
1037
1312
|
}
|
|
@@ -1064,6 +1339,7 @@ export function detectDrift(manifest) {
|
|
|
1064
1339
|
unchanged: [],
|
|
1065
1340
|
orphaned: [],
|
|
1066
1341
|
missing: [],
|
|
1342
|
+
outOfPlace: [],
|
|
1067
1343
|
};
|
|
1068
1344
|
const manifestMcps = manifest.mcps ?? {};
|
|
1069
1345
|
for (const name of Object.keys(manifestMcps)) {
|
|
@@ -1079,9 +1355,13 @@ export function detectDrift(manifest) {
|
|
|
1079
1355
|
continue;
|
|
1080
1356
|
}
|
|
1081
1357
|
const missing = mcpNeedsRepairInTargets(manifestMcps[name], (prevMcp.targets ?? []), prevMcp.renderedHash);
|
|
1082
|
-
|
|
1358
|
+
const outOfPlace = mcpOutOfPlace(manifestMcps[name], prevMcp);
|
|
1359
|
+
if (missing.length > 0 || outOfPlace.length > 0) {
|
|
1083
1360
|
mcps.toUpdate.push(name);
|
|
1084
|
-
|
|
1361
|
+
if (missing.length > 0)
|
|
1362
|
+
mcps.missing.push(name);
|
|
1363
|
+
if (outOfPlace.length > 0)
|
|
1364
|
+
mcps.outOfPlace.push(name);
|
|
1085
1365
|
}
|
|
1086
1366
|
else {
|
|
1087
1367
|
mcps.unchanged.push(name);
|
package/dist/lazy-mcp.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, 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({
|
|
29
|
+
name: mcp.name,
|
|
30
|
+
...(mcp.lazy.protocolVersion ? { protocolVersion: mcp.lazy.protocolVersion } : {}),
|
|
31
|
+
tools: mcp.lazy.tools,
|
|
32
|
+
}, null, 2)}\n`);
|
|
33
|
+
}
|
|
34
|
+
function writeIfChanged(path, content) {
|
|
35
|
+
if (existsSync(path) && readFileSync(path, "utf-8") === content)
|
|
36
|
+
return;
|
|
37
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
38
|
+
writeFileSync(tmp, content, { mode: 0o644 });
|
|
39
|
+
renameSync(tmp, path);
|
|
40
|
+
}
|
|
41
|
+
export function lazyProxySource() {
|
|
42
|
+
const product = JSON.stringify(COMMAND_NAME);
|
|
43
|
+
return `#!/usr/bin/env node
|
|
44
|
+
// Proxy de arranque diferido de ${COMMAND_NAME}. Generado por el instalador: no
|
|
45
|
+
// editar, se reescribe en cada install/update. Uso:
|
|
46
|
+
// node lazy-mcp-proxy.mjs <tools.json> -- <comando real> [args…]
|
|
47
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
48
|
+
import { readFileSync } from "node:fs";
|
|
49
|
+
import { createInterface } from "node:readline";
|
|
50
|
+
|
|
51
|
+
const argv = process.argv.slice(2);
|
|
52
|
+
const sep = argv.indexOf("--");
|
|
53
|
+
if (sep !== 1 || argv.length < 3) {
|
|
54
|
+
process.stderr.write("uso: lazy-mcp-proxy.mjs <tools.json> -- <comando> [args…]\\n");
|
|
55
|
+
process.exit(2);
|
|
56
|
+
}
|
|
57
|
+
const spec = JSON.parse(readFileSync(argv[0], "utf8"));
|
|
58
|
+
const [cmd, ...args] = argv.slice(2);
|
|
59
|
+
const INIT_ID = "__alegra_lazy_init__";
|
|
60
|
+
const START_TIMEOUT_MS = 10 * 60 * 1000;
|
|
61
|
+
|
|
62
|
+
const send = (m) => process.stdout.write(JSON.stringify(m) + "\\n");
|
|
63
|
+
const key = (id) => JSON.stringify(id);
|
|
64
|
+
let initParams = null;
|
|
65
|
+
let child = null;
|
|
66
|
+
let ready = false;
|
|
67
|
+
let dead = null;
|
|
68
|
+
let startTimer = null;
|
|
69
|
+
const queue = [];
|
|
70
|
+
// Requests reenviadas al server que todavía no tuvieron respuesta.
|
|
71
|
+
const inflight = new Map();
|
|
72
|
+
|
|
73
|
+
function errorFor(m, reason) {
|
|
74
|
+
if (m.method === "tools/call") {
|
|
75
|
+
return { jsonrpc: "2.0", id: m.id, result: { content: [{ type: "text", text: reason }], isError: true } };
|
|
76
|
+
}
|
|
77
|
+
return { jsonrpc: "2.0", id: m.id, error: { code: -32603, message: reason } };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function killChild() {
|
|
81
|
+
if (!child || child.exitCode !== null) return;
|
|
82
|
+
if (process.platform === "win32") {
|
|
83
|
+
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
84
|
+
} else {
|
|
85
|
+
child.kill("SIGTERM");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function die(reason) {
|
|
90
|
+
if (dead) return;
|
|
91
|
+
dead = reason;
|
|
92
|
+
ready = false;
|
|
93
|
+
clearTimeout(startTimer);
|
|
94
|
+
for (const m of queue.splice(0)) send(errorFor(m, reason));
|
|
95
|
+
for (const m of inflight.values()) send(errorFor(m, reason));
|
|
96
|
+
inflight.clear();
|
|
97
|
+
killChild();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function forward(m) {
|
|
101
|
+
if (m.id !== undefined && m.id !== null && m.method) inflight.set(key(m.id), m);
|
|
102
|
+
child.stdin.write(JSON.stringify(m) + "\\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function start() {
|
|
106
|
+
if (child || dead) return;
|
|
107
|
+
child = spawn(cmd, args, { stdio: ["pipe", "pipe", "inherit"] });
|
|
108
|
+
startTimer = setTimeout(
|
|
109
|
+
() => die("El MCP " + spec.name + " tardó más de 10 min en arrancar (¿se cortó la descarga?). Reintentá en una sesión nueva."),
|
|
110
|
+
START_TIMEOUT_MS,
|
|
111
|
+
);
|
|
112
|
+
child.on("error", (e) => die("No se pudo arrancar el MCP " + spec.name + ": " + e.message));
|
|
113
|
+
// "close" y no "exit": en "exit" el stdout del server todavía puede tener
|
|
114
|
+
// respuestas sin leer.
|
|
115
|
+
child.on("close", (code, signal) => {
|
|
116
|
+
die("El MCP " + spec.name + " terminó (" + (signal ?? "código " + code) + "). Reintentá en una sesión nueva; si persiste, corré " + ${product} + " status.");
|
|
117
|
+
});
|
|
118
|
+
createInterface({ input: child.stdout }).on("line", (line) => {
|
|
119
|
+
let m;
|
|
120
|
+
try { m = JSON.parse(line); } catch { m = null; }
|
|
121
|
+
if (ready) {
|
|
122
|
+
if (m && m.id !== undefined && !m.method) inflight.delete(key(m.id));
|
|
123
|
+
process.stdout.write(line + "\\n");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!m || m.id !== INIT_ID) return; // notificaciones previas al handshake
|
|
127
|
+
if (m.error) {
|
|
128
|
+
die("El MCP " + spec.name + " rechazó el handshake: " + (m.error.message ?? "sin detalle"));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
ready = true;
|
|
132
|
+
clearTimeout(startTimer);
|
|
133
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\\n");
|
|
134
|
+
for (const q of queue.splice(0)) forward(q);
|
|
135
|
+
});
|
|
136
|
+
child.stdin.on("error", () => {});
|
|
137
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: INIT_ID, method: "initialize", params: initParams }) + "\\n");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
createInterface({ input: process.stdin }).on("line", (line) => {
|
|
141
|
+
let m;
|
|
142
|
+
try { m = JSON.parse(line); } catch { return; }
|
|
143
|
+
if (ready) {
|
|
144
|
+
forward(m);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
switch (m.method) {
|
|
148
|
+
case "initialize":
|
|
149
|
+
initParams = m.params ?? {};
|
|
150
|
+
send({
|
|
151
|
+
jsonrpc: "2.0",
|
|
152
|
+
id: m.id,
|
|
153
|
+
result: {
|
|
154
|
+
protocolVersion: spec.protocolVersion ?? initParams.protocolVersion ?? "2025-06-18",
|
|
155
|
+
capabilities: { tools: {} },
|
|
156
|
+
serverInfo: { name: spec.name, version: "lazy" },
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
case "ping":
|
|
161
|
+
send({ jsonrpc: "2.0", id: m.id, result: {} });
|
|
162
|
+
return;
|
|
163
|
+
case "tools/list":
|
|
164
|
+
send({ jsonrpc: "2.0", id: m.id, result: { tools: spec.tools } });
|
|
165
|
+
return;
|
|
166
|
+
case "resources/list":
|
|
167
|
+
send({ jsonrpc: "2.0", id: m.id, result: { resources: [] } });
|
|
168
|
+
return;
|
|
169
|
+
case "resources/templates/list":
|
|
170
|
+
send({ jsonrpc: "2.0", id: m.id, result: { resourceTemplates: [] } });
|
|
171
|
+
return;
|
|
172
|
+
case "prompts/list":
|
|
173
|
+
send({ jsonrpc: "2.0", id: m.id, result: { prompts: [] } });
|
|
174
|
+
return;
|
|
175
|
+
case "notifications/cancelled": {
|
|
176
|
+
// Una request cancelada mientras el server arranca no se reenvía.
|
|
177
|
+
const i = queue.findIndex((q) => key(q.id) === key(m.params?.requestId));
|
|
178
|
+
if (i >= 0) queue.splice(i, 1);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (m.id === undefined || m.id === null) return; // notificación: nada que contestar
|
|
183
|
+
if (dead) {
|
|
184
|
+
send(errorFor(m, dead));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
queue.push(m);
|
|
188
|
+
start();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
function shutdown() {
|
|
192
|
+
killChild();
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
process.stdin.on("end", shutdown);
|
|
196
|
+
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) process.on(sig, shutdown);
|
|
197
|
+
`;
|
|
198
|
+
}
|
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";
|
|
@@ -29,6 +29,84 @@ export function getClaudeSettingsPath() {
|
|
|
29
29
|
export function getClaudeJsonPath() {
|
|
30
30
|
return join(homedir(), ".claude.json");
|
|
31
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 isMcpScopeEnvOverride() {
|
|
92
|
+
const env = process.env.ALEGRA_DATA_AGENT_MCP_DIR;
|
|
93
|
+
if (env === undefined)
|
|
94
|
+
return false;
|
|
95
|
+
return env.trim() !== (readAgentConf().MCP_DIR ?? "").trim();
|
|
96
|
+
}
|
|
97
|
+
export function getMcpScopeWarning() {
|
|
98
|
+
return resolveMcpScope().warning;
|
|
99
|
+
}
|
|
100
|
+
export function getMcpScopeDir() {
|
|
101
|
+
return resolveMcpScope().dir;
|
|
102
|
+
}
|
|
103
|
+
function mcpScopedPath(relPath, fallback) {
|
|
104
|
+
const dir = getMcpScopeDir();
|
|
105
|
+
return dir ? join(dir, relPath) : fallback;
|
|
106
|
+
}
|
|
107
|
+
export function getClaudeMcpConfigPath() {
|
|
108
|
+
return mcpScopedPath(".mcp.json", getClaudeJsonPath());
|
|
109
|
+
}
|
|
32
110
|
export function getCursorSkillsDir() {
|
|
33
111
|
return join(homedir(), ".cursor", "skills");
|
|
34
112
|
}
|
|
@@ -36,8 +114,18 @@ export function getCursorLegacyRulesDir() {
|
|
|
36
114
|
return join(homedir(), ".cursor", "rules");
|
|
37
115
|
}
|
|
38
116
|
export function getCursorMcpPath() {
|
|
117
|
+
return mcpScopedPath(join(".cursor", "mcp.json"), getCursorUserMcpPath());
|
|
118
|
+
}
|
|
119
|
+
export function getCursorUserMcpPath() {
|
|
39
120
|
return join(homedir(), ".cursor", "mcp.json");
|
|
40
121
|
}
|
|
122
|
+
export function getUserScopeMcpConfigPath(target) {
|
|
123
|
+
if (target === "claude-code")
|
|
124
|
+
return getClaudeJsonPath();
|
|
125
|
+
if (target === "cursor")
|
|
126
|
+
return getCursorUserMcpPath();
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
41
129
|
export function getOpenCodeSkillsDir() {
|
|
42
130
|
const xdgHome = process.env.XDG_CONFIG_HOME;
|
|
43
131
|
const base = xdgHome && xdgHome.length > 0 ? xdgHome : join(homedir(), ".config");
|
|
@@ -137,7 +225,7 @@ export function getTargetSkillsDir(target) {
|
|
|
137
225
|
export function getTargetMcpConfigPath(target) {
|
|
138
226
|
switch (target) {
|
|
139
227
|
case "claude-code":
|
|
140
|
-
return
|
|
228
|
+
return getClaudeMcpConfigPath();
|
|
141
229
|
case "cursor":
|
|
142
230
|
return getCursorMcpPath();
|
|
143
231
|
case "opencode":
|
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 };
|
|
@@ -125,7 +126,7 @@ function bearerInterpolation(env, target) {
|
|
|
125
126
|
}
|
|
126
127
|
export function renderHermesMcpOps(mcp) {
|
|
127
128
|
if (mcp.transport === "stdio") {
|
|
128
|
-
const { command, args } =
|
|
129
|
+
const { command, args } = stdioLaunch(mcp);
|
|
129
130
|
const ops = [{ path: "command", value: command }];
|
|
130
131
|
if (args.length > 0)
|
|
131
132
|
ops.push({ path: "args", value: JSON.stringify(args) });
|
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
|
},
|