alegra-data-agent 0.2.0 → 0.2.2
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 +1 -1
- package/dist/api.js +1 -1
- package/dist/auth.js +28 -2
- package/dist/bounce.js +56 -0
- package/dist/commands/check.js +1 -1
- package/dist/commands/hook.js +27 -4
- package/dist/commands/install.js +34 -19
- package/dist/commands/login.js +6 -3
- package/dist/commands/status.js +2 -2
- package/dist/commands/uninstall.js +23 -1
- package/dist/commands/update.js +33 -9
- package/dist/commands/whoami.js +1 -1
- package/dist/installer.js +114 -10
- package/dist/lib.js +19 -1
- package/dist/main.js +9 -2
- package/dist/render.js +36 -1
- package/dist/targets.js +15 -1
- package/dist/ui.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ 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
|
|
17
|
+
- Uno de: Claude Code, Cursor, OpenCode, Antigravity, Codex
|
|
18
18
|
|
|
19
19
|
## Soporte
|
|
20
20
|
|
package/dist/api.js
CHANGED
|
@@ -79,7 +79,7 @@ async function authedFetch(path, opts = {}) {
|
|
|
79
79
|
if (res.status === 401) {
|
|
80
80
|
if (opts.requireAuth === false)
|
|
81
81
|
return res;
|
|
82
|
-
throw new AuthRequiredError(`
|
|
82
|
+
throw new AuthRequiredError(`Sesión expirada. Agente: ejecutá vos mismo \`pnpm dlx ${COMMAND_NAME} login\` — al usuario solo le toca el click del navegador.`);
|
|
83
83
|
}
|
|
84
84
|
if (res.status === 403) {
|
|
85
85
|
throw new GatewayError(403, path, "Forbidden — tu cuenta no es @alegra.com o tu acceso fue revocado. " +
|
package/dist/auth.js
CHANGED
|
@@ -54,6 +54,31 @@ export function clearAuth() {
|
|
|
54
54
|
export function hasAuth() {
|
|
55
55
|
return readAuth() !== null;
|
|
56
56
|
}
|
|
57
|
+
function decodeJwtExp(jwt) {
|
|
58
|
+
const parts = jwt.split(".");
|
|
59
|
+
if (parts.length !== 3)
|
|
60
|
+
return null;
|
|
61
|
+
try {
|
|
62
|
+
const padded = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
63
|
+
const payload = JSON.parse(Buffer.from(padded, "base64").toString("utf-8"));
|
|
64
|
+
return typeof payload.exp === "number" ? payload.exp : null;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function isAuthExpired(record) {
|
|
71
|
+
if (!record)
|
|
72
|
+
return true;
|
|
73
|
+
const exp = decodeJwtExp(record.token);
|
|
74
|
+
if (exp === null)
|
|
75
|
+
return false;
|
|
76
|
+
return Date.now() >= exp * 1000 - 60_000;
|
|
77
|
+
}
|
|
78
|
+
export function hasValidAuth() {
|
|
79
|
+
const record = readAuth();
|
|
80
|
+
return record !== null && !isAuthExpired(record);
|
|
81
|
+
}
|
|
57
82
|
export async function performBrowserLogin(timeoutMs = 5 * 60 * 1000) {
|
|
58
83
|
const state = randomBytes(24).toString("hex");
|
|
59
84
|
return new Promise((resolve, reject) => {
|
|
@@ -199,6 +224,7 @@ function openBrowser(url) {
|
|
|
199
224
|
args: ["url.dll,FileProtocolHandler", url],
|
|
200
225
|
});
|
|
201
226
|
candidates.push({ cmd: "cmd.exe", args: ["/c", "start", '""', `"${url}"`] });
|
|
227
|
+
candidates.push({ cmd: "wslview", args: [url] });
|
|
202
228
|
candidates.push({ cmd: "xdg-open", args: [url] });
|
|
203
229
|
}
|
|
204
230
|
else {
|
|
@@ -226,8 +252,8 @@ function htmlPage(kind, subject) {
|
|
|
226
252
|
const color = ok ? "#10b981" : "#ef4444";
|
|
227
253
|
const title = ok ? "Autenticación exitosa" : "Error";
|
|
228
254
|
const body = ok
|
|
229
|
-
? `Conectaste como <strong>${escapeHtml(subject)}</strong>.<br/>Ya puedes cerrar esta pestaña y volver
|
|
230
|
-
: `<strong>${escapeHtml(subject)}</strong><br/>
|
|
255
|
+
? `Conectaste como <strong>${escapeHtml(subject)}</strong>.<br/>Ya puedes cerrar esta pestaña y volver a tu agente.`
|
|
256
|
+
: `<strong>${escapeHtml(subject)}</strong><br/>Volvé a tu agente e intentá de nuevo — él se encarga.`;
|
|
231
257
|
return `<!doctype html>
|
|
232
258
|
<html lang="es">
|
|
233
259
|
<head>
|
package/dist/bounce.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { COMMAND_NAME, getPackageVersion } from "./lib.js";
|
|
3
|
+
export const NO_BOUNCE_ENV = "ALEGRA_DATA_AGENT_NO_BOUNCE";
|
|
4
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${COMMAND_NAME}`;
|
|
5
|
+
const FETCH_TIMEOUT_MS = 2_500;
|
|
6
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
7
|
+
export function compareSemver(a, b) {
|
|
8
|
+
const pa = a.split(".").map((n) => Number.parseInt(n, 10));
|
|
9
|
+
const pb = b.split(".").map((n) => Number.parseInt(n, 10));
|
|
10
|
+
for (let i = 0; i < 3; i++) {
|
|
11
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
12
|
+
if (diff !== 0)
|
|
13
|
+
return diff;
|
|
14
|
+
}
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
export function shouldBounce(current, latest) {
|
|
18
|
+
if (!latest)
|
|
19
|
+
return false;
|
|
20
|
+
if (!SEMVER_RE.test(latest))
|
|
21
|
+
return false;
|
|
22
|
+
if (!SEMVER_RE.test(current))
|
|
23
|
+
return false;
|
|
24
|
+
return compareSemver(latest, current) > 0;
|
|
25
|
+
}
|
|
26
|
+
async function fetchLatestVersion() {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(REGISTRY_URL, {
|
|
29
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
30
|
+
headers: { accept: "application/vnd.npm.install-v1+json" },
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok)
|
|
33
|
+
return null;
|
|
34
|
+
const data = (await res.json());
|
|
35
|
+
return data["dist-tags"]?.latest ?? null;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function maybeBounceToLatest(argv) {
|
|
42
|
+
if (process.env[NO_BOUNCE_ENV] === "1")
|
|
43
|
+
return;
|
|
44
|
+
const current = getPackageVersion();
|
|
45
|
+
const latest = await fetchLatestVersion();
|
|
46
|
+
if (!shouldBounce(current, latest))
|
|
47
|
+
return;
|
|
48
|
+
const child = spawnSync("pnpm", ["dlx", "--config.minimumReleaseAge=0", `${COMMAND_NAME}@${latest}`, ...argv], {
|
|
49
|
+
stdio: "inherit",
|
|
50
|
+
env: { ...process.env, [NO_BOUNCE_ENV]: "1" },
|
|
51
|
+
shell: process.platform === "win32",
|
|
52
|
+
});
|
|
53
|
+
if (child.error)
|
|
54
|
+
return;
|
|
55
|
+
process.exit(child.status ?? 0);
|
|
56
|
+
}
|
package/dist/commands/check.js
CHANGED
|
@@ -24,7 +24,7 @@ export async function runCheck() {
|
|
|
24
24
|
lines.push(` ${cyan("↻")} ${name} ${dim("(versión nueva)")}`);
|
|
25
25
|
}
|
|
26
26
|
lines.push("");
|
|
27
|
-
lines.push(` ${dim("
|
|
27
|
+
lines.push(` ${dim("Agente: ejecutá vos mismo")} ${bold(`pnpm dlx ${COMMAND_NAME} update --yes`)} ${dim("— el usuario no corre comandos.")}`);
|
|
28
28
|
lines.push("");
|
|
29
29
|
process.stdout.write(lines.join("\n"));
|
|
30
30
|
return 0;
|
package/dist/commands/hook.js
CHANGED
|
@@ -8,7 +8,7 @@ function hookCommandFor(mode) {
|
|
|
8
8
|
const minor = parts[1] ?? "0";
|
|
9
9
|
const range = `~${major}.${minor}.0`;
|
|
10
10
|
const verb = mode === "auto" ? "update --yes" : "check";
|
|
11
|
-
return `pnpm dlx ${COMMAND_NAME}@${range} ${verb}`;
|
|
11
|
+
return `pnpm dlx --config.minimumReleaseAge=0 ${COMMAND_NAME}@${range} ${verb} --no-bounce`;
|
|
12
12
|
}
|
|
13
13
|
const HOOK_SIGNATURE = COMMAND_NAME;
|
|
14
14
|
function readSettings() {
|
|
@@ -19,7 +19,7 @@ function readSettings() {
|
|
|
19
19
|
return JSON.parse(readFileSync(path, "utf-8"));
|
|
20
20
|
}
|
|
21
21
|
catch {
|
|
22
|
-
logError(`No se pudo parsear ${path}.
|
|
22
|
+
logError(`No se pudo parsear ${path}. Agente: reparalo vos mismo (la skill agent-onboarding tiene la guía) — no le pidas al usuario editar JSON.`);
|
|
23
23
|
return {};
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -41,7 +41,7 @@ function isOurMatcher(m) {
|
|
|
41
41
|
function buildOurMatcher(command) {
|
|
42
42
|
return { hooks: [{ type: "command", command }] };
|
|
43
43
|
}
|
|
44
|
-
export function runHookInstall(mode = "
|
|
44
|
+
export function runHookInstall(mode = "auto") {
|
|
45
45
|
const settings = readSettings();
|
|
46
46
|
settings.hooks = settings.hooks ?? {};
|
|
47
47
|
const command = hookCommandFor(mode);
|
|
@@ -49,7 +49,7 @@ export function runHookInstall(mode = "check") {
|
|
|
49
49
|
const compact = process.env.CLAUDECODE === "1" || process.env.CLAUDE_CODE === "1";
|
|
50
50
|
if (typeof existing === "string") {
|
|
51
51
|
if (!existing.includes(HOOK_SIGNATURE)) {
|
|
52
|
-
logWarn(`Ya hay un SessionStart hook
|
|
52
|
+
logWarn(`Ya hay un SessionStart hook ajeno en formato string legacy:\n ${existing}\n No lo sobrescribo. Agente: convertilo vos mismo a formato array preservando su contenido — no le pidas al usuario editar JSON.`);
|
|
53
53
|
return 1;
|
|
54
54
|
}
|
|
55
55
|
settings.hooks.SessionStart = [buildOurMatcher(command)];
|
|
@@ -82,6 +82,29 @@ export function runHookInstall(mode = "check") {
|
|
|
82
82
|
}
|
|
83
83
|
return 0;
|
|
84
84
|
}
|
|
85
|
+
export function refreshHookCommand() {
|
|
86
|
+
try {
|
|
87
|
+
const settings = readSettings();
|
|
88
|
+
if (!settings.hooks)
|
|
89
|
+
return;
|
|
90
|
+
const existing = settings.hooks.SessionStart;
|
|
91
|
+
if (!Array.isArray(existing))
|
|
92
|
+
return;
|
|
93
|
+
const ours = existing.find(isOurMatcher);
|
|
94
|
+
if (!ours)
|
|
95
|
+
return;
|
|
96
|
+
const current = ours.hooks.find((h) => h.type === "command")?.command ?? "";
|
|
97
|
+
const fresh = hookCommandFor("auto");
|
|
98
|
+
if (current === fresh)
|
|
99
|
+
return;
|
|
100
|
+
const others = existing.filter((m) => !isOurMatcher(m));
|
|
101
|
+
others.push(buildOurMatcher(fresh));
|
|
102
|
+
settings.hooks.SessionStart = others;
|
|
103
|
+
writeSettings(settings);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
}
|
|
107
|
+
}
|
|
85
108
|
export function runHookUninstall() {
|
|
86
109
|
const settings = readSettings();
|
|
87
110
|
const existing = settings.hooks?.SessionStart;
|
package/dist/commands/install.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuthRequiredError } from "../api.js";
|
|
2
|
-
import {
|
|
2
|
+
import { hasValidAuth, readAuth } from "../auth.js";
|
|
3
3
|
import { bold, cyan, dim, gray, green, log, logError, red, yellow } from "../colors.js";
|
|
4
4
|
import { installAll } from "../installer.js";
|
|
5
5
|
import { COMMAND_NAME, formatBytes, getClaudeSkillsDir, getPackageVersion } from "../lib.js";
|
|
@@ -147,9 +147,9 @@ export async function runInstall(opts = {}) {
|
|
|
147
147
|
const compact = isClaudeCode();
|
|
148
148
|
if (!compact)
|
|
149
149
|
fancyBanner();
|
|
150
|
-
if (!
|
|
150
|
+
if (!hasValidAuth()) {
|
|
151
151
|
if (!compact)
|
|
152
|
-
log(dim(" No hay sesión activa. Inicio el
|
|
152
|
+
log(dim(" No hay sesión activa (o expiró). Inicio el login antes de instalar.\n"));
|
|
153
153
|
const code = await runLogin({});
|
|
154
154
|
if (code !== 0)
|
|
155
155
|
return code;
|
|
@@ -166,21 +166,13 @@ export async function runInstall(opts = {}) {
|
|
|
166
166
|
if (!compact) {
|
|
167
167
|
section("Descargando skills del gateway");
|
|
168
168
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
catch (err) {
|
|
179
|
-
if (err instanceof AuthRequiredError) {
|
|
180
|
-
logError(err.message);
|
|
181
|
-
log(dim(` Corre: ${COMMAND_NAME} login`));
|
|
182
|
-
return 2;
|
|
183
|
-
}
|
|
169
|
+
const installOpts = {
|
|
170
|
+
dryRun: opts.dryRun,
|
|
171
|
+
force: opts.force,
|
|
172
|
+
only: opts.only,
|
|
173
|
+
targets: opts.targets,
|
|
174
|
+
};
|
|
175
|
+
const reportInstallError = (err) => {
|
|
184
176
|
const msg = err instanceof Error ? err.message : String(err);
|
|
185
177
|
logError(msg);
|
|
186
178
|
if (msg.includes("EACCES") || msg.includes("permission denied")) {
|
|
@@ -195,6 +187,29 @@ export async function runInstall(opts = {}) {
|
|
|
195
187
|
log(dim(" Verifica tu conexión a internet y el estado del gateway."));
|
|
196
188
|
}
|
|
197
189
|
return 1;
|
|
190
|
+
};
|
|
191
|
+
let result;
|
|
192
|
+
try {
|
|
193
|
+
result = await installAll(installOpts);
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
if (!(err instanceof AuthRequiredError))
|
|
197
|
+
return reportInstallError(err);
|
|
198
|
+
if (!compact)
|
|
199
|
+
log(dim(" La sesión expiró durante la sincronización; reabro el login y reintento.\n"));
|
|
200
|
+
const code = await runLogin({ force: true });
|
|
201
|
+
if (code !== 0)
|
|
202
|
+
return code;
|
|
203
|
+
try {
|
|
204
|
+
result = await installAll(installOpts);
|
|
205
|
+
}
|
|
206
|
+
catch (err2) {
|
|
207
|
+
if (err2 instanceof AuthRequiredError) {
|
|
208
|
+
logError(err2.message);
|
|
209
|
+
return 2;
|
|
210
|
+
}
|
|
211
|
+
return reportInstallError(err2);
|
|
212
|
+
}
|
|
198
213
|
}
|
|
199
214
|
if ("error" in result) {
|
|
200
215
|
logError(result.error);
|
|
@@ -258,7 +273,7 @@ function printCompactSummary(results, manifest, email) {
|
|
|
258
273
|
.map((r) => r.name)
|
|
259
274
|
.sort()
|
|
260
275
|
.join(", "))}`);
|
|
261
|
-
log(` ${gray("destino:")} ${cyan(getClaudeSkillsDir())}${gray(" · ")}${bold(yellow("reinicia tu cliente para
|
|
276
|
+
log(` ${gray("destino:")} ${cyan(getClaudeSkillsDir())}${gray(" · ")}${bold(yellow("reinicia tu cliente para activar skills y MCPs"))}`);
|
|
262
277
|
if (unchanged > 0 && installed === 0 && updated === 0) {
|
|
263
278
|
log(` ${dim(`(${unchanged} sin cambios desde la última instalación)`)}`);
|
|
264
279
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
import { performBrowserLogin, readAuth, writeAuth } from "../auth.js";
|
|
2
|
-
import { bold, dim, green, log, logError } from "../colors.js";
|
|
1
|
+
import { isAuthExpired, performBrowserLogin, readAuth, writeAuth } from "../auth.js";
|
|
2
|
+
import { bold, dim, green, log, logError, logWarn } from "../colors.js";
|
|
3
3
|
import { GATEWAY_URL, getAuthFilePath } from "../lib.js";
|
|
4
4
|
import { banner } from "../ui.js";
|
|
5
5
|
export async function runLogin(opts = {}) {
|
|
6
6
|
banner();
|
|
7
7
|
if (!opts.force) {
|
|
8
8
|
const existing = readAuth();
|
|
9
|
-
if (existing) {
|
|
9
|
+
if (existing && !isAuthExpired(existing)) {
|
|
10
10
|
log(` ${green("✓")} Ya estás autenticado como ${bold(existing.email)}.`);
|
|
11
11
|
log(dim(` Token: ${getAuthFilePath()}`));
|
|
12
12
|
log(dim(" Para volver a iniciar sesión: --force"));
|
|
13
13
|
log("");
|
|
14
14
|
return 0;
|
|
15
15
|
}
|
|
16
|
+
if (existing) {
|
|
17
|
+
logWarn(`Tu sesión (${existing.email}) expiró — reabro el login.`);
|
|
18
|
+
}
|
|
16
19
|
}
|
|
17
20
|
log(` Gateway: ${dim(GATEWAY_URL)}`);
|
|
18
21
|
try {
|
package/dist/commands/status.js
CHANGED
|
@@ -48,10 +48,10 @@ export async function runStatus() {
|
|
|
48
48
|
log("");
|
|
49
49
|
const total = drift.toInstall.length + drift.toUpdate.length;
|
|
50
50
|
if (total > 0) {
|
|
51
|
-
logWarn(`Hay ${total} skill(s) por actualizar.
|
|
51
|
+
logWarn(`Hay ${total} skill(s) por actualizar. Agente: ejecutá vos mismo ${bold(`pnpm dlx ${COMMAND_NAME} update --yes`)} — el usuario no corre comandos.`);
|
|
52
52
|
}
|
|
53
53
|
else if (Object.keys(lock.skills).length === 0) {
|
|
54
|
-
logWarn(`Aún no hay skills instaladas.
|
|
54
|
+
logWarn(`Aún no hay skills instaladas. Agente: ejecutá vos mismo ${bold(`pnpm dlx ${COMMAND_NAME}`)} — el usuario no corre comandos.`);
|
|
55
55
|
}
|
|
56
56
|
else {
|
|
57
57
|
log(` ${green("Todo al día.")}`);
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import { dim, gray, log, logOk, logWarn, red } from "../colors.js";
|
|
2
5
|
import { uninstallAll } from "../installer.js";
|
|
3
6
|
import { readLockFile } from "../lib.js";
|
|
@@ -22,7 +25,26 @@ export async function runUninstall(opts = {}) {
|
|
|
22
25
|
}
|
|
23
26
|
}
|
|
24
27
|
const results = uninstallAll({ dryRun: opts.dryRun });
|
|
28
|
+
if (!opts.dryRun)
|
|
29
|
+
removeNpmrcCooldownExclude();
|
|
25
30
|
log("");
|
|
26
|
-
|
|
31
|
+
if (opts.dryRun) {
|
|
32
|
+
logOk(`${results.length} skill(s) se eliminarían (dry-run, nada borrado).`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
logOk(`${results.length} skill(s) eliminadas.`);
|
|
36
|
+
}
|
|
27
37
|
return 0;
|
|
28
38
|
}
|
|
39
|
+
function removeNpmrcCooldownExclude() {
|
|
40
|
+
const npmrc = join(homedir(), ".npmrc");
|
|
41
|
+
if (!existsSync(npmrc))
|
|
42
|
+
return;
|
|
43
|
+
const EXCLUDE = "minimum-release-age-exclude[]=alegra-data-agent";
|
|
44
|
+
const lines = readFileSync(npmrc, "utf-8").split("\n");
|
|
45
|
+
const kept = lines.filter((l) => l.trim() !== EXCLUDE);
|
|
46
|
+
if (kept.length !== lines.length) {
|
|
47
|
+
writeFileSync(npmrc, kept.join("\n"));
|
|
48
|
+
logOk("Removido minimum-release-age-exclude de ~/.npmrc");
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/commands/update.js
CHANGED
|
@@ -1,21 +1,42 @@
|
|
|
1
1
|
import { AuthRequiredError, fetchManifest } from "../api.js";
|
|
2
|
+
import { hasValidAuth } from "../auth.js";
|
|
2
3
|
import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
|
|
3
4
|
import { detectDrift, installAll, summarize } from "../installer.js";
|
|
4
5
|
import { banner } from "../ui.js";
|
|
6
|
+
import { refreshHookCommand } from "./hook.js";
|
|
7
|
+
import { runLogin } from "./login.js";
|
|
5
8
|
export async function runUpdate(opts = {}) {
|
|
6
9
|
banner();
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
if (!hasValidAuth()) {
|
|
11
|
+
const code = await runLogin({});
|
|
12
|
+
if (code !== 0)
|
|
13
|
+
return code;
|
|
10
14
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return
|
|
15
|
+
let reauthed = false;
|
|
16
|
+
for (;;) {
|
|
17
|
+
try {
|
|
18
|
+
return await runUpdateOnce(opts);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
if (err instanceof AuthRequiredError && !reauthed) {
|
|
22
|
+
reauthed = true;
|
|
23
|
+
log(dim(" La sesión expiró durante la actualización; reabro el login y reintento.\n"));
|
|
24
|
+
const code = await runLogin({ force: true });
|
|
25
|
+
if (code !== 0)
|
|
26
|
+
return code;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (err instanceof AuthRequiredError) {
|
|
30
|
+
logError(err.message);
|
|
31
|
+
return 2;
|
|
32
|
+
}
|
|
33
|
+
logError(err instanceof Error ? err.message : String(err));
|
|
34
|
+
return 1;
|
|
15
35
|
}
|
|
16
|
-
logError(err instanceof Error ? err.message : String(err));
|
|
17
|
-
return 1;
|
|
18
36
|
}
|
|
37
|
+
}
|
|
38
|
+
async function runUpdateOnce(opts) {
|
|
39
|
+
const manifest = await fetchManifest();
|
|
19
40
|
if (!manifest) {
|
|
20
41
|
logError("No se pudo cargar el manifest.");
|
|
21
42
|
return 1;
|
|
@@ -29,6 +50,8 @@ export async function runUpdate(opts = {}) {
|
|
|
29
50
|
];
|
|
30
51
|
if (pending.length === 0) {
|
|
31
52
|
log(green(" ✓ Todo al día (skills + MCPs).\n"));
|
|
53
|
+
if (!opts.dryRun)
|
|
54
|
+
refreshHookCommand();
|
|
32
55
|
return 0;
|
|
33
56
|
}
|
|
34
57
|
log(cyan(` → ${pending.length} elemento(s) por actualizar:\n`));
|
|
@@ -51,6 +74,7 @@ export async function runUpdate(opts = {}) {
|
|
|
51
74
|
return 1;
|
|
52
75
|
}
|
|
53
76
|
summarize(result.results);
|
|
77
|
+
refreshHookCommand();
|
|
54
78
|
const failed = result.results.some((r) => r.status === "failed");
|
|
55
79
|
return failed ? 1 : 0;
|
|
56
80
|
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -4,7 +4,7 @@ import { COMMAND_NAME, getAuthFilePath } from "../lib.js";
|
|
|
4
4
|
export function runWhoami() {
|
|
5
5
|
const auth = readAuth();
|
|
6
6
|
if (!auth) {
|
|
7
|
-
logWarn(`No autenticado.
|
|
7
|
+
logWarn(`No autenticado. Agente: ejecutá vos mismo \`pnpm dlx ${COMMAND_NAME} login\` — al usuario solo le toca el click del navegador.`);
|
|
8
8
|
return 1;
|
|
9
9
|
}
|
|
10
10
|
log("");
|
package/dist/installer.js
CHANGED
|
@@ -4,7 +4,7 @@ import { dirname, join, resolve, sep } from "node:path";
|
|
|
4
4
|
import { fetchManifest, fetchSkillFile } from "./api.js";
|
|
5
5
|
import { dim, gray, green, logError, red, yellow } from "./colors.js";
|
|
6
6
|
import { ensureDir, getLockFilePath, getPackageVersion, readLockFile, sha256File, writeLockFile, } from "./lib.js";
|
|
7
|
-
import { renderMcpForTarget } from "./render.js";
|
|
7
|
+
import { inlineReferences, renderMcpForTarget } from "./render.js";
|
|
8
8
|
import { TARGETS, detectInstalledTargets } from "./targets.js";
|
|
9
9
|
const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
10
10
|
const MCP_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
@@ -32,6 +32,14 @@ function validateRelPath(rel) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
function mcpConfigHash(mcp) {
|
|
36
|
+
const salient = JSON.stringify({
|
|
37
|
+
url: mcp.url,
|
|
38
|
+
type: mcp.auth.type,
|
|
39
|
+
tokenEnv: mcp.auth.tokenEnv ?? null,
|
|
40
|
+
});
|
|
41
|
+
return createHash("sha256").update(salient).digest("hex");
|
|
42
|
+
}
|
|
35
43
|
function safeJoinSkillFile(skillRoot, rel) {
|
|
36
44
|
const candidate = resolve(skillRoot, rel);
|
|
37
45
|
const root = resolve(skillRoot);
|
|
@@ -95,17 +103,21 @@ function writeSkillToTarget(skillName, staged, spec) {
|
|
|
95
103
|
if (!entry) {
|
|
96
104
|
throw new Error(`Para target ${spec.id} se requiere SKILL.md o README.md en la skill`);
|
|
97
105
|
}
|
|
106
|
+
const references = staged
|
|
107
|
+
.filter((f) => f.rel !== "SKILL.md" && f.rel !== "README.md")
|
|
108
|
+
.map((f) => ({ rel: f.rel, text: new TextDecoder().decode(f.data) }));
|
|
98
109
|
const content = new TextDecoder().decode(entry.data);
|
|
99
|
-
const rendered = renderSkillForTargetFromContent(content, skillName, spec.skillFormat);
|
|
110
|
+
const rendered = renderSkillForTargetFromContent(content, skillName, spec.skillFormat, references);
|
|
100
111
|
ensureDir(spec.skillsDir);
|
|
101
112
|
const dst = join(spec.skillsDir, rendered.filename);
|
|
102
113
|
atomicWriteFile(dst, rendered.content);
|
|
103
114
|
}
|
|
104
|
-
function renderSkillForTargetFromContent(content, skillName, format) {
|
|
115
|
+
function renderSkillForTargetFromContent(content, skillName, format, references = []) {
|
|
105
116
|
if (format === "folder") {
|
|
106
117
|
throw new Error("renderSkillForTargetFromContent no soporta 'folder'");
|
|
107
118
|
}
|
|
108
119
|
const stripped = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").replace(/^\s+/, "");
|
|
120
|
+
const body = stripped + inlineReferences(references);
|
|
109
121
|
const fm = parseFrontmatterMinimal(content);
|
|
110
122
|
if (format === "cursor-mdc") {
|
|
111
123
|
const desc = fm.description ?? `Skill ${skillName}`;
|
|
@@ -113,10 +125,10 @@ function renderSkillForTargetFromContent(content, skillName, format) {
|
|
|
113
125
|
const safeDesc = cursorDesc.replace(/\n/g, " ").replace(/"/g, '\\"');
|
|
114
126
|
return {
|
|
115
127
|
filename: `${skillName}.mdc`,
|
|
116
|
-
content: `---\ndescription: "${safeDesc}"\nalwaysApply: false\n---\n\n${
|
|
128
|
+
content: `---\ndescription: "${safeDesc}"\nalwaysApply: false\n---\n\n${body}`,
|
|
117
129
|
};
|
|
118
130
|
}
|
|
119
|
-
return { filename: `${skillName}.md`, content:
|
|
131
|
+
return { filename: `${skillName}.md`, content: body };
|
|
120
132
|
}
|
|
121
133
|
function parseFrontmatterMinimal(content) {
|
|
122
134
|
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -151,11 +163,15 @@ function readJsonOrEmpty(path) {
|
|
|
151
163
|
return JSON.parse(readFileSync(path, "utf-8"));
|
|
152
164
|
}
|
|
153
165
|
catch {
|
|
154
|
-
throw new Error(`${path} no es JSON válido.
|
|
166
|
+
throw new Error(`${path} no es JSON válido. Agente: respaldalo vos mismo (renombralo a .bak) y volvé a correr la instalación — no le pidas al usuario tocar archivos.`);
|
|
155
167
|
}
|
|
156
168
|
}
|
|
157
169
|
function registerMcpInTarget(mcp, spec) {
|
|
158
170
|
validateMcpName(mcp.name);
|
|
171
|
+
if (spec.mcpMechanism === "toml-block") {
|
|
172
|
+
registerMcpInTomlBlock(mcp, spec);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
159
175
|
const cfg = readJsonOrEmpty(spec.mcpConfigPath);
|
|
160
176
|
const section = cfg[spec.mcpRootKey] ?? {};
|
|
161
177
|
section[mcp.name] = renderMcpForTarget(mcp, spec);
|
|
@@ -164,6 +180,9 @@ function registerMcpInTarget(mcp, spec) {
|
|
|
164
180
|
atomicWriteFile(spec.mcpConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
165
181
|
}
|
|
166
182
|
function unregisterMcpInTarget(mcpName, spec) {
|
|
183
|
+
if (spec.mcpMechanism === "toml-block") {
|
|
184
|
+
return unregisterMcpInTomlBlock(mcpName, spec);
|
|
185
|
+
}
|
|
167
186
|
if (!existsSync(spec.mcpConfigPath))
|
|
168
187
|
return false;
|
|
169
188
|
const cfg = readJsonOrEmpty(spec.mcpConfigPath);
|
|
@@ -176,6 +195,77 @@ function unregisterMcpInTarget(mcpName, spec) {
|
|
|
176
195
|
atomicWriteFile(spec.mcpConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
177
196
|
return true;
|
|
178
197
|
}
|
|
198
|
+
function tomlBasicString(s) {
|
|
199
|
+
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
200
|
+
}
|
|
201
|
+
export function renderMcpToml(mcp) {
|
|
202
|
+
const lines = [`[mcp_servers.${mcp.name}]`, `url = ${tomlBasicString(mcp.url)}`];
|
|
203
|
+
if (mcp.auth.type === "gateway") {
|
|
204
|
+
if (!mcp.auth.tokenEnv) {
|
|
205
|
+
throw new Error(`MCP '${mcp.name}' es type=gateway pero no trae tokenEnv`);
|
|
206
|
+
}
|
|
207
|
+
lines.push(`bearer_token_env_var = ${tomlBasicString(mcp.auth.tokenEnv)}`);
|
|
208
|
+
}
|
|
209
|
+
return lines.join("\n");
|
|
210
|
+
}
|
|
211
|
+
function tomlMarkers(name) {
|
|
212
|
+
return {
|
|
213
|
+
begin: `# >>> alegra-data-agent:${name} >>>`,
|
|
214
|
+
end: `# <<< alegra-data-agent:${name} <<<`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
export function upsertTomlBlock(content, name, block) {
|
|
218
|
+
const { begin, end } = tomlMarkers(name);
|
|
219
|
+
const fenced = `${begin}\n${block}\n${end}`;
|
|
220
|
+
const bi = content.indexOf(begin);
|
|
221
|
+
if (bi !== -1) {
|
|
222
|
+
const ei = content.indexOf(end, bi);
|
|
223
|
+
if (ei !== -1) {
|
|
224
|
+
return content.slice(0, bi) + fenced + content.slice(ei + end.length);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (content.length === 0)
|
|
228
|
+
return `${fenced}\n`;
|
|
229
|
+
const sep = content.endsWith("\n\n") ? "" : content.endsWith("\n") ? "\n" : "\n\n";
|
|
230
|
+
return `${content}${sep}${fenced}\n`;
|
|
231
|
+
}
|
|
232
|
+
export function removeTomlBlock(content, name) {
|
|
233
|
+
const { begin, end } = tomlMarkers(name);
|
|
234
|
+
const bi = content.indexOf(begin);
|
|
235
|
+
if (bi === -1)
|
|
236
|
+
return content;
|
|
237
|
+
const ei = content.indexOf(end, bi);
|
|
238
|
+
if (ei === -1)
|
|
239
|
+
return content;
|
|
240
|
+
let start = bi;
|
|
241
|
+
let stop = ei + end.length;
|
|
242
|
+
if (content[stop] === "\n")
|
|
243
|
+
stop++;
|
|
244
|
+
if (start > 0 && content[start - 1] === "\n")
|
|
245
|
+
start--;
|
|
246
|
+
return content.slice(0, start) + content.slice(stop);
|
|
247
|
+
}
|
|
248
|
+
function registerMcpInTomlBlock(mcp, spec) {
|
|
249
|
+
const existing = existsSync(spec.mcpConfigPath)
|
|
250
|
+
? readFileSync(spec.mcpConfigPath, "utf-8")
|
|
251
|
+
: "";
|
|
252
|
+
const next = upsertTomlBlock(existing, mcp.name, renderMcpToml(mcp));
|
|
253
|
+
if (next === existing)
|
|
254
|
+
return;
|
|
255
|
+
backupIfExists(spec.mcpConfigPath);
|
|
256
|
+
atomicWriteFile(spec.mcpConfigPath, next);
|
|
257
|
+
}
|
|
258
|
+
function unregisterMcpInTomlBlock(name, spec) {
|
|
259
|
+
if (!existsSync(spec.mcpConfigPath))
|
|
260
|
+
return false;
|
|
261
|
+
const existing = readFileSync(spec.mcpConfigPath, "utf-8");
|
|
262
|
+
const next = removeTomlBlock(existing, name);
|
|
263
|
+
if (next === existing)
|
|
264
|
+
return false;
|
|
265
|
+
backupIfExists(spec.mcpConfigPath);
|
|
266
|
+
atomicWriteFile(spec.mcpConfigPath, next);
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
179
269
|
function resolveTargets(opts) {
|
|
180
270
|
const ids = opts.targets?.length ? opts.targets : detectInstalledTargets();
|
|
181
271
|
if (ids.length === 0) {
|
|
@@ -274,7 +364,7 @@ export function installMcp(mcp, opts = {}) {
|
|
|
274
364
|
const targetsMatch = prev?.targets &&
|
|
275
365
|
prev.targets.length === targetIds.length &&
|
|
276
366
|
prev.targets.every((t) => targetIds.includes(t));
|
|
277
|
-
if (prev && targetsMatch && !opts.force) {
|
|
367
|
+
if (prev && targetsMatch && prev.configHash === mcpConfigHash(mcp) && !opts.force) {
|
|
278
368
|
return { name: mcp.name, kind: "mcp", status: "unchanged", targets: targetIds };
|
|
279
369
|
}
|
|
280
370
|
if (opts.dryRun) {
|
|
@@ -293,6 +383,7 @@ export function installMcp(mcp, opts = {}) {
|
|
|
293
383
|
name: mcp.name,
|
|
294
384
|
installedFrom: getPackageVersion(),
|
|
295
385
|
installedAt: new Date().toISOString(),
|
|
386
|
+
configHash: mcpConfigHash(mcp),
|
|
296
387
|
targets: targetIds,
|
|
297
388
|
};
|
|
298
389
|
writeLockFile(lock);
|
|
@@ -399,8 +490,18 @@ export function uninstallMcp(name, opts = {}) {
|
|
|
399
490
|
}
|
|
400
491
|
const targetIds = (prev.targets ?? ["claude-code"]);
|
|
401
492
|
if (!opts.dryRun) {
|
|
402
|
-
|
|
403
|
-
|
|
493
|
+
try {
|
|
494
|
+
for (const tId of targetIds) {
|
|
495
|
+
unregisterMcpInTarget(name, TARGETS[tId]);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
return {
|
|
500
|
+
name,
|
|
501
|
+
kind: "mcp",
|
|
502
|
+
status: "failed",
|
|
503
|
+
error: err instanceof Error ? err.message : String(err),
|
|
504
|
+
};
|
|
404
505
|
}
|
|
405
506
|
delete lock.mcps[name];
|
|
406
507
|
writeLockFile(lock);
|
|
@@ -447,8 +548,11 @@ export function detectDrift(manifest) {
|
|
|
447
548
|
};
|
|
448
549
|
const manifestMcps = manifest.mcps ?? {};
|
|
449
550
|
for (const name of Object.keys(manifestMcps)) {
|
|
450
|
-
|
|
551
|
+
const prevMcp = lock.mcps?.[name];
|
|
552
|
+
if (!prevMcp)
|
|
451
553
|
mcps.toInstall.push(name);
|
|
554
|
+
else if (prevMcp.configHash !== mcpConfigHash(manifestMcps[name]))
|
|
555
|
+
mcps.toUpdate.push(name);
|
|
452
556
|
else
|
|
453
557
|
mcps.unchanged.push(name);
|
|
454
558
|
}
|
package/dist/lib.js
CHANGED
|
@@ -8,7 +8,13 @@ export const COMMAND_NAME = "alegra-data-agent";
|
|
|
8
8
|
export const REPO_SLUG = "alegradev/alegra-data-agent";
|
|
9
9
|
export const REPO_DEFAULT_BRANCH = "main";
|
|
10
10
|
export const GATEWAY_URL = process.env.ALEGRA_DATA_AGENT_GATEWAY_URL ?? "https://alegra-data-agent.vercel.app";
|
|
11
|
-
export const ALL_TARGETS = [
|
|
11
|
+
export const ALL_TARGETS = [
|
|
12
|
+
"claude-code",
|
|
13
|
+
"cursor",
|
|
14
|
+
"opencode",
|
|
15
|
+
"antigravity",
|
|
16
|
+
"codex",
|
|
17
|
+
];
|
|
12
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
19
|
export function getPackageRoot() {
|
|
14
20
|
return join(__dirname, "..");
|
|
@@ -47,6 +53,14 @@ export function getAntigravitySkillsDir() {
|
|
|
47
53
|
export function getAntigravitySettingsPath() {
|
|
48
54
|
return join(homedir(), ".antigravity", "settings.json");
|
|
49
55
|
}
|
|
56
|
+
export function getCodexSkillsDir() {
|
|
57
|
+
return join(homedir(), ".agents", "skills");
|
|
58
|
+
}
|
|
59
|
+
export function getCodexConfigPath() {
|
|
60
|
+
const codexHome = process.env.CODEX_HOME;
|
|
61
|
+
const base = codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
|
|
62
|
+
return join(base, "config.toml");
|
|
63
|
+
}
|
|
50
64
|
export function getTargetSkillsDir(target) {
|
|
51
65
|
switch (target) {
|
|
52
66
|
case "claude-code":
|
|
@@ -57,6 +71,8 @@ export function getTargetSkillsDir(target) {
|
|
|
57
71
|
return getOpenCodeSkillsDir();
|
|
58
72
|
case "antigravity":
|
|
59
73
|
return getAntigravitySkillsDir();
|
|
74
|
+
case "codex":
|
|
75
|
+
return getCodexSkillsDir();
|
|
60
76
|
}
|
|
61
77
|
}
|
|
62
78
|
export function getTargetMcpConfigPath(target) {
|
|
@@ -69,6 +85,8 @@ export function getTargetMcpConfigPath(target) {
|
|
|
69
85
|
return getOpenCodeConfigPath();
|
|
70
86
|
case "antigravity":
|
|
71
87
|
return getAntigravitySettingsPath();
|
|
88
|
+
case "codex":
|
|
89
|
+
return getCodexConfigPath();
|
|
72
90
|
}
|
|
73
91
|
}
|
|
74
92
|
export function getLockFilePath() {
|
package/dist/main.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { maybeBounceToLatest } from "./bounce.js";
|
|
1
2
|
import { log, logError } from "./colors.js";
|
|
2
3
|
import { ALL_TARGETS, getPackageVersion } from "./lib.js";
|
|
3
4
|
import { helpText } from "./ui.js";
|
|
@@ -34,6 +35,7 @@ function parseArgs(argv) {
|
|
|
34
35
|
help: false,
|
|
35
36
|
version: false,
|
|
36
37
|
noHook: false,
|
|
38
|
+
noBounce: false,
|
|
37
39
|
autoUpdate: false,
|
|
38
40
|
};
|
|
39
41
|
const positional = [];
|
|
@@ -50,6 +52,8 @@ function parseArgs(argv) {
|
|
|
50
52
|
flags.force = true;
|
|
51
53
|
else if (a === "--no-hook")
|
|
52
54
|
flags.noHook = true;
|
|
55
|
+
else if (a === "--no-bounce")
|
|
56
|
+
flags.noBounce = true;
|
|
53
57
|
else if (a === "--auto-update")
|
|
54
58
|
flags.autoUpdate = true;
|
|
55
59
|
else if (a === "-h" || a === "--help")
|
|
@@ -87,6 +91,9 @@ function parseArgs(argv) {
|
|
|
87
91
|
}
|
|
88
92
|
async function main() {
|
|
89
93
|
const args = parseArgs(process.argv.slice(2));
|
|
94
|
+
if (!args.flags.noBounce) {
|
|
95
|
+
await maybeBounceToLatest(process.argv.slice(2));
|
|
96
|
+
}
|
|
90
97
|
if (args.flags.version) {
|
|
91
98
|
log(getPackageVersion());
|
|
92
99
|
process.exit(0);
|
|
@@ -114,7 +121,7 @@ async function main() {
|
|
|
114
121
|
only: args.only.length > 0 ? args.only : undefined,
|
|
115
122
|
targets: args.targets.length > 0 ? args.targets : undefined,
|
|
116
123
|
noHook: args.flags.noHook,
|
|
117
|
-
hookMode:
|
|
124
|
+
hookMode: "auto",
|
|
118
125
|
});
|
|
119
126
|
break;
|
|
120
127
|
case "update":
|
|
@@ -139,7 +146,7 @@ async function main() {
|
|
|
139
146
|
break;
|
|
140
147
|
case "hook":
|
|
141
148
|
if (args.subcommand === "install") {
|
|
142
|
-
exitCode = runHookInstall(
|
|
149
|
+
exitCode = runHookInstall("auto");
|
|
143
150
|
}
|
|
144
151
|
else if (args.subcommand === "uninstall") {
|
|
145
152
|
exitCode = runHookUninstall();
|
package/dist/render.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { extname, join } from "node:path";
|
|
3
3
|
import { parseFrontmatter } from "./lib.js";
|
|
4
4
|
function readSkillEntryPoint(skillDir) {
|
|
5
5
|
const candidates = ["SKILL.md", "README.md"];
|
|
@@ -37,6 +37,39 @@ function stripFrontmatter(content) {
|
|
|
37
37
|
return content;
|
|
38
38
|
return m[1].replace(/^\s+/, "");
|
|
39
39
|
}
|
|
40
|
+
const INLINE_TEXT_EXT = new Set([".md", ".sql", ".txt", ".json", ".yaml", ".yml", ".csv"]);
|
|
41
|
+
const INLINE_MAX_BYTES = 64 * 1024;
|
|
42
|
+
const FENCE_LANG = {
|
|
43
|
+
".sql": "sql",
|
|
44
|
+
".json": "json",
|
|
45
|
+
".yaml": "yaml",
|
|
46
|
+
".yml": "yaml",
|
|
47
|
+
".csv": "csv",
|
|
48
|
+
".txt": "",
|
|
49
|
+
};
|
|
50
|
+
export function inlineReferences(refs) {
|
|
51
|
+
const sections = [];
|
|
52
|
+
for (const ref of refs) {
|
|
53
|
+
const ext = extname(ref.rel).toLowerCase();
|
|
54
|
+
const bytes = Buffer.byteLength(ref.text, "utf-8");
|
|
55
|
+
if (!INLINE_TEXT_EXT.has(ext)) {
|
|
56
|
+
sections.push(`### \`${ref.rel}\`\n\n> (omitido: tipo \`${ext || "sin extensión"}\` no inlineable)`);
|
|
57
|
+
}
|
|
58
|
+
else if (bytes > INLINE_MAX_BYTES) {
|
|
59
|
+
sections.push(`### \`${ref.rel}\`\n\n> (omitido: ${Math.round(bytes / 1024)} KB; supera el límite de inline)`);
|
|
60
|
+
}
|
|
61
|
+
else if (ext === ".md") {
|
|
62
|
+
sections.push(`### \`${ref.rel}\`\n\n${ref.text.trim()}`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const lang = FENCE_LANG[ext] ?? "";
|
|
66
|
+
sections.push(`### \`${ref.rel}\`\n\n\`\`\`${lang}\n${ref.text.replace(/\s+$/, "")}\n\`\`\``);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (sections.length === 0)
|
|
70
|
+
return "";
|
|
71
|
+
return `\n\n---\n\n# Referencias (incluidas inline)\n\n> Este cliente instala la skill como un solo archivo; el contenido de \`references/\` se incluye acá para que las menciones del cuerpo sigan disponibles.\n\n${sections.join("\n\n")}\n`;
|
|
72
|
+
}
|
|
40
73
|
export function renderMcpForTarget(mcp, spec) {
|
|
41
74
|
const obj = {
|
|
42
75
|
[spec.mcpUrlKey]: mcp.url,
|
|
@@ -64,5 +97,7 @@ function bearerInterpolation(env, target) {
|
|
|
64
97
|
return `Bearer \${env:${env}}`;
|
|
65
98
|
case "opencode":
|
|
66
99
|
return `Bearer {env:${env}}`;
|
|
100
|
+
case "codex":
|
|
101
|
+
throw new Error("codex usa config TOML (bearer_token_env_var), no headers JSON");
|
|
67
102
|
}
|
|
68
103
|
}
|
package/dist/targets.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { getAntigravitySettingsPath, getAntigravitySkillsDir, getClaudeSettingsPath, getClaudeSkillsDir, getCursorMcpPath, getCursorSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
|
|
4
|
+
import { getAntigravitySettingsPath, getAntigravitySkillsDir, getClaudeSettingsPath, getClaudeSkillsDir, getCodexConfigPath, getCodexSkillsDir, getCursorMcpPath, getCursorSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
|
|
5
5
|
export const TARGETS = {
|
|
6
6
|
"claude-code": {
|
|
7
7
|
id: "claude-code",
|
|
@@ -40,17 +40,31 @@ export const TARGETS = {
|
|
|
40
40
|
mcpRootKey: "mcpServers",
|
|
41
41
|
mcpUrlKey: "serverUrl",
|
|
42
42
|
},
|
|
43
|
+
codex: {
|
|
44
|
+
id: "codex",
|
|
45
|
+
label: "Codex",
|
|
46
|
+
skillsDir: getCodexSkillsDir(),
|
|
47
|
+
skillFormat: "folder",
|
|
48
|
+
mcpConfigPath: getCodexConfigPath(),
|
|
49
|
+
mcpRootKey: "mcpServers",
|
|
50
|
+
mcpUrlKey: "url",
|
|
51
|
+
mcpMechanism: "toml-block",
|
|
52
|
+
},
|
|
43
53
|
};
|
|
44
54
|
export function detectInstalledTargets() {
|
|
45
55
|
const home = homedir();
|
|
46
56
|
const xdgConfig = process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME.length > 0
|
|
47
57
|
? process.env.XDG_CONFIG_HOME
|
|
48
58
|
: join(home, ".config");
|
|
59
|
+
const codexHome = process.env.CODEX_HOME && process.env.CODEX_HOME.length > 0
|
|
60
|
+
? process.env.CODEX_HOME
|
|
61
|
+
: join(home, ".codex");
|
|
49
62
|
const sentinels = {
|
|
50
63
|
"claude-code": join(home, ".claude"),
|
|
51
64
|
cursor: join(home, ".cursor"),
|
|
52
65
|
opencode: join(xdgConfig, "opencode"),
|
|
53
66
|
antigravity: join(home, ".antigravity"),
|
|
67
|
+
codex: codexHome,
|
|
54
68
|
};
|
|
55
69
|
const out = [];
|
|
56
70
|
for (const id of Object.keys(sentinels)) {
|
package/dist/ui.js
CHANGED
|
@@ -59,9 +59,10 @@ ${bold("Opciones:")}
|
|
|
59
59
|
-y, --yes No pedir confirmación
|
|
60
60
|
--dry-run Mostrar qué se haría, sin tocar archivos
|
|
61
61
|
--only=a,b,c Instalar solo esas skills/MCPs
|
|
62
|
-
--target=NAME Restringir al cliente NAME (claude-code|cursor|opencode|antigravity)
|
|
62
|
+
--target=NAME Restringir al cliente NAME (claude-code|cursor|opencode|antigravity|codex)
|
|
63
63
|
--no-hook No instalar el hook de SessionStart
|
|
64
|
-
--
|
|
64
|
+
--no-bounce No saltar a la última versión publicada (la usa el hook)
|
|
65
|
+
--auto-update Aceptado por compatibilidad — el hook ya es auto-update por default
|
|
65
66
|
-h, --help Mostrar ayuda
|
|
66
67
|
-v, --version Mostrar versión
|
|
67
68
|
|