alegra-data-agent 0.4.2 → 0.4.4
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/commands/status.js +10 -1
- package/dist/commands/update.js +3 -1
- package/dist/installer.js +214 -12
- package/dist/lazy-mcp.js +63 -18
- package/dist/lib.js +20 -1
- package/package.json +1 -1
package/dist/commands/status.js
CHANGED
|
@@ -4,7 +4,7 @@ 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";
|
|
7
|
+
import { detectDrift, findMisplacedMcps, inspectMcpInTarget } from "../installer.js";
|
|
8
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() {
|
|
@@ -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)
|
|
@@ -320,6 +328,7 @@ export async function runStatus() {
|
|
|
320
328
|
log("");
|
|
321
329
|
reportMcps(manifest, lock);
|
|
322
330
|
reportRogueMetabase(manifest);
|
|
331
|
+
reportMisplacedMcps(manifest);
|
|
323
332
|
reportClaudeAiDuplicates(manifest);
|
|
324
333
|
reportAccountConnectorOverlap(manifest);
|
|
325
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
|
@@ -4,8 +4,8 @@ import { chmodSync, copyFileSync, existsSync, readFileSync, readdirSync, renameS
|
|
|
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 { 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";
|
|
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";
|
|
9
9
|
import { inlineReferences, renderHermesMcpOps, renderMcpForTarget, } from "./render.js";
|
|
10
10
|
import { TARGETS, detectInstalledTargets } from "./targets.js";
|
|
11
11
|
const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
@@ -288,10 +288,9 @@ export function updateJsonFile(path, mutate) {
|
|
|
288
288
|
}
|
|
289
289
|
throw new Error(`${path} cambió 5 veces mientras se escribía; volvé a correr la instalación.`);
|
|
290
290
|
}
|
|
291
|
-
function setClaudeProjectMcpApproval(name, spec, approved) {
|
|
291
|
+
function setClaudeProjectMcpApproval(name, spec, approved, dir = getMcpScopeDir()) {
|
|
292
292
|
if (spec.id !== "claude-code")
|
|
293
293
|
return;
|
|
294
|
-
const dir = getMcpScopeDir();
|
|
295
294
|
if (!dir)
|
|
296
295
|
return;
|
|
297
296
|
const settingsPath = join(dir, ".claude", "settings.json");
|
|
@@ -311,6 +310,134 @@ function setClaudeProjectMcpApproval(name, spec, approved) {
|
|
|
311
310
|
catch {
|
|
312
311
|
}
|
|
313
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
|
+
}
|
|
314
441
|
function registerMcpInTarget(mcp, spec) {
|
|
315
442
|
validateMcpName(mcp.name);
|
|
316
443
|
if (spec.mcpMechanism === "hermes-cli") {
|
|
@@ -884,14 +1011,33 @@ export function installMcp(mcp, opts = {}) {
|
|
|
884
1011
|
error: `entry local retirada — ${hint}`,
|
|
885
1012
|
};
|
|
886
1013
|
}
|
|
887
|
-
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));
|
|
888
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;
|
|
889
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
|
+
}
|
|
890
1036
|
return {
|
|
891
1037
|
name: mcp.name,
|
|
892
1038
|
kind: "mcp",
|
|
893
|
-
status: "skipped",
|
|
894
|
-
error: "ningún target soportado disponible",
|
|
1039
|
+
status: retiredUnscoped.length > 0 ? "updated" : "skipped",
|
|
1040
|
+
error: unscopedNote ?? "ningún target soportado disponible",
|
|
895
1041
|
};
|
|
896
1042
|
}
|
|
897
1043
|
if (mcp.lazy && !opts.dryRun) {
|
|
@@ -922,6 +1068,27 @@ export function installMcp(mcp, opts = {}) {
|
|
|
922
1068
|
.filter((t) => !writeTargets.includes(t) && !needsWrite(t))
|
|
923
1069
|
.map((t) => t.id);
|
|
924
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
|
+
}
|
|
925
1092
|
return { name: mcp.name, kind: "mcp", status: "unchanged", targets: targetIds };
|
|
926
1093
|
}
|
|
927
1094
|
if (opts.dryRun) {
|
|
@@ -953,6 +1120,7 @@ export function installMcp(mcp, opts = {}) {
|
|
|
953
1120
|
error: failures.join("; "),
|
|
954
1121
|
};
|
|
955
1122
|
}
|
|
1123
|
+
const retired = retireStaleMcpCopies(mcp, okTargets.map((id) => TARGETS[id]), prev);
|
|
956
1124
|
const renderedHash = { ...(prev?.renderedHash ?? {}) };
|
|
957
1125
|
for (const t of writeTargets) {
|
|
958
1126
|
if (okTargets.includes(t.id))
|
|
@@ -960,7 +1128,10 @@ export function installMcp(mcp, opts = {}) {
|
|
|
960
1128
|
}
|
|
961
1129
|
const outOfScopeTargets = configChanged
|
|
962
1130
|
? []
|
|
963
|
-
: (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)));
|
|
964
1135
|
const lockTargets = [...new Set([...outOfScopeTargets, ...okTargets])];
|
|
965
1136
|
lock.mcps[mcp.name] = {
|
|
966
1137
|
name: mcp.name,
|
|
@@ -969,6 +1140,7 @@ export function installMcp(mcp, opts = {}) {
|
|
|
969
1140
|
configHash: mcpConfigHash(mcp),
|
|
970
1141
|
targets: lockTargets,
|
|
971
1142
|
renderedHash,
|
|
1143
|
+
configPaths: { ...(prev?.configPaths ?? {}), ...scopedConfigPaths(okTargets) },
|
|
972
1144
|
};
|
|
973
1145
|
writeLockFile(lock);
|
|
974
1146
|
return {
|
|
@@ -976,7 +1148,13 @@ export function installMcp(mcp, opts = {}) {
|
|
|
976
1148
|
kind: "mcp",
|
|
977
1149
|
status: prev ? "updated" : "installed",
|
|
978
1150
|
targets: okTargets,
|
|
979
|
-
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,
|
|
980
1158
|
};
|
|
981
1159
|
}
|
|
982
1160
|
export async function installAll(opts = {}) {
|
|
@@ -1079,6 +1257,18 @@ export function uninstallMcp(name, opts = {}) {
|
|
|
1079
1257
|
try {
|
|
1080
1258
|
for (const tId of targetIds) {
|
|
1081
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
|
+
}
|
|
1082
1272
|
}
|
|
1083
1273
|
}
|
|
1084
1274
|
catch (err) {
|
|
@@ -1091,6 +1281,7 @@ export function uninstallMcp(name, opts = {}) {
|
|
|
1091
1281
|
}
|
|
1092
1282
|
delete lock.mcps[name];
|
|
1093
1283
|
writeLockFile(lock);
|
|
1284
|
+
rmSync(lazyToolsPath(name), { force: true });
|
|
1094
1285
|
}
|
|
1095
1286
|
return { name, kind: "mcp", status: "installed", targets: targetIds };
|
|
1096
1287
|
}
|
|
@@ -1109,7 +1300,13 @@ export function uninstallAll(opts = {}) {
|
|
|
1109
1300
|
catch {
|
|
1110
1301
|
}
|
|
1111
1302
|
}
|
|
1112
|
-
|
|
1303
|
+
if (Object.keys(readLockFile().mcps ?? {}).length === 0) {
|
|
1304
|
+
try {
|
|
1305
|
+
rmSync(lazyMcpDir(), { recursive: true, force: true });
|
|
1306
|
+
}
|
|
1307
|
+
catch {
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1113
1310
|
}
|
|
1114
1311
|
return results;
|
|
1115
1312
|
}
|
|
@@ -1142,6 +1339,7 @@ export function detectDrift(manifest) {
|
|
|
1142
1339
|
unchanged: [],
|
|
1143
1340
|
orphaned: [],
|
|
1144
1341
|
missing: [],
|
|
1342
|
+
outOfPlace: [],
|
|
1145
1343
|
};
|
|
1146
1344
|
const manifestMcps = manifest.mcps ?? {};
|
|
1147
1345
|
for (const name of Object.keys(manifestMcps)) {
|
|
@@ -1157,9 +1355,13 @@ export function detectDrift(manifest) {
|
|
|
1157
1355
|
continue;
|
|
1158
1356
|
}
|
|
1159
1357
|
const missing = mcpNeedsRepairInTargets(manifestMcps[name], (prevMcp.targets ?? []), prevMcp.renderedHash);
|
|
1160
|
-
|
|
1358
|
+
const outOfPlace = mcpOutOfPlace(manifestMcps[name], prevMcp);
|
|
1359
|
+
if (missing.length > 0 || outOfPlace.length > 0) {
|
|
1161
1360
|
mcps.toUpdate.push(name);
|
|
1162
|
-
|
|
1361
|
+
if (missing.length > 0)
|
|
1362
|
+
mcps.missing.push(name);
|
|
1363
|
+
if (outOfPlace.length > 0)
|
|
1364
|
+
mcps.outOfPlace.push(name);
|
|
1163
1365
|
}
|
|
1164
1366
|
else {
|
|
1165
1367
|
mcps.unchanged.push(name);
|
package/dist/lazy-mcp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { COMMAND_NAME, adaptStdioCommand } from "./lib.js";
|
|
@@ -25,12 +25,18 @@ export function ensureLazyProxy(mcp) {
|
|
|
25
25
|
return;
|
|
26
26
|
mkdirSync(lazyMcpDir(), { recursive: true });
|
|
27
27
|
writeIfChanged(lazyProxyPath(), lazyProxySource());
|
|
28
|
-
writeIfChanged(lazyToolsPath(mcp.name), `${JSON.stringify({
|
|
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`);
|
|
29
33
|
}
|
|
30
34
|
function writeIfChanged(path, content) {
|
|
31
35
|
if (existsSync(path) && readFileSync(path, "utf-8") === content)
|
|
32
36
|
return;
|
|
33
|
-
|
|
37
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
38
|
+
writeFileSync(tmp, content, { mode: 0o644 });
|
|
39
|
+
renameSync(tmp, path);
|
|
34
40
|
}
|
|
35
41
|
export function lazyProxySource() {
|
|
36
42
|
const product = JSON.stringify(COMMAND_NAME);
|
|
@@ -38,7 +44,7 @@ export function lazyProxySource() {
|
|
|
38
44
|
// Proxy de arranque diferido de ${COMMAND_NAME}. Generado por el instalador: no
|
|
39
45
|
// editar, se reescribe en cada install/update. Uso:
|
|
40
46
|
// node lazy-mcp-proxy.mjs <tools.json> -- <comando real> [args…]
|
|
41
|
-
import { spawn } from "node:child_process";
|
|
47
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
42
48
|
import { readFileSync } from "node:fs";
|
|
43
49
|
import { createInterface } from "node:readline";
|
|
44
50
|
|
|
@@ -51,13 +57,18 @@ if (sep !== 1 || argv.length < 3) {
|
|
|
51
57
|
const spec = JSON.parse(readFileSync(argv[0], "utf8"));
|
|
52
58
|
const [cmd, ...args] = argv.slice(2);
|
|
53
59
|
const INIT_ID = "__alegra_lazy_init__";
|
|
60
|
+
const START_TIMEOUT_MS = 10 * 60 * 1000;
|
|
54
61
|
|
|
55
62
|
const send = (m) => process.stdout.write(JSON.stringify(m) + "\\n");
|
|
63
|
+
const key = (id) => JSON.stringify(id);
|
|
56
64
|
let initParams = null;
|
|
57
65
|
let child = null;
|
|
58
66
|
let ready = false;
|
|
59
67
|
let dead = null;
|
|
68
|
+
let startTimer = null;
|
|
60
69
|
const queue = [];
|
|
70
|
+
// Requests reenviadas al server que todavía no tuvieron respuesta.
|
|
71
|
+
const inflight = new Map();
|
|
61
72
|
|
|
62
73
|
function errorFor(m, reason) {
|
|
63
74
|
if (m.method === "tools/call") {
|
|
@@ -66,47 +77,73 @@ function errorFor(m, reason) {
|
|
|
66
77
|
return { jsonrpc: "2.0", id: m.id, error: { code: -32603, message: reason } };
|
|
67
78
|
}
|
|
68
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
|
+
|
|
69
89
|
function die(reason) {
|
|
90
|
+
if (dead) return;
|
|
70
91
|
dead = reason;
|
|
71
92
|
ready = false;
|
|
93
|
+
clearTimeout(startTimer);
|
|
72
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");
|
|
73
103
|
}
|
|
74
104
|
|
|
75
105
|
function start() {
|
|
76
106
|
if (child || dead) return;
|
|
77
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
|
+
);
|
|
78
112
|
child.on("error", (e) => die("No se pudo arrancar el MCP " + spec.name + ": " + e.message));
|
|
79
|
-
|
|
80
|
-
|
|
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.");
|
|
81
117
|
});
|
|
82
118
|
createInterface({ input: child.stdout }).on("line", (line) => {
|
|
119
|
+
let m;
|
|
120
|
+
try { m = JSON.parse(line); } catch { m = null; }
|
|
83
121
|
if (ready) {
|
|
122
|
+
if (m && m.id !== undefined && !m.method) inflight.delete(key(m.id));
|
|
84
123
|
process.stdout.write(line + "\\n");
|
|
85
124
|
return;
|
|
86
125
|
}
|
|
87
|
-
|
|
88
|
-
try { m = JSON.parse(line); } catch { return; }
|
|
89
|
-
if (m.id !== INIT_ID) return; // notificaciones previas al handshake
|
|
126
|
+
if (!m || m.id !== INIT_ID) return; // notificaciones previas al handshake
|
|
90
127
|
if (m.error) {
|
|
91
128
|
die("El MCP " + spec.name + " rechazó el handshake: " + (m.error.message ?? "sin detalle"));
|
|
92
|
-
child.kill();
|
|
93
129
|
return;
|
|
94
130
|
}
|
|
95
131
|
ready = true;
|
|
132
|
+
clearTimeout(startTimer);
|
|
96
133
|
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\\n");
|
|
97
|
-
for (const q of queue.splice(0))
|
|
134
|
+
for (const q of queue.splice(0)) forward(q);
|
|
98
135
|
});
|
|
99
136
|
child.stdin.on("error", () => {});
|
|
100
137
|
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: INIT_ID, method: "initialize", params: initParams }) + "\\n");
|
|
101
138
|
}
|
|
102
139
|
|
|
103
140
|
createInterface({ input: process.stdin }).on("line", (line) => {
|
|
141
|
+
let m;
|
|
142
|
+
try { m = JSON.parse(line); } catch { return; }
|
|
104
143
|
if (ready) {
|
|
105
|
-
|
|
144
|
+
forward(m);
|
|
106
145
|
return;
|
|
107
146
|
}
|
|
108
|
-
let m;
|
|
109
|
-
try { m = JSON.parse(line); } catch { return; }
|
|
110
147
|
switch (m.method) {
|
|
111
148
|
case "initialize":
|
|
112
149
|
initParams = m.params ?? {};
|
|
@@ -114,7 +151,7 @@ createInterface({ input: process.stdin }).on("line", (line) => {
|
|
|
114
151
|
jsonrpc: "2.0",
|
|
115
152
|
id: m.id,
|
|
116
153
|
result: {
|
|
117
|
-
protocolVersion: initParams.protocolVersion ?? "2025-06-18",
|
|
154
|
+
protocolVersion: spec.protocolVersion ?? initParams.protocolVersion ?? "2025-06-18",
|
|
118
155
|
capabilities: { tools: {} },
|
|
119
156
|
serverInfo: { name: spec.name, version: "lazy" },
|
|
120
157
|
},
|
|
@@ -135,6 +172,12 @@ createInterface({ input: process.stdin }).on("line", (line) => {
|
|
|
135
172
|
case "prompts/list":
|
|
136
173
|
send({ jsonrpc: "2.0", id: m.id, result: { prompts: [] } });
|
|
137
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
|
+
}
|
|
138
181
|
}
|
|
139
182
|
if (m.id === undefined || m.id === null) return; // notificación: nada que contestar
|
|
140
183
|
if (dead) {
|
|
@@ -145,9 +188,11 @@ createInterface({ input: process.stdin }).on("line", (line) => {
|
|
|
145
188
|
start();
|
|
146
189
|
});
|
|
147
190
|
|
|
148
|
-
|
|
149
|
-
|
|
191
|
+
function shutdown() {
|
|
192
|
+
killChild();
|
|
150
193
|
process.exit(0);
|
|
151
|
-
}
|
|
194
|
+
}
|
|
195
|
+
process.stdin.on("end", shutdown);
|
|
196
|
+
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) process.on(sig, shutdown);
|
|
152
197
|
`;
|
|
153
198
|
}
|
package/dist/lib.js
CHANGED
|
@@ -43,6 +43,9 @@ function readAgentConf() {
|
|
|
43
43
|
catch {
|
|
44
44
|
return {};
|
|
45
45
|
}
|
|
46
|
+
return parseAgentConf(raw);
|
|
47
|
+
}
|
|
48
|
+
export function parseAgentConf(raw) {
|
|
46
49
|
const out = {};
|
|
47
50
|
for (const line0 of raw.split("\n")) {
|
|
48
51
|
const line = line0.trim();
|
|
@@ -88,6 +91,12 @@ function resolveMcpScope() {
|
|
|
88
91
|
}
|
|
89
92
|
return { dir, warning: undefined };
|
|
90
93
|
}
|
|
94
|
+
export function isMcpScopeEnvOverride() {
|
|
95
|
+
const env = process.env.ALEGRA_DATA_AGENT_MCP_DIR;
|
|
96
|
+
if (env === undefined)
|
|
97
|
+
return false;
|
|
98
|
+
return env.trim() !== (readAgentConf().MCP_DIR ?? "").trim();
|
|
99
|
+
}
|
|
91
100
|
export function getMcpScopeWarning() {
|
|
92
101
|
return resolveMcpScope().warning;
|
|
93
102
|
}
|
|
@@ -108,7 +117,17 @@ export function getCursorLegacyRulesDir() {
|
|
|
108
117
|
return join(homedir(), ".cursor", "rules");
|
|
109
118
|
}
|
|
110
119
|
export function getCursorMcpPath() {
|
|
111
|
-
return mcpScopedPath(join(".cursor", "mcp.json"),
|
|
120
|
+
return mcpScopedPath(join(".cursor", "mcp.json"), getCursorUserMcpPath());
|
|
121
|
+
}
|
|
122
|
+
export function getCursorUserMcpPath() {
|
|
123
|
+
return join(homedir(), ".cursor", "mcp.json");
|
|
124
|
+
}
|
|
125
|
+
export function getUserScopeMcpConfigPath(target) {
|
|
126
|
+
if (target === "claude-code")
|
|
127
|
+
return getClaudeJsonPath();
|
|
128
|
+
if (target === "cursor")
|
|
129
|
+
return getCursorUserMcpPath();
|
|
130
|
+
return undefined;
|
|
112
131
|
}
|
|
113
132
|
export function getOpenCodeSkillsDir() {
|
|
114
133
|
const xdgHome = process.env.XDG_CONFIG_HOME;
|