alegra-data-agent 0.1.0 → 0.2.1

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
@@ -1,8 +1,8 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomBytes, timingSafeEqual } from "node:crypto";
1
3
  import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
4
  import { createServer } from "node:http";
3
5
  import { dirname } from "node:path";
4
- import { spawn } from "node:child_process";
5
- import { randomBytes, timingSafeEqual } from "node:crypto";
6
6
  import { GATEWAY_URL, getAuthFilePath } from "./lib.js";
7
7
  function decodeJwtEmail(jwt) {
8
8
  const parts = jwt.split(".");
@@ -39,7 +39,7 @@ export function readAuth() {
39
39
  export function writeAuth(record) {
40
40
  const path = getAuthFilePath();
41
41
  mkdirSync(dirname(path), { recursive: true });
42
- writeFileSync(path, JSON.stringify(record, null, 2) + "\n", "utf-8");
42
+ writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
43
43
  try {
44
44
  chmodSync(path, 0o600);
45
45
  }
@@ -54,6 +54,31 @@ export function clearAuth() {
54
54
  export function hasAuth() {
55
55
  return readAuth() !== null;
56
56
  }
57
+ function decodeJwtExp(jwt) {
58
+ const parts = jwt.split(".");
59
+ if (parts.length !== 3)
60
+ return null;
61
+ try {
62
+ const padded = parts[1].replace(/-/g, "+").replace(/_/g, "/");
63
+ const payload = JSON.parse(Buffer.from(padded, "base64").toString("utf-8"));
64
+ return typeof payload.exp === "number" ? payload.exp : null;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ export function isAuthExpired(record) {
71
+ if (!record)
72
+ return true;
73
+ const exp = decodeJwtExp(record.token);
74
+ if (exp === null)
75
+ return false;
76
+ return Date.now() >= exp * 1000 - 60_000;
77
+ }
78
+ export function hasValidAuth() {
79
+ const record = readAuth();
80
+ return record !== null && !isAuthExpired(record);
81
+ }
57
82
  export async function performBrowserLogin(timeoutMs = 5 * 60 * 1000) {
58
83
  const state = randomBytes(24).toString("hex");
59
84
  return new Promise((resolve, reject) => {
@@ -139,7 +164,7 @@ export async function performBrowserLogin(timeoutMs = 5 * 60 * 1000) {
139
164
  }
140
165
  });
141
166
  server.listen(0, "127.0.0.1", () => {
142
- const address = server.address();
167
+ const address = server?.address();
143
168
  if (typeof address !== "object" || !address) {
144
169
  cleanup();
145
170
  reject(new Error("No se pudo determinar el puerto."));
@@ -150,26 +175,21 @@ export async function performBrowserLogin(timeoutMs = 5 * 60 * 1000) {
150
175
  authUrl.searchParams.set("port", String(port));
151
176
  authUrl.searchParams.set("state", state);
152
177
  const u = authUrl.toString();
153
- process.stdout.write("\n" +
154
- " ╭────────────────────────────────────────────────────────────╮\n" +
155
- " │ Inicia sesión con tu cuenta @alegra.com en el navegador │\n" +
156
- " ╰────────────────────────────────────────────────────────────╯\n" +
157
- "\n" +
158
- " Intento abrir tu navegador. Si no se abre solo, copia toda\n" +
159
- " esta URL (triple-click selecciona la línea completa) y\n" +
160
- " pégala en tu navegador:\n" +
161
- "\n" +
162
- " ────────────────────────────────────────────────────────────\n" +
163
- `${u}\n` +
164
- " ────────────────────────────────────────────────────────────\n" +
165
- "\n" +
166
- ` Esperando hasta ${Math.round(timeoutMs / 1000)}s a que completes el login…\n` +
167
- "\n");
178
+ process.stdout.write(`\n Abrí esta URL para iniciar sesión con tu cuenta @alegra.com:\n\n${u}\n\n Si el navegador no se abrió solo: copiá la URL de arriba y pegala en tu navegador.\n Esperando hasta ${Math.round(timeoutMs / 1000)}s…\n\n`);
179
+ try {
180
+ process.stdout._flush?.();
181
+ }
182
+ catch {
183
+ }
168
184
  openBrowser(u);
185
+ const nudgeTimer = setTimeout(() => {
186
+ if (resolved)
187
+ return;
188
+ process.stdout.write(`\n ¿No se abrió el navegador? Copiá esta URL y pegala manualmente:\n\n${u}\n\n`);
189
+ }, 8000);
169
190
  timer = setTimeout(() => {
170
- safeReject(new Error(`Timeout: pasaron ${Math.round(timeoutMs / 1000)}s sin recibir la auth. ` +
171
- "Si abriste el link en otra máquina sin conexión al CLI, debes correr el comando " +
172
- "desde la misma máquina donde abrirás el browser. Reintenta."));
191
+ clearTimeout(nudgeTimer);
192
+ safeReject(new Error(`Timeout: pasaron ${Math.round(timeoutMs / 1000)}s sin recibir la auth.\n\nSi el navegador no se abrió, copiá esta URL manualmente en tu navegador y completá el login:\n\n${u}\n\nSi abriste el link en otra máquina sin conexión al CLI, debés correr el comando desde la misma máquina donde abrirás el navegador.`));
173
193
  }, timeoutMs);
174
194
  });
175
195
  });
@@ -204,6 +224,7 @@ function openBrowser(url) {
204
224
  args: ["url.dll,FileProtocolHandler", url],
205
225
  });
206
226
  candidates.push({ cmd: "cmd.exe", args: ["/c", "start", '""', `"${url}"`] });
227
+ candidates.push({ cmd: "wslview", args: [url] });
207
228
  candidates.push({ cmd: "xdg-open", args: [url] });
208
229
  }
209
230
  else {
package/dist/colors.js CHANGED
@@ -1,7 +1,5 @@
1
- const SUPPORTS_COLOR = process.stdout.isTTY &&
2
- process.env.TERM !== "dumb" &&
3
- process.env.NO_COLOR === undefined;
4
- const wrap = (open, close) => (s) => (SUPPORTS_COLOR ? `${open}${s}${close}` : String(s));
1
+ const SUPPORTS_COLOR = process.stdout.isTTY && process.env.TERM !== "dumb" && process.env.NO_COLOR === undefined;
2
+ const wrap = (open, close) => (s) => SUPPORTS_COLOR ? `${open}${s}${close}` : String(s);
5
3
  export const bold = wrap("\x1b[1m", "\x1b[22m");
6
4
  export const dim = wrap("\x1b[2m", "\x1b[22m");
7
5
  export const red = wrap("\x1b[31m", "\x1b[39m");
@@ -19,17 +17,17 @@ export function write(s) {
19
17
  process.stdout.write(s);
20
18
  }
21
19
  export function log(s = "") {
22
- process.stdout.write(s + "\n");
20
+ process.stdout.write(`${s}\n`);
23
21
  }
24
22
  export function logError(s) {
25
- process.stderr.write(red("✗ ") + s + "\n");
23
+ process.stderr.write(`${red("✗ ") + s}\n`);
26
24
  }
27
25
  export function logWarn(s) {
28
- process.stdout.write(yellow("⚠ ") + s + "\n");
26
+ process.stdout.write(`${yellow("⚠ ") + s}\n`);
29
27
  }
30
28
  export function logOk(s) {
31
- process.stdout.write(green("✓ ") + s + "\n");
29
+ process.stdout.write(`${green("✓ ") + s}\n`);
32
30
  }
33
31
  export function logInfo(s) {
34
- process.stdout.write(cyan("ℹ ") + s + "\n");
32
+ process.stdout.write(`${cyan("ℹ ") + s}\n`);
35
33
  }
@@ -1,7 +1,7 @@
1
1
  import { fetchManifest } from "../api.js";
2
- import { detectDrift } from "../installer.js";
3
2
  import { hasAuth } from "../auth.js";
4
3
  import { bold, cyan, dim, yellow } from "../colors.js";
4
+ import { detectDrift } from "../installer.js";
5
5
  import { COMMAND_NAME } from "../lib.js";
6
6
  export async function runCheck() {
7
7
  try {
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { COMMAND_NAME, getClaudeSettingsPath, getPackageVersion, PRODUCT_NAME, } from "../lib.js";
3
2
  import { logError, logOk, logWarn } from "../colors.js";
3
+ import { COMMAND_NAME, PRODUCT_NAME, getClaudeSettingsPath, getPackageVersion } from "../lib.js";
4
4
  function hookCommandFor(mode) {
5
5
  const v = getPackageVersion();
6
6
  const parts = v.split(".");
@@ -8,7 +8,7 @@ function hookCommandFor(mode) {
8
8
  const minor = parts[1] ?? "0";
9
9
  const range = `~${major}.${minor}.0`;
10
10
  const verb = mode === "auto" ? "update --yes" : "check";
11
- return `pnpm dlx ${COMMAND_NAME}@${range} ${verb}`;
11
+ return `pnpm dlx --config.minimumReleaseAge=0 ${COMMAND_NAME}@${range} ${verb}`;
12
12
  }
13
13
  const HOOK_SIGNATURE = COMMAND_NAME;
14
14
  function readSettings() {
@@ -24,7 +24,7 @@ function readSettings() {
24
24
  }
25
25
  }
26
26
  function writeSettings(s) {
27
- writeFileSync(getClaudeSettingsPath(), JSON.stringify(s, null, 2) + "\n", "utf-8");
27
+ writeFileSync(getClaudeSettingsPath(), `${JSON.stringify(s, null, 2)}\n`, "utf-8");
28
28
  }
29
29
  function isOurMatcher(m) {
30
30
  if (!m || typeof m !== "object")
@@ -49,8 +49,7 @@ export function runHookInstall(mode = "check") {
49
49
  const compact = process.env.CLAUDECODE === "1" || process.env.CLAUDE_CODE === "1";
50
50
  if (typeof existing === "string") {
51
51
  if (!existing.includes(HOOK_SIGNATURE)) {
52
- logWarn(`Ya hay un SessionStart hook configurado (formato string legacy):\n ${existing}\n` +
53
- ` No lo sobrescribo. Conviértelo a formato array manualmente.`);
52
+ logWarn(`Ya hay un SessionStart hook configurado (formato string legacy):\n ${existing}\n No lo sobrescribo. Conviértelo a formato array manualmente.`);
54
53
  return 1;
55
54
  }
56
55
  settings.hooks.SessionStart = [buildOurMatcher(command)];
@@ -95,7 +94,7 @@ export function runHookUninstall() {
95
94
  logWarn(`El SessionStart hook actual no es de ${PRODUCT_NAME}. No lo toco.`);
96
95
  return 1;
97
96
  }
98
- delete settings.hooks.SessionStart;
97
+ settings.hooks.SessionStart = undefined;
99
98
  writeSettings(settings);
100
99
  logOk("Hook removido.");
101
100
  return 0;
@@ -110,7 +109,7 @@ export function runHookUninstall() {
110
109
  return 1;
111
110
  }
112
111
  if (filtered.length === 0) {
113
- delete settings.hooks.SessionStart;
112
+ settings.hooks.SessionStart = undefined;
114
113
  }
115
114
  else {
116
115
  settings.hooks.SessionStart = filtered;
@@ -1,7 +1,7 @@
1
1
  import { AuthRequiredError } from "../api.js";
2
- import { hasAuth, readAuth } from "../auth.js";
3
- import { installAll } from "../installer.js";
2
+ import { hasValidAuth, readAuth } from "../auth.js";
4
3
  import { bold, cyan, dim, gray, green, log, logError, red, yellow } from "../colors.js";
4
+ import { installAll } from "../installer.js";
5
5
  import { COMMAND_NAME, formatBytes, getClaudeSkillsDir, getPackageVersion } from "../lib.js";
6
6
  import { runHookInstall } from "./hook.js";
7
7
  import { runLogin } from "./login.js";
@@ -11,10 +11,10 @@ const SHORT_DESCRIPTIONS = {
11
11
  "alegra-sales": "New logos, MRR, deals, funnel, cuota",
12
12
  "alegra-accountant": "Programa Contadores, FC 2.0, halo",
13
13
  "alegra-growth": "Signups, MQL/PQL/SQL, conversión",
14
- "alegra-ce-universe": "CHS, churn, retención, voice of customer",
15
- "alegra-producto": "Métricas SaaS — MRR, ARPA, churn rate",
14
+ "alegra-cx": "CHS, churn, retención, voice of customer",
15
+ "alegra-product": "Métricas SaaS — MRR, ARPA, churn rate",
16
16
  "alegra-strategic": "MRR walk, cross-sell, upsell, pricing",
17
- "alanube": "Tablas Alanube en Redshift",
17
+ alanube: "Tablas Alanube en Redshift",
18
18
  };
19
19
  function padToWidth(text, visibleLen, width) {
20
20
  const pad = Math.max(0, width - visibleLen);
@@ -26,21 +26,18 @@ function fancyBanner() {
26
26
  const nameLen = COMMAND_NAME.length;
27
27
  const versionLen = `v${v}`.length;
28
28
  const spacesBetween = Math.max(1, innerWidth - 4 - nameLen - versionLen);
29
- const line1 = " " +
30
- bold(COMMAND_NAME) +
31
- " ".repeat(spacesBetween) +
32
- dim(`v${v}`);
29
+ const line1 = ` ${bold(COMMAND_NAME)}${" ".repeat(spacesBetween)}${dim(`v${v}`)}`;
33
30
  const line1PadCount = nameLen + spacesBetween + versionLen + 3;
34
31
  const line2Plain = " Agente de Data · Alegra";
35
- const line2 = " " + gray("Agente de Data · Alegra");
32
+ const line2 = ` ${gray("Agente de Data · Alegra")}`;
36
33
  const emptyPad = innerWidth + 2;
37
34
  log("");
38
- log(" " + cyan("╔" + "═".repeat(BANNER_WIDTH) + "╗"));
39
- log(" " + cyan("║") + " ".repeat(emptyPad) + cyan("║"));
40
- log(" " + cyan("║") + padToWidth(line1, line1PadCount, innerWidth + 2) + cyan("║"));
41
- log(" " + cyan("║") + padToWidth(line2, line2Plain.length, innerWidth + 2) + cyan("║"));
42
- log(" " + cyan("║") + " ".repeat(emptyPad) + cyan("║"));
43
- log(" " + cyan("╚" + "═".repeat(BANNER_WIDTH) + "╝"));
35
+ log(` ${cyan(`╔${"═".repeat(BANNER_WIDTH)}╗`)}`);
36
+ log(` ${cyan("║")}${" ".repeat(emptyPad)}${cyan("║")}`);
37
+ log(` ${cyan("║")}${padToWidth(line1, line1PadCount, innerWidth + 2)}${cyan("║")}`);
38
+ log(` ${cyan("║")}${padToWidth(line2, line2Plain.length, innerWidth + 2)}${cyan("║")}`);
39
+ log(` ${cyan("║")}${" ".repeat(emptyPad)}${cyan("║")}`);
40
+ log(` ${cyan(`╚${"═".repeat(BANNER_WIDTH)}╝`)}`);
44
41
  log("");
45
42
  }
46
43
  function section(title) {
@@ -52,7 +49,7 @@ function shortDesc(name, manifestDesc) {
52
49
  if (handPicked)
53
50
  return handPicked;
54
51
  const oneLine = manifestDesc.split(/\.|\n/)[0].trim();
55
- return oneLine.length > 55 ? oneLine.slice(0, 54) + "…" : oneLine;
52
+ return oneLine.length > 55 ? `${oneLine.slice(0, 54)}…` : oneLine;
56
53
  }
57
54
  function printResultRow(result, manifest) {
58
55
  const isSkill = result.kind === "skill";
@@ -61,7 +58,7 @@ function printResultRow(result, manifest) {
61
58
  ? entry
62
59
  ? shortDesc(result.name, entry.description)
63
60
  : ""
64
- : entry?.description ?? "MCP server";
61
+ : (entry?.description ?? "MCP server");
65
62
  let icon;
66
63
  switch (result.status) {
67
64
  case "installed":
@@ -82,7 +79,7 @@ function printResultRow(result, manifest) {
82
79
  }
83
80
  const kindBadge = isSkill ? "" : dim("[mcp] ");
84
81
  const padName = result.name.padEnd(22);
85
- log(` ${icon} ${kindBadge}${bold(padName)} ${gray("· " + desc)}`);
82
+ log(` ${icon} ${kindBadge}${bold(padName)} ${gray(`· ${desc}`)}`);
86
83
  }
87
84
  function printSummary(results, manifest) {
88
85
  const installed = results.filter((r) => r.status === "installed").length;
@@ -114,22 +111,24 @@ function printSummary(results, manifest) {
114
111
  parts.push(yellow(`${skipped} omitidas`));
115
112
  if (failed > 0)
116
113
  parts.push(red(`${failed} fallidas`));
117
- log(" " +
118
- green("✓") +
119
- " " +
120
- bold(parts.join(gray(" · "))) +
121
- gray(` · ${totalFiles} archivos · ${formatBytes(totalBytes)}`));
122
- log(" " + green("✓") + " " + gray("destino:") + " " + getClaudeSkillsDir());
114
+ log(` ${green("✓")} ${bold(parts.join(gray(" · ")))}${gray(` · ${totalFiles} archivos · ${formatBytes(totalBytes)}`)}`);
115
+ log(` ${green("✓")} ${gray("destino:")} ${getClaudeSkillsDir()}`);
123
116
  log("");
124
117
  for (const r of results.filter((x) => x.status === "failed")) {
125
118
  logError(`${r.name}: ${r.error}`);
126
119
  }
127
120
  }
128
121
  function printNextSteps(noHook) {
129
- section("Siguientes pasos");
130
- log(" " + bold(yellow("→ Reinicia Claude Code para activar las skills")));
122
+ section("Ya estás listo");
123
+ log(` ${bold(yellow("1."))} ${bold("Reiniciá tu cliente")} ${gray("(Claude Code, Cursor, OpenCode o Antigravity)")}.`);
124
+ log(` ${bold(yellow("2."))} ${bold("Hacele tu pregunta en español.")}`);
125
+ log(` ${gray("Ejemplo:")} ${cyan('"cuántos nuevos clientes en mayo en Colombia?"')}`);
126
+ log("");
127
+ log(` ${gray("La primera vez que use un servicio (Metabase, Amplitude, HubSpot, Wiki),")}`);
128
+ log(` ${gray("te va a pedir 1 click para darle permiso con tu cuenta")} ${bold("@alegra.com")}${gray(".")}`);
129
+ log(` ${gray("Después no te pide nada más.")}`);
131
130
  log("");
132
- log(" " + dim("Comandos útiles:"));
131
+ log(` ${dim("Comandos útiles:")}`);
133
132
  log(` ${dim("$")} ${bold(`${COMMAND_NAME} list`)} ${gray("ver skills disponibles")}`);
134
133
  log(` ${dim("$")} ${bold(`${COMMAND_NAME} status`)} ${gray("estado actual + drift")}`);
135
134
  log(` ${dim("$")} ${bold(`${COMMAND_NAME} update`)} ${gray("actualizar a la última")}`);
@@ -138,9 +137,7 @@ function printNextSteps(noHook) {
138
137
  log(` ${dim("$")} ${bold(`${COMMAND_NAME} hook uninstall`)} ${gray("apagar aviso")}`);
139
138
  }
140
139
  log("");
141
- log(" " +
142
- dim("Docs · ") +
143
- cyan("github.com/alegradev/alegra-data-agent"));
140
+ log(` ${dim("Docs · ")}${cyan("github.com/alegradev/alegra-data-agent")}`);
144
141
  log("");
145
142
  }
146
143
  function isClaudeCode() {
@@ -150,9 +147,9 @@ export async function runInstall(opts = {}) {
150
147
  const compact = isClaudeCode();
151
148
  if (!compact)
152
149
  fancyBanner();
153
- if (!hasAuth()) {
150
+ if (!hasValidAuth()) {
154
151
  if (!compact)
155
- log(dim(" No hay sesión activa. Inicio el flow de login antes de instalar.\n"));
152
+ log(dim(" No hay sesión activa (o expiró). Inicio el login antes de instalar.\n"));
156
153
  const code = await runLogin({});
157
154
  if (code !== 0)
158
155
  return code;
@@ -160,7 +157,7 @@ export async function runInstall(opts = {}) {
160
157
  const auth = readAuth();
161
158
  if (auth && !compact) {
162
159
  section("Cuenta autorizada");
163
- log(" " + green("✓") + " " + bold(auth.email));
160
+ log(` ${green("✓")} ${bold(auth.email)}`);
164
161
  log("");
165
162
  }
166
163
  if (opts.dryRun) {
@@ -169,33 +166,50 @@ export async function runInstall(opts = {}) {
169
166
  if (!compact) {
170
167
  section("Descargando skills del gateway");
171
168
  }
172
- let result;
173
- try {
174
- result = await installAll({
175
- dryRun: opts.dryRun,
176
- force: opts.force,
177
- only: opts.only,
178
- targets: opts.targets,
179
- });
180
- }
181
- catch (err) {
182
- if (err instanceof AuthRequiredError) {
183
- logError(err.message);
184
- log(dim(` Corre: ${COMMAND_NAME} login`));
185
- return 2;
186
- }
169
+ const installOpts = {
170
+ dryRun: opts.dryRun,
171
+ force: opts.force,
172
+ only: opts.only,
173
+ targets: opts.targets,
174
+ };
175
+ const reportInstallError = (err) => {
187
176
  const msg = err instanceof Error ? err.message : String(err);
188
177
  logError(msg);
189
178
  if (msg.includes("EACCES") || msg.includes("permission denied")) {
190
- log(dim(` Sin permisos para escribir. Verifica permisos de ~/.claude/skills/`));
179
+ log(dim(" Sin permisos para escribir. Verifica permisos de ~/.claude/skills/"));
191
180
  }
192
181
  else if (msg.includes("ENOSPC") || msg.includes("no space")) {
193
- log(dim(` No hay espacio en disco. Libera espacio y reintenta.`));
182
+ log(dim(" No hay espacio en disco. Libera espacio y reintenta."));
194
183
  }
195
- else if (msg.includes("network") || msg.includes("ENOTFOUND") || msg.includes("ECONNREFUSED")) {
196
- log(dim(` Verifica tu conexión a internet y el estado del gateway.`));
184
+ else if (msg.includes("network") ||
185
+ msg.includes("ENOTFOUND") ||
186
+ msg.includes("ECONNREFUSED")) {
187
+ log(dim(" Verifica tu conexión a internet y el estado del gateway."));
197
188
  }
198
189
  return 1;
190
+ };
191
+ let result;
192
+ try {
193
+ result = await installAll(installOpts);
194
+ }
195
+ catch (err) {
196
+ if (!(err instanceof AuthRequiredError))
197
+ return reportInstallError(err);
198
+ if (!compact)
199
+ log(dim(" La sesión expiró durante la sincronización; reabro el login y reintento.\n"));
200
+ const code = await runLogin({ force: true });
201
+ if (code !== 0)
202
+ return code;
203
+ try {
204
+ result = await installAll(installOpts);
205
+ }
206
+ catch (err2) {
207
+ if (err2 instanceof AuthRequiredError) {
208
+ logError(err2.message);
209
+ return 2;
210
+ }
211
+ return reportInstallError(err2);
212
+ }
199
213
  }
200
214
  if ("error" in result) {
201
215
  logError(result.error);
@@ -217,7 +231,7 @@ export async function runInstall(opts = {}) {
217
231
  return 0;
218
232
  if (!opts.noHook) {
219
233
  if (!compact)
220
- log(" " + gray("Instalando hook de notificaciones…"));
234
+ log(` ${gray("Instalando hook de notificaciones…")}`);
221
235
  runHookInstall(opts.hookMode ?? "check");
222
236
  if (!compact)
223
237
  log("");
@@ -245,27 +259,23 @@ function printCompactSummary(results, manifest, email) {
245
259
  }
246
260
  }
247
261
  const verbo = updated > 0 && installed === 0 ? "actualizadas" : "instaladas";
248
- const skillList = skillOks.map((r) => r.name).sort().join(", ");
262
+ const skillList = skillOks
263
+ .map((r) => r.name)
264
+ .sort()
265
+ .join(", ");
249
266
  const userPart = email ? gray(` · ${email}`) : "";
250
267
  log("");
251
- log(" " +
252
- green("✓") +
253
- " " +
254
- bold(`${skillOks.length} skills + ${mcpOks.length} MCPs ${verbo}`) +
255
- gray(` · ${totalFiles} archivos · ${formatBytes(totalBytes)}`) +
256
- userPart);
268
+ log(` ${green("✓")} ${bold(`${skillOks.length} skills + ${mcpOks.length} MCPs ${verbo}`)}${gray(` · ${totalFiles} archivos · ${formatBytes(totalBytes)}`)}${userPart}`);
257
269
  if (skillList)
258
- log(" " + gray("skills: ") + gray(skillList));
270
+ log(` ${gray("skills: ")}${gray(skillList)}`);
259
271
  if (mcpOks.length > 0)
260
- log(" " + gray("mcps: ") + gray(mcpOks.map((r) => r.name).sort().join(", ")));
261
- log(" " +
262
- gray("destino:") +
263
- " " +
264
- cyan(getClaudeSkillsDir()) +
265
- gray(" · ") +
266
- bold(yellow("reinicia tu cliente para activarlas")));
272
+ log(` ${gray("mcps: ")}${gray(mcpOks
273
+ .map((r) => r.name)
274
+ .sort()
275
+ .join(", "))}`);
276
+ log(` ${gray("destino:")} ${cyan(getClaudeSkillsDir())}${gray(" · ")}${bold(yellow("reinicia tu cliente para activarlas"))}`);
267
277
  if (unchanged > 0 && installed === 0 && updated === 0) {
268
- log(" " + dim(`(${unchanged} sin cambios desde la última instalación)`));
278
+ log(` ${dim(`(${unchanged} sin cambios desde la última instalación)`)}`);
269
279
  }
270
280
  log("");
271
281
  for (const r of failed) {
@@ -1,6 +1,6 @@
1
1
  import { AuthRequiredError, fetchManifest } from "../api.js";
2
- import { readLockFile, formatBytes } from "../lib.js";
3
2
  import { bold, cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
3
+ import { formatBytes, readLockFile } from "../lib.js";
4
4
  export async function runList() {
5
5
  let manifest;
6
6
  try {
@@ -21,7 +21,7 @@ export async function runList() {
21
21
  const lock = readLockFile();
22
22
  log("");
23
23
  log(bold(cyan(" Skills disponibles")) + dim(` — ${Object.keys(manifest.skills).length} totales`));
24
- log(gray(" " + "─".repeat(70)));
24
+ log(gray(` ${"─".repeat(70)}`));
25
25
  const names = Object.keys(manifest.skills).sort();
26
26
  for (const name of names) {
27
27
  const entry = manifest.skills[name];
@@ -36,7 +36,7 @@ export async function runList() {
36
36
  badge = yellow("[actualizar] ");
37
37
  const team = entry.team ? gray(` ${entry.team}`) : "";
38
38
  log(` ${badge} ${bold(name)}${team} ${dim(formatBytes(entry.bytes))}`);
39
- const desc = entry.description.length > 200 ? entry.description.slice(0, 200) + "…" : entry.description;
39
+ const desc = entry.description.length > 200 ? `${entry.description.slice(0, 200)}…` : entry.description;
40
40
  log(gray(` ${desc}`));
41
41
  log("");
42
42
  }
@@ -1,18 +1,21 @@
1
- import { performBrowserLogin, readAuth, writeAuth } from "../auth.js";
2
- import { banner } from "../ui.js";
3
- import { bold, dim, green, log, logError } from "../colors.js";
1
+ import { isAuthExpired, performBrowserLogin, readAuth, writeAuth } from "../auth.js";
2
+ import { bold, dim, green, log, logError, logWarn } from "../colors.js";
4
3
  import { GATEWAY_URL, getAuthFilePath } from "../lib.js";
4
+ import { banner } from "../ui.js";
5
5
  export async function runLogin(opts = {}) {
6
6
  banner();
7
7
  if (!opts.force) {
8
8
  const existing = readAuth();
9
- if (existing) {
9
+ if (existing && !isAuthExpired(existing)) {
10
10
  log(` ${green("✓")} Ya estás autenticado como ${bold(existing.email)}.`);
11
11
  log(dim(` Token: ${getAuthFilePath()}`));
12
- log(dim(` Para volver a iniciar sesión: --force`));
12
+ log(dim(" Para volver a iniciar sesión: --force"));
13
13
  log("");
14
14
  return 0;
15
15
  }
16
+ if (existing) {
17
+ logWarn(`Tu sesión (${existing.email}) expiró — reabro el login.`);
18
+ }
16
19
  }
17
20
  log(` Gateway: ${dim(GATEWAY_URL)}`);
18
21
  try {
@@ -1,13 +1,13 @@
1
1
  import { AuthRequiredError, fetchManifest } from "../api.js";
2
2
  import { readAuth } from "../auth.js";
3
+ import { bold, cyan, dim, gray, green, log, logError, logWarn, yellow } from "../colors.js";
3
4
  import { detectDrift } from "../installer.js";
4
5
  import { COMMAND_NAME, GATEWAY_URL, getClaudeSkillsDir, getLockFilePath, getPackageVersion, readLockFile, } from "../lib.js";
5
- import { bold, cyan, dim, gray, green, log, logError, logWarn, yellow } from "../colors.js";
6
6
  export async function runStatus() {
7
7
  const auth = readAuth();
8
8
  log("");
9
9
  log(bold(cyan(" Estado")));
10
- log(gray(" " + "─".repeat(70)));
10
+ log(gray(` ${"─".repeat(70)}`));
11
11
  log(` ${gray("Paquete:")} ${bold(COMMAND_NAME)} v${getPackageVersion()}`);
12
12
  log(` ${gray("Gateway:")} ${GATEWAY_URL}`);
13
13
  log(` ${gray("Autenticado:")} ${auth ? green(auth.email) : gray("no")}`);
@@ -1,7 +1,10 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { dim, gray, log, logOk, logWarn, red } from "../colors.js";
1
5
  import { uninstallAll } from "../installer.js";
2
6
  import { readLockFile } from "../lib.js";
3
7
  import { banner, confirm } from "../ui.js";
4
- import { dim, gray, log, logOk, logWarn, red } from "../colors.js";
5
8
  export async function runUninstall(opts = {}) {
6
9
  banner();
7
10
  const lock = readLockFile();
@@ -22,7 +25,26 @@ export async function runUninstall(opts = {}) {
22
25
  }
23
26
  }
24
27
  const results = uninstallAll({ dryRun: opts.dryRun });
28
+ if (!opts.dryRun)
29
+ removeNpmrcCooldownExclude();
25
30
  log("");
26
- logOk(`${results.length} skill(s) eliminadas.`);
31
+ if (opts.dryRun) {
32
+ logOk(`${results.length} skill(s) se eliminarían (dry-run, nada borrado).`);
33
+ }
34
+ else {
35
+ logOk(`${results.length} skill(s) eliminadas.`);
36
+ }
27
37
  return 0;
28
38
  }
39
+ function removeNpmrcCooldownExclude() {
40
+ const npmrc = join(homedir(), ".npmrc");
41
+ if (!existsSync(npmrc))
42
+ return;
43
+ const EXCLUDE = "minimum-release-age-exclude[]=alegra-data-agent";
44
+ const lines = readFileSync(npmrc, "utf-8").split("\n");
45
+ const kept = lines.filter((l) => l.trim() !== EXCLUDE);
46
+ if (kept.length !== lines.length) {
47
+ writeFileSync(npmrc, kept.join("\n"));
48
+ logOk("Removido minimum-release-age-exclude de ~/.npmrc");
49
+ }
50
+ }
@@ -1,21 +1,41 @@
1
1
  import { AuthRequiredError, fetchManifest } from "../api.js";
2
+ import { hasValidAuth } from "../auth.js";
3
+ import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
2
4
  import { detectDrift, installAll, summarize } from "../installer.js";
3
5
  import { banner } from "../ui.js";
4
- import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
6
+ import { runLogin } from "./login.js";
5
7
  export async function runUpdate(opts = {}) {
6
8
  banner();
7
- let manifest;
8
- try {
9
- manifest = await fetchManifest();
9
+ if (!hasValidAuth()) {
10
+ const code = await runLogin({});
11
+ if (code !== 0)
12
+ return code;
10
13
  }
11
- catch (err) {
12
- if (err instanceof AuthRequiredError) {
13
- logError(err.message);
14
- return 2;
14
+ let reauthed = false;
15
+ for (;;) {
16
+ try {
17
+ return await runUpdateOnce(opts);
18
+ }
19
+ catch (err) {
20
+ if (err instanceof AuthRequiredError && !reauthed) {
21
+ reauthed = true;
22
+ log(dim(" La sesión expiró durante la actualización; reabro el login y reintento.\n"));
23
+ const code = await runLogin({ force: true });
24
+ if (code !== 0)
25
+ return code;
26
+ continue;
27
+ }
28
+ if (err instanceof AuthRequiredError) {
29
+ logError(err.message);
30
+ return 2;
31
+ }
32
+ logError(err instanceof Error ? err.message : String(err));
33
+ return 1;
15
34
  }
16
- logError(err instanceof Error ? err.message : String(err));
17
- return 1;
18
35
  }
36
+ }
37
+ async function runUpdateOnce(opts) {
38
+ const manifest = await fetchManifest();
19
39
  if (!manifest) {
20
40
  logError("No se pudo cargar el manifest.");
21
41
  return 1;
package/dist/installer.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
- import { copyFileSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
+ import { copyFileSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve, sep } from "node:path";
4
4
  import { fetchManifest, fetchSkillFile } from "./api.js";
5
5
  import { dim, gray, green, logError, red, yellow } from "./colors.js";
6
6
  import { ensureDir, getLockFilePath, getPackageVersion, readLockFile, sha256File, writeLockFile, } from "./lib.js";
7
- import { renderMcpForTarget } from "./render.js";
8
- import { detectInstalledTargets, TARGETS } from "./targets.js";
7
+ import { inlineReferences, renderMcpForTarget } from "./render.js";
8
+ import { TARGETS, detectInstalledTargets } from "./targets.js";
9
9
  const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
10
10
  const MCP_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
11
11
  function validateSkillName(name) {
@@ -32,6 +32,14 @@ function validateRelPath(rel) {
32
32
  }
33
33
  }
34
34
  }
35
+ function mcpConfigHash(mcp) {
36
+ const salient = JSON.stringify({
37
+ url: mcp.url,
38
+ type: mcp.auth.type,
39
+ tokenEnv: mcp.auth.tokenEnv ?? null,
40
+ });
41
+ return createHash("sha256").update(salient).digest("hex");
42
+ }
35
43
  function safeJoinSkillFile(skillRoot, rel) {
36
44
  const candidate = resolve(skillRoot, rel);
37
45
  const root = resolve(skillRoot);
@@ -95,17 +103,21 @@ function writeSkillToTarget(skillName, staged, spec) {
95
103
  if (!entry) {
96
104
  throw new Error(`Para target ${spec.id} se requiere SKILL.md o README.md en la skill`);
97
105
  }
106
+ const references = staged
107
+ .filter((f) => f.rel !== "SKILL.md" && f.rel !== "README.md")
108
+ .map((f) => ({ rel: f.rel, text: new TextDecoder().decode(f.data) }));
98
109
  const content = new TextDecoder().decode(entry.data);
99
- const rendered = renderSkillForTargetFromContent(content, skillName, spec.skillFormat);
110
+ const rendered = renderSkillForTargetFromContent(content, skillName, spec.skillFormat, references);
100
111
  ensureDir(spec.skillsDir);
101
112
  const dst = join(spec.skillsDir, rendered.filename);
102
113
  atomicWriteFile(dst, rendered.content);
103
114
  }
104
- function renderSkillForTargetFromContent(content, skillName, format) {
115
+ function renderSkillForTargetFromContent(content, skillName, format, references = []) {
105
116
  if (format === "folder") {
106
117
  throw new Error("renderSkillForTargetFromContent no soporta 'folder'");
107
118
  }
108
119
  const stripped = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").replace(/^\s+/, "");
120
+ const body = stripped + inlineReferences(references);
109
121
  const fm = parseFrontmatterMinimal(content);
110
122
  if (format === "cursor-mdc") {
111
123
  const desc = fm.description ?? `Skill ${skillName}`;
@@ -113,10 +125,10 @@ function renderSkillForTargetFromContent(content, skillName, format) {
113
125
  const safeDesc = cursorDesc.replace(/\n/g, " ").replace(/"/g, '\\"');
114
126
  return {
115
127
  filename: `${skillName}.mdc`,
116
- content: `---\ndescription: "${safeDesc}"\nalwaysApply: false\n---\n\n${stripped}`,
128
+ content: `---\ndescription: "${safeDesc}"\nalwaysApply: false\n---\n\n${body}`,
117
129
  };
118
130
  }
119
- return { filename: `${skillName}.md`, content: stripped };
131
+ return { filename: `${skillName}.md`, content: body };
120
132
  }
121
133
  function parseFrontmatterMinimal(content) {
122
134
  const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
@@ -156,14 +168,21 @@ function readJsonOrEmpty(path) {
156
168
  }
157
169
  function registerMcpInTarget(mcp, spec) {
158
170
  validateMcpName(mcp.name);
171
+ if (spec.mcpMechanism === "toml-block") {
172
+ registerMcpInTomlBlock(mcp, spec);
173
+ return;
174
+ }
159
175
  const cfg = readJsonOrEmpty(spec.mcpConfigPath);
160
176
  const section = cfg[spec.mcpRootKey] ?? {};
161
177
  section[mcp.name] = renderMcpForTarget(mcp, spec);
162
178
  cfg[spec.mcpRootKey] = section;
163
179
  backupIfExists(spec.mcpConfigPath);
164
- atomicWriteFile(spec.mcpConfigPath, JSON.stringify(cfg, null, 2) + "\n");
180
+ atomicWriteFile(spec.mcpConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
165
181
  }
166
182
  function unregisterMcpInTarget(mcpName, spec) {
183
+ if (spec.mcpMechanism === "toml-block") {
184
+ return unregisterMcpInTomlBlock(mcpName, spec);
185
+ }
167
186
  if (!existsSync(spec.mcpConfigPath))
168
187
  return false;
169
188
  const cfg = readJsonOrEmpty(spec.mcpConfigPath);
@@ -173,13 +192,82 @@ function unregisterMcpInTarget(mcpName, spec) {
173
192
  delete section[mcpName];
174
193
  cfg[spec.mcpRootKey] = section;
175
194
  backupIfExists(spec.mcpConfigPath);
176
- atomicWriteFile(spec.mcpConfigPath, JSON.stringify(cfg, null, 2) + "\n");
195
+ atomicWriteFile(spec.mcpConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
196
+ return true;
197
+ }
198
+ function tomlBasicString(s) {
199
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
200
+ }
201
+ export function renderMcpToml(mcp) {
202
+ const lines = [`[mcp_servers.${mcp.name}]`, `url = ${tomlBasicString(mcp.url)}`];
203
+ if (mcp.auth.type === "gateway") {
204
+ if (!mcp.auth.tokenEnv) {
205
+ throw new Error(`MCP '${mcp.name}' es type=gateway pero no trae tokenEnv`);
206
+ }
207
+ lines.push(`bearer_token_env_var = ${tomlBasicString(mcp.auth.tokenEnv)}`);
208
+ }
209
+ return lines.join("\n");
210
+ }
211
+ function tomlMarkers(name) {
212
+ return {
213
+ begin: `# >>> alegra-data-agent:${name} >>>`,
214
+ end: `# <<< alegra-data-agent:${name} <<<`,
215
+ };
216
+ }
217
+ export function upsertTomlBlock(content, name, block) {
218
+ const { begin, end } = tomlMarkers(name);
219
+ const fenced = `${begin}\n${block}\n${end}`;
220
+ const bi = content.indexOf(begin);
221
+ if (bi !== -1) {
222
+ const ei = content.indexOf(end, bi);
223
+ if (ei !== -1) {
224
+ return content.slice(0, bi) + fenced + content.slice(ei + end.length);
225
+ }
226
+ }
227
+ if (content.length === 0)
228
+ return `${fenced}\n`;
229
+ const sep = content.endsWith("\n\n") ? "" : content.endsWith("\n") ? "\n" : "\n\n";
230
+ return `${content}${sep}${fenced}\n`;
231
+ }
232
+ export function removeTomlBlock(content, name) {
233
+ const { begin, end } = tomlMarkers(name);
234
+ const bi = content.indexOf(begin);
235
+ if (bi === -1)
236
+ return content;
237
+ const ei = content.indexOf(end, bi);
238
+ if (ei === -1)
239
+ return content;
240
+ let start = bi;
241
+ let stop = ei + end.length;
242
+ if (content[stop] === "\n")
243
+ stop++;
244
+ if (start > 0 && content[start - 1] === "\n")
245
+ start--;
246
+ return content.slice(0, start) + content.slice(stop);
247
+ }
248
+ function registerMcpInTomlBlock(mcp, spec) {
249
+ const existing = existsSync(spec.mcpConfigPath)
250
+ ? readFileSync(spec.mcpConfigPath, "utf-8")
251
+ : "";
252
+ const next = upsertTomlBlock(existing, mcp.name, renderMcpToml(mcp));
253
+ if (next === existing)
254
+ return;
255
+ backupIfExists(spec.mcpConfigPath);
256
+ atomicWriteFile(spec.mcpConfigPath, next);
257
+ }
258
+ function unregisterMcpInTomlBlock(name, spec) {
259
+ if (!existsSync(spec.mcpConfigPath))
260
+ return false;
261
+ const existing = readFileSync(spec.mcpConfigPath, "utf-8");
262
+ const next = removeTomlBlock(existing, name);
263
+ if (next === existing)
264
+ return false;
265
+ backupIfExists(spec.mcpConfigPath);
266
+ atomicWriteFile(spec.mcpConfigPath, next);
177
267
  return true;
178
268
  }
179
269
  function resolveTargets(opts) {
180
- const ids = opts.targets?.length
181
- ? opts.targets
182
- : detectInstalledTargets();
270
+ const ids = opts.targets?.length ? opts.targets : detectInstalledTargets();
183
271
  if (ids.length === 0) {
184
272
  ids.push("claude-code");
185
273
  }
@@ -276,7 +364,7 @@ export function installMcp(mcp, opts = {}) {
276
364
  const targetsMatch = prev?.targets &&
277
365
  prev.targets.length === targetIds.length &&
278
366
  prev.targets.every((t) => targetIds.includes(t));
279
- if (prev && targetsMatch && !opts.force) {
367
+ if (prev && targetsMatch && prev.configHash === mcpConfigHash(mcp) && !opts.force) {
280
368
  return { name: mcp.name, kind: "mcp", status: "unchanged", targets: targetIds };
281
369
  }
282
370
  if (opts.dryRun) {
@@ -295,6 +383,7 @@ export function installMcp(mcp, opts = {}) {
295
383
  name: mcp.name,
296
384
  installedFrom: getPackageVersion(),
297
385
  installedAt: new Date().toISOString(),
386
+ configHash: mcpConfigHash(mcp),
298
387
  targets: targetIds,
299
388
  };
300
389
  writeLockFile(lock);
@@ -330,7 +419,7 @@ export async function installAll(opts = {}) {
330
419
  ? opts.only.filter((s) => manifest.skills[s])
331
420
  : Object.keys(manifest.skills);
332
421
  const mcpNames = opts.only && opts.only.length > 0
333
- ? opts.only.filter((s) => manifest.mcps && manifest.mcps[s])
422
+ ? opts.only.filter((s) => manifest.mcps?.[s])
334
423
  : Object.keys(manifest.mcps ?? {});
335
424
  const results = [];
336
425
  for (const name of skillNames) {
@@ -401,8 +490,18 @@ export function uninstallMcp(name, opts = {}) {
401
490
  }
402
491
  const targetIds = (prev.targets ?? ["claude-code"]);
403
492
  if (!opts.dryRun) {
404
- for (const tId of targetIds) {
405
- unregisterMcpInTarget(name, TARGETS[tId]);
493
+ try {
494
+ for (const tId of targetIds) {
495
+ unregisterMcpInTarget(name, TARGETS[tId]);
496
+ }
497
+ }
498
+ catch (err) {
499
+ return {
500
+ name,
501
+ kind: "mcp",
502
+ status: "failed",
503
+ error: err instanceof Error ? err.message : String(err),
504
+ };
406
505
  }
407
506
  delete lock.mcps[name];
408
507
  writeLockFile(lock);
@@ -449,8 +548,11 @@ export function detectDrift(manifest) {
449
548
  };
450
549
  const manifestMcps = manifest.mcps ?? {};
451
550
  for (const name of Object.keys(manifestMcps)) {
452
- if (!lock.mcps?.[name])
551
+ const prevMcp = lock.mcps?.[name];
552
+ if (!prevMcp)
453
553
  mcps.toInstall.push(name);
554
+ else if (prevMcp.configHash !== mcpConfigHash(manifestMcps[name]))
555
+ mcps.toUpdate.push(name);
454
556
  else
455
557
  mcps.unchanged.push(name);
456
558
  }
package/dist/lib.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, relative, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -7,8 +7,7 @@ export const PRODUCT_NAME = "alegra-data-agent";
7
7
  export const COMMAND_NAME = "alegra-data-agent";
8
8
  export const REPO_SLUG = "alegradev/alegra-data-agent";
9
9
  export const REPO_DEFAULT_BRANCH = "main";
10
- export const GATEWAY_URL = process.env.ALEGRA_DATA_AGENT_GATEWAY_URL ??
11
- "https://alegra-data-agent.vercel.app";
10
+ export const GATEWAY_URL = process.env.ALEGRA_DATA_AGENT_GATEWAY_URL ?? "https://alegra-data-agent.vercel.app";
12
11
  export const ALL_TARGETS = ["claude-code", "cursor", "opencode", "antigravity"];
13
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
13
  export function getPackageRoot() {
@@ -48,6 +47,14 @@ export function getAntigravitySkillsDir() {
48
47
  export function getAntigravitySettingsPath() {
49
48
  return join(homedir(), ".antigravity", "settings.json");
50
49
  }
50
+ export function getCodexSkillsDir() {
51
+ return join(homedir(), ".agents", "skills");
52
+ }
53
+ export function getCodexConfigPath() {
54
+ const codexHome = process.env.CODEX_HOME;
55
+ const base = codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
56
+ return join(base, "config.toml");
57
+ }
51
58
  export function getTargetSkillsDir(target) {
52
59
  switch (target) {
53
60
  case "claude-code":
@@ -58,6 +65,8 @@ export function getTargetSkillsDir(target) {
58
65
  return getOpenCodeSkillsDir();
59
66
  case "antigravity":
60
67
  return getAntigravitySkillsDir();
68
+ case "codex":
69
+ return getCodexSkillsDir();
61
70
  }
62
71
  }
63
72
  export function getTargetMcpConfigPath(target) {
@@ -70,6 +79,8 @@ export function getTargetMcpConfigPath(target) {
70
79
  return getOpenCodeConfigPath();
71
80
  case "antigravity":
72
81
  return getAntigravitySettingsPath();
82
+ case "codex":
83
+ return getCodexConfigPath();
73
84
  }
74
85
  }
75
86
  export function getLockFilePath() {
@@ -122,7 +133,7 @@ export function readLockFile() {
122
133
  export function writeLockFile(lock) {
123
134
  const path = getLockFilePath();
124
135
  mkdirSync(dirname(path), { recursive: true });
125
- writeFileSync(path, JSON.stringify(lock, null, 2) + "\n", "utf-8");
136
+ writeFileSync(path, `${JSON.stringify(lock, null, 2)}\n`, "utf-8");
126
137
  }
127
138
  export function sha256(buf) {
128
139
  return createHash("sha256").update(buf).digest("hex");
package/dist/main.js CHANGED
@@ -1,6 +1,6 @@
1
+ import { log, logError } from "./colors.js";
1
2
  import { ALL_TARGETS, getPackageVersion } from "./lib.js";
2
3
  import { helpText } from "./ui.js";
3
- import { log, logError } from "./colors.js";
4
4
  import { runCheck } from "./commands/check.js";
5
5
  import { runHookInstall, runHookUninstall } from "./commands/hook.js";
6
6
  import { runInstall } from "./commands/install.js";
@@ -13,7 +13,10 @@ import { runUpdate } from "./commands/update.js";
13
13
  import { runWhoami } from "./commands/whoami.js";
14
14
  function parseTargetList(raw) {
15
15
  const out = [];
16
- for (const piece of raw.split(",").map((s) => s.trim()).filter(Boolean)) {
16
+ for (const piece of raw
17
+ .split(",")
18
+ .map((s) => s.trim())
19
+ .filter(Boolean)) {
17
20
  if (ALL_TARGETS.includes(piece)) {
18
21
  out.push(piece);
19
22
  }
package/dist/render.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { extname, join } from "node:path";
3
3
  import { parseFrontmatter } from "./lib.js";
4
4
  function readSkillEntryPoint(skillDir) {
5
5
  const candidates = ["SKILL.md", "README.md"];
@@ -37,6 +37,39 @@ function stripFrontmatter(content) {
37
37
  return content;
38
38
  return m[1].replace(/^\s+/, "");
39
39
  }
40
+ const INLINE_TEXT_EXT = new Set([".md", ".sql", ".txt", ".json", ".yaml", ".yml", ".csv"]);
41
+ const INLINE_MAX_BYTES = 64 * 1024;
42
+ const FENCE_LANG = {
43
+ ".sql": "sql",
44
+ ".json": "json",
45
+ ".yaml": "yaml",
46
+ ".yml": "yaml",
47
+ ".csv": "csv",
48
+ ".txt": "",
49
+ };
50
+ export function inlineReferences(refs) {
51
+ const sections = [];
52
+ for (const ref of refs) {
53
+ const ext = extname(ref.rel).toLowerCase();
54
+ const bytes = Buffer.byteLength(ref.text, "utf-8");
55
+ if (!INLINE_TEXT_EXT.has(ext)) {
56
+ sections.push(`### \`${ref.rel}\`\n\n> (omitido: tipo \`${ext || "sin extensión"}\` no inlineable)`);
57
+ }
58
+ else if (bytes > INLINE_MAX_BYTES) {
59
+ sections.push(`### \`${ref.rel}\`\n\n> (omitido: ${Math.round(bytes / 1024)} KB; supera el límite de inline)`);
60
+ }
61
+ else if (ext === ".md") {
62
+ sections.push(`### \`${ref.rel}\`\n\n${ref.text.trim()}`);
63
+ }
64
+ else {
65
+ const lang = FENCE_LANG[ext] ?? "";
66
+ sections.push(`### \`${ref.rel}\`\n\n\`\`\`${lang}\n${ref.text.replace(/\s+$/, "")}\n\`\`\``);
67
+ }
68
+ }
69
+ if (sections.length === 0)
70
+ return "";
71
+ return `\n\n---\n\n# Referencias (incluidas inline)\n\n> Este cliente instala la skill como un solo archivo; el contenido de \`references/\` se incluye acá para que las menciones del cuerpo sigan disponibles.\n\n${sections.join("\n\n")}\n`;
72
+ }
40
73
  export function renderMcpForTarget(mcp, spec) {
41
74
  const obj = {
42
75
  [spec.mcpUrlKey]: mcp.url,
@@ -64,5 +97,7 @@ function bearerInterpolation(env, target) {
64
97
  return `Bearer \${env:${env}}`;
65
98
  case "opencode":
66
99
  return `Bearer {env:${env}}`;
100
+ case "codex":
101
+ throw new Error("codex usa config TOML (bearer_token_env_var), no headers JSON");
67
102
  }
68
103
  }
package/dist/signature.js CHANGED
@@ -5,11 +5,13 @@ export function canonicalize(obj) {
5
5
  if (obj === null || typeof obj !== "object")
6
6
  return JSON.stringify(obj);
7
7
  if (Array.isArray(obj)) {
8
- return "[" + obj.map((v) => canonicalize(v)).join(",") + "]";
8
+ return `[${obj.map((v) => canonicalize(v)).join(",")}]`;
9
9
  }
10
10
  const o = obj;
11
- const keys = Object.keys(o).filter((k) => k !== "signature").sort();
12
- return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalize(o[k])).join(",") + "}";
11
+ const keys = Object.keys(o)
12
+ .filter((k) => k !== "signature")
13
+ .sort();
14
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(o[k])}`).join(",")}}`;
13
15
  }
14
16
  const MAX_MANIFEST_AGE_DAYS = 90;
15
17
  export function verifyManifestSignature(manifest, options = {}) {
@@ -24,11 +26,14 @@ export function verifyManifestSignature(manifest, options = {}) {
24
26
  return { ok: false, reason: `algoritmo no soportado: ${sig.alg}` };
25
27
  }
26
28
  if (sig.keyId !== SIGNING_KEY_ID) {
27
- return { ok: false, reason: `keyId no reconocido: ${sig.keyId} (CLI espera ${SIGNING_KEY_ID})` };
29
+ return {
30
+ ok: false,
31
+ reason: `keyId no reconocido: ${sig.keyId} (CLI espera ${SIGNING_KEY_ID})`,
32
+ };
28
33
  }
29
34
  if (typeof manifest.generatedAt === "string") {
30
35
  const ts = Date.parse(manifest.generatedAt);
31
- if (!isNaN(ts)) {
36
+ if (!Number.isNaN(ts)) {
32
37
  const ageMs = Date.now() - ts;
33
38
  const maxAgeMs = MAX_MANIFEST_AGE_DAYS * 24 * 3600 * 1000;
34
39
  if (ageMs > maxAgeMs) {
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 { getAntigravitySettingsPath, getAntigravitySkillsDir, getClaudeSettingsPath, getClaudeSkillsDir, getCursorMcpPath, getCursorSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
4
+ import { getAntigravitySettingsPath, getAntigravitySkillsDir, getClaudeSettingsPath, getClaudeSkillsDir, getCodexConfigPath, getCodexSkillsDir, getCursorMcpPath, getCursorSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
5
5
  export const TARGETS = {
6
6
  "claude-code": {
7
7
  id: "claude-code",
@@ -40,17 +40,31 @@ export const TARGETS = {
40
40
  mcpRootKey: "mcpServers",
41
41
  mcpUrlKey: "serverUrl",
42
42
  },
43
+ codex: {
44
+ id: "codex",
45
+ label: "Codex",
46
+ skillsDir: getCodexSkillsDir(),
47
+ skillFormat: "folder",
48
+ mcpConfigPath: getCodexConfigPath(),
49
+ mcpRootKey: "mcpServers",
50
+ mcpUrlKey: "url",
51
+ mcpMechanism: "toml-block",
52
+ },
43
53
  };
44
54
  export function detectInstalledTargets() {
45
55
  const home = homedir();
46
56
  const xdgConfig = process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME.length > 0
47
57
  ? process.env.XDG_CONFIG_HOME
48
58
  : join(home, ".config");
59
+ const codexHome = process.env.CODEX_HOME && process.env.CODEX_HOME.length > 0
60
+ ? process.env.CODEX_HOME
61
+ : join(home, ".codex");
49
62
  const sentinels = {
50
63
  "claude-code": join(home, ".claude"),
51
64
  cursor: join(home, ".cursor"),
52
65
  opencode: join(xdgConfig, "opencode"),
53
66
  antigravity: join(home, ".antigravity"),
67
+ codex: codexHome,
54
68
  };
55
69
  const out = [];
56
70
  for (const id of Object.keys(sentinels)) {
package/dist/ui.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createInterface } from "node:readline/promises";
2
1
  import { stdin as input, stdout as output } from "node:process";
2
+ import { createInterface } from "node:readline/promises";
3
3
  import { bold, cyan, dim, gray, log } from "./colors.js";
4
4
  import { COMMAND_NAME, getPackageVersion } from "./lib.js";
5
5
  export function banner() {
@@ -25,7 +25,7 @@ export async function confirm(question, defaultYes = true) {
25
25
  export async function ask(question, defaultValue) {
26
26
  const rl = createInterface({ input, output });
27
27
  try {
28
- const suffix = defaultValue ? dim(`(${defaultValue})`) + " " : "";
28
+ const suffix = defaultValue ? `${dim(`(${defaultValue})`)} ` : "";
29
29
  const answer = (await rl.question(`${question} ${suffix}`)).trim();
30
30
  return answer === "" && defaultValue !== undefined ? defaultValue : answer;
31
31
  }
package/index.mjs CHANGED
@@ -8,9 +8,7 @@ const [major, minor] = process.versions.node.split(".").map(Number);
8
8
 
9
9
  if (major < 22 || (major === 22 && minor < 6)) {
10
10
  console.error(
11
- `\n ⚠ alegra-data-agent requiere Node.js >= 22.6.0.` +
12
- `\n Versión actual: ${process.version}` +
13
- `\n Actualiza desde → https://nodejs.org\n`,
11
+ `\n ⚠ alegra-data-agent requiere Node.js >= 22.6.0.\n Versión actual: ${process.version}\n Actualiza desde → https://nodejs.org\n`,
14
12
  );
15
13
  process.exit(1);
16
14
  }
@@ -27,7 +25,7 @@ if (existsSync(distPath)) {
27
25
  // Desarrollo: ejecuta el .ts directo (requiere --experimental-strip-types).
28
26
  const tsPath = join(__dirname, "src", "main.ts");
29
27
  if (!existsSync(tsPath)) {
30
- console.error(`\n ⚠ Instalación incompleta: no se encontró dist/ ni src/.\n`);
28
+ console.error("\n ⚠ Instalación incompleta: no se encontró dist/ ni src/.\n");
31
29
  process.exit(1);
32
30
  }
33
31
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alegra-data-agent",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
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",
@@ -37,7 +37,9 @@
37
37
  "manifest:generate": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/generate-manifest.mjs",
38
38
  "manifest:sign": "node scripts/sign-manifest.mjs",
39
39
  "manifest:scan": "node scripts/scan-skills.mjs",
40
- "manifest": "pnpm run manifest:scan && pnpm run manifest:generate && pnpm run manifest:sign",
40
+ "manifest:validate": "node scripts/validate-skills.mjs --legacy-ok",
41
+ "manifest:validate:strict": "node scripts/validate-skills.mjs",
42
+ "manifest": "pnpm run manifest:validate && pnpm run manifest:scan && pnpm run manifest:generate && pnpm run manifest:sign",
41
43
  "test": "node tests/runner.mjs",
42
44
  "test:report": "node tests/runner.mjs --report markdown",
43
45
  "test:security": "node tests/security/runner.mjs",