alegra-data-agent 0.4.4 → 0.5.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/README.md +2 -2
- package/dist/api.js +48 -7
- package/dist/bounce.js +11 -0
- package/dist/colors.js +0 -9
- package/dist/commands/hook.js +2 -1
- package/dist/commands/status.js +23 -2
- package/dist/commands/uninstall.js +0 -16
- package/dist/commands/update.js +67 -14
- package/dist/installer.js +110 -80
- package/dist/lazy-mcp.js +24 -3
- package/dist/lib.js +232 -6
- package/dist/main.js +5 -88
- package/dist/render.js +0 -77
- package/dist/signature.js +27 -19
- package/dist/ui.js +0 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,8 +14,8 @@ Solo accesible para cuentas `@alegra.com` (OAuth Google). El CLI te abre el nave
|
|
|
14
14
|
|
|
15
15
|
- Node ≥ 22.6.0
|
|
16
16
|
- pnpm ≥ 11.1.2
|
|
17
|
-
- Uno de: Claude Code, Cursor, OpenCode, Antigravity, Codex
|
|
17
|
+
- Uno de: Claude Code, Cursor, OpenCode, Antigravity, Codex, Hermes Agent
|
|
18
18
|
|
|
19
19
|
## Soporte
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Guía de uso, documentación y soporte para el equipo de Alegra: entra a https://alegra-data-agent.vercel.app con tu cuenta `@alegra.com`.
|
package/dist/api.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { readAuth } from "./auth.js";
|
|
4
|
-
import { COMMAND_NAME, GATEWAY_URL, getManifestCachePath, getPackageVersion } from "./lib.js";
|
|
5
|
-
import { verifyManifestSignature } from "./signature.js";
|
|
4
|
+
import { COMMAND_NAME, GATEWAY_URL, getManifestCachePath, getPackageVersion, readLockFile, writeLockFile, } from "./lib.js";
|
|
5
|
+
import { manifestTimestamp, verifyManifestSignature } from "./signature.js";
|
|
6
6
|
export class ManifestSignatureError extends Error {
|
|
7
7
|
constructor(reason) {
|
|
8
8
|
super(`Firma del manifest inválida: ${reason}. Posible compromiso del gateway.`);
|
|
@@ -204,8 +204,49 @@ function writeManifestCache(etag, manifest) {
|
|
|
204
204
|
catch {
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
export const LOCK_MANIFEST_FLOOR_KEY = "manifestGeneratedAt";
|
|
208
|
+
function lockFloor() {
|
|
209
|
+
try {
|
|
210
|
+
const raw = readLockFile()[LOCK_MANIFEST_FLOOR_KEY];
|
|
211
|
+
return manifestTimestamp({ generatedAt: raw });
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
export function recordManifestFloor(manifest) {
|
|
218
|
+
try {
|
|
219
|
+
const ts = manifestTimestamp(manifest);
|
|
220
|
+
if (ts === null)
|
|
221
|
+
return;
|
|
222
|
+
const current = lockFloor();
|
|
223
|
+
if (current !== null && current >= ts)
|
|
224
|
+
return;
|
|
225
|
+
const lock = readLockFile();
|
|
226
|
+
lock[LOCK_MANIFEST_FLOOR_KEY] = manifest.generatedAt;
|
|
227
|
+
writeLockFile(lock);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function trustedCacheFloor(cached) {
|
|
233
|
+
if (!cached)
|
|
234
|
+
return null;
|
|
235
|
+
try {
|
|
236
|
+
if (!verifyManifestSignature(cached.manifest, { softFail: false }).ok)
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return manifestTimestamp(cached.manifest);
|
|
243
|
+
}
|
|
244
|
+
function maxFloor(...floors) {
|
|
245
|
+
const valid = floors.filter((f) => typeof f === "number");
|
|
246
|
+
return valid.length > 0 ? Math.max(...valid) : null;
|
|
247
|
+
}
|
|
248
|
+
function verifiedOrThrow(manifest, notOlderThan) {
|
|
249
|
+
const check = verifyManifestSignature(manifest, { softFail: false, notOlderThan });
|
|
209
250
|
if (!check.ok) {
|
|
210
251
|
throw new ManifestSignatureError(check.reason);
|
|
211
252
|
}
|
|
@@ -221,15 +262,15 @@ export async function fetchManifest(opts = {}) {
|
|
|
221
262
|
});
|
|
222
263
|
if (res.status === 304) {
|
|
223
264
|
if (cached)
|
|
224
|
-
return verifiedOrThrow(cached.manifest);
|
|
265
|
+
return verifiedOrThrow(cached.manifest, lockFloor());
|
|
225
266
|
const retry = await authedFetch("/api/manifest", { ...opts, allowNotModified: false });
|
|
226
267
|
if (!retry.ok)
|
|
227
268
|
return null;
|
|
228
|
-
return verifiedOrThrow((await retry.json()));
|
|
269
|
+
return verifiedOrThrow((await retry.json()), lockFloor());
|
|
229
270
|
}
|
|
230
271
|
if (!res.ok)
|
|
231
272
|
return null;
|
|
232
|
-
const manifest = verifiedOrThrow((await res.json()));
|
|
273
|
+
const manifest = verifiedOrThrow((await res.json()), maxFloor(trustedCacheFloor(cached), lockFloor()));
|
|
233
274
|
const etag = res.headers.get("etag");
|
|
234
275
|
if (etag)
|
|
235
276
|
writeManifestCache(etag, manifest);
|
package/dist/bounce.js
CHANGED
|
@@ -23,6 +23,12 @@ export function shouldBounce(current, latest) {
|
|
|
23
23
|
return false;
|
|
24
24
|
return compareSemver(latest, current) > 0;
|
|
25
25
|
}
|
|
26
|
+
const WIN32_CMD_UNSAFE = /[&|<>^%!"\s]/;
|
|
27
|
+
export function unsafeBounceArgs(argv, platform = process.platform) {
|
|
28
|
+
if (platform !== "win32")
|
|
29
|
+
return [];
|
|
30
|
+
return argv.filter((a) => WIN32_CMD_UNSAFE.test(a));
|
|
31
|
+
}
|
|
26
32
|
async function fetchLatestVersion() {
|
|
27
33
|
try {
|
|
28
34
|
const res = await fetch(REGISTRY_URL, {
|
|
@@ -45,6 +51,11 @@ export async function maybeBounceToLatest(argv) {
|
|
|
45
51
|
const latest = await fetchLatestVersion();
|
|
46
52
|
if (!shouldBounce(current, latest))
|
|
47
53
|
return;
|
|
54
|
+
const unsafe = unsafeBounceArgs(argv);
|
|
55
|
+
if (unsafe.length > 0) {
|
|
56
|
+
process.stderr.write(` ⚠ No reejecuto con ${COMMAND_NAME}@${latest}: estos argumentos tienen caracteres que cmd.exe interpreta (& | < > ^ % ! " o espacios): ${unsafe.map((a) => JSON.stringify(a)).join(", ")}. Sigo con la versión ${current}.\n`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
48
59
|
const child = spawnSync("pnpm", ["dlx", "--config.minimumReleaseAge=0", `${COMMAND_NAME}@${latest}`, ...argv], {
|
|
49
60
|
stdio: "inherit",
|
|
50
61
|
env: { ...process.env, [NO_BOUNCE_ENV]: "1" },
|
package/dist/colors.js
CHANGED
|
@@ -5,14 +5,8 @@ export const dim = wrap("\x1b[2m", "\x1b[22m");
|
|
|
5
5
|
export const red = wrap("\x1b[31m", "\x1b[39m");
|
|
6
6
|
export const green = wrap("\x1b[32m", "\x1b[39m");
|
|
7
7
|
export const yellow = wrap("\x1b[33m", "\x1b[39m");
|
|
8
|
-
export const blue = wrap("\x1b[34m", "\x1b[39m");
|
|
9
|
-
export const magenta = wrap("\x1b[35m", "\x1b[39m");
|
|
10
8
|
export const cyan = wrap("\x1b[36m", "\x1b[39m");
|
|
11
9
|
export const gray = wrap("\x1b[90m", "\x1b[39m");
|
|
12
|
-
export const HIDE_CURSOR = SUPPORTS_COLOR ? "\x1b[?25l" : "";
|
|
13
|
-
export const SHOW_CURSOR = SUPPORTS_COLOR ? "\x1b[?25h" : "";
|
|
14
|
-
export const CLEAR_LINE = SUPPORTS_COLOR ? "\r\x1b[K" : "\r";
|
|
15
|
-
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
16
10
|
export function write(s) {
|
|
17
11
|
process.stdout.write(s);
|
|
18
12
|
}
|
|
@@ -28,6 +22,3 @@ export function logWarn(s) {
|
|
|
28
22
|
export function logOk(s) {
|
|
29
23
|
process.stdout.write(`${green("✓ ") + s}\n`);
|
|
30
24
|
}
|
|
31
|
-
export function logInfo(s) {
|
|
32
|
-
process.stdout.write(`${cyan("ℹ ") + s}\n`);
|
|
33
|
-
}
|
package/dist/commands/hook.js
CHANGED
|
@@ -9,11 +9,12 @@ function versionRangeForHook(version) {
|
|
|
9
9
|
return "latest";
|
|
10
10
|
return major === 0 ? "0.x" : `^${major}.0.0`;
|
|
11
11
|
}
|
|
12
|
+
export const AUTO_UPDATE_MIN_RELEASE_AGE_MINUTES = 0;
|
|
12
13
|
function hookCommandFor(mode) {
|
|
13
14
|
const v = getPackageVersion();
|
|
14
15
|
const range = versionRangeForHook(v);
|
|
15
16
|
const verb = mode === "auto" ? "update --yes --background" : "check";
|
|
16
|
-
return `pnpm dlx --config.minimumReleaseAge
|
|
17
|
+
return `pnpm dlx --config.minimumReleaseAge=${AUTO_UPDATE_MIN_RELEASE_AGE_MINUTES} ${COMMAND_NAME}@${range} ${verb} --no-bounce`;
|
|
17
18
|
}
|
|
18
19
|
const HOOK_SIGNATURE = COMMAND_NAME;
|
|
19
20
|
const HOOK_TIMEOUT_SECONDS = 120;
|
package/dist/commands/status.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
4
|
import { AuthRequiredError, fetchManifest } from "../api.js";
|
|
5
5
|
import { readAuth } from "../auth.js";
|
|
6
6
|
import { bold, cyan, dim, gray, green, log, logError, logWarn, yellow } from "../colors.js";
|
|
@@ -15,7 +15,27 @@ function reportAutoUpdateLog() {
|
|
|
15
15
|
const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
16
16
|
if (lines.length === 0)
|
|
17
17
|
return;
|
|
18
|
-
|
|
18
|
+
const last = lines[lines.length - 1];
|
|
19
|
+
if (/login no se completó/.test(last)) {
|
|
20
|
+
logWarn(`El auto-update necesita volver a iniciar sesión (el login automático no se completó ${lines.length} vez/veces) — última: "${last}". Agente: corré ${bold(`${COMMAND_NAME} update`)}, que abre el login y actualiza en el acto.`);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
logWarn(`El auto-update no pudo correr ${lines.length} vez/veces en un cliente GUI — última: "${last}". Agente: corré ${bold(`${COMMAND_NAME} update`)} (actualiza ya mismo y limpia este aviso); si reaparece, el cliente GUI no encuentra pnpm en su PATH y hay que reinstalar con ${bold(`${COMMAND_NAME} install`)} desde una terminal con pnpm disponible.`);
|
|
24
|
+
}
|
|
25
|
+
log("");
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function reportSkillBackups() {
|
|
31
|
+
const backupsLog = join(dirname(getAutoUpdateLogPath()), "backups", "backups.log");
|
|
32
|
+
if (!existsSync(backupsLog))
|
|
33
|
+
return;
|
|
34
|
+
try {
|
|
35
|
+
const lines = readFileSync(backupsLog, "utf-8").trim().split("\n").filter(Boolean);
|
|
36
|
+
if (lines.length === 0)
|
|
37
|
+
return;
|
|
38
|
+
logWarn(`Se guardaron ${lines.length} backup(s) de skills que habías modificado antes de actualizarlas — último: "${lines[lines.length - 1]}". Están en ${dirname(backupsLog)}.`);
|
|
19
39
|
log("");
|
|
20
40
|
}
|
|
21
41
|
catch {
|
|
@@ -288,6 +308,7 @@ export async function runStatus() {
|
|
|
288
308
|
logWarn(`${scopeWarning} — los MCPs quedaron en scope usuario.`);
|
|
289
309
|
log("");
|
|
290
310
|
reportAutoUpdateLog();
|
|
311
|
+
reportSkillBackups();
|
|
291
312
|
if (!auth) {
|
|
292
313
|
logWarn(`Inicia sesión con: ${bold(`${COMMAND_NAME} login`)}`);
|
|
293
314
|
log("");
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
1
|
import { removeAutoUpdate } from "../autoupdate.js";
|
|
5
2
|
import { dim, gray, log, logOk, logWarn, red } from "../colors.js";
|
|
6
3
|
import { uninstallAll } from "../installer.js";
|
|
@@ -37,7 +34,6 @@ export async function runUninstall(opts = {}) {
|
|
|
37
34
|
if (!opts.dryRun) {
|
|
38
35
|
for (const t of Object.keys(TARGETS))
|
|
39
36
|
removeAutoUpdate(t);
|
|
40
|
-
removeNpmrcCooldownExclude();
|
|
41
37
|
}
|
|
42
38
|
log("");
|
|
43
39
|
if (opts.dryRun) {
|
|
@@ -48,15 +44,3 @@ export async function runUninstall(opts = {}) {
|
|
|
48
44
|
}
|
|
49
45
|
return 0;
|
|
50
46
|
}
|
|
51
|
-
function removeNpmrcCooldownExclude() {
|
|
52
|
-
const npmrc = join(homedir(), ".npmrc");
|
|
53
|
-
if (!existsSync(npmrc))
|
|
54
|
-
return;
|
|
55
|
-
const EXCLUDE = "minimum-release-age-exclude[]=alegra-data-agent";
|
|
56
|
-
const lines = readFileSync(npmrc, "utf-8").split("\n");
|
|
57
|
-
const kept = lines.filter((l) => l.trim() !== EXCLUDE);
|
|
58
|
-
if (kept.length !== lines.length) {
|
|
59
|
-
writeFileSync(npmrc, kept.join("\n"));
|
|
60
|
-
logOk("Removido minimum-release-age-exclude de ~/.npmrc");
|
|
61
|
-
}
|
|
62
|
-
}
|
package/dist/commands/update.js
CHANGED
|
@@ -1,45 +1,90 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { AuthRequiredError, fetchManifest } from "../api.js";
|
|
4
4
|
import { hasValidAuth } from "../auth.js";
|
|
5
5
|
import { refreshAutoUpdate } from "../autoupdate.js";
|
|
6
6
|
import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
|
|
7
7
|
import { detectDrift, installAll, summarize } from "../installer.js";
|
|
8
|
-
import { getAutoUpdateLogPath, getLastCheckPath } from "../lib.js";
|
|
8
|
+
import { COMMAND_NAME, getAutoUpdateLogPath, getLastCheckPath } from "../lib.js";
|
|
9
9
|
import { banner } from "../ui.js";
|
|
10
10
|
import { runLogin } from "./login.js";
|
|
11
11
|
const BACKGROUND_COOLDOWN_MS = 15 * 60_000;
|
|
12
|
-
const BACKGROUND_LOGIN_TIMEOUT_MS =
|
|
13
|
-
|
|
12
|
+
const BACKGROUND_LOGIN_TIMEOUT_MS = (() => {
|
|
13
|
+
const v = Number(process.env.ALEGRA_DATA_AGENT_BG_LOGIN_TIMEOUT_MS);
|
|
14
|
+
return Number.isFinite(v) && v > 0 ? v : 90_000;
|
|
15
|
+
})();
|
|
16
|
+
export const BACKGROUND_AUTH_FAIL_COOLDOWN_MS = 60 * 60_000;
|
|
17
|
+
function readLastCheck() {
|
|
14
18
|
try {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
if (Number.isNaN(at))
|
|
18
|
-
return false;
|
|
19
|
-
const age = Date.now() - at;
|
|
20
|
-
return age >= 0 && age < BACKGROUND_COOLDOWN_MS;
|
|
19
|
+
const parsed = JSON.parse(readFileSync(getLastCheckPath(), "utf-8"));
|
|
20
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
21
21
|
}
|
|
22
22
|
catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function withinWindow(iso, windowMs) {
|
|
27
|
+
const at = Date.parse(iso ?? "");
|
|
28
|
+
if (Number.isNaN(at))
|
|
23
29
|
return false;
|
|
30
|
+
const age = Date.now() - at;
|
|
31
|
+
return age >= 0 && age < windowMs;
|
|
32
|
+
}
|
|
33
|
+
function checkedRecently() {
|
|
34
|
+
return withinWindow(readLastCheck().checkedAt, BACKGROUND_COOLDOWN_MS);
|
|
35
|
+
}
|
|
36
|
+
function authFailedRecently() {
|
|
37
|
+
return withinWindow(readLastCheck().authFailedAt, BACKGROUND_AUTH_FAIL_COOLDOWN_MS);
|
|
38
|
+
}
|
|
39
|
+
function writeLastCheck(data) {
|
|
40
|
+
try {
|
|
41
|
+
const path = getLastCheckPath();
|
|
42
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
43
|
+
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
24
46
|
}
|
|
25
47
|
}
|
|
26
48
|
function markChecked() {
|
|
49
|
+
writeLastCheck({ checkedAt: new Date().toISOString() });
|
|
50
|
+
}
|
|
51
|
+
function markAuthFailed() {
|
|
52
|
+
writeLastCheck({ ...readLastCheck(), authFailedAt: new Date().toISOString() });
|
|
53
|
+
}
|
|
54
|
+
function appendHookLog(message) {
|
|
27
55
|
try {
|
|
28
|
-
const path =
|
|
56
|
+
const path = getAutoUpdateLogPath();
|
|
29
57
|
mkdirSync(dirname(path), { recursive: true });
|
|
30
|
-
|
|
58
|
+
appendFileSync(path, `${new Date().toISOString()} ${message}\n`);
|
|
31
59
|
}
|
|
32
60
|
catch {
|
|
33
61
|
}
|
|
34
62
|
}
|
|
63
|
+
async function loginFromBackground(reason, force = false) {
|
|
64
|
+
const code = await runLogin({
|
|
65
|
+
timeoutMs: BACKGROUND_LOGIN_TIMEOUT_MS,
|
|
66
|
+
...(force ? { force: true } : {}),
|
|
67
|
+
});
|
|
68
|
+
if (code === 0)
|
|
69
|
+
return true;
|
|
70
|
+
appendHookLog(`[update --background] ${reason} y el login no se completó. Se reintenta solo en la próxima sesión pasada 1 h (o al correr \`${COMMAND_NAME} update\`).`);
|
|
71
|
+
markAuthFailed();
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
35
74
|
export async function runUpdate(opts = {}) {
|
|
36
75
|
if (opts.background && !opts.dryRun && checkedRecently())
|
|
37
76
|
return 0;
|
|
77
|
+
if (opts.background && !hasValidAuth()) {
|
|
78
|
+
if (authFailedRecently())
|
|
79
|
+
return 0;
|
|
80
|
+
if (!(await loginFromBackground("sesión vencida o ausente")))
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
38
83
|
banner();
|
|
39
84
|
if (!hasValidAuth()) {
|
|
40
|
-
const code = await runLogin(
|
|
85
|
+
const code = await runLogin({});
|
|
41
86
|
if (code !== 0)
|
|
42
|
-
return
|
|
87
|
+
return code;
|
|
43
88
|
}
|
|
44
89
|
let reauthed = false;
|
|
45
90
|
for (;;) {
|
|
@@ -47,6 +92,12 @@ export async function runUpdate(opts = {}) {
|
|
|
47
92
|
return await runUpdateOnce(opts);
|
|
48
93
|
}
|
|
49
94
|
catch (err) {
|
|
95
|
+
if (err instanceof AuthRequiredError && opts.background && !reauthed) {
|
|
96
|
+
reauthed = true;
|
|
97
|
+
if (!(await loginFromBackground("el gateway rechazó la sesión", true)))
|
|
98
|
+
return 0;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
50
101
|
if (err instanceof AuthRequiredError && !reauthed) {
|
|
51
102
|
reauthed = true;
|
|
52
103
|
log(dim(" La sesión expiró durante la actualización; reabro el login y reintento.\n"));
|
|
@@ -56,6 +107,8 @@ export async function runUpdate(opts = {}) {
|
|
|
56
107
|
continue;
|
|
57
108
|
}
|
|
58
109
|
if (err instanceof AuthRequiredError) {
|
|
110
|
+
if (opts.background)
|
|
111
|
+
return 0;
|
|
59
112
|
logError(err.message);
|
|
60
113
|
return 2;
|
|
61
114
|
}
|
package/dist/installer.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { chmodSync, copyFileSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { appendFileSync, chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
4
4
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
5
|
-
import { AuthRequiredError, fetchManifest, fetchSkillFile } from "./api.js";
|
|
6
|
-
import { dim, gray, green, logError, red, yellow } from "./colors.js";
|
|
5
|
+
import { AuthRequiredError, fetchManifest, fetchSkillFile, recordManifestFloor } from "./api.js";
|
|
6
|
+
import { dim, gray, green, logError, logWarn, red, yellow } from "./colors.js";
|
|
7
7
|
import { ensureLazyProxy, lazyMcpDir, lazyProxyPath, lazyToolsPath, stdioLaunch, } from "./lazy-mcp.js";
|
|
8
|
-
import { ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getCursorLegacyRulesDir, getHermesBin, getLockFilePath, getMcpScopeDir, getMcpScopeWarning, getOpenCodeLegacyPromptsDir, getPackageVersion, getUserScopeMcpConfigPath, isMcpScopeEnvOverride, readLockFile, sha256File, writeLockFile, } from "./lib.js";
|
|
9
|
-
import {
|
|
8
|
+
import { computeBundleHash, ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getConfigDir, getCursorLegacyRulesDir, getHermesBin, getLockFilePath, getMcpScopeDir, getMcpScopeWarning, getOpenCodeLegacyPromptsDir, getPackageVersion, getUserScopeMcpConfigPath, isMcpScopeEnvOverride, readLockFile, sha256File, walkSkillFiles, writeLockFile, } from "./lib.js";
|
|
9
|
+
import { renderHermesMcpOps, renderMcpForTarget } from "./render.js";
|
|
10
10
|
import { TARGETS, detectInstalledTargets } from "./targets.js";
|
|
11
11
|
const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
12
12
|
const MCP_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
@@ -120,66 +120,100 @@ async function stageSkill(skillName, entry) {
|
|
|
120
120
|
function skillDirsFor(spec) {
|
|
121
121
|
return [spec.skillsDir, ...(spec.mirrorSkillsDirs ?? [])];
|
|
122
122
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
123
|
+
const OS_JUNK_FILES = new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
|
|
124
|
+
function diskBundleHash(dir) {
|
|
125
|
+
const shas = {};
|
|
126
|
+
for (const rel of walkSkillFiles(dir)) {
|
|
127
|
+
if (OS_JUNK_FILES.has(basename(rel)))
|
|
128
|
+
continue;
|
|
129
|
+
shas[rel] = sha256File(join(dir, rel));
|
|
130
|
+
}
|
|
131
|
+
return computeBundleHash(shas);
|
|
132
|
+
}
|
|
133
|
+
function localDateStamp(d = new Date()) {
|
|
134
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
135
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
136
|
+
}
|
|
137
|
+
export function skillBackupRoot(target) {
|
|
138
|
+
return join(getConfigDir(), "backups", target);
|
|
139
|
+
}
|
|
140
|
+
export function skillBackupLogPath() {
|
|
141
|
+
return join(getConfigDir(), "backups", "backups.log");
|
|
142
|
+
}
|
|
143
|
+
function moveToBackup(dir, skillName, backupRoot) {
|
|
144
|
+
mkdirSync(backupRoot, { recursive: true });
|
|
145
|
+
const base = join(backupRoot, `${skillName}-${localDateStamp()}`);
|
|
146
|
+
let dst = base;
|
|
147
|
+
for (let n = 2; existsSync(dst); n++)
|
|
148
|
+
dst = `${base}-${n}`;
|
|
149
|
+
try {
|
|
150
|
+
withFsRetry(() => renameSync(dir, dst));
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
if (err.code !== "EXDEV")
|
|
154
|
+
throw err;
|
|
155
|
+
cpSync(dir, dst, { recursive: true });
|
|
156
|
+
withFsRetry(() => rmSync(dir, { recursive: true, force: true }));
|
|
157
|
+
}
|
|
158
|
+
return dst;
|
|
159
|
+
}
|
|
160
|
+
function noteSkillBackup(skillName, from, to) {
|
|
161
|
+
const line = `${skillName}: tenías cambios locales en ${from}; los guardé en ${to} antes de actualizar.`;
|
|
162
|
+
logWarn(line);
|
|
163
|
+
try {
|
|
164
|
+
mkdirSync(dirname(skillBackupLogPath()), { recursive: true });
|
|
165
|
+
appendFileSync(skillBackupLogPath(), `${new Date().toISOString()} ${line}\n`);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export function replaceSkillDir(dst, skillName, staged, opts) {
|
|
171
|
+
let backup = null;
|
|
172
|
+
if (existsSync(dst)) {
|
|
173
|
+
const known = new Set(opts.knownBundleHashes.filter((h) => !!h));
|
|
174
|
+
if (!known.has(diskBundleHash(dst))) {
|
|
175
|
+
backup = moveToBackup(dst, skillName, opts.backupRoot);
|
|
176
|
+
noteSkillBackup(skillName, dst, backup);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
withFsRetry(() => rmSync(dst, { recursive: true, force: true }));
|
|
136
180
|
}
|
|
137
|
-
return;
|
|
138
181
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
.filter((f) => f.rel !== "SKILL.md" && f.rel !== "README.md")
|
|
145
|
-
.map((f) => ({ rel: f.rel, text: new TextDecoder().decode(f.data) }));
|
|
146
|
-
const content = new TextDecoder().decode(entry.data);
|
|
147
|
-
const rendered = renderSkillForTargetFromContent(content, skillName, spec.skillFormat, references);
|
|
148
|
-
ensureDir(spec.skillsDir);
|
|
149
|
-
const dst = join(spec.skillsDir, rendered.filename);
|
|
150
|
-
atomicWriteFile(dst, rendered.content);
|
|
151
|
-
}
|
|
152
|
-
function renderSkillForTargetFromContent(content, skillName, format, references = []) {
|
|
153
|
-
if (format === "folder") {
|
|
154
|
-
throw new Error("renderSkillForTargetFromContent no soporta 'folder'");
|
|
155
|
-
}
|
|
156
|
-
const stripped = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").replace(/^\s+/, "");
|
|
157
|
-
const body = stripped + inlineReferences(references);
|
|
158
|
-
const fm = parseFrontmatterMinimal(content);
|
|
159
|
-
if (format === "cursor-mdc") {
|
|
160
|
-
const desc = fm.description ?? `Skill ${skillName}`;
|
|
161
|
-
const cursorDesc = desc.startsWith("USE WHEN") ? desc : `USE WHEN ${desc}`;
|
|
162
|
-
const safeDesc = cursorDesc.replace(/\n/g, " ").replace(/"/g, '\\"');
|
|
163
|
-
return {
|
|
164
|
-
filename: `${skillName}.mdc`,
|
|
165
|
-
content: `---\ndescription: "${safeDesc}"\nalwaysApply: false\n---\n\n${body}`,
|
|
166
|
-
};
|
|
182
|
+
ensureDir(dst);
|
|
183
|
+
for (const f of staged) {
|
|
184
|
+
const dstFile = safeJoinSkillFile(dst, f.rel);
|
|
185
|
+
ensureDir(dirname(dstFile));
|
|
186
|
+
withFsRetry(() => writeFileSync(dstFile, f.data));
|
|
167
187
|
}
|
|
168
|
-
return
|
|
188
|
+
return backup;
|
|
169
189
|
}
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
190
|
+
function assertFolderFormat(spec) {
|
|
191
|
+
if (spec.skillFormat !== "folder") {
|
|
192
|
+
throw new Error(`formato de skill no soportado para ${spec.id}: ${spec.skillFormat}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function writeSkillToTarget(skillName, staged, spec, knownBundleHashes) {
|
|
196
|
+
validateSkillName(skillName);
|
|
197
|
+
assertFolderFormat(spec);
|
|
198
|
+
for (const base of skillDirsFor(spec)) {
|
|
199
|
+
replaceSkillDir(join(base, skillName), skillName, staged, {
|
|
200
|
+
knownBundleHashes,
|
|
201
|
+
backupRoot: skillBackupRoot(spec.id),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function brokenSkillDirs(name, entry, specs) {
|
|
206
|
+
const broken = [];
|
|
207
|
+
for (const spec of specs) {
|
|
208
|
+
if (spec.skillFormat !== "folder")
|
|
209
|
+
continue;
|
|
210
|
+
for (const base of skillDirsFor(spec)) {
|
|
211
|
+
const dir = join(base, name);
|
|
212
|
+
if (!verifyInstalledFiles(dir, entry).ok)
|
|
213
|
+
broken.push(dir);
|
|
180
214
|
}
|
|
181
215
|
}
|
|
182
|
-
return
|
|
216
|
+
return broken;
|
|
183
217
|
}
|
|
184
218
|
function sleepSync(ms) {
|
|
185
219
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
@@ -885,14 +919,7 @@ export async function installSkill(name, manifest, opts = {}) {
|
|
|
885
919
|
prev.targets.length === targetIds.length &&
|
|
886
920
|
prev.targets.every((t) => targetIds.includes(t));
|
|
887
921
|
if (prev && prev.bundleHash === entry.bundleHash && targetsMatch && !opts.force) {
|
|
888
|
-
|
|
889
|
-
if (folderTarget) {
|
|
890
|
-
const dst = join(folderTarget.skillsDir, name);
|
|
891
|
-
const drift = verifyInstalledFiles(dst, entry);
|
|
892
|
-
if (drift.ok)
|
|
893
|
-
return { name, kind: "skill", status: "unchanged", targets: targetIds };
|
|
894
|
-
}
|
|
895
|
-
else {
|
|
922
|
+
if (brokenSkillDirs(name, entry, targets).length === 0) {
|
|
896
923
|
return { name, kind: "skill", status: "unchanged", targets: targetIds };
|
|
897
924
|
}
|
|
898
925
|
}
|
|
@@ -923,7 +950,7 @@ export async function installSkill(name, manifest, opts = {}) {
|
|
|
923
950
|
const failures = [];
|
|
924
951
|
for (const t of targets) {
|
|
925
952
|
try {
|
|
926
|
-
writeSkillToTarget(name, staged, t);
|
|
953
|
+
writeSkillToTarget(name, staged, t, [prev?.bundleHash, entry.bundleHash]);
|
|
927
954
|
okTargets.push(t.id);
|
|
928
955
|
}
|
|
929
956
|
catch (err) {
|
|
@@ -1193,6 +1220,8 @@ export async function installAll(opts = {}) {
|
|
|
1193
1220
|
}
|
|
1194
1221
|
results.push(installMcp(mcp, { ...opts, targets }));
|
|
1195
1222
|
}
|
|
1223
|
+
if (!opts.dryRun)
|
|
1224
|
+
recordManifestFloor(manifest);
|
|
1196
1225
|
return { manifest, results, targets };
|
|
1197
1226
|
}
|
|
1198
1227
|
export function uninstallSkill(name, opts = {}) {
|
|
@@ -1216,18 +1245,10 @@ export function uninstallSkill(name, opts = {}) {
|
|
|
1216
1245
|
if (!opts.dryRun) {
|
|
1217
1246
|
for (const tId of targetIds) {
|
|
1218
1247
|
const spec = TARGETS[tId];
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
const dst = join(base, name);
|
|
1222
|
-
if (existsSync(dst))
|
|
1223
|
-
rmSync(dst, { recursive: true, force: true });
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
else {
|
|
1227
|
-
const ext = spec.skillFormat === "cursor-mdc" ? ".mdc" : ".md";
|
|
1228
|
-
const dst = join(spec.skillsDir, `${name}${ext}`);
|
|
1248
|
+
for (const base of skillDirsFor(spec)) {
|
|
1249
|
+
const dst = join(base, name);
|
|
1229
1250
|
if (existsSync(dst))
|
|
1230
|
-
rmSync(dst, { force: true });
|
|
1251
|
+
rmSync(dst, { recursive: true, force: true });
|
|
1231
1252
|
}
|
|
1232
1253
|
}
|
|
1233
1254
|
delete lock.skills[name];
|
|
@@ -1316,13 +1337,22 @@ export function detectDrift(manifest) {
|
|
|
1316
1337
|
const toUpdate = [];
|
|
1317
1338
|
const unchanged = [];
|
|
1318
1339
|
const orphaned = [];
|
|
1340
|
+
const detected = new Set(detectInstalledTargets());
|
|
1319
1341
|
for (const name of Object.keys(manifest.skills)) {
|
|
1320
1342
|
const prev = lock.skills[name];
|
|
1321
1343
|
if (!prev) {
|
|
1322
1344
|
toInstall.push(name);
|
|
1323
1345
|
continue;
|
|
1324
1346
|
}
|
|
1325
|
-
|
|
1347
|
+
const entry = manifest.skills[name];
|
|
1348
|
+
if (prev.bundleHash !== entry.bundleHash) {
|
|
1349
|
+
toUpdate.push(name);
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
const specs = (prev.targets ?? ["claude-code"])
|
|
1353
|
+
.filter((t) => detected.has(t) && TARGETS[t])
|
|
1354
|
+
.map((t) => TARGETS[t]);
|
|
1355
|
+
if (brokenSkillDirs(name, entry, specs).length > 0) {
|
|
1326
1356
|
toUpdate.push(name);
|
|
1327
1357
|
}
|
|
1328
1358
|
else {
|
package/dist/lazy-mcp.js
CHANGED
|
@@ -58,6 +58,8 @@ const spec = JSON.parse(readFileSync(argv[0], "utf8"));
|
|
|
58
58
|
const [cmd, ...args] = argv.slice(2);
|
|
59
59
|
const INIT_ID = "__alegra_lazy_init__";
|
|
60
60
|
const START_TIMEOUT_MS = 10 * 60 * 1000;
|
|
61
|
+
const LEVELS = ["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"];
|
|
62
|
+
let logLevel = "info";
|
|
61
63
|
|
|
62
64
|
const send = (m) => process.stdout.write(JSON.stringify(m) + "\\n");
|
|
63
65
|
const key = (id) => JSON.stringify(id);
|
|
@@ -102,8 +104,23 @@ function forward(m) {
|
|
|
102
104
|
child.stdin.write(JSON.stringify(m) + "\\n");
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
function
|
|
107
|
+
function notifyStarting(trigger) {
|
|
108
|
+
const text =
|
|
109
|
+
"Preparando " + spec.name + ": la primera vez tarda 1-2 min mientras se descarga " +
|
|
110
|
+
"(las siguientes llamadas de la sesión son inmediatas).";
|
|
111
|
+
process.stderr.write("[" + spec.name + "] " + text + "\\n");
|
|
112
|
+
if (LEVELS.indexOf(logLevel) <= LEVELS.indexOf("info")) {
|
|
113
|
+
send({ jsonrpc: "2.0", method: "notifications/message", params: { level: "info", logger: spec.name, data: text } });
|
|
114
|
+
}
|
|
115
|
+
const token = trigger && trigger.params && trigger.params._meta && trigger.params._meta.progressToken;
|
|
116
|
+
if (token !== undefined && token !== null) {
|
|
117
|
+
send({ jsonrpc: "2.0", method: "notifications/progress", params: { progressToken: token, progress: 0, message: text } });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function start(trigger) {
|
|
106
122
|
if (child || dead) return;
|
|
123
|
+
notifyStarting(trigger);
|
|
107
124
|
child = spawn(cmd, args, { stdio: ["pipe", "pipe", "inherit"] });
|
|
108
125
|
startTimer = setTimeout(
|
|
109
126
|
() => die("El MCP " + spec.name + " tardó más de 10 min en arrancar (¿se cortó la descarga?). Reintentá en una sesión nueva."),
|
|
@@ -152,7 +169,7 @@ createInterface({ input: process.stdin }).on("line", (line) => {
|
|
|
152
169
|
id: m.id,
|
|
153
170
|
result: {
|
|
154
171
|
protocolVersion: spec.protocolVersion ?? initParams.protocolVersion ?? "2025-06-18",
|
|
155
|
-
capabilities: { tools: {} },
|
|
172
|
+
capabilities: { tools: {}, logging: {} },
|
|
156
173
|
serverInfo: { name: spec.name, version: "lazy" },
|
|
157
174
|
},
|
|
158
175
|
});
|
|
@@ -160,6 +177,10 @@ createInterface({ input: process.stdin }).on("line", (line) => {
|
|
|
160
177
|
case "ping":
|
|
161
178
|
send({ jsonrpc: "2.0", id: m.id, result: {} });
|
|
162
179
|
return;
|
|
180
|
+
case "logging/setLevel":
|
|
181
|
+
if (LEVELS.includes(m.params?.level)) logLevel = m.params.level;
|
|
182
|
+
send({ jsonrpc: "2.0", id: m.id, result: {} });
|
|
183
|
+
return;
|
|
163
184
|
case "tools/list":
|
|
164
185
|
send({ jsonrpc: "2.0", id: m.id, result: { tools: spec.tools } });
|
|
165
186
|
return;
|
|
@@ -185,7 +206,7 @@ createInterface({ input: process.stdin }).on("line", (line) => {
|
|
|
185
206
|
return;
|
|
186
207
|
}
|
|
187
208
|
queue.push(m);
|
|
188
|
-
start();
|
|
209
|
+
start(m);
|
|
189
210
|
});
|
|
190
211
|
|
|
191
212
|
function shutdown() {
|
package/dist/lib.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync, } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -7,7 +7,34 @@ export const PRODUCT_NAME = "alegra-data-agent";
|
|
|
7
7
|
export const COMMAND_NAME = "alegra-data-agent";
|
|
8
8
|
export const REPO_SLUG = "Alegra-Team/data-agent-selfservice";
|
|
9
9
|
export const REPO_DEFAULT_BRANCH = "main";
|
|
10
|
-
export const
|
|
10
|
+
export const DEFAULT_GATEWAY_URL = "https://alegra-data-agent.vercel.app";
|
|
11
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost"]);
|
|
12
|
+
export function resolveGatewayUrl(raw) {
|
|
13
|
+
if (raw === undefined || raw.trim() === "")
|
|
14
|
+
return { url: DEFAULT_GATEWAY_URL };
|
|
15
|
+
let parsed;
|
|
16
|
+
try {
|
|
17
|
+
parsed = new URL(raw);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return {
|
|
21
|
+
url: DEFAULT_GATEWAY_URL,
|
|
22
|
+
warning: `ALEGRA_DATA_AGENT_GATEWAY_URL no es una URL válida ('${raw}'); uso ${DEFAULT_GATEWAY_URL}.`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (parsed.protocol === "https:")
|
|
26
|
+
return { url: raw };
|
|
27
|
+
if (parsed.protocol === "http:" && LOOPBACK_HOSTS.has(parsed.hostname))
|
|
28
|
+
return { url: raw };
|
|
29
|
+
return {
|
|
30
|
+
url: DEFAULT_GATEWAY_URL,
|
|
31
|
+
warning: `ALEGRA_DATA_AGENT_GATEWAY_URL debe ser https:// (http:// solo para 127.0.0.1/localhost); ignoro '${raw}' y uso ${DEFAULT_GATEWAY_URL}.`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const _gateway = resolveGatewayUrl(process.env.ALEGRA_DATA_AGENT_GATEWAY_URL);
|
|
35
|
+
if (_gateway.warning)
|
|
36
|
+
process.stderr.write(` ⚠ ${_gateway.warning}\n`);
|
|
37
|
+
export const GATEWAY_URL = _gateway.url;
|
|
11
38
|
export const ALL_TARGETS = [
|
|
12
39
|
"claude-code",
|
|
13
40
|
"cursor",
|
|
@@ -278,27 +305,226 @@ let _lockCache = null;
|
|
|
278
305
|
function emptyLock() {
|
|
279
306
|
return { schemaVersion: 1, productName: PRODUCT_NAME, skills: {} };
|
|
280
307
|
}
|
|
308
|
+
function warn(message) {
|
|
309
|
+
process.stderr.write(` ⚠ ${message}\n`);
|
|
310
|
+
}
|
|
311
|
+
function isLockShape(v) {
|
|
312
|
+
if (!v || typeof v !== "object" || Array.isArray(v))
|
|
313
|
+
return false;
|
|
314
|
+
const skills = v.skills;
|
|
315
|
+
return !!skills && typeof skills === "object" && !Array.isArray(skills);
|
|
316
|
+
}
|
|
281
317
|
export function readLockFile() {
|
|
282
318
|
if (_lockCache)
|
|
283
319
|
return _lockCache;
|
|
284
320
|
const path = getLockFilePath();
|
|
285
321
|
let lock = emptyLock();
|
|
286
322
|
if (existsSync(path)) {
|
|
323
|
+
let parsed;
|
|
324
|
+
let ok = false;
|
|
287
325
|
try {
|
|
288
|
-
|
|
326
|
+
parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
327
|
+
ok = isLockShape(parsed);
|
|
289
328
|
}
|
|
290
329
|
catch {
|
|
291
|
-
|
|
330
|
+
ok = false;
|
|
331
|
+
}
|
|
332
|
+
if (ok) {
|
|
333
|
+
lock = parsed;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
const backup = `${path}.corrupt-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
337
|
+
try {
|
|
338
|
+
renameSync(path, backup);
|
|
339
|
+
warn(`El lock del agente (${path}) estaba corrupto: lo aparté en ${backup} y sigo como si no hubiera nada instalado. Un \`install\` lo regenera.`);
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
warn(`El lock del agente (${path}) está corrupto y no pude apartarlo; lo leo como vacío.`);
|
|
343
|
+
}
|
|
292
344
|
}
|
|
293
345
|
}
|
|
294
346
|
_lockCache = lock;
|
|
295
347
|
return lock;
|
|
296
348
|
}
|
|
297
|
-
export
|
|
349
|
+
export const LOCKFILE_STALE_MS = 60_000;
|
|
350
|
+
export const LOCKFILE_MAX_WAIT_MS = 10_000;
|
|
351
|
+
function sleepSync(ms) {
|
|
352
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
353
|
+
}
|
|
354
|
+
function tryCreateLock(lockPath) {
|
|
355
|
+
let fd;
|
|
356
|
+
try {
|
|
357
|
+
fd = openSync(lockPath, "wx");
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
if (err.code === "EEXIST")
|
|
361
|
+
return false;
|
|
362
|
+
throw err;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
writeSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
closeSync(fd);
|
|
369
|
+
}
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
function pidAlive(pid) {
|
|
373
|
+
try {
|
|
374
|
+
process.kill(pid, 0);
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
return err.code === "EPERM";
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function lockIsStale(lockPath) {
|
|
382
|
+
let at;
|
|
383
|
+
let pid;
|
|
384
|
+
try {
|
|
385
|
+
const raw = JSON.parse(readFileSync(lockPath, "utf-8"));
|
|
386
|
+
if (typeof raw.at === "number")
|
|
387
|
+
at = raw.at;
|
|
388
|
+
if (typeof raw.pid === "number")
|
|
389
|
+
pid = raw.pid;
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
}
|
|
393
|
+
if (at === undefined) {
|
|
394
|
+
try {
|
|
395
|
+
at = statSync(lockPath).mtimeMs;
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const age = Date.now() - at;
|
|
402
|
+
if (age < 0 || age > LOCKFILE_STALE_MS)
|
|
403
|
+
return true;
|
|
404
|
+
if (pid !== undefined && pid !== process.pid && !pidAlive(pid))
|
|
405
|
+
return true;
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
function acquireWriteLock(lockPath, maxWaitMs) {
|
|
409
|
+
const deadline = Date.now() + maxWaitMs;
|
|
410
|
+
let backoff = 25;
|
|
411
|
+
for (;;) {
|
|
412
|
+
if (tryCreateLock(lockPath))
|
|
413
|
+
return true;
|
|
414
|
+
if (lockIsStale(lockPath)) {
|
|
415
|
+
const grave = `${lockPath}.stale-${process.pid}-${Date.now()}`;
|
|
416
|
+
try {
|
|
417
|
+
renameSync(lockPath, grave);
|
|
418
|
+
rmSync(grave, { force: true });
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
}
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (Date.now() >= deadline)
|
|
425
|
+
return false;
|
|
426
|
+
sleepSync(Math.min(backoff, Math.max(1, deadline - Date.now())));
|
|
427
|
+
backoff = Math.min(backoff * 2, 500);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
export function writeLockFile(lock, opts = {}) {
|
|
298
431
|
const path = getLockFilePath();
|
|
299
432
|
_lockCache = lock;
|
|
300
433
|
mkdirSync(dirname(path), { recursive: true });
|
|
301
|
-
|
|
434
|
+
const lockPath = `${path}.lock`;
|
|
435
|
+
if (!acquireWriteLock(lockPath, opts.maxWaitMs ?? LOCKFILE_MAX_WAIT_MS)) {
|
|
436
|
+
warn(`Otro proceso del agente está escribiendo ${path} (${lockPath}); no lo pisé. El próximo \`update\` lo reconcilia.`);
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
441
|
+
writeFileSync(tmp, `${JSON.stringify(lock, null, 2)}\n`, "utf-8");
|
|
442
|
+
renameSync(tmp, path);
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
rmSync(lockPath, { force: true });
|
|
446
|
+
}
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
export function parseTargetList(raw) {
|
|
450
|
+
const out = [];
|
|
451
|
+
for (const piece of raw
|
|
452
|
+
.split(",")
|
|
453
|
+
.map((s) => s.trim())
|
|
454
|
+
.filter(Boolean)) {
|
|
455
|
+
if (ALL_TARGETS.includes(piece)) {
|
|
456
|
+
out.push(piece);
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
throw new Error(`Target desconocido: '${piece}'. Válidos: ${ALL_TARGETS.join(", ")}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
export function parseArgs(argv) {
|
|
465
|
+
const flags = {
|
|
466
|
+
yes: false,
|
|
467
|
+
dryRun: false,
|
|
468
|
+
force: false,
|
|
469
|
+
help: false,
|
|
470
|
+
version: false,
|
|
471
|
+
noHook: false,
|
|
472
|
+
noBounce: false,
|
|
473
|
+
autoUpdate: false,
|
|
474
|
+
background: false,
|
|
475
|
+
};
|
|
476
|
+
const positional = [];
|
|
477
|
+
const only = [];
|
|
478
|
+
const targets = [];
|
|
479
|
+
let i = 0;
|
|
480
|
+
while (i < argv.length) {
|
|
481
|
+
const a = argv[i];
|
|
482
|
+
if (a === "-y" || a === "--yes")
|
|
483
|
+
flags.yes = true;
|
|
484
|
+
else if (a === "--dry-run")
|
|
485
|
+
flags.dryRun = true;
|
|
486
|
+
else if (a === "--force")
|
|
487
|
+
flags.force = true;
|
|
488
|
+
else if (a === "--no-hook")
|
|
489
|
+
flags.noHook = true;
|
|
490
|
+
else if (a === "--no-bounce")
|
|
491
|
+
flags.noBounce = true;
|
|
492
|
+
else if (a === "--auto-update")
|
|
493
|
+
flags.autoUpdate = true;
|
|
494
|
+
else if (a === "--background")
|
|
495
|
+
flags.background = true;
|
|
496
|
+
else if (a === "-h" || a === "--help")
|
|
497
|
+
flags.help = true;
|
|
498
|
+
else if (a === "-v" || a === "--version")
|
|
499
|
+
flags.version = true;
|
|
500
|
+
else if (a === "--only") {
|
|
501
|
+
i++;
|
|
502
|
+
if (argv[i])
|
|
503
|
+
only.push(...argv[i].split(","));
|
|
504
|
+
}
|
|
505
|
+
else if (a.startsWith("--only=")) {
|
|
506
|
+
only.push(...a.slice("--only=".length).split(","));
|
|
507
|
+
}
|
|
508
|
+
else if (a === "--target") {
|
|
509
|
+
i++;
|
|
510
|
+
if (argv[i])
|
|
511
|
+
targets.push(...parseTargetList(argv[i]));
|
|
512
|
+
}
|
|
513
|
+
else if (a.startsWith("--target=")) {
|
|
514
|
+
targets.push(...parseTargetList(a.slice("--target=".length)));
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
positional.push(a);
|
|
518
|
+
}
|
|
519
|
+
i++;
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
command: positional[0] ?? "install",
|
|
523
|
+
subcommand: positional[1],
|
|
524
|
+
flags,
|
|
525
|
+
only,
|
|
526
|
+
targets,
|
|
527
|
+
};
|
|
302
528
|
}
|
|
303
529
|
export function sha256(buf) {
|
|
304
530
|
return createHash("sha256").update(buf).digest("hex");
|
package/dist/main.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { maybeBounceToLatest } from "./bounce.js";
|
|
2
2
|
import { log, logError } from "./colors.js";
|
|
3
|
-
import {
|
|
3
|
+
import { getPackageVersion, parseArgs } from "./lib.js";
|
|
4
4
|
import { helpText } from "./ui.js";
|
|
5
5
|
import { runCheck } from "./commands/check.js";
|
|
6
6
|
import { runHookInstall, runHookUninstall } from "./commands/hook.js";
|
|
@@ -12,92 +12,9 @@ import { runStatus } from "./commands/status.js";
|
|
|
12
12
|
import { runUninstall } from "./commands/uninstall.js";
|
|
13
13
|
import { runUpdate } from "./commands/update.js";
|
|
14
14
|
import { runWhoami } from "./commands/whoami.js";
|
|
15
|
-
function parseTargetList(raw) {
|
|
16
|
-
const out = [];
|
|
17
|
-
for (const piece of raw
|
|
18
|
-
.split(",")
|
|
19
|
-
.map((s) => s.trim())
|
|
20
|
-
.filter(Boolean)) {
|
|
21
|
-
if (ALL_TARGETS.includes(piece)) {
|
|
22
|
-
out.push(piece);
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
throw new Error(`Target desconocido: '${piece}'. Válidos: ${ALL_TARGETS.join(", ")}`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return out;
|
|
29
|
-
}
|
|
30
|
-
function parseArgs(argv) {
|
|
31
|
-
const flags = {
|
|
32
|
-
yes: false,
|
|
33
|
-
dryRun: false,
|
|
34
|
-
force: false,
|
|
35
|
-
help: false,
|
|
36
|
-
version: false,
|
|
37
|
-
noHook: false,
|
|
38
|
-
noBounce: false,
|
|
39
|
-
autoUpdate: false,
|
|
40
|
-
background: false,
|
|
41
|
-
};
|
|
42
|
-
const positional = [];
|
|
43
|
-
const only = [];
|
|
44
|
-
const targets = [];
|
|
45
|
-
let i = 0;
|
|
46
|
-
while (i < argv.length) {
|
|
47
|
-
const a = argv[i];
|
|
48
|
-
if (a === "-y" || a === "--yes")
|
|
49
|
-
flags.yes = true;
|
|
50
|
-
else if (a === "--dry-run")
|
|
51
|
-
flags.dryRun = true;
|
|
52
|
-
else if (a === "--force")
|
|
53
|
-
flags.force = true;
|
|
54
|
-
else if (a === "--no-hook")
|
|
55
|
-
flags.noHook = true;
|
|
56
|
-
else if (a === "--no-bounce")
|
|
57
|
-
flags.noBounce = true;
|
|
58
|
-
else if (a === "--auto-update")
|
|
59
|
-
flags.autoUpdate = true;
|
|
60
|
-
else if (a === "--background")
|
|
61
|
-
flags.background = true;
|
|
62
|
-
else if (a === "-h" || a === "--help")
|
|
63
|
-
flags.help = true;
|
|
64
|
-
else if (a === "-v" || a === "--version")
|
|
65
|
-
flags.version = true;
|
|
66
|
-
else if (a === "--only") {
|
|
67
|
-
i++;
|
|
68
|
-
if (argv[i])
|
|
69
|
-
only.push(...argv[i].split(","));
|
|
70
|
-
}
|
|
71
|
-
else if (a.startsWith("--only=")) {
|
|
72
|
-
only.push(...a.slice("--only=".length).split(","));
|
|
73
|
-
}
|
|
74
|
-
else if (a === "--target") {
|
|
75
|
-
i++;
|
|
76
|
-
if (argv[i])
|
|
77
|
-
targets.push(...parseTargetList(argv[i]));
|
|
78
|
-
}
|
|
79
|
-
else if (a.startsWith("--target=")) {
|
|
80
|
-
targets.push(...parseTargetList(a.slice("--target=".length)));
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
positional.push(a);
|
|
84
|
-
}
|
|
85
|
-
i++;
|
|
86
|
-
}
|
|
87
|
-
return {
|
|
88
|
-
command: positional[0] ?? "install",
|
|
89
|
-
subcommand: positional[1],
|
|
90
|
-
flags,
|
|
91
|
-
only,
|
|
92
|
-
targets,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
15
|
async function main() {
|
|
96
16
|
const args = parseArgs(process.argv.slice(2));
|
|
97
|
-
if (
|
|
98
|
-
await maybeBounceToLatest(process.argv.slice(2));
|
|
99
|
-
}
|
|
100
|
-
if (args.flags.version) {
|
|
17
|
+
if (args.flags.version || args.command === "version") {
|
|
101
18
|
log(getPackageVersion());
|
|
102
19
|
process.exit(0);
|
|
103
20
|
}
|
|
@@ -105,6 +22,9 @@ async function main() {
|
|
|
105
22
|
log(helpText());
|
|
106
23
|
process.exit(0);
|
|
107
24
|
}
|
|
25
|
+
if (!args.flags.noBounce) {
|
|
26
|
+
await maybeBounceToLatest(process.argv.slice(2));
|
|
27
|
+
}
|
|
108
28
|
let exitCode = 0;
|
|
109
29
|
switch (args.command) {
|
|
110
30
|
case "login":
|
|
@@ -158,9 +78,6 @@ async function main() {
|
|
|
158
78
|
exitCode = 1;
|
|
159
79
|
}
|
|
160
80
|
break;
|
|
161
|
-
case "version":
|
|
162
|
-
log(getPackageVersion());
|
|
163
|
-
break;
|
|
164
81
|
default:
|
|
165
82
|
logError(`Comando desconocido: '${args.command}'.`);
|
|
166
83
|
log(helpText());
|
package/dist/render.js
CHANGED
|
@@ -1,76 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { extname, join } from "node:path";
|
|
3
1
|
import { stdioLaunch } from "./lazy-mcp.js";
|
|
4
|
-
import { parseFrontmatter } from "./lib.js";
|
|
5
|
-
function readSkillEntryPoint(skillDir) {
|
|
6
|
-
const candidates = ["SKILL.md", "README.md"];
|
|
7
|
-
for (const c of candidates) {
|
|
8
|
-
const p = join(skillDir, c);
|
|
9
|
-
if (existsSync(p)) {
|
|
10
|
-
return { content: readFileSync(p, "utf-8"), path: p };
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
throw new Error(`No SKILL.md ni README.md en ${skillDir}`);
|
|
14
|
-
}
|
|
15
|
-
export function renderSkillForTarget(skillSrcDir, skillName, format) {
|
|
16
|
-
if (format === "folder") {
|
|
17
|
-
return { kind: "folder" };
|
|
18
|
-
}
|
|
19
|
-
const { content } = readSkillEntryPoint(skillSrcDir);
|
|
20
|
-
const fm = parseFrontmatter(content);
|
|
21
|
-
const body = stripFrontmatter(content);
|
|
22
|
-
if (format === "cursor-mdc") {
|
|
23
|
-
const desc = fm.description ?? `Skill ${skillName}`;
|
|
24
|
-
const cursorDesc = desc.startsWith("USE WHEN") ? desc : `USE WHEN ${desc}`;
|
|
25
|
-
const safeDesc = cursorDesc.replace(/\n/g, " ").replace(/"/g, '\\"');
|
|
26
|
-
const mdc = `---\ndescription: "${safeDesc}"\nalwaysApply: false\n---\n\n${body}`;
|
|
27
|
-
return { kind: "single", filename: `${skillName}.mdc`, content: mdc };
|
|
28
|
-
}
|
|
29
|
-
if (format === "flat-md") {
|
|
30
|
-
return { kind: "single", filename: `${skillName}.md`, content: body };
|
|
31
|
-
}
|
|
32
|
-
const _exhaustive = format;
|
|
33
|
-
throw new Error(`SkillFormat no soportado: ${String(_exhaustive)}`);
|
|
34
|
-
}
|
|
35
|
-
function stripFrontmatter(content) {
|
|
36
|
-
const m = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]*)$/);
|
|
37
|
-
if (!m)
|
|
38
|
-
return content;
|
|
39
|
-
return m[1].replace(/^\s+/, "");
|
|
40
|
-
}
|
|
41
|
-
const INLINE_TEXT_EXT = new Set([".md", ".sql", ".txt", ".json", ".yaml", ".yml", ".csv"]);
|
|
42
|
-
const INLINE_MAX_BYTES = 64 * 1024;
|
|
43
|
-
const FENCE_LANG = {
|
|
44
|
-
".sql": "sql",
|
|
45
|
-
".json": "json",
|
|
46
|
-
".yaml": "yaml",
|
|
47
|
-
".yml": "yaml",
|
|
48
|
-
".csv": "csv",
|
|
49
|
-
".txt": "",
|
|
50
|
-
};
|
|
51
|
-
export function inlineReferences(refs) {
|
|
52
|
-
const sections = [];
|
|
53
|
-
for (const ref of refs) {
|
|
54
|
-
const ext = extname(ref.rel).toLowerCase();
|
|
55
|
-
const bytes = Buffer.byteLength(ref.text, "utf-8");
|
|
56
|
-
if (!INLINE_TEXT_EXT.has(ext)) {
|
|
57
|
-
sections.push(`### \`${ref.rel}\`\n\n> (omitido: tipo \`${ext || "sin extensión"}\` no inlineable)`);
|
|
58
|
-
}
|
|
59
|
-
else if (bytes > INLINE_MAX_BYTES) {
|
|
60
|
-
sections.push(`### \`${ref.rel}\`\n\n> (omitido: ${Math.round(bytes / 1024)} KB; supera el límite de inline)`);
|
|
61
|
-
}
|
|
62
|
-
else if (ext === ".md") {
|
|
63
|
-
sections.push(`### \`${ref.rel}\`\n\n${ref.text.trim()}`);
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
const lang = FENCE_LANG[ext] ?? "";
|
|
67
|
-
sections.push(`### \`${ref.rel}\`\n\n\`\`\`${lang}\n${ref.text.replace(/\s+$/, "")}\n\`\`\``);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (sections.length === 0)
|
|
71
|
-
return "";
|
|
72
|
-
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`;
|
|
73
|
-
}
|
|
74
2
|
export function renderMcpForTarget(mcp, spec) {
|
|
75
3
|
if (mcp.transport === "stdio") {
|
|
76
4
|
return renderStdioMcpForTarget(mcp, spec);
|
|
@@ -150,8 +78,3 @@ export function renderHermesMcpOps(mcp) {
|
|
|
150
78
|
}
|
|
151
79
|
return ops;
|
|
152
80
|
}
|
|
153
|
-
export function hermesManagedFields(mcp) {
|
|
154
|
-
return renderHermesMcpOps(mcp)
|
|
155
|
-
.map((op) => op.path)
|
|
156
|
-
.sort();
|
|
157
|
-
}
|
package/dist/signature.js
CHANGED
|
@@ -5,10 +5,6 @@ export const TRUSTED_KEYS = [
|
|
|
5
5
|
keyId: "alegra-data-agent-v2",
|
|
6
6
|
publicKeyB64: "MCowBQYDK2VwAyEA5nrWo7oWlhyad9rJjQHqgL0xaPZ83Yn5dJotGXxCo0c=",
|
|
7
7
|
},
|
|
8
|
-
{
|
|
9
|
-
keyId: "alegra-data-agent-v1",
|
|
10
|
-
publicKeyB64: "MCowBQYDK2VwAyEApoojQPy6INQXl/xZ0gQumsI3tt9HvMrb0PaVlNn7TkE=",
|
|
11
|
-
},
|
|
12
8
|
];
|
|
13
9
|
export const PUBLIC_KEY_B64 = TRUSTED_KEYS[0].publicKeyB64;
|
|
14
10
|
export function canonicalize(obj) {
|
|
@@ -27,6 +23,12 @@ function canonicalValue(value, isRoot) {
|
|
|
27
23
|
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalValue(o[k], false)}`).join(",")}}`;
|
|
28
24
|
}
|
|
29
25
|
const MAX_MANIFEST_AGE_DAYS = 90;
|
|
26
|
+
export function manifestTimestamp(m) {
|
|
27
|
+
if (!m || typeof m.generatedAt !== "string")
|
|
28
|
+
return null;
|
|
29
|
+
const ts = Date.parse(m.generatedAt);
|
|
30
|
+
return Number.isNaN(ts) ? null : ts;
|
|
31
|
+
}
|
|
30
32
|
export function verifyManifestSignature(manifest, options = {}) {
|
|
31
33
|
const sig = manifest.signature;
|
|
32
34
|
if (!sig) {
|
|
@@ -46,21 +48,27 @@ export function verifyManifestSignature(manifest, options = {}) {
|
|
|
46
48
|
reason: `keyId no reconocido: ${sig.keyId} (este CLI acepta ${keys.map((k) => k.keyId).join(", ")})`,
|
|
47
49
|
};
|
|
48
50
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
const ts = manifestTimestamp(manifest);
|
|
52
|
+
if (ts === null) {
|
|
53
|
+
return { ok: false, reason: "manifest sin generatedAt válido (ISO 8601)" };
|
|
54
|
+
}
|
|
55
|
+
const ageMs = Date.now() - ts;
|
|
56
|
+
const maxAgeMs = MAX_MANIFEST_AGE_DAYS * 24 * 3600 * 1000;
|
|
57
|
+
if (ageMs > maxAgeMs) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
reason: `manifest demasiado viejo (${Math.round(ageMs / (24 * 3600 * 1000))}d, máx ${MAX_MANIFEST_AGE_DAYS}d)`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (ageMs < -3600_000) {
|
|
64
|
+
return { ok: false, reason: "manifest tiene generatedAt en el futuro" };
|
|
65
|
+
}
|
|
66
|
+
const floor = options.notOlderThan;
|
|
67
|
+
if (typeof floor === "number" && Number.isFinite(floor) && ts < floor) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
reason: `manifest más viejo que el ya aceptado en esta máquina (generatedAt ${manifest.generatedAt} < ${new Date(floor).toISOString()}): posible downgrade`,
|
|
71
|
+
};
|
|
64
72
|
}
|
|
65
73
|
const canonical = canonicalize(manifest);
|
|
66
74
|
const publicKey = createPublicKey({
|
package/dist/ui.js
CHANGED
|
@@ -22,17 +22,6 @@ export async function confirm(question, defaultYes = true) {
|
|
|
22
22
|
rl.close();
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
export async function ask(question, defaultValue) {
|
|
26
|
-
const rl = createInterface({ input, output });
|
|
27
|
-
try {
|
|
28
|
-
const suffix = defaultValue ? `${dim(`(${defaultValue})`)} ` : "";
|
|
29
|
-
const answer = (await rl.question(`${question} ${suffix}`)).trim();
|
|
30
|
-
return answer === "" && defaultValue !== undefined ? defaultValue : answer;
|
|
31
|
-
}
|
|
32
|
-
finally {
|
|
33
|
-
rl.close();
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
25
|
export function helpText() {
|
|
37
26
|
return `
|
|
38
27
|
${bold(COMMAND_NAME)} — Agente de Data interno de Alegra
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alegra-data-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"README.md"
|
|
20
20
|
],
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@types/node": "26.
|
|
22
|
+
"@types/node": "26.6.3",
|
|
23
23
|
"typescript": "7.0.2"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|