alegra-data-agent 0.2.4 → 0.2.6

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/api.js CHANGED
@@ -1,5 +1,7 @@
1
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
1
3
  import { readAuth } from "./auth.js";
2
- import { COMMAND_NAME, GATEWAY_URL, getPackageVersion } from "./lib.js";
4
+ import { COMMAND_NAME, GATEWAY_URL, getManifestCachePath, getPackageVersion } from "./lib.js";
3
5
  import { verifyManifestSignature } from "./signature.js";
4
6
  export class ManifestSignatureError extends Error {
5
7
  constructor(reason) {
@@ -29,7 +31,7 @@ function newRequestId() {
29
31
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
30
32
  }
31
33
  function userAgent() {
32
- return `${COMMAND_NAME}/${getPackageVersion()} (+https://github.com/alegradev/alegra-data-agent)`;
34
+ return `${COMMAND_NAME}/${getPackageVersion()} (+https://github.com/Alegra-Team/data-agent-selfservice)`;
33
35
  }
34
36
  function sleep(ms) {
35
37
  return new Promise((r) => setTimeout(r, ms));
@@ -63,6 +65,7 @@ async function authedFetch(path, opts = {}) {
63
65
  Accept: "application/json,application/octet-stream",
64
66
  "User-Agent": userAgent(),
65
67
  "X-Request-Id": reqId,
68
+ ...opts.headers,
66
69
  },
67
70
  signal: ac.signal,
68
71
  });
@@ -109,6 +112,8 @@ async function authedFetch(path, opts = {}) {
109
112
  }
110
113
  throw new GatewayError(res.status, path, `Gateway con error ${res.status}. Reintenta en unos segundos. Si persiste, reporta a #data (request-id ${reqId}).`);
111
114
  }
115
+ if (res.status === 304 && opts.allowNotModified)
116
+ return res;
112
117
  if (!res.ok) {
113
118
  throw new GatewayError(res.status, path, `Gateway respondió ${res.status} en ${path}`);
114
119
  }
@@ -158,16 +163,76 @@ function mapNetworkError(err, path, timeoutMs) {
158
163
  }
159
164
  return new GatewayError(0, path, `Error de red al hablar con el gateway: ${err.message}`);
160
165
  }
166
+ export function parseManifestCache(raw) {
167
+ try {
168
+ const parsed = JSON.parse(raw);
169
+ if (typeof parsed !== "object" || parsed === null)
170
+ return null;
171
+ const { etag, manifest } = parsed;
172
+ if (typeof etag !== "string" || etag.length === 0)
173
+ return null;
174
+ if (typeof manifest !== "object" || manifest === null)
175
+ return null;
176
+ return { etag, manifest: manifest };
177
+ }
178
+ catch {
179
+ return null;
180
+ }
181
+ }
182
+ function readManifestCache() {
183
+ try {
184
+ return parseManifestCache(readFileSync(getManifestCachePath(), "utf-8"));
185
+ }
186
+ catch {
187
+ return null;
188
+ }
189
+ }
190
+ function writeManifestCache(etag, manifest) {
191
+ const path = getManifestCachePath();
192
+ try {
193
+ mkdirSync(dirname(path), { recursive: true });
194
+ const tmp = `${path}.${process.pid}.tmp`;
195
+ writeFileSync(tmp, `${JSON.stringify({ etag, manifest })}\n`, { mode: 0o600 });
196
+ try {
197
+ renameSync(tmp, path);
198
+ }
199
+ catch (err) {
200
+ rmSync(tmp, { force: true });
201
+ throw err;
202
+ }
203
+ }
204
+ catch {
205
+ }
206
+ }
207
+ function verifiedOrThrow(manifest) {
208
+ const check = verifyManifestSignature(manifest, { softFail: false });
209
+ if (!check.ok) {
210
+ throw new ManifestSignatureError(check.reason);
211
+ }
212
+ return manifest;
213
+ }
161
214
  export async function fetchManifest(opts = {}) {
162
215
  try {
163
- const res = await authedFetch("/api/manifest", opts);
216
+ const cached = readManifestCache();
217
+ const res = await authedFetch("/api/manifest", {
218
+ ...opts,
219
+ allowNotModified: true,
220
+ headers: cached ? { ...opts.headers, "If-None-Match": cached.etag } : opts.headers,
221
+ });
222
+ if (res.status === 304) {
223
+ if (cached)
224
+ return verifiedOrThrow(cached.manifest);
225
+ const retry = await authedFetch("/api/manifest", { ...opts, allowNotModified: false });
226
+ if (!retry.ok)
227
+ return null;
228
+ return verifiedOrThrow((await retry.json()));
229
+ }
164
230
  if (!res.ok)
165
231
  return null;
166
- const manifest = (await res.json());
167
- const check = verifyManifestSignature(manifest, { softFail: false });
168
- if (!check.ok) {
169
- throw new ManifestSignatureError(check.reason);
170
- }
232
+ const manifest = verifiedOrThrow((await res.json()));
233
+ const etag = res.headers.get("etag");
234
+ if (etag)
235
+ writeManifestCache(etag, manifest);
171
236
  return manifest;
172
237
  }
173
238
  catch (err) {
@@ -185,8 +250,3 @@ export async function fetchSkillFile(skillName, relPath) {
185
250
  const buffer = await res.arrayBuffer();
186
251
  return new Uint8Array(buffer);
187
252
  }
188
- export async function fetchMcps(opts = {}) {
189
- const res = await authedFetch("/api/mcps", opts);
190
- const body = (await res.json());
191
- return body.mcps ?? {};
192
- }
package/dist/auth.js CHANGED
@@ -25,19 +25,26 @@ function constantTimeStringEq(a, b) {
25
25
  return false;
26
26
  return timingSafeEqual(ab, bb);
27
27
  }
28
+ let _authCache = null;
28
29
  export function readAuth() {
30
+ if (_authCache)
31
+ return _authCache.value;
29
32
  const path = getAuthFilePath();
30
- if (!existsSync(path))
31
- return null;
32
- try {
33
- return JSON.parse(readFileSync(path, "utf-8"));
34
- }
35
- catch {
36
- return null;
33
+ let value = null;
34
+ if (existsSync(path)) {
35
+ try {
36
+ value = JSON.parse(readFileSync(path, "utf-8"));
37
+ }
38
+ catch {
39
+ value = null;
40
+ }
37
41
  }
42
+ _authCache = { value };
43
+ return value;
38
44
  }
39
45
  export function writeAuth(record) {
40
46
  const path = getAuthFilePath();
47
+ _authCache = { value: record };
41
48
  mkdirSync(dirname(path), { recursive: true });
42
49
  writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
43
50
  try {
@@ -48,6 +55,7 @@ export function writeAuth(record) {
48
55
  }
49
56
  export function clearAuth() {
50
57
  const path = getAuthFilePath();
58
+ _authCache = { value: null };
51
59
  if (existsSync(path))
52
60
  rmSync(path, { force: true });
53
61
  }
@@ -278,6 +286,13 @@ function htmlPage(kind, subject) {
278
286
  </body>
279
287
  </html>`;
280
288
  }
289
+ const HTML_ESCAPES = {
290
+ "&": "&amp;",
291
+ "<": "&lt;",
292
+ ">": "&gt;",
293
+ '"': "&quot;",
294
+ "'": "&#39;",
295
+ };
281
296
  function escapeHtml(s) {
282
- return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
297
+ return s.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c] ?? c);
283
298
  }
@@ -2,16 +2,21 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "
2
2
  import { dirname } from "node:path";
3
3
  import { logError, logOk, logWarn } from "../colors.js";
4
4
  import { COMMAND_NAME, PRODUCT_NAME, getClaudeSettingsPath, getPackageVersion } from "../lib.js";
5
+ function versionRangeForHook(version) {
6
+ const [rawMajor] = version.split(".");
7
+ const major = Number.parseInt(rawMajor ?? "0", 10);
8
+ if (!Number.isFinite(major))
9
+ return "latest";
10
+ return major === 0 ? "0.x" : `^${major}.0.0`;
11
+ }
5
12
  function hookCommandFor(mode) {
6
13
  const v = getPackageVersion();
7
- const parts = v.split(".");
8
- const major = parts[0] ?? "0";
9
- const minor = parts[1] ?? "0";
10
- const range = `~${major}.${minor}.0`;
11
- const verb = mode === "auto" ? "update --yes" : "check";
14
+ const range = versionRangeForHook(v);
15
+ const verb = mode === "auto" ? "update --yes --background" : "check";
12
16
  return `pnpm dlx --config.minimumReleaseAge=0 ${COMMAND_NAME}@${range} ${verb} --no-bounce`;
13
17
  }
14
18
  const HOOK_SIGNATURE = COMMAND_NAME;
19
+ const HOOK_TIMEOUT_SECONDS = 120;
15
20
  function readSettings() {
16
21
  const path = getClaudeSettingsPath();
17
22
  if (!existsSync(path))
@@ -44,7 +49,7 @@ function isOurMatcher(m) {
44
49
  h.command.includes(HOOK_SIGNATURE));
45
50
  }
46
51
  function buildOurMatcher(command) {
47
- return { hooks: [{ type: "command", command }] };
52
+ return { hooks: [{ type: "command", command, timeout: HOOK_TIMEOUT_SECONDS }] };
48
53
  }
49
54
  export function runHookInstall(mode = "auto") {
50
55
  const settings = readSettings();
@@ -89,29 +94,6 @@ export function runHookInstall(mode = "auto") {
89
94
  }
90
95
  return 0;
91
96
  }
92
- export function refreshHookCommand() {
93
- try {
94
- const settings = readSettings();
95
- if (!settings?.hooks)
96
- return;
97
- const existing = settings.hooks.SessionStart;
98
- if (!Array.isArray(existing))
99
- return;
100
- const ours = existing.find(isOurMatcher);
101
- if (!ours)
102
- return;
103
- const current = ours.hooks.find((h) => h.type === "command")?.command ?? "";
104
- const fresh = hookCommandFor("auto");
105
- if (current === fresh)
106
- return;
107
- const others = existing.filter((m) => !isOurMatcher(m));
108
- others.push(buildOurMatcher(fresh));
109
- settings.hooks.SessionStart = others;
110
- writeSettings(settings);
111
- }
112
- catch {
113
- }
114
- }
115
97
  export function runHookUninstall(opts = {}) {
116
98
  const say = opts.quiet ? () => { } : logWarn;
117
99
  const sayOk = opts.quiet ? () => { } : logOk;
@@ -155,4 +137,4 @@ export function runHookUninstall(opts = {}) {
155
137
  sayOk("Hook removido.");
156
138
  return 0;
157
139
  }
158
- export { hookCommandFor };
140
+ export { hookCommandFor, versionRangeForHook };
@@ -141,7 +141,7 @@ function printNextSteps(noHook) {
141
141
  log(` ${dim("$")} ${bold(`${COMMAND_NAME} hook uninstall`)} ${gray("apagar aviso")}`);
142
142
  }
143
143
  log("");
144
- log(` ${dim("Docs · ")}${cyan("github.com/alegradev/alegra-data-agent")}`);
144
+ log(` ${dim("Docs · ")}${cyan("github.com/Alegra-Team/data-agent-selfservice")}`);
145
145
  log("");
146
146
  }
147
147
  function isClaudeCode() {
@@ -19,7 +19,7 @@ export async function runLogin(opts = {}) {
19
19
  }
20
20
  log(` Gateway: ${dim(GATEWAY_URL)}`);
21
21
  try {
22
- const record = await performBrowserLogin();
22
+ const record = await performBrowserLogin(opts.timeoutMs);
23
23
  writeAuth(record);
24
24
  log("");
25
25
  log(` ${green("✓")} Autenticado como ${bold(record.email)}.`);
@@ -2,8 +2,9 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { AuthRequiredError, fetchManifest } from "../api.js";
3
3
  import { readAuth } from "../auth.js";
4
4
  import { bold, cyan, dim, gray, green, log, logError, logWarn, yellow } from "../colors.js";
5
- import { detectDrift } from "../installer.js";
5
+ import { detectDrift, inspectMcpInTarget } from "../installer.js";
6
6
  import { COMMAND_NAME, GATEWAY_URL, getAutoUpdateLogPath, getClaudeSkillsDir, getLockFilePath, getPackageVersion, getTargetSkillsDir, readLockFile, } from "../lib.js";
7
+ import { TARGETS } from "../targets.js";
7
8
  function reportAutoUpdateLog() {
8
9
  const logPath = getAutoUpdateLogPath();
9
10
  if (!existsSync(logPath))
@@ -18,6 +19,141 @@ function reportAutoUpdateLog() {
18
19
  catch {
19
20
  }
20
21
  }
22
+ function reportMcps(manifest, lock) {
23
+ const mcps = manifest.mcps ?? {};
24
+ const names = Object.keys(mcps);
25
+ if (names.length === 0)
26
+ return;
27
+ log(bold(" MCPs:"));
28
+ for (const name of names) {
29
+ const entry = mcps[name];
30
+ const targets = (lock.mcps?.[name]?.targets ?? []);
31
+ if (targets.length === 0) {
32
+ log(` ${cyan("+")} ${name} ${gray("(disponible, sin instalar)")}`);
33
+ continue;
34
+ }
35
+ const ok = [];
36
+ const missing = [];
37
+ const modified = [];
38
+ const broken = [];
39
+ const stale = [];
40
+ const prevHashes = lock.mcps?.[name]?.renderedHash;
41
+ for (const t of targets) {
42
+ const label = TARGETS[t].label;
43
+ const state = inspectMcpInTarget(entry, TARGETS[t], prevHashes?.[t]);
44
+ if (state === "ok")
45
+ ok.push(label);
46
+ else if (state === "missing")
47
+ missing.push(label);
48
+ else if (state === "stale")
49
+ stale.push(label);
50
+ else if (state === "broken")
51
+ broken.push(label);
52
+ else
53
+ modified.push(label);
54
+ }
55
+ const notes = [];
56
+ if (ok.length > 0)
57
+ notes.push(gray(`ok en ${ok.join(", ")}`));
58
+ if (modified.length > 0)
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`));
64
+ if (missing.length > 0)
65
+ notes.push(yellow(`falta en ${missing.join(", ")}`));
66
+ const icon = missing.length > 0 || broken.length > 0
67
+ ? yellow("✗")
68
+ : stale.length > 0 || modified.length > 0
69
+ ? dim("~")
70
+ : green("✓");
71
+ log(` ${icon} ${name} ${gray("·")} ${notes.join(gray(" · "))}`);
72
+ }
73
+ log("");
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 que duplican un MCP del agente:`);
105
+ for (const o of overlap)
106
+ 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.`));
111
+ log("");
112
+ }
113
+ export function findRogueMetabase(manifest, specs = Object.values(TARGETS)) {
114
+ const official = manifest.mcps?.metabase?.url;
115
+ if (!official)
116
+ return [];
117
+ const norm = (u) => u.trim().replace(/\/+$/, "").toLowerCase();
118
+ const officialNorm = norm(official);
119
+ const found = [];
120
+ for (const spec of specs) {
121
+ if (!spec.mcpConfigPath.endsWith(".json"))
122
+ continue;
123
+ if (!existsSync(spec.mcpConfigPath))
124
+ continue;
125
+ try {
126
+ const cfg = JSON.parse(readFileSync(spec.mcpConfigPath, "utf-8"));
127
+ const servers = cfg[spec.mcpRootKey];
128
+ if (!servers || typeof servers !== "object")
129
+ continue;
130
+ for (const [name, def] of Object.entries(servers)) {
131
+ if (!/metabase/i.test(name))
132
+ continue;
133
+ const url = def?.[spec.mcpUrlKey];
134
+ if (typeof url === "string" && norm(url) === officialNorm)
135
+ continue;
136
+ found.push(`${spec.label} → ${name}${typeof url === "string" ? ` (${url})` : ""}`);
137
+ }
138
+ }
139
+ catch {
140
+ }
141
+ }
142
+ return found;
143
+ }
144
+ function reportRogueMetabase(manifest) {
145
+ const official = manifest.mcps?.metabase?.url;
146
+ if (!official)
147
+ return;
148
+ const found = findRogueMetabase(manifest);
149
+ if (found.length === 0)
150
+ return;
151
+ logWarn(`Hay ${found.length} MCP(s) de Metabase que NO son el oficial de Alegra: ${found.join(" · ")}`);
152
+ log(gray(` El oficial es ${official}. Los otros son otra fuente y dan otros números.`));
153
+ log(gray(" Las skills tienen instrucción de no usarlos, pero eso no los bloquea:"));
154
+ log(gray(" si no los necesitás, conviene quitarlos del cliente."));
155
+ log("");
156
+ }
21
157
  export async function runStatus() {
22
158
  const auth = readAuth();
23
159
  const lockForTargets = readLockFile();
@@ -63,6 +199,12 @@ export async function runStatus() {
63
199
  }
64
200
  const lock = readLockFile();
65
201
  const drift = detectDrift(manifest);
202
+ const cliVersion = getPackageVersion();
203
+ if (manifest.productVersion && manifest.productVersion !== cliVersion) {
204
+ log(` ${gray("Manifest:")} generado por v${manifest.productVersion} ${gray(`· tu CLI corre v${cliVersion}`)}`);
205
+ log(gray(" No es un error: el gateway y npm se despliegan por separado."));
206
+ log("");
207
+ }
66
208
  log(bold(" Skills:"));
67
209
  log(` ${green("✓")} ${drift.unchanged.length} al día`);
68
210
  if (drift.toInstall.length > 0)
@@ -72,9 +214,15 @@ export async function runStatus() {
72
214
  if (drift.orphaned.length > 0)
73
215
  log(` ${dim("·")} ${drift.orphaned.length} huérfanas: ${drift.orphaned.join(", ")}`);
74
216
  log("");
75
- const total = drift.toInstall.length + drift.toUpdate.length;
217
+ reportMcps(manifest, lock);
218
+ reportRogueMetabase(manifest);
219
+ reportAccountConnectorOverlap(manifest);
220
+ const total = drift.toInstall.length +
221
+ drift.toUpdate.length +
222
+ drift.mcps.toInstall.length +
223
+ drift.mcps.toUpdate.length;
76
224
  if (total > 0) {
77
- logWarn(`Hay ${total} skill(s) por actualizar. Agente: ejecutá vos mismo ${bold(`pnpm dlx ${COMMAND_NAME} update --yes`)} — el usuario no corre comandos.`);
225
+ logWarn(`Hay ${total} elemento(s) (skills + MCPs) por actualizar. Agente: ejecutá vos mismo ${bold(`pnpm dlx ${COMMAND_NAME} update --yes`)} — el usuario no corre comandos.`);
78
226
  }
79
227
  else if (Object.keys(lock.skills).length === 0) {
80
228
  logWarn(`Aún no hay skills instaladas. Agente: ejecutá vos mismo ${bold(`pnpm dlx ${COMMAND_NAME}`)} — el usuario no corre comandos.`);
@@ -1,18 +1,45 @@
1
- import { rmSync } from "node:fs";
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
2
3
  import { AuthRequiredError, fetchManifest } from "../api.js";
3
4
  import { hasValidAuth } from "../auth.js";
4
5
  import { refreshAutoUpdate } from "../autoupdate.js";
5
6
  import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
6
7
  import { detectDrift, installAll, summarize } from "../installer.js";
7
- import { getAutoUpdateLogPath } from "../lib.js";
8
+ import { getAutoUpdateLogPath, getLastCheckPath } from "../lib.js";
8
9
  import { banner } from "../ui.js";
9
10
  import { runLogin } from "./login.js";
11
+ const BACKGROUND_COOLDOWN_MS = 15 * 60_000;
12
+ const BACKGROUND_LOGIN_TIMEOUT_MS = 90_000;
13
+ function checkedRecently() {
14
+ try {
15
+ const raw = readFileSync(getLastCheckPath(), "utf-8");
16
+ const at = Date.parse(JSON.parse(raw).checkedAt ?? "");
17
+ if (Number.isNaN(at))
18
+ return false;
19
+ const age = Date.now() - at;
20
+ return age >= 0 && age < BACKGROUND_COOLDOWN_MS;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ function markChecked() {
27
+ try {
28
+ const path = getLastCheckPath();
29
+ mkdirSync(dirname(path), { recursive: true });
30
+ writeFileSync(path, `${JSON.stringify({ checkedAt: new Date().toISOString() }, null, 2)}\n`);
31
+ }
32
+ catch {
33
+ }
34
+ }
10
35
  export async function runUpdate(opts = {}) {
36
+ if (opts.background && !opts.dryRun && checkedRecently())
37
+ return 0;
11
38
  banner();
12
39
  if (!hasValidAuth()) {
13
- const code = await runLogin({});
40
+ const code = await runLogin(opts.background ? { timeoutMs: BACKGROUND_LOGIN_TIMEOUT_MS } : {});
14
41
  if (code !== 0)
15
- return code;
42
+ return opts.background ? 0 : code;
16
43
  }
17
44
  let reauthed = false;
18
45
  for (;;) {
@@ -52,6 +79,8 @@ async function runUpdateOnce(opts) {
52
79
  ];
53
80
  if (pending.length === 0) {
54
81
  log(green(" ✓ Todo al día (skills + MCPs).\n"));
82
+ if (!opts.dryRun)
83
+ markChecked();
55
84
  if (!opts.dryRun) {
56
85
  refreshAutoUpdate();
57
86
  clearAutoUpdateLog();
@@ -65,14 +94,18 @@ async function runUpdateOnce(opts) {
65
94
  log(` ${yellow("↻")} ${name} ${gray("(skill nueva versión)")}`);
66
95
  for (const name of drift.mcps.toInstall)
67
96
  log(` ${cyan("+")} ${name} ${gray("(MCP nuevo)")}`);
68
- for (const name of drift.mcps.toUpdate)
69
- log(` ${yellow("↻")} ${name} ${gray("(MCP actualizado)")}`);
97
+ for (const name of drift.mcps.toUpdate) {
98
+ const motivo = drift.mcps.missing.includes(name)
99
+ ? "(MCP ausente o descartado por el cliente — se repone)"
100
+ : "(MCP actualizado)";
101
+ log(` ${yellow("↻")} ${name} ${gray(motivo)}`);
102
+ }
70
103
  log("");
71
104
  if (opts.dryRun) {
72
105
  log(dim(" Modo dry-run: no se modificará nada.\n"));
73
106
  return 0;
74
107
  }
75
- const result = await installAll({ only: pending, targets: opts.targets, force: true });
108
+ const result = await installAll({ only: pending, targets: opts.targets });
76
109
  if ("error" in result) {
77
110
  logError(result.error);
78
111
  return 1;
@@ -80,8 +113,10 @@ async function runUpdateOnce(opts) {
80
113
  summarize(result.results);
81
114
  refreshAutoUpdate();
82
115
  const failed = result.results.some((r) => r.status === "failed");
83
- if (!failed)
116
+ if (!failed) {
84
117
  clearAutoUpdateLog();
118
+ markChecked();
119
+ }
85
120
  return failed ? 1 : 0;
86
121
  }
87
122
  function clearAutoUpdateLog() {
package/dist/installer.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { copyFileSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
- import { dirname, join, resolve, sep } from "node:path";
2
+ import { copyFileSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
+ import { basename, dirname, join, resolve, sep } from "node:path";
4
4
  import { AuthRequiredError, fetchManifest, fetchSkillFile } from "./api.js";
5
5
  import { dim, gray, green, logError, red, yellow } from "./colors.js";
6
6
  import { adaptStdioCommand, ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getCursorLegacyRulesDir, getLockFilePath, getOpenCodeLegacyPromptsDir, getPackageVersion, readLockFile, sha256File, writeLockFile, } from "./lib.js";
@@ -44,6 +44,17 @@ function mcpConfigHash(mcp) {
44
44
  });
45
45
  return createHash("sha256").update(salient).digest("hex");
46
46
  }
47
+ function stableStringify(value) {
48
+ if (value === null || typeof value !== "object")
49
+ return JSON.stringify(value);
50
+ if (Array.isArray(value))
51
+ return `[${value.map(stableStringify).join(",")}]`;
52
+ const o = value;
53
+ return `{${Object.keys(o)
54
+ .sort()
55
+ .map((k) => `${JSON.stringify(k)}:${stableStringify(o[k])}`)
56
+ .join(",")}}`;
57
+ }
47
58
  function safeJoinSkillFile(skillRoot, rel) {
48
59
  const candidate = resolve(skillRoot, rel);
49
60
  const root = resolve(skillRoot);
@@ -73,33 +84,51 @@ export function verifyInstalledFiles(dir, entry) {
73
84
  function sha256Buffer(buf) {
74
85
  return createHash("sha256").update(buf).digest("hex");
75
86
  }
87
+ const DOWNLOAD_CONCURRENCY = 8;
88
+ async function mapWithLimit(items, limit, fn) {
89
+ const out = new Array(items.length);
90
+ let next = 0;
91
+ const worker = async () => {
92
+ for (;;) {
93
+ const i = next++;
94
+ if (i >= items.length)
95
+ return;
96
+ out[i] = await fn(items[i]);
97
+ }
98
+ };
99
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
100
+ return out;
101
+ }
76
102
  async function stageSkill(skillName, entry) {
77
103
  validateSkillName(skillName);
78
104
  for (const rel of entry.files)
79
105
  validateRelPath(rel);
80
- const staged = [];
81
- for (const rel of entry.files) {
106
+ return mapWithLimit(entry.files, DOWNLOAD_CONCURRENCY, async (rel) => {
82
107
  const data = await fetchSkillFile(skillName, rel);
83
108
  const actualHash = sha256Buffer(data);
84
109
  const expectedHash = entry.sha256[rel];
85
110
  if (actualHash !== expectedHash) {
86
111
  throw new Error(`hash inválido para ${skillName}/${rel}: esperaba ${expectedHash.slice(0, 12)}…, recibido ${actualHash.slice(0, 12)}…`);
87
112
  }
88
- staged.push({ rel, data });
89
- }
90
- return staged;
113
+ return { rel, data };
114
+ });
115
+ }
116
+ function skillDirsFor(spec) {
117
+ return [spec.skillsDir, ...(spec.mirrorSkillsDirs ?? [])];
91
118
  }
92
119
  function writeSkillToTarget(skillName, staged, spec) {
93
120
  validateSkillName(skillName);
94
121
  if (spec.skillFormat === "folder") {
95
- const dst = join(spec.skillsDir, skillName);
96
- if (existsSync(dst))
97
- withFsRetry(() => rmSync(dst, { recursive: true, force: true }));
98
- ensureDir(dst);
99
- for (const f of staged) {
100
- const dstFile = safeJoinSkillFile(dst, f.rel);
101
- ensureDir(dirname(dstFile));
102
- withFsRetry(() => writeFileSync(dstFile, f.data));
122
+ for (const base of skillDirsFor(spec)) {
123
+ const dst = join(base, skillName);
124
+ if (existsSync(dst))
125
+ withFsRetry(() => rmSync(dst, { recursive: true, force: true }));
126
+ ensureDir(dst);
127
+ for (const f of staged) {
128
+ const dstFile = safeJoinSkillFile(dst, f.rel);
129
+ ensureDir(dirname(dstFile));
130
+ withFsRetry(() => writeFileSync(dstFile, f.data));
131
+ }
103
132
  }
104
133
  return;
105
134
  }
@@ -190,11 +219,30 @@ function atomicWriteFile(path, content) {
190
219
  }
191
220
  }
192
221
  }
222
+ const MAX_BACKUPS = 3;
223
+ const backedUpThisRun = new Set();
193
224
  function backupIfExists(path) {
194
225
  if (!existsSync(path))
195
226
  return;
196
- const bak = `${path}.bak-${Date.now()}`;
197
- copyFileSync(path, bak);
227
+ if (backedUpThisRun.has(path))
228
+ return;
229
+ copyFileSync(path, `${path}.bak-${Date.now()}`);
230
+ backedUpThisRun.add(path);
231
+ pruneBackups(path);
232
+ }
233
+ function pruneBackups(path) {
234
+ try {
235
+ const dir = dirname(path);
236
+ const prefix = `${basename(path)}.bak-`;
237
+ const stale = readdirSync(dir)
238
+ .filter((f) => f.startsWith(prefix))
239
+ .sort()
240
+ .slice(0, -MAX_BACKUPS);
241
+ for (const f of stale)
242
+ rmSync(join(dir, f), { force: true });
243
+ }
244
+ catch {
245
+ }
198
246
  }
199
247
  function readJsonOrEmpty(path) {
200
248
  if (!existsSync(path))
@@ -282,6 +330,19 @@ export function upsertTomlBlock(content, name, block) {
282
330
  const sep = content.endsWith("\n\n") ? "" : content.endsWith("\n") ? "\n" : "\n\n";
283
331
  return `${content}${sep}${fenced}\n`;
284
332
  }
333
+ export function extractTomlBlock(content, name) {
334
+ const { begin, end } = tomlMarkers(name);
335
+ const bi = content.indexOf(begin);
336
+ if (bi === -1)
337
+ return null;
338
+ const ei = content.indexOf(end, bi);
339
+ if (ei === -1)
340
+ return null;
341
+ return content
342
+ .slice(bi + begin.length, ei)
343
+ .replace(/^\n/, "")
344
+ .replace(/\n$/, "");
345
+ }
285
346
  export function removeTomlBlock(content, name) {
286
347
  const { begin, end } = tomlMarkers(name);
287
348
  const bi = content.indexOf(begin);
@@ -317,14 +378,77 @@ function unregisterMcpInTomlBlock(name, spec) {
317
378
  atomicWriteFile(spec.mcpConfigPath, next);
318
379
  return true;
319
380
  }
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) {
401
+ try {
402
+ if (!existsSync(spec.mcpConfigPath))
403
+ return "missing";
404
+ if (spec.mcpMechanism === "toml-block") {
405
+ const block = extractTomlBlock(readFileSync(spec.mcpConfigPath, "utf-8"), mcp.name);
406
+ if (block === null)
407
+ return "missing";
408
+ const current = block.trim();
409
+ if (current === renderMcpToml(mcp).trim())
410
+ return "ok";
411
+ return prevHash && hashOf(current) === prevHash ? "stale" : "modified";
412
+ }
413
+ const cfg = JSON.parse(readFileSync(spec.mcpConfigPath, "utf-8"));
414
+ const section = cfg[spec.mcpRootKey];
415
+ if (!section || typeof section !== "object")
416
+ return "missing";
417
+ const current = section[mcp.name];
418
+ if (current === undefined)
419
+ return "missing";
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";
428
+ }
429
+ catch {
430
+ return "missing";
431
+ }
432
+ }
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
+ });
438
+ }
439
+ const cleanedTargets = new Set();
320
440
  function resolveTargets(opts) {
321
441
  const ids = opts.targets?.length ? opts.targets : detectInstalledTargets();
322
442
  if (ids.length === 0) {
323
443
  ids.push("claude-code");
324
444
  }
325
445
  const specs = ids.map((id) => TARGETS[id]);
326
- for (const spec of specs)
446
+ for (const spec of specs) {
447
+ if (cleanedTargets.has(spec.id))
448
+ continue;
449
+ cleanedTargets.add(spec.id);
327
450
  cleanupLegacySkillArtifacts(spec);
451
+ }
328
452
  return specs;
329
453
  }
330
454
  const LEGACY_RENAMED_SKILLS = ["alanube", "alegra-producto", "alegra-ce-universe"];
@@ -360,10 +484,12 @@ function stripMcpEntries(path, names) {
360
484
  function cleanupLegacySkillArtifacts(spec) {
361
485
  try {
362
486
  if (spec.skillFormat === "folder") {
363
- for (const old of LEGACY_RENAMED_SKILLS) {
364
- const dir = join(spec.skillsDir, old);
365
- if (existsSync(dir))
366
- rmSync(dir, { recursive: true, force: true });
487
+ for (const base of skillDirsFor(spec)) {
488
+ for (const old of LEGACY_RENAMED_SKILLS) {
489
+ const dir = join(base, old);
490
+ if (existsSync(dir))
491
+ rmSync(dir, { recursive: true, force: true });
492
+ }
367
493
  }
368
494
  }
369
495
  const lock = readLockFile();
@@ -459,7 +585,13 @@ export async function installSkill(name, manifest, opts = {}) {
459
585
  }
460
586
  }
461
587
  if (okTargets.length === 0) {
462
- return { name, kind: "skill", status: "failed", targets: targetIds, error: failures.join("; ") };
588
+ return {
589
+ name,
590
+ kind: "skill",
591
+ status: "failed",
592
+ targets: targetIds,
593
+ error: failures.join("; "),
594
+ };
463
595
  }
464
596
  lock.skills[name] = {
465
597
  name,
@@ -506,7 +638,17 @@ export function installMcp(mcp, opts = {}) {
506
638
  const targetsMatch = prev?.targets &&
507
639
  prev.targets.length === targetIds.length &&
508
640
  prev.targets.every((t) => targetIds.includes(t));
509
- if (prev && targetsMatch && prev.configHash === mcpConfigHash(mcp) && !opts.force) {
641
+ const configChanged = !prev || prev.configHash !== mcpConfigHash(mcp);
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);
648
+ const alreadyThere = wantedTargets
649
+ .filter((t) => !writeTargets.includes(t) && !needsWrite(t))
650
+ .map((t) => t.id);
651
+ if (writeTargets.length === 0 && targetsMatch) {
510
652
  return { name: mcp.name, kind: "mcp", status: "unchanged", targets: targetIds };
511
653
  }
512
654
  if (opts.dryRun) {
@@ -514,20 +656,21 @@ export function installMcp(mcp, opts = {}) {
514
656
  name: mcp.name,
515
657
  kind: "mcp",
516
658
  status: prev ? "updated" : "installed",
517
- targets: targetIds,
659
+ targets: writeTargets.map((t) => t.id),
518
660
  };
519
661
  }
520
- const okTargets = [];
662
+ const written = [];
521
663
  const failures = [];
522
- for (const t of wantedTargets) {
664
+ for (const t of writeTargets) {
523
665
  try {
524
666
  registerMcpInTarget(mcp, t);
525
- okTargets.push(t.id);
667
+ written.push(t.id);
526
668
  }
527
669
  catch (err) {
528
670
  failures.push(`${t.id}: ${err instanceof Error ? err.message : String(err)}`);
529
671
  }
530
672
  }
673
+ const okTargets = [...new Set([...alreadyThere, ...written])];
531
674
  if (okTargets.length === 0) {
532
675
  return {
533
676
  name: mcp.name,
@@ -537,12 +680,18 @@ export function installMcp(mcp, opts = {}) {
537
680
  error: failures.join("; "),
538
681
  };
539
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
+ }
540
688
  lock.mcps[mcp.name] = {
541
689
  name: mcp.name,
542
690
  installedFrom: getPackageVersion(),
543
691
  installedAt: new Date().toISOString(),
544
692
  configHash: mcpConfigHash(mcp),
545
693
  targets: okTargets,
694
+ renderedHash,
546
695
  };
547
696
  writeLockFile(lock);
548
697
  return {
@@ -604,9 +753,11 @@ export function uninstallSkill(name, opts = {}) {
604
753
  for (const tId of targetIds) {
605
754
  const spec = TARGETS[tId];
606
755
  if (spec.skillFormat === "folder") {
607
- const dst = join(spec.skillsDir, name);
608
- if (existsSync(dst))
609
- rmSync(dst, { recursive: true, force: true });
756
+ for (const base of skillDirsFor(spec)) {
757
+ const dst = join(base, name);
758
+ if (existsSync(dst))
759
+ rmSync(dst, { recursive: true, force: true });
760
+ }
610
761
  }
611
762
  else {
612
763
  const ext = spec.skillFormat === "cursor-mdc" ? ".mdc" : ".md";
@@ -703,16 +854,27 @@ export function detectDrift(manifest) {
703
854
  toUpdate: [],
704
855
  unchanged: [],
705
856
  orphaned: [],
857
+ missing: [],
706
858
  };
707
859
  const manifestMcps = manifest.mcps ?? {};
708
860
  for (const name of Object.keys(manifestMcps)) {
709
861
  const prevMcp = lock.mcps?.[name];
710
- if (!prevMcp)
862
+ if (!prevMcp) {
711
863
  mcps.toInstall.push(name);
712
- else if (prevMcp.configHash !== mcpConfigHash(manifestMcps[name]))
864
+ continue;
865
+ }
866
+ if (prevMcp.configHash !== mcpConfigHash(manifestMcps[name])) {
867
+ mcps.toUpdate.push(name);
868
+ continue;
869
+ }
870
+ const missing = mcpNeedsRepairInTargets(manifestMcps[name], (prevMcp.targets ?? []), prevMcp.renderedHash);
871
+ if (missing.length > 0) {
713
872
  mcps.toUpdate.push(name);
714
- else
873
+ mcps.missing.push(name);
874
+ }
875
+ else {
715
876
  mcps.unchanged.push(name);
877
+ }
716
878
  }
717
879
  for (const name of Object.keys(lock.mcps ?? {})) {
718
880
  if (!manifestMcps[name])
package/dist/lib.js CHANGED
@@ -5,7 +5,7 @@ import { dirname, join, relative, 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";
8
- export const REPO_SLUG = "alegradev/alegra-data-agent";
8
+ export const REPO_SLUG = "Alegra-Team/data-agent-selfservice";
9
9
  export const REPO_DEFAULT_BRANCH = "main";
10
10
  export const GATEWAY_URL = process.env.ALEGRA_DATA_AGENT_GATEWAY_URL ?? "https://alegra-data-agent.vercel.app";
11
11
  export const ALL_TARGETS = [
@@ -19,12 +19,6 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
19
19
  export function getPackageRoot() {
20
20
  return join(__dirname, "..");
21
21
  }
22
- export function getRegistryDir() {
23
- return join(getPackageRoot(), "skills-registry");
24
- }
25
- export function getManifestPath() {
26
- return join(getRegistryDir(), "index.json");
27
- }
28
22
  export function getClaudeSkillsDir() {
29
23
  return join(homedir(), ".claude", "skills");
30
24
  }
@@ -59,6 +53,9 @@ export function getOpenCodeConfigPath() {
59
53
  return join(base, "opencode", "opencode.json");
60
54
  }
61
55
  export function getAntigravitySkillsDir() {
56
+ return join(homedir(), ".gemini", "antigravity-cli", "skills");
57
+ }
58
+ export function getAntigravityGeminiSkillsDir() {
62
59
  return join(homedir(), ".gemini", "skills");
63
60
  }
64
61
  export function getAntigravityMcpConfigPath() {
@@ -142,6 +139,12 @@ export function getConfigDir() {
142
139
  export function getAuthFilePath() {
143
140
  return join(getConfigDir(), "auth.json");
144
141
  }
142
+ export function getLastCheckPath() {
143
+ return join(getConfigDir(), "last-check.json");
144
+ }
145
+ export function getManifestCachePath() {
146
+ return join(getConfigDir(), "manifest-cache.json");
147
+ }
145
148
  export function getAutoUpdateLogPath() {
146
149
  return join(getConfigDir(), "autoupdate.log");
147
150
  }
@@ -158,31 +161,29 @@ export function getPackageVersion() {
158
161
  }
159
162
  return _cachedVersion;
160
163
  }
161
- export function readManifest() {
162
- const path = getManifestPath();
163
- if (!existsSync(path))
164
- return null;
165
- try {
166
- return JSON.parse(readFileSync(path, "utf-8"));
167
- }
168
- catch {
169
- return null;
170
- }
164
+ let _lockCache = null;
165
+ function emptyLock() {
166
+ return { schemaVersion: 1, productName: PRODUCT_NAME, skills: {} };
171
167
  }
172
168
  export function readLockFile() {
169
+ if (_lockCache)
170
+ return _lockCache;
173
171
  const path = getLockFilePath();
174
- if (!existsSync(path)) {
175
- return { schemaVersion: 1, productName: PRODUCT_NAME, skills: {} };
176
- }
177
- try {
178
- return JSON.parse(readFileSync(path, "utf-8"));
179
- }
180
- catch {
181
- return { schemaVersion: 1, productName: PRODUCT_NAME, skills: {} };
172
+ let lock = emptyLock();
173
+ if (existsSync(path)) {
174
+ try {
175
+ lock = JSON.parse(readFileSync(path, "utf-8"));
176
+ }
177
+ catch {
178
+ lock = emptyLock();
179
+ }
182
180
  }
181
+ _lockCache = lock;
182
+ return lock;
183
183
  }
184
184
  export function writeLockFile(lock) {
185
185
  const path = getLockFilePath();
186
+ _lockCache = lock;
186
187
  mkdirSync(dirname(path), { recursive: true });
187
188
  writeFileSync(path, `${JSON.stringify(lock, null, 2)}\n`, "utf-8");
188
189
  }
package/dist/main.js CHANGED
@@ -37,6 +37,7 @@ function parseArgs(argv) {
37
37
  noHook: false,
38
38
  noBounce: false,
39
39
  autoUpdate: false,
40
+ background: false,
40
41
  };
41
42
  const positional = [];
42
43
  const only = [];
@@ -56,6 +57,8 @@ function parseArgs(argv) {
56
57
  flags.noBounce = true;
57
58
  else if (a === "--auto-update")
58
59
  flags.autoUpdate = true;
60
+ else if (a === "--background")
61
+ flags.background = true;
59
62
  else if (a === "-h" || a === "--help")
60
63
  flags.help = true;
61
64
  else if (a === "-v" || a === "--version")
@@ -128,6 +131,7 @@ async function main() {
128
131
  exitCode = await runUpdate({
129
132
  yes: args.flags.yes,
130
133
  dryRun: args.flags.dryRun,
134
+ background: args.flags.background,
131
135
  targets: args.targets.length > 0 ? args.targets : undefined,
132
136
  });
133
137
  break;
package/dist/render.js CHANGED
@@ -81,6 +81,9 @@ export function renderMcpForTarget(mcp, spec) {
81
81
  obj.type = "remote";
82
82
  obj.enabled = true;
83
83
  }
84
+ else if (spec.mcpHttpTypeValue) {
85
+ obj.type = spec.mcpHttpTypeValue;
86
+ }
84
87
  if (mcp.auth.type === "gateway") {
85
88
  if (!mcp.auth.tokenEnv) {
86
89
  throw new Error(`MCP '${mcp.name}' es type=gateway pero no trae tokenEnv`);
package/dist/signature.js CHANGED
@@ -1,6 +1,16 @@
1
1
  import { createPublicKey, verify } from "node:crypto";
2
- export const SIGNING_KEY_ID = "alegra-data-agent-v1";
3
- export const PUBLIC_KEY_B64 = "MCowBQYDK2VwAyEApoojQPy6INQXl/xZ0gQumsI3tt9HvMrb0PaVlNn7TkE=";
2
+ export const SIGNING_KEY_ID = "alegra-data-agent-v2";
3
+ export const TRUSTED_KEYS = [
4
+ {
5
+ keyId: "alegra-data-agent-v2",
6
+ publicKeyB64: "MCowBQYDK2VwAyEA5nrWo7oWlhyad9rJjQHqgL0xaPZ83Yn5dJotGXxCo0c=",
7
+ },
8
+ {
9
+ keyId: "alegra-data-agent-v1",
10
+ publicKeyB64: "MCowBQYDK2VwAyEApoojQPy6INQXl/xZ0gQumsI3tt9HvMrb0PaVlNn7TkE=",
11
+ },
12
+ ];
13
+ export const PUBLIC_KEY_B64 = TRUSTED_KEYS[0].publicKeyB64;
4
14
  export function canonicalize(obj) {
5
15
  if (obj === null || typeof obj !== "object")
6
16
  return JSON.stringify(obj);
@@ -25,10 +35,12 @@ export function verifyManifestSignature(manifest, options = {}) {
25
35
  if (sig.alg !== "Ed25519") {
26
36
  return { ok: false, reason: `algoritmo no soportado: ${sig.alg}` };
27
37
  }
28
- if (sig.keyId !== SIGNING_KEY_ID) {
38
+ const keys = options.trustedKeys ?? TRUSTED_KEYS;
39
+ const trusted = keys.find((k) => k.keyId === sig.keyId);
40
+ if (!trusted) {
29
41
  return {
30
42
  ok: false,
31
- reason: `keyId no reconocido: ${sig.keyId} (CLI espera ${SIGNING_KEY_ID})`,
43
+ reason: `keyId no reconocido: ${sig.keyId} (este CLI acepta ${keys.map((k) => k.keyId).join(", ")})`,
32
44
  };
33
45
  }
34
46
  if (typeof manifest.generatedAt === "string") {
@@ -49,7 +61,7 @@ export function verifyManifestSignature(manifest, options = {}) {
49
61
  }
50
62
  const canonical = canonicalize(manifest);
51
63
  const publicKey = createPublicKey({
52
- key: Buffer.from(PUBLIC_KEY_B64, "base64"),
64
+ key: Buffer.from(trusted.publicKeyB64, "base64"),
53
65
  format: "der",
54
66
  type: "spki",
55
67
  });
package/dist/targets.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { getAntigravityMcpConfigPath, getAntigravitySkillsDir, getClaudeJsonPath, getClaudeSkillsDir, getCodexConfigPath, getCodexSkillsDir, getCursorMcpPath, getCursorSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
4
+ import { getAntigravityGeminiSkillsDir, getAntigravityMcpConfigPath, getAntigravitySkillsDir, getClaudeJsonPath, getClaudeSkillsDir, getCodexConfigPath, getCodexSkillsDir, getCursorMcpPath, getCursorSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
5
5
  export const TARGETS = {
6
6
  "claude-code": {
7
7
  id: "claude-code",
8
8
  label: "Claude Code",
9
+ mcpHttpTypeValue: "http",
9
10
  skillsDir: getClaudeSkillsDir(),
10
11
  skillFormat: "folder",
11
12
  mcpConfigPath: getClaudeJsonPath(),
@@ -14,6 +15,7 @@ export const TARGETS = {
14
15
  },
15
16
  cursor: {
16
17
  id: "cursor",
18
+ mcpHttpTypeValue: "http",
17
19
  label: "Cursor",
18
20
  skillsDir: getCursorSkillsDir(),
19
21
  skillFormat: "folder",
@@ -35,6 +37,7 @@ export const TARGETS = {
35
37
  id: "antigravity",
36
38
  label: "Antigravity",
37
39
  skillsDir: getAntigravitySkillsDir(),
40
+ mirrorSkillsDirs: [getAntigravityGeminiSkillsDir()],
38
41
  skillFormat: "folder",
39
42
  mcpConfigPath: getAntigravityMcpConfigPath(),
40
43
  mcpRootKey: "mcpServers",
@@ -60,15 +63,15 @@ export function detectInstalledTargets() {
60
63
  ? process.env.CODEX_HOME
61
64
  : join(home, ".codex");
62
65
  const sentinels = {
63
- "claude-code": join(home, ".claude"),
64
- cursor: join(home, ".cursor"),
65
- opencode: join(xdgConfig, "opencode"),
66
- antigravity: join(home, ".gemini", "antigravity"),
67
- codex: codexHome,
66
+ "claude-code": [join(home, ".claude")],
67
+ cursor: [join(home, ".cursor")],
68
+ opencode: [join(xdgConfig, "opencode")],
69
+ antigravity: [join(home, ".gemini", "antigravity-cli"), join(home, ".gemini", "antigravity")],
70
+ codex: [codexHome],
68
71
  };
69
72
  const out = [];
70
73
  for (const id of Object.keys(sentinels)) {
71
- if (existsSync(sentinels[id]))
74
+ if (sentinels[id].some((p) => existsSync(p)))
72
75
  out.push(id);
73
76
  }
74
77
  return out;
package/dist/ui.js CHANGED
@@ -67,6 +67,6 @@ ${bold("Opciones:")}
67
67
  -v, --version Mostrar versión
68
68
 
69
69
  ${bold("Documentación:")}
70
- https://github.com/alegradev/alegra-data-agent
70
+ https://github.com/Alegra-Team/data-agent-selfservice
71
71
  `;
72
72
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alegra-data-agent",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
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",
@@ -19,31 +19,34 @@
19
19
  "README.md"
20
20
  ],
21
21
  "devDependencies": {
22
- "@types/node": "25.8.0",
23
- "typescript": "6.0.3"
22
+ "@types/node": "26.1.2",
23
+ "typescript": "7.0.2"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",
27
- "url": "git+https://github.com/alegradev/alegra-data-agent.git"
27
+ "url": "git+https://github.com/Alegra-Team/data-agent-selfservice.git"
28
28
  },
29
- "homepage": "https://github.com/alegradev/alegra-data-agent#readme",
29
+ "homepage": "https://github.com/Alegra-Team/data-agent-selfservice#readme",
30
30
  "bugs": {
31
- "url": "https://github.com/alegradev/alegra-data-agent/issues"
31
+ "url": "https://github.com/Alegra-Team/data-agent-selfservice/issues"
32
32
  },
33
33
  "scripts": {
34
34
  "typecheck": "tsc -p tsconfig.json",
35
35
  "build": "tsc -p tsconfig.build.json",
36
36
  "clean": "rm -rf dist",
37
37
  "manifest:generate": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/generate-manifest.mjs",
38
- "manifest:sign": "node scripts/sign-manifest.mjs",
38
+ "manifest:sign": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/sign-manifest.mjs",
39
+ "signing-key:generate": "node scripts/generate-signing-key.mjs",
39
40
  "manifest:scan": "node scripts/scan-skills.mjs",
40
41
  "manifest:validate": "node scripts/validate-skills.mjs --legacy-ok",
41
42
  "manifest:validate:strict": "node scripts/validate-skills.mjs",
42
43
  "manifest": "pnpm run manifest:validate && pnpm run manifest:scan && pnpm run manifest:generate && pnpm run manifest:sign",
43
44
  "test": "node tests/runner.mjs",
44
45
  "test:report": "node tests/runner.mjs --report markdown",
45
- "test:security": "node tests/security/runner.mjs",
46
- "test:all": "node tests/runner.mjs && node tests/security/runner.mjs",
46
+ "test:security": "node --experimental-strip-types --disable-warning=ExperimentalWarning tests/security/runner.mjs",
47
+ "test:all": "pnpm run test && pnpm run test:security",
48
+ "eval:run": "node tests/evals/run-phase2.mjs",
49
+ "verify:clients": "node scripts/verify-clients.mjs",
47
50
  "lint": "pnpm dlx @biomejs/biome@1.9.4 check .",
48
51
  "lint:fix": "pnpm dlx @biomejs/biome@1.9.4 check --write .",
49
52
  "format": "pnpm dlx @biomejs/biome@1.9.4 format --write ."