alegra-data-agent 0.2.3 → 0.2.4

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
@@ -34,6 +34,7 @@ function userAgent() {
34
34
  function sleep(ms) {
35
35
  return new Promise((r) => setTimeout(r, ms));
36
36
  }
37
+ let rateLimitCooldownUntil = 0;
37
38
  async function authedFetch(path, opts = {}) {
38
39
  const auth = readAuth();
39
40
  if (!auth) {
@@ -49,6 +50,9 @@ async function authedFetch(path, opts = {}) {
49
50
  let attempt = 0;
50
51
  const delays = [250, 750, 2250];
51
52
  while (true) {
53
+ const pending = rateLimitCooldownUntil - Date.now();
54
+ if (pending > 0)
55
+ await sleep(pending);
52
56
  const ac = new AbortController();
53
57
  const timer = setTimeout(() => ac.abort(), timeoutMs);
54
58
  let res;
@@ -86,13 +90,16 @@ async function authedFetch(path, opts = {}) {
86
90
  "Si saliste de Alegra, esto es esperado. Si no, contacta a #data.");
87
91
  }
88
92
  if (res.status === 429) {
93
+ const cap = maxRetries + 4;
89
94
  const ra = parseRetryAfter(res.headers.get("retry-after"));
90
- if (ra !== null && ra <= 5_000 && attempt < maxRetries) {
91
- await sleep(ra);
95
+ const wait = ra !== null ? ra : delays[Math.min(attempt, delays.length - 1)] * 2;
96
+ rateLimitCooldownUntil = Math.max(rateLimitCooldownUntil, Date.now() + wait);
97
+ if (attempt < cap) {
98
+ await sleep(wait);
92
99
  attempt++;
93
100
  continue;
94
101
  }
95
- throw new GatewayError(429, path, "Rate limit excedido. Espera un minuto y vuelve a intentar.");
102
+ throw new GatewayError(429, path, `Rate limit del gateway tras ${cap + 1} intentos. Esperá un minuto y reintentá (request-id ${reqId}).`);
96
103
  }
97
104
  if (res.status >= 500 && res.status < 600) {
98
105
  if (attempt < maxRetries) {
@@ -0,0 +1,216 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { logWarn } from "./colors.js";
4
+ import { hookCommandFor, runHookInstall, runHookUninstall, } from "./commands/hook.js";
5
+ import { COMMAND_NAME, getClaudeSettingsPath, getCodexHooksPath, getCursorHooksPath, getGeminiSettingsPath, getOpenCodePluginPath, readLockFile, } from "./lib.js";
6
+ function readJsonSafe(path) {
7
+ if (!existsSync(path))
8
+ return {};
9
+ try {
10
+ return JSON.parse(readFileSync(path, "utf-8"));
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ function writeJson(path, data) {
17
+ mkdirSync(dirname(path), { recursive: true });
18
+ const tmp = `${path}.tmp-${process.pid}`;
19
+ writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
20
+ renameSync(tmp, path);
21
+ }
22
+ function entryIsOurs(m) {
23
+ if (!m || typeof m !== "object")
24
+ return false;
25
+ const o = m;
26
+ if (typeof o.command === "string" && o.command.includes(COMMAND_NAME))
27
+ return true;
28
+ if (Array.isArray(o.hooks)) {
29
+ return o.hooks.some((h) => h &&
30
+ typeof h === "object" &&
31
+ typeof h.command === "string" &&
32
+ h.command.includes(COMMAND_NAME));
33
+ }
34
+ return false;
35
+ }
36
+ function freshHookList(cfg, key) {
37
+ const hooks = (cfg.hooks && typeof cfg.hooks === "object" ? cfg.hooks : {});
38
+ cfg.hooks = hooks;
39
+ const list = Array.isArray(hooks[key]) ? hooks[key] : [];
40
+ return list.filter((m) => !entryIsOurs(m));
41
+ }
42
+ export function guiResilientCommand(base, opts) {
43
+ const platform = opts.platform ?? process.platform;
44
+ if (platform === "win32")
45
+ return opts.silence ? `${base} >NUL 2>&1` : base;
46
+ const extraDirs = [
47
+ dirname(process.execPath),
48
+ process.env.PNPM_HOME ?? "",
49
+ "$HOME/.local/share/pnpm",
50
+ "$HOME/Library/pnpm",
51
+ "/opt/homebrew/bin",
52
+ "/usr/local/bin",
53
+ ].filter(Boolean);
54
+ const run = opts.silence ? `${base} >/dev/null 2>&1` : base;
55
+ const logFile = `"\${XDG_CONFIG_HOME:-$HOME/.config}/${COMMAND_NAME}/autoupdate.log"`;
56
+ return (`PATH="$PATH:${extraDirs.join(":")}"; ` +
57
+ `if command -v pnpm >/dev/null 2>&1; then ${run}; ` +
58
+ `else mkdir -p "$(dirname ${logFile})" && ` +
59
+ `echo "$(date '+%Y-%m-%d %H:%M:%S') [${opts.client}] pnpm no esta en el PATH del cliente; auto-update omitido" >> ${logFile}; fi`);
60
+ }
61
+ function installCursor(command) {
62
+ const path = getCursorHooksPath();
63
+ const cfg = readJsonSafe(path);
64
+ if (cfg === null) {
65
+ logWarn(`${path} no es JSON válido; salteo el auto-update de Cursor.`);
66
+ return;
67
+ }
68
+ if (typeof cfg.version !== "number")
69
+ cfg.version = 1;
70
+ const list = freshHookList(cfg, "workspaceOpen");
71
+ list.push({ command });
72
+ cfg.hooks.workspaceOpen = list;
73
+ writeJson(path, cfg);
74
+ }
75
+ function installCodex(command) {
76
+ const path = getCodexHooksPath();
77
+ const cfg = readJsonSafe(path);
78
+ if (cfg === null) {
79
+ logWarn(`${path} no es JSON válido; salteo el auto-update de Codex.`);
80
+ return;
81
+ }
82
+ const list = freshHookList(cfg, "SessionStart");
83
+ list.push({ matcher: "startup|resume", hooks: [{ type: "command", command }] });
84
+ cfg.hooks.SessionStart = list;
85
+ writeJson(path, cfg);
86
+ }
87
+ function installAntigravity(command) {
88
+ const path = getGeminiSettingsPath();
89
+ const cfg = readJsonSafe(path);
90
+ if (cfg === null) {
91
+ logWarn(`${path} no es JSON válido; salteo el auto-update de Antigravity.`);
92
+ return;
93
+ }
94
+ const list = freshHookList(cfg, "SessionStart");
95
+ list.push({ hooks: [{ type: "command", command }] });
96
+ cfg.hooks.SessionStart = list;
97
+ writeJson(path, cfg);
98
+ }
99
+ function installOpenCode(command) {
100
+ const path = getOpenCodePluginPath();
101
+ mkdirSync(dirname(path), { recursive: true });
102
+ const plugin = `// alegra-data-agent: auto-update al iniciar OpenCode. Generado por el instalador.
103
+ // Borrar este archivo desactiva el auto-update. No editar a mano.
104
+ export const AlegraDataAgentAutoUpdate = async ({ $ }) => {
105
+ if (!$) return {}
106
+ try {
107
+ $\`${command}\`.quiet().nothrow().catch(() => {})
108
+ } catch (_e) {}
109
+ return {}
110
+ }
111
+ `;
112
+ writeFileSync(path, plugin, "utf-8");
113
+ }
114
+ function removeJsonHook(path, key) {
115
+ const cfg = readJsonSafe(path);
116
+ if (!cfg)
117
+ return;
118
+ const hooks = cfg.hooks;
119
+ if (!hooks || !Array.isArray(hooks[key]))
120
+ return;
121
+ const list = hooks[key];
122
+ const kept = list.filter((m) => !entryIsOurs(m));
123
+ if (kept.length === list.length)
124
+ return;
125
+ if (kept.length === 0)
126
+ delete hooks[key];
127
+ else
128
+ hooks[key] = kept;
129
+ writeJson(path, cfg);
130
+ }
131
+ export function installAutoUpdate(target, mode = "auto") {
132
+ const effective = target === "claude-code" ? mode : "auto";
133
+ try {
134
+ switch (target) {
135
+ case "claude-code":
136
+ runHookInstall(effective);
137
+ break;
138
+ case "cursor":
139
+ installCursor(guiResilientCommand(hookCommandFor(effective), { client: "cursor" }));
140
+ break;
141
+ case "codex":
142
+ installCodex(hookCommandFor(effective));
143
+ break;
144
+ case "antigravity":
145
+ installAntigravity(guiResilientCommand(hookCommandFor(effective), { client: "antigravity", silence: true }));
146
+ break;
147
+ case "opencode":
148
+ installOpenCode(hookCommandFor(effective));
149
+ break;
150
+ }
151
+ }
152
+ catch (err) {
153
+ logWarn(`No pude instalar el auto-update de ${target}: ${err instanceof Error ? err.message : String(err)}`);
154
+ }
155
+ }
156
+ export function removeAutoUpdate(target) {
157
+ try {
158
+ switch (target) {
159
+ case "claude-code":
160
+ runHookUninstall({ quiet: true });
161
+ break;
162
+ case "cursor":
163
+ removeJsonHook(getCursorHooksPath(), "workspaceOpen");
164
+ break;
165
+ case "codex":
166
+ removeJsonHook(getCodexHooksPath(), "SessionStart");
167
+ break;
168
+ case "antigravity":
169
+ removeJsonHook(getGeminiSettingsPath(), "SessionStart");
170
+ break;
171
+ case "opencode": {
172
+ const p = getOpenCodePluginPath();
173
+ if (existsSync(p))
174
+ rmSync(p, { force: true });
175
+ break;
176
+ }
177
+ }
178
+ }
179
+ catch (err) {
180
+ logWarn(`No pude quitar el auto-update de ${target}: ${err instanceof Error ? err.message : String(err)}`);
181
+ }
182
+ }
183
+ function jsonHookHasOurs(path, key) {
184
+ const cfg = readJsonSafe(path);
185
+ const hooks = cfg?.hooks;
186
+ if (!hooks || !Array.isArray(hooks[key]))
187
+ return false;
188
+ return hooks[key].some(entryIsOurs);
189
+ }
190
+ export function hasAutoUpdate(target) {
191
+ switch (target) {
192
+ case "claude-code":
193
+ return jsonHookHasOurs(getClaudeSettingsPath(), "SessionStart");
194
+ case "cursor":
195
+ return jsonHookHasOurs(getCursorHooksPath(), "workspaceOpen");
196
+ case "codex":
197
+ return jsonHookHasOurs(getCodexHooksPath(), "SessionStart");
198
+ case "antigravity":
199
+ return jsonHookHasOurs(getGeminiSettingsPath(), "SessionStart");
200
+ case "opencode":
201
+ return existsSync(getOpenCodePluginPath());
202
+ }
203
+ }
204
+ export function refreshAutoUpdate() {
205
+ const lock = readLockFile();
206
+ const targets = [
207
+ ...new Set([
208
+ ...Object.values(lock.skills).flatMap((s) => s.targets ?? []),
209
+ ...Object.values(lock.mcps ?? {}).flatMap((m) => m.targets ?? []),
210
+ ]),
211
+ ];
212
+ for (const t of targets) {
213
+ if (hasAutoUpdate(t))
214
+ installAutoUpdate(t, "auto");
215
+ }
216
+ }
@@ -1,4 +1,5 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
2
3
  import { logError, logOk, logWarn } from "../colors.js";
3
4
  import { COMMAND_NAME, PRODUCT_NAME, getClaudeSettingsPath, getPackageVersion } from "../lib.js";
4
5
  function hookCommandFor(mode) {
@@ -20,11 +21,15 @@ function readSettings() {
20
21
  }
21
22
  catch {
22
23
  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
- return {};
24
+ return null;
24
25
  }
25
26
  }
26
27
  function writeSettings(s) {
27
- writeFileSync(getClaudeSettingsPath(), `${JSON.stringify(s, null, 2)}\n`, "utf-8");
28
+ const path = getClaudeSettingsPath();
29
+ mkdirSync(dirname(path), { recursive: true });
30
+ const tmp = `${path}.tmp-${process.pid}`;
31
+ writeFileSync(tmp, `${JSON.stringify(s, null, 2)}\n`, "utf-8");
32
+ renameSync(tmp, path);
28
33
  }
29
34
  function isOurMatcher(m) {
30
35
  if (!m || typeof m !== "object")
@@ -43,6 +48,8 @@ function buildOurMatcher(command) {
43
48
  }
44
49
  export function runHookInstall(mode = "auto") {
45
50
  const settings = readSettings();
51
+ if (settings === null)
52
+ return 1;
46
53
  settings.hooks = settings.hooks ?? {};
47
54
  const command = hookCommandFor(mode);
48
55
  const existing = settings.hooks.SessionStart;
@@ -85,7 +92,7 @@ export function runHookInstall(mode = "auto") {
85
92
  export function refreshHookCommand() {
86
93
  try {
87
94
  const settings = readSettings();
88
- if (!settings.hooks)
95
+ if (!settings?.hooks)
89
96
  return;
90
97
  const existing = settings.hooks.SessionStart;
91
98
  if (!Array.isArray(existing))
@@ -105,40 +112,47 @@ export function refreshHookCommand() {
105
112
  catch {
106
113
  }
107
114
  }
108
- export function runHookUninstall() {
115
+ export function runHookUninstall(opts = {}) {
116
+ const say = opts.quiet ? () => { } : logWarn;
117
+ const sayOk = opts.quiet ? () => { } : logOk;
109
118
  const settings = readSettings();
110
- const existing = settings.hooks?.SessionStart;
111
- if (existing === undefined) {
112
- logWarn("No había hook que desinstalar.");
119
+ if (settings === null) {
120
+ say("settings.json corrupto — no lo toco. Agente: reparalo vos mismo (la skill agent-onboarding tiene la guía) y re-corré uninstall — no le pidas al usuario editar JSON.");
121
+ return 1;
122
+ }
123
+ const hooks = settings.hooks;
124
+ const existing = hooks?.SessionStart;
125
+ if (!hooks || existing === undefined) {
126
+ say("No había hook que desinstalar.");
113
127
  return 0;
114
128
  }
115
129
  if (typeof existing === "string") {
116
130
  if (!existing.includes(HOOK_SIGNATURE)) {
117
- logWarn(`El SessionStart hook actual no es de ${PRODUCT_NAME}. No lo toco.`);
131
+ say(`El SessionStart hook actual no es de ${PRODUCT_NAME}. No lo toco.`);
118
132
  return 1;
119
133
  }
120
- settings.hooks.SessionStart = undefined;
134
+ hooks.SessionStart = undefined;
121
135
  writeSettings(settings);
122
- logOk("Hook removido.");
136
+ sayOk("Hook removido.");
123
137
  return 0;
124
138
  }
125
139
  if (!Array.isArray(existing)) {
126
- logWarn("No había hook que desinstalar.");
140
+ say("No había hook que desinstalar.");
127
141
  return 0;
128
142
  }
129
143
  const filtered = existing.filter((m) => !isOurMatcher(m));
130
144
  if (filtered.length === existing.length) {
131
- logWarn(`No encontré ningún SessionStart hook de ${PRODUCT_NAME}. No lo toco.`);
145
+ say(`No encontré ningún SessionStart hook de ${PRODUCT_NAME}. No lo toco.`);
132
146
  return 1;
133
147
  }
134
148
  if (filtered.length === 0) {
135
- settings.hooks.SessionStart = undefined;
149
+ hooks.SessionStart = undefined;
136
150
  }
137
151
  else {
138
- settings.hooks.SessionStart = filtered;
152
+ hooks.SessionStart = filtered;
139
153
  }
140
154
  writeSettings(settings);
141
- logOk("Hook removido.");
155
+ sayOk("Hook removido.");
142
156
  return 0;
143
157
  }
144
158
  export { hookCommandFor };
@@ -1,9 +1,9 @@
1
1
  import { AuthRequiredError } from "../api.js";
2
2
  import { hasValidAuth, readAuth } from "../auth.js";
3
+ import { installAutoUpdate } from "../autoupdate.js";
3
4
  import { bold, cyan, dim, gray, green, log, logError, red, yellow } from "../colors.js";
4
5
  import { installAll } from "../installer.js";
5
- import { COMMAND_NAME, formatBytes, getClaudeSkillsDir, getPackageVersion } from "../lib.js";
6
- import { runHookInstall } from "./hook.js";
6
+ import { COMMAND_NAME, formatBytes, getPackageVersion, getTargetSkillsDir } from "../lib.js";
7
7
  import { runLogin } from "./login.js";
8
8
  const BANNER_WIDTH = 58;
9
9
  const SHORT_DESCRIPTIONS = {
@@ -51,6 +51,10 @@ function shortDesc(name, manifestDesc) {
51
51
  const oneLine = manifestDesc.split(/\.|\n/)[0].trim();
52
52
  return oneLine.length > 55 ? `${oneLine.slice(0, 54)}…` : oneLine;
53
53
  }
54
+ function formatDestinos(targets) {
55
+ const dirs = targets.map((t) => getTargetSkillsDir(t));
56
+ return dirs.length > 0 ? dirs.join(" · ") : "(ninguno)";
57
+ }
54
58
  function printResultRow(result, manifest) {
55
59
  const isSkill = result.kind === "skill";
56
60
  const entry = isSkill ? manifest.skills[result.name] : manifest.mcps?.[result.name];
@@ -81,7 +85,7 @@ function printResultRow(result, manifest) {
81
85
  const padName = result.name.padEnd(22);
82
86
  log(` ${icon} ${kindBadge}${bold(padName)} ${gray(`· ${desc}`)}`);
83
87
  }
84
- function printSummary(results, manifest) {
88
+ function printSummary(results, manifest, targets) {
85
89
  const installed = results.filter((r) => r.status === "installed").length;
86
90
  const updated = results.filter((r) => r.status === "updated").length;
87
91
  const unchanged = results.filter((r) => r.status === "unchanged").length;
@@ -112,7 +116,7 @@ function printSummary(results, manifest) {
112
116
  if (failed > 0)
113
117
  parts.push(red(`${failed} fallidas`));
114
118
  log(` ${green("✓")} ${bold(parts.join(gray(" · ")))}${gray(` · ${totalFiles} archivos · ${formatBytes(totalBytes)}`)}`);
115
- log(` ${green("✓")} ${gray("destino:")} ${getClaudeSkillsDir()}`);
119
+ log(` ${green("✓")} ${gray("destino:")} ${formatDestinos(targets)}`);
116
120
  log("");
117
121
  for (const r of results.filter((x) => x.status === "failed")) {
118
122
  logError(`${r.name}: ${r.error}`);
@@ -120,7 +124,7 @@ function printSummary(results, manifest) {
120
124
  }
121
125
  function printNextSteps(noHook) {
122
126
  section("Ya estás listo");
123
- log(` ${bold(yellow("1."))} ${bold("Reiniciá tu cliente")} ${gray("(Claude Code, Cursor, OpenCode o Antigravity)")}.`);
127
+ log(` ${bold(yellow("1."))} ${bold("Reiniciá tu cliente")} ${gray("(Claude Code, Cursor, OpenCode, Antigravity o Codex)")}.`);
124
128
  log(` ${bold(yellow("2."))} ${bold("Hacele tu pregunta en español.")}`);
125
129
  log(` ${gray("Ejemplo:")} ${cyan('"cuántos nuevos clientes en mayo en Colombia?"')}`);
126
130
  log("");
@@ -176,7 +180,10 @@ export async function runInstall(opts = {}) {
176
180
  const msg = err instanceof Error ? err.message : String(err);
177
181
  logError(msg);
178
182
  if (msg.includes("EACCES") || msg.includes("permission denied")) {
179
- log(dim(" Sin permisos para escribir. Verifica permisos de ~/.claude/skills/"));
183
+ const dirs = opts.targets?.length
184
+ ? formatDestinos(opts.targets)
185
+ : "las carpetas de skills de tus clientes AI";
186
+ log(dim(` Sin permisos para escribir. Verifica permisos de ${dirs}`));
180
187
  }
181
188
  else if (msg.includes("ENOSPC") || msg.includes("no space")) {
182
189
  log(dim(" No hay espacio en disco. Libera espacio y reintenta."));
@@ -216,32 +223,34 @@ export async function runInstall(opts = {}) {
216
223
  return 1;
217
224
  }
218
225
  if (compact) {
219
- printCompactSummary(result.results, result.manifest, auth?.email);
226
+ printCompactSummary(result.results, result.manifest, result.targets, auth?.email);
220
227
  }
221
228
  else {
222
229
  for (const r of result.results) {
223
230
  printResultRow(r, result.manifest);
224
231
  }
225
- printSummary(result.results, result.manifest);
232
+ printSummary(result.results, result.manifest, result.targets);
226
233
  }
227
234
  const hasFails = result.results.some((r) => r.status === "failed");
228
- if (hasFails)
229
- return 1;
230
235
  if (opts.dryRun)
231
- return 0;
236
+ return hasFails ? 1 : 0;
232
237
  if (!opts.noHook) {
233
238
  if (!compact)
234
- log(` ${gray("Instalando hook de notificaciones…")}`);
235
- runHookInstall(opts.hookMode ?? "check");
239
+ log(` ${gray("Configurando auto-update por cliente…")}`);
240
+ for (const t of result.targets) {
241
+ installAutoUpdate(t, opts.hookMode ?? "auto");
242
+ }
236
243
  if (!compact)
237
244
  log("");
238
245
  }
246
+ if (hasFails)
247
+ return 1;
239
248
  if (!compact) {
240
249
  printNextSteps(opts.noHook ?? false);
241
250
  }
242
251
  return 0;
243
252
  }
244
- function printCompactSummary(results, manifest, email) {
253
+ function printCompactSummary(results, manifest, targets, email) {
245
254
  const ok = results.filter((r) => r.status === "installed" || r.status === "updated" || r.status === "unchanged");
246
255
  const failed = results.filter((r) => r.status === "failed");
247
256
  const installed = results.filter((r) => r.status === "installed").length;
@@ -273,7 +282,7 @@ function printCompactSummary(results, manifest, email) {
273
282
  .map((r) => r.name)
274
283
  .sort()
275
284
  .join(", "))}`);
276
- log(` ${gray("destino:")} ${cyan(getClaudeSkillsDir())}${gray(" · ")}${bold(yellow("reinicia tu cliente para activar skills y MCPs"))}`);
285
+ log(` ${gray("destino:")} ${cyan(formatDestinos(targets))}${gray(" · ")}${bold(yellow("reinicia tu cliente para activar skills y MCPs"))}`);
277
286
  if (unchanged > 0 && installed === 0 && updated === 0) {
278
287
  log(` ${dim(`(${unchanged} sin cambios desde la última instalación)`)}`);
279
288
  }
@@ -1,19 +1,45 @@
1
+ import { existsSync, readFileSync } from "node:fs";
1
2
  import { AuthRequiredError, fetchManifest } from "../api.js";
2
3
  import { readAuth } from "../auth.js";
3
4
  import { bold, cyan, dim, gray, green, log, logError, logWarn, yellow } from "../colors.js";
4
5
  import { detectDrift } from "../installer.js";
5
- import { COMMAND_NAME, GATEWAY_URL, getClaudeSkillsDir, getLockFilePath, getPackageVersion, readLockFile, } from "../lib.js";
6
+ import { COMMAND_NAME, GATEWAY_URL, getAutoUpdateLogPath, getClaudeSkillsDir, getLockFilePath, getPackageVersion, getTargetSkillsDir, readLockFile, } from "../lib.js";
7
+ function reportAutoUpdateLog() {
8
+ const logPath = getAutoUpdateLogPath();
9
+ if (!existsSync(logPath))
10
+ return;
11
+ try {
12
+ const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
13
+ if (lines.length === 0)
14
+ return;
15
+ logWarn(`El auto-update no pudo correr ${lines.length} vez/veces en un cliente GUI — última: "${lines[lines.length - 1]}". Agente: corré ${bold(`${COMMAND_NAME} update`)} (actualiza ya mismo y limpia este aviso); si reaparece, el cliente GUI no encuentra pnpm en su PATH y hay que reinstalar con ${bold(`${COMMAND_NAME} install`)} desde una terminal con pnpm disponible.`);
16
+ log("");
17
+ }
18
+ catch {
19
+ }
20
+ }
6
21
  export async function runStatus() {
7
22
  const auth = readAuth();
23
+ const lockForTargets = readLockFile();
24
+ const installedTargets = [
25
+ ...new Set([
26
+ ...Object.values(lockForTargets.skills).flatMap((s) => s.targets ?? []),
27
+ ...Object.values(lockForTargets.mcps ?? {}).flatMap((m) => m.targets ?? []),
28
+ ]),
29
+ ];
30
+ const destino = installedTargets.length
31
+ ? installedTargets.map((t) => getTargetSkillsDir(t)).join(" · ")
32
+ : getClaudeSkillsDir();
8
33
  log("");
9
34
  log(bold(cyan(" Estado")));
10
35
  log(gray(` ${"─".repeat(70)}`));
11
36
  log(` ${gray("Paquete:")} ${bold(COMMAND_NAME)} v${getPackageVersion()}`);
12
37
  log(` ${gray("Gateway:")} ${GATEWAY_URL}`);
13
38
  log(` ${gray("Autenticado:")} ${auth ? green(auth.email) : gray("no")}`);
14
- log(` ${gray("Destino:")} ${getClaudeSkillsDir()}`);
39
+ log(` ${gray("Destino:")} ${destino}`);
15
40
  log(` ${gray("Lock file:")} ${getLockFilePath()}`);
16
41
  log("");
42
+ reportAutoUpdateLog();
17
43
  if (!auth) {
18
44
  logWarn(`Inicia sesión con: ${bold(`${COMMAND_NAME} login`)}`);
19
45
  log("");
@@ -1,21 +1,30 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { removeAutoUpdate } from "../autoupdate.js";
4
5
  import { dim, gray, log, logOk, logWarn, red } from "../colors.js";
5
6
  import { uninstallAll } from "../installer.js";
6
7
  import { readLockFile } from "../lib.js";
8
+ import { TARGETS } from "../targets.js";
7
9
  import { banner, confirm } from "../ui.js";
8
10
  export async function runUninstall(opts = {}) {
9
11
  banner();
10
12
  const lock = readLockFile();
11
13
  const skills = Object.keys(lock.skills);
12
- if (skills.length === 0) {
13
- logWarn("No hay skills instaladas por este paquete.");
14
+ const mcps = Object.keys(lock.mcps ?? {});
15
+ if (skills.length === 0 && mcps.length === 0) {
16
+ logWarn("No hay skills ni MCPs instalados por este paquete.");
17
+ if (!opts.dryRun) {
18
+ for (const t of Object.keys(TARGETS))
19
+ removeAutoUpdate(t);
20
+ }
14
21
  return 0;
15
22
  }
16
- log(red(` Vas a eliminar ${skills.length} skill(s):`));
23
+ log(red(` Vas a eliminar ${skills.length} skill(s) y ${mcps.length} MCP(s):`));
17
24
  for (const s of skills)
18
25
  log(gray(` - ${s}`));
26
+ for (const m of mcps)
27
+ log(gray(` - ${m} (MCP)`));
19
28
  log("");
20
29
  if (!opts.yes && !opts.dryRun) {
21
30
  const ok = await confirm("¿Continuar?", false);
@@ -25,8 +34,11 @@ export async function runUninstall(opts = {}) {
25
34
  }
26
35
  }
27
36
  const results = uninstallAll({ dryRun: opts.dryRun });
28
- if (!opts.dryRun)
37
+ if (!opts.dryRun) {
38
+ for (const t of Object.keys(TARGETS))
39
+ removeAutoUpdate(t);
29
40
  removeNpmrcCooldownExclude();
41
+ }
30
42
  log("");
31
43
  if (opts.dryRun) {
32
44
  logOk(`${results.length} skill(s) se eliminarían (dry-run, nada borrado).`);
@@ -1,9 +1,11 @@
1
+ import { rmSync } from "node:fs";
1
2
  import { AuthRequiredError, fetchManifest } from "../api.js";
2
3
  import { hasValidAuth } from "../auth.js";
4
+ import { refreshAutoUpdate } from "../autoupdate.js";
3
5
  import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
4
6
  import { detectDrift, installAll, summarize } from "../installer.js";
7
+ import { getAutoUpdateLogPath } from "../lib.js";
5
8
  import { banner } from "../ui.js";
6
- import { refreshHookCommand } from "./hook.js";
7
9
  import { runLogin } from "./login.js";
8
10
  export async function runUpdate(opts = {}) {
9
11
  banner();
@@ -50,8 +52,10 @@ async function runUpdateOnce(opts) {
50
52
  ];
51
53
  if (pending.length === 0) {
52
54
  log(green(" ✓ Todo al día (skills + MCPs).\n"));
53
- if (!opts.dryRun)
54
- refreshHookCommand();
55
+ if (!opts.dryRun) {
56
+ refreshAutoUpdate();
57
+ clearAutoUpdateLog();
58
+ }
55
59
  return 0;
56
60
  }
57
61
  log(cyan(` → ${pending.length} elemento(s) por actualizar:\n`));
@@ -74,7 +78,16 @@ async function runUpdateOnce(opts) {
74
78
  return 1;
75
79
  }
76
80
  summarize(result.results);
77
- refreshHookCommand();
81
+ refreshAutoUpdate();
78
82
  const failed = result.results.some((r) => r.status === "failed");
83
+ if (!failed)
84
+ clearAutoUpdateLog();
79
85
  return failed ? 1 : 0;
80
86
  }
87
+ function clearAutoUpdateLog() {
88
+ try {
89
+ rmSync(getAutoUpdateLogPath(), { force: true });
90
+ }
91
+ catch {
92
+ }
93
+ }
package/dist/installer.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { copyFileSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve, sep } from "node:path";
4
- import { fetchManifest, fetchSkillFile } from "./api.js";
4
+ import { AuthRequiredError, fetchManifest, fetchSkillFile } from "./api.js";
5
5
  import { dim, gray, green, logError, red, yellow } from "./colors.js";
6
- import { adaptStdioCommand, ensureDir, getLockFilePath, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getCursorLegacyRulesDir, getOpenCodeLegacyPromptsDir, getPackageVersion, readLockFile, sha256File, writeLockFile, } from "./lib.js";
6
+ import { adaptStdioCommand, ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getCursorLegacyRulesDir, getLockFilePath, getOpenCodeLegacyPromptsDir, getPackageVersion, readLockFile, sha256File, writeLockFile, } from "./lib.js";
7
7
  import { inlineReferences, renderMcpForTarget } from "./render.js";
8
8
  import { TARGETS, detectInstalledTargets } from "./targets.js";
9
9
  const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
@@ -94,12 +94,12 @@ function writeSkillToTarget(skillName, staged, spec) {
94
94
  if (spec.skillFormat === "folder") {
95
95
  const dst = join(spec.skillsDir, skillName);
96
96
  if (existsSync(dst))
97
- rmSync(dst, { recursive: true, force: true });
97
+ withFsRetry(() => rmSync(dst, { recursive: true, force: true }));
98
98
  ensureDir(dst);
99
99
  for (const f of staged) {
100
100
  const dstFile = safeJoinSkillFile(dst, f.rel);
101
101
  ensureDir(dirname(dstFile));
102
- writeFileSync(dstFile, f.data);
102
+ withFsRetry(() => writeFileSync(dstFile, f.data));
103
103
  }
104
104
  return;
105
105
  }
@@ -151,6 +151,22 @@ function parseFrontmatterMinimal(content) {
151
151
  function sleepSync(ms) {
152
152
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
153
153
  }
154
+ function withFsRetry(fn) {
155
+ for (let attempt = 0;; attempt++) {
156
+ try {
157
+ fn();
158
+ return;
159
+ }
160
+ catch (err) {
161
+ const code = err.code;
162
+ if (attempt < 4 && (code === "EPERM" || code === "EBUSY" || code === "EACCES")) {
163
+ sleepSync(150);
164
+ continue;
165
+ }
166
+ throw err;
167
+ }
168
+ }
169
+ }
154
170
  function atomicWriteFile(path, content) {
155
171
  ensureDir(dirname(path));
156
172
  const tmp = `${path}.${process.pid}.tmp`;
@@ -283,9 +299,7 @@ export function removeTomlBlock(content, name) {
283
299
  return content.slice(0, start) + content.slice(stop);
284
300
  }
285
301
  function registerMcpInTomlBlock(mcp, spec) {
286
- const existing = existsSync(spec.mcpConfigPath)
287
- ? readFileSync(spec.mcpConfigPath, "utf-8")
288
- : "";
302
+ const existing = existsSync(spec.mcpConfigPath) ? readFileSync(spec.mcpConfigPath, "utf-8") : "";
289
303
  const next = upsertTomlBlock(existing, mcp.name, renderMcpToml(mcp));
290
304
  if (next === existing)
291
305
  return;
@@ -368,12 +382,19 @@ function cleanupLegacySkillArtifacts(spec) {
368
382
  stripMcpEntries(getClaudeSettingsPath(), ourMcps);
369
383
  }
370
384
  if (spec.id === "antigravity") {
371
- for (const name of [...Object.keys(lock.skills), ...LEGACY_RENAMED_SKILLS]) {
372
- const d = join(getAntigravityLegacySkillsDir(), name);
373
- if (existsSync(d))
374
- rmSync(d, { recursive: true, force: true });
385
+ const legacySkillDirs = [
386
+ getAntigravityLegacySkillsDir(),
387
+ getAntigravityGeminiLegacySkillsDir(),
388
+ ];
389
+ for (const base of legacySkillDirs) {
390
+ for (const name of [...Object.keys(lock.skills), ...LEGACY_RENAMED_SKILLS]) {
391
+ const d = join(base, name);
392
+ if (existsSync(d))
393
+ rmSync(d, { recursive: true, force: true });
394
+ }
375
395
  }
376
396
  stripMcpEntries(getAntigravityLegacyMcpConfigPath(), ourMcps);
397
+ stripMcpEntries(getAntigravityGeminiLegacyMcpConfigPath(), ourMcps);
377
398
  }
378
399
  }
379
400
  catch {
@@ -411,27 +432,13 @@ export async function installSkill(name, manifest, opts = {}) {
411
432
  targets: targetIds,
412
433
  };
413
434
  }
435
+ let staged;
414
436
  try {
415
- const staged = await stageSkill(name, entry);
416
- for (const t of targets) {
417
- writeSkillToTarget(name, staged, t);
418
- }
419
- lock.skills[name] = {
420
- name,
421
- installedFrom: getPackageVersion(),
422
- installedAt: new Date().toISOString(),
423
- bundleHash: entry.bundleHash,
424
- targets: targetIds,
425
- };
426
- writeLockFile(lock);
427
- return {
428
- name,
429
- kind: "skill",
430
- status: prev ? "updated" : "installed",
431
- targets: targetIds,
432
- };
437
+ staged = await stageSkill(name, entry);
433
438
  }
434
439
  catch (err) {
440
+ if (err instanceof AuthRequiredError)
441
+ throw err;
435
442
  return {
436
443
  name,
437
444
  kind: "skill",
@@ -440,6 +447,35 @@ export async function installSkill(name, manifest, opts = {}) {
440
447
  error: err instanceof Error ? err.message : String(err),
441
448
  };
442
449
  }
450
+ const okTargets = [];
451
+ const failures = [];
452
+ for (const t of targets) {
453
+ try {
454
+ writeSkillToTarget(name, staged, t);
455
+ okTargets.push(t.id);
456
+ }
457
+ catch (err) {
458
+ failures.push(`${t.id}: ${err instanceof Error ? err.message : String(err)}`);
459
+ }
460
+ }
461
+ if (okTargets.length === 0) {
462
+ return { name, kind: "skill", status: "failed", targets: targetIds, error: failures.join("; ") };
463
+ }
464
+ lock.skills[name] = {
465
+ name,
466
+ installedFrom: getPackageVersion(),
467
+ installedAt: new Date().toISOString(),
468
+ bundleHash: entry.bundleHash,
469
+ targets: okTargets,
470
+ };
471
+ writeLockFile(lock);
472
+ return {
473
+ name,
474
+ kind: "skill",
475
+ status: prev ? "updated" : "installed",
476
+ targets: okTargets,
477
+ error: failures.length > 0 ? `targets con fallo — ${failures.join("; ")}` : undefined,
478
+ };
443
479
  }
444
480
  export function installMcp(mcp, opts = {}) {
445
481
  try {
@@ -481,34 +517,41 @@ export function installMcp(mcp, opts = {}) {
481
517
  targets: targetIds,
482
518
  };
483
519
  }
484
- try {
485
- for (const t of wantedTargets) {
520
+ const okTargets = [];
521
+ const failures = [];
522
+ for (const t of wantedTargets) {
523
+ try {
486
524
  registerMcpInTarget(mcp, t);
525
+ okTargets.push(t.id);
526
+ }
527
+ catch (err) {
528
+ failures.push(`${t.id}: ${err instanceof Error ? err.message : String(err)}`);
487
529
  }
488
- lock.mcps[mcp.name] = {
489
- name: mcp.name,
490
- installedFrom: getPackageVersion(),
491
- installedAt: new Date().toISOString(),
492
- configHash: mcpConfigHash(mcp),
493
- targets: targetIds,
494
- };
495
- writeLockFile(lock);
496
- return {
497
- name: mcp.name,
498
- kind: "mcp",
499
- status: prev ? "updated" : "installed",
500
- targets: targetIds,
501
- };
502
530
  }
503
- catch (err) {
531
+ if (okTargets.length === 0) {
504
532
  return {
505
533
  name: mcp.name,
506
534
  kind: "mcp",
507
535
  status: "failed",
508
536
  targets: targetIds,
509
- error: err instanceof Error ? err.message : String(err),
537
+ error: failures.join("; "),
510
538
  };
511
539
  }
540
+ lock.mcps[mcp.name] = {
541
+ name: mcp.name,
542
+ installedFrom: getPackageVersion(),
543
+ installedAt: new Date().toISOString(),
544
+ configHash: mcpConfigHash(mcp),
545
+ targets: okTargets,
546
+ };
547
+ writeLockFile(lock);
548
+ return {
549
+ name: mcp.name,
550
+ kind: "mcp",
551
+ status: prev ? "updated" : "installed",
552
+ targets: okTargets,
553
+ error: failures.length > 0 ? `targets con fallo — ${failures.join("; ")}` : undefined,
554
+ };
512
555
  }
513
556
  export async function installAll(opts = {}) {
514
557
  let manifest;
@@ -621,6 +664,15 @@ export function uninstallAll(opts = {}) {
621
664
  results.push(uninstallSkill(name, opts));
622
665
  for (const name of Object.keys(lock.mcps ?? {}))
623
666
  results.push(uninstallMcp(name, opts));
667
+ if (!opts.dryRun) {
668
+ for (const spec of Object.values(TARGETS)) {
669
+ try {
670
+ cleanupLegacySkillArtifacts(spec);
671
+ }
672
+ catch {
673
+ }
674
+ }
675
+ }
624
676
  return results;
625
677
  }
626
678
  export function detectDrift(manifest) {
package/dist/lib.js CHANGED
@@ -59,10 +59,13 @@ export function getOpenCodeConfigPath() {
59
59
  return join(base, "opencode", "opencode.json");
60
60
  }
61
61
  export function getAntigravitySkillsDir() {
62
- return join(homedir(), ".gemini", "antigravity", "skills");
62
+ return join(homedir(), ".gemini", "skills");
63
63
  }
64
64
  export function getAntigravityMcpConfigPath() {
65
- return join(homedir(), ".gemini", "antigravity", "mcp_config.json");
65
+ return join(homedir(), ".gemini", "config", "mcp_config.json");
66
+ }
67
+ export function getGeminiSettingsPath() {
68
+ return join(homedir(), ".gemini", "settings.json");
66
69
  }
67
70
  export function getAntigravityLegacySkillsDir() {
68
71
  return join(homedir(), ".antigravity", "skills");
@@ -70,6 +73,28 @@ export function getAntigravityLegacySkillsDir() {
70
73
  export function getAntigravityLegacyMcpConfigPath() {
71
74
  return join(homedir(), ".antigravity", "settings.json");
72
75
  }
76
+ export function getAntigravityGeminiLegacySkillsDir() {
77
+ return join(homedir(), ".gemini", "antigravity", "skills");
78
+ }
79
+ export function getAntigravityGeminiLegacyMcpConfigPath() {
80
+ return join(homedir(), ".gemini", "antigravity", "mcp_config.json");
81
+ }
82
+ export function getCursorHooksPath() {
83
+ return join(homedir(), ".cursor", "hooks.json");
84
+ }
85
+ export function getCodexHooksPath() {
86
+ const codexHome = process.env.CODEX_HOME;
87
+ const base = codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
88
+ return join(base, "hooks.json");
89
+ }
90
+ export function getOpenCodePluginsDir() {
91
+ const xdgHome = process.env.XDG_CONFIG_HOME;
92
+ const base = xdgHome && xdgHome.length > 0 ? xdgHome : join(homedir(), ".config");
93
+ return join(base, "opencode", "plugins");
94
+ }
95
+ export function getOpenCodePluginPath() {
96
+ return join(getOpenCodePluginsDir(), "alegra-data-agent-autoupdate.js");
97
+ }
73
98
  export function getCodexSkillsDir() {
74
99
  return join(homedir(), ".agents", "skills");
75
100
  }
@@ -117,6 +142,9 @@ export function getConfigDir() {
117
142
  export function getAuthFilePath() {
118
143
  return join(getConfigDir(), "auth.json");
119
144
  }
145
+ export function getAutoUpdateLogPath() {
146
+ return join(getConfigDir(), "autoupdate.log");
147
+ }
120
148
  let _cachedVersion = null;
121
149
  export function getPackageVersion() {
122
150
  if (_cachedVersion)
@@ -254,7 +282,8 @@ export function ensureDir(path) {
254
282
  mkdirSync(path, { recursive: true });
255
283
  }
256
284
  export function adaptStdioCommand(command, args, platform = process.platform) {
257
- if (platform === "win32" && (command === "npx" || command === "npm" || command === "pnpm")) {
285
+ const winShims = new Set(["npx", "npm", "pnpm", "sf", "uvx", "bunx", "yarn", "deno"]);
286
+ if (platform === "win32" && winShims.has(command)) {
258
287
  return { command: "cmd", args: ["/c", command, ...args] };
259
288
  }
260
289
  return { command, args };
package/dist/ui.js CHANGED
@@ -44,7 +44,7 @@ ${bold("Comandos:")}
44
44
  login Autenticarte con tu cuenta @alegra.com
45
45
  logout Cerrar sesión (borra el token local)
46
46
  whoami Mostrar la cuenta actualmente autenticada
47
- install Instala skills + MCPs en tus clientes (Claude Code, Cursor, OpenCode, Antigravity)
47
+ install Instala skills + MCPs en tus clientes (Claude Code, Cursor, OpenCode, Antigravity, Codex)
48
48
  update Re-descarga skills/MCPs que cambiaron
49
49
  list Lista skills y MCPs disponibles en el gateway
50
50
  status Muestra estado: auth, instalado, drift
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alegra-data-agent",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
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",