alegra-data-agent 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@ Solo accesible para cuentas `@alegra.com` (OAuth Google). El CLI te abre el nave
14
14
 
15
15
  - Node ≥ 22.6.0
16
16
  - pnpm ≥ 11.1.2
17
- - Uno de: Claude Code, Cursor, OpenCode, Antigravity
17
+ - Uno de: Claude Code, Cursor, OpenCode, Antigravity, Codex
18
18
 
19
19
  ## Soporte
20
20
 
package/dist/api.js CHANGED
@@ -79,7 +79,7 @@ async function authedFetch(path, opts = {}) {
79
79
  if (res.status === 401) {
80
80
  if (opts.requireAuth === false)
81
81
  return res;
82
- throw new AuthRequiredError(`Token expirado o inválido. Corre: ${COMMAND_NAME} login`);
82
+ throw new AuthRequiredError(`Sesión expirada. Agente: ejecutá vos mismo \`pnpm dlx ${COMMAND_NAME} login\` — al usuario solo le toca el click del navegador.`);
83
83
  }
84
84
  if (res.status === 403) {
85
85
  throw new GatewayError(403, path, "Forbidden — tu cuenta no es @alegra.com o tu acceso fue revocado. " +
package/dist/auth.js CHANGED
@@ -252,8 +252,8 @@ function htmlPage(kind, subject) {
252
252
  const color = ok ? "#10b981" : "#ef4444";
253
253
  const title = ok ? "Autenticación exitosa" : "Error";
254
254
  const body = ok
255
- ? `Conectaste como <strong>${escapeHtml(subject)}</strong>.<br/>Ya puedes cerrar esta pestaña y volver al terminal.`
256
- : `<strong>${escapeHtml(subject)}</strong><br/>Revisa el terminal e intenta de nuevo.`;
255
+ ? `Conectaste como <strong>${escapeHtml(subject)}</strong>.<br/>Ya puedes cerrar esta pestaña y volver a tu agente.`
256
+ : `<strong>${escapeHtml(subject)}</strong><br/>Volvé a tu agente e intentá de nuevo — él se encarga.`;
257
257
  return `<!doctype html>
258
258
  <html lang="es">
259
259
  <head>
package/dist/bounce.js ADDED
@@ -0,0 +1,56 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { COMMAND_NAME, getPackageVersion } from "./lib.js";
3
+ export const NO_BOUNCE_ENV = "ALEGRA_DATA_AGENT_NO_BOUNCE";
4
+ const REGISTRY_URL = `https://registry.npmjs.org/${COMMAND_NAME}`;
5
+ const FETCH_TIMEOUT_MS = 2_500;
6
+ const SEMVER_RE = /^\d+\.\d+\.\d+$/;
7
+ export function compareSemver(a, b) {
8
+ const pa = a.split(".").map((n) => Number.parseInt(n, 10));
9
+ const pb = b.split(".").map((n) => Number.parseInt(n, 10));
10
+ for (let i = 0; i < 3; i++) {
11
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
12
+ if (diff !== 0)
13
+ return diff;
14
+ }
15
+ return 0;
16
+ }
17
+ export function shouldBounce(current, latest) {
18
+ if (!latest)
19
+ return false;
20
+ if (!SEMVER_RE.test(latest))
21
+ return false;
22
+ if (!SEMVER_RE.test(current))
23
+ return false;
24
+ return compareSemver(latest, current) > 0;
25
+ }
26
+ async function fetchLatestVersion() {
27
+ try {
28
+ const res = await fetch(REGISTRY_URL, {
29
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
30
+ headers: { accept: "application/vnd.npm.install-v1+json" },
31
+ });
32
+ if (!res.ok)
33
+ return null;
34
+ const data = (await res.json());
35
+ return data["dist-tags"]?.latest ?? null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ export async function maybeBounceToLatest(argv) {
42
+ if (process.env[NO_BOUNCE_ENV] === "1")
43
+ return;
44
+ const current = getPackageVersion();
45
+ const latest = await fetchLatestVersion();
46
+ if (!shouldBounce(current, latest))
47
+ return;
48
+ const child = spawnSync("pnpm", ["dlx", "--config.minimumReleaseAge=0", `${COMMAND_NAME}@${latest}`, ...argv], {
49
+ stdio: "inherit",
50
+ env: { ...process.env, [NO_BOUNCE_ENV]: "1" },
51
+ shell: process.platform === "win32",
52
+ });
53
+ if (child.error)
54
+ return;
55
+ process.exit(child.status ?? 0);
56
+ }
@@ -24,7 +24,7 @@ export async function runCheck() {
24
24
  lines.push(` ${cyan("↻")} ${name} ${dim("(versión nueva)")}`);
25
25
  }
26
26
  lines.push("");
27
- lines.push(` ${dim("Actualiza con:")} ${bold(`${COMMAND_NAME} update`)}`);
27
+ lines.push(` ${dim("Agente: ejecutá vos mismo")} ${bold(`pnpm dlx ${COMMAND_NAME} update --yes`)} ${dim("— el usuario no corre comandos.")}`);
28
28
  lines.push("");
29
29
  process.stdout.write(lines.join("\n"));
30
30
  return 0;
@@ -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 --config.minimumReleaseAge=0 ${COMMAND_NAME}@${range} ${verb}`;
11
+ return `pnpm dlx --config.minimumReleaseAge=0 ${COMMAND_NAME}@${range} ${verb} --no-bounce`;
12
12
  }
13
13
  const HOOK_SIGNATURE = COMMAND_NAME;
14
14
  function readSettings() {
@@ -19,7 +19,7 @@ function readSettings() {
19
19
  return JSON.parse(readFileSync(path, "utf-8"));
20
20
  }
21
21
  catch {
22
- logError(`No se pudo parsear ${path}. Revisa el JSON manualmente.`);
22
+ logError(`No se pudo parsear ${path}. Agente: reparalo vos mismo (la skill agent-onboarding tiene la guía) — no le pidas al usuario editar JSON.`);
23
23
  return {};
24
24
  }
25
25
  }
@@ -41,7 +41,7 @@ function isOurMatcher(m) {
41
41
  function buildOurMatcher(command) {
42
42
  return { hooks: [{ type: "command", command }] };
43
43
  }
44
- export function runHookInstall(mode = "check") {
44
+ export function runHookInstall(mode = "auto") {
45
45
  const settings = readSettings();
46
46
  settings.hooks = settings.hooks ?? {};
47
47
  const command = hookCommandFor(mode);
@@ -49,7 +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 No lo sobrescribo. Conviértelo a formato array manualmente.`);
52
+ logWarn(`Ya hay un SessionStart hook ajeno en formato string legacy:\n ${existing}\n No lo sobrescribo. Agente: convertilo vos mismo a formato array preservando su contenido — no le pidas al usuario editar JSON.`);
53
53
  return 1;
54
54
  }
55
55
  settings.hooks.SessionStart = [buildOurMatcher(command)];
@@ -82,6 +82,29 @@ export function runHookInstall(mode = "check") {
82
82
  }
83
83
  return 0;
84
84
  }
85
+ export function refreshHookCommand() {
86
+ try {
87
+ const settings = readSettings();
88
+ if (!settings.hooks)
89
+ return;
90
+ const existing = settings.hooks.SessionStart;
91
+ if (!Array.isArray(existing))
92
+ return;
93
+ const ours = existing.find(isOurMatcher);
94
+ if (!ours)
95
+ return;
96
+ const current = ours.hooks.find((h) => h.type === "command")?.command ?? "";
97
+ const fresh = hookCommandFor("auto");
98
+ if (current === fresh)
99
+ return;
100
+ const others = existing.filter((m) => !isOurMatcher(m));
101
+ others.push(buildOurMatcher(fresh));
102
+ settings.hooks.SessionStart = others;
103
+ writeSettings(settings);
104
+ }
105
+ catch {
106
+ }
107
+ }
85
108
  export function runHookUninstall() {
86
109
  const settings = readSettings();
87
110
  const existing = settings.hooks?.SessionStart;
@@ -273,7 +273,7 @@ function printCompactSummary(results, manifest, email) {
273
273
  .map((r) => r.name)
274
274
  .sort()
275
275
  .join(", "))}`);
276
- log(` ${gray("destino:")} ${cyan(getClaudeSkillsDir())}${gray(" · ")}${bold(yellow("reinicia tu cliente para activarlas"))}`);
276
+ log(` ${gray("destino:")} ${cyan(getClaudeSkillsDir())}${gray(" · ")}${bold(yellow("reinicia tu cliente para activar skills y MCPs"))}`);
277
277
  if (unchanged > 0 && installed === 0 && updated === 0) {
278
278
  log(` ${dim(`(${unchanged} sin cambios desde la última instalación)`)}`);
279
279
  }
@@ -48,10 +48,10 @@ export async function runStatus() {
48
48
  log("");
49
49
  const total = drift.toInstall.length + drift.toUpdate.length;
50
50
  if (total > 0) {
51
- logWarn(`Hay ${total} skill(s) por actualizar. Corre: ${bold(`${COMMAND_NAME} update`)}`);
51
+ logWarn(`Hay ${total} skill(s) por actualizar. Agente: ejecutá vos mismo ${bold(`pnpm dlx ${COMMAND_NAME} update --yes`)} — el usuario no corre comandos.`);
52
52
  }
53
53
  else if (Object.keys(lock.skills).length === 0) {
54
- logWarn(`Aún no hay skills instaladas. Corre: ${bold(`${COMMAND_NAME} install`)}`);
54
+ logWarn(`Aún no hay skills instaladas. Agente: ejecutá vos mismo ${bold(`pnpm dlx ${COMMAND_NAME}`)} — el usuario no corre comandos.`);
55
55
  }
56
56
  else {
57
57
  log(` ${green("Todo al día.")}`);
@@ -3,6 +3,7 @@ import { hasValidAuth } from "../auth.js";
3
3
  import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
4
4
  import { detectDrift, installAll, summarize } from "../installer.js";
5
5
  import { banner } from "../ui.js";
6
+ import { refreshHookCommand } from "./hook.js";
6
7
  import { runLogin } from "./login.js";
7
8
  export async function runUpdate(opts = {}) {
8
9
  banner();
@@ -49,6 +50,8 @@ async function runUpdateOnce(opts) {
49
50
  ];
50
51
  if (pending.length === 0) {
51
52
  log(green(" ✓ Todo al día (skills + MCPs).\n"));
53
+ if (!opts.dryRun)
54
+ refreshHookCommand();
52
55
  return 0;
53
56
  }
54
57
  log(cyan(` → ${pending.length} elemento(s) por actualizar:\n`));
@@ -71,6 +74,7 @@ async function runUpdateOnce(opts) {
71
74
  return 1;
72
75
  }
73
76
  summarize(result.results);
77
+ refreshHookCommand();
74
78
  const failed = result.results.some((r) => r.status === "failed");
75
79
  return failed ? 1 : 0;
76
80
  }
@@ -4,7 +4,7 @@ import { COMMAND_NAME, getAuthFilePath } from "../lib.js";
4
4
  export function runWhoami() {
5
5
  const auth = readAuth();
6
6
  if (!auth) {
7
- logWarn(`No autenticado. Corre: ${COMMAND_NAME} login`);
7
+ logWarn(`No autenticado. Agente: ejecutá vos mismo \`pnpm dlx ${COMMAND_NAME} login\` — al usuario solo le toca el click del navegador.`);
8
8
  return 1;
9
9
  }
10
10
  log("");
package/dist/installer.js CHANGED
@@ -163,7 +163,7 @@ function readJsonOrEmpty(path) {
163
163
  return JSON.parse(readFileSync(path, "utf-8"));
164
164
  }
165
165
  catch {
166
- throw new Error(`${path} no es JSON válido. Arréglalo manualmente o muévelo y vuelve a instalar.`);
166
+ throw new Error(`${path} no es JSON válido. Agente: respaldalo vos mismo (renombralo a .bak) y volvé a correr la instalación — no le pidas al usuario tocar archivos.`);
167
167
  }
168
168
  }
169
169
  function registerMcpInTarget(mcp, spec) {
package/dist/lib.js CHANGED
@@ -8,7 +8,13 @@ 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
10
  export const GATEWAY_URL = process.env.ALEGRA_DATA_AGENT_GATEWAY_URL ?? "https://alegra-data-agent.vercel.app";
11
- export const ALL_TARGETS = ["claude-code", "cursor", "opencode", "antigravity"];
11
+ export const ALL_TARGETS = [
12
+ "claude-code",
13
+ "cursor",
14
+ "opencode",
15
+ "antigravity",
16
+ "codex",
17
+ ];
12
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
19
  export function getPackageRoot() {
14
20
  return join(__dirname, "..");
package/dist/main.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { maybeBounceToLatest } from "./bounce.js";
1
2
  import { log, logError } from "./colors.js";
2
3
  import { ALL_TARGETS, getPackageVersion } from "./lib.js";
3
4
  import { helpText } from "./ui.js";
@@ -34,6 +35,7 @@ function parseArgs(argv) {
34
35
  help: false,
35
36
  version: false,
36
37
  noHook: false,
38
+ noBounce: false,
37
39
  autoUpdate: false,
38
40
  };
39
41
  const positional = [];
@@ -50,6 +52,8 @@ function parseArgs(argv) {
50
52
  flags.force = true;
51
53
  else if (a === "--no-hook")
52
54
  flags.noHook = true;
55
+ else if (a === "--no-bounce")
56
+ flags.noBounce = true;
53
57
  else if (a === "--auto-update")
54
58
  flags.autoUpdate = true;
55
59
  else if (a === "-h" || a === "--help")
@@ -87,6 +91,9 @@ function parseArgs(argv) {
87
91
  }
88
92
  async function main() {
89
93
  const args = parseArgs(process.argv.slice(2));
94
+ if (!args.flags.noBounce) {
95
+ await maybeBounceToLatest(process.argv.slice(2));
96
+ }
90
97
  if (args.flags.version) {
91
98
  log(getPackageVersion());
92
99
  process.exit(0);
@@ -114,7 +121,7 @@ async function main() {
114
121
  only: args.only.length > 0 ? args.only : undefined,
115
122
  targets: args.targets.length > 0 ? args.targets : undefined,
116
123
  noHook: args.flags.noHook,
117
- hookMode: args.flags.autoUpdate ? "auto" : "check",
124
+ hookMode: "auto",
118
125
  });
119
126
  break;
120
127
  case "update":
@@ -139,7 +146,7 @@ async function main() {
139
146
  break;
140
147
  case "hook":
141
148
  if (args.subcommand === "install") {
142
- exitCode = runHookInstall(args.flags.autoUpdate ? "auto" : "check");
149
+ exitCode = runHookInstall("auto");
143
150
  }
144
151
  else if (args.subcommand === "uninstall") {
145
152
  exitCode = runHookUninstall();
package/dist/ui.js CHANGED
@@ -59,9 +59,10 @@ ${bold("Opciones:")}
59
59
  -y, --yes No pedir confirmación
60
60
  --dry-run Mostrar qué se haría, sin tocar archivos
61
61
  --only=a,b,c Instalar solo esas skills/MCPs
62
- --target=NAME Restringir al cliente NAME (claude-code|cursor|opencode|antigravity)
62
+ --target=NAME Restringir al cliente NAME (claude-code|cursor|opencode|antigravity|codex)
63
63
  --no-hook No instalar el hook de SessionStart
64
- --auto-update Hook en modo auto-update (instala en silencio)
64
+ --no-bounce No saltar a la última versión publicada (la usa el hook)
65
+ --auto-update Aceptado por compatibilidad — el hook ya es auto-update por default
65
66
  -h, --help Mostrar ayuda
66
67
  -v, --version Mostrar versión
67
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alegra-data-agent",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "private": false,
5
5
  "description": "Agente de Data interno de Alegra. Instala skills + MCPs en tu cliente de IA con un comando.",
6
6
  "license": "UNLICENSED",