alegra-data-agent 0.2.7 → 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";
@@ -156,6 +158,87 @@ function reportRogueMetabase(manifest) {
156
158
  log(gray(" si no los necesitás, conviene quitarlos del cliente."));
157
159
  log("");
158
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
+ }
159
242
  export async function runStatus() {
160
243
  const auth = readAuth();
161
244
  const lockForTargets = readLockFile();
@@ -218,6 +301,7 @@ export async function runStatus() {
218
301
  log("");
219
302
  reportMcps(manifest, lock);
220
303
  reportRogueMetabase(manifest);
304
+ reportClaudeAiDuplicates(manifest);
221
305
  reportAccountConnectorOverlap(manifest);
222
306
  const total = drift.toInstall.length +
223
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.7",
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 ."