alegra-data-agent 0.2.2 → 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 { ensureDir, getLockFilePath, 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-]*$/;
@@ -34,7 +34,11 @@ function validateRelPath(rel) {
34
34
  }
35
35
  function mcpConfigHash(mcp) {
36
36
  const salient = JSON.stringify({
37
- url: mcp.url,
37
+ transport: mcp.transport ?? "http",
38
+ url: mcp.url ?? null,
39
+ command: mcp.command ?? null,
40
+ args: mcp.args ?? null,
41
+ env: mcp.env ?? null,
38
42
  type: mcp.auth.type,
39
43
  tokenEnv: mcp.auth.tokenEnv ?? null,
40
44
  });
@@ -90,16 +94,16 @@ function writeSkillToTarget(skillName, staged, spec) {
90
94
  if (spec.skillFormat === "folder") {
91
95
  const dst = join(spec.skillsDir, skillName);
92
96
  if (existsSync(dst))
93
- rmSync(dst, { recursive: true, force: true });
97
+ withFsRetry(() => rmSync(dst, { recursive: true, force: true }));
94
98
  ensureDir(dst);
95
99
  for (const f of staged) {
96
100
  const dstFile = safeJoinSkillFile(dst, f.rel);
97
101
  ensureDir(dirname(dstFile));
98
- writeFileSync(dstFile, f.data);
102
+ withFsRetry(() => writeFileSync(dstFile, f.data));
99
103
  }
100
104
  return;
101
105
  }
102
- const entry = staged.find((f) => f.rel === "SKILL.md" || f.rel === "README.md");
106
+ const entry = staged.find((f) => f.rel === "SKILL.md") ?? staged.find((f) => f.rel === "README.md");
103
107
  if (!entry) {
104
108
  throw new Error(`Para target ${spec.id} se requiere SKILL.md o README.md en la skill`);
105
109
  }
@@ -144,11 +148,47 @@ function parseFrontmatterMinimal(content) {
144
148
  }
145
149
  return out;
146
150
  }
151
+ function sleepSync(ms) {
152
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
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
+ }
147
170
  function atomicWriteFile(path, content) {
148
171
  ensureDir(dirname(path));
149
172
  const tmp = `${path}.${process.pid}.tmp`;
150
173
  writeFileSync(tmp, content);
151
- renameSync(tmp, path);
174
+ for (let attempt = 0;; attempt++) {
175
+ try {
176
+ renameSync(tmp, path);
177
+ return;
178
+ }
179
+ catch (err) {
180
+ const code = err.code;
181
+ if (attempt < 4 && (code === "EPERM" || code === "EBUSY" || code === "EACCES")) {
182
+ sleepSync(150);
183
+ continue;
184
+ }
185
+ try {
186
+ rmSync(tmp, { force: true });
187
+ }
188
+ catch { }
189
+ throw err;
190
+ }
191
+ }
152
192
  }
153
193
  function backupIfExists(path) {
154
194
  if (!existsSync(path))
@@ -199,7 +239,20 @@ function tomlBasicString(s) {
199
239
  return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
200
240
  }
201
241
  export function renderMcpToml(mcp) {
202
- const lines = [`[mcp_servers.${mcp.name}]`, `url = ${tomlBasicString(mcp.url)}`];
242
+ if (mcp.transport === "stdio") {
243
+ const { command, args } = adaptStdioCommand(mcp.command ?? "", mcp.args ?? []);
244
+ const lines = [`[mcp_servers.${mcp.name}]`, `command = ${tomlBasicString(command)}`];
245
+ if (args.length > 0) {
246
+ lines.push(`args = [${args.map((a) => tomlBasicString(a)).join(", ")}]`);
247
+ }
248
+ if (mcp.env && Object.keys(mcp.env).length > 0) {
249
+ lines.push(`[mcp_servers.${mcp.name}.env]`);
250
+ for (const [k, v] of Object.entries(mcp.env))
251
+ lines.push(`${k} = ${tomlBasicString(v)}`);
252
+ }
253
+ return lines.join("\n");
254
+ }
255
+ const lines = [`[mcp_servers.${mcp.name}]`, `url = ${tomlBasicString(mcp.url ?? "")}`];
203
256
  if (mcp.auth.type === "gateway") {
204
257
  if (!mcp.auth.tokenEnv) {
205
258
  throw new Error(`MCP '${mcp.name}' es type=gateway pero no trae tokenEnv`);
@@ -246,9 +299,7 @@ export function removeTomlBlock(content, name) {
246
299
  return content.slice(0, start) + content.slice(stop);
247
300
  }
248
301
  function registerMcpInTomlBlock(mcp, spec) {
249
- const existing = existsSync(spec.mcpConfigPath)
250
- ? readFileSync(spec.mcpConfigPath, "utf-8")
251
- : "";
302
+ const existing = existsSync(spec.mcpConfigPath) ? readFileSync(spec.mcpConfigPath, "utf-8") : "";
252
303
  const next = upsertTomlBlock(existing, mcp.name, renderMcpToml(mcp));
253
304
  if (next === existing)
254
305
  return;
@@ -271,7 +322,83 @@ function resolveTargets(opts) {
271
322
  if (ids.length === 0) {
272
323
  ids.push("claude-code");
273
324
  }
274
- return ids.map((id) => TARGETS[id]);
325
+ const specs = ids.map((id) => TARGETS[id]);
326
+ for (const spec of specs)
327
+ cleanupLegacySkillArtifacts(spec);
328
+ return specs;
329
+ }
330
+ const LEGACY_RENAMED_SKILLS = ["alanube", "alegra-producto", "alegra-ce-universe"];
331
+ function stripMcpEntries(path, names) {
332
+ if (!existsSync(path) || names.length === 0)
333
+ return;
334
+ let cfg;
335
+ try {
336
+ cfg = JSON.parse(readFileSync(path, "utf-8"));
337
+ }
338
+ catch {
339
+ return;
340
+ }
341
+ let changed = false;
342
+ for (const key of ["mcpServers", "mcp"]) {
343
+ const section = cfg[key];
344
+ if (!section || typeof section !== "object")
345
+ continue;
346
+ for (const n of names) {
347
+ if (n in section) {
348
+ delete section[n];
349
+ changed = true;
350
+ }
351
+ }
352
+ if (Object.keys(section).length === 0)
353
+ delete cfg[key];
354
+ }
355
+ if (!changed)
356
+ return;
357
+ backupIfExists(path);
358
+ atomicWriteFile(path, `${JSON.stringify(cfg, null, 2)}\n`);
359
+ }
360
+ function cleanupLegacySkillArtifacts(spec) {
361
+ try {
362
+ if (spec.skillFormat === "folder") {
363
+ for (const old of LEGACY_RENAMED_SKILLS) {
364
+ const dir = join(spec.skillsDir, old);
365
+ if (existsSync(dir))
366
+ rmSync(dir, { recursive: true, force: true });
367
+ }
368
+ }
369
+ const lock = readLockFile();
370
+ if (spec.id === "opencode" || spec.id === "cursor") {
371
+ const legacyDir = spec.id === "opencode" ? getOpenCodeLegacyPromptsDir() : getCursorLegacyRulesDir();
372
+ const ext = spec.id === "opencode" ? ".md" : ".mdc";
373
+ const managed = [...Object.keys(lock.skills), ...LEGACY_RENAMED_SKILLS];
374
+ for (const name of managed) {
375
+ const f = join(legacyDir, `${name}${ext}`);
376
+ if (existsSync(f))
377
+ rmSync(f, { force: true });
378
+ }
379
+ }
380
+ const ourMcps = Object.keys(lock.mcps ?? {});
381
+ if (spec.id === "claude-code") {
382
+ stripMcpEntries(getClaudeSettingsPath(), ourMcps);
383
+ }
384
+ if (spec.id === "antigravity") {
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
+ }
395
+ }
396
+ stripMcpEntries(getAntigravityLegacyMcpConfigPath(), ourMcps);
397
+ stripMcpEntries(getAntigravityGeminiLegacyMcpConfigPath(), ourMcps);
398
+ }
399
+ }
400
+ catch {
401
+ }
275
402
  }
276
403
  export async function installSkill(name, manifest, opts = {}) {
277
404
  const entry = manifest.skills[name];
@@ -305,27 +432,13 @@ export async function installSkill(name, manifest, opts = {}) {
305
432
  targets: targetIds,
306
433
  };
307
434
  }
435
+ let staged;
308
436
  try {
309
- const staged = await stageSkill(name, entry);
310
- for (const t of targets) {
311
- writeSkillToTarget(name, staged, t);
312
- }
313
- lock.skills[name] = {
314
- name,
315
- installedFrom: getPackageVersion(),
316
- installedAt: new Date().toISOString(),
317
- bundleHash: entry.bundleHash,
318
- targets: targetIds,
319
- };
320
- writeLockFile(lock);
321
- return {
322
- name,
323
- kind: "skill",
324
- status: prev ? "updated" : "installed",
325
- targets: targetIds,
326
- };
437
+ staged = await stageSkill(name, entry);
327
438
  }
328
439
  catch (err) {
440
+ if (err instanceof AuthRequiredError)
441
+ throw err;
329
442
  return {
330
443
  name,
331
444
  kind: "skill",
@@ -334,6 +447,35 @@ export async function installSkill(name, manifest, opts = {}) {
334
447
  error: err instanceof Error ? err.message : String(err),
335
448
  };
336
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
+ };
337
479
  }
338
480
  export function installMcp(mcp, opts = {}) {
339
481
  try {
@@ -375,34 +517,41 @@ export function installMcp(mcp, opts = {}) {
375
517
  targets: targetIds,
376
518
  };
377
519
  }
378
- try {
379
- for (const t of wantedTargets) {
520
+ const okTargets = [];
521
+ const failures = [];
522
+ for (const t of wantedTargets) {
523
+ try {
380
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)}`);
381
529
  }
382
- lock.mcps[mcp.name] = {
383
- name: mcp.name,
384
- installedFrom: getPackageVersion(),
385
- installedAt: new Date().toISOString(),
386
- configHash: mcpConfigHash(mcp),
387
- targets: targetIds,
388
- };
389
- writeLockFile(lock);
390
- return {
391
- name: mcp.name,
392
- kind: "mcp",
393
- status: prev ? "updated" : "installed",
394
- targets: targetIds,
395
- };
396
530
  }
397
- catch (err) {
531
+ if (okTargets.length === 0) {
398
532
  return {
399
533
  name: mcp.name,
400
534
  kind: "mcp",
401
535
  status: "failed",
402
536
  targets: targetIds,
403
- error: err instanceof Error ? err.message : String(err),
537
+ error: failures.join("; "),
404
538
  };
405
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
+ };
406
555
  }
407
556
  export async function installAll(opts = {}) {
408
557
  let manifest;
@@ -515,6 +664,15 @@ export function uninstallAll(opts = {}) {
515
664
  results.push(uninstallSkill(name, opts));
516
665
  for (const name of Object.keys(lock.mcps ?? {}))
517
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
+ }
518
676
  return results;
519
677
  }
520
678
  export function detectDrift(manifest) {
package/dist/lib.js CHANGED
@@ -31,13 +31,24 @@ export function getClaudeSkillsDir() {
31
31
  export function getClaudeSettingsPath() {
32
32
  return join(homedir(), ".claude", "settings.json");
33
33
  }
34
+ export function getClaudeJsonPath() {
35
+ return join(homedir(), ".claude.json");
36
+ }
34
37
  export function getCursorSkillsDir() {
38
+ return join(homedir(), ".cursor", "skills");
39
+ }
40
+ export function getCursorLegacyRulesDir() {
35
41
  return join(homedir(), ".cursor", "rules");
36
42
  }
37
43
  export function getCursorMcpPath() {
38
44
  return join(homedir(), ".cursor", "mcp.json");
39
45
  }
40
46
  export function getOpenCodeSkillsDir() {
47
+ const xdgHome = process.env.XDG_CONFIG_HOME;
48
+ const base = xdgHome && xdgHome.length > 0 ? xdgHome : join(homedir(), ".config");
49
+ return join(base, "opencode", "skills");
50
+ }
51
+ export function getOpenCodeLegacyPromptsDir() {
41
52
  const xdgHome = process.env.XDG_CONFIG_HOME;
42
53
  const base = xdgHome && xdgHome.length > 0 ? xdgHome : join(homedir(), ".config");
43
54
  return join(base, "opencode", "prompts");
@@ -48,11 +59,42 @@ export function getOpenCodeConfigPath() {
48
59
  return join(base, "opencode", "opencode.json");
49
60
  }
50
61
  export function getAntigravitySkillsDir() {
62
+ return join(homedir(), ".gemini", "skills");
63
+ }
64
+ export function getAntigravityMcpConfigPath() {
65
+ return join(homedir(), ".gemini", "config", "mcp_config.json");
66
+ }
67
+ export function getGeminiSettingsPath() {
68
+ return join(homedir(), ".gemini", "settings.json");
69
+ }
70
+ export function getAntigravityLegacySkillsDir() {
51
71
  return join(homedir(), ".antigravity", "skills");
52
72
  }
53
- export function getAntigravitySettingsPath() {
73
+ export function getAntigravityLegacyMcpConfigPath() {
54
74
  return join(homedir(), ".antigravity", "settings.json");
55
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
+ }
56
98
  export function getCodexSkillsDir() {
57
99
  return join(homedir(), ".agents", "skills");
58
100
  }
@@ -78,13 +120,13 @@ export function getTargetSkillsDir(target) {
78
120
  export function getTargetMcpConfigPath(target) {
79
121
  switch (target) {
80
122
  case "claude-code":
81
- return getClaudeSettingsPath();
123
+ return getClaudeJsonPath();
82
124
  case "cursor":
83
125
  return getCursorMcpPath();
84
126
  case "opencode":
85
127
  return getOpenCodeConfigPath();
86
128
  case "antigravity":
87
- return getAntigravitySettingsPath();
129
+ return getAntigravityMcpConfigPath();
88
130
  case "codex":
89
131
  return getCodexConfigPath();
90
132
  }
@@ -100,6 +142,9 @@ export function getConfigDir() {
100
142
  export function getAuthFilePath() {
101
143
  return join(getConfigDir(), "auth.json");
102
144
  }
145
+ export function getAutoUpdateLogPath() {
146
+ return join(getConfigDir(), "autoupdate.log");
147
+ }
103
148
  let _cachedVersion = null;
104
149
  export function getPackageVersion() {
105
150
  if (_cachedVersion)
@@ -236,3 +281,10 @@ export function ensureDir(path) {
236
281
  if (!existsSync(path))
237
282
  mkdirSync(path, { recursive: true });
238
283
  }
284
+ export function adaptStdioCommand(command, args, platform = process.platform) {
285
+ const winShims = new Set(["npx", "npm", "pnpm", "sf", "uvx", "bunx", "yarn", "deno"]);
286
+ if (platform === "win32" && winShims.has(command)) {
287
+ return { command: "cmd", args: ["/c", command, ...args] };
288
+ }
289
+ return { command, args };
290
+ }
package/dist/render.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { extname, join } from "node:path";
3
- import { parseFrontmatter } from "./lib.js";
3
+ import { adaptStdioCommand, parseFrontmatter } from "./lib.js";
4
4
  function readSkillEntryPoint(skillDir) {
5
5
  const candidates = ["SKILL.md", "README.md"];
6
6
  for (const c of candidates) {
@@ -71,6 +71,9 @@ export function inlineReferences(refs) {
71
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
72
  }
73
73
  export function renderMcpForTarget(mcp, spec) {
74
+ if (mcp.transport === "stdio") {
75
+ return renderStdioMcpForTarget(mcp, spec);
76
+ }
74
77
  const obj = {
75
78
  [spec.mcpUrlKey]: mcp.url,
76
79
  };
@@ -88,6 +91,20 @@ export function renderMcpForTarget(mcp, spec) {
88
91
  }
89
92
  return obj;
90
93
  }
94
+ function renderStdioMcpForTarget(mcp, spec) {
95
+ const { command, args } = adaptStdioCommand(mcp.command ?? "", mcp.args ?? []);
96
+ const hasEnv = mcp.env && Object.keys(mcp.env).length > 0;
97
+ if (spec.id === "opencode") {
98
+ const obj = { type: "local", command: [command, ...args], enabled: true };
99
+ if (hasEnv)
100
+ obj.environment = mcp.env;
101
+ return obj;
102
+ }
103
+ const obj = { command, args };
104
+ if (hasEnv)
105
+ obj.env = mcp.env;
106
+ return obj;
107
+ }
91
108
  function bearerInterpolation(env, target) {
92
109
  switch (target) {
93
110
  case "claude-code":
package/dist/targets.js CHANGED
@@ -1,14 +1,14 @@
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, getCodexConfigPath, getCodexSkillsDir, getCursorMcpPath, getCursorSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
4
+ import { getAntigravityMcpConfigPath, getAntigravitySkillsDir, getClaudeJsonPath, getClaudeSkillsDir, getCodexConfigPath, getCodexSkillsDir, getCursorMcpPath, getCursorSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
5
5
  export const TARGETS = {
6
6
  "claude-code": {
7
7
  id: "claude-code",
8
8
  label: "Claude Code",
9
9
  skillsDir: getClaudeSkillsDir(),
10
10
  skillFormat: "folder",
11
- mcpConfigPath: getClaudeSettingsPath(),
11
+ mcpConfigPath: getClaudeJsonPath(),
12
12
  mcpRootKey: "mcpServers",
13
13
  mcpUrlKey: "url",
14
14
  },
@@ -16,7 +16,7 @@ export const TARGETS = {
16
16
  id: "cursor",
17
17
  label: "Cursor",
18
18
  skillsDir: getCursorSkillsDir(),
19
- skillFormat: "cursor-mdc",
19
+ skillFormat: "folder",
20
20
  mcpConfigPath: getCursorMcpPath(),
21
21
  mcpRootKey: "mcpServers",
22
22
  mcpUrlKey: "url",
@@ -25,7 +25,7 @@ export const TARGETS = {
25
25
  id: "opencode",
26
26
  label: "OpenCode",
27
27
  skillsDir: getOpenCodeSkillsDir(),
28
- skillFormat: "flat-md",
28
+ skillFormat: "folder",
29
29
  mcpConfigPath: getOpenCodeConfigPath(),
30
30
  mcpRootKey: "mcp",
31
31
  mcpUrlKey: "url",
@@ -36,7 +36,7 @@ export const TARGETS = {
36
36
  label: "Antigravity",
37
37
  skillsDir: getAntigravitySkillsDir(),
38
38
  skillFormat: "folder",
39
- mcpConfigPath: getAntigravitySettingsPath(),
39
+ mcpConfigPath: getAntigravityMcpConfigPath(),
40
40
  mcpRootKey: "mcpServers",
41
41
  mcpUrlKey: "serverUrl",
42
42
  },
@@ -63,7 +63,7 @@ export function detectInstalledTargets() {
63
63
  "claude-code": join(home, ".claude"),
64
64
  cursor: join(home, ".cursor"),
65
65
  opencode: join(xdgConfig, "opencode"),
66
- antigravity: join(home, ".antigravity"),
66
+ antigravity: join(home, ".gemini", "antigravity"),
67
67
  codex: codexHome,
68
68
  };
69
69
  const out = [];
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.2",
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",