alegra-data-agent 0.1.0 → 0.2.1
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 +43 -22
- package/dist/colors.js +7 -9
- package/dist/commands/check.js +1 -1
- package/dist/commands/hook.js +6 -7
- package/dist/commands/install.js +80 -70
- package/dist/commands/list.js +3 -3
- package/dist/commands/login.js +8 -5
- package/dist/commands/status.js +2 -2
- package/dist/commands/uninstall.js +24 -2
- package/dist/commands/update.js +30 -10
- package/dist/installer.js +119 -17
- package/dist/lib.js +15 -4
- package/dist/main.js +5 -2
- package/dist/render.js +36 -1
- package/dist/signature.js +10 -5
- package/dist/targets.js +15 -1
- 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
|
}
|
|
@@ -54,6 +54,31 @@ export function clearAuth() {
|
|
|
54
54
|
export function hasAuth() {
|
|
55
55
|
return readAuth() !== null;
|
|
56
56
|
}
|
|
57
|
+
function decodeJwtExp(jwt) {
|
|
58
|
+
const parts = jwt.split(".");
|
|
59
|
+
if (parts.length !== 3)
|
|
60
|
+
return null;
|
|
61
|
+
try {
|
|
62
|
+
const padded = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
63
|
+
const payload = JSON.parse(Buffer.from(padded, "base64").toString("utf-8"));
|
|
64
|
+
return typeof payload.exp === "number" ? payload.exp : null;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function isAuthExpired(record) {
|
|
71
|
+
if (!record)
|
|
72
|
+
return true;
|
|
73
|
+
const exp = decodeJwtExp(record.token);
|
|
74
|
+
if (exp === null)
|
|
75
|
+
return false;
|
|
76
|
+
return Date.now() >= exp * 1000 - 60_000;
|
|
77
|
+
}
|
|
78
|
+
export function hasValidAuth() {
|
|
79
|
+
const record = readAuth();
|
|
80
|
+
return record !== null && !isAuthExpired(record);
|
|
81
|
+
}
|
|
57
82
|
export async function performBrowserLogin(timeoutMs = 5 * 60 * 1000) {
|
|
58
83
|
const state = randomBytes(24).toString("hex");
|
|
59
84
|
return new Promise((resolve, reject) => {
|
|
@@ -139,7 +164,7 @@ export async function performBrowserLogin(timeoutMs = 5 * 60 * 1000) {
|
|
|
139
164
|
}
|
|
140
165
|
});
|
|
141
166
|
server.listen(0, "127.0.0.1", () => {
|
|
142
|
-
const address = server
|
|
167
|
+
const address = server?.address();
|
|
143
168
|
if (typeof address !== "object" || !address) {
|
|
144
169
|
cleanup();
|
|
145
170
|
reject(new Error("No se pudo determinar el puerto."));
|
|
@@ -150,26 +175,21 @@ export async function performBrowserLogin(timeoutMs = 5 * 60 * 1000) {
|
|
|
150
175
|
authUrl.searchParams.set("port", String(port));
|
|
151
176
|
authUrl.searchParams.set("state", state);
|
|
152
177
|
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");
|
|
178
|
+
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`);
|
|
179
|
+
try {
|
|
180
|
+
process.stdout._flush?.();
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
}
|
|
168
184
|
openBrowser(u);
|
|
185
|
+
const nudgeTimer = setTimeout(() => {
|
|
186
|
+
if (resolved)
|
|
187
|
+
return;
|
|
188
|
+
process.stdout.write(`\n ¿No se abrió el navegador? Copiá esta URL y pegala manualmente:\n\n${u}\n\n`);
|
|
189
|
+
}, 8000);
|
|
169
190
|
timer = setTimeout(() => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
"desde la misma máquina donde abrirás el browser. Reintenta."));
|
|
191
|
+
clearTimeout(nudgeTimer);
|
|
192
|
+
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
193
|
}, timeoutMs);
|
|
174
194
|
});
|
|
175
195
|
});
|
|
@@ -204,6 +224,7 @@ function openBrowser(url) {
|
|
|
204
224
|
args: ["url.dll,FileProtocolHandler", url],
|
|
205
225
|
});
|
|
206
226
|
candidates.push({ cmd: "cmd.exe", args: ["/c", "start", '""', `"${url}"`] });
|
|
227
|
+
candidates.push({ cmd: "wslview", args: [url] });
|
|
207
228
|
candidates.push({ cmd: "xdg-open", args: [url] });
|
|
208
229
|
}
|
|
209
230
|
else {
|
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(".");
|
|
@@ -8,7 +8,7 @@ function hookCommandFor(mode) {
|
|
|
8
8
|
const minor = parts[1] ?? "0";
|
|
9
9
|
const range = `~${major}.${minor}.0`;
|
|
10
10
|
const verb = mode === "auto" ? "update --yes" : "check";
|
|
11
|
-
return `pnpm dlx ${COMMAND_NAME}@${range} ${verb}`;
|
|
11
|
+
return `pnpm dlx --config.minimumReleaseAge=0 ${COMMAND_NAME}@${range} ${verb}`;
|
|
12
12
|
}
|
|
13
13
|
const HOOK_SIGNATURE = COMMAND_NAME;
|
|
14
14
|
function readSettings() {
|
|
@@ -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
|
-
import {
|
|
3
|
-
import { installAll } from "../installer.js";
|
|
2
|
+
import { hasValidAuth, readAuth } from "../auth.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() {
|
|
@@ -150,9 +147,9 @@ export async function runInstall(opts = {}) {
|
|
|
150
147
|
const compact = isClaudeCode();
|
|
151
148
|
if (!compact)
|
|
152
149
|
fancyBanner();
|
|
153
|
-
if (!
|
|
150
|
+
if (!hasValidAuth()) {
|
|
154
151
|
if (!compact)
|
|
155
|
-
log(dim(" No hay sesión activa. Inicio el
|
|
152
|
+
log(dim(" No hay sesión activa (o expiró). Inicio el login antes de instalar.\n"));
|
|
156
153
|
const code = await runLogin({});
|
|
157
154
|
if (code !== 0)
|
|
158
155
|
return code;
|
|
@@ -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) {
|
|
@@ -169,33 +166,50 @@ export async function runInstall(opts = {}) {
|
|
|
169
166
|
if (!compact) {
|
|
170
167
|
section("Descargando skills del gateway");
|
|
171
168
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
}
|
|
169
|
+
const installOpts = {
|
|
170
|
+
dryRun: opts.dryRun,
|
|
171
|
+
force: opts.force,
|
|
172
|
+
only: opts.only,
|
|
173
|
+
targets: opts.targets,
|
|
174
|
+
};
|
|
175
|
+
const reportInstallError = (err) => {
|
|
187
176
|
const msg = err instanceof Error ? err.message : String(err);
|
|
188
177
|
logError(msg);
|
|
189
178
|
if (msg.includes("EACCES") || msg.includes("permission denied")) {
|
|
190
|
-
log(dim(
|
|
179
|
+
log(dim(" Sin permisos para escribir. Verifica permisos de ~/.claude/skills/"));
|
|
191
180
|
}
|
|
192
181
|
else if (msg.includes("ENOSPC") || msg.includes("no space")) {
|
|
193
|
-
log(dim(
|
|
182
|
+
log(dim(" No hay espacio en disco. Libera espacio y reintenta."));
|
|
194
183
|
}
|
|
195
|
-
else if (msg.includes("network") ||
|
|
196
|
-
|
|
184
|
+
else if (msg.includes("network") ||
|
|
185
|
+
msg.includes("ENOTFOUND") ||
|
|
186
|
+
msg.includes("ECONNREFUSED")) {
|
|
187
|
+
log(dim(" Verifica tu conexión a internet y el estado del gateway."));
|
|
197
188
|
}
|
|
198
189
|
return 1;
|
|
190
|
+
};
|
|
191
|
+
let result;
|
|
192
|
+
try {
|
|
193
|
+
result = await installAll(installOpts);
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
if (!(err instanceof AuthRequiredError))
|
|
197
|
+
return reportInstallError(err);
|
|
198
|
+
if (!compact)
|
|
199
|
+
log(dim(" La sesión expiró durante la sincronización; reabro el login y reintento.\n"));
|
|
200
|
+
const code = await runLogin({ force: true });
|
|
201
|
+
if (code !== 0)
|
|
202
|
+
return code;
|
|
203
|
+
try {
|
|
204
|
+
result = await installAll(installOpts);
|
|
205
|
+
}
|
|
206
|
+
catch (err2) {
|
|
207
|
+
if (err2 instanceof AuthRequiredError) {
|
|
208
|
+
logError(err2.message);
|
|
209
|
+
return 2;
|
|
210
|
+
}
|
|
211
|
+
return reportInstallError(err2);
|
|
212
|
+
}
|
|
199
213
|
}
|
|
200
214
|
if ("error" in result) {
|
|
201
215
|
logError(result.error);
|
|
@@ -217,7 +231,7 @@ export async function runInstall(opts = {}) {
|
|
|
217
231
|
return 0;
|
|
218
232
|
if (!opts.noHook) {
|
|
219
233
|
if (!compact)
|
|
220
|
-
log(
|
|
234
|
+
log(` ${gray("Instalando hook de notificaciones…")}`);
|
|
221
235
|
runHookInstall(opts.hookMode ?? "check");
|
|
222
236
|
if (!compact)
|
|
223
237
|
log("");
|
|
@@ -245,27 +259,23 @@ function printCompactSummary(results, manifest, email) {
|
|
|
245
259
|
}
|
|
246
260
|
}
|
|
247
261
|
const verbo = updated > 0 && installed === 0 ? "actualizadas" : "instaladas";
|
|
248
|
-
const skillList = skillOks
|
|
262
|
+
const skillList = skillOks
|
|
263
|
+
.map((r) => r.name)
|
|
264
|
+
.sort()
|
|
265
|
+
.join(", ");
|
|
249
266
|
const userPart = email ? gray(` · ${email}`) : "";
|
|
250
267
|
log("");
|
|
251
|
-
log(
|
|
252
|
-
green("✓") +
|
|
253
|
-
" " +
|
|
254
|
-
bold(`${skillOks.length} skills + ${mcpOks.length} MCPs ${verbo}`) +
|
|
255
|
-
gray(` · ${totalFiles} archivos · ${formatBytes(totalBytes)}`) +
|
|
256
|
-
userPart);
|
|
268
|
+
log(` ${green("✓")} ${bold(`${skillOks.length} skills + ${mcpOks.length} MCPs ${verbo}`)}${gray(` · ${totalFiles} archivos · ${formatBytes(totalBytes)}`)}${userPart}`);
|
|
257
269
|
if (skillList)
|
|
258
|
-
log(
|
|
270
|
+
log(` ${gray("skills: ")}${gray(skillList)}`);
|
|
259
271
|
if (mcpOks.length > 0)
|
|
260
|
-
log(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
gray(" · ") +
|
|
266
|
-
bold(yellow("reinicia tu cliente para activarlas")));
|
|
272
|
+
log(` ${gray("mcps: ")}${gray(mcpOks
|
|
273
|
+
.map((r) => r.name)
|
|
274
|
+
.sort()
|
|
275
|
+
.join(", "))}`);
|
|
276
|
+
log(` ${gray("destino:")} ${cyan(getClaudeSkillsDir())}${gray(" · ")}${bold(yellow("reinicia tu cliente para activarlas"))}`);
|
|
267
277
|
if (unchanged > 0 && installed === 0 && updated === 0) {
|
|
268
|
-
log(
|
|
278
|
+
log(` ${dim(`(${unchanged} sin cambios desde la última instalación)`)}`);
|
|
269
279
|
}
|
|
270
280
|
log("");
|
|
271
281
|
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,18 +1,21 @@
|
|
|
1
|
-
import { performBrowserLogin, readAuth, writeAuth } from "../auth.js";
|
|
2
|
-
import {
|
|
3
|
-
import { bold, dim, green, log, logError } from "../colors.js";
|
|
1
|
+
import { isAuthExpired, performBrowserLogin, readAuth, writeAuth } from "../auth.js";
|
|
2
|
+
import { bold, dim, green, log, logError, logWarn } 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) {
|
|
8
8
|
const existing = readAuth();
|
|
9
|
-
if (existing) {
|
|
9
|
+
if (existing && !isAuthExpired(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
|
}
|
|
16
|
+
if (existing) {
|
|
17
|
+
logWarn(`Tu sesión (${existing.email}) expiró — reabro el login.`);
|
|
18
|
+
}
|
|
16
19
|
}
|
|
17
20
|
log(` Gateway: ${dim(GATEWAY_URL)}`);
|
|
18
21
|
try {
|
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,10 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { dim, gray, log, logOk, logWarn, red } from "../colors.js";
|
|
1
5
|
import { uninstallAll } from "../installer.js";
|
|
2
6
|
import { readLockFile } from "../lib.js";
|
|
3
7
|
import { banner, confirm } from "../ui.js";
|
|
4
|
-
import { dim, gray, log, logOk, logWarn, red } from "../colors.js";
|
|
5
8
|
export async function runUninstall(opts = {}) {
|
|
6
9
|
banner();
|
|
7
10
|
const lock = readLockFile();
|
|
@@ -22,7 +25,26 @@ export async function runUninstall(opts = {}) {
|
|
|
22
25
|
}
|
|
23
26
|
}
|
|
24
27
|
const results = uninstallAll({ dryRun: opts.dryRun });
|
|
28
|
+
if (!opts.dryRun)
|
|
29
|
+
removeNpmrcCooldownExclude();
|
|
25
30
|
log("");
|
|
26
|
-
|
|
31
|
+
if (opts.dryRun) {
|
|
32
|
+
logOk(`${results.length} skill(s) se eliminarían (dry-run, nada borrado).`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
logOk(`${results.length} skill(s) eliminadas.`);
|
|
36
|
+
}
|
|
27
37
|
return 0;
|
|
28
38
|
}
|
|
39
|
+
function removeNpmrcCooldownExclude() {
|
|
40
|
+
const npmrc = join(homedir(), ".npmrc");
|
|
41
|
+
if (!existsSync(npmrc))
|
|
42
|
+
return;
|
|
43
|
+
const EXCLUDE = "minimum-release-age-exclude[]=alegra-data-agent";
|
|
44
|
+
const lines = readFileSync(npmrc, "utf-8").split("\n");
|
|
45
|
+
const kept = lines.filter((l) => l.trim() !== EXCLUDE);
|
|
46
|
+
if (kept.length !== lines.length) {
|
|
47
|
+
writeFileSync(npmrc, kept.join("\n"));
|
|
48
|
+
logOk("Removido minimum-release-age-exclude de ~/.npmrc");
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/commands/update.js
CHANGED
|
@@ -1,21 +1,41 @@
|
|
|
1
1
|
import { AuthRequiredError, fetchManifest } from "../api.js";
|
|
2
|
+
import { hasValidAuth } from "../auth.js";
|
|
3
|
+
import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
|
|
2
4
|
import { detectDrift, installAll, summarize } from "../installer.js";
|
|
3
5
|
import { banner } from "../ui.js";
|
|
4
|
-
import {
|
|
6
|
+
import { runLogin } from "./login.js";
|
|
5
7
|
export async function runUpdate(opts = {}) {
|
|
6
8
|
banner();
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
if (!hasValidAuth()) {
|
|
10
|
+
const code = await runLogin({});
|
|
11
|
+
if (code !== 0)
|
|
12
|
+
return code;
|
|
10
13
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return
|
|
14
|
+
let reauthed = false;
|
|
15
|
+
for (;;) {
|
|
16
|
+
try {
|
|
17
|
+
return await runUpdateOnce(opts);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
if (err instanceof AuthRequiredError && !reauthed) {
|
|
21
|
+
reauthed = true;
|
|
22
|
+
log(dim(" La sesión expiró durante la actualización; reabro el login y reintento.\n"));
|
|
23
|
+
const code = await runLogin({ force: true });
|
|
24
|
+
if (code !== 0)
|
|
25
|
+
return code;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (err instanceof AuthRequiredError) {
|
|
29
|
+
logError(err.message);
|
|
30
|
+
return 2;
|
|
31
|
+
}
|
|
32
|
+
logError(err instanceof Error ? err.message : String(err));
|
|
33
|
+
return 1;
|
|
15
34
|
}
|
|
16
|
-
logError(err instanceof Error ? err.message : String(err));
|
|
17
|
-
return 1;
|
|
18
35
|
}
|
|
36
|
+
}
|
|
37
|
+
async function runUpdateOnce(opts) {
|
|
38
|
+
const manifest = await fetchManifest();
|
|
19
39
|
if (!manifest) {
|
|
20
40
|
logError("No se pudo cargar el manifest.");
|
|
21
41
|
return 1;
|
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
|
-
import { renderMcpForTarget } from "./render.js";
|
|
8
|
-
import {
|
|
7
|
+
import { inlineReferences, renderMcpForTarget } from "./render.js";
|
|
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) {
|
|
@@ -32,6 +32,14 @@ function validateRelPath(rel) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
function mcpConfigHash(mcp) {
|
|
36
|
+
const salient = JSON.stringify({
|
|
37
|
+
url: mcp.url,
|
|
38
|
+
type: mcp.auth.type,
|
|
39
|
+
tokenEnv: mcp.auth.tokenEnv ?? null,
|
|
40
|
+
});
|
|
41
|
+
return createHash("sha256").update(salient).digest("hex");
|
|
42
|
+
}
|
|
35
43
|
function safeJoinSkillFile(skillRoot, rel) {
|
|
36
44
|
const candidate = resolve(skillRoot, rel);
|
|
37
45
|
const root = resolve(skillRoot);
|
|
@@ -95,17 +103,21 @@ function writeSkillToTarget(skillName, staged, spec) {
|
|
|
95
103
|
if (!entry) {
|
|
96
104
|
throw new Error(`Para target ${spec.id} se requiere SKILL.md o README.md en la skill`);
|
|
97
105
|
}
|
|
106
|
+
const references = staged
|
|
107
|
+
.filter((f) => f.rel !== "SKILL.md" && f.rel !== "README.md")
|
|
108
|
+
.map((f) => ({ rel: f.rel, text: new TextDecoder().decode(f.data) }));
|
|
98
109
|
const content = new TextDecoder().decode(entry.data);
|
|
99
|
-
const rendered = renderSkillForTargetFromContent(content, skillName, spec.skillFormat);
|
|
110
|
+
const rendered = renderSkillForTargetFromContent(content, skillName, spec.skillFormat, references);
|
|
100
111
|
ensureDir(spec.skillsDir);
|
|
101
112
|
const dst = join(spec.skillsDir, rendered.filename);
|
|
102
113
|
atomicWriteFile(dst, rendered.content);
|
|
103
114
|
}
|
|
104
|
-
function renderSkillForTargetFromContent(content, skillName, format) {
|
|
115
|
+
function renderSkillForTargetFromContent(content, skillName, format, references = []) {
|
|
105
116
|
if (format === "folder") {
|
|
106
117
|
throw new Error("renderSkillForTargetFromContent no soporta 'folder'");
|
|
107
118
|
}
|
|
108
119
|
const stripped = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").replace(/^\s+/, "");
|
|
120
|
+
const body = stripped + inlineReferences(references);
|
|
109
121
|
const fm = parseFrontmatterMinimal(content);
|
|
110
122
|
if (format === "cursor-mdc") {
|
|
111
123
|
const desc = fm.description ?? `Skill ${skillName}`;
|
|
@@ -113,10 +125,10 @@ function renderSkillForTargetFromContent(content, skillName, format) {
|
|
|
113
125
|
const safeDesc = cursorDesc.replace(/\n/g, " ").replace(/"/g, '\\"');
|
|
114
126
|
return {
|
|
115
127
|
filename: `${skillName}.mdc`,
|
|
116
|
-
content: `---\ndescription: "${safeDesc}"\nalwaysApply: false\n---\n\n${
|
|
128
|
+
content: `---\ndescription: "${safeDesc}"\nalwaysApply: false\n---\n\n${body}`,
|
|
117
129
|
};
|
|
118
130
|
}
|
|
119
|
-
return { filename: `${skillName}.md`, content:
|
|
131
|
+
return { filename: `${skillName}.md`, content: body };
|
|
120
132
|
}
|
|
121
133
|
function parseFrontmatterMinimal(content) {
|
|
122
134
|
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -156,14 +168,21 @@ function readJsonOrEmpty(path) {
|
|
|
156
168
|
}
|
|
157
169
|
function registerMcpInTarget(mcp, spec) {
|
|
158
170
|
validateMcpName(mcp.name);
|
|
171
|
+
if (spec.mcpMechanism === "toml-block") {
|
|
172
|
+
registerMcpInTomlBlock(mcp, spec);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
159
175
|
const cfg = readJsonOrEmpty(spec.mcpConfigPath);
|
|
160
176
|
const section = cfg[spec.mcpRootKey] ?? {};
|
|
161
177
|
section[mcp.name] = renderMcpForTarget(mcp, spec);
|
|
162
178
|
cfg[spec.mcpRootKey] = section;
|
|
163
179
|
backupIfExists(spec.mcpConfigPath);
|
|
164
|
-
atomicWriteFile(spec.mcpConfigPath, JSON.stringify(cfg, null, 2)
|
|
180
|
+
atomicWriteFile(spec.mcpConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
165
181
|
}
|
|
166
182
|
function unregisterMcpInTarget(mcpName, spec) {
|
|
183
|
+
if (spec.mcpMechanism === "toml-block") {
|
|
184
|
+
return unregisterMcpInTomlBlock(mcpName, spec);
|
|
185
|
+
}
|
|
167
186
|
if (!existsSync(spec.mcpConfigPath))
|
|
168
187
|
return false;
|
|
169
188
|
const cfg = readJsonOrEmpty(spec.mcpConfigPath);
|
|
@@ -173,13 +192,82 @@ function unregisterMcpInTarget(mcpName, spec) {
|
|
|
173
192
|
delete section[mcpName];
|
|
174
193
|
cfg[spec.mcpRootKey] = section;
|
|
175
194
|
backupIfExists(spec.mcpConfigPath);
|
|
176
|
-
atomicWriteFile(spec.mcpConfigPath, JSON.stringify(cfg, null, 2)
|
|
195
|
+
atomicWriteFile(spec.mcpConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
function tomlBasicString(s) {
|
|
199
|
+
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
200
|
+
}
|
|
201
|
+
export function renderMcpToml(mcp) {
|
|
202
|
+
const lines = [`[mcp_servers.${mcp.name}]`, `url = ${tomlBasicString(mcp.url)}`];
|
|
203
|
+
if (mcp.auth.type === "gateway") {
|
|
204
|
+
if (!mcp.auth.tokenEnv) {
|
|
205
|
+
throw new Error(`MCP '${mcp.name}' es type=gateway pero no trae tokenEnv`);
|
|
206
|
+
}
|
|
207
|
+
lines.push(`bearer_token_env_var = ${tomlBasicString(mcp.auth.tokenEnv)}`);
|
|
208
|
+
}
|
|
209
|
+
return lines.join("\n");
|
|
210
|
+
}
|
|
211
|
+
function tomlMarkers(name) {
|
|
212
|
+
return {
|
|
213
|
+
begin: `# >>> alegra-data-agent:${name} >>>`,
|
|
214
|
+
end: `# <<< alegra-data-agent:${name} <<<`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
export function upsertTomlBlock(content, name, block) {
|
|
218
|
+
const { begin, end } = tomlMarkers(name);
|
|
219
|
+
const fenced = `${begin}\n${block}\n${end}`;
|
|
220
|
+
const bi = content.indexOf(begin);
|
|
221
|
+
if (bi !== -1) {
|
|
222
|
+
const ei = content.indexOf(end, bi);
|
|
223
|
+
if (ei !== -1) {
|
|
224
|
+
return content.slice(0, bi) + fenced + content.slice(ei + end.length);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (content.length === 0)
|
|
228
|
+
return `${fenced}\n`;
|
|
229
|
+
const sep = content.endsWith("\n\n") ? "" : content.endsWith("\n") ? "\n" : "\n\n";
|
|
230
|
+
return `${content}${sep}${fenced}\n`;
|
|
231
|
+
}
|
|
232
|
+
export function removeTomlBlock(content, name) {
|
|
233
|
+
const { begin, end } = tomlMarkers(name);
|
|
234
|
+
const bi = content.indexOf(begin);
|
|
235
|
+
if (bi === -1)
|
|
236
|
+
return content;
|
|
237
|
+
const ei = content.indexOf(end, bi);
|
|
238
|
+
if (ei === -1)
|
|
239
|
+
return content;
|
|
240
|
+
let start = bi;
|
|
241
|
+
let stop = ei + end.length;
|
|
242
|
+
if (content[stop] === "\n")
|
|
243
|
+
stop++;
|
|
244
|
+
if (start > 0 && content[start - 1] === "\n")
|
|
245
|
+
start--;
|
|
246
|
+
return content.slice(0, start) + content.slice(stop);
|
|
247
|
+
}
|
|
248
|
+
function registerMcpInTomlBlock(mcp, spec) {
|
|
249
|
+
const existing = existsSync(spec.mcpConfigPath)
|
|
250
|
+
? readFileSync(spec.mcpConfigPath, "utf-8")
|
|
251
|
+
: "";
|
|
252
|
+
const next = upsertTomlBlock(existing, mcp.name, renderMcpToml(mcp));
|
|
253
|
+
if (next === existing)
|
|
254
|
+
return;
|
|
255
|
+
backupIfExists(spec.mcpConfigPath);
|
|
256
|
+
atomicWriteFile(spec.mcpConfigPath, next);
|
|
257
|
+
}
|
|
258
|
+
function unregisterMcpInTomlBlock(name, spec) {
|
|
259
|
+
if (!existsSync(spec.mcpConfigPath))
|
|
260
|
+
return false;
|
|
261
|
+
const existing = readFileSync(spec.mcpConfigPath, "utf-8");
|
|
262
|
+
const next = removeTomlBlock(existing, name);
|
|
263
|
+
if (next === existing)
|
|
264
|
+
return false;
|
|
265
|
+
backupIfExists(spec.mcpConfigPath);
|
|
266
|
+
atomicWriteFile(spec.mcpConfigPath, next);
|
|
177
267
|
return true;
|
|
178
268
|
}
|
|
179
269
|
function resolveTargets(opts) {
|
|
180
|
-
const ids = opts.targets?.length
|
|
181
|
-
? opts.targets
|
|
182
|
-
: detectInstalledTargets();
|
|
270
|
+
const ids = opts.targets?.length ? opts.targets : detectInstalledTargets();
|
|
183
271
|
if (ids.length === 0) {
|
|
184
272
|
ids.push("claude-code");
|
|
185
273
|
}
|
|
@@ -276,7 +364,7 @@ export function installMcp(mcp, opts = {}) {
|
|
|
276
364
|
const targetsMatch = prev?.targets &&
|
|
277
365
|
prev.targets.length === targetIds.length &&
|
|
278
366
|
prev.targets.every((t) => targetIds.includes(t));
|
|
279
|
-
if (prev && targetsMatch && !opts.force) {
|
|
367
|
+
if (prev && targetsMatch && prev.configHash === mcpConfigHash(mcp) && !opts.force) {
|
|
280
368
|
return { name: mcp.name, kind: "mcp", status: "unchanged", targets: targetIds };
|
|
281
369
|
}
|
|
282
370
|
if (opts.dryRun) {
|
|
@@ -295,6 +383,7 @@ export function installMcp(mcp, opts = {}) {
|
|
|
295
383
|
name: mcp.name,
|
|
296
384
|
installedFrom: getPackageVersion(),
|
|
297
385
|
installedAt: new Date().toISOString(),
|
|
386
|
+
configHash: mcpConfigHash(mcp),
|
|
298
387
|
targets: targetIds,
|
|
299
388
|
};
|
|
300
389
|
writeLockFile(lock);
|
|
@@ -330,7 +419,7 @@ export async function installAll(opts = {}) {
|
|
|
330
419
|
? opts.only.filter((s) => manifest.skills[s])
|
|
331
420
|
: Object.keys(manifest.skills);
|
|
332
421
|
const mcpNames = opts.only && opts.only.length > 0
|
|
333
|
-
? opts.only.filter((s) => manifest.mcps
|
|
422
|
+
? opts.only.filter((s) => manifest.mcps?.[s])
|
|
334
423
|
: Object.keys(manifest.mcps ?? {});
|
|
335
424
|
const results = [];
|
|
336
425
|
for (const name of skillNames) {
|
|
@@ -401,8 +490,18 @@ export function uninstallMcp(name, opts = {}) {
|
|
|
401
490
|
}
|
|
402
491
|
const targetIds = (prev.targets ?? ["claude-code"]);
|
|
403
492
|
if (!opts.dryRun) {
|
|
404
|
-
|
|
405
|
-
|
|
493
|
+
try {
|
|
494
|
+
for (const tId of targetIds) {
|
|
495
|
+
unregisterMcpInTarget(name, TARGETS[tId]);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
return {
|
|
500
|
+
name,
|
|
501
|
+
kind: "mcp",
|
|
502
|
+
status: "failed",
|
|
503
|
+
error: err instanceof Error ? err.message : String(err),
|
|
504
|
+
};
|
|
406
505
|
}
|
|
407
506
|
delete lock.mcps[name];
|
|
408
507
|
writeLockFile(lock);
|
|
@@ -449,8 +548,11 @@ export function detectDrift(manifest) {
|
|
|
449
548
|
};
|
|
450
549
|
const manifestMcps = manifest.mcps ?? {};
|
|
451
550
|
for (const name of Object.keys(manifestMcps)) {
|
|
452
|
-
|
|
551
|
+
const prevMcp = lock.mcps?.[name];
|
|
552
|
+
if (!prevMcp)
|
|
453
553
|
mcps.toInstall.push(name);
|
|
554
|
+
else if (prevMcp.configHash !== mcpConfigHash(manifestMcps[name]))
|
|
555
|
+
mcps.toUpdate.push(name);
|
|
454
556
|
else
|
|
455
557
|
mcps.unchanged.push(name);
|
|
456
558
|
}
|
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() {
|
|
@@ -48,6 +47,14 @@ export function getAntigravitySkillsDir() {
|
|
|
48
47
|
export function getAntigravitySettingsPath() {
|
|
49
48
|
return join(homedir(), ".antigravity", "settings.json");
|
|
50
49
|
}
|
|
50
|
+
export function getCodexSkillsDir() {
|
|
51
|
+
return join(homedir(), ".agents", "skills");
|
|
52
|
+
}
|
|
53
|
+
export function getCodexConfigPath() {
|
|
54
|
+
const codexHome = process.env.CODEX_HOME;
|
|
55
|
+
const base = codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
|
|
56
|
+
return join(base, "config.toml");
|
|
57
|
+
}
|
|
51
58
|
export function getTargetSkillsDir(target) {
|
|
52
59
|
switch (target) {
|
|
53
60
|
case "claude-code":
|
|
@@ -58,6 +65,8 @@ export function getTargetSkillsDir(target) {
|
|
|
58
65
|
return getOpenCodeSkillsDir();
|
|
59
66
|
case "antigravity":
|
|
60
67
|
return getAntigravitySkillsDir();
|
|
68
|
+
case "codex":
|
|
69
|
+
return getCodexSkillsDir();
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
72
|
export function getTargetMcpConfigPath(target) {
|
|
@@ -70,6 +79,8 @@ export function getTargetMcpConfigPath(target) {
|
|
|
70
79
|
return getOpenCodeConfigPath();
|
|
71
80
|
case "antigravity":
|
|
72
81
|
return getAntigravitySettingsPath();
|
|
82
|
+
case "codex":
|
|
83
|
+
return getCodexConfigPath();
|
|
73
84
|
}
|
|
74
85
|
}
|
|
75
86
|
export function getLockFilePath() {
|
|
@@ -122,7 +133,7 @@ export function readLockFile() {
|
|
|
122
133
|
export function writeLockFile(lock) {
|
|
123
134
|
const path = getLockFilePath();
|
|
124
135
|
mkdirSync(dirname(path), { recursive: true });
|
|
125
|
-
writeFileSync(path, JSON.stringify(lock, null, 2)
|
|
136
|
+
writeFileSync(path, `${JSON.stringify(lock, null, 2)}\n`, "utf-8");
|
|
126
137
|
}
|
|
127
138
|
export function sha256(buf) {
|
|
128
139
|
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/render.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { extname, join } from "node:path";
|
|
3
3
|
import { parseFrontmatter } from "./lib.js";
|
|
4
4
|
function readSkillEntryPoint(skillDir) {
|
|
5
5
|
const candidates = ["SKILL.md", "README.md"];
|
|
@@ -37,6 +37,39 @@ function stripFrontmatter(content) {
|
|
|
37
37
|
return content;
|
|
38
38
|
return m[1].replace(/^\s+/, "");
|
|
39
39
|
}
|
|
40
|
+
const INLINE_TEXT_EXT = new Set([".md", ".sql", ".txt", ".json", ".yaml", ".yml", ".csv"]);
|
|
41
|
+
const INLINE_MAX_BYTES = 64 * 1024;
|
|
42
|
+
const FENCE_LANG = {
|
|
43
|
+
".sql": "sql",
|
|
44
|
+
".json": "json",
|
|
45
|
+
".yaml": "yaml",
|
|
46
|
+
".yml": "yaml",
|
|
47
|
+
".csv": "csv",
|
|
48
|
+
".txt": "",
|
|
49
|
+
};
|
|
50
|
+
export function inlineReferences(refs) {
|
|
51
|
+
const sections = [];
|
|
52
|
+
for (const ref of refs) {
|
|
53
|
+
const ext = extname(ref.rel).toLowerCase();
|
|
54
|
+
const bytes = Buffer.byteLength(ref.text, "utf-8");
|
|
55
|
+
if (!INLINE_TEXT_EXT.has(ext)) {
|
|
56
|
+
sections.push(`### \`${ref.rel}\`\n\n> (omitido: tipo \`${ext || "sin extensión"}\` no inlineable)`);
|
|
57
|
+
}
|
|
58
|
+
else if (bytes > INLINE_MAX_BYTES) {
|
|
59
|
+
sections.push(`### \`${ref.rel}\`\n\n> (omitido: ${Math.round(bytes / 1024)} KB; supera el límite de inline)`);
|
|
60
|
+
}
|
|
61
|
+
else if (ext === ".md") {
|
|
62
|
+
sections.push(`### \`${ref.rel}\`\n\n${ref.text.trim()}`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const lang = FENCE_LANG[ext] ?? "";
|
|
66
|
+
sections.push(`### \`${ref.rel}\`\n\n\`\`\`${lang}\n${ref.text.replace(/\s+$/, "")}\n\`\`\``);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (sections.length === 0)
|
|
70
|
+
return "";
|
|
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
|
+
}
|
|
40
73
|
export function renderMcpForTarget(mcp, spec) {
|
|
41
74
|
const obj = {
|
|
42
75
|
[spec.mcpUrlKey]: mcp.url,
|
|
@@ -64,5 +97,7 @@ function bearerInterpolation(env, target) {
|
|
|
64
97
|
return `Bearer \${env:${env}}`;
|
|
65
98
|
case "opencode":
|
|
66
99
|
return `Bearer {env:${env}}`;
|
|
100
|
+
case "codex":
|
|
101
|
+
throw new Error("codex usa config TOML (bearer_token_env_var), no headers JSON");
|
|
67
102
|
}
|
|
68
103
|
}
|
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/targets.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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, getCursorMcpPath, getCursorSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
|
|
4
|
+
import { getAntigravitySettingsPath, getAntigravitySkillsDir, getClaudeSettingsPath, getClaudeSkillsDir, getCodexConfigPath, getCodexSkillsDir, getCursorMcpPath, getCursorSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
|
|
5
5
|
export const TARGETS = {
|
|
6
6
|
"claude-code": {
|
|
7
7
|
id: "claude-code",
|
|
@@ -40,17 +40,31 @@ export const TARGETS = {
|
|
|
40
40
|
mcpRootKey: "mcpServers",
|
|
41
41
|
mcpUrlKey: "serverUrl",
|
|
42
42
|
},
|
|
43
|
+
codex: {
|
|
44
|
+
id: "codex",
|
|
45
|
+
label: "Codex",
|
|
46
|
+
skillsDir: getCodexSkillsDir(),
|
|
47
|
+
skillFormat: "folder",
|
|
48
|
+
mcpConfigPath: getCodexConfigPath(),
|
|
49
|
+
mcpRootKey: "mcpServers",
|
|
50
|
+
mcpUrlKey: "url",
|
|
51
|
+
mcpMechanism: "toml-block",
|
|
52
|
+
},
|
|
43
53
|
};
|
|
44
54
|
export function detectInstalledTargets() {
|
|
45
55
|
const home = homedir();
|
|
46
56
|
const xdgConfig = process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME.length > 0
|
|
47
57
|
? process.env.XDG_CONFIG_HOME
|
|
48
58
|
: join(home, ".config");
|
|
59
|
+
const codexHome = process.env.CODEX_HOME && process.env.CODEX_HOME.length > 0
|
|
60
|
+
? process.env.CODEX_HOME
|
|
61
|
+
: join(home, ".codex");
|
|
49
62
|
const sentinels = {
|
|
50
63
|
"claude-code": join(home, ".claude"),
|
|
51
64
|
cursor: join(home, ".cursor"),
|
|
52
65
|
opencode: join(xdgConfig, "opencode"),
|
|
53
66
|
antigravity: join(home, ".antigravity"),
|
|
67
|
+
codex: codexHome,
|
|
54
68
|
};
|
|
55
69
|
const out = [];
|
|
56
70
|
for (const id of Object.keys(sentinels)) {
|
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.1
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|