alegra-data-agent 0.2.3 → 0.2.5

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.
@@ -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("");
@@ -137,7 +141,7 @@ function printNextSteps(noHook) {
137
141
  log(` ${dim("$")} ${bold(`${COMMAND_NAME} hook uninstall`)} ${gray("apagar aviso")}`);
138
142
  }
139
143
  log("");
140
- log(` ${dim("Docs · ")}${cyan("github.com/alegradev/alegra-data-agent")}`);
144
+ log(` ${dim("Docs · ")}${cyan("github.com/Alegra-Team/data-agent-selfservice")}`);
141
145
  log("");
142
146
  }
143
147
  function isClaudeCode() {
@@ -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
  }
@@ -19,7 +19,7 @@ export async function runLogin(opts = {}) {
19
19
  }
20
20
  log(` Gateway: ${dim(GATEWAY_URL)}`);
21
21
  try {
22
- const record = await performBrowserLogin();
22
+ const record = await performBrowserLogin(opts.timeoutMs);
23
23
  writeAuth(record);
24
24
  log("");
25
25
  log(` ${green("✓")} Autenticado como ${bold(record.email)}.`);
@@ -1,19 +1,128 @@
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
- import { detectDrift } from "../installer.js";
5
- import { COMMAND_NAME, GATEWAY_URL, getClaudeSkillsDir, getLockFilePath, getPackageVersion, readLockFile, } from "../lib.js";
5
+ import { detectDrift, inspectMcpInTarget } from "../installer.js";
6
+ import { COMMAND_NAME, GATEWAY_URL, getAutoUpdateLogPath, getClaudeSkillsDir, getLockFilePath, getPackageVersion, getTargetSkillsDir, readLockFile, } from "../lib.js";
7
+ import { TARGETS } from "../targets.js";
8
+ function reportAutoUpdateLog() {
9
+ const logPath = getAutoUpdateLogPath();
10
+ if (!existsSync(logPath))
11
+ return;
12
+ try {
13
+ const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
14
+ if (lines.length === 0)
15
+ return;
16
+ 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.`);
17
+ log("");
18
+ }
19
+ catch {
20
+ }
21
+ }
22
+ function reportMcps(manifest, lock) {
23
+ const mcps = manifest.mcps ?? {};
24
+ const names = Object.keys(mcps);
25
+ if (names.length === 0)
26
+ return;
27
+ log(bold(" MCPs:"));
28
+ for (const name of names) {
29
+ const entry = mcps[name];
30
+ const targets = (lock.mcps?.[name]?.targets ?? []);
31
+ if (targets.length === 0) {
32
+ log(` ${cyan("+")} ${name} ${gray("(disponible, sin instalar)")}`);
33
+ continue;
34
+ }
35
+ const ok = [];
36
+ const missing = [];
37
+ const modified = [];
38
+ for (const t of targets) {
39
+ const label = TARGETS[t].label;
40
+ const state = inspectMcpInTarget(entry, TARGETS[t]);
41
+ if (state === "ok")
42
+ ok.push(label);
43
+ else if (state === "missing")
44
+ missing.push(label);
45
+ else
46
+ modified.push(label);
47
+ }
48
+ const notes = [];
49
+ if (ok.length > 0)
50
+ notes.push(gray(`ok en ${ok.join(", ")}`));
51
+ if (modified.length > 0)
52
+ notes.push(dim(`editado a mano en ${modified.join(", ")}`));
53
+ if (missing.length > 0)
54
+ notes.push(yellow(`falta en ${missing.join(", ")}`));
55
+ const icon = missing.length > 0 ? yellow("✗") : modified.length > 0 ? dim("~") : green("✓");
56
+ log(` ${icon} ${name} ${gray("·")} ${notes.join(gray(" · "))}`);
57
+ }
58
+ log("");
59
+ }
60
+ export function findRogueMetabase(manifest, specs = Object.values(TARGETS)) {
61
+ const official = manifest.mcps?.metabase?.url;
62
+ if (!official)
63
+ return [];
64
+ const norm = (u) => u.trim().replace(/\/+$/, "").toLowerCase();
65
+ const officialNorm = norm(official);
66
+ const found = [];
67
+ for (const spec of specs) {
68
+ if (!spec.mcpConfigPath.endsWith(".json"))
69
+ continue;
70
+ if (!existsSync(spec.mcpConfigPath))
71
+ continue;
72
+ try {
73
+ const cfg = JSON.parse(readFileSync(spec.mcpConfigPath, "utf-8"));
74
+ const servers = cfg[spec.mcpRootKey];
75
+ if (!servers || typeof servers !== "object")
76
+ continue;
77
+ for (const [name, def] of Object.entries(servers)) {
78
+ if (!/metabase/i.test(name))
79
+ continue;
80
+ const url = def?.[spec.mcpUrlKey];
81
+ if (typeof url === "string" && norm(url) === officialNorm)
82
+ continue;
83
+ found.push(`${spec.label} → ${name}${typeof url === "string" ? ` (${url})` : ""}`);
84
+ }
85
+ }
86
+ catch {
87
+ }
88
+ }
89
+ return found;
90
+ }
91
+ function reportRogueMetabase(manifest) {
92
+ const official = manifest.mcps?.metabase?.url;
93
+ if (!official)
94
+ return;
95
+ const found = findRogueMetabase(manifest);
96
+ if (found.length === 0)
97
+ return;
98
+ logWarn(`Hay ${found.length} MCP(s) de Metabase que NO son el oficial de Alegra: ${found.join(" · ")}`);
99
+ log(gray(` El oficial es ${official}. Los otros son otra fuente y dan otros números.`));
100
+ log(gray(" Las skills tienen instrucción de no usarlos, pero eso no los bloquea:"));
101
+ log(gray(" si no los necesitás, conviene quitarlos del cliente."));
102
+ log("");
103
+ }
6
104
  export async function runStatus() {
7
105
  const auth = readAuth();
106
+ const lockForTargets = readLockFile();
107
+ const installedTargets = [
108
+ ...new Set([
109
+ ...Object.values(lockForTargets.skills).flatMap((s) => s.targets ?? []),
110
+ ...Object.values(lockForTargets.mcps ?? {}).flatMap((m) => m.targets ?? []),
111
+ ]),
112
+ ];
113
+ const destino = installedTargets.length
114
+ ? installedTargets.map((t) => getTargetSkillsDir(t)).join(" · ")
115
+ : getClaudeSkillsDir();
8
116
  log("");
9
117
  log(bold(cyan(" Estado")));
10
118
  log(gray(` ${"─".repeat(70)}`));
11
119
  log(` ${gray("Paquete:")} ${bold(COMMAND_NAME)} v${getPackageVersion()}`);
12
120
  log(` ${gray("Gateway:")} ${GATEWAY_URL}`);
13
121
  log(` ${gray("Autenticado:")} ${auth ? green(auth.email) : gray("no")}`);
14
- log(` ${gray("Destino:")} ${getClaudeSkillsDir()}`);
122
+ log(` ${gray("Destino:")} ${destino}`);
15
123
  log(` ${gray("Lock file:")} ${getLockFilePath()}`);
16
124
  log("");
125
+ reportAutoUpdateLog();
17
126
  if (!auth) {
18
127
  logWarn(`Inicia sesión con: ${bold(`${COMMAND_NAME} login`)}`);
19
128
  log("");
@@ -37,6 +146,12 @@ export async function runStatus() {
37
146
  }
38
147
  const lock = readLockFile();
39
148
  const drift = detectDrift(manifest);
149
+ const cliVersion = getPackageVersion();
150
+ if (manifest.productVersion && manifest.productVersion !== cliVersion) {
151
+ log(` ${gray("Manifest:")} generado por v${manifest.productVersion} ${gray(`· tu CLI corre v${cliVersion}`)}`);
152
+ log(gray(" No es un error: el gateway y npm se despliegan por separado."));
153
+ log("");
154
+ }
40
155
  log(bold(" Skills:"));
41
156
  log(` ${green("✓")} ${drift.unchanged.length} al día`);
42
157
  if (drift.toInstall.length > 0)
@@ -46,9 +161,14 @@ export async function runStatus() {
46
161
  if (drift.orphaned.length > 0)
47
162
  log(` ${dim("·")} ${drift.orphaned.length} huérfanas: ${drift.orphaned.join(", ")}`);
48
163
  log("");
49
- const total = drift.toInstall.length + drift.toUpdate.length;
164
+ reportMcps(manifest, lock);
165
+ reportRogueMetabase(manifest);
166
+ const total = drift.toInstall.length +
167
+ drift.toUpdate.length +
168
+ drift.mcps.toInstall.length +
169
+ drift.mcps.toUpdate.length;
50
170
  if (total > 0) {
51
- logWarn(`Hay ${total} skill(s) por actualizar. Agente: ejecutá vos mismo ${bold(`pnpm dlx ${COMMAND_NAME} update --yes`)} — el usuario no corre comandos.`);
171
+ logWarn(`Hay ${total} elemento(s) (skills + MCPs) por actualizar. Agente: ejecutá vos mismo ${bold(`pnpm dlx ${COMMAND_NAME} update --yes`)} — el usuario no corre comandos.`);
52
172
  }
53
173
  else if (Object.keys(lock.skills).length === 0) {
54
174
  logWarn(`Aún no hay skills instaladas. Agente: ejecutá vos mismo ${bold(`pnpm dlx ${COMMAND_NAME}`)} — el usuario no corre comandos.`);
@@ -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,16 +1,45 @@
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
1
3
  import { AuthRequiredError, fetchManifest } from "../api.js";
2
4
  import { hasValidAuth } from "../auth.js";
5
+ import { refreshAutoUpdate } from "../autoupdate.js";
3
6
  import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
4
7
  import { detectDrift, installAll, summarize } from "../installer.js";
8
+ import { getAutoUpdateLogPath, getLastCheckPath } from "../lib.js";
5
9
  import { banner } from "../ui.js";
6
- import { refreshHookCommand } from "./hook.js";
7
10
  import { runLogin } from "./login.js";
11
+ const BACKGROUND_COOLDOWN_MS = 15 * 60_000;
12
+ const BACKGROUND_LOGIN_TIMEOUT_MS = 90_000;
13
+ function checkedRecently() {
14
+ try {
15
+ const raw = readFileSync(getLastCheckPath(), "utf-8");
16
+ const at = Date.parse(JSON.parse(raw).checkedAt ?? "");
17
+ if (Number.isNaN(at))
18
+ return false;
19
+ const age = Date.now() - at;
20
+ return age >= 0 && age < BACKGROUND_COOLDOWN_MS;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ function markChecked() {
27
+ try {
28
+ const path = getLastCheckPath();
29
+ mkdirSync(dirname(path), { recursive: true });
30
+ writeFileSync(path, `${JSON.stringify({ checkedAt: new Date().toISOString() }, null, 2)}\n`);
31
+ }
32
+ catch {
33
+ }
34
+ }
8
35
  export async function runUpdate(opts = {}) {
36
+ if (opts.background && !opts.dryRun && checkedRecently())
37
+ return 0;
9
38
  banner();
10
39
  if (!hasValidAuth()) {
11
- const code = await runLogin({});
40
+ const code = await runLogin(opts.background ? { timeoutMs: BACKGROUND_LOGIN_TIMEOUT_MS } : {});
12
41
  if (code !== 0)
13
- return code;
42
+ return opts.background ? 0 : code;
14
43
  }
15
44
  let reauthed = false;
16
45
  for (;;) {
@@ -51,7 +80,11 @@ async function runUpdateOnce(opts) {
51
80
  if (pending.length === 0) {
52
81
  log(green(" ✓ Todo al día (skills + MCPs).\n"));
53
82
  if (!opts.dryRun)
54
- refreshHookCommand();
83
+ markChecked();
84
+ if (!opts.dryRun) {
85
+ refreshAutoUpdate();
86
+ clearAutoUpdateLog();
87
+ }
55
88
  return 0;
56
89
  }
57
90
  log(cyan(` → ${pending.length} elemento(s) por actualizar:\n`));
@@ -61,20 +94,35 @@ async function runUpdateOnce(opts) {
61
94
  log(` ${yellow("↻")} ${name} ${gray("(skill nueva versión)")}`);
62
95
  for (const name of drift.mcps.toInstall)
63
96
  log(` ${cyan("+")} ${name} ${gray("(MCP nuevo)")}`);
64
- for (const name of drift.mcps.toUpdate)
65
- log(` ${yellow("↻")} ${name} ${gray("(MCP actualizado)")}`);
97
+ for (const name of drift.mcps.toUpdate) {
98
+ const motivo = drift.mcps.missing.includes(name)
99
+ ? "(MCP ausente en el cliente — se repone)"
100
+ : "(MCP actualizado)";
101
+ log(` ${yellow("↻")} ${name} ${gray(motivo)}`);
102
+ }
66
103
  log("");
67
104
  if (opts.dryRun) {
68
105
  log(dim(" Modo dry-run: no se modificará nada.\n"));
69
106
  return 0;
70
107
  }
71
- const result = await installAll({ only: pending, targets: opts.targets, force: true });
108
+ const result = await installAll({ only: pending, targets: opts.targets });
72
109
  if ("error" in result) {
73
110
  logError(result.error);
74
111
  return 1;
75
112
  }
76
113
  summarize(result.results);
77
- refreshHookCommand();
114
+ refreshAutoUpdate();
78
115
  const failed = result.results.some((r) => r.status === "failed");
116
+ if (!failed) {
117
+ clearAutoUpdateLog();
118
+ markChecked();
119
+ }
79
120
  return failed ? 1 : 0;
80
121
  }
122
+ function clearAutoUpdateLog() {
123
+ try {
124
+ rmSync(getAutoUpdateLogPath(), { force: true });
125
+ }
126
+ catch {
127
+ }
128
+ }