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 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
  }
@@ -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 = TARGETS["claude-code"].mcpConfigPath;
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 = TARGETS["claude-code"].mcpConfigPath) {
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
- const localServers = cfg[TARGETS["claude-code"].mcpRootKey];
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 +
@@ -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
- : "(MCP actualizado)";
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 { adaptStdioCommand, ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getCursorLegacyRulesDir, getHermesBin, getLockFilePath, 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";
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
- writeFileSync(tmp, content);
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 readJsonOrEmpty(path) {
250
- if (!existsSync(path))
265
+ function parseJsonConfig(path, raw) {
266
+ if (raw === null)
251
267
  return {};
252
268
  try {
253
- return JSON.parse(readFileSync(path, "utf-8"));
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
- const cfg = readJsonOrEmpty(spec.mcpConfigPath);
270
- const section = cfg[spec.mcpRootKey] ?? {};
271
- section[mcp.name] = renderMcpForTarget(mcp, spec);
272
- cfg[spec.mcpRootKey] = section;
273
- backupIfExists(spec.mcpConfigPath);
274
- atomicWriteFile(spec.mcpConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
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 cfg = readJsonOrEmpty(spec.mcpConfigPath);
286
- const section = cfg[spec.mcpRootKey];
287
- if (!section || !(mcpName in section))
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
- delete section[mcpName];
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 } = adaptStdioCommand(mcp.command ?? "", mcp.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
- cfg = JSON.parse(readFileSync(path, "utf-8"));
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 wantedTargets = resolveTargets(opts).filter((t) => !mcp.targets || mcp.targets.includes(t.id));
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) && (!mcp.targets || mcp.targets.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: failures.length > 0 ? `targets con fallo — ${failures.join("; ")}` : undefined,
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
- if (missing.length > 0) {
1358
+ const outOfPlace = mcpOutOfPlace(manifestMcps[name], prevMcp);
1359
+ if (missing.length > 0 || outOfPlace.length > 0) {
1083
1360
  mcps.toUpdate.push(name);
1084
- mcps.missing.push(name);
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);
@@ -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 getClaudeJsonPath();
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 { adaptStdioCommand, parseFrontmatter } from "./lib.js";
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 } = adaptStdioCommand(mcp.command ?? "", mcp.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 } = adaptStdioCommand(mcp.command ?? "", mcp.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
- if (obj === null || typeof obj !== "object")
16
- return JSON.stringify(obj);
17
- if (Array.isArray(obj)) {
18
- return `[${obj.map((v) => canonicalize(v)).join(",")}]`;
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 = obj;
23
+ const o = value;
21
24
  const keys = Object.keys(o)
22
- .filter((k) => k !== "signature")
25
+ .filter((k) => !(isRoot && k === "signature"))
23
26
  .sort();
24
- return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(o[k])}`).join(",")}}`;
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, getClaudeJsonPath, getClaudeSkillsDir, getCodexConfigPath, getCodexSkillsDir, getCursorMcpPath, getCursorSkillsDir, getHermesConfigPath, getHermesHome, getHermesSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
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: getClaudeJsonPath(),
12
+ mcpConfigPath: getClaudeMcpConfigPath(),
13
13
  mcpRootKey: "mcpServers",
14
14
  mcpUrlKey: "url",
15
15
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alegra-data-agent",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
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",