alegra-data-agent 0.4.1 → 0.4.2

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
  }
@@ -5,7 +5,7 @@ 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
7
  import { detectDrift, inspectMcpInTarget } from "../installer.js";
8
- import { COMMAND_NAME, GATEWAY_URL, getAutoUpdateLogPath, getClaudeSkillsDir, getLockFilePath, getPackageVersion, getTargetSkillsDir, readLockFile, } from "../lib.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 = [];
@@ -180,7 +180,7 @@ function connectorUrlsFromCredentials(ourServerNames, credentialsPath = join(hom
180
180
  }
181
181
  return urls;
182
182
  }
183
- export function findClaudeAiDuplicates(manifest, claudeJsonPath = TARGETS["claude-code"].mcpConfigPath) {
183
+ export function findClaudeAiDuplicates(manifest, claudeJsonPath = getClaudeJsonPath(), mcpConfigPath = claudeJsonPath) {
184
184
  if (!existsSync(claudeJsonPath))
185
185
  return [];
186
186
  let cfg;
@@ -193,7 +193,18 @@ export function findClaudeAiDuplicates(manifest, claudeJsonPath = TARGETS["claud
193
193
  const connectors = cfg.claudeAiMcpEverConnected;
194
194
  if (!Array.isArray(connectors))
195
195
  return [];
196
- const localServers = cfg[TARGETS["claude-code"].mcpRootKey];
196
+ let mcpCfg = cfg;
197
+ if (mcpConfigPath !== claudeJsonPath) {
198
+ if (!existsSync(mcpConfigPath))
199
+ return [];
200
+ try {
201
+ mcpCfg = JSON.parse(readFileSync(mcpConfigPath, "utf-8"));
202
+ }
203
+ catch {
204
+ return [];
205
+ }
206
+ }
207
+ const localServers = mcpCfg[TARGETS["claude-code"].mcpRootKey];
197
208
  if (!localServers || typeof localServers !== "object")
198
209
  return [];
199
210
  const ourNames = new Set(Object.keys(localServers));
@@ -222,7 +233,7 @@ export function findClaudeAiDuplicates(manifest, claudeJsonPath = TARGETS["claud
222
233
  return out;
223
234
  }
224
235
  function reportClaudeAiDuplicates(manifest) {
225
- const dupes = findClaudeAiDuplicates(manifest);
236
+ const dupes = findClaudeAiDuplicates(manifest, getClaudeJsonPath(), TARGETS["claude-code"].mcpConfigPath);
226
237
  if (dupes.length === 0)
227
238
  return;
228
239
  logWarn(`Hay ${dupes.length} MCP(s) que tenés por dos vías: server local y conector de claude.ai.`);
@@ -259,6 +270,14 @@ export async function runStatus() {
259
270
  log(` ${gray("Autenticado:")} ${auth ? green(auth.email) : gray("no")}`);
260
271
  log(` ${gray("Destino:")} ${destino}`);
261
272
  log(` ${gray("Lock file:")} ${getLockFilePath()}`);
273
+ log(` ${gray("MCPs en:")} ${TARGETS["claude-code"].mcpConfigPath}`);
274
+ const scopeDir = getMcpScopeDir();
275
+ if (scopeDir) {
276
+ log(` ${gray("Scope MCPs:")} proyecto ${dim(`(solo dentro de ${scopeDir})`)}`);
277
+ }
278
+ const scopeWarning = getMcpScopeWarning();
279
+ if (scopeWarning)
280
+ logWarn(`${scopeWarning} — los MCPs quedaron en scope usuario.`);
262
281
  log("");
263
282
  reportAutoUpdateLog();
264
283
  if (!auth) {
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, 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";
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,55 @@ 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) {
292
+ if (spec.id !== "claude-code")
293
+ return;
294
+ const dir = getMcpScopeDir();
295
+ if (!dir)
296
+ return;
297
+ const settingsPath = join(dir, ".claude", "settings.json");
298
+ try {
299
+ updateJsonFile(settingsPath, (cfg) => {
300
+ const current = Array.isArray(cfg.enabledMcpjsonServers)
301
+ ? cfg.enabledMcpjsonServers.filter((v) => typeof v === "string")
302
+ : [];
303
+ if (current.includes(name) === approved)
304
+ return false;
305
+ cfg.enabledMcpjsonServers = approved
306
+ ? [...current, name].sort()
307
+ : current.filter((n) => n !== name);
308
+ return true;
309
+ });
310
+ }
311
+ catch {
312
+ }
313
+ }
259
314
  function registerMcpInTarget(mcp, spec) {
260
315
  validateMcpName(mcp.name);
261
316
  if (spec.mcpMechanism === "hermes-cli") {
@@ -266,12 +321,13 @@ function registerMcpInTarget(mcp, spec) {
266
321
  registerMcpInTomlBlock(mcp, spec);
267
322
  return;
268
323
  }
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`);
324
+ updateJsonFile(spec.mcpConfigPath, (cfg) => {
325
+ const section = cfg[spec.mcpRootKey] ?? {};
326
+ section[mcp.name] = renderMcpForTarget(mcp, spec);
327
+ cfg[spec.mcpRootKey] = section;
328
+ return true;
329
+ });
330
+ setClaudeProjectMcpApproval(mcp.name, spec, true);
275
331
  }
276
332
  function unregisterMcpInTarget(mcpName, spec) {
277
333
  if (spec.mcpMechanism === "hermes-cli") {
@@ -282,14 +338,17 @@ function unregisterMcpInTarget(mcpName, spec) {
282
338
  }
283
339
  if (!existsSync(spec.mcpConfigPath))
284
340
  return false;
285
- const cfg = readJsonOrEmpty(spec.mcpConfigPath);
286
- const section = cfg[spec.mcpRootKey];
287
- if (!section || !(mcpName in section))
341
+ const removed = updateJsonFile(spec.mcpConfigPath, (cfg) => {
342
+ const section = cfg[spec.mcpRootKey];
343
+ if (!section || !(mcpName in section))
344
+ return false;
345
+ delete section[mcpName];
346
+ cfg[spec.mcpRootKey] = section;
347
+ return true;
348
+ });
349
+ if (!removed)
288
350
  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`);
351
+ setClaudeProjectMcpApproval(mcpName, spec, false);
293
352
  return true;
294
353
  }
295
354
  const HERMES_CLI_TIMEOUT_MS = 20_000;
@@ -439,7 +498,7 @@ function tomlBasicString(s) {
439
498
  }
440
499
  export function renderMcpToml(mcp) {
441
500
  if (mcp.transport === "stdio") {
442
- const { command, args } = adaptStdioCommand(mcp.command ?? "", mcp.args ?? []);
501
+ const { command, args } = stdioLaunch(mcp);
443
502
  const lines = [`[mcp_servers.${mcp.name}]`, `command = ${tomlBasicString(command)}`];
444
503
  if (args.length > 0) {
445
504
  lines.push(`args = [${args.map((a) => tomlBasicString(a)).join(", ")}]`);
@@ -619,31 +678,27 @@ const LEGACY_RENAMED_SKILLS = ["alanube", "alegra-producto", "alegra-ce-universe
619
678
  function stripMcpEntries(path, names) {
620
679
  if (!existsSync(path) || names.length === 0)
621
680
  return;
622
- let cfg;
623
681
  try {
624
- cfg = JSON.parse(readFileSync(path, "utf-8"));
682
+ updateJsonFile(path, (cfg) => {
683
+ let changed = false;
684
+ for (const key of ["mcpServers", "mcp"]) {
685
+ const section = cfg[key];
686
+ if (!section || typeof section !== "object")
687
+ continue;
688
+ for (const n of names) {
689
+ if (n in section) {
690
+ delete section[n];
691
+ changed = true;
692
+ }
693
+ }
694
+ if (Object.keys(section).length === 0)
695
+ delete cfg[key];
696
+ }
697
+ return changed;
698
+ });
625
699
  }
626
700
  catch {
627
- return;
628
701
  }
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
- }
643
- if (!changed)
644
- return;
645
- backupIfExists(path);
646
- atomicWriteFile(path, `${JSON.stringify(cfg, null, 2)}\n`);
647
702
  }
648
703
  function cleanupLegacySkillArtifacts(spec) {
649
704
  try {
@@ -839,6 +894,19 @@ export function installMcp(mcp, opts = {}) {
839
894
  error: "ningún target soportado disponible",
840
895
  };
841
896
  }
897
+ if (mcp.lazy && !opts.dryRun) {
898
+ try {
899
+ ensureLazyProxy(mcp);
900
+ }
901
+ catch (err) {
902
+ return {
903
+ name: mcp.name,
904
+ kind: "mcp",
905
+ status: "failed",
906
+ error: `no se pudo escribir el proxy de arranque diferido: ${err instanceof Error ? err.message : String(err)}`,
907
+ };
908
+ }
909
+ }
842
910
  const prev = lock.mcps[mcp.name];
843
911
  const targetsMatch = prev?.targets &&
844
912
  prev.targets.length === targetIds.length &&
@@ -936,6 +1004,15 @@ export async function installAll(opts = {}) {
936
1004
  const mcp = manifest.mcps?.[name];
937
1005
  if (!mcp)
938
1006
  continue;
1007
+ if (mcp.name !== name) {
1008
+ results.push({
1009
+ name,
1010
+ kind: "mcp",
1011
+ status: "failed",
1012
+ error: `la entry "${name}" declara name "${mcp.name}"`,
1013
+ });
1014
+ continue;
1015
+ }
939
1016
  results.push(installMcp(mcp, { ...opts, targets }));
940
1017
  }
941
1018
  return { manifest, results, targets };
@@ -1032,6 +1109,7 @@ export function uninstallAll(opts = {}) {
1032
1109
  catch {
1033
1110
  }
1034
1111
  }
1112
+ rmSync(lazyMcpDir(), { recursive: true, force: true });
1035
1113
  }
1036
1114
  return results;
1037
1115
  }
@@ -0,0 +1,153 @@
1
+ import { existsSync, mkdirSync, readFileSync, 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({ name: mcp.name, tools: mcp.lazy.tools }, null, 2)}\n`);
29
+ }
30
+ function writeIfChanged(path, content) {
31
+ if (existsSync(path) && readFileSync(path, "utf-8") === content)
32
+ return;
33
+ writeFileSync(path, content, { mode: 0o644 });
34
+ }
35
+ export function lazyProxySource() {
36
+ const product = JSON.stringify(COMMAND_NAME);
37
+ return `#!/usr/bin/env node
38
+ // Proxy de arranque diferido de ${COMMAND_NAME}. Generado por el instalador: no
39
+ // editar, se reescribe en cada install/update. Uso:
40
+ // node lazy-mcp-proxy.mjs <tools.json> -- <comando real> [args…]
41
+ import { spawn } from "node:child_process";
42
+ import { readFileSync } from "node:fs";
43
+ import { createInterface } from "node:readline";
44
+
45
+ const argv = process.argv.slice(2);
46
+ const sep = argv.indexOf("--");
47
+ if (sep !== 1 || argv.length < 3) {
48
+ process.stderr.write("uso: lazy-mcp-proxy.mjs <tools.json> -- <comando> [args…]\\n");
49
+ process.exit(2);
50
+ }
51
+ const spec = JSON.parse(readFileSync(argv[0], "utf8"));
52
+ const [cmd, ...args] = argv.slice(2);
53
+ const INIT_ID = "__alegra_lazy_init__";
54
+
55
+ const send = (m) => process.stdout.write(JSON.stringify(m) + "\\n");
56
+ let initParams = null;
57
+ let child = null;
58
+ let ready = false;
59
+ let dead = null;
60
+ const queue = [];
61
+
62
+ function errorFor(m, reason) {
63
+ if (m.method === "tools/call") {
64
+ return { jsonrpc: "2.0", id: m.id, result: { content: [{ type: "text", text: reason }], isError: true } };
65
+ }
66
+ return { jsonrpc: "2.0", id: m.id, error: { code: -32603, message: reason } };
67
+ }
68
+
69
+ function die(reason) {
70
+ dead = reason;
71
+ ready = false;
72
+ for (const m of queue.splice(0)) send(errorFor(m, reason));
73
+ }
74
+
75
+ function start() {
76
+ if (child || dead) return;
77
+ child = spawn(cmd, args, { stdio: ["pipe", "pipe", "inherit"] });
78
+ child.on("error", (e) => die("No se pudo arrancar el MCP " + spec.name + ": " + e.message));
79
+ child.on("exit", (code, signal) => {
80
+ if (!dead) die("El MCP " + spec.name + " terminó (" + (signal ?? "código " + code) + "). Reintentá en una sesión nueva; si persiste, corré " + ${product} + " status.");
81
+ });
82
+ createInterface({ input: child.stdout }).on("line", (line) => {
83
+ if (ready) {
84
+ process.stdout.write(line + "\\n");
85
+ return;
86
+ }
87
+ let m;
88
+ try { m = JSON.parse(line); } catch { return; }
89
+ if (m.id !== INIT_ID) return; // notificaciones previas al handshake
90
+ if (m.error) {
91
+ die("El MCP " + spec.name + " rechazó el handshake: " + (m.error.message ?? "sin detalle"));
92
+ child.kill();
93
+ return;
94
+ }
95
+ ready = true;
96
+ child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\\n");
97
+ for (const q of queue.splice(0)) child.stdin.write(JSON.stringify(q) + "\\n");
98
+ });
99
+ child.stdin.on("error", () => {});
100
+ child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: INIT_ID, method: "initialize", params: initParams }) + "\\n");
101
+ }
102
+
103
+ createInterface({ input: process.stdin }).on("line", (line) => {
104
+ if (ready) {
105
+ child.stdin.write(line + "\\n");
106
+ return;
107
+ }
108
+ let m;
109
+ try { m = JSON.parse(line); } catch { return; }
110
+ switch (m.method) {
111
+ case "initialize":
112
+ initParams = m.params ?? {};
113
+ send({
114
+ jsonrpc: "2.0",
115
+ id: m.id,
116
+ result: {
117
+ protocolVersion: initParams.protocolVersion ?? "2025-06-18",
118
+ capabilities: { tools: {} },
119
+ serverInfo: { name: spec.name, version: "lazy" },
120
+ },
121
+ });
122
+ return;
123
+ case "ping":
124
+ send({ jsonrpc: "2.0", id: m.id, result: {} });
125
+ return;
126
+ case "tools/list":
127
+ send({ jsonrpc: "2.0", id: m.id, result: { tools: spec.tools } });
128
+ return;
129
+ case "resources/list":
130
+ send({ jsonrpc: "2.0", id: m.id, result: { resources: [] } });
131
+ return;
132
+ case "resources/templates/list":
133
+ send({ jsonrpc: "2.0", id: m.id, result: { resourceTemplates: [] } });
134
+ return;
135
+ case "prompts/list":
136
+ send({ jsonrpc: "2.0", id: m.id, result: { prompts: [] } });
137
+ return;
138
+ }
139
+ if (m.id === undefined || m.id === null) return; // notificación: nada que contestar
140
+ if (dead) {
141
+ send(errorFor(m, dead));
142
+ return;
143
+ }
144
+ queue.push(m);
145
+ start();
146
+ });
147
+
148
+ process.stdin.on("end", () => {
149
+ if (child) child.kill();
150
+ process.exit(0);
151
+ });
152
+ `;
153
+ }
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,78 @@ 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 getMcpScopeWarning() {
92
+ return resolveMcpScope().warning;
93
+ }
94
+ export function getMcpScopeDir() {
95
+ return resolveMcpScope().dir;
96
+ }
97
+ function mcpScopedPath(relPath, fallback) {
98
+ const dir = getMcpScopeDir();
99
+ return dir ? join(dir, relPath) : fallback;
100
+ }
101
+ export function getClaudeMcpConfigPath() {
102
+ return mcpScopedPath(".mcp.json", getClaudeJsonPath());
103
+ }
32
104
  export function getCursorSkillsDir() {
33
105
  return join(homedir(), ".cursor", "skills");
34
106
  }
@@ -36,7 +108,7 @@ export function getCursorLegacyRulesDir() {
36
108
  return join(homedir(), ".cursor", "rules");
37
109
  }
38
110
  export function getCursorMcpPath() {
39
- return join(homedir(), ".cursor", "mcp.json");
111
+ return mcpScopedPath(join(".cursor", "mcp.json"), join(homedir(), ".cursor", "mcp.json"));
40
112
  }
41
113
  export function getOpenCodeSkillsDir() {
42
114
  const xdgHome = process.env.XDG_CONFIG_HOME;
@@ -137,7 +209,7 @@ export function getTargetSkillsDir(target) {
137
209
  export function getTargetMcpConfigPath(target) {
138
210
  switch (target) {
139
211
  case "claude-code":
140
- return getClaudeJsonPath();
212
+ return getClaudeMcpConfigPath();
141
213
  case "cursor":
142
214
  return getCursorMcpPath();
143
215
  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.2",
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",