alegra-data-agent 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +73 -13
- package/dist/auth.js +23 -8
- package/dist/commands/hook.js +12 -30
- package/dist/commands/install.js +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/commands/status.js +97 -3
- package/dist/commands/update.js +43 -8
- package/dist/installer.js +157 -34
- package/dist/lib.js +26 -25
- package/dist/main.js +4 -0
- package/dist/render.js +3 -0
- package/dist/signature.js +17 -5
- package/dist/targets.js +10 -7
- package/dist/ui.js +1 -1
- package/package.json +12 -9
package/dist/api.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
1
3
|
import { readAuth } from "./auth.js";
|
|
2
|
-
import { COMMAND_NAME, GATEWAY_URL, getPackageVersion } from "./lib.js";
|
|
4
|
+
import { COMMAND_NAME, GATEWAY_URL, getManifestCachePath, getPackageVersion } from "./lib.js";
|
|
3
5
|
import { verifyManifestSignature } from "./signature.js";
|
|
4
6
|
export class ManifestSignatureError extends Error {
|
|
5
7
|
constructor(reason) {
|
|
@@ -29,7 +31,7 @@ function newRequestId() {
|
|
|
29
31
|
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
30
32
|
}
|
|
31
33
|
function userAgent() {
|
|
32
|
-
return `${COMMAND_NAME}/${getPackageVersion()} (+https://github.com/
|
|
34
|
+
return `${COMMAND_NAME}/${getPackageVersion()} (+https://github.com/Alegra-Team/data-agent-selfservice)`;
|
|
33
35
|
}
|
|
34
36
|
function sleep(ms) {
|
|
35
37
|
return new Promise((r) => setTimeout(r, ms));
|
|
@@ -63,6 +65,7 @@ async function authedFetch(path, opts = {}) {
|
|
|
63
65
|
Accept: "application/json,application/octet-stream",
|
|
64
66
|
"User-Agent": userAgent(),
|
|
65
67
|
"X-Request-Id": reqId,
|
|
68
|
+
...opts.headers,
|
|
66
69
|
},
|
|
67
70
|
signal: ac.signal,
|
|
68
71
|
});
|
|
@@ -109,6 +112,8 @@ async function authedFetch(path, opts = {}) {
|
|
|
109
112
|
}
|
|
110
113
|
throw new GatewayError(res.status, path, `Gateway con error ${res.status}. Reintenta en unos segundos. Si persiste, reporta a #data (request-id ${reqId}).`);
|
|
111
114
|
}
|
|
115
|
+
if (res.status === 304 && opts.allowNotModified)
|
|
116
|
+
return res;
|
|
112
117
|
if (!res.ok) {
|
|
113
118
|
throw new GatewayError(res.status, path, `Gateway respondió ${res.status} en ${path}`);
|
|
114
119
|
}
|
|
@@ -158,16 +163,76 @@ function mapNetworkError(err, path, timeoutMs) {
|
|
|
158
163
|
}
|
|
159
164
|
return new GatewayError(0, path, `Error de red al hablar con el gateway: ${err.message}`);
|
|
160
165
|
}
|
|
166
|
+
export function parseManifestCache(raw) {
|
|
167
|
+
try {
|
|
168
|
+
const parsed = JSON.parse(raw);
|
|
169
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
170
|
+
return null;
|
|
171
|
+
const { etag, manifest } = parsed;
|
|
172
|
+
if (typeof etag !== "string" || etag.length === 0)
|
|
173
|
+
return null;
|
|
174
|
+
if (typeof manifest !== "object" || manifest === null)
|
|
175
|
+
return null;
|
|
176
|
+
return { etag, manifest: manifest };
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function readManifestCache() {
|
|
183
|
+
try {
|
|
184
|
+
return parseManifestCache(readFileSync(getManifestCachePath(), "utf-8"));
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function writeManifestCache(etag, manifest) {
|
|
191
|
+
const path = getManifestCachePath();
|
|
192
|
+
try {
|
|
193
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
194
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
195
|
+
writeFileSync(tmp, `${JSON.stringify({ etag, manifest })}\n`, { mode: 0o600 });
|
|
196
|
+
try {
|
|
197
|
+
renameSync(tmp, path);
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
rmSync(tmp, { force: true });
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function verifiedOrThrow(manifest) {
|
|
208
|
+
const check = verifyManifestSignature(manifest, { softFail: false });
|
|
209
|
+
if (!check.ok) {
|
|
210
|
+
throw new ManifestSignatureError(check.reason);
|
|
211
|
+
}
|
|
212
|
+
return manifest;
|
|
213
|
+
}
|
|
161
214
|
export async function fetchManifest(opts = {}) {
|
|
162
215
|
try {
|
|
163
|
-
const
|
|
216
|
+
const cached = readManifestCache();
|
|
217
|
+
const res = await authedFetch("/api/manifest", {
|
|
218
|
+
...opts,
|
|
219
|
+
allowNotModified: true,
|
|
220
|
+
headers: cached ? { ...opts.headers, "If-None-Match": cached.etag } : opts.headers,
|
|
221
|
+
});
|
|
222
|
+
if (res.status === 304) {
|
|
223
|
+
if (cached)
|
|
224
|
+
return verifiedOrThrow(cached.manifest);
|
|
225
|
+
const retry = await authedFetch("/api/manifest", { ...opts, allowNotModified: false });
|
|
226
|
+
if (!retry.ok)
|
|
227
|
+
return null;
|
|
228
|
+
return verifiedOrThrow((await retry.json()));
|
|
229
|
+
}
|
|
164
230
|
if (!res.ok)
|
|
165
231
|
return null;
|
|
166
|
-
const manifest = (await res.json());
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
}
|
|
232
|
+
const manifest = verifiedOrThrow((await res.json()));
|
|
233
|
+
const etag = res.headers.get("etag");
|
|
234
|
+
if (etag)
|
|
235
|
+
writeManifestCache(etag, manifest);
|
|
171
236
|
return manifest;
|
|
172
237
|
}
|
|
173
238
|
catch (err) {
|
|
@@ -185,8 +250,3 @@ export async function fetchSkillFile(skillName, relPath) {
|
|
|
185
250
|
const buffer = await res.arrayBuffer();
|
|
186
251
|
return new Uint8Array(buffer);
|
|
187
252
|
}
|
|
188
|
-
export async function fetchMcps(opts = {}) {
|
|
189
|
-
const res = await authedFetch("/api/mcps", opts);
|
|
190
|
-
const body = (await res.json());
|
|
191
|
-
return body.mcps ?? {};
|
|
192
|
-
}
|
package/dist/auth.js
CHANGED
|
@@ -25,19 +25,26 @@ function constantTimeStringEq(a, b) {
|
|
|
25
25
|
return false;
|
|
26
26
|
return timingSafeEqual(ab, bb);
|
|
27
27
|
}
|
|
28
|
+
let _authCache = null;
|
|
28
29
|
export function readAuth() {
|
|
30
|
+
if (_authCache)
|
|
31
|
+
return _authCache.value;
|
|
29
32
|
const path = getAuthFilePath();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
let value = null;
|
|
34
|
+
if (existsSync(path)) {
|
|
35
|
+
try {
|
|
36
|
+
value = JSON.parse(readFileSync(path, "utf-8"));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
value = null;
|
|
40
|
+
}
|
|
37
41
|
}
|
|
42
|
+
_authCache = { value };
|
|
43
|
+
return value;
|
|
38
44
|
}
|
|
39
45
|
export function writeAuth(record) {
|
|
40
46
|
const path = getAuthFilePath();
|
|
47
|
+
_authCache = { value: record };
|
|
41
48
|
mkdirSync(dirname(path), { recursive: true });
|
|
42
49
|
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
|
|
43
50
|
try {
|
|
@@ -48,6 +55,7 @@ export function writeAuth(record) {
|
|
|
48
55
|
}
|
|
49
56
|
export function clearAuth() {
|
|
50
57
|
const path = getAuthFilePath();
|
|
58
|
+
_authCache = { value: null };
|
|
51
59
|
if (existsSync(path))
|
|
52
60
|
rmSync(path, { force: true });
|
|
53
61
|
}
|
|
@@ -278,6 +286,13 @@ function htmlPage(kind, subject) {
|
|
|
278
286
|
</body>
|
|
279
287
|
</html>`;
|
|
280
288
|
}
|
|
289
|
+
const HTML_ESCAPES = {
|
|
290
|
+
"&": "&",
|
|
291
|
+
"<": "<",
|
|
292
|
+
">": ">",
|
|
293
|
+
'"': """,
|
|
294
|
+
"'": "'",
|
|
295
|
+
};
|
|
281
296
|
function escapeHtml(s) {
|
|
282
|
-
return s.replace(/[&<>"']/g, (c) =>
|
|
297
|
+
return s.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c] ?? c);
|
|
283
298
|
}
|
package/dist/commands/hook.js
CHANGED
|
@@ -2,16 +2,21 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { logError, logOk, logWarn } from "../colors.js";
|
|
4
4
|
import { COMMAND_NAME, PRODUCT_NAME, getClaudeSettingsPath, getPackageVersion } from "../lib.js";
|
|
5
|
+
function versionRangeForHook(version) {
|
|
6
|
+
const [rawMajor] = version.split(".");
|
|
7
|
+
const major = Number.parseInt(rawMajor ?? "0", 10);
|
|
8
|
+
if (!Number.isFinite(major))
|
|
9
|
+
return "latest";
|
|
10
|
+
return major === 0 ? "0.x" : `^${major}.0.0`;
|
|
11
|
+
}
|
|
5
12
|
function hookCommandFor(mode) {
|
|
6
13
|
const v = getPackageVersion();
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const minor = parts[1] ?? "0";
|
|
10
|
-
const range = `~${major}.${minor}.0`;
|
|
11
|
-
const verb = mode === "auto" ? "update --yes" : "check";
|
|
14
|
+
const range = versionRangeForHook(v);
|
|
15
|
+
const verb = mode === "auto" ? "update --yes --background" : "check";
|
|
12
16
|
return `pnpm dlx --config.minimumReleaseAge=0 ${COMMAND_NAME}@${range} ${verb} --no-bounce`;
|
|
13
17
|
}
|
|
14
18
|
const HOOK_SIGNATURE = COMMAND_NAME;
|
|
19
|
+
const HOOK_TIMEOUT_SECONDS = 120;
|
|
15
20
|
function readSettings() {
|
|
16
21
|
const path = getClaudeSettingsPath();
|
|
17
22
|
if (!existsSync(path))
|
|
@@ -44,7 +49,7 @@ function isOurMatcher(m) {
|
|
|
44
49
|
h.command.includes(HOOK_SIGNATURE));
|
|
45
50
|
}
|
|
46
51
|
function buildOurMatcher(command) {
|
|
47
|
-
return { hooks: [{ type: "command", command }] };
|
|
52
|
+
return { hooks: [{ type: "command", command, timeout: HOOK_TIMEOUT_SECONDS }] };
|
|
48
53
|
}
|
|
49
54
|
export function runHookInstall(mode = "auto") {
|
|
50
55
|
const settings = readSettings();
|
|
@@ -89,29 +94,6 @@ export function runHookInstall(mode = "auto") {
|
|
|
89
94
|
}
|
|
90
95
|
return 0;
|
|
91
96
|
}
|
|
92
|
-
export function refreshHookCommand() {
|
|
93
|
-
try {
|
|
94
|
-
const settings = readSettings();
|
|
95
|
-
if (!settings?.hooks)
|
|
96
|
-
return;
|
|
97
|
-
const existing = settings.hooks.SessionStart;
|
|
98
|
-
if (!Array.isArray(existing))
|
|
99
|
-
return;
|
|
100
|
-
const ours = existing.find(isOurMatcher);
|
|
101
|
-
if (!ours)
|
|
102
|
-
return;
|
|
103
|
-
const current = ours.hooks.find((h) => h.type === "command")?.command ?? "";
|
|
104
|
-
const fresh = hookCommandFor("auto");
|
|
105
|
-
if (current === fresh)
|
|
106
|
-
return;
|
|
107
|
-
const others = existing.filter((m) => !isOurMatcher(m));
|
|
108
|
-
others.push(buildOurMatcher(fresh));
|
|
109
|
-
settings.hooks.SessionStart = others;
|
|
110
|
-
writeSettings(settings);
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
97
|
export function runHookUninstall(opts = {}) {
|
|
116
98
|
const say = opts.quiet ? () => { } : logWarn;
|
|
117
99
|
const sayOk = opts.quiet ? () => { } : logOk;
|
|
@@ -155,4 +137,4 @@ export function runHookUninstall(opts = {}) {
|
|
|
155
137
|
sayOk("Hook removido.");
|
|
156
138
|
return 0;
|
|
157
139
|
}
|
|
158
|
-
export { hookCommandFor };
|
|
140
|
+
export { hookCommandFor, versionRangeForHook };
|
package/dist/commands/install.js
CHANGED
|
@@ -141,7 +141,7 @@ function printNextSteps(noHook) {
|
|
|
141
141
|
log(` ${dim("$")} ${bold(`${COMMAND_NAME} hook uninstall`)} ${gray("apagar aviso")}`);
|
|
142
142
|
}
|
|
143
143
|
log("");
|
|
144
|
-
log(` ${dim("Docs · ")}${cyan("github.com/
|
|
144
|
+
log(` ${dim("Docs · ")}${cyan("github.com/Alegra-Team/data-agent-selfservice")}`);
|
|
145
145
|
log("");
|
|
146
146
|
}
|
|
147
147
|
function isClaudeCode() {
|
package/dist/commands/login.js
CHANGED
|
@@ -19,7 +19,7 @@ export async function runLogin(opts = {}) {
|
|
|
19
19
|
}
|
|
20
20
|
log(` Gateway: ${dim(GATEWAY_URL)}`);
|
|
21
21
|
try {
|
|
22
|
-
const record = await performBrowserLogin();
|
|
22
|
+
const record = await performBrowserLogin(opts.timeoutMs);
|
|
23
23
|
writeAuth(record);
|
|
24
24
|
log("");
|
|
25
25
|
log(` ${green("✓")} Autenticado como ${bold(record.email)}.`);
|
package/dist/commands/status.js
CHANGED
|
@@ -2,8 +2,9 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { AuthRequiredError, fetchManifest } from "../api.js";
|
|
3
3
|
import { readAuth } from "../auth.js";
|
|
4
4
|
import { bold, cyan, dim, gray, green, log, logError, logWarn, yellow } from "../colors.js";
|
|
5
|
-
import { detectDrift } from "../installer.js";
|
|
5
|
+
import { detectDrift, inspectMcpInTarget } from "../installer.js";
|
|
6
6
|
import { COMMAND_NAME, GATEWAY_URL, getAutoUpdateLogPath, getClaudeSkillsDir, getLockFilePath, getPackageVersion, getTargetSkillsDir, readLockFile, } from "../lib.js";
|
|
7
|
+
import { TARGETS } from "../targets.js";
|
|
7
8
|
function reportAutoUpdateLog() {
|
|
8
9
|
const logPath = getAutoUpdateLogPath();
|
|
9
10
|
if (!existsSync(logPath))
|
|
@@ -18,6 +19,88 @@ function reportAutoUpdateLog() {
|
|
|
18
19
|
catch {
|
|
19
20
|
}
|
|
20
21
|
}
|
|
22
|
+
function reportMcps(manifest, lock) {
|
|
23
|
+
const mcps = manifest.mcps ?? {};
|
|
24
|
+
const names = Object.keys(mcps);
|
|
25
|
+
if (names.length === 0)
|
|
26
|
+
return;
|
|
27
|
+
log(bold(" MCPs:"));
|
|
28
|
+
for (const name of names) {
|
|
29
|
+
const entry = mcps[name];
|
|
30
|
+
const targets = (lock.mcps?.[name]?.targets ?? []);
|
|
31
|
+
if (targets.length === 0) {
|
|
32
|
+
log(` ${cyan("+")} ${name} ${gray("(disponible, sin instalar)")}`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const ok = [];
|
|
36
|
+
const missing = [];
|
|
37
|
+
const modified = [];
|
|
38
|
+
for (const t of targets) {
|
|
39
|
+
const label = TARGETS[t].label;
|
|
40
|
+
const state = inspectMcpInTarget(entry, TARGETS[t]);
|
|
41
|
+
if (state === "ok")
|
|
42
|
+
ok.push(label);
|
|
43
|
+
else if (state === "missing")
|
|
44
|
+
missing.push(label);
|
|
45
|
+
else
|
|
46
|
+
modified.push(label);
|
|
47
|
+
}
|
|
48
|
+
const notes = [];
|
|
49
|
+
if (ok.length > 0)
|
|
50
|
+
notes.push(gray(`ok en ${ok.join(", ")}`));
|
|
51
|
+
if (modified.length > 0)
|
|
52
|
+
notes.push(dim(`editado a mano en ${modified.join(", ")}`));
|
|
53
|
+
if (missing.length > 0)
|
|
54
|
+
notes.push(yellow(`falta en ${missing.join(", ")}`));
|
|
55
|
+
const icon = missing.length > 0 ? yellow("✗") : modified.length > 0 ? dim("~") : green("✓");
|
|
56
|
+
log(` ${icon} ${name} ${gray("·")} ${notes.join(gray(" · "))}`);
|
|
57
|
+
}
|
|
58
|
+
log("");
|
|
59
|
+
}
|
|
60
|
+
export function findRogueMetabase(manifest, specs = Object.values(TARGETS)) {
|
|
61
|
+
const official = manifest.mcps?.metabase?.url;
|
|
62
|
+
if (!official)
|
|
63
|
+
return [];
|
|
64
|
+
const norm = (u) => u.trim().replace(/\/+$/, "").toLowerCase();
|
|
65
|
+
const officialNorm = norm(official);
|
|
66
|
+
const found = [];
|
|
67
|
+
for (const spec of specs) {
|
|
68
|
+
if (!spec.mcpConfigPath.endsWith(".json"))
|
|
69
|
+
continue;
|
|
70
|
+
if (!existsSync(spec.mcpConfigPath))
|
|
71
|
+
continue;
|
|
72
|
+
try {
|
|
73
|
+
const cfg = JSON.parse(readFileSync(spec.mcpConfigPath, "utf-8"));
|
|
74
|
+
const servers = cfg[spec.mcpRootKey];
|
|
75
|
+
if (!servers || typeof servers !== "object")
|
|
76
|
+
continue;
|
|
77
|
+
for (const [name, def] of Object.entries(servers)) {
|
|
78
|
+
if (!/metabase/i.test(name))
|
|
79
|
+
continue;
|
|
80
|
+
const url = def?.[spec.mcpUrlKey];
|
|
81
|
+
if (typeof url === "string" && norm(url) === officialNorm)
|
|
82
|
+
continue;
|
|
83
|
+
found.push(`${spec.label} → ${name}${typeof url === "string" ? ` (${url})` : ""}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return found;
|
|
90
|
+
}
|
|
91
|
+
function reportRogueMetabase(manifest) {
|
|
92
|
+
const official = manifest.mcps?.metabase?.url;
|
|
93
|
+
if (!official)
|
|
94
|
+
return;
|
|
95
|
+
const found = findRogueMetabase(manifest);
|
|
96
|
+
if (found.length === 0)
|
|
97
|
+
return;
|
|
98
|
+
logWarn(`Hay ${found.length} MCP(s) de Metabase que NO son el oficial de Alegra: ${found.join(" · ")}`);
|
|
99
|
+
log(gray(` El oficial es ${official}. Los otros son otra fuente y dan otros números.`));
|
|
100
|
+
log(gray(" Las skills tienen instrucción de no usarlos, pero eso no los bloquea:"));
|
|
101
|
+
log(gray(" si no los necesitás, conviene quitarlos del cliente."));
|
|
102
|
+
log("");
|
|
103
|
+
}
|
|
21
104
|
export async function runStatus() {
|
|
22
105
|
const auth = readAuth();
|
|
23
106
|
const lockForTargets = readLockFile();
|
|
@@ -63,6 +146,12 @@ export async function runStatus() {
|
|
|
63
146
|
}
|
|
64
147
|
const lock = readLockFile();
|
|
65
148
|
const drift = detectDrift(manifest);
|
|
149
|
+
const cliVersion = getPackageVersion();
|
|
150
|
+
if (manifest.productVersion && manifest.productVersion !== cliVersion) {
|
|
151
|
+
log(` ${gray("Manifest:")} generado por v${manifest.productVersion} ${gray(`· tu CLI corre v${cliVersion}`)}`);
|
|
152
|
+
log(gray(" No es un error: el gateway y npm se despliegan por separado."));
|
|
153
|
+
log("");
|
|
154
|
+
}
|
|
66
155
|
log(bold(" Skills:"));
|
|
67
156
|
log(` ${green("✓")} ${drift.unchanged.length} al día`);
|
|
68
157
|
if (drift.toInstall.length > 0)
|
|
@@ -72,9 +161,14 @@ export async function runStatus() {
|
|
|
72
161
|
if (drift.orphaned.length > 0)
|
|
73
162
|
log(` ${dim("·")} ${drift.orphaned.length} huérfanas: ${drift.orphaned.join(", ")}`);
|
|
74
163
|
log("");
|
|
75
|
-
|
|
164
|
+
reportMcps(manifest, lock);
|
|
165
|
+
reportRogueMetabase(manifest);
|
|
166
|
+
const total = drift.toInstall.length +
|
|
167
|
+
drift.toUpdate.length +
|
|
168
|
+
drift.mcps.toInstall.length +
|
|
169
|
+
drift.mcps.toUpdate.length;
|
|
76
170
|
if (total > 0) {
|
|
77
|
-
logWarn(`Hay ${total}
|
|
171
|
+
logWarn(`Hay ${total} elemento(s) (skills + MCPs) por actualizar. Agente: ejecutá vos mismo ${bold(`pnpm dlx ${COMMAND_NAME} update --yes`)} — el usuario no corre comandos.`);
|
|
78
172
|
}
|
|
79
173
|
else if (Object.keys(lock.skills).length === 0) {
|
|
80
174
|
logWarn(`Aún no hay skills instaladas. Agente: ejecutá vos mismo ${bold(`pnpm dlx ${COMMAND_NAME}`)} — el usuario no corre comandos.`);
|
package/dist/commands/update.js
CHANGED
|
@@ -1,18 +1,45 @@
|
|
|
1
|
-
import { rmSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
2
3
|
import { AuthRequiredError, fetchManifest } from "../api.js";
|
|
3
4
|
import { hasValidAuth } from "../auth.js";
|
|
4
5
|
import { refreshAutoUpdate } from "../autoupdate.js";
|
|
5
6
|
import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
|
|
6
7
|
import { detectDrift, installAll, summarize } from "../installer.js";
|
|
7
|
-
import { getAutoUpdateLogPath } from "../lib.js";
|
|
8
|
+
import { getAutoUpdateLogPath, getLastCheckPath } from "../lib.js";
|
|
8
9
|
import { banner } from "../ui.js";
|
|
9
10
|
import { runLogin } from "./login.js";
|
|
11
|
+
const BACKGROUND_COOLDOWN_MS = 15 * 60_000;
|
|
12
|
+
const BACKGROUND_LOGIN_TIMEOUT_MS = 90_000;
|
|
13
|
+
function checkedRecently() {
|
|
14
|
+
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;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function markChecked() {
|
|
27
|
+
try {
|
|
28
|
+
const path = getLastCheckPath();
|
|
29
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
30
|
+
writeFileSync(path, `${JSON.stringify({ checkedAt: new Date().toISOString() }, null, 2)}\n`);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
}
|
|
34
|
+
}
|
|
10
35
|
export async function runUpdate(opts = {}) {
|
|
36
|
+
if (opts.background && !opts.dryRun && checkedRecently())
|
|
37
|
+
return 0;
|
|
11
38
|
banner();
|
|
12
39
|
if (!hasValidAuth()) {
|
|
13
|
-
const code = await runLogin({});
|
|
40
|
+
const code = await runLogin(opts.background ? { timeoutMs: BACKGROUND_LOGIN_TIMEOUT_MS } : {});
|
|
14
41
|
if (code !== 0)
|
|
15
|
-
return code;
|
|
42
|
+
return opts.background ? 0 : code;
|
|
16
43
|
}
|
|
17
44
|
let reauthed = false;
|
|
18
45
|
for (;;) {
|
|
@@ -52,6 +79,8 @@ async function runUpdateOnce(opts) {
|
|
|
52
79
|
];
|
|
53
80
|
if (pending.length === 0) {
|
|
54
81
|
log(green(" ✓ Todo al día (skills + MCPs).\n"));
|
|
82
|
+
if (!opts.dryRun)
|
|
83
|
+
markChecked();
|
|
55
84
|
if (!opts.dryRun) {
|
|
56
85
|
refreshAutoUpdate();
|
|
57
86
|
clearAutoUpdateLog();
|
|
@@ -65,14 +94,18 @@ async function runUpdateOnce(opts) {
|
|
|
65
94
|
log(` ${yellow("↻")} ${name} ${gray("(skill nueva versión)")}`);
|
|
66
95
|
for (const name of drift.mcps.toInstall)
|
|
67
96
|
log(` ${cyan("+")} ${name} ${gray("(MCP nuevo)")}`);
|
|
68
|
-
for (const name of drift.mcps.toUpdate)
|
|
69
|
-
|
|
97
|
+
for (const name of drift.mcps.toUpdate) {
|
|
98
|
+
const motivo = drift.mcps.missing.includes(name)
|
|
99
|
+
? "(MCP ausente en el cliente — se repone)"
|
|
100
|
+
: "(MCP actualizado)";
|
|
101
|
+
log(` ${yellow("↻")} ${name} ${gray(motivo)}`);
|
|
102
|
+
}
|
|
70
103
|
log("");
|
|
71
104
|
if (opts.dryRun) {
|
|
72
105
|
log(dim(" Modo dry-run: no se modificará nada.\n"));
|
|
73
106
|
return 0;
|
|
74
107
|
}
|
|
75
|
-
const result = await installAll({ only: pending, targets: opts.targets
|
|
108
|
+
const result = await installAll({ only: pending, targets: opts.targets });
|
|
76
109
|
if ("error" in result) {
|
|
77
110
|
logError(result.error);
|
|
78
111
|
return 1;
|
|
@@ -80,8 +113,10 @@ async function runUpdateOnce(opts) {
|
|
|
80
113
|
summarize(result.results);
|
|
81
114
|
refreshAutoUpdate();
|
|
82
115
|
const failed = result.results.some((r) => r.status === "failed");
|
|
83
|
-
if (!failed)
|
|
116
|
+
if (!failed) {
|
|
84
117
|
clearAutoUpdateLog();
|
|
118
|
+
markChecked();
|
|
119
|
+
}
|
|
85
120
|
return failed ? 1 : 0;
|
|
86
121
|
}
|
|
87
122
|
function clearAutoUpdateLog() {
|
package/dist/installer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { copyFileSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join, resolve, sep } from "node:path";
|
|
2
|
+
import { copyFileSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
4
4
|
import { AuthRequiredError, fetchManifest, fetchSkillFile } from "./api.js";
|
|
5
5
|
import { dim, gray, green, logError, red, yellow } from "./colors.js";
|
|
6
6
|
import { adaptStdioCommand, ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getCursorLegacyRulesDir, getLockFilePath, getOpenCodeLegacyPromptsDir, getPackageVersion, readLockFile, sha256File, writeLockFile, } from "./lib.js";
|
|
@@ -44,6 +44,17 @@ function mcpConfigHash(mcp) {
|
|
|
44
44
|
});
|
|
45
45
|
return createHash("sha256").update(salient).digest("hex");
|
|
46
46
|
}
|
|
47
|
+
function stableStringify(value) {
|
|
48
|
+
if (value === null || typeof value !== "object")
|
|
49
|
+
return JSON.stringify(value);
|
|
50
|
+
if (Array.isArray(value))
|
|
51
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
52
|
+
const o = value;
|
|
53
|
+
return `{${Object.keys(o)
|
|
54
|
+
.sort()
|
|
55
|
+
.map((k) => `${JSON.stringify(k)}:${stableStringify(o[k])}`)
|
|
56
|
+
.join(",")}}`;
|
|
57
|
+
}
|
|
47
58
|
function safeJoinSkillFile(skillRoot, rel) {
|
|
48
59
|
const candidate = resolve(skillRoot, rel);
|
|
49
60
|
const root = resolve(skillRoot);
|
|
@@ -73,33 +84,51 @@ export function verifyInstalledFiles(dir, entry) {
|
|
|
73
84
|
function sha256Buffer(buf) {
|
|
74
85
|
return createHash("sha256").update(buf).digest("hex");
|
|
75
86
|
}
|
|
87
|
+
const DOWNLOAD_CONCURRENCY = 8;
|
|
88
|
+
async function mapWithLimit(items, limit, fn) {
|
|
89
|
+
const out = new Array(items.length);
|
|
90
|
+
let next = 0;
|
|
91
|
+
const worker = async () => {
|
|
92
|
+
for (;;) {
|
|
93
|
+
const i = next++;
|
|
94
|
+
if (i >= items.length)
|
|
95
|
+
return;
|
|
96
|
+
out[i] = await fn(items[i]);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
76
102
|
async function stageSkill(skillName, entry) {
|
|
77
103
|
validateSkillName(skillName);
|
|
78
104
|
for (const rel of entry.files)
|
|
79
105
|
validateRelPath(rel);
|
|
80
|
-
|
|
81
|
-
for (const rel of entry.files) {
|
|
106
|
+
return mapWithLimit(entry.files, DOWNLOAD_CONCURRENCY, async (rel) => {
|
|
82
107
|
const data = await fetchSkillFile(skillName, rel);
|
|
83
108
|
const actualHash = sha256Buffer(data);
|
|
84
109
|
const expectedHash = entry.sha256[rel];
|
|
85
110
|
if (actualHash !== expectedHash) {
|
|
86
111
|
throw new Error(`hash inválido para ${skillName}/${rel}: esperaba ${expectedHash.slice(0, 12)}…, recibido ${actualHash.slice(0, 12)}…`);
|
|
87
112
|
}
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
113
|
+
return { rel, data };
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function skillDirsFor(spec) {
|
|
117
|
+
return [spec.skillsDir, ...(spec.mirrorSkillsDirs ?? [])];
|
|
91
118
|
}
|
|
92
119
|
function writeSkillToTarget(skillName, staged, spec) {
|
|
93
120
|
validateSkillName(skillName);
|
|
94
121
|
if (spec.skillFormat === "folder") {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
122
|
+
for (const base of skillDirsFor(spec)) {
|
|
123
|
+
const dst = join(base, skillName);
|
|
124
|
+
if (existsSync(dst))
|
|
125
|
+
withFsRetry(() => rmSync(dst, { recursive: true, force: true }));
|
|
126
|
+
ensureDir(dst);
|
|
127
|
+
for (const f of staged) {
|
|
128
|
+
const dstFile = safeJoinSkillFile(dst, f.rel);
|
|
129
|
+
ensureDir(dirname(dstFile));
|
|
130
|
+
withFsRetry(() => writeFileSync(dstFile, f.data));
|
|
131
|
+
}
|
|
103
132
|
}
|
|
104
133
|
return;
|
|
105
134
|
}
|
|
@@ -190,11 +219,30 @@ function atomicWriteFile(path, content) {
|
|
|
190
219
|
}
|
|
191
220
|
}
|
|
192
221
|
}
|
|
222
|
+
const MAX_BACKUPS = 3;
|
|
223
|
+
const backedUpThisRun = new Set();
|
|
193
224
|
function backupIfExists(path) {
|
|
194
225
|
if (!existsSync(path))
|
|
195
226
|
return;
|
|
196
|
-
|
|
197
|
-
|
|
227
|
+
if (backedUpThisRun.has(path))
|
|
228
|
+
return;
|
|
229
|
+
copyFileSync(path, `${path}.bak-${Date.now()}`);
|
|
230
|
+
backedUpThisRun.add(path);
|
|
231
|
+
pruneBackups(path);
|
|
232
|
+
}
|
|
233
|
+
function pruneBackups(path) {
|
|
234
|
+
try {
|
|
235
|
+
const dir = dirname(path);
|
|
236
|
+
const prefix = `${basename(path)}.bak-`;
|
|
237
|
+
const stale = readdirSync(dir)
|
|
238
|
+
.filter((f) => f.startsWith(prefix))
|
|
239
|
+
.sort()
|
|
240
|
+
.slice(0, -MAX_BACKUPS);
|
|
241
|
+
for (const f of stale)
|
|
242
|
+
rmSync(join(dir, f), { force: true });
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
}
|
|
198
246
|
}
|
|
199
247
|
function readJsonOrEmpty(path) {
|
|
200
248
|
if (!existsSync(path))
|
|
@@ -282,6 +330,19 @@ export function upsertTomlBlock(content, name, block) {
|
|
|
282
330
|
const sep = content.endsWith("\n\n") ? "" : content.endsWith("\n") ? "\n" : "\n\n";
|
|
283
331
|
return `${content}${sep}${fenced}\n`;
|
|
284
332
|
}
|
|
333
|
+
export function extractTomlBlock(content, name) {
|
|
334
|
+
const { begin, end } = tomlMarkers(name);
|
|
335
|
+
const bi = content.indexOf(begin);
|
|
336
|
+
if (bi === -1)
|
|
337
|
+
return null;
|
|
338
|
+
const ei = content.indexOf(end, bi);
|
|
339
|
+
if (ei === -1)
|
|
340
|
+
return null;
|
|
341
|
+
return content
|
|
342
|
+
.slice(bi + begin.length, ei)
|
|
343
|
+
.replace(/^\n/, "")
|
|
344
|
+
.replace(/\n$/, "");
|
|
345
|
+
}
|
|
285
346
|
export function removeTomlBlock(content, name) {
|
|
286
347
|
const { begin, end } = tomlMarkers(name);
|
|
287
348
|
const bi = content.indexOf(begin);
|
|
@@ -317,14 +378,47 @@ function unregisterMcpInTomlBlock(name, spec) {
|
|
|
317
378
|
atomicWriteFile(spec.mcpConfigPath, next);
|
|
318
379
|
return true;
|
|
319
380
|
}
|
|
381
|
+
export function inspectMcpInTarget(mcp, spec) {
|
|
382
|
+
try {
|
|
383
|
+
if (!existsSync(spec.mcpConfigPath))
|
|
384
|
+
return "missing";
|
|
385
|
+
if (spec.mcpMechanism === "toml-block") {
|
|
386
|
+
const block = extractTomlBlock(readFileSync(spec.mcpConfigPath, "utf-8"), mcp.name);
|
|
387
|
+
if (block === null)
|
|
388
|
+
return "missing";
|
|
389
|
+
return block.trim() === renderMcpToml(mcp).trim() ? "ok" : "modified";
|
|
390
|
+
}
|
|
391
|
+
const cfg = JSON.parse(readFileSync(spec.mcpConfigPath, "utf-8"));
|
|
392
|
+
const section = cfg[spec.mcpRootKey];
|
|
393
|
+
if (!section || typeof section !== "object")
|
|
394
|
+
return "missing";
|
|
395
|
+
const current = section[mcp.name];
|
|
396
|
+
if (current === undefined)
|
|
397
|
+
return "missing";
|
|
398
|
+
return stableStringify(current) === stableStringify(renderMcpForTarget(mcp, spec))
|
|
399
|
+
? "ok"
|
|
400
|
+
: "modified";
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return "missing";
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function mcpMissingInTargets(mcp, targets) {
|
|
407
|
+
return targets.filter((id) => inspectMcpInTarget(mcp, TARGETS[id]) === "missing");
|
|
408
|
+
}
|
|
409
|
+
const cleanedTargets = new Set();
|
|
320
410
|
function resolveTargets(opts) {
|
|
321
411
|
const ids = opts.targets?.length ? opts.targets : detectInstalledTargets();
|
|
322
412
|
if (ids.length === 0) {
|
|
323
413
|
ids.push("claude-code");
|
|
324
414
|
}
|
|
325
415
|
const specs = ids.map((id) => TARGETS[id]);
|
|
326
|
-
for (const spec of specs)
|
|
416
|
+
for (const spec of specs) {
|
|
417
|
+
if (cleanedTargets.has(spec.id))
|
|
418
|
+
continue;
|
|
419
|
+
cleanedTargets.add(spec.id);
|
|
327
420
|
cleanupLegacySkillArtifacts(spec);
|
|
421
|
+
}
|
|
328
422
|
return specs;
|
|
329
423
|
}
|
|
330
424
|
const LEGACY_RENAMED_SKILLS = ["alanube", "alegra-producto", "alegra-ce-universe"];
|
|
@@ -360,10 +454,12 @@ function stripMcpEntries(path, names) {
|
|
|
360
454
|
function cleanupLegacySkillArtifacts(spec) {
|
|
361
455
|
try {
|
|
362
456
|
if (spec.skillFormat === "folder") {
|
|
363
|
-
for (const
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
457
|
+
for (const base of skillDirsFor(spec)) {
|
|
458
|
+
for (const old of LEGACY_RENAMED_SKILLS) {
|
|
459
|
+
const dir = join(base, old);
|
|
460
|
+
if (existsSync(dir))
|
|
461
|
+
rmSync(dir, { recursive: true, force: true });
|
|
462
|
+
}
|
|
367
463
|
}
|
|
368
464
|
}
|
|
369
465
|
const lock = readLockFile();
|
|
@@ -459,7 +555,13 @@ export async function installSkill(name, manifest, opts = {}) {
|
|
|
459
555
|
}
|
|
460
556
|
}
|
|
461
557
|
if (okTargets.length === 0) {
|
|
462
|
-
return {
|
|
558
|
+
return {
|
|
559
|
+
name,
|
|
560
|
+
kind: "skill",
|
|
561
|
+
status: "failed",
|
|
562
|
+
targets: targetIds,
|
|
563
|
+
error: failures.join("; "),
|
|
564
|
+
};
|
|
463
565
|
}
|
|
464
566
|
lock.skills[name] = {
|
|
465
567
|
name,
|
|
@@ -506,7 +608,14 @@ export function installMcp(mcp, opts = {}) {
|
|
|
506
608
|
const targetsMatch = prev?.targets &&
|
|
507
609
|
prev.targets.length === targetIds.length &&
|
|
508
610
|
prev.targets.every((t) => targetIds.includes(t));
|
|
509
|
-
|
|
611
|
+
const configChanged = !prev || prev.configHash !== mcpConfigHash(mcp);
|
|
612
|
+
const writeTargets = opts.force || configChanged
|
|
613
|
+
? wantedTargets
|
|
614
|
+
: wantedTargets.filter((t) => inspectMcpInTarget(mcp, t) === "missing");
|
|
615
|
+
const alreadyThere = wantedTargets
|
|
616
|
+
.filter((t) => !writeTargets.includes(t) && inspectMcpInTarget(mcp, t) !== "missing")
|
|
617
|
+
.map((t) => t.id);
|
|
618
|
+
if (writeTargets.length === 0 && targetsMatch) {
|
|
510
619
|
return { name: mcp.name, kind: "mcp", status: "unchanged", targets: targetIds };
|
|
511
620
|
}
|
|
512
621
|
if (opts.dryRun) {
|
|
@@ -514,20 +623,21 @@ export function installMcp(mcp, opts = {}) {
|
|
|
514
623
|
name: mcp.name,
|
|
515
624
|
kind: "mcp",
|
|
516
625
|
status: prev ? "updated" : "installed",
|
|
517
|
-
targets:
|
|
626
|
+
targets: writeTargets.map((t) => t.id),
|
|
518
627
|
};
|
|
519
628
|
}
|
|
520
|
-
const
|
|
629
|
+
const written = [];
|
|
521
630
|
const failures = [];
|
|
522
|
-
for (const t of
|
|
631
|
+
for (const t of writeTargets) {
|
|
523
632
|
try {
|
|
524
633
|
registerMcpInTarget(mcp, t);
|
|
525
|
-
|
|
634
|
+
written.push(t.id);
|
|
526
635
|
}
|
|
527
636
|
catch (err) {
|
|
528
637
|
failures.push(`${t.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
529
638
|
}
|
|
530
639
|
}
|
|
640
|
+
const okTargets = [...new Set([...alreadyThere, ...written])];
|
|
531
641
|
if (okTargets.length === 0) {
|
|
532
642
|
return {
|
|
533
643
|
name: mcp.name,
|
|
@@ -604,9 +714,11 @@ export function uninstallSkill(name, opts = {}) {
|
|
|
604
714
|
for (const tId of targetIds) {
|
|
605
715
|
const spec = TARGETS[tId];
|
|
606
716
|
if (spec.skillFormat === "folder") {
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
717
|
+
for (const base of skillDirsFor(spec)) {
|
|
718
|
+
const dst = join(base, name);
|
|
719
|
+
if (existsSync(dst))
|
|
720
|
+
rmSync(dst, { recursive: true, force: true });
|
|
721
|
+
}
|
|
610
722
|
}
|
|
611
723
|
else {
|
|
612
724
|
const ext = spec.skillFormat === "cursor-mdc" ? ".mdc" : ".md";
|
|
@@ -703,16 +815,27 @@ export function detectDrift(manifest) {
|
|
|
703
815
|
toUpdate: [],
|
|
704
816
|
unchanged: [],
|
|
705
817
|
orphaned: [],
|
|
818
|
+
missing: [],
|
|
706
819
|
};
|
|
707
820
|
const manifestMcps = manifest.mcps ?? {};
|
|
708
821
|
for (const name of Object.keys(manifestMcps)) {
|
|
709
822
|
const prevMcp = lock.mcps?.[name];
|
|
710
|
-
if (!prevMcp)
|
|
823
|
+
if (!prevMcp) {
|
|
711
824
|
mcps.toInstall.push(name);
|
|
712
|
-
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if (prevMcp.configHash !== mcpConfigHash(manifestMcps[name])) {
|
|
713
828
|
mcps.toUpdate.push(name);
|
|
714
|
-
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
const missing = mcpMissingInTargets(manifestMcps[name], (prevMcp.targets ?? []));
|
|
832
|
+
if (missing.length > 0) {
|
|
833
|
+
mcps.toUpdate.push(name);
|
|
834
|
+
mcps.missing.push(name);
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
715
837
|
mcps.unchanged.push(name);
|
|
838
|
+
}
|
|
716
839
|
}
|
|
717
840
|
for (const name of Object.keys(lock.mcps ?? {})) {
|
|
718
841
|
if (!manifestMcps[name])
|
package/dist/lib.js
CHANGED
|
@@ -5,7 +5,7 @@ import { dirname, join, relative, sep } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
export const PRODUCT_NAME = "alegra-data-agent";
|
|
7
7
|
export const COMMAND_NAME = "alegra-data-agent";
|
|
8
|
-
export const REPO_SLUG = "
|
|
8
|
+
export const REPO_SLUG = "Alegra-Team/data-agent-selfservice";
|
|
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
11
|
export const ALL_TARGETS = [
|
|
@@ -19,12 +19,6 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
19
19
|
export function getPackageRoot() {
|
|
20
20
|
return join(__dirname, "..");
|
|
21
21
|
}
|
|
22
|
-
export function getRegistryDir() {
|
|
23
|
-
return join(getPackageRoot(), "skills-registry");
|
|
24
|
-
}
|
|
25
|
-
export function getManifestPath() {
|
|
26
|
-
return join(getRegistryDir(), "index.json");
|
|
27
|
-
}
|
|
28
22
|
export function getClaudeSkillsDir() {
|
|
29
23
|
return join(homedir(), ".claude", "skills");
|
|
30
24
|
}
|
|
@@ -59,6 +53,9 @@ export function getOpenCodeConfigPath() {
|
|
|
59
53
|
return join(base, "opencode", "opencode.json");
|
|
60
54
|
}
|
|
61
55
|
export function getAntigravitySkillsDir() {
|
|
56
|
+
return join(homedir(), ".gemini", "antigravity-cli", "skills");
|
|
57
|
+
}
|
|
58
|
+
export function getAntigravityGeminiSkillsDir() {
|
|
62
59
|
return join(homedir(), ".gemini", "skills");
|
|
63
60
|
}
|
|
64
61
|
export function getAntigravityMcpConfigPath() {
|
|
@@ -142,6 +139,12 @@ export function getConfigDir() {
|
|
|
142
139
|
export function getAuthFilePath() {
|
|
143
140
|
return join(getConfigDir(), "auth.json");
|
|
144
141
|
}
|
|
142
|
+
export function getLastCheckPath() {
|
|
143
|
+
return join(getConfigDir(), "last-check.json");
|
|
144
|
+
}
|
|
145
|
+
export function getManifestCachePath() {
|
|
146
|
+
return join(getConfigDir(), "manifest-cache.json");
|
|
147
|
+
}
|
|
145
148
|
export function getAutoUpdateLogPath() {
|
|
146
149
|
return join(getConfigDir(), "autoupdate.log");
|
|
147
150
|
}
|
|
@@ -158,31 +161,29 @@ export function getPackageVersion() {
|
|
|
158
161
|
}
|
|
159
162
|
return _cachedVersion;
|
|
160
163
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return null;
|
|
165
|
-
try {
|
|
166
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
167
|
-
}
|
|
168
|
-
catch {
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
164
|
+
let _lockCache = null;
|
|
165
|
+
function emptyLock() {
|
|
166
|
+
return { schemaVersion: 1, productName: PRODUCT_NAME, skills: {} };
|
|
171
167
|
}
|
|
172
168
|
export function readLockFile() {
|
|
169
|
+
if (_lockCache)
|
|
170
|
+
return _lockCache;
|
|
173
171
|
const path = getLockFilePath();
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
172
|
+
let lock = emptyLock();
|
|
173
|
+
if (existsSync(path)) {
|
|
174
|
+
try {
|
|
175
|
+
lock = JSON.parse(readFileSync(path, "utf-8"));
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
lock = emptyLock();
|
|
179
|
+
}
|
|
182
180
|
}
|
|
181
|
+
_lockCache = lock;
|
|
182
|
+
return lock;
|
|
183
183
|
}
|
|
184
184
|
export function writeLockFile(lock) {
|
|
185
185
|
const path = getLockFilePath();
|
|
186
|
+
_lockCache = lock;
|
|
186
187
|
mkdirSync(dirname(path), { recursive: true });
|
|
187
188
|
writeFileSync(path, `${JSON.stringify(lock, null, 2)}\n`, "utf-8");
|
|
188
189
|
}
|
package/dist/main.js
CHANGED
|
@@ -37,6 +37,7 @@ function parseArgs(argv) {
|
|
|
37
37
|
noHook: false,
|
|
38
38
|
noBounce: false,
|
|
39
39
|
autoUpdate: false,
|
|
40
|
+
background: false,
|
|
40
41
|
};
|
|
41
42
|
const positional = [];
|
|
42
43
|
const only = [];
|
|
@@ -56,6 +57,8 @@ function parseArgs(argv) {
|
|
|
56
57
|
flags.noBounce = true;
|
|
57
58
|
else if (a === "--auto-update")
|
|
58
59
|
flags.autoUpdate = true;
|
|
60
|
+
else if (a === "--background")
|
|
61
|
+
flags.background = true;
|
|
59
62
|
else if (a === "-h" || a === "--help")
|
|
60
63
|
flags.help = true;
|
|
61
64
|
else if (a === "-v" || a === "--version")
|
|
@@ -128,6 +131,7 @@ async function main() {
|
|
|
128
131
|
exitCode = await runUpdate({
|
|
129
132
|
yes: args.flags.yes,
|
|
130
133
|
dryRun: args.flags.dryRun,
|
|
134
|
+
background: args.flags.background,
|
|
131
135
|
targets: args.targets.length > 0 ? args.targets : undefined,
|
|
132
136
|
});
|
|
133
137
|
break;
|
package/dist/render.js
CHANGED
|
@@ -81,6 +81,9 @@ export function renderMcpForTarget(mcp, spec) {
|
|
|
81
81
|
obj.type = "remote";
|
|
82
82
|
obj.enabled = true;
|
|
83
83
|
}
|
|
84
|
+
else if (spec.mcpHttpTypeValue) {
|
|
85
|
+
obj.type = spec.mcpHttpTypeValue;
|
|
86
|
+
}
|
|
84
87
|
if (mcp.auth.type === "gateway") {
|
|
85
88
|
if (!mcp.auth.tokenEnv) {
|
|
86
89
|
throw new Error(`MCP '${mcp.name}' es type=gateway pero no trae tokenEnv`);
|
package/dist/signature.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { createPublicKey, verify } from "node:crypto";
|
|
2
|
-
export const SIGNING_KEY_ID = "alegra-data-agent-
|
|
3
|
-
export const
|
|
2
|
+
export const SIGNING_KEY_ID = "alegra-data-agent-v2";
|
|
3
|
+
export const TRUSTED_KEYS = [
|
|
4
|
+
{
|
|
5
|
+
keyId: "alegra-data-agent-v2",
|
|
6
|
+
publicKeyB64: "MCowBQYDK2VwAyEA5nrWo7oWlhyad9rJjQHqgL0xaPZ83Yn5dJotGXxCo0c=",
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
keyId: "alegra-data-agent-v1",
|
|
10
|
+
publicKeyB64: "MCowBQYDK2VwAyEApoojQPy6INQXl/xZ0gQumsI3tt9HvMrb0PaVlNn7TkE=",
|
|
11
|
+
},
|
|
12
|
+
];
|
|
13
|
+
export const PUBLIC_KEY_B64 = TRUSTED_KEYS[0].publicKeyB64;
|
|
4
14
|
export function canonicalize(obj) {
|
|
5
15
|
if (obj === null || typeof obj !== "object")
|
|
6
16
|
return JSON.stringify(obj);
|
|
@@ -25,10 +35,12 @@ export function verifyManifestSignature(manifest, options = {}) {
|
|
|
25
35
|
if (sig.alg !== "Ed25519") {
|
|
26
36
|
return { ok: false, reason: `algoritmo no soportado: ${sig.alg}` };
|
|
27
37
|
}
|
|
28
|
-
|
|
38
|
+
const keys = options.trustedKeys ?? TRUSTED_KEYS;
|
|
39
|
+
const trusted = keys.find((k) => k.keyId === sig.keyId);
|
|
40
|
+
if (!trusted) {
|
|
29
41
|
return {
|
|
30
42
|
ok: false,
|
|
31
|
-
reason: `keyId no reconocido: ${sig.keyId} (CLI
|
|
43
|
+
reason: `keyId no reconocido: ${sig.keyId} (este CLI acepta ${keys.map((k) => k.keyId).join(", ")})`,
|
|
32
44
|
};
|
|
33
45
|
}
|
|
34
46
|
if (typeof manifest.generatedAt === "string") {
|
|
@@ -49,7 +61,7 @@ export function verifyManifestSignature(manifest, options = {}) {
|
|
|
49
61
|
}
|
|
50
62
|
const canonical = canonicalize(manifest);
|
|
51
63
|
const publicKey = createPublicKey({
|
|
52
|
-
key: Buffer.from(
|
|
64
|
+
key: Buffer.from(trusted.publicKeyB64, "base64"),
|
|
53
65
|
format: "der",
|
|
54
66
|
type: "spki",
|
|
55
67
|
});
|
package/dist/targets.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { getAntigravityMcpConfigPath, getAntigravitySkillsDir, getClaudeJsonPath, getClaudeSkillsDir, getCodexConfigPath, getCodexSkillsDir, getCursorMcpPath, getCursorSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
|
|
4
|
+
import { getAntigravityGeminiSkillsDir, getAntigravityMcpConfigPath, getAntigravitySkillsDir, getClaudeJsonPath, getClaudeSkillsDir, getCodexConfigPath, getCodexSkillsDir, getCursorMcpPath, getCursorSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
|
|
5
5
|
export const TARGETS = {
|
|
6
6
|
"claude-code": {
|
|
7
7
|
id: "claude-code",
|
|
8
8
|
label: "Claude Code",
|
|
9
|
+
mcpHttpTypeValue: "http",
|
|
9
10
|
skillsDir: getClaudeSkillsDir(),
|
|
10
11
|
skillFormat: "folder",
|
|
11
12
|
mcpConfigPath: getClaudeJsonPath(),
|
|
@@ -14,6 +15,7 @@ export const TARGETS = {
|
|
|
14
15
|
},
|
|
15
16
|
cursor: {
|
|
16
17
|
id: "cursor",
|
|
18
|
+
mcpHttpTypeValue: "http",
|
|
17
19
|
label: "Cursor",
|
|
18
20
|
skillsDir: getCursorSkillsDir(),
|
|
19
21
|
skillFormat: "folder",
|
|
@@ -35,6 +37,7 @@ export const TARGETS = {
|
|
|
35
37
|
id: "antigravity",
|
|
36
38
|
label: "Antigravity",
|
|
37
39
|
skillsDir: getAntigravitySkillsDir(),
|
|
40
|
+
mirrorSkillsDirs: [getAntigravityGeminiSkillsDir()],
|
|
38
41
|
skillFormat: "folder",
|
|
39
42
|
mcpConfigPath: getAntigravityMcpConfigPath(),
|
|
40
43
|
mcpRootKey: "mcpServers",
|
|
@@ -60,15 +63,15 @@ export function detectInstalledTargets() {
|
|
|
60
63
|
? process.env.CODEX_HOME
|
|
61
64
|
: join(home, ".codex");
|
|
62
65
|
const sentinels = {
|
|
63
|
-
"claude-code": join(home, ".claude"),
|
|
64
|
-
cursor: join(home, ".cursor"),
|
|
65
|
-
opencode: join(xdgConfig, "opencode"),
|
|
66
|
-
antigravity: join(home, ".gemini", "antigravity"),
|
|
67
|
-
codex: codexHome,
|
|
66
|
+
"claude-code": [join(home, ".claude")],
|
|
67
|
+
cursor: [join(home, ".cursor")],
|
|
68
|
+
opencode: [join(xdgConfig, "opencode")],
|
|
69
|
+
antigravity: [join(home, ".gemini", "antigravity-cli"), join(home, ".gemini", "antigravity")],
|
|
70
|
+
codex: [codexHome],
|
|
68
71
|
};
|
|
69
72
|
const out = [];
|
|
70
73
|
for (const id of Object.keys(sentinels)) {
|
|
71
|
-
if (
|
|
74
|
+
if (sentinels[id].some((p) => existsSync(p)))
|
|
72
75
|
out.push(id);
|
|
73
76
|
}
|
|
74
77
|
return out;
|
package/dist/ui.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alegra-data-agent",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
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,31 +19,34 @@
|
|
|
19
19
|
"README.md"
|
|
20
20
|
],
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@types/node": "
|
|
23
|
-
"typescript": "
|
|
22
|
+
"@types/node": "26.1.2",
|
|
23
|
+
"typescript": "7.0.2"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|
|
27
|
-
"url": "git+https://github.com/
|
|
27
|
+
"url": "git+https://github.com/Alegra-Team/data-agent-selfservice.git"
|
|
28
28
|
},
|
|
29
|
-
"homepage": "https://github.com/
|
|
29
|
+
"homepage": "https://github.com/Alegra-Team/data-agent-selfservice#readme",
|
|
30
30
|
"bugs": {
|
|
31
|
-
"url": "https://github.com/
|
|
31
|
+
"url": "https://github.com/Alegra-Team/data-agent-selfservice/issues"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"typecheck": "tsc -p tsconfig.json",
|
|
35
35
|
"build": "tsc -p tsconfig.build.json",
|
|
36
36
|
"clean": "rm -rf dist",
|
|
37
37
|
"manifest:generate": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/generate-manifest.mjs",
|
|
38
|
-
"manifest:sign": "node scripts/sign-manifest.mjs",
|
|
38
|
+
"manifest:sign": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/sign-manifest.mjs",
|
|
39
|
+
"signing-key:generate": "node scripts/generate-signing-key.mjs",
|
|
39
40
|
"manifest:scan": "node scripts/scan-skills.mjs",
|
|
40
41
|
"manifest:validate": "node scripts/validate-skills.mjs --legacy-ok",
|
|
41
42
|
"manifest:validate:strict": "node scripts/validate-skills.mjs",
|
|
42
43
|
"manifest": "pnpm run manifest:validate && pnpm run manifest:scan && pnpm run manifest:generate && pnpm run manifest:sign",
|
|
43
44
|
"test": "node tests/runner.mjs",
|
|
44
45
|
"test:report": "node tests/runner.mjs --report markdown",
|
|
45
|
-
"test:security": "node tests/security/runner.mjs",
|
|
46
|
-
"test:all": "
|
|
46
|
+
"test:security": "node --experimental-strip-types --disable-warning=ExperimentalWarning tests/security/runner.mjs",
|
|
47
|
+
"test:all": "pnpm run test && pnpm run test:security",
|
|
48
|
+
"eval:run": "node tests/evals/run-phase2.mjs",
|
|
49
|
+
"verify:clients": "node scripts/verify-clients.mjs",
|
|
47
50
|
"lint": "pnpm dlx @biomejs/biome@1.9.4 check .",
|
|
48
51
|
"lint:fix": "pnpm dlx @biomejs/biome@1.9.4 check --write .",
|
|
49
52
|
"format": "pnpm dlx @biomejs/biome@1.9.4 format --write ."
|