alegra-data-agent 0.2.6 → 0.2.8

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.
@@ -1,4 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
2
4
  import { AuthRequiredError, fetchManifest } from "../api.js";
3
5
  import { readAuth } from "../auth.js";
4
6
  import { bold, cyan, dim, gray, green, log, logError, logWarn, yellow } from "../colors.js";
@@ -101,13 +103,15 @@ function reportAccountConnectorOverlap(manifest) {
101
103
  }
102
104
  if (overlap.length === 0)
103
105
  return;
104
- logWarn(`Tenés ${overlap.length} connector(s) de tu cuenta que duplican un MCP del agente:`);
106
+ logWarn(`Tenés ${overlap.length} connector(s) de tu cuenta con el mismo nombre que un MCP:`);
105
107
  for (const o of overlap)
106
108
  log(gray(` ${o}`));
107
- log(gray(" Son servidores distintos, cada uno con su propia autenticación."));
108
- log(gray(" Si el oficial no está autenticado, el agente puede responder por el"));
109
- log(gray(" tuyo — y un connector personalizado apunta a donde alguien escribió."));
110
- log(gray(` Autenticá los del agente: /mcp → click en el servicio → Authenticate.`));
109
+ log(gray(" Suelen ser el MISMO servidor visto dos veces, pero cada uno tiene"));
110
+ log(gray(" su propia autenticación, y un connector personalizado apunta a la"));
111
+ log(gray(" URL que escribió una persona: desde acá no se puede saber cuál es."));
112
+ log(gray(" Lo que importa: si el del agente no está autenticado, la respuesta"));
113
+ log(gray(" puede venir del connector sin que se note."));
114
+ log(gray(" Autenticá los del agente: /mcp → click en el servicio → Authenticate."));
111
115
  log("");
112
116
  }
113
117
  export function findRogueMetabase(manifest, specs = Object.values(TARGETS)) {
@@ -154,6 +158,87 @@ function reportRogueMetabase(manifest) {
154
158
  log(gray(" si no los necesitás, conviene quitarlos del cliente."));
155
159
  log("");
156
160
  }
161
+ function connectorUrlsFromCredentials(ourServerNames, credentialsPath = join(homedir(), ".claude", ".credentials.json")) {
162
+ const urls = new Set();
163
+ if (!existsSync(credentialsPath))
164
+ return urls;
165
+ try {
166
+ const raw = JSON.parse(readFileSync(credentialsPath, "utf-8"));
167
+ const entries = raw.mcpOAuth;
168
+ if (!entries || typeof entries !== "object")
169
+ return urls;
170
+ for (const [key, val] of Object.entries(entries)) {
171
+ const name = key.split("|")[0];
172
+ if (ourServerNames.has(name))
173
+ continue;
174
+ const url = val?.serverUrl;
175
+ if (typeof url === "string")
176
+ urls.add(url.trim().replace(/\/+$/, "").toLowerCase());
177
+ }
178
+ }
179
+ catch {
180
+ }
181
+ return urls;
182
+ }
183
+ export function findClaudeAiDuplicates(manifest, claudeJsonPath = TARGETS["claude-code"].mcpConfigPath) {
184
+ if (!existsSync(claudeJsonPath))
185
+ return [];
186
+ let cfg;
187
+ try {
188
+ cfg = JSON.parse(readFileSync(claudeJsonPath, "utf-8"));
189
+ }
190
+ catch {
191
+ return [];
192
+ }
193
+ const connectors = cfg.claudeAiMcpEverConnected;
194
+ if (!Array.isArray(connectors))
195
+ return [];
196
+ const localServers = cfg[TARGETS["claude-code"].mcpRootKey];
197
+ if (!localServers || typeof localServers !== "object")
198
+ return [];
199
+ const ourNames = new Set(Object.keys(localServers));
200
+ const connectorUrls = connectorUrlsFromCredentials(ourNames);
201
+ const norm = (u) => u.trim().replace(/\/+$/, "").toLowerCase();
202
+ const flat = (v) => v.toLowerCase().replace(/[^a-z0-9]/g, "");
203
+ const out = [];
204
+ for (const name of ourNames) {
205
+ const official = manifest.mcps?.[name]?.url;
206
+ if (!manifest.mcps?.[name])
207
+ continue;
208
+ const needle = flat(name);
209
+ for (const c of connectors) {
210
+ if (typeof c !== "string")
211
+ continue;
212
+ const hay = flat(c.replace(/^claude\.ai\s*/i, ""));
213
+ if (!hay.includes(needle))
214
+ continue;
215
+ out.push({
216
+ mcp: name,
217
+ connector: c,
218
+ sameUrl: typeof official === "string" && connectorUrls.has(norm(official)),
219
+ });
220
+ }
221
+ }
222
+ return out;
223
+ }
224
+ function reportClaudeAiDuplicates(manifest) {
225
+ const dupes = findClaudeAiDuplicates(manifest);
226
+ if (dupes.length === 0)
227
+ return;
228
+ logWarn(`Hay ${dupes.length} MCP(s) que tenés por dos vías: server local y conector de claude.ai.`);
229
+ for (const d of dupes) {
230
+ const marca = d.sameUrl
231
+ ? " — mismo servidor (misma URL)"
232
+ : " — mismo nombre, URL sin verificar";
233
+ log(gray(` ${d.mcp} (local) · ${d.connector}${marca}`));
234
+ }
235
+ log(gray(" En Claude Code gana el local y el conector queda oculto, así que"));
236
+ log(gray(" hoy no se pisan — pero autenticás dos veces el mismo servidor."));
237
+ log(gray(" Si preferís el conector (una sola auth, y sirve también en Desktop,"));
238
+ log(gray(" claude.ai, Cowork y móvil): claude mcp remove <nombre>."));
239
+ log(gray(" Verificá antes que apunte a la MISMA URL que el oficial."));
240
+ log("");
241
+ }
157
242
  export async function runStatus() {
158
243
  const auth = readAuth();
159
244
  const lockForTargets = readLockFile();
@@ -216,6 +301,7 @@ export async function runStatus() {
216
301
  log("");
217
302
  reportMcps(manifest, lock);
218
303
  reportRogueMetabase(manifest);
304
+ reportClaudeAiDuplicates(manifest);
219
305
  reportAccountConnectorOverlap(manifest);
220
306
  const total = drift.toInstall.length +
221
307
  drift.toUpdate.length +
package/dist/installer.js CHANGED
@@ -41,6 +41,7 @@ function mcpConfigHash(mcp) {
41
41
  env: mcp.env ?? null,
42
42
  type: mcp.auth.type,
43
43
  tokenEnv: mcp.auth.tokenEnv ?? null,
44
+ ...(mcp.delivery ? { delivery: mcp.delivery } : {}),
44
45
  });
45
46
  return createHash("sha256").update(salient).digest("hex");
46
47
  }
@@ -624,6 +625,43 @@ export function installMcp(mcp, opts = {}) {
624
625
  const lock = readLockFile();
625
626
  if (!lock.mcps)
626
627
  lock.mcps = {};
628
+ if (mcp.delivery === "connector") {
629
+ const hint = mcp.connectorHint ?? "se habilita como conector de la cuenta, no como server local";
630
+ const stale = lock.mcps[mcp.name];
631
+ if (!stale)
632
+ return { name: mcp.name, kind: "mcp", status: "skipped", error: hint };
633
+ const staleTargets = (stale.targets ?? ["claude-code"]);
634
+ if (opts.dryRun) {
635
+ return {
636
+ name: mcp.name,
637
+ kind: "mcp",
638
+ status: "skipped",
639
+ targets: staleTargets,
640
+ error: `${hint} (se retirará la entry local)`,
641
+ };
642
+ }
643
+ try {
644
+ for (const tId of staleTargets)
645
+ unregisterMcpInTarget(mcp.name, TARGETS[tId]);
646
+ }
647
+ catch (err) {
648
+ return {
649
+ name: mcp.name,
650
+ kind: "mcp",
651
+ status: "failed",
652
+ error: err instanceof Error ? err.message : String(err),
653
+ };
654
+ }
655
+ delete lock.mcps[mcp.name];
656
+ writeLockFile(lock);
657
+ return {
658
+ name: mcp.name,
659
+ kind: "mcp",
660
+ status: "skipped",
661
+ targets: staleTargets,
662
+ error: `entry local retirada — ${hint}`,
663
+ };
664
+ }
627
665
  const wantedTargets = resolveTargets(opts).filter((t) => !mcp.targets || mcp.targets.includes(t.id));
628
666
  const targetIds = wantedTargets.map((t) => t.id);
629
667
  if (targetIds.length === 0) {
@@ -858,6 +896,8 @@ export function detectDrift(manifest) {
858
896
  };
859
897
  const manifestMcps = manifest.mcps ?? {};
860
898
  for (const name of Object.keys(manifestMcps)) {
899
+ if (manifestMcps[name].delivery === "connector")
900
+ continue;
861
901
  const prevMcp = lock.mcps?.[name];
862
902
  if (!prevMcp) {
863
903
  mcps.toInstall.push(name);
@@ -916,6 +956,11 @@ export function summarize(results) {
916
956
  logError(`${r.name}: ${r.error}`);
917
957
  }
918
958
  }
959
+ for (const r of results) {
960
+ if (r.status === "skipped" && r.error) {
961
+ process.stdout.write(yellow(` - ${r.name}: ${r.error}\n`));
962
+ }
963
+ }
919
964
  process.stdout.write(dim(` Lock: ${getLockFilePath()}\n`));
920
965
  process.stdout.write("\n");
921
966
  }
package/dist/main.js CHANGED
@@ -118,7 +118,6 @@ async function main() {
118
118
  break;
119
119
  case "install":
120
120
  exitCode = await runInstall({
121
- yes: args.flags.yes,
122
121
  dryRun: args.flags.dryRun,
123
122
  force: args.flags.force,
124
123
  only: args.only.length > 0 ? args.only : undefined,
@@ -129,7 +128,6 @@ async function main() {
129
128
  break;
130
129
  case "update":
131
130
  exitCode = await runUpdate({
132
- yes: args.flags.yes,
133
131
  dryRun: args.flags.dryRun,
134
132
  background: args.flags.background,
135
133
  targets: args.targets.length > 0 ? args.targets : undefined,
package/dist/ui.js CHANGED
@@ -56,7 +56,7 @@ ${bold("Comandos:")}
56
56
  version Muestra la versión
57
57
 
58
58
  ${bold("Opciones:")}
59
- -y, --yes No pedir confirmación
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
62
  --target=NAME Restringir al cliente NAME (claude-code|cursor|opencode|antigravity|codex)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alegra-data-agent",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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",
@@ -47,6 +47,7 @@
47
47
  "test:all": "pnpm run test && pnpm run test:security",
48
48
  "eval:run": "node tests/evals/run-phase2.mjs",
49
49
  "verify:clients": "node scripts/verify-clients.mjs",
50
+ "pack:web": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/pack-skills-web.mjs",
50
51
  "lint": "pnpm dlx @biomejs/biome@1.9.4 check .",
51
52
  "lint:fix": "pnpm dlx @biomejs/biome@1.9.4 check --write .",
52
53
  "format": "pnpm dlx @biomejs/biome@1.9.4 format --write ."