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 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
- Documentación y soporte para el equipo de Alegra: canal interno de BI Data.
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
- function verifiedOrThrow(manifest) {
208
- const check = verifyManifestSignature(manifest, { softFail: false });
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
- }
@@ -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=0 ${COMMAND_NAME}@${range} ${verb} --no-bounce`;
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;
@@ -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
- logWarn(`El auto-update no pudo correr ${lines.length} vez/veces en un cliente GUI — última: "${lines[lines.length - 1]}". Agente: corré ${bold(`${COMMAND_NAME} update`)} (actualiza ya mismo y limpia este aviso); si reaparece, el cliente GUI no encuentra pnpm en su PATH y hay que reinstalar con ${bold(`${COMMAND_NAME} install`)} desde una terminal con pnpm disponible.`);
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
- }
@@ -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 = 90_000;
13
- function checkedRecently() {
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 raw = readFileSync(getLastCheckPath(), "utf-8");
16
- const at = Date.parse(JSON.parse(raw).checkedAt ?? "");
17
- if (Number.isNaN(at))
18
- return false;
19
- const age = Date.now() - at;
20
- return age >= 0 && age < BACKGROUND_COOLDOWN_MS;
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 = getLastCheckPath();
56
+ const path = getAutoUpdateLogPath();
29
57
  mkdirSync(dirname(path), { recursive: true });
30
- writeFileSync(path, `${JSON.stringify({ checkedAt: new Date().toISOString() }, null, 2)}\n`);
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(opts.background ? { timeoutMs: BACKGROUND_LOGIN_TIMEOUT_MS } : {});
85
+ const code = await runLogin({});
41
86
  if (code !== 0)
42
- return opts.background ? 0 : code;
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 { inlineReferences, renderHermesMcpOps, renderMcpForTarget, } from "./render.js";
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
- function writeSkillToTarget(skillName, staged, spec) {
124
- validateSkillName(skillName);
125
- if (spec.skillFormat === "folder") {
126
- for (const base of skillDirsFor(spec)) {
127
- const dst = join(base, skillName);
128
- if (existsSync(dst))
129
- withFsRetry(() => rmSync(dst, { recursive: true, force: true }));
130
- ensureDir(dst);
131
- for (const f of staged) {
132
- const dstFile = safeJoinSkillFile(dst, f.rel);
133
- ensureDir(dirname(dstFile));
134
- withFsRetry(() => writeFileSync(dstFile, f.data));
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
- const entry = staged.find((f) => f.rel === "SKILL.md") ?? staged.find((f) => f.rel === "README.md");
140
- if (!entry) {
141
- throw new Error(`Para target ${spec.id} se requiere SKILL.md o README.md en la skill`);
142
- }
143
- const references = staged
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 { filename: `${skillName}.md`, content: body };
188
+ return backup;
169
189
  }
170
- function parseFrontmatterMinimal(content) {
171
- const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
172
- if (!m)
173
- return {};
174
- const out = {};
175
- for (const line of m[1].split(/\r?\n/)) {
176
- const kv = line.match(/^description:\s*(.+)$/);
177
- if (kv) {
178
- out.description = kv[1].replace(/^["']|["']$/g, "").trim();
179
- return out;
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 out;
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
- const folderTarget = targets.find((t) => t.skillFormat === "folder");
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
- if (spec.skillFormat === "folder") {
1220
- for (const base of skillDirsFor(spec)) {
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
- if (prev.bundleHash !== manifest.skills[name].bundleHash) {
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 start() {
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 GATEWAY_URL = process.env.ALEGRA_DATA_AGENT_GATEWAY_URL ?? "https://alegra-data-agent.vercel.app";
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
- lock = JSON.parse(readFileSync(path, "utf-8"));
326
+ parsed = JSON.parse(readFileSync(path, "utf-8"));
327
+ ok = isLockShape(parsed);
289
328
  }
290
329
  catch {
291
- lock = emptyLock();
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 function writeLockFile(lock) {
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
- writeFileSync(path, `${JSON.stringify(lock, null, 2)}\n`, "utf-8");
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 { ALL_TARGETS, getPackageVersion } from "./lib.js";
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 (!args.flags.noBounce) {
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
- if (typeof manifest.generatedAt === "string") {
50
- const ts = Date.parse(manifest.generatedAt);
51
- if (!Number.isNaN(ts)) {
52
- const ageMs = Date.now() - ts;
53
- const maxAgeMs = MAX_MANIFEST_AGE_DAYS * 24 * 3600 * 1000;
54
- if (ageMs > maxAgeMs) {
55
- return {
56
- ok: false,
57
- reason: `manifest demasiado viejo (${Math.round(ageMs / (24 * 3600 * 1000))}d, máx ${MAX_MANIFEST_AGE_DAYS}d)`,
58
- };
59
- }
60
- if (ageMs < -3600_000) {
61
- return { ok: false, reason: "manifest tiene generatedAt en el futuro" };
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.4.4",
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.4.0",
22
+ "@types/node": "26.6.3",
23
23
  "typescript": "7.0.2"
24
24
  },
25
25
  "repository": {