alegra-data-agent 0.2.5 → 0.2.7

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.
@@ -35,13 +35,20 @@ function reportMcps(manifest, lock) {
35
35
  const ok = [];
36
36
  const missing = [];
37
37
  const modified = [];
38
+ const broken = [];
39
+ const stale = [];
40
+ const prevHashes = lock.mcps?.[name]?.renderedHash;
38
41
  for (const t of targets) {
39
42
  const label = TARGETS[t].label;
40
- const state = inspectMcpInTarget(entry, TARGETS[t]);
43
+ const state = inspectMcpInTarget(entry, TARGETS[t], prevHashes?.[t]);
41
44
  if (state === "ok")
42
45
  ok.push(label);
43
46
  else if (state === "missing")
44
47
  missing.push(label);
48
+ else if (state === "stale")
49
+ stale.push(label);
50
+ else if (state === "broken")
51
+ broken.push(label);
45
52
  else
46
53
  modified.push(label);
47
54
  }
@@ -50,13 +57,61 @@ function reportMcps(manifest, lock) {
50
57
  notes.push(gray(`ok en ${ok.join(", ")}`));
51
58
  if (modified.length > 0)
52
59
  notes.push(dim(`editado a mano en ${modified.join(", ")}`));
60
+ if (broken.length > 0)
61
+ notes.push(yellow(`sin \`type\` — el cliente la descarta en ${broken.join(", ")}`));
62
+ if (stale.length > 0)
63
+ notes.push(dim(`formato viejo en ${stale.join(", ")} — se actualiza`));
53
64
  if (missing.length > 0)
54
65
  notes.push(yellow(`falta en ${missing.join(", ")}`));
55
- const icon = missing.length > 0 ? yellow("✗") : modified.length > 0 ? dim("~") : green("✓");
66
+ const icon = missing.length > 0 || broken.length > 0
67
+ ? yellow("✗")
68
+ : stale.length > 0 || modified.length > 0
69
+ ? dim("~")
70
+ : green("✓");
56
71
  log(` ${icon} ${name} ${gray("·")} ${notes.join(gray(" · "))}`);
57
72
  }
58
73
  log("");
59
74
  }
75
+ function reportAccountConnectorOverlap(manifest) {
76
+ const claudeCfg = TARGETS["claude-code"].mcpConfigPath;
77
+ if (!existsSync(claudeCfg))
78
+ return;
79
+ let connected = [];
80
+ try {
81
+ const cfg = JSON.parse(readFileSync(claudeCfg, "utf-8"));
82
+ if (!Array.isArray(cfg.claudeAiMcpEverConnected))
83
+ return;
84
+ connected = cfg.claudeAiMcpEverConnected.filter((x) => typeof x === "string");
85
+ }
86
+ catch {
87
+ return;
88
+ }
89
+ if (connected.length === 0)
90
+ return;
91
+ const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
92
+ const overlap = [];
93
+ for (const name of Object.keys(manifest.mcps ?? {})) {
94
+ const ours = norm(name);
95
+ const dup = connected.find((c) => {
96
+ const label = norm(c.replace(/^claude\.ai\s*/i, ""));
97
+ return label.includes(ours) || ours.includes(label);
98
+ });
99
+ if (dup)
100
+ overlap.push(`${name} ↔ "${dup}"`);
101
+ }
102
+ if (overlap.length === 0)
103
+ return;
104
+ logWarn(`Tenés ${overlap.length} connector(s) de tu cuenta con el mismo nombre que un MCP:`);
105
+ for (const o of overlap)
106
+ log(gray(` ${o}`));
107
+ log(gray(" Suelen ser el MISMO servidor visto dos veces, pero cada uno tiene"));
108
+ log(gray(" su propia autenticación, y un connector personalizado apunta a la"));
109
+ log(gray(" URL que escribió una persona: desde acá no se puede saber cuál es."));
110
+ log(gray(" Lo que importa: si el del agente no está autenticado, la respuesta"));
111
+ log(gray(" puede venir del connector sin que se note."));
112
+ log(gray(" Autenticá los del agente: /mcp → click en el servicio → Authenticate."));
113
+ log("");
114
+ }
60
115
  export function findRogueMetabase(manifest, specs = Object.values(TARGETS)) {
61
116
  const official = manifest.mcps?.metabase?.url;
62
117
  if (!official)
@@ -163,6 +218,7 @@ export async function runStatus() {
163
218
  log("");
164
219
  reportMcps(manifest, lock);
165
220
  reportRogueMetabase(manifest);
221
+ reportAccountConnectorOverlap(manifest);
166
222
  const total = drift.toInstall.length +
167
223
  drift.toUpdate.length +
168
224
  drift.mcps.toInstall.length +
@@ -96,7 +96,7 @@ async function runUpdateOnce(opts) {
96
96
  log(` ${cyan("+")} ${name} ${gray("(MCP nuevo)")}`);
97
97
  for (const name of drift.mcps.toUpdate) {
98
98
  const motivo = drift.mcps.missing.includes(name)
99
- ? "(MCP ausente en el cliente — se repone)"
99
+ ? "(MCP ausente o descartado por el cliente — se repone)"
100
100
  : "(MCP actualizado)";
101
101
  log(` ${yellow("↻")} ${name} ${gray(motivo)}`);
102
102
  }
package/dist/installer.js CHANGED
@@ -378,7 +378,26 @@ function unregisterMcpInTomlBlock(name, spec) {
378
378
  atomicWriteFile(spec.mcpConfigPath, next);
379
379
  return true;
380
380
  }
381
- export function inspectMcpInTarget(mcp, spec) {
381
+ export function renderedEntryHash(mcp, spec) {
382
+ const rendered = spec.mcpMechanism === "toml-block"
383
+ ? renderMcpToml(mcp).trim()
384
+ : stableStringify(renderMcpForTarget(mcp, spec));
385
+ return createHash("sha256").update(rendered).digest("hex");
386
+ }
387
+ function hashOf(value) {
388
+ return createHash("sha256").update(value).digest("hex");
389
+ }
390
+ function isDiscardedByClient(current, spec) {
391
+ if (!spec.mcpHttpTypeValue)
392
+ return false;
393
+ if (!current || typeof current !== "object")
394
+ return false;
395
+ const entry = current;
396
+ if (typeof entry[spec.mcpUrlKey] !== "string")
397
+ return false;
398
+ return typeof entry.type !== "string";
399
+ }
400
+ export function inspectMcpInTarget(mcp, spec, prevHash) {
382
401
  try {
383
402
  if (!existsSync(spec.mcpConfigPath))
384
403
  return "missing";
@@ -386,7 +405,10 @@ export function inspectMcpInTarget(mcp, spec) {
386
405
  const block = extractTomlBlock(readFileSync(spec.mcpConfigPath, "utf-8"), mcp.name);
387
406
  if (block === null)
388
407
  return "missing";
389
- return block.trim() === renderMcpToml(mcp).trim() ? "ok" : "modified";
408
+ const current = block.trim();
409
+ if (current === renderMcpToml(mcp).trim())
410
+ return "ok";
411
+ return prevHash && hashOf(current) === prevHash ? "stale" : "modified";
390
412
  }
391
413
  const cfg = JSON.parse(readFileSync(spec.mcpConfigPath, "utf-8"));
392
414
  const section = cfg[spec.mcpRootKey];
@@ -395,16 +417,24 @@ export function inspectMcpInTarget(mcp, spec) {
395
417
  const current = section[mcp.name];
396
418
  if (current === undefined)
397
419
  return "missing";
398
- return stableStringify(current) === stableStringify(renderMcpForTarget(mcp, spec))
399
- ? "ok"
400
- : "modified";
420
+ const currentStr = stableStringify(current);
421
+ if (currentStr === stableStringify(renderMcpForTarget(mcp, spec)))
422
+ return "ok";
423
+ if (prevHash && hashOf(currentStr) === prevHash)
424
+ return "stale";
425
+ if (isDiscardedByClient(current, spec))
426
+ return "broken";
427
+ return "modified";
401
428
  }
402
429
  catch {
403
430
  return "missing";
404
431
  }
405
432
  }
406
- function mcpMissingInTargets(mcp, targets) {
407
- return targets.filter((id) => inspectMcpInTarget(mcp, TARGETS[id]) === "missing");
433
+ function mcpNeedsRepairInTargets(mcp, targets, renderedHash) {
434
+ return targets.filter((id) => {
435
+ const state = inspectMcpInTarget(mcp, TARGETS[id], renderedHash?.[id]);
436
+ return state === "missing" || state === "stale" || state === "broken";
437
+ });
408
438
  }
409
439
  const cleanedTargets = new Set();
410
440
  function resolveTargets(opts) {
@@ -609,11 +639,14 @@ export function installMcp(mcp, opts = {}) {
609
639
  prev.targets.length === targetIds.length &&
610
640
  prev.targets.every((t) => targetIds.includes(t));
611
641
  const configChanged = !prev || prev.configHash !== mcpConfigHash(mcp);
612
- const writeTargets = opts.force || configChanged
613
- ? wantedTargets
614
- : wantedTargets.filter((t) => inspectMcpInTarget(mcp, t) === "missing");
642
+ const prevHashes = readLockFile().mcps?.[mcp.name]?.renderedHash;
643
+ const needsWrite = (t) => {
644
+ const state = inspectMcpInTarget(mcp, t, prevHashes?.[t.id]);
645
+ return state === "missing" || state === "stale" || state === "broken";
646
+ };
647
+ const writeTargets = opts.force || configChanged ? wantedTargets : wantedTargets.filter(needsWrite);
615
648
  const alreadyThere = wantedTargets
616
- .filter((t) => !writeTargets.includes(t) && inspectMcpInTarget(mcp, t) !== "missing")
649
+ .filter((t) => !writeTargets.includes(t) && !needsWrite(t))
617
650
  .map((t) => t.id);
618
651
  if (writeTargets.length === 0 && targetsMatch) {
619
652
  return { name: mcp.name, kind: "mcp", status: "unchanged", targets: targetIds };
@@ -647,12 +680,18 @@ export function installMcp(mcp, opts = {}) {
647
680
  error: failures.join("; "),
648
681
  };
649
682
  }
683
+ const renderedHash = { ...(prev?.renderedHash ?? {}) };
684
+ for (const t of writeTargets) {
685
+ if (okTargets.includes(t.id))
686
+ renderedHash[t.id] = renderedEntryHash(mcp, t);
687
+ }
650
688
  lock.mcps[mcp.name] = {
651
689
  name: mcp.name,
652
690
  installedFrom: getPackageVersion(),
653
691
  installedAt: new Date().toISOString(),
654
692
  configHash: mcpConfigHash(mcp),
655
693
  targets: okTargets,
694
+ renderedHash,
656
695
  };
657
696
  writeLockFile(lock);
658
697
  return {
@@ -828,7 +867,7 @@ export function detectDrift(manifest) {
828
867
  mcps.toUpdate.push(name);
829
868
  continue;
830
869
  }
831
- const missing = mcpMissingInTargets(manifestMcps[name], (prevMcp.targets ?? []));
870
+ const missing = mcpNeedsRepairInTargets(manifestMcps[name], (prevMcp.targets ?? []), prevMcp.renderedHash);
832
871
  if (missing.length > 0) {
833
872
  mcps.toUpdate.push(name);
834
873
  mcps.missing.push(name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alegra-data-agent",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "private": false,
5
5
  "description": "Agente de Data interno de Alegra. Instala skills + MCPs en tu cliente de IA con un comando.",
6
6
  "license": "UNLICENSED",