alegra-data-agent 0.1.0

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.
@@ -0,0 +1,274 @@
1
+ import { AuthRequiredError } from "../api.js";
2
+ import { hasAuth, readAuth } from "../auth.js";
3
+ import { installAll } from "../installer.js";
4
+ import { bold, cyan, dim, gray, green, log, logError, red, yellow } from "../colors.js";
5
+ import { COMMAND_NAME, formatBytes, getClaudeSkillsDir, getPackageVersion } from "../lib.js";
6
+ import { runHookInstall } from "./hook.js";
7
+ import { runLogin } from "./login.js";
8
+ const BANNER_WIDTH = 58;
9
+ const SHORT_DESCRIPTIONS = {
10
+ "alegra-bi": "Router principal de BI",
11
+ "alegra-sales": "New logos, MRR, deals, funnel, cuota",
12
+ "alegra-accountant": "Programa Contadores, FC 2.0, halo",
13
+ "alegra-growth": "Signups, MQL/PQL/SQL, conversión",
14
+ "alegra-ce-universe": "CHS, churn, retención, voice of customer",
15
+ "alegra-producto": "Métricas SaaS — MRR, ARPA, churn rate",
16
+ "alegra-strategic": "MRR walk, cross-sell, upsell, pricing",
17
+ "alanube": "Tablas Alanube en Redshift",
18
+ };
19
+ function padToWidth(text, visibleLen, width) {
20
+ const pad = Math.max(0, width - visibleLen);
21
+ return text + " ".repeat(pad);
22
+ }
23
+ function fancyBanner() {
24
+ const v = getPackageVersion();
25
+ const innerWidth = BANNER_WIDTH - 2;
26
+ const nameLen = COMMAND_NAME.length;
27
+ const versionLen = `v${v}`.length;
28
+ const spacesBetween = Math.max(1, innerWidth - 4 - nameLen - versionLen);
29
+ const line1 = " " +
30
+ bold(COMMAND_NAME) +
31
+ " ".repeat(spacesBetween) +
32
+ dim(`v${v}`);
33
+ const line1PadCount = nameLen + spacesBetween + versionLen + 3;
34
+ const line2Plain = " Agente de Data · Alegra";
35
+ const line2 = " " + gray("Agente de Data · Alegra");
36
+ const emptyPad = innerWidth + 2;
37
+ log("");
38
+ log(" " + cyan("╔" + "═".repeat(BANNER_WIDTH) + "╗"));
39
+ log(" " + cyan("║") + " ".repeat(emptyPad) + cyan("║"));
40
+ log(" " + cyan("║") + padToWidth(line1, line1PadCount, innerWidth + 2) + cyan("║"));
41
+ log(" " + cyan("║") + padToWidth(line2, line2Plain.length, innerWidth + 2) + cyan("║"));
42
+ log(" " + cyan("║") + " ".repeat(emptyPad) + cyan("║"));
43
+ log(" " + cyan("╚" + "═".repeat(BANNER_WIDTH) + "╝"));
44
+ log("");
45
+ }
46
+ function section(title) {
47
+ log(cyan("▶ ") + bold(title));
48
+ log("");
49
+ }
50
+ function shortDesc(name, manifestDesc) {
51
+ const handPicked = SHORT_DESCRIPTIONS[name];
52
+ if (handPicked)
53
+ return handPicked;
54
+ const oneLine = manifestDesc.split(/\.|\n/)[0].trim();
55
+ return oneLine.length > 55 ? oneLine.slice(0, 54) + "…" : oneLine;
56
+ }
57
+ function printResultRow(result, manifest) {
58
+ const isSkill = result.kind === "skill";
59
+ const entry = isSkill ? manifest.skills[result.name] : manifest.mcps?.[result.name];
60
+ const desc = isSkill
61
+ ? entry
62
+ ? shortDesc(result.name, entry.description)
63
+ : ""
64
+ : entry?.description ?? "MCP server";
65
+ let icon;
66
+ switch (result.status) {
67
+ case "installed":
68
+ icon = green("✓");
69
+ break;
70
+ case "updated":
71
+ icon = green("↻");
72
+ break;
73
+ case "unchanged":
74
+ icon = gray("·");
75
+ break;
76
+ case "skipped":
77
+ icon = yellow("-");
78
+ break;
79
+ case "failed":
80
+ icon = red("✗");
81
+ break;
82
+ }
83
+ const kindBadge = isSkill ? "" : dim("[mcp] ");
84
+ const padName = result.name.padEnd(22);
85
+ log(` ${icon} ${kindBadge}${bold(padName)} ${gray("· " + desc)}`);
86
+ }
87
+ function printSummary(results, manifest) {
88
+ const installed = results.filter((r) => r.status === "installed").length;
89
+ const updated = results.filter((r) => r.status === "updated").length;
90
+ const unchanged = results.filter((r) => r.status === "unchanged").length;
91
+ const failed = results.filter((r) => r.status === "failed").length;
92
+ const skipped = results.filter((r) => r.status === "skipped").length;
93
+ let totalBytes = 0;
94
+ let totalFiles = 0;
95
+ for (const r of results) {
96
+ if (r.kind !== "skill")
97
+ continue;
98
+ const e = manifest.skills[r.name];
99
+ if (e && r.status !== "failed" && r.status !== "skipped") {
100
+ totalBytes += e.bytes;
101
+ totalFiles += e.files.length;
102
+ }
103
+ }
104
+ log("");
105
+ section("Resumen");
106
+ const parts = [];
107
+ if (installed > 0)
108
+ parts.push(green(`${installed} instaladas`));
109
+ if (updated > 0)
110
+ parts.push(green(`${updated} actualizadas`));
111
+ if (unchanged > 0)
112
+ parts.push(gray(`${unchanged} sin cambios`));
113
+ if (skipped > 0)
114
+ parts.push(yellow(`${skipped} omitidas`));
115
+ if (failed > 0)
116
+ parts.push(red(`${failed} fallidas`));
117
+ log(" " +
118
+ green("✓") +
119
+ " " +
120
+ bold(parts.join(gray(" · "))) +
121
+ gray(` · ${totalFiles} archivos · ${formatBytes(totalBytes)}`));
122
+ log(" " + green("✓") + " " + gray("destino:") + " " + getClaudeSkillsDir());
123
+ log("");
124
+ for (const r of results.filter((x) => x.status === "failed")) {
125
+ logError(`${r.name}: ${r.error}`);
126
+ }
127
+ }
128
+ function printNextSteps(noHook) {
129
+ section("Siguientes pasos");
130
+ log(" " + bold(yellow("→ Reinicia Claude Code para activar las skills")));
131
+ log("");
132
+ log(" " + dim("Comandos útiles:"));
133
+ log(` ${dim("$")} ${bold(`${COMMAND_NAME} list`)} ${gray("ver skills disponibles")}`);
134
+ log(` ${dim("$")} ${bold(`${COMMAND_NAME} status`)} ${gray("estado actual + drift")}`);
135
+ log(` ${dim("$")} ${bold(`${COMMAND_NAME} update`)} ${gray("actualizar a la última")}`);
136
+ log(` ${dim("$")} ${bold(`${COMMAND_NAME} whoami`)} ${gray("cuenta autenticada")}`);
137
+ if (!noHook) {
138
+ log(` ${dim("$")} ${bold(`${COMMAND_NAME} hook uninstall`)} ${gray("apagar aviso")}`);
139
+ }
140
+ log("");
141
+ log(" " +
142
+ dim("Docs · ") +
143
+ cyan("github.com/alegradev/alegra-data-agent"));
144
+ log("");
145
+ }
146
+ function isClaudeCode() {
147
+ return process.env.CLAUDECODE === "1" || process.env.CLAUDE_CODE === "1";
148
+ }
149
+ export async function runInstall(opts = {}) {
150
+ const compact = isClaudeCode();
151
+ if (!compact)
152
+ fancyBanner();
153
+ if (!hasAuth()) {
154
+ if (!compact)
155
+ log(dim(" No hay sesión activa. Inicio el flow de login antes de instalar.\n"));
156
+ const code = await runLogin({});
157
+ if (code !== 0)
158
+ return code;
159
+ }
160
+ const auth = readAuth();
161
+ if (auth && !compact) {
162
+ section("Cuenta autorizada");
163
+ log(" " + green("✓") + " " + bold(auth.email));
164
+ log("");
165
+ }
166
+ if (opts.dryRun) {
167
+ log(dim(" Modo dry-run: no se modificará nada.\n"));
168
+ }
169
+ if (!compact) {
170
+ section("Descargando skills del gateway");
171
+ }
172
+ let result;
173
+ try {
174
+ result = await installAll({
175
+ dryRun: opts.dryRun,
176
+ force: opts.force,
177
+ only: opts.only,
178
+ targets: opts.targets,
179
+ });
180
+ }
181
+ catch (err) {
182
+ if (err instanceof AuthRequiredError) {
183
+ logError(err.message);
184
+ log(dim(` Corre: ${COMMAND_NAME} login`));
185
+ return 2;
186
+ }
187
+ const msg = err instanceof Error ? err.message : String(err);
188
+ logError(msg);
189
+ if (msg.includes("EACCES") || msg.includes("permission denied")) {
190
+ log(dim(` Sin permisos para escribir. Verifica permisos de ~/.claude/skills/`));
191
+ }
192
+ else if (msg.includes("ENOSPC") || msg.includes("no space")) {
193
+ log(dim(` No hay espacio en disco. Libera espacio y reintenta.`));
194
+ }
195
+ else if (msg.includes("network") || msg.includes("ENOTFOUND") || msg.includes("ECONNREFUSED")) {
196
+ log(dim(` Verifica tu conexión a internet y el estado del gateway.`));
197
+ }
198
+ return 1;
199
+ }
200
+ if ("error" in result) {
201
+ logError(result.error);
202
+ return 1;
203
+ }
204
+ if (compact) {
205
+ printCompactSummary(result.results, result.manifest, auth?.email);
206
+ }
207
+ else {
208
+ for (const r of result.results) {
209
+ printResultRow(r, result.manifest);
210
+ }
211
+ printSummary(result.results, result.manifest);
212
+ }
213
+ const hasFails = result.results.some((r) => r.status === "failed");
214
+ if (hasFails)
215
+ return 1;
216
+ if (opts.dryRun)
217
+ return 0;
218
+ if (!opts.noHook) {
219
+ if (!compact)
220
+ log(" " + gray("Instalando hook de notificaciones…"));
221
+ runHookInstall(opts.hookMode ?? "check");
222
+ if (!compact)
223
+ log("");
224
+ }
225
+ if (!compact) {
226
+ printNextSteps(opts.noHook ?? false);
227
+ }
228
+ return 0;
229
+ }
230
+ function printCompactSummary(results, manifest, email) {
231
+ const ok = results.filter((r) => r.status === "installed" || r.status === "updated" || r.status === "unchanged");
232
+ const failed = results.filter((r) => r.status === "failed");
233
+ const installed = results.filter((r) => r.status === "installed").length;
234
+ const updated = results.filter((r) => r.status === "updated").length;
235
+ const unchanged = results.filter((r) => r.status === "unchanged").length;
236
+ let totalBytes = 0;
237
+ let totalFiles = 0;
238
+ const skillOks = ok.filter((r) => r.kind === "skill");
239
+ const mcpOks = ok.filter((r) => r.kind === "mcp");
240
+ for (const r of skillOks) {
241
+ const e = manifest.skills[r.name];
242
+ if (e) {
243
+ totalBytes += e.bytes;
244
+ totalFiles += e.files.length;
245
+ }
246
+ }
247
+ const verbo = updated > 0 && installed === 0 ? "actualizadas" : "instaladas";
248
+ const skillList = skillOks.map((r) => r.name).sort().join(", ");
249
+ const userPart = email ? gray(` · ${email}`) : "";
250
+ log("");
251
+ log(" " +
252
+ green("✓") +
253
+ " " +
254
+ bold(`${skillOks.length} skills + ${mcpOks.length} MCPs ${verbo}`) +
255
+ gray(` · ${totalFiles} archivos · ${formatBytes(totalBytes)}`) +
256
+ userPart);
257
+ if (skillList)
258
+ log(" " + gray("skills: ") + gray(skillList));
259
+ if (mcpOks.length > 0)
260
+ log(" " + gray("mcps: ") + gray(mcpOks.map((r) => r.name).sort().join(", ")));
261
+ log(" " +
262
+ gray("destino:") +
263
+ " " +
264
+ cyan(getClaudeSkillsDir()) +
265
+ gray(" · ") +
266
+ bold(yellow("reinicia tu cliente para activarlas")));
267
+ if (unchanged > 0 && installed === 0 && updated === 0) {
268
+ log(" " + dim(`(${unchanged} sin cambios desde la última instalación)`));
269
+ }
270
+ log("");
271
+ for (const r of failed) {
272
+ logError(`${r.name}: ${r.error}`);
273
+ }
274
+ }
@@ -0,0 +1,44 @@
1
+ import { AuthRequiredError, fetchManifest } from "../api.js";
2
+ import { readLockFile, formatBytes } from "../lib.js";
3
+ import { bold, cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
4
+ export async function runList() {
5
+ let manifest;
6
+ try {
7
+ manifest = await fetchManifest();
8
+ }
9
+ catch (err) {
10
+ if (err instanceof AuthRequiredError) {
11
+ logError(err.message);
12
+ return 2;
13
+ }
14
+ logError(err instanceof Error ? err.message : String(err));
15
+ return 1;
16
+ }
17
+ if (!manifest) {
18
+ logError("No se pudo cargar el manifest del gateway.");
19
+ return 1;
20
+ }
21
+ const lock = readLockFile();
22
+ log("");
23
+ log(bold(cyan(" Skills disponibles")) + dim(` — ${Object.keys(manifest.skills).length} totales`));
24
+ log(gray(" " + "─".repeat(70)));
25
+ const names = Object.keys(manifest.skills).sort();
26
+ for (const name of names) {
27
+ const entry = manifest.skills[name];
28
+ const installed = lock.skills[name];
29
+ const sameHash = installed?.bundleHash === entry.bundleHash;
30
+ let badge;
31
+ if (!installed)
32
+ badge = gray("[no instalada]");
33
+ else if (sameHash)
34
+ badge = green("[instalada] ");
35
+ else
36
+ badge = yellow("[actualizar] ");
37
+ const team = entry.team ? gray(` ${entry.team}`) : "";
38
+ log(` ${badge} ${bold(name)}${team} ${dim(formatBytes(entry.bytes))}`);
39
+ const desc = entry.description.length > 200 ? entry.description.slice(0, 200) + "…" : entry.description;
40
+ log(gray(` ${desc}`));
41
+ log("");
42
+ }
43
+ return 0;
44
+ }
@@ -0,0 +1,31 @@
1
+ import { performBrowserLogin, readAuth, writeAuth } from "../auth.js";
2
+ import { banner } from "../ui.js";
3
+ import { bold, dim, green, log, logError } from "../colors.js";
4
+ import { GATEWAY_URL, getAuthFilePath } from "../lib.js";
5
+ export async function runLogin(opts = {}) {
6
+ banner();
7
+ if (!opts.force) {
8
+ const existing = readAuth();
9
+ if (existing) {
10
+ log(` ${green("✓")} Ya estás autenticado como ${bold(existing.email)}.`);
11
+ log(dim(` Token: ${getAuthFilePath()}`));
12
+ log(dim(` Para volver a iniciar sesión: --force`));
13
+ log("");
14
+ return 0;
15
+ }
16
+ }
17
+ log(` Gateway: ${dim(GATEWAY_URL)}`);
18
+ try {
19
+ const record = await performBrowserLogin();
20
+ writeAuth(record);
21
+ log("");
22
+ log(` ${green("✓")} Autenticado como ${bold(record.email)}.`);
23
+ log(dim(` Token guardado en: ${getAuthFilePath()}`));
24
+ log("");
25
+ return 0;
26
+ }
27
+ catch (err) {
28
+ logError(err instanceof Error ? err.message : String(err));
29
+ return 1;
30
+ }
31
+ }
@@ -0,0 +1,13 @@
1
+ import { clearAuth, readAuth } from "../auth.js";
2
+ import { dim, log, logOk, logWarn } from "../colors.js";
3
+ export function runLogout() {
4
+ const existing = readAuth();
5
+ if (!existing) {
6
+ logWarn("No hay sesión activa.");
7
+ return 0;
8
+ }
9
+ clearAuth();
10
+ logOk(`Sesión cerrada (${existing.email}).`);
11
+ log(dim(" Tus skills instaladas no se eliminan. Para borrarlas: uninstall."));
12
+ return 0;
13
+ }
@@ -0,0 +1,61 @@
1
+ import { AuthRequiredError, fetchManifest } from "../api.js";
2
+ import { readAuth } from "../auth.js";
3
+ import { detectDrift } from "../installer.js";
4
+ import { COMMAND_NAME, GATEWAY_URL, getClaudeSkillsDir, getLockFilePath, getPackageVersion, readLockFile, } from "../lib.js";
5
+ import { bold, cyan, dim, gray, green, log, logError, logWarn, yellow } from "../colors.js";
6
+ export async function runStatus() {
7
+ const auth = readAuth();
8
+ log("");
9
+ log(bold(cyan(" Estado")));
10
+ log(gray(" " + "─".repeat(70)));
11
+ log(` ${gray("Paquete:")} ${bold(COMMAND_NAME)} v${getPackageVersion()}`);
12
+ log(` ${gray("Gateway:")} ${GATEWAY_URL}`);
13
+ log(` ${gray("Autenticado:")} ${auth ? green(auth.email) : gray("no")}`);
14
+ log(` ${gray("Destino:")} ${getClaudeSkillsDir()}`);
15
+ log(` ${gray("Lock file:")} ${getLockFilePath()}`);
16
+ log("");
17
+ if (!auth) {
18
+ logWarn(`Inicia sesión con: ${bold(`${COMMAND_NAME} login`)}`);
19
+ log("");
20
+ return 0;
21
+ }
22
+ let manifest;
23
+ try {
24
+ manifest = await fetchManifest();
25
+ }
26
+ catch (err) {
27
+ if (err instanceof AuthRequiredError) {
28
+ logError(err.message);
29
+ return 2;
30
+ }
31
+ logError(err instanceof Error ? err.message : String(err));
32
+ return 1;
33
+ }
34
+ if (!manifest) {
35
+ logError("No se pudo cargar el manifest.");
36
+ return 1;
37
+ }
38
+ const lock = readLockFile();
39
+ const drift = detectDrift(manifest);
40
+ log(bold(" Skills:"));
41
+ log(` ${green("✓")} ${drift.unchanged.length} al día`);
42
+ if (drift.toInstall.length > 0)
43
+ log(` ${cyan("+")} ${drift.toInstall.length} nuevas disponibles: ${drift.toInstall.join(", ")}`);
44
+ if (drift.toUpdate.length > 0)
45
+ log(` ${yellow("↻")} ${drift.toUpdate.length} con actualización: ${drift.toUpdate.join(", ")}`);
46
+ if (drift.orphaned.length > 0)
47
+ log(` ${dim("·")} ${drift.orphaned.length} huérfanas: ${drift.orphaned.join(", ")}`);
48
+ log("");
49
+ const total = drift.toInstall.length + drift.toUpdate.length;
50
+ if (total > 0) {
51
+ logWarn(`Hay ${total} skill(s) por actualizar. Corre: ${bold(`${COMMAND_NAME} update`)}`);
52
+ }
53
+ else if (Object.keys(lock.skills).length === 0) {
54
+ logWarn(`Aún no hay skills instaladas. Corre: ${bold(`${COMMAND_NAME} install`)}`);
55
+ }
56
+ else {
57
+ log(` ${green("Todo al día.")}`);
58
+ }
59
+ log("");
60
+ return 0;
61
+ }
@@ -0,0 +1,28 @@
1
+ import { uninstallAll } from "../installer.js";
2
+ import { readLockFile } from "../lib.js";
3
+ import { banner, confirm } from "../ui.js";
4
+ import { dim, gray, log, logOk, logWarn, red } from "../colors.js";
5
+ export async function runUninstall(opts = {}) {
6
+ banner();
7
+ const lock = readLockFile();
8
+ const skills = Object.keys(lock.skills);
9
+ if (skills.length === 0) {
10
+ logWarn("No hay skills instaladas por este paquete.");
11
+ return 0;
12
+ }
13
+ log(red(` Vas a eliminar ${skills.length} skill(s):`));
14
+ for (const s of skills)
15
+ log(gray(` - ${s}`));
16
+ log("");
17
+ if (!opts.yes && !opts.dryRun) {
18
+ const ok = await confirm("¿Continuar?", false);
19
+ if (!ok) {
20
+ log(dim(" Cancelado."));
21
+ return 0;
22
+ }
23
+ }
24
+ const results = uninstallAll({ dryRun: opts.dryRun });
25
+ log("");
26
+ logOk(`${results.length} skill(s) eliminadas.`);
27
+ return 0;
28
+ }
@@ -0,0 +1,56 @@
1
+ import { AuthRequiredError, fetchManifest } from "../api.js";
2
+ import { detectDrift, installAll, summarize } from "../installer.js";
3
+ import { banner } from "../ui.js";
4
+ import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
5
+ export async function runUpdate(opts = {}) {
6
+ banner();
7
+ let manifest;
8
+ try {
9
+ manifest = await fetchManifest();
10
+ }
11
+ catch (err) {
12
+ if (err instanceof AuthRequiredError) {
13
+ logError(err.message);
14
+ return 2;
15
+ }
16
+ logError(err instanceof Error ? err.message : String(err));
17
+ return 1;
18
+ }
19
+ if (!manifest) {
20
+ logError("No se pudo cargar el manifest.");
21
+ return 1;
22
+ }
23
+ const drift = detectDrift(manifest);
24
+ const pending = [
25
+ ...drift.toInstall,
26
+ ...drift.toUpdate,
27
+ ...drift.mcps.toInstall,
28
+ ...drift.mcps.toUpdate,
29
+ ];
30
+ if (pending.length === 0) {
31
+ log(green(" ✓ Todo al día (skills + MCPs).\n"));
32
+ return 0;
33
+ }
34
+ log(cyan(` → ${pending.length} elemento(s) por actualizar:\n`));
35
+ for (const name of drift.toInstall)
36
+ log(` ${cyan("+")} ${name} ${gray("(skill nueva)")}`);
37
+ for (const name of drift.toUpdate)
38
+ log(` ${yellow("↻")} ${name} ${gray("(skill nueva versión)")}`);
39
+ for (const name of drift.mcps.toInstall)
40
+ log(` ${cyan("+")} ${name} ${gray("(MCP nuevo)")}`);
41
+ for (const name of drift.mcps.toUpdate)
42
+ log(` ${yellow("↻")} ${name} ${gray("(MCP actualizado)")}`);
43
+ log("");
44
+ if (opts.dryRun) {
45
+ log(dim(" Modo dry-run: no se modificará nada.\n"));
46
+ return 0;
47
+ }
48
+ const result = await installAll({ only: pending, targets: opts.targets, force: true });
49
+ if ("error" in result) {
50
+ logError(result.error);
51
+ return 1;
52
+ }
53
+ summarize(result.results);
54
+ const failed = result.results.some((r) => r.status === "failed");
55
+ return failed ? 1 : 0;
56
+ }
@@ -0,0 +1,16 @@
1
+ import { readAuth } from "../auth.js";
2
+ import { bold, dim, log, logWarn } from "../colors.js";
3
+ import { COMMAND_NAME, getAuthFilePath } from "../lib.js";
4
+ export function runWhoami() {
5
+ const auth = readAuth();
6
+ if (!auth) {
7
+ logWarn(`No autenticado. Corre: ${COMMAND_NAME} login`);
8
+ return 1;
9
+ }
10
+ log("");
11
+ log(` ${bold(auth.email)}`);
12
+ log(dim(` Conectado desde: ${auth.obtainedAt}`));
13
+ log(dim(` Token en: ${getAuthFilePath()}`));
14
+ log("");
15
+ return 0;
16
+ }