alegra-data-agent 0.1.0 → 0.2.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.
- package/dist/auth.js +17 -22
- package/dist/colors.js +7 -9
- package/dist/commands/check.js +1 -1
- package/dist/commands/hook.js +5 -6
- package/dist/commands/install.js +47 -52
- package/dist/commands/list.js +3 -3
- package/dist/commands/login.js +2 -2
- package/dist/commands/status.js +2 -2
- package/dist/commands/uninstall.js +1 -1
- package/dist/commands/update.js +1 -1
- package/dist/installer.js +6 -8
- package/dist/lib.js +3 -4
- package/dist/main.js +5 -2
- package/dist/signature.js +10 -5
- package/dist/ui.js +2 -2
- package/index.mjs +2 -4
- package/package.json +4 -2
package/dist/auth.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
1
3
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
4
|
import { createServer } from "node:http";
|
|
3
5
|
import { dirname } from "node:path";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
6
6
|
import { GATEWAY_URL, getAuthFilePath } from "./lib.js";
|
|
7
7
|
function decodeJwtEmail(jwt) {
|
|
8
8
|
const parts = jwt.split(".");
|
|
@@ -39,7 +39,7 @@ export function readAuth() {
|
|
|
39
39
|
export function writeAuth(record) {
|
|
40
40
|
const path = getAuthFilePath();
|
|
41
41
|
mkdirSync(dirname(path), { recursive: true });
|
|
42
|
-
writeFileSync(path, JSON.stringify(record, null, 2)
|
|
42
|
+
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
|
|
43
43
|
try {
|
|
44
44
|
chmodSync(path, 0o600);
|
|
45
45
|
}
|
|
@@ -139,7 +139,7 @@ export async function performBrowserLogin(timeoutMs = 5 * 60 * 1000) {
|
|
|
139
139
|
}
|
|
140
140
|
});
|
|
141
141
|
server.listen(0, "127.0.0.1", () => {
|
|
142
|
-
const address = server
|
|
142
|
+
const address = server?.address();
|
|
143
143
|
if (typeof address !== "object" || !address) {
|
|
144
144
|
cleanup();
|
|
145
145
|
reject(new Error("No se pudo determinar el puerto."));
|
|
@@ -150,26 +150,21 @@ export async function performBrowserLogin(timeoutMs = 5 * 60 * 1000) {
|
|
|
150
150
|
authUrl.searchParams.set("port", String(port));
|
|
151
151
|
authUrl.searchParams.set("state", state);
|
|
152
152
|
const u = authUrl.toString();
|
|
153
|
-
process.stdout.write(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
" esta URL (triple-click selecciona la línea completa) y\n" +
|
|
160
|
-
" pégala en tu navegador:\n" +
|
|
161
|
-
"\n" +
|
|
162
|
-
" ────────────────────────────────────────────────────────────\n" +
|
|
163
|
-
`${u}\n` +
|
|
164
|
-
" ────────────────────────────────────────────────────────────\n" +
|
|
165
|
-
"\n" +
|
|
166
|
-
` Esperando hasta ${Math.round(timeoutMs / 1000)}s a que completes el login…\n` +
|
|
167
|
-
"\n");
|
|
153
|
+
process.stdout.write(`\n Abrí esta URL para iniciar sesión con tu cuenta @alegra.com:\n\n${u}\n\n Si el navegador no se abrió solo: copiá la URL de arriba y pegala en tu navegador.\n Esperando hasta ${Math.round(timeoutMs / 1000)}s…\n\n`);
|
|
154
|
+
try {
|
|
155
|
+
process.stdout._flush?.();
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
}
|
|
168
159
|
openBrowser(u);
|
|
160
|
+
const nudgeTimer = setTimeout(() => {
|
|
161
|
+
if (resolved)
|
|
162
|
+
return;
|
|
163
|
+
process.stdout.write(`\n ¿No se abrió el navegador? Copiá esta URL y pegala manualmente:\n\n${u}\n\n`);
|
|
164
|
+
}, 8000);
|
|
169
165
|
timer = setTimeout(() => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
"desde la misma máquina donde abrirás el browser. Reintenta."));
|
|
166
|
+
clearTimeout(nudgeTimer);
|
|
167
|
+
safeReject(new Error(`Timeout: pasaron ${Math.round(timeoutMs / 1000)}s sin recibir la auth.\n\nSi el navegador no se abrió, copiá esta URL manualmente en tu navegador y completá el login:\n\n${u}\n\nSi abriste el link en otra máquina sin conexión al CLI, debés correr el comando desde la misma máquina donde abrirás el navegador.`));
|
|
173
168
|
}, timeoutMs);
|
|
174
169
|
});
|
|
175
170
|
});
|
package/dist/colors.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
const SUPPORTS_COLOR = process.stdout.isTTY &&
|
|
2
|
-
|
|
3
|
-
process.env.NO_COLOR === undefined;
|
|
4
|
-
const wrap = (open, close) => (s) => (SUPPORTS_COLOR ? `${open}${s}${close}` : String(s));
|
|
1
|
+
const SUPPORTS_COLOR = process.stdout.isTTY && process.env.TERM !== "dumb" && process.env.NO_COLOR === undefined;
|
|
2
|
+
const wrap = (open, close) => (s) => SUPPORTS_COLOR ? `${open}${s}${close}` : String(s);
|
|
5
3
|
export const bold = wrap("\x1b[1m", "\x1b[22m");
|
|
6
4
|
export const dim = wrap("\x1b[2m", "\x1b[22m");
|
|
7
5
|
export const red = wrap("\x1b[31m", "\x1b[39m");
|
|
@@ -19,17 +17,17 @@ export function write(s) {
|
|
|
19
17
|
process.stdout.write(s);
|
|
20
18
|
}
|
|
21
19
|
export function log(s = "") {
|
|
22
|
-
process.stdout.write(s
|
|
20
|
+
process.stdout.write(`${s}\n`);
|
|
23
21
|
}
|
|
24
22
|
export function logError(s) {
|
|
25
|
-
process.stderr.write(red("✗ ") + s
|
|
23
|
+
process.stderr.write(`${red("✗ ") + s}\n`);
|
|
26
24
|
}
|
|
27
25
|
export function logWarn(s) {
|
|
28
|
-
process.stdout.write(yellow("⚠ ") + s
|
|
26
|
+
process.stdout.write(`${yellow("⚠ ") + s}\n`);
|
|
29
27
|
}
|
|
30
28
|
export function logOk(s) {
|
|
31
|
-
process.stdout.write(green("✓ ") + s
|
|
29
|
+
process.stdout.write(`${green("✓ ") + s}\n`);
|
|
32
30
|
}
|
|
33
31
|
export function logInfo(s) {
|
|
34
|
-
process.stdout.write(cyan("ℹ ") + s
|
|
32
|
+
process.stdout.write(`${cyan("ℹ ") + s}\n`);
|
|
35
33
|
}
|
package/dist/commands/check.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fetchManifest } from "../api.js";
|
|
2
|
-
import { detectDrift } from "../installer.js";
|
|
3
2
|
import { hasAuth } from "../auth.js";
|
|
4
3
|
import { bold, cyan, dim, yellow } from "../colors.js";
|
|
4
|
+
import { detectDrift } from "../installer.js";
|
|
5
5
|
import { COMMAND_NAME } from "../lib.js";
|
|
6
6
|
export async function runCheck() {
|
|
7
7
|
try {
|
package/dist/commands/hook.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { COMMAND_NAME, getClaudeSettingsPath, getPackageVersion, PRODUCT_NAME, } from "../lib.js";
|
|
3
2
|
import { logError, logOk, logWarn } from "../colors.js";
|
|
3
|
+
import { COMMAND_NAME, PRODUCT_NAME, getClaudeSettingsPath, getPackageVersion } from "../lib.js";
|
|
4
4
|
function hookCommandFor(mode) {
|
|
5
5
|
const v = getPackageVersion();
|
|
6
6
|
const parts = v.split(".");
|
|
@@ -24,7 +24,7 @@ function readSettings() {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
function writeSettings(s) {
|
|
27
|
-
writeFileSync(getClaudeSettingsPath(), JSON.stringify(s, null, 2)
|
|
27
|
+
writeFileSync(getClaudeSettingsPath(), `${JSON.stringify(s, null, 2)}\n`, "utf-8");
|
|
28
28
|
}
|
|
29
29
|
function isOurMatcher(m) {
|
|
30
30
|
if (!m || typeof m !== "object")
|
|
@@ -49,8 +49,7 @@ export function runHookInstall(mode = "check") {
|
|
|
49
49
|
const compact = process.env.CLAUDECODE === "1" || process.env.CLAUDE_CODE === "1";
|
|
50
50
|
if (typeof existing === "string") {
|
|
51
51
|
if (!existing.includes(HOOK_SIGNATURE)) {
|
|
52
|
-
logWarn(`Ya hay un SessionStart hook configurado (formato string legacy):\n ${existing}\n
|
|
53
|
-
` No lo sobrescribo. Conviértelo a formato array manualmente.`);
|
|
52
|
+
logWarn(`Ya hay un SessionStart hook configurado (formato string legacy):\n ${existing}\n No lo sobrescribo. Conviértelo a formato array manualmente.`);
|
|
54
53
|
return 1;
|
|
55
54
|
}
|
|
56
55
|
settings.hooks.SessionStart = [buildOurMatcher(command)];
|
|
@@ -95,7 +94,7 @@ export function runHookUninstall() {
|
|
|
95
94
|
logWarn(`El SessionStart hook actual no es de ${PRODUCT_NAME}. No lo toco.`);
|
|
96
95
|
return 1;
|
|
97
96
|
}
|
|
98
|
-
|
|
97
|
+
settings.hooks.SessionStart = undefined;
|
|
99
98
|
writeSettings(settings);
|
|
100
99
|
logOk("Hook removido.");
|
|
101
100
|
return 0;
|
|
@@ -110,7 +109,7 @@ export function runHookUninstall() {
|
|
|
110
109
|
return 1;
|
|
111
110
|
}
|
|
112
111
|
if (filtered.length === 0) {
|
|
113
|
-
|
|
112
|
+
settings.hooks.SessionStart = undefined;
|
|
114
113
|
}
|
|
115
114
|
else {
|
|
116
115
|
settings.hooks.SessionStart = filtered;
|
package/dist/commands/install.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AuthRequiredError } from "../api.js";
|
|
2
2
|
import { hasAuth, readAuth } from "../auth.js";
|
|
3
|
-
import { installAll } from "../installer.js";
|
|
4
3
|
import { bold, cyan, dim, gray, green, log, logError, red, yellow } from "../colors.js";
|
|
4
|
+
import { installAll } from "../installer.js";
|
|
5
5
|
import { COMMAND_NAME, formatBytes, getClaudeSkillsDir, getPackageVersion } from "../lib.js";
|
|
6
6
|
import { runHookInstall } from "./hook.js";
|
|
7
7
|
import { runLogin } from "./login.js";
|
|
@@ -11,10 +11,10 @@ const SHORT_DESCRIPTIONS = {
|
|
|
11
11
|
"alegra-sales": "New logos, MRR, deals, funnel, cuota",
|
|
12
12
|
"alegra-accountant": "Programa Contadores, FC 2.0, halo",
|
|
13
13
|
"alegra-growth": "Signups, MQL/PQL/SQL, conversión",
|
|
14
|
-
"alegra-
|
|
15
|
-
"alegra-
|
|
14
|
+
"alegra-cx": "CHS, churn, retención, voice of customer",
|
|
15
|
+
"alegra-product": "Métricas SaaS — MRR, ARPA, churn rate",
|
|
16
16
|
"alegra-strategic": "MRR walk, cross-sell, upsell, pricing",
|
|
17
|
-
|
|
17
|
+
alanube: "Tablas Alanube en Redshift",
|
|
18
18
|
};
|
|
19
19
|
function padToWidth(text, visibleLen, width) {
|
|
20
20
|
const pad = Math.max(0, width - visibleLen);
|
|
@@ -26,21 +26,18 @@ function fancyBanner() {
|
|
|
26
26
|
const nameLen = COMMAND_NAME.length;
|
|
27
27
|
const versionLen = `v${v}`.length;
|
|
28
28
|
const spacesBetween = Math.max(1, innerWidth - 4 - nameLen - versionLen);
|
|
29
|
-
const line1 =
|
|
30
|
-
bold(COMMAND_NAME) +
|
|
31
|
-
" ".repeat(spacesBetween) +
|
|
32
|
-
dim(`v${v}`);
|
|
29
|
+
const line1 = ` ${bold(COMMAND_NAME)}${" ".repeat(spacesBetween)}${dim(`v${v}`)}`;
|
|
33
30
|
const line1PadCount = nameLen + spacesBetween + versionLen + 3;
|
|
34
31
|
const line2Plain = " Agente de Data · Alegra";
|
|
35
|
-
const line2 =
|
|
32
|
+
const line2 = ` ${gray("Agente de Data · Alegra")}`;
|
|
36
33
|
const emptyPad = innerWidth + 2;
|
|
37
34
|
log("");
|
|
38
|
-
log(
|
|
39
|
-
log(
|
|
40
|
-
log(
|
|
41
|
-
log(
|
|
42
|
-
log(
|
|
43
|
-
log(
|
|
35
|
+
log(` ${cyan(`╔${"═".repeat(BANNER_WIDTH)}╗`)}`);
|
|
36
|
+
log(` ${cyan("║")}${" ".repeat(emptyPad)}${cyan("║")}`);
|
|
37
|
+
log(` ${cyan("║")}${padToWidth(line1, line1PadCount, innerWidth + 2)}${cyan("║")}`);
|
|
38
|
+
log(` ${cyan("║")}${padToWidth(line2, line2Plain.length, innerWidth + 2)}${cyan("║")}`);
|
|
39
|
+
log(` ${cyan("║")}${" ".repeat(emptyPad)}${cyan("║")}`);
|
|
40
|
+
log(` ${cyan(`╚${"═".repeat(BANNER_WIDTH)}╝`)}`);
|
|
44
41
|
log("");
|
|
45
42
|
}
|
|
46
43
|
function section(title) {
|
|
@@ -52,7 +49,7 @@ function shortDesc(name, manifestDesc) {
|
|
|
52
49
|
if (handPicked)
|
|
53
50
|
return handPicked;
|
|
54
51
|
const oneLine = manifestDesc.split(/\.|\n/)[0].trim();
|
|
55
|
-
return oneLine.length > 55 ? oneLine.slice(0, 54)
|
|
52
|
+
return oneLine.length > 55 ? `${oneLine.slice(0, 54)}…` : oneLine;
|
|
56
53
|
}
|
|
57
54
|
function printResultRow(result, manifest) {
|
|
58
55
|
const isSkill = result.kind === "skill";
|
|
@@ -61,7 +58,7 @@ function printResultRow(result, manifest) {
|
|
|
61
58
|
? entry
|
|
62
59
|
? shortDesc(result.name, entry.description)
|
|
63
60
|
: ""
|
|
64
|
-
: entry?.description ?? "MCP server";
|
|
61
|
+
: (entry?.description ?? "MCP server");
|
|
65
62
|
let icon;
|
|
66
63
|
switch (result.status) {
|
|
67
64
|
case "installed":
|
|
@@ -82,7 +79,7 @@ function printResultRow(result, manifest) {
|
|
|
82
79
|
}
|
|
83
80
|
const kindBadge = isSkill ? "" : dim("[mcp] ");
|
|
84
81
|
const padName = result.name.padEnd(22);
|
|
85
|
-
log(` ${icon} ${kindBadge}${bold(padName)} ${gray(
|
|
82
|
+
log(` ${icon} ${kindBadge}${bold(padName)} ${gray(`· ${desc}`)}`);
|
|
86
83
|
}
|
|
87
84
|
function printSummary(results, manifest) {
|
|
88
85
|
const installed = results.filter((r) => r.status === "installed").length;
|
|
@@ -114,22 +111,24 @@ function printSummary(results, manifest) {
|
|
|
114
111
|
parts.push(yellow(`${skipped} omitidas`));
|
|
115
112
|
if (failed > 0)
|
|
116
113
|
parts.push(red(`${failed} fallidas`));
|
|
117
|
-
log(
|
|
118
|
-
|
|
119
|
-
" " +
|
|
120
|
-
bold(parts.join(gray(" · "))) +
|
|
121
|
-
gray(` · ${totalFiles} archivos · ${formatBytes(totalBytes)}`));
|
|
122
|
-
log(" " + green("✓") + " " + gray("destino:") + " " + getClaudeSkillsDir());
|
|
114
|
+
log(` ${green("✓")} ${bold(parts.join(gray(" · ")))}${gray(` · ${totalFiles} archivos · ${formatBytes(totalBytes)}`)}`);
|
|
115
|
+
log(` ${green("✓")} ${gray("destino:")} ${getClaudeSkillsDir()}`);
|
|
123
116
|
log("");
|
|
124
117
|
for (const r of results.filter((x) => x.status === "failed")) {
|
|
125
118
|
logError(`${r.name}: ${r.error}`);
|
|
126
119
|
}
|
|
127
120
|
}
|
|
128
121
|
function printNextSteps(noHook) {
|
|
129
|
-
section("
|
|
130
|
-
log(
|
|
122
|
+
section("Ya estás listo");
|
|
123
|
+
log(` ${bold(yellow("1."))} ${bold("Reiniciá tu cliente")} ${gray("(Claude Code, Cursor, OpenCode o Antigravity)")}.`);
|
|
124
|
+
log(` ${bold(yellow("2."))} ${bold("Hacele tu pregunta en español.")}`);
|
|
125
|
+
log(` ${gray("Ejemplo:")} ${cyan('"cuántos nuevos clientes en mayo en Colombia?"')}`);
|
|
126
|
+
log("");
|
|
127
|
+
log(` ${gray("La primera vez que use un servicio (Metabase, Amplitude, HubSpot, Wiki),")}`);
|
|
128
|
+
log(` ${gray("te va a pedir 1 click para darle permiso con tu cuenta")} ${bold("@alegra.com")}${gray(".")}`);
|
|
129
|
+
log(` ${gray("Después no te pide nada más.")}`);
|
|
131
130
|
log("");
|
|
132
|
-
log(
|
|
131
|
+
log(` ${dim("Comandos útiles:")}`);
|
|
133
132
|
log(` ${dim("$")} ${bold(`${COMMAND_NAME} list`)} ${gray("ver skills disponibles")}`);
|
|
134
133
|
log(` ${dim("$")} ${bold(`${COMMAND_NAME} status`)} ${gray("estado actual + drift")}`);
|
|
135
134
|
log(` ${dim("$")} ${bold(`${COMMAND_NAME} update`)} ${gray("actualizar a la última")}`);
|
|
@@ -138,9 +137,7 @@ function printNextSteps(noHook) {
|
|
|
138
137
|
log(` ${dim("$")} ${bold(`${COMMAND_NAME} hook uninstall`)} ${gray("apagar aviso")}`);
|
|
139
138
|
}
|
|
140
139
|
log("");
|
|
141
|
-
log(
|
|
142
|
-
dim("Docs · ") +
|
|
143
|
-
cyan("github.com/alegradev/alegra-data-agent"));
|
|
140
|
+
log(` ${dim("Docs · ")}${cyan("github.com/alegradev/alegra-data-agent")}`);
|
|
144
141
|
log("");
|
|
145
142
|
}
|
|
146
143
|
function isClaudeCode() {
|
|
@@ -160,7 +157,7 @@ export async function runInstall(opts = {}) {
|
|
|
160
157
|
const auth = readAuth();
|
|
161
158
|
if (auth && !compact) {
|
|
162
159
|
section("Cuenta autorizada");
|
|
163
|
-
log(
|
|
160
|
+
log(` ${green("✓")} ${bold(auth.email)}`);
|
|
164
161
|
log("");
|
|
165
162
|
}
|
|
166
163
|
if (opts.dryRun) {
|
|
@@ -187,13 +184,15 @@ export async function runInstall(opts = {}) {
|
|
|
187
184
|
const msg = err instanceof Error ? err.message : String(err);
|
|
188
185
|
logError(msg);
|
|
189
186
|
if (msg.includes("EACCES") || msg.includes("permission denied")) {
|
|
190
|
-
log(dim(
|
|
187
|
+
log(dim(" Sin permisos para escribir. Verifica permisos de ~/.claude/skills/"));
|
|
191
188
|
}
|
|
192
189
|
else if (msg.includes("ENOSPC") || msg.includes("no space")) {
|
|
193
|
-
log(dim(
|
|
190
|
+
log(dim(" No hay espacio en disco. Libera espacio y reintenta."));
|
|
194
191
|
}
|
|
195
|
-
else if (msg.includes("network") ||
|
|
196
|
-
|
|
192
|
+
else if (msg.includes("network") ||
|
|
193
|
+
msg.includes("ENOTFOUND") ||
|
|
194
|
+
msg.includes("ECONNREFUSED")) {
|
|
195
|
+
log(dim(" Verifica tu conexión a internet y el estado del gateway."));
|
|
197
196
|
}
|
|
198
197
|
return 1;
|
|
199
198
|
}
|
|
@@ -217,7 +216,7 @@ export async function runInstall(opts = {}) {
|
|
|
217
216
|
return 0;
|
|
218
217
|
if (!opts.noHook) {
|
|
219
218
|
if (!compact)
|
|
220
|
-
log(
|
|
219
|
+
log(` ${gray("Instalando hook de notificaciones…")}`);
|
|
221
220
|
runHookInstall(opts.hookMode ?? "check");
|
|
222
221
|
if (!compact)
|
|
223
222
|
log("");
|
|
@@ -245,27 +244,23 @@ function printCompactSummary(results, manifest, email) {
|
|
|
245
244
|
}
|
|
246
245
|
}
|
|
247
246
|
const verbo = updated > 0 && installed === 0 ? "actualizadas" : "instaladas";
|
|
248
|
-
const skillList = skillOks
|
|
247
|
+
const skillList = skillOks
|
|
248
|
+
.map((r) => r.name)
|
|
249
|
+
.sort()
|
|
250
|
+
.join(", ");
|
|
249
251
|
const userPart = email ? gray(` · ${email}`) : "";
|
|
250
252
|
log("");
|
|
251
|
-
log(
|
|
252
|
-
green("✓") +
|
|
253
|
-
" " +
|
|
254
|
-
bold(`${skillOks.length} skills + ${mcpOks.length} MCPs ${verbo}`) +
|
|
255
|
-
gray(` · ${totalFiles} archivos · ${formatBytes(totalBytes)}`) +
|
|
256
|
-
userPart);
|
|
253
|
+
log(` ${green("✓")} ${bold(`${skillOks.length} skills + ${mcpOks.length} MCPs ${verbo}`)}${gray(` · ${totalFiles} archivos · ${formatBytes(totalBytes)}`)}${userPart}`);
|
|
257
254
|
if (skillList)
|
|
258
|
-
log(
|
|
255
|
+
log(` ${gray("skills: ")}${gray(skillList)}`);
|
|
259
256
|
if (mcpOks.length > 0)
|
|
260
|
-
log(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
gray(" · ") +
|
|
266
|
-
bold(yellow("reinicia tu cliente para activarlas")));
|
|
257
|
+
log(` ${gray("mcps: ")}${gray(mcpOks
|
|
258
|
+
.map((r) => r.name)
|
|
259
|
+
.sort()
|
|
260
|
+
.join(", "))}`);
|
|
261
|
+
log(` ${gray("destino:")} ${cyan(getClaudeSkillsDir())}${gray(" · ")}${bold(yellow("reinicia tu cliente para activarlas"))}`);
|
|
267
262
|
if (unchanged > 0 && installed === 0 && updated === 0) {
|
|
268
|
-
log(
|
|
263
|
+
log(` ${dim(`(${unchanged} sin cambios desde la última instalación)`)}`);
|
|
269
264
|
}
|
|
270
265
|
log("");
|
|
271
266
|
for (const r of failed) {
|
package/dist/commands/list.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AuthRequiredError, fetchManifest } from "../api.js";
|
|
2
|
-
import { readLockFile, formatBytes } from "../lib.js";
|
|
3
2
|
import { bold, cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
|
|
3
|
+
import { formatBytes, readLockFile } from "../lib.js";
|
|
4
4
|
export async function runList() {
|
|
5
5
|
let manifest;
|
|
6
6
|
try {
|
|
@@ -21,7 +21,7 @@ export async function runList() {
|
|
|
21
21
|
const lock = readLockFile();
|
|
22
22
|
log("");
|
|
23
23
|
log(bold(cyan(" Skills disponibles")) + dim(` — ${Object.keys(manifest.skills).length} totales`));
|
|
24
|
-
log(gray(
|
|
24
|
+
log(gray(` ${"─".repeat(70)}`));
|
|
25
25
|
const names = Object.keys(manifest.skills).sort();
|
|
26
26
|
for (const name of names) {
|
|
27
27
|
const entry = manifest.skills[name];
|
|
@@ -36,7 +36,7 @@ export async function runList() {
|
|
|
36
36
|
badge = yellow("[actualizar] ");
|
|
37
37
|
const team = entry.team ? gray(` ${entry.team}`) : "";
|
|
38
38
|
log(` ${badge} ${bold(name)}${team} ${dim(formatBytes(entry.bytes))}`);
|
|
39
|
-
const desc = entry.description.length > 200 ? entry.description.slice(0, 200)
|
|
39
|
+
const desc = entry.description.length > 200 ? `${entry.description.slice(0, 200)}…` : entry.description;
|
|
40
40
|
log(gray(` ${desc}`));
|
|
41
41
|
log("");
|
|
42
42
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { performBrowserLogin, readAuth, writeAuth } from "../auth.js";
|
|
2
|
-
import { banner } from "../ui.js";
|
|
3
2
|
import { bold, dim, green, log, logError } from "../colors.js";
|
|
4
3
|
import { GATEWAY_URL, getAuthFilePath } from "../lib.js";
|
|
4
|
+
import { banner } from "../ui.js";
|
|
5
5
|
export async function runLogin(opts = {}) {
|
|
6
6
|
banner();
|
|
7
7
|
if (!opts.force) {
|
|
@@ -9,7 +9,7 @@ export async function runLogin(opts = {}) {
|
|
|
9
9
|
if (existing) {
|
|
10
10
|
log(` ${green("✓")} Ya estás autenticado como ${bold(existing.email)}.`);
|
|
11
11
|
log(dim(` Token: ${getAuthFilePath()}`));
|
|
12
|
-
log(dim(
|
|
12
|
+
log(dim(" Para volver a iniciar sesión: --force"));
|
|
13
13
|
log("");
|
|
14
14
|
return 0;
|
|
15
15
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { AuthRequiredError, fetchManifest } from "../api.js";
|
|
2
2
|
import { readAuth } from "../auth.js";
|
|
3
|
+
import { bold, cyan, dim, gray, green, log, logError, logWarn, yellow } from "../colors.js";
|
|
3
4
|
import { detectDrift } from "../installer.js";
|
|
4
5
|
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
6
|
export async function runStatus() {
|
|
7
7
|
const auth = readAuth();
|
|
8
8
|
log("");
|
|
9
9
|
log(bold(cyan(" Estado")));
|
|
10
|
-
log(gray(
|
|
10
|
+
log(gray(` ${"─".repeat(70)}`));
|
|
11
11
|
log(` ${gray("Paquete:")} ${bold(COMMAND_NAME)} v${getPackageVersion()}`);
|
|
12
12
|
log(` ${gray("Gateway:")} ${GATEWAY_URL}`);
|
|
13
13
|
log(` ${gray("Autenticado:")} ${auth ? green(auth.email) : gray("no")}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { dim, gray, log, logOk, logWarn, red } from "../colors.js";
|
|
1
2
|
import { uninstallAll } from "../installer.js";
|
|
2
3
|
import { readLockFile } from "../lib.js";
|
|
3
4
|
import { banner, confirm } from "../ui.js";
|
|
4
|
-
import { dim, gray, log, logOk, logWarn, red } from "../colors.js";
|
|
5
5
|
export async function runUninstall(opts = {}) {
|
|
6
6
|
banner();
|
|
7
7
|
const lock = readLockFile();
|
package/dist/commands/update.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AuthRequiredError, fetchManifest } from "../api.js";
|
|
2
|
+
import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
|
|
2
3
|
import { detectDrift, installAll, summarize } from "../installer.js";
|
|
3
4
|
import { banner } from "../ui.js";
|
|
4
|
-
import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
|
|
5
5
|
export async function runUpdate(opts = {}) {
|
|
6
6
|
banner();
|
|
7
7
|
let manifest;
|
package/dist/installer.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { copyFileSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync
|
|
2
|
+
import { copyFileSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve, sep } from "node:path";
|
|
4
4
|
import { fetchManifest, fetchSkillFile } from "./api.js";
|
|
5
5
|
import { dim, gray, green, logError, red, yellow } from "./colors.js";
|
|
6
6
|
import { ensureDir, getLockFilePath, getPackageVersion, readLockFile, sha256File, writeLockFile, } from "./lib.js";
|
|
7
7
|
import { renderMcpForTarget } from "./render.js";
|
|
8
|
-
import {
|
|
8
|
+
import { TARGETS, detectInstalledTargets } from "./targets.js";
|
|
9
9
|
const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
10
10
|
const MCP_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
11
11
|
function validateSkillName(name) {
|
|
@@ -161,7 +161,7 @@ function registerMcpInTarget(mcp, spec) {
|
|
|
161
161
|
section[mcp.name] = renderMcpForTarget(mcp, spec);
|
|
162
162
|
cfg[spec.mcpRootKey] = section;
|
|
163
163
|
backupIfExists(spec.mcpConfigPath);
|
|
164
|
-
atomicWriteFile(spec.mcpConfigPath, JSON.stringify(cfg, null, 2)
|
|
164
|
+
atomicWriteFile(spec.mcpConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
165
165
|
}
|
|
166
166
|
function unregisterMcpInTarget(mcpName, spec) {
|
|
167
167
|
if (!existsSync(spec.mcpConfigPath))
|
|
@@ -173,13 +173,11 @@ function unregisterMcpInTarget(mcpName, spec) {
|
|
|
173
173
|
delete section[mcpName];
|
|
174
174
|
cfg[spec.mcpRootKey] = section;
|
|
175
175
|
backupIfExists(spec.mcpConfigPath);
|
|
176
|
-
atomicWriteFile(spec.mcpConfigPath, JSON.stringify(cfg, null, 2)
|
|
176
|
+
atomicWriteFile(spec.mcpConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
177
177
|
return true;
|
|
178
178
|
}
|
|
179
179
|
function resolveTargets(opts) {
|
|
180
|
-
const ids = opts.targets?.length
|
|
181
|
-
? opts.targets
|
|
182
|
-
: detectInstalledTargets();
|
|
180
|
+
const ids = opts.targets?.length ? opts.targets : detectInstalledTargets();
|
|
183
181
|
if (ids.length === 0) {
|
|
184
182
|
ids.push("claude-code");
|
|
185
183
|
}
|
|
@@ -330,7 +328,7 @@ export async function installAll(opts = {}) {
|
|
|
330
328
|
? opts.only.filter((s) => manifest.skills[s])
|
|
331
329
|
: Object.keys(manifest.skills);
|
|
332
330
|
const mcpNames = opts.only && opts.only.length > 0
|
|
333
|
-
? opts.only.filter((s) => manifest.mcps
|
|
331
|
+
? opts.only.filter((s) => manifest.mcps?.[s])
|
|
334
332
|
: Object.keys(manifest.mcps ?? {});
|
|
335
333
|
const results = [];
|
|
336
334
|
for (const name of skillNames) {
|
package/dist/lib.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, relative, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -7,8 +7,7 @@ export const PRODUCT_NAME = "alegra-data-agent";
|
|
|
7
7
|
export const COMMAND_NAME = "alegra-data-agent";
|
|
8
8
|
export const REPO_SLUG = "alegradev/alegra-data-agent";
|
|
9
9
|
export const REPO_DEFAULT_BRANCH = "main";
|
|
10
|
-
export const GATEWAY_URL = process.env.ALEGRA_DATA_AGENT_GATEWAY_URL ??
|
|
11
|
-
"https://alegra-data-agent.vercel.app";
|
|
10
|
+
export const GATEWAY_URL = process.env.ALEGRA_DATA_AGENT_GATEWAY_URL ?? "https://alegra-data-agent.vercel.app";
|
|
12
11
|
export const ALL_TARGETS = ["claude-code", "cursor", "opencode", "antigravity"];
|
|
13
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
13
|
export function getPackageRoot() {
|
|
@@ -122,7 +121,7 @@ export function readLockFile() {
|
|
|
122
121
|
export function writeLockFile(lock) {
|
|
123
122
|
const path = getLockFilePath();
|
|
124
123
|
mkdirSync(dirname(path), { recursive: true });
|
|
125
|
-
writeFileSync(path, JSON.stringify(lock, null, 2)
|
|
124
|
+
writeFileSync(path, `${JSON.stringify(lock, null, 2)}\n`, "utf-8");
|
|
126
125
|
}
|
|
127
126
|
export function sha256(buf) {
|
|
128
127
|
return createHash("sha256").update(buf).digest("hex");
|
package/dist/main.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { log, logError } from "./colors.js";
|
|
1
2
|
import { ALL_TARGETS, getPackageVersion } from "./lib.js";
|
|
2
3
|
import { helpText } from "./ui.js";
|
|
3
|
-
import { log, logError } from "./colors.js";
|
|
4
4
|
import { runCheck } from "./commands/check.js";
|
|
5
5
|
import { runHookInstall, runHookUninstall } from "./commands/hook.js";
|
|
6
6
|
import { runInstall } from "./commands/install.js";
|
|
@@ -13,7 +13,10 @@ import { runUpdate } from "./commands/update.js";
|
|
|
13
13
|
import { runWhoami } from "./commands/whoami.js";
|
|
14
14
|
function parseTargetList(raw) {
|
|
15
15
|
const out = [];
|
|
16
|
-
for (const piece of raw
|
|
16
|
+
for (const piece of raw
|
|
17
|
+
.split(",")
|
|
18
|
+
.map((s) => s.trim())
|
|
19
|
+
.filter(Boolean)) {
|
|
17
20
|
if (ALL_TARGETS.includes(piece)) {
|
|
18
21
|
out.push(piece);
|
|
19
22
|
}
|
package/dist/signature.js
CHANGED
|
@@ -5,11 +5,13 @@ export function canonicalize(obj) {
|
|
|
5
5
|
if (obj === null || typeof obj !== "object")
|
|
6
6
|
return JSON.stringify(obj);
|
|
7
7
|
if (Array.isArray(obj)) {
|
|
8
|
-
return
|
|
8
|
+
return `[${obj.map((v) => canonicalize(v)).join(",")}]`;
|
|
9
9
|
}
|
|
10
10
|
const o = obj;
|
|
11
|
-
const keys = Object.keys(o)
|
|
12
|
-
|
|
11
|
+
const keys = Object.keys(o)
|
|
12
|
+
.filter((k) => k !== "signature")
|
|
13
|
+
.sort();
|
|
14
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(o[k])}`).join(",")}}`;
|
|
13
15
|
}
|
|
14
16
|
const MAX_MANIFEST_AGE_DAYS = 90;
|
|
15
17
|
export function verifyManifestSignature(manifest, options = {}) {
|
|
@@ -24,11 +26,14 @@ export function verifyManifestSignature(manifest, options = {}) {
|
|
|
24
26
|
return { ok: false, reason: `algoritmo no soportado: ${sig.alg}` };
|
|
25
27
|
}
|
|
26
28
|
if (sig.keyId !== SIGNING_KEY_ID) {
|
|
27
|
-
return {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
reason: `keyId no reconocido: ${sig.keyId} (CLI espera ${SIGNING_KEY_ID})`,
|
|
32
|
+
};
|
|
28
33
|
}
|
|
29
34
|
if (typeof manifest.generatedAt === "string") {
|
|
30
35
|
const ts = Date.parse(manifest.generatedAt);
|
|
31
|
-
if (!isNaN(ts)) {
|
|
36
|
+
if (!Number.isNaN(ts)) {
|
|
32
37
|
const ageMs = Date.now() - ts;
|
|
33
38
|
const maxAgeMs = MAX_MANIFEST_AGE_DAYS * 24 * 3600 * 1000;
|
|
34
39
|
if (ageMs > maxAgeMs) {
|
package/dist/ui.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createInterface } from "node:readline/promises";
|
|
2
1
|
import { stdin as input, stdout as output } from "node:process";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
3
|
import { bold, cyan, dim, gray, log } from "./colors.js";
|
|
4
4
|
import { COMMAND_NAME, getPackageVersion } from "./lib.js";
|
|
5
5
|
export function banner() {
|
|
@@ -25,7 +25,7 @@ export async function confirm(question, defaultYes = true) {
|
|
|
25
25
|
export async function ask(question, defaultValue) {
|
|
26
26
|
const rl = createInterface({ input, output });
|
|
27
27
|
try {
|
|
28
|
-
const suffix = defaultValue ? dim(`(${defaultValue})`)
|
|
28
|
+
const suffix = defaultValue ? `${dim(`(${defaultValue})`)} ` : "";
|
|
29
29
|
const answer = (await rl.question(`${question} ${suffix}`)).trim();
|
|
30
30
|
return answer === "" && defaultValue !== undefined ? defaultValue : answer;
|
|
31
31
|
}
|
package/index.mjs
CHANGED
|
@@ -8,9 +8,7 @@ const [major, minor] = process.versions.node.split(".").map(Number);
|
|
|
8
8
|
|
|
9
9
|
if (major < 22 || (major === 22 && minor < 6)) {
|
|
10
10
|
console.error(
|
|
11
|
-
`\n ⚠ alegra-data-agent requiere Node.js >= 22.6.0
|
|
12
|
-
`\n Versión actual: ${process.version}` +
|
|
13
|
-
`\n Actualiza desde → https://nodejs.org\n`,
|
|
11
|
+
`\n ⚠ alegra-data-agent requiere Node.js >= 22.6.0.\n Versión actual: ${process.version}\n Actualiza desde → https://nodejs.org\n`,
|
|
14
12
|
);
|
|
15
13
|
process.exit(1);
|
|
16
14
|
}
|
|
@@ -27,7 +25,7 @@ if (existsSync(distPath)) {
|
|
|
27
25
|
// Desarrollo: ejecuta el .ts directo (requiere --experimental-strip-types).
|
|
28
26
|
const tsPath = join(__dirname, "src", "main.ts");
|
|
29
27
|
if (!existsSync(tsPath)) {
|
|
30
|
-
console.error(
|
|
28
|
+
console.error("\n ⚠ Instalación incompleta: no se encontró dist/ ni src/.\n");
|
|
31
29
|
process.exit(1);
|
|
32
30
|
}
|
|
33
31
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alegra-data-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
"manifest:generate": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/generate-manifest.mjs",
|
|
38
38
|
"manifest:sign": "node scripts/sign-manifest.mjs",
|
|
39
39
|
"manifest:scan": "node scripts/scan-skills.mjs",
|
|
40
|
-
"manifest": "
|
|
40
|
+
"manifest:validate": "node scripts/validate-skills.mjs --legacy-ok",
|
|
41
|
+
"manifest:validate:strict": "node scripts/validate-skills.mjs",
|
|
42
|
+
"manifest": "pnpm run manifest:validate && pnpm run manifest:scan && pnpm run manifest:generate && pnpm run manifest:sign",
|
|
41
43
|
"test": "node tests/runner.mjs",
|
|
42
44
|
"test:report": "node tests/runner.mjs --report markdown",
|
|
43
45
|
"test:security": "node tests/security/runner.mjs",
|