@bitam/ebavel-apps 0.0.0-stage → 0.1.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/CHANGELOG.md +11 -0
- package/LICENSE +201 -0
- package/README.md +83 -2
- package/SDK.md +110 -0
- package/dist/chunk-FFVQA4KG.js +9 -0
- package/dist/ebavel-runner.html +92 -0
- package/dist/ebavel.js +3 -0
- package/dist/index.d.ts +160 -0
- package/dist/index.js +729 -0
- package/dist/resource.d.ts +25 -0
- package/dist/resource.js +41 -0
- package/examples/solicitudes.html +382 -0
- package/package.json +56 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CDN_ORIGINS
|
|
3
|
+
} from "./chunk-FFVQA4KG.js";
|
|
4
|
+
|
|
5
|
+
// src/sdk/errors.ts
|
|
6
|
+
var ERROR_CODES = ["NOT_CONNECTED", "DECLINED", "TOOL_ERROR", "UNSUPPORTED", "TIMEOUT", "ABORTED", "INVALID_ARGS"];
|
|
7
|
+
var isErrorCode = (v) => typeof v === "string" && ERROR_CODES.includes(v);
|
|
8
|
+
var MAX_MESSAGE = 500;
|
|
9
|
+
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
10
|
+
var clip = (s, max = MAX_MESSAGE) => s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
11
|
+
var EbavelAppError = class extends Error {
|
|
12
|
+
code;
|
|
13
|
+
/** Cuerpo del error de la tool (p. ej. `{ success: false, errors: [...] }`), si lo hubo. */
|
|
14
|
+
details;
|
|
15
|
+
constructor(code, message, details) {
|
|
16
|
+
super(clip(message));
|
|
17
|
+
this.name = "EbavelAppError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
if (details !== void 0) this.details = details;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
function toAppError(e, fallback = "TOOL_ERROR") {
|
|
23
|
+
if (e instanceof EbavelAppError) return e;
|
|
24
|
+
if (e instanceof Error) return new EbavelAppError(fallback, e.message);
|
|
25
|
+
if (isRecord(e) && typeof e.message === "string") return new EbavelAppError(fallback, e.message);
|
|
26
|
+
return new EbavelAppError(fallback, typeof e === "string" ? e : "Error desconocido");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/sdk/result.ts
|
|
30
|
+
function textOf(res) {
|
|
31
|
+
const content = Array.isArray(res.content) ? res.content : [];
|
|
32
|
+
const item = content.find((x) => isRecord(x) && x.type === "text" && typeof x.text === "string");
|
|
33
|
+
return item?.text;
|
|
34
|
+
}
|
|
35
|
+
function errorMessageOf(body, fallback) {
|
|
36
|
+
if (isRecord(body)) {
|
|
37
|
+
const first = Array.isArray(body.errors) ? body.errors[0] : void 0;
|
|
38
|
+
if (isRecord(first) && typeof first.message === "string") return typeof first.code === "string" ? `${first.code}: ${first.message}` : first.message;
|
|
39
|
+
if (isRecord(body.error) && typeof body.error.message === "string") return body.error.message;
|
|
40
|
+
if (typeof body.error === "string") return body.error;
|
|
41
|
+
if (typeof body.message === "string") return body.message;
|
|
42
|
+
}
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
function unwrapToolResult(res) {
|
|
46
|
+
if (!isRecord(res)) throw new EbavelAppError("TOOL_ERROR", "La tool devolvi\xF3 una respuesta que no es un objeto.");
|
|
47
|
+
let payload = res.payload ?? res.structuredContent;
|
|
48
|
+
const text = textOf(res);
|
|
49
|
+
if ((payload === void 0 || payload === null) && text !== void 0) {
|
|
50
|
+
try {
|
|
51
|
+
payload = JSON.parse(text);
|
|
52
|
+
} catch {
|
|
53
|
+
if (res.isError === true) throw new EbavelAppError("TOOL_ERROR", clip(text));
|
|
54
|
+
throw new EbavelAppError("TOOL_ERROR", "La tool devolvi\xF3 un texto que no es JSON.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (res.isError === true) throw new EbavelAppError("TOOL_ERROR", errorMessageOf(payload, text ?? "La tool devolvi\xF3 un error."), payload);
|
|
58
|
+
if (payload === void 0 || payload === null) throw new EbavelAppError("TOOL_ERROR", "La tool no devolvi\xF3 un resultado.");
|
|
59
|
+
return payload;
|
|
60
|
+
}
|
|
61
|
+
function assertSuccessBody(payload) {
|
|
62
|
+
if (!isRecord(payload)) throw new EbavelAppError("TOOL_ERROR", "La respuesta de la tool no es un objeto.");
|
|
63
|
+
if (payload.success === false || payload.error !== void 0) throw new EbavelAppError("TOOL_ERROR", errorMessageOf(payload, "La tool devolvi\xF3 un error."), payload);
|
|
64
|
+
return payload;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/sdk/url.ts
|
|
68
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1"]);
|
|
69
|
+
var isDevBuild = () => typeof __EBAVEL_DEV__ === "boolean" && __EBAVEL_DEV__;
|
|
70
|
+
function safeUrl(v) {
|
|
71
|
+
if (typeof v !== "string" || v.length > 4096) return null;
|
|
72
|
+
try {
|
|
73
|
+
const u = new URL(v);
|
|
74
|
+
return u.protocol === "https:" || isDevBuild() && u.protocol === "http:" && LOCAL_HOSTS.has(u.hostname) ? u.toString() : null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/sdk/claude.ts
|
|
81
|
+
var SIGNATURE_TOOLS = ["query", "describe_form"];
|
|
82
|
+
var REJECTION_CODES = {
|
|
83
|
+
bad_request: "TOOL_ERROR",
|
|
84
|
+
server_not_connected: "NOT_CONNECTED",
|
|
85
|
+
needs_reauth: "NOT_CONNECTED",
|
|
86
|
+
selection_required: "NOT_CONNECTED",
|
|
87
|
+
not_in_manifest: "DECLINED",
|
|
88
|
+
approval_required: "DECLINED",
|
|
89
|
+
consent_required: "DECLINED",
|
|
90
|
+
blocked_by_policy: "DECLINED"
|
|
91
|
+
};
|
|
92
|
+
function mapClaudeRejection(e) {
|
|
93
|
+
if (e instanceof EbavelAppError) return e;
|
|
94
|
+
const code = isRecord(e) && typeof e.code === "string" ? e.code : void 0;
|
|
95
|
+
const message = isRecord(e) && typeof e.message === "string" ? e.message : e instanceof Error ? e.message : typeof e === "string" ? e : "Error de Claude";
|
|
96
|
+
const mapped = (code !== void 0 ? REJECTION_CODES[code] : void 0) ?? "TOOL_ERROR";
|
|
97
|
+
const hint = mapped === "NOT_CONNECTED" ? " Conecta (o vuelve a autorizar) el conector eBavel en claude.ai." : mapped === "DECLINED" ? " Acepta el permiso de conectores que muestra claude.ai al abrir el artifact." : "";
|
|
98
|
+
return new EbavelAppError(mapped, `${code ? `[${code}] ` : ""}${message}${hint}`);
|
|
99
|
+
}
|
|
100
|
+
var isMcp = (v) => isRecord(v) && typeof v.callTool === "function" && typeof v.listTools === "function";
|
|
101
|
+
function candidatesOf(listing) {
|
|
102
|
+
const servers = isRecord(listing) && Array.isArray(listing.servers) ? listing.servers : [];
|
|
103
|
+
const all = [];
|
|
104
|
+
const matching = [];
|
|
105
|
+
for (const s of servers) {
|
|
106
|
+
if (!isRecord(s) || typeof s.server !== "string") continue;
|
|
107
|
+
all.push(s.server);
|
|
108
|
+
const names = new Set((Array.isArray(s.tools) ? s.tools : []).map((t) => isRecord(t) ? t.name : t));
|
|
109
|
+
if (SIGNATURE_TOOLS.every((t) => names.has(t))) matching.push(s.server);
|
|
110
|
+
}
|
|
111
|
+
return { all, matching };
|
|
112
|
+
}
|
|
113
|
+
function createClaudeConductor(opts) {
|
|
114
|
+
let mcpPromise;
|
|
115
|
+
let discovered;
|
|
116
|
+
const getMcp = () => {
|
|
117
|
+
mcpPromise ??= (async () => {
|
|
118
|
+
let mcp;
|
|
119
|
+
try {
|
|
120
|
+
mcp = await opts.claude.use("mcp");
|
|
121
|
+
} catch (e) {
|
|
122
|
+
mcpPromise = void 0;
|
|
123
|
+
throw mapClaudeRejection(e);
|
|
124
|
+
}
|
|
125
|
+
if (!isMcp(mcp)) {
|
|
126
|
+
mcpPromise = void 0;
|
|
127
|
+
throw new EbavelAppError("NOT_CONNECTED", 'Claude no expone el acceso a conectores MCP (`window.claude.use("mcp")`). Declara el conector eBavel al crear el artifact.');
|
|
128
|
+
}
|
|
129
|
+
return mcp;
|
|
130
|
+
})();
|
|
131
|
+
return mcpPromise;
|
|
132
|
+
};
|
|
133
|
+
async function resolveServer(mcp) {
|
|
134
|
+
const fixed = opts.getServer();
|
|
135
|
+
if (fixed !== void 0) return fixed;
|
|
136
|
+
if (discovered !== void 0) return discovered;
|
|
137
|
+
let listing;
|
|
138
|
+
try {
|
|
139
|
+
listing = await mcp.listTools();
|
|
140
|
+
} catch (e) {
|
|
141
|
+
throw mapClaudeRejection(e);
|
|
142
|
+
}
|
|
143
|
+
const { all, matching } = candidatesOf(listing);
|
|
144
|
+
const only = matching[0];
|
|
145
|
+
if (matching.length === 1 && only !== void 0) {
|
|
146
|
+
discovered = only;
|
|
147
|
+
return only;
|
|
148
|
+
}
|
|
149
|
+
const list = (names) => names.length > 0 ? names.map((n) => `\xAB${n}\xBB`).join(", ") : "ninguno";
|
|
150
|
+
throw new EbavelAppError(
|
|
151
|
+
"NOT_CONNECTED",
|
|
152
|
+
matching.length === 0 ? `No hay ning\xFAn conector con las tools ${SIGNATURE_TOOLS.join(" y ")}. Conectores disponibles: ${list(all)}. Conecta eBavel en claude.ai o usa ebavel.configure({ server }).` : `Hay m\xE1s de un conector eBavel candidato: ${list(matching)}. Elige uno con ebavel.configure({ server: '\u2026' }).`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
async function call(tool, args) {
|
|
156
|
+
const mcp = await getMcp();
|
|
157
|
+
const server = await resolveServer(mcp);
|
|
158
|
+
let res;
|
|
159
|
+
try {
|
|
160
|
+
res = await mcp.callTool(server, tool, args);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
throw mapClaudeRejection(e);
|
|
163
|
+
}
|
|
164
|
+
return unwrapToolResult(res);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
host: "claude",
|
|
168
|
+
canExport: true,
|
|
169
|
+
query: (args) => call("query", args),
|
|
170
|
+
async export(input) {
|
|
171
|
+
const { html, title } = input.source(input.title);
|
|
172
|
+
const body = assertSuccessBody(await call("export_dashboard", { html, title }));
|
|
173
|
+
const importUrl = safeUrl(body.importUrl);
|
|
174
|
+
if (importUrl === null) throw new EbavelAppError("TOOL_ERROR", "La respuesta de export_dashboard no trae un enlace https v\xE1lido.");
|
|
175
|
+
const expiresAt = typeof body.expiresAt === "string" ? clip(body.expiresAt, 64) : void 0;
|
|
176
|
+
return { importUrl, ...expiresAt !== void 0 ? { expiresAt } : {}, opened: opts.open(importUrl) };
|
|
177
|
+
},
|
|
178
|
+
openLink(url) {
|
|
179
|
+
const safe = safeUrl(url);
|
|
180
|
+
if (safe === null) return Promise.reject(new EbavelAppError("INVALID_ARGS", "Solo se pueden abrir enlaces https."));
|
|
181
|
+
return Promise.resolve({ opened: opts.open(safe), url: safe });
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/sdk/limiter.ts
|
|
187
|
+
var MAX_IN_FLIGHT = 4;
|
|
188
|
+
var aborted = () => new EbavelAppError("ABORTED", "La operaci\xF3n se cancel\xF3.");
|
|
189
|
+
var Limiter = class {
|
|
190
|
+
constructor(max = MAX_IN_FLIGHT) {
|
|
191
|
+
this.max = max;
|
|
192
|
+
}
|
|
193
|
+
max;
|
|
194
|
+
active = 0;
|
|
195
|
+
waiting = [];
|
|
196
|
+
pump() {
|
|
197
|
+
while (this.active < this.max && this.waiting.length > 0) this.waiting.shift().start();
|
|
198
|
+
}
|
|
199
|
+
acquire(signal) {
|
|
200
|
+
if (signal?.aborted) return Promise.reject(aborted());
|
|
201
|
+
return new Promise((resolve, reject) => {
|
|
202
|
+
if (this.active < this.max) {
|
|
203
|
+
this.active++;
|
|
204
|
+
return resolve();
|
|
205
|
+
}
|
|
206
|
+
const entry = {
|
|
207
|
+
start: () => {
|
|
208
|
+
signal?.removeEventListener("abort", entry.cancel);
|
|
209
|
+
this.active++;
|
|
210
|
+
resolve();
|
|
211
|
+
},
|
|
212
|
+
cancel: () => {
|
|
213
|
+
const i = this.waiting.indexOf(entry);
|
|
214
|
+
if (i >= 0) this.waiting.splice(i, 1);
|
|
215
|
+
reject(aborted());
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
signal?.addEventListener("abort", entry.cancel, { once: true });
|
|
219
|
+
this.waiting.push(entry);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Ejecuta `fn` cuando haya un lugar libre. El plazo cuenta desde que arranca (no desde que espera en la cola).
|
|
224
|
+
* Un lugar se libera cuando `fn` termina, o al vencer el plazo; al cancelar, cuando `fn` termina (la llamada
|
|
225
|
+
* sigue en el anfitrión y cuenta contra su propio límite).
|
|
226
|
+
*/
|
|
227
|
+
async run(fn, opts) {
|
|
228
|
+
await this.acquire(opts.signal);
|
|
229
|
+
let released = false;
|
|
230
|
+
const release = () => {
|
|
231
|
+
if (released) return;
|
|
232
|
+
released = true;
|
|
233
|
+
this.active--;
|
|
234
|
+
this.pump();
|
|
235
|
+
};
|
|
236
|
+
let work;
|
|
237
|
+
try {
|
|
238
|
+
work = fn();
|
|
239
|
+
} catch (e) {
|
|
240
|
+
release();
|
|
241
|
+
throw e;
|
|
242
|
+
}
|
|
243
|
+
work.then(release, release);
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
const { signal } = opts;
|
|
246
|
+
const onAbort = () => {
|
|
247
|
+
cleanup();
|
|
248
|
+
reject(aborted());
|
|
249
|
+
};
|
|
250
|
+
const timer = setTimeout(() => {
|
|
251
|
+
cleanup();
|
|
252
|
+
release();
|
|
253
|
+
reject(new EbavelAppError("TIMEOUT", `El anfitri\xF3n no respondi\xF3 en ${Math.round(opts.timeoutMs / 1e3)} s.`));
|
|
254
|
+
}, opts.timeoutMs);
|
|
255
|
+
const cleanup = () => {
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
signal?.removeEventListener("abort", onAbort);
|
|
258
|
+
};
|
|
259
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
260
|
+
work.then(
|
|
261
|
+
(v) => {
|
|
262
|
+
cleanup();
|
|
263
|
+
resolve(v);
|
|
264
|
+
},
|
|
265
|
+
(e) => {
|
|
266
|
+
cleanup();
|
|
267
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// src/sdk/hostvars.ts
|
|
275
|
+
var VAR_RE = /^--[a-z0-9-]+$/;
|
|
276
|
+
var VALUE_RE = /^[^;{}<>\\]{1,200}$/;
|
|
277
|
+
var RESOURCE_RE = /url\s*\(|image-set\s*\(|@import/i;
|
|
278
|
+
var MAX_VARS = 200;
|
|
279
|
+
var LANG_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}$/;
|
|
280
|
+
var asTheme = (v) => v === "light" || v === "dark" ? v : void 0;
|
|
281
|
+
var asLang = (v) => typeof v === "string" && LANG_RE.test(v) ? v : void 0;
|
|
282
|
+
function sanitizeVars(vars) {
|
|
283
|
+
const out = {};
|
|
284
|
+
if (!isRecord(vars)) return out;
|
|
285
|
+
let n = 0;
|
|
286
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
287
|
+
if (n >= MAX_VARS) break;
|
|
288
|
+
if (typeof value === "string" && VAR_RE.test(name) && VALUE_RE.test(value) && !RESOURCE_RE.test(value)) {
|
|
289
|
+
out[name] = value;
|
|
290
|
+
n++;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/sdk/protocol.ts
|
|
297
|
+
var PROTOCOL_VERSION = 1;
|
|
298
|
+
var MAX_REQUEST_BYTES = 128 * 1024;
|
|
299
|
+
var MAX_ID_LENGTH = 64;
|
|
300
|
+
var envelope = (msg) => ({ ebavel: PROTOCOL_VERSION, ...msg });
|
|
301
|
+
var validId = (v) => typeof v === "string" && v.length > 0 && v.length <= MAX_ID_LENGTH;
|
|
302
|
+
function parseRunnerMessage(data) {
|
|
303
|
+
if (!isRecord(data) || data.ebavel !== PROTOCOL_VERSION) return null;
|
|
304
|
+
switch (data.type) {
|
|
305
|
+
case "ready": {
|
|
306
|
+
const theme = asTheme(data.theme);
|
|
307
|
+
const lang = asLang(data.lang);
|
|
308
|
+
if (!validId(data.id) || !theme || typeof data.canExport !== "boolean") return null;
|
|
309
|
+
return { type: "ready", id: data.id, theme, lang: lang ?? "es", vars: sanitizeVars(data.vars), canExport: data.canExport };
|
|
310
|
+
}
|
|
311
|
+
case "result":
|
|
312
|
+
return validId(data.id) && "value" in data ? { type: "result", id: data.id, value: data.value } : null;
|
|
313
|
+
case "error": {
|
|
314
|
+
if (!validId(data.id) || !isErrorCode(data.code) || typeof data.message !== "string") return null;
|
|
315
|
+
return { type: "error", id: data.id, code: data.code, message: clip(data.message), ..."details" in data ? { details: data.details } : {} };
|
|
316
|
+
}
|
|
317
|
+
case "theme": {
|
|
318
|
+
const theme = asTheme(data.theme);
|
|
319
|
+
return theme ? { type: "theme", theme, lang: asLang(data.lang) ?? "es", vars: sanitizeVars(data.vars) } : null;
|
|
320
|
+
}
|
|
321
|
+
default:
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/sdk/mcp-apps.ts
|
|
327
|
+
var HELLO_SCHEDULE = [0, 200, 500, 1e3, 2e3, 3500];
|
|
328
|
+
var MAX_REPORTED_HEIGHT = 4e3;
|
|
329
|
+
function idFactory() {
|
|
330
|
+
const salt = Math.random().toString(36).slice(2, 8);
|
|
331
|
+
let n = 0;
|
|
332
|
+
return () => `c${++n}-${salt}`;
|
|
333
|
+
}
|
|
334
|
+
function startSizeReporter(win, send) {
|
|
335
|
+
const root = win.document.documentElement;
|
|
336
|
+
let last = -1;
|
|
337
|
+
let scheduled = false;
|
|
338
|
+
const report = () => {
|
|
339
|
+
scheduled = false;
|
|
340
|
+
const h = Math.min(Math.ceil(root.getBoundingClientRect().height), MAX_REPORTED_HEIGHT);
|
|
341
|
+
if (h > 0 && h !== last) {
|
|
342
|
+
last = h;
|
|
343
|
+
send({ type: "size", height: h });
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const schedule = () => {
|
|
347
|
+
if (scheduled) return;
|
|
348
|
+
scheduled = true;
|
|
349
|
+
if (typeof win.requestAnimationFrame === "function") win.requestAnimationFrame(report);
|
|
350
|
+
else win.setTimeout(report, 16);
|
|
351
|
+
};
|
|
352
|
+
const RO = win.ResizeObserver;
|
|
353
|
+
if (typeof RO === "function") {
|
|
354
|
+
const ro = new RO(schedule);
|
|
355
|
+
ro.observe(root);
|
|
356
|
+
if (win.document.body) ro.observe(win.document.body);
|
|
357
|
+
else win.addEventListener("DOMContentLoaded", () => ro.observe(win.document.body), { once: true });
|
|
358
|
+
}
|
|
359
|
+
win.addEventListener("load", schedule, { once: true });
|
|
360
|
+
schedule();
|
|
361
|
+
}
|
|
362
|
+
function connectMcpApps(win, opts) {
|
|
363
|
+
const parent = win.parent;
|
|
364
|
+
const nextId = idFactory();
|
|
365
|
+
const pending = /* @__PURE__ */ new Map();
|
|
366
|
+
const helloIds = /* @__PURE__ */ new Set();
|
|
367
|
+
const timers = [];
|
|
368
|
+
let handshaken = false;
|
|
369
|
+
let firstDone = false;
|
|
370
|
+
let resolveFirst;
|
|
371
|
+
let resolveSettled;
|
|
372
|
+
const first = new Promise((r) => resolveFirst = r);
|
|
373
|
+
const settled = new Promise((r) => resolveSettled = r);
|
|
374
|
+
const send = (msg) => {
|
|
375
|
+
parent.postMessage(envelope(msg), "*");
|
|
376
|
+
};
|
|
377
|
+
const request = (build) => new Promise((resolve, reject) => {
|
|
378
|
+
const msg = build(nextId());
|
|
379
|
+
pending.set(msg.id, { resolve, reject });
|
|
380
|
+
try {
|
|
381
|
+
send(msg);
|
|
382
|
+
} catch (e) {
|
|
383
|
+
pending.delete(msg.id);
|
|
384
|
+
reject(toAppError(e));
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
const conductorFor = (canExport) => ({
|
|
388
|
+
host: "mcp-apps",
|
|
389
|
+
canExport,
|
|
390
|
+
async query(args) {
|
|
391
|
+
const m = await request((id) => ({ type: "query", id, args }));
|
|
392
|
+
if (m.type !== "result") throw new EbavelAppError("TOOL_ERROR", "Respuesta inesperada del runner.");
|
|
393
|
+
return m.value;
|
|
394
|
+
},
|
|
395
|
+
async export() {
|
|
396
|
+
const m = await request((id) => ({ type: "export", id }));
|
|
397
|
+
const v = m.type === "result" && typeof m.value === "object" && m.value !== null ? m.value : void 0;
|
|
398
|
+
const importUrl = v ? safeUrl(v.importUrl) : null;
|
|
399
|
+
if (!v || importUrl === null) throw new EbavelAppError("TOOL_ERROR", "Respuesta de exportaci\xF3n inv\xE1lida.");
|
|
400
|
+
return { importUrl, ...typeof v.expiresAt === "string" ? { expiresAt: v.expiresAt } : {}, opened: v.opened === true };
|
|
401
|
+
},
|
|
402
|
+
async openLink(url) {
|
|
403
|
+
const safe = safeUrl(url);
|
|
404
|
+
if (safe === null) throw new EbavelAppError("INVALID_ARGS", "Solo se pueden abrir enlaces https.");
|
|
405
|
+
const m = await request((id) => ({ type: "openLink", id, url: safe }));
|
|
406
|
+
const v = m.type === "result" && typeof m.value === "object" && m.value !== null ? m.value : void 0;
|
|
407
|
+
return { opened: v?.opened === true, url: safe };
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
const stop = () => {
|
|
411
|
+
for (const t of timers) win.clearTimeout(t);
|
|
412
|
+
timers.length = 0;
|
|
413
|
+
if (!handshaken) win.removeEventListener("message", onMessage);
|
|
414
|
+
resolveSettled();
|
|
415
|
+
};
|
|
416
|
+
function onMessage(ev) {
|
|
417
|
+
if (ev.source !== win.parent) return;
|
|
418
|
+
const msg = parseRunnerMessage(ev.data);
|
|
419
|
+
if (!msg) return;
|
|
420
|
+
if (msg.type === "theme") {
|
|
421
|
+
opts.onTheme({ theme: msg.theme, lang: msg.lang, vars: msg.vars });
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (msg.type === "ready") {
|
|
425
|
+
if (!helloIds.has(msg.id) || handshaken) return;
|
|
426
|
+
handshaken = true;
|
|
427
|
+
startSizeReporter(win, send);
|
|
428
|
+
const hs = { conductor: conductorFor(msg.canExport), theme: msg.theme, lang: msg.lang, vars: msg.vars };
|
|
429
|
+
for (const t of timers) win.clearTimeout(t);
|
|
430
|
+
timers.length = 0;
|
|
431
|
+
if (firstDone) opts.onLate(hs);
|
|
432
|
+
else {
|
|
433
|
+
firstDone = true;
|
|
434
|
+
resolveFirst(hs);
|
|
435
|
+
}
|
|
436
|
+
resolveSettled();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const entry = pending.get(msg.id);
|
|
440
|
+
if (!entry) return;
|
|
441
|
+
pending.delete(msg.id);
|
|
442
|
+
if (msg.type === "error") entry.reject(new EbavelAppError(msg.code, msg.message, msg.details));
|
|
443
|
+
else entry.resolve(msg);
|
|
444
|
+
}
|
|
445
|
+
win.addEventListener("message", onMessage);
|
|
446
|
+
for (const at of HELLO_SCHEDULE) {
|
|
447
|
+
if (at >= opts.retryMs) break;
|
|
448
|
+
const fire = () => {
|
|
449
|
+
if (handshaken) return;
|
|
450
|
+
const id = nextId();
|
|
451
|
+
helloIds.add(id);
|
|
452
|
+
try {
|
|
453
|
+
send({ type: "hello", id });
|
|
454
|
+
} catch {
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
if (at === 0) fire();
|
|
458
|
+
else timers.push(win.setTimeout(fire, at));
|
|
459
|
+
}
|
|
460
|
+
timers.push(
|
|
461
|
+
win.setTimeout(() => {
|
|
462
|
+
if (!firstDone) {
|
|
463
|
+
firstDone = true;
|
|
464
|
+
resolveFirst(null);
|
|
465
|
+
}
|
|
466
|
+
}, Math.min(opts.helloTimeoutMs, opts.retryMs))
|
|
467
|
+
);
|
|
468
|
+
timers.push(win.setTimeout(stop, opts.retryMs));
|
|
469
|
+
return { first, settled };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/sdk/source.ts
|
|
473
|
+
var SOURCE_TEMPLATE_ID = "ebavel-source";
|
|
474
|
+
var MAX_HTML_BYTES = 512 * 1024;
|
|
475
|
+
var MAX_TITLE = 120;
|
|
476
|
+
var CRED_KEY = /["'`]\s*cred\s*["'`]|(?:^|[^\w$.-])cred\s*:/;
|
|
477
|
+
var hasClaudeCredential = (s) => s.includes("__FRAME_") || CRED_KEY.test(s);
|
|
478
|
+
var MISSING_TEMPLATE = 'Para exportar desde Claude, envuelve tu dashboard en <template id="ebavel-source"> (ver SDK.md). El SDK nunca env\xEDa el documento completo: trae credenciales de Claude.';
|
|
479
|
+
var utf8Length = (s) => new TextEncoder().encode(s).length;
|
|
480
|
+
var normalizeTitle = (s) => clip(s.replace(/\s+/g, " ").trim() || "Dashboard", MAX_TITLE);
|
|
481
|
+
function createSource(win) {
|
|
482
|
+
const doc = win.document;
|
|
483
|
+
let snapshot;
|
|
484
|
+
const take = () => {
|
|
485
|
+
const all = Array.from(doc.querySelectorAll(`[id="${SOURCE_TEMPLATE_ID}"]`));
|
|
486
|
+
if (all.length > 1) return { kind: "duplicate" };
|
|
487
|
+
const el = all[0];
|
|
488
|
+
if (el !== void 0 && el.tagName === "TEMPLATE" && (el.parentElement === doc.head || el.parentElement === doc.body)) return { kind: "html", html: el.innerHTML };
|
|
489
|
+
return { kind: "none" };
|
|
490
|
+
};
|
|
491
|
+
const titleOf = () => normalizeTitle(doc.title || doc.querySelector("h1")?.textContent || "");
|
|
492
|
+
return {
|
|
493
|
+
capture() {
|
|
494
|
+
snapshot ??= take();
|
|
495
|
+
},
|
|
496
|
+
get(titleOverride) {
|
|
497
|
+
snapshot ??= take();
|
|
498
|
+
if (snapshot.kind === "duplicate") throw new EbavelAppError("INVALID_ARGS", `Hay m\xE1s de un elemento con id="${SOURCE_TEMPLATE_ID}": solo puede haber un <template>.`);
|
|
499
|
+
if (snapshot.kind === "none") throw new EbavelAppError("INVALID_ARGS", MISSING_TEMPLATE);
|
|
500
|
+
const title = titleOverride === void 0 ? titleOf() : normalizeTitle(titleOverride);
|
|
501
|
+
const titleEl = doc.createElement("title");
|
|
502
|
+
titleEl.textContent = title;
|
|
503
|
+
const html = `<!doctype html>
|
|
504
|
+
<html lang="${asLang(doc.documentElement.lang) ?? "es"}"><head><meta charset="utf-8">${titleEl.outerHTML}</head><body>${snapshot.html}</body></html>`;
|
|
505
|
+
if (hasClaudeCredential(html)) {
|
|
506
|
+
throw new EbavelAppError("INVALID_ARGS", "El HTML a exportar contiene `__FRAME_` o una llave `cred` (la credencial del frame de Claude, o texto que se le parece); no se envi\xF3 ni se modific\xF3 nada.");
|
|
507
|
+
}
|
|
508
|
+
if (utf8Length(html) > MAX_HTML_BYTES) throw new EbavelAppError("INVALID_ARGS", `El HTML supera el m\xE1ximo de ${MAX_HTML_BYTES / 1024} KB.`);
|
|
509
|
+
return { html, title };
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/sdk/theme.ts
|
|
515
|
+
function createThemeApplier(doc) {
|
|
516
|
+
let applied = /* @__PURE__ */ new Set();
|
|
517
|
+
return {
|
|
518
|
+
apply(theme, vars) {
|
|
519
|
+
const root = doc.documentElement;
|
|
520
|
+
root.setAttribute("data-theme", theme);
|
|
521
|
+
root.style.colorScheme = theme;
|
|
522
|
+
const now = /* @__PURE__ */ new Set();
|
|
523
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
524
|
+
root.style.setProperty(name, value);
|
|
525
|
+
now.add(name);
|
|
526
|
+
}
|
|
527
|
+
for (const name of applied) if (!now.has(name)) root.style.removeProperty(name);
|
|
528
|
+
applied = now;
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
function systemTheme(win) {
|
|
533
|
+
try {
|
|
534
|
+
return typeof win.matchMedia === "function" && win.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
535
|
+
} catch {
|
|
536
|
+
return "light";
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/sdk/version.ts
|
|
541
|
+
var VERSION = "0.1.0";
|
|
542
|
+
|
|
543
|
+
// src/sdk/core.ts
|
|
544
|
+
var SDK_MARK = /* @__PURE__ */ Symbol.for("@bitam/ebavel-apps");
|
|
545
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
546
|
+
var HELLO_TIMEOUT_MS = 1500;
|
|
547
|
+
var HELLO_RETRY_MS = 5e3;
|
|
548
|
+
var MAX_ARGS_BYTES = 64 * 1024;
|
|
549
|
+
var claudeOf = (win) => {
|
|
550
|
+
const c = win.claude;
|
|
551
|
+
return isRecord(c) && typeof c.use === "function" ? c : void 0;
|
|
552
|
+
};
|
|
553
|
+
var NO_HOST = "No hay anfitri\xF3n: abre el dashboard como artifact de Claude (con el conector eBavel) o dentro del runner MCP Apps.";
|
|
554
|
+
function openWindow(win, url) {
|
|
555
|
+
try {
|
|
556
|
+
if (win?.navigator?.userActivation?.isActive !== true) return false;
|
|
557
|
+
const w = win.open(url, "_blank");
|
|
558
|
+
if (!w) return false;
|
|
559
|
+
try {
|
|
560
|
+
w.opener = null;
|
|
561
|
+
} catch {
|
|
562
|
+
}
|
|
563
|
+
return true;
|
|
564
|
+
} catch {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function checkArgs(args) {
|
|
569
|
+
if (!isRecord(args)) throw new EbavelAppError("INVALID_ARGS", "ebavel.query espera un objeto: { queries: { alias: { entity, \u2026 } } }.");
|
|
570
|
+
let json;
|
|
571
|
+
try {
|
|
572
|
+
json = JSON.stringify(args);
|
|
573
|
+
} catch {
|
|
574
|
+
throw new EbavelAppError("INVALID_ARGS", "Los argumentos de ebavel.query no se pueden serializar a JSON.");
|
|
575
|
+
}
|
|
576
|
+
if (json.length > MAX_ARGS_BYTES) throw new EbavelAppError("INVALID_ARGS", `Los argumentos de ebavel.query superan ${MAX_ARGS_BYTES / 1024} KB.`);
|
|
577
|
+
}
|
|
578
|
+
function createSdk(win, options = {}) {
|
|
579
|
+
const config = { timeoutMs: DEFAULT_TIMEOUT_MS };
|
|
580
|
+
const limiter = new Limiter();
|
|
581
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
582
|
+
let theme = win ? systemTheme(win) : "light";
|
|
583
|
+
const applier = win ? createThemeApplier(win.document) : void 0;
|
|
584
|
+
const source = win ? createSource(win) : void 0;
|
|
585
|
+
let conductor = null;
|
|
586
|
+
let session;
|
|
587
|
+
let claudeSetUp = false;
|
|
588
|
+
if (win && source) {
|
|
589
|
+
const doc = win.document;
|
|
590
|
+
if (doc.readyState === "loading") doc.addEventListener("DOMContentLoaded", () => source.capture(), { once: true });
|
|
591
|
+
else source.capture();
|
|
592
|
+
}
|
|
593
|
+
const setTheme = (next, vars) => {
|
|
594
|
+
applier?.apply(next, vars);
|
|
595
|
+
if (next === theme) return;
|
|
596
|
+
theme = next;
|
|
597
|
+
for (const fn of [...listeners]) {
|
|
598
|
+
try {
|
|
599
|
+
fn(next);
|
|
600
|
+
} catch {
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
const lang = () => (win?.document.documentElement.lang || win?.navigator?.language || "es").slice(0, 35);
|
|
605
|
+
function setUpClaude(claude) {
|
|
606
|
+
if (!win || claudeSetUp) return;
|
|
607
|
+
claudeSetUp = true;
|
|
608
|
+
setTheme(systemTheme(win), {});
|
|
609
|
+
try {
|
|
610
|
+
win.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => setTheme(systemTheme(win), {}));
|
|
611
|
+
} catch {
|
|
612
|
+
}
|
|
613
|
+
conductor = createClaudeConductor({ claude, getServer: () => config.server, open: (url) => openWindow(win, url) });
|
|
614
|
+
}
|
|
615
|
+
async function connect() {
|
|
616
|
+
if (!win) return { host: "none", theme, lang: "es", canExport: false };
|
|
617
|
+
const claude = claudeOf(win);
|
|
618
|
+
if (claude) {
|
|
619
|
+
setUpClaude(claude);
|
|
620
|
+
return { host: "claude", theme, lang: lang(), canExport: true };
|
|
621
|
+
}
|
|
622
|
+
if (win.parent && win.parent !== win) {
|
|
623
|
+
session = connectMcpApps(win, {
|
|
624
|
+
helloTimeoutMs: options.helloTimeoutMs ?? HELLO_TIMEOUT_MS,
|
|
625
|
+
retryMs: options.helloRetryMs ?? HELLO_RETRY_MS,
|
|
626
|
+
onTheme: (t) => setTheme(t.theme, t.vars),
|
|
627
|
+
// El runner tardó en escuchar: se adopta cuando contesta (`ready` ya se cumplió con «none»).
|
|
628
|
+
onLate: (h) => {
|
|
629
|
+
conductor = h.conductor;
|
|
630
|
+
setTheme(h.theme, h.vars);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
const hs = await session.first;
|
|
634
|
+
if (hs) {
|
|
635
|
+
conductor = hs.conductor;
|
|
636
|
+
setTheme(hs.theme, hs.vars);
|
|
637
|
+
return { host: "mcp-apps", theme, lang: hs.lang, canExport: hs.conductor.canExport };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return { host: "none", theme, lang: lang(), canExport: false };
|
|
641
|
+
}
|
|
642
|
+
const ready = connect();
|
|
643
|
+
const getConductor = async () => {
|
|
644
|
+
await ready;
|
|
645
|
+
if (conductor) return conductor;
|
|
646
|
+
const claude = win ? claudeOf(win) : void 0;
|
|
647
|
+
if (claude) setUpClaude(claude);
|
|
648
|
+
if (!conductor && session) await session.settled;
|
|
649
|
+
if (!conductor) throw new EbavelAppError("NOT_CONNECTED", NO_HOST);
|
|
650
|
+
return conductor;
|
|
651
|
+
};
|
|
652
|
+
const sdk = {
|
|
653
|
+
version: VERSION,
|
|
654
|
+
configure(opts) {
|
|
655
|
+
if (!isRecord(opts)) throw new EbavelAppError("INVALID_ARGS", "ebavel.configure espera un objeto.");
|
|
656
|
+
if (opts.server !== void 0) {
|
|
657
|
+
if (typeof opts.server !== "string" || opts.server.trim() === "") throw new EbavelAppError("INVALID_ARGS", "`server` debe ser el nombre del conector (texto no vac\xEDo).");
|
|
658
|
+
config.server = opts.server;
|
|
659
|
+
}
|
|
660
|
+
if (opts.timeoutMs !== void 0) {
|
|
661
|
+
if (typeof opts.timeoutMs !== "number" || !Number.isFinite(opts.timeoutMs) || opts.timeoutMs < 100 || opts.timeoutMs > 6e5) {
|
|
662
|
+
throw new EbavelAppError("INVALID_ARGS", "`timeoutMs` debe estar entre 100 y 600000.");
|
|
663
|
+
}
|
|
664
|
+
config.timeoutMs = opts.timeoutMs;
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
ready,
|
|
668
|
+
async query(args, opts) {
|
|
669
|
+
checkArgs(args);
|
|
670
|
+
const signal = opts?.signal;
|
|
671
|
+
if (signal?.aborted) throw new EbavelAppError("ABORTED", "La operaci\xF3n se cancel\xF3.");
|
|
672
|
+
const c = await getConductor();
|
|
673
|
+
return await limiter.run(() => c.query(args), { ...signal ? { signal } : {}, timeoutMs: config.timeoutMs });
|
|
674
|
+
},
|
|
675
|
+
async export(opts) {
|
|
676
|
+
const rawTitle = opts?.title;
|
|
677
|
+
if (rawTitle !== void 0 && typeof rawTitle !== "string") throw new EbavelAppError("INVALID_ARGS", "`title` debe ser un texto.");
|
|
678
|
+
const title = typeof rawTitle === "string" ? normalizeTitle(rawTitle) : void 0;
|
|
679
|
+
const signal = opts?.signal;
|
|
680
|
+
if (signal?.aborted) throw new EbavelAppError("ABORTED", "La operaci\xF3n se cancel\xF3.");
|
|
681
|
+
const c = await getConductor();
|
|
682
|
+
if (!c.canExport) throw new EbavelAppError("UNSUPPORTED", "Este dashboard ya est\xE1 guardado: no se puede exportar desde aqu\xED.");
|
|
683
|
+
const input = {
|
|
684
|
+
source: (t) => {
|
|
685
|
+
if (!source) throw new EbavelAppError("INVALID_ARGS", "No hay documento del que exportar.");
|
|
686
|
+
return source.get(t);
|
|
687
|
+
},
|
|
688
|
+
title
|
|
689
|
+
};
|
|
690
|
+
return limiter.run(() => c.export(input), { ...signal ? { signal } : {}, timeoutMs: config.timeoutMs });
|
|
691
|
+
},
|
|
692
|
+
get theme() {
|
|
693
|
+
return theme;
|
|
694
|
+
},
|
|
695
|
+
onThemeChange(listener) {
|
|
696
|
+
if (typeof listener !== "function") throw new EbavelAppError("INVALID_ARGS", "onThemeChange espera una funci\xF3n.");
|
|
697
|
+
listeners.add(listener);
|
|
698
|
+
return () => void listeners.delete(listener);
|
|
699
|
+
},
|
|
700
|
+
async openLink(url) {
|
|
701
|
+
const c = await getConductor();
|
|
702
|
+
return c.openLink(url);
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
Object.defineProperty(sdk, SDK_MARK, { value: true });
|
|
706
|
+
return sdk;
|
|
707
|
+
}
|
|
708
|
+
function getEbavel(win = typeof window === "undefined" ? void 0 : window, options) {
|
|
709
|
+
const existing = win?.ebavel;
|
|
710
|
+
if (isRecord(existing) && existing[SDK_MARK] === true) return existing;
|
|
711
|
+
const sdk = createSdk(win, options);
|
|
712
|
+
if (win) Object.defineProperty(win, "ebavel", { value: sdk, configurable: true, writable: true, enumerable: false });
|
|
713
|
+
return sdk;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/sdk/index.ts
|
|
717
|
+
var ebavel = getEbavel();
|
|
718
|
+
export {
|
|
719
|
+
CDN_ORIGINS,
|
|
720
|
+
ERROR_CODES,
|
|
721
|
+
EbavelAppError,
|
|
722
|
+
PROTOCOL_VERSION,
|
|
723
|
+
SDK_MARK,
|
|
724
|
+
VERSION,
|
|
725
|
+
createSdk,
|
|
726
|
+
ebavel,
|
|
727
|
+
getEbavel,
|
|
728
|
+
isErrorCode
|
|
729
|
+
};
|