beplus-mcp 0.18.1 → 0.20.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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +518 -57
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -82,6 +82,7 @@ Reinicie o cliente. Rode a tool **`whoami`** para confirmar o vínculo.
82
82
  |------|-----------|
83
83
  | `generate_image` | Gera imagem (nano-banana / gpt-image-2). Bloqueante ~90s; retorna URL + imagem inline. Suporta refs, tamanho/qualidade, e extras do gpt-image-2 (background, moderation, output_format/compression). |
84
84
  | `generate_video` | Gera vídeo (Seedance / Kling). Aguarda ~120s; senão devolve o id pra `check_generation`. Suporta first/last frame, refs de imagem/vídeo/áudio, prompt negativo, áudio gerado, mode e motion-control. |
85
+ | `upscale` | Melhora a resolução de uma imagem ou vídeo que já existe (Topaz). Detecta o tipo pelo arquivo. Imagem até 4x (~20s, a partir de 6💎); vídeo até 30s e saída até 1080p, custa por bloco de 10s e **descarta o áudio**. Mede o arquivo sozinho para o preço sair certo. ⚠️ Vídeo é restrito à equipe. |
85
86
  | `generate_audio` | Sintetiza fala (Gemini TTS, ~30 vozes, multi-locutor). Síncrono. |
86
87
  | `generate_music` | Gera música completa (Suno v5.5 / v4.5) — modo descrição ou letra própria, tags, instrumental, vocal_gender, controles criativos. Async. |
87
88
  | `check_generation` | Status de uma geração async por id. |
package/dist/index.js CHANGED
@@ -515,6 +515,24 @@ var VIDEO_MODELS = [
515
515
  "kwaivgi/kling-v3-video",
516
516
  "kwaivgi/kling-v3-omni-video"
517
517
  ];
518
+ var UPSCALE_IMAGE_VARIANTS = [
519
+ "Wonder 3.5",
520
+ "Wonder 3",
521
+ "Wonder 2",
522
+ "Wonder",
523
+ "Recover 3",
524
+ "Standard MAX",
525
+ "Redefine",
526
+ "Recovery V2",
527
+ "Recovery"
528
+ ];
529
+ var UPSCALE_VIDEO_VARIANTS = [
530
+ "Starlight Precise 2.6",
531
+ "Starlight HQ",
532
+ "Starlight Mini",
533
+ "Starlight Sharp",
534
+ "Starlight Fast 2"
535
+ ];
518
536
  var MUSIC_MODELS = [
519
537
  "suno/v5-5",
520
538
  "suno/v4-5"
@@ -996,7 +1014,7 @@ ${list}${flagsLine(status)}` }, ...blocks] };
996
1014
  return textResult(`Nenhuma gera\xE7\xE3o${scope} encontrada.`);
997
1015
  }
998
1016
  const lines = generations.map((g, i) => {
999
- const type = g.generation_type || "image";
1017
+ const type = g.model?.startsWith("topaz/upscale") ? "upscale" : g.generation_type || "image";
1000
1018
  const when = g.created_at ? new Date(g.created_at).toISOString().slice(0, 10) : "\u2014";
1001
1019
  const prompt = (g.prompt || "").replace(/\s+/g, " ").slice(0, 80);
1002
1020
  const url = firstOutputUrl(g.output);
@@ -1098,8 +1116,415 @@ ${list}${flagsLine(status)}` }, ...blocks] };
1098
1116
  );
1099
1117
  }
1100
1118
 
1101
- // src/tools/audio.ts
1119
+ // src/tools/upscale.ts
1102
1120
  import { z as z2 } from "zod";
1121
+
1122
+ // src/probe.ts
1123
+ import { readFile as readFile2, open } from "fs/promises";
1124
+ import { extname as extname2, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
1125
+ import { homedir as homedir2 } from "os";
1126
+ import { fileURLToPath as fileURLToPath2 } from "url";
1127
+ var IMAGE_EXT = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp"]);
1128
+ var VIDEO_EXT = /* @__PURE__ */ new Set([".mp4", ".mov", ".m4v"]);
1129
+ var HEADER_BYTES = 128 * 1024;
1130
+ var MAX_SCAN_BYTES = 8 * 1024 * 1024;
1131
+ var FETCH_TIMEOUT_MS = 3e4;
1132
+ function isRemote(ref) {
1133
+ return /^https?:\/\//i.test(ref);
1134
+ }
1135
+ function toLocalPath2(ref) {
1136
+ if (ref.startsWith("file://")) return fileURLToPath2(ref);
1137
+ if (ref === "~") return homedir2();
1138
+ if (ref.startsWith("~/")) return resolve2(homedir2(), ref.slice(2));
1139
+ return isAbsolute2(ref) ? ref : resolve2(process.cwd(), ref);
1140
+ }
1141
+ function kindFromExt(ref) {
1142
+ const ext = extname2(new URL(ref, "file:///").pathname || ref).toLowerCase();
1143
+ if (IMAGE_EXT.has(ext)) return "image";
1144
+ if (VIDEO_EXT.has(ext)) return "video";
1145
+ return null;
1146
+ }
1147
+ async function readHead(ref, bytes) {
1148
+ if (!isRemote(ref)) {
1149
+ const fh = await open(toLocalPath2(ref), "r");
1150
+ try {
1151
+ const buf = Buffer.alloc(bytes);
1152
+ const { bytesRead } = await fh.read(buf, 0, bytes, 0);
1153
+ return buf.subarray(0, bytesRead);
1154
+ } finally {
1155
+ await fh.close();
1156
+ }
1157
+ }
1158
+ const ctrl = new AbortController();
1159
+ const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
1160
+ try {
1161
+ const res = await fetch(ref, {
1162
+ headers: { Range: `bytes=0-${bytes - 1}` },
1163
+ signal: ctrl.signal
1164
+ });
1165
+ if (!res.ok && res.status !== 206) throw new Error(`HTTP ${res.status}`);
1166
+ if (!res.body) return Buffer.from(await res.arrayBuffer());
1167
+ const chunks = [];
1168
+ let total = 0;
1169
+ const reader = res.body.getReader();
1170
+ while (total < bytes) {
1171
+ const { done, value } = await reader.read();
1172
+ if (done || !value) break;
1173
+ chunks.push(Buffer.from(value));
1174
+ total += value.length;
1175
+ }
1176
+ void reader.cancel().catch(() => {
1177
+ });
1178
+ return Buffer.concat(chunks).subarray(0, bytes);
1179
+ } finally {
1180
+ clearTimeout(timer);
1181
+ }
1182
+ }
1183
+ function probePng(b) {
1184
+ if (b.length < 24) return null;
1185
+ if (b.readUInt32BE(0) !== 2303741511) return null;
1186
+ if (b.toString("ascii", 12, 16) !== "IHDR") return null;
1187
+ return { width: b.readUInt32BE(16), height: b.readUInt32BE(20) };
1188
+ }
1189
+ function probeJpeg(b) {
1190
+ if (b.length < 4 || b[0] !== 255 || b[1] !== 216) return null;
1191
+ let i = 2;
1192
+ while (i + 9 < b.length) {
1193
+ if (b[i] !== 255) {
1194
+ i += 1;
1195
+ continue;
1196
+ }
1197
+ const marker = b[i + 1];
1198
+ if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
1199
+ return { height: b.readUInt16BE(i + 5), width: b.readUInt16BE(i + 7) };
1200
+ }
1201
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
1202
+ i += 2;
1203
+ continue;
1204
+ }
1205
+ i += 2 + b.readUInt16BE(i + 2);
1206
+ }
1207
+ return null;
1208
+ }
1209
+ function probeWebp(b) {
1210
+ if (b.length < 30) return null;
1211
+ if (b.toString("ascii", 0, 4) !== "RIFF" || b.toString("ascii", 8, 12) !== "WEBP") return null;
1212
+ const fmt2 = b.toString("ascii", 12, 16);
1213
+ if (fmt2 === "VP8 ") {
1214
+ return { width: b.readUInt16LE(26) & 16383, height: b.readUInt16LE(28) & 16383 };
1215
+ }
1216
+ if (fmt2 === "VP8L") {
1217
+ const bits = b.readUInt32LE(21);
1218
+ return { width: (bits & 16383) + 1, height: (bits >> 14 & 16383) + 1 };
1219
+ }
1220
+ if (fmt2 === "VP8X") {
1221
+ const w = b[24] | b[25] << 8 | b[26] << 16;
1222
+ const h = b[27] | b[28] << 8 | b[29] << 16;
1223
+ return { width: w + 1, height: h + 1 };
1224
+ }
1225
+ return null;
1226
+ }
1227
+ function probeMp4(b) {
1228
+ let duration;
1229
+ let width = 0;
1230
+ let height = 0;
1231
+ let frames = 0;
1232
+ let mediaDuration;
1233
+ const walk = (start, end, depth) => {
1234
+ let i = start;
1235
+ while (i + 8 <= end && depth < 8) {
1236
+ let size = b.readUInt32BE(i);
1237
+ const type = b.toString("ascii", i + 4, i + 8);
1238
+ let head = 8;
1239
+ if (size === 1) {
1240
+ if (i + 16 > end) return;
1241
+ size = Number(b.readBigUInt64BE(i + 8));
1242
+ head = 16;
1243
+ }
1244
+ if (size < head || i + size > end) {
1245
+ return;
1246
+ }
1247
+ if (type === "moov" || type === "trak" || type === "mdia" || type === "minf" || type === "stbl") {
1248
+ walk(i + head, i + size, depth + 1);
1249
+ } else if (type === "mvhd") {
1250
+ const version = b[i + head];
1251
+ if (version === 1) {
1252
+ const timescale = b.readUInt32BE(i + head + 20);
1253
+ const dur2 = Number(b.readBigUInt64BE(i + head + 24));
1254
+ if (timescale) duration = dur2 / timescale;
1255
+ } else {
1256
+ const timescale = b.readUInt32BE(i + head + 12);
1257
+ const dur2 = b.readUInt32BE(i + head + 16);
1258
+ if (timescale) duration = dur2 / timescale;
1259
+ }
1260
+ } else if (type === "tkhd") {
1261
+ const version = b[i + head];
1262
+ const matrixAt = i + head + (version === 1 ? 52 : 40);
1263
+ const w = b.readUInt32BE(matrixAt + 36) / 65536;
1264
+ const h = b.readUInt32BE(matrixAt + 40) / 65536;
1265
+ if (w > 0 && h > 0) {
1266
+ const a = b.readInt32BE(matrixAt) / 65536;
1267
+ const d = b.readInt32BE(matrixAt + 16) / 65536;
1268
+ const rotated = Math.abs(a) < 0.01 && Math.abs(d) < 0.01;
1269
+ const tw = Math.round(rotated ? h : w);
1270
+ const th = Math.round(rotated ? w : h);
1271
+ if (tw * th > width * height) {
1272
+ width = tw;
1273
+ height = th;
1274
+ }
1275
+ }
1276
+ } else if (type === "mdhd") {
1277
+ const version = b[i + head];
1278
+ if (version === 1) {
1279
+ const ts = b.readUInt32BE(i + head + 20);
1280
+ const dur2 = Number(b.readBigUInt64BE(i + head + 24));
1281
+ if (ts) mediaDuration = dur2 / ts;
1282
+ } else {
1283
+ const ts = b.readUInt32BE(i + head + 12);
1284
+ const dur2 = b.readUInt32BE(i + head + 16);
1285
+ if (ts) mediaDuration = dur2 / ts;
1286
+ }
1287
+ } else if (type === "stsz") {
1288
+ const count = b.readUInt32BE(i + head + 8);
1289
+ if (count > frames) frames = count;
1290
+ }
1291
+ i += size;
1292
+ }
1293
+ };
1294
+ walk(0, b.length, 0);
1295
+ if (!width || !height) return null;
1296
+ const dur = duration ?? mediaDuration;
1297
+ const fps = dur && frames ? Math.round(frames / dur * 100) / 100 : void 0;
1298
+ return { width, height, duration: dur, fps };
1299
+ }
1300
+ var ProbeError = class extends Error {
1301
+ };
1302
+ async function probeMedia(ref) {
1303
+ const kind = kindFromExt(ref);
1304
+ if (!kind) {
1305
+ throw new ProbeError(
1306
+ "Formato n\xE3o reconhecido pela extens\xE3o. Aceito: png, jpg, jpeg, webp, mp4, mov, m4v."
1307
+ );
1308
+ }
1309
+ let head;
1310
+ try {
1311
+ head = await readHead(ref, HEADER_BYTES);
1312
+ } catch (e) {
1313
+ throw new ProbeError(`N\xE3o consegui ler o arquivo: ${e instanceof Error ? e.message : String(e)}`);
1314
+ }
1315
+ if (!head.length) throw new ProbeError("Arquivo vazio.");
1316
+ if (kind === "image") {
1317
+ const dim = probePng(head) ?? probeJpeg(head) ?? probeWebp(head);
1318
+ if (!dim) throw new ProbeError("N\xE3o consegui ler as dimens\xF5es da imagem pelo cabe\xE7alho.");
1319
+ return { kind, width: dim.width, height: dim.height };
1320
+ }
1321
+ let info = probeMp4(head);
1322
+ if (!info && !isRemote(ref)) {
1323
+ try {
1324
+ const whole = await readFile2(toLocalPath2(ref));
1325
+ info = probeMp4(whole.subarray(0, Math.min(whole.length, MAX_SCAN_BYTES))) ?? probeMp4(whole.subarray(Math.max(0, whole.length - MAX_SCAN_BYTES)));
1326
+ } catch {
1327
+ }
1328
+ }
1329
+ if (!info) {
1330
+ throw new ProbeError(
1331
+ "N\xE3o consegui ler as dimens\xF5es do v\xEDdeo pelo cabe\xE7alho (o \xEDndice pode estar no fim do arquivo)."
1332
+ );
1333
+ }
1334
+ return { kind, width: info.width, height: info.height, duration: info.duration, fps: info.fps };
1335
+ }
1336
+
1337
+ // src/tools/upscale.ts
1338
+ var IMAGE_MODEL = "topaz/upscale/image/generative";
1339
+ var VIDEO_MODEL = "topaz/upscale/video/generative";
1340
+ var MAX_VIDEO_SECONDS = 300;
1341
+ var MAX_OUTPUT_HEIGHT = 1080;
1342
+ var IMAGE_BUDGET_MS2 = 9e4;
1343
+ var VIDEO_BUDGET_MS2 = 24e4;
1344
+ var DEFAULT_IMAGE_VARIANT = "Wonder 3";
1345
+ var DEFAULT_VIDEO_VARIANT = "Starlight Precise 2.6";
1346
+ var SOFTNESS_ONLY = "Starlight Precise 2.6";
1347
+ function registerUpscaleTools(server, client, cfg) {
1348
+ server.registerTool(
1349
+ "upscale",
1350
+ {
1351
+ title: "Melhorar resolu\xE7\xE3o (upscale Topaz)",
1352
+ description: "Aumenta a resolu\xE7\xE3o de uma IMAGEM ou de um V\xCDDEO que j\xE1 existe, com o Topaz. Passe o arquivo em `file` (caminho local ou URL) \u2014 o tipo \xE9 detectado pela extens\xE3o e define o modelo e os ajustes v\xE1lidos. Imagem: at\xE9 4x, ~20s, a partir de 6\u{1F48E}. V\xEDdeo: at\xE9 5 minutos (o teto do provedor) e sa\xEDda at\xE9 1080p, custa por bloco de 10s (5s \u2248 87\u{1F48E}, 1 min \u2248 520\u{1F48E}, os 5 min chegam a ~2.600\u{1F48E}) e o \xC1UDIO N\xC3O \xC9 MANTIDO. \u23F1\uFE0F V\xEDdeo longo demora MUITO: o ritmo \xE9 ~1,7 megapixel-frame/s, ent\xE3o 1 min em 1080p passa de 25 minutos e 5 min passam de 2 horas \u2014 a tool devolve o id e voc\xEA acompanha com check_generation. \u26A0\uFE0F Melhorar v\xEDdeo \xE9 restrito a quem tem a tag de budget admin. Use `estimate_cost` antes se quiser confirmar o pre\xE7o." + TEAM_GATING_NOTE,
1353
+ inputSchema: {
1354
+ file: z2.string().min(1).describe(
1355
+ "Arquivo a melhorar: caminho local (/foto.png, ~/Desktop/clipe.mp4) OU URL p\xFAblica. Caminho local sobe pro R2 automaticamente. Imagem: png, jpg, jpeg, webp. V\xEDdeo: mp4, mov, m4v."
1356
+ ),
1357
+ factor: z2.number().min(1).max(4).optional().describe(
1358
+ "Quantas vezes aumentar, de 1 a 4 \u2014 aceita fracion\xE1rio (2.5 \xE9 v\xE1lido), como a API do Topaz. Padr\xE3o 2. Em 1x s\xF3 melhora a qualidade, sem ampliar."
1359
+ ),
1360
+ variant: z2.string().optional().describe(
1361
+ "Modelo do Topaz. Imagem: Wonder 3 (padr\xE3o, serve para quase tudo), Wonder 3.5 (detalhe fino), Wonder 2, Wonder, Recover 3 (imagem muito danificada), Standard MAX (fiel ao original), Redefine (usa a descri\xE7\xE3o), Recovery V2, Recovery. V\xEDdeo: Starlight Precise 2.6 (padr\xE3o), Starlight HQ, Starlight Mini, Starlight Sharp, Starlight Fast 2 (mais r\xE1pido e custa metade)."
1362
+ ),
1363
+ project: z2.string().max(64).optional().describe('Projeto de destino (uuid OU code curto, ex.: "VRAO-26"). Ausente \u2192 projeto ativo.'),
1364
+ // ── só imagem ────────────────────────────────────────────────────────
1365
+ output_format: z2.enum(["jpeg", "png"]).optional().describe("[imagem] Formato da sa\xEDda. Padr\xE3o png."),
1366
+ subject_detection: z2.enum(["All", "Foreground", "Background"]).optional().describe("[imagem] Onde aplicar: imagem toda, s\xF3 o primeiro plano ou s\xF3 o fundo. Padr\xE3o All."),
1367
+ face_enhancement: z2.boolean().optional().describe("[imagem] Tratar rostos separadamente. Padr\xE3o true."),
1368
+ face_enhancement_strength: z2.number().min(0).max(1).optional().describe("[imagem] For\xE7a nos rostos (0 a 1). Padr\xE3o 0.8."),
1369
+ face_enhancement_creativity: z2.number().min(0).max(1).optional().describe("[imagem] Liberdade nos rostos (0 a 1). Padr\xE3o 0."),
1370
+ enhancement_strength: z2.enum(["low", "medium", "high"]).optional().describe("[imagem] Intensidade geral do tratamento."),
1371
+ creativity: z2.number().int().min(1).max(6).optional().describe("[imagem] Quanto detalhe o modelo pode inventar (1 a 6)."),
1372
+ texture: z2.number().int().min(1).max(5).optional().describe("[imagem] Textura (1 a 5)."),
1373
+ sharpen: z2.number().min(0).max(1).optional().describe("[imagem] Nitidez (0 a 1)."),
1374
+ denoise: z2.number().min(0).max(1).optional().describe("[imagem] Redu\xE7\xE3o de ru\xEDdo (0 a 1)."),
1375
+ detail: z2.number().min(0).max(1).optional().describe("[imagem] Detalhe (0 a 1)."),
1376
+ crop_to_fill: z2.boolean().optional().describe("[imagem] Cortar para preencher em vez de encaixar."),
1377
+ autoprompt: z2.boolean().optional().describe("[imagem] O modelo escreve sozinho a orienta\xE7\xE3o a partir da imagem."),
1378
+ prompt: z2.string().max(2e3).optional().describe("[imagem] Orienta\xE7\xE3o para guiar a reconstru\xE7\xE3o (o que a imagem mostra). \xDAtil no Redefine."),
1379
+ // ── só vídeo ─────────────────────────────────────────────────────────
1380
+ softness: z2.number().int().min(1).max(5).optional().describe("[v\xEDdeo] Suavidade, 1 mais n\xEDtido e 5 mais suave. S\xF3 vale no Starlight Precise 2.6."),
1381
+ target_fps: z2.number().int().min(16).max(60).optional().describe("[v\xEDdeo] Quadros por segundo da sa\xEDda. Mudar o valor liga a interpola\xE7\xE3o de quadros."),
1382
+ h264_output: z2.boolean().optional().describe(
1383
+ "[v\xEDdeo] Exportar em H.264. Padr\xE3o true \u2014 desligado sai em H.265, que n\xE3o toca no Firefox e depende de hardware no Chrome."
1384
+ ),
1385
+ // ── medidas ──────────────────────────────────────────────────────────
1386
+ source_width: z2.number().int().positive().optional().describe("Largura do arquivo. S\xF3 se a leitura autom\xE1tica falhar."),
1387
+ source_height: z2.number().int().positive().optional().describe("Altura do arquivo. S\xF3 se a leitura autom\xE1tica falhar."),
1388
+ source_duration: z2.number().positive().optional().describe("[v\xEDdeo] Dura\xE7\xE3o em segundos. S\xF3 se a leitura autom\xE1tica falhar."),
1389
+ source_fps: z2.number().positive().optional().describe("[v\xEDdeo] Quadros por segundo do arquivo. S\xF3 se a leitura autom\xE1tica falhar.")
1390
+ }
1391
+ },
1392
+ async (args) => {
1393
+ try {
1394
+ const kind = kindFromExt(args.file);
1395
+ if (!kind) {
1396
+ return errorResult(
1397
+ "N\xE3o reconheci o tipo do arquivo pela extens\xE3o. Imagem: png, jpg, jpeg, webp. V\xEDdeo: mp4, mov, m4v."
1398
+ );
1399
+ }
1400
+ const isVideo = kind === "video";
1401
+ const factor = args.factor ?? 2;
1402
+ const variant = args.variant ?? (isVideo ? DEFAULT_VIDEO_VARIANT : DEFAULT_IMAGE_VARIANT);
1403
+ const allowed = isVideo ? UPSCALE_VIDEO_VARIANTS : UPSCALE_IMAGE_VARIANTS;
1404
+ if (!allowed.includes(variant)) {
1405
+ return errorResult(
1406
+ `"${variant}" n\xE3o \xE9 um modelo de ${isVideo ? "v\xEDdeo" : "imagem"}. Use um destes: ${allowed.join(", ")}.`
1407
+ );
1408
+ }
1409
+ let width = args.source_width;
1410
+ let height = args.source_height;
1411
+ let duration = args.source_duration;
1412
+ let fps = args.source_fps;
1413
+ if (!width || !height || isVideo && !duration) {
1414
+ try {
1415
+ const probed = await probeMedia(args.file);
1416
+ width = width ?? probed.width;
1417
+ height = height ?? probed.height;
1418
+ duration = duration ?? probed.duration;
1419
+ fps = fps ?? probed.fps;
1420
+ } catch (e) {
1421
+ if (e instanceof ProbeError) {
1422
+ return errorResult(
1423
+ `${e.message}
1424
+
1425
+ \u{1F4A1} Passe as medidas na m\xE3o para seguir: source_width, source_height` + (isVideo ? ", source_duration e source_fps" : "") + ". Sem elas o custo sai errado e a trava de tamanho n\xE3o funciona."
1426
+ );
1427
+ }
1428
+ throw e;
1429
+ }
1430
+ }
1431
+ if (isVideo) {
1432
+ if (duration && duration > MAX_VIDEO_SECONDS) {
1433
+ return errorResult(
1434
+ `Este v\xEDdeo tem ${Math.round(duration)}s e o limite para melhorar \xE9 ${Math.round(MAX_VIDEO_SECONDS / 60)} minutos. Corte um trecho menor e tente de novo.`
1435
+ );
1436
+ }
1437
+ if (height && height * factor > MAX_OUTPUT_HEIGHT) {
1438
+ const maxFactor = Math.floor(MAX_OUTPUT_HEIGHT / height);
1439
+ return errorResult(
1440
+ maxFactor >= 2 ? `Neste v\xEDdeo o aumento m\xE1ximo \xE9 ${maxFactor}x \u2014 acima disso a sa\xEDda passa de 1080p.` : "Este v\xEDdeo j\xE1 est\xE1 em 1080p ou acima, ent\xE3o n\xE3o d\xE1 para aumentar mais."
1441
+ );
1442
+ }
1443
+ }
1444
+ const url = await resolveRef(cfg, args.file);
1445
+ const label = [isVideo ? "V\xEDdeo" : "Imagem", `${factor}x`, variant].join(" \xB7 ");
1446
+ const input = compact({
1447
+ [isVideo ? "video" : "image"]: url,
1448
+ model: variant,
1449
+ upscale_factor: factor,
1450
+ prompt: args.prompt || label,
1451
+ source_width: width,
1452
+ source_height: height,
1453
+ source_duration: duration,
1454
+ source_fps: fps,
1455
+ ...isVideo ? {
1456
+ target_fps: args.target_fps,
1457
+ // O backend descarta softness fora desta variante; filtrar aqui
1458
+ // deixa o motivo visível em vez de sumir em silêncio.
1459
+ softness: variant === SOFTNESS_ONLY ? args.softness : void 0,
1460
+ H264_output: args.h264_output ?? true
1461
+ } : {
1462
+ output_format: args.output_format,
1463
+ subject_detection: args.subject_detection,
1464
+ face_enhancement: args.face_enhancement,
1465
+ face_enhancement_strength: args.face_enhancement_strength,
1466
+ face_enhancement_creativity: args.face_enhancement_creativity,
1467
+ enhancement_strength: args.enhancement_strength,
1468
+ creativity: args.creativity,
1469
+ texture: args.texture,
1470
+ sharpen: args.sharpen,
1471
+ denoise: args.denoise,
1472
+ detail: args.detail,
1473
+ crop_to_fill: args.crop_to_fill,
1474
+ autoprompt: args.autoprompt
1475
+ }
1476
+ });
1477
+ const start = await client.startGeneration(
1478
+ compact({
1479
+ model: isVideo ? VIDEO_MODEL : IMAGE_MODEL,
1480
+ input,
1481
+ project: args.project ?? cfg.activeProject ?? void 0
1482
+ })
1483
+ );
1484
+ if (!start.success || !start.generation_id) {
1485
+ if (isTeamGatingError(start.message)) return errorResult(await buildPickProjectMessage(client));
1486
+ return errorResult(start.message || "Falha ao iniciar o upscale.");
1487
+ }
1488
+ const footer = costFooter(start, cfg);
1489
+ const { settled, status } = await pollUntilDone(
1490
+ client,
1491
+ start.generation_id,
1492
+ isVideo ? VIDEO_BUDGET_MS2 : IMAGE_BUDGET_MS2
1493
+ );
1494
+ if (!settled) {
1495
+ return textResult(
1496
+ `\u23F3 Upscale ainda em processamento (id: ${start.generation_id}, ${status.progress ?? 0}%). ` + (isVideo ? "V\xEDdeo costuma levar de 3 a 15 minutos, dependendo da dura\xE7\xE3o e da resolu\xE7\xE3o de sa\xEDda \u2014 " : "") + `use check_generation com esse id.
1497
+ ${footer}`
1498
+ );
1499
+ }
1500
+ if (status.status === "failed") {
1501
+ return errorResult(`Upscale falhou: ${status.error_message || "erro desconhecido"}.
1502
+ ${footer}`);
1503
+ }
1504
+ const urls = outputUrls(status);
1505
+ const list = urls.map((u, i) => `${i + 1}. ${u}`).join("\n") || "(sem URLs)";
1506
+ const saida = width && height ? ` \u2014 sa\xEDda ${width * factor} \xD7 ${height * factor}` : "";
1507
+ const semAudio = isVideo ? "\nO \xE1udio n\xE3o foi mantido (o Topaz descarta a faixa)." : "";
1508
+ return textResult(
1509
+ `\u2705 ${isVideo ? "V\xEDdeo" : "Imagem"} melhorado em ${factor}x com ${variant}${saida}.
1510
+ ${list}${semAudio}
1511
+ ${footer}`
1512
+ );
1513
+ } catch (e) {
1514
+ const msg = e instanceof ApiError ? e.message : e instanceof Error ? e.message : "";
1515
+ if (isTeamGatingError(msg)) return errorResult(await buildPickProjectMessage(client));
1516
+ if (isBudgetBlockedError(msg)) {
1517
+ return errorResult(`${apiErrorToUserMessage(e)}
1518
+ ${BUDGET_BLOCKED_GUIDANCE}`);
1519
+ }
1520
+ return errorResult(apiErrorToUserMessage(e));
1521
+ }
1522
+ }
1523
+ );
1524
+ }
1525
+
1526
+ // src/tools/audio.ts
1527
+ import { z as z3 } from "zod";
1103
1528
  function registerAudioTools(server, client, cfg) {
1104
1529
  server.registerTool(
1105
1530
  "generate_audio",
@@ -1107,15 +1532,15 @@ function registerAudioTools(server, client, cfg) {
1107
1532
  title: "Gerar \xE1udio (TTS)",
1108
1533
  description: 'Sintetiza fala a partir de texto (s\xEDncrono \u2014 retorna a URL na hora) usando o IA Lab da BePlus. ~30 vozes. Opcional `style_instruction` (notas de dire\xE7\xE3o, ex.: "fale animado e pausado") e di\xE1logo com 2 locutores via `multi_speaker`. Consome diamantes por segundo de \xE1udio.' + TEAM_GATING_NOTE,
1109
1534
  inputSchema: {
1110
- text: z2.string().min(1).max(1e4).describe("Texto a ser falado."),
1111
- voice: z2.enum(VOICES).default(DEFAULTS.voice).describe("Voz (ex.: Kore, Puck, Charon)."),
1112
- model: z2.enum(TTS_MODELS).default(DEFAULTS.ttsModel).describe("Modelo TTS."),
1113
- style_instruction: z2.string().max(2e3).optional().describe("Instru\xE7\xE3o de estilo/tom aplicada \xE0 fala."),
1114
- multi_speaker: z2.object({
1115
- speaker1: z2.object({ name: z2.string().min(1).max(50), voice: z2.enum(VOICES) }),
1116
- speaker2: z2.object({ name: z2.string().min(1).max(50), voice: z2.enum(VOICES) })
1535
+ text: z3.string().min(1).max(1e4).describe("Texto a ser falado."),
1536
+ voice: z3.enum(VOICES).default(DEFAULTS.voice).describe("Voz (ex.: Kore, Puck, Charon)."),
1537
+ model: z3.enum(TTS_MODELS).default(DEFAULTS.ttsModel).describe("Modelo TTS."),
1538
+ style_instruction: z3.string().max(2e3).optional().describe("Instru\xE7\xE3o de estilo/tom aplicada \xE0 fala."),
1539
+ multi_speaker: z3.object({
1540
+ speaker1: z3.object({ name: z3.string().min(1).max(50), voice: z3.enum(VOICES) }),
1541
+ speaker2: z3.object({ name: z3.string().min(1).max(50), voice: z3.enum(VOICES) })
1117
1542
  }).optional().describe('Di\xE1logo de 2 locutores. Marque as falas no texto como "Nome: ...".'),
1118
- project: z2.string().max(64).optional().describe('Projeto (uuid OU code curto, ex.: "VRAO-26"). Omita para usar o projeto ativo da conta.')
1543
+ project: z3.string().max(64).optional().describe('Projeto (uuid OU code curto, ex.: "VRAO-26"). Omita para usar o projeto ativo da conta.')
1119
1544
  }
1120
1545
  },
1121
1546
  async (args) => {
@@ -1156,7 +1581,7 @@ ${BUDGET_BLOCKED_GUIDANCE}`);
1156
1581
  }
1157
1582
 
1158
1583
  // src/tools/music.ts
1159
- import { z as z3 } from "zod";
1584
+ import { z as z4 } from "zod";
1160
1585
  var MUSIC_BUDGET_MS = 15e4;
1161
1586
  function registerMusicTools(server, client, cfg) {
1162
1587
  server.registerTool(
@@ -1165,18 +1590,18 @@ function registerMusicTools(server, client, cfg) {
1165
1590
  title: "Gerar m\xFAsica",
1166
1591
  description: 'Gera uma m\xFAsica completa (com vocais ou instrumental) usando o Suno no IA Lab da BePlus. ASS\xCDNCRONO e LENTO (geralmente 30s\u20132+ min). Aguarda at\xE9 ~2,5 min; se n\xE3o terminar, retorna o generation_id e o status \u2014 ent\xE3o use check_generation. Consome diamantes. DOIS modos: (a) DESCRI\xC7\xC3O (custom_mode=false, padr\xE3o) \u2192 passe `description` ("uma balada triste de piano"); (b) LETRA PR\xD3PRIA (custom_mode=true) \u2192 passe `lyrics` (a letra) e opcionalmente `title`. Use `tags` para g\xEAnero/estilo em ambos os modos (ex.: "sertanejo, animado, vocal feminino"). `vocal_gender` (f/m) s\xF3 \xE9 aceito pelo Suno v4.5 e \xE9 ignorado quando `make_instrumental=true`.' + TEAM_GATING_NOTE,
1167
1592
  inputSchema: {
1168
- model: z3.enum(MUSIC_MODELS).default(DEFAULTS.musicModel).describe("suno/v5-5 = mais novo; suno/v4-5 = r\xE1pido/consistente (\xFAnico que aceita vocal_gender)."),
1169
- custom_mode: z3.boolean().optional().describe("false (padr\xE3o) = gera a partir de `description`; true = usa sua `lyrics` pr\xF3pria."),
1170
- description: z3.string().max(2500).optional().describe("Modo descri\xE7\xE3o (custom_mode=false): descreva a m\xFAsica desejada. Obrigat\xF3rio nesse modo."),
1171
- lyrics: z3.string().max(5e3).optional().describe("Modo letra (custom_mode=true): a letra completa da m\xFAsica. Obrigat\xF3rio nesse modo."),
1172
- title: z3.string().max(120).optional().describe("T\xEDtulo da m\xFAsica (modo letra)."),
1173
- tags: z3.string().max(1e3).optional().describe('G\xEAnero/estilo, separados por v\xEDrgula (ex.: "pop, energ\xE9tico, vocal masculino").'),
1174
- make_instrumental: z3.boolean().optional().describe("true = sem vocais (apenas instrumental). Padr\xE3o false."),
1175
- vocal_gender: z3.enum(VOCAL_GENDERS).optional().describe("G\xEAnero do vocal: f (feminino) ou m (masculino). S\xF3 Suno v4.5; ignorado se instrumental."),
1176
- negative_tags: z3.string().max(1e3).optional().describe("Estilos/elementos a evitar."),
1177
- style_weight: z3.number().min(0).max(1).optional().describe("Peso do estilo (0\u20131). Maior = segue mais as tags. Omita para o modelo decidir."),
1178
- weirdness_constraint: z3.number().min(0).max(1).optional().describe("Criatividade/imprevisibilidade (0\u20131). Omita para o modelo decidir."),
1179
- project: z3.string().max(64).optional().describe('Projeto (uuid OU code curto, ex.: "VRAO-26"). S\xF3 omita se o usu\xE1rio J\xC1 confirmou o projeto ativo nesta sess\xE3o.')
1593
+ model: z4.enum(MUSIC_MODELS).default(DEFAULTS.musicModel).describe("suno/v5-5 = mais novo; suno/v4-5 = r\xE1pido/consistente (\xFAnico que aceita vocal_gender)."),
1594
+ custom_mode: z4.boolean().optional().describe("false (padr\xE3o) = gera a partir de `description`; true = usa sua `lyrics` pr\xF3pria."),
1595
+ description: z4.string().max(2500).optional().describe("Modo descri\xE7\xE3o (custom_mode=false): descreva a m\xFAsica desejada. Obrigat\xF3rio nesse modo."),
1596
+ lyrics: z4.string().max(5e3).optional().describe("Modo letra (custom_mode=true): a letra completa da m\xFAsica. Obrigat\xF3rio nesse modo."),
1597
+ title: z4.string().max(120).optional().describe("T\xEDtulo da m\xFAsica (modo letra)."),
1598
+ tags: z4.string().max(1e3).optional().describe('G\xEAnero/estilo, separados por v\xEDrgula (ex.: "pop, energ\xE9tico, vocal masculino").'),
1599
+ make_instrumental: z4.boolean().optional().describe("true = sem vocais (apenas instrumental). Padr\xE3o false."),
1600
+ vocal_gender: z4.enum(VOCAL_GENDERS).optional().describe("G\xEAnero do vocal: f (feminino) ou m (masculino). S\xF3 Suno v4.5; ignorado se instrumental."),
1601
+ negative_tags: z4.string().max(1e3).optional().describe("Estilos/elementos a evitar."),
1602
+ style_weight: z4.number().min(0).max(1).optional().describe("Peso do estilo (0\u20131). Maior = segue mais as tags. Omita para o modelo decidir."),
1603
+ weirdness_constraint: z4.number().min(0).max(1).optional().describe("Criatividade/imprevisibilidade (0\u20131). Omita para o modelo decidir."),
1604
+ project: z4.string().max(64).optional().describe('Projeto (uuid OU code curto, ex.: "VRAO-26"). S\xF3 omita se o usu\xE1rio J\xC1 confirmou o projeto ativo nesta sess\xE3o.')
1180
1605
  }
1181
1606
  },
1182
1607
  async (args) => {
@@ -1243,7 +1668,7 @@ ${BUDGET_BLOCKED_GUIDANCE}`);
1243
1668
  }
1244
1669
 
1245
1670
  // src/tools/account.ts
1246
- import { z as z4 } from "zod";
1671
+ import { z as z5 } from "zod";
1247
1672
  function registerAccountTools(server, client, _cfg) {
1248
1673
  server.registerTool(
1249
1674
  "whoami",
@@ -1296,7 +1721,7 @@ ${budget}` : "")
1296
1721
  title: "Listar modelos",
1297
1722
  description: "Lista os modelos dispon\xEDveis com pre\xE7o-base e limites por usu\xE1rio. Use quando estiver em d\xFAvida sobre qual modelo/params usar ou quanto custa. \xC9 a fonte de verdade em runtime.",
1298
1723
  inputSchema: {
1299
- kind: z4.enum(["image", "video", "audio", "all"]).default("all").optional().describe("Filtrar por tipo.")
1724
+ kind: z5.enum(["image", "video", "audio", "all"]).default("all").optional().describe("Filtrar por tipo.")
1300
1725
  }
1301
1726
  },
1302
1727
  async () => {
@@ -1330,16 +1755,45 @@ Limites simult\xE2neos por usu\xE1rio: imagem=${m.limits.perUserMaxImage}, v\xED
1330
1755
  title: "Estimar custo",
1331
1756
  description: "Retorna o custo autoritativo em diamantes ANTES de gerar. Para \xE1udio (TTS) o custo \xE9 por segundo e s\xF3 \xE9 conhecido ap\xF3s a s\xEDntese.",
1332
1757
  inputSchema: {
1333
- model: z4.string().min(1).describe("Id do modelo (ex.: google/nano-banana-pro, bytedance/seedance-2.0)."),
1334
- resolution: z4.string().optional(),
1335
- duration: z4.number().int().optional(),
1336
- mode: z4.string().optional(),
1337
- size: z4.string().optional(),
1338
- quality: z4.string().optional()
1758
+ model: z5.string().min(1).describe("Id do modelo (ex.: google/nano-banana-pro, bytedance/seedance-2.0)."),
1759
+ resolution: z5.string().optional(),
1760
+ duration: z5.number().int().optional(),
1761
+ mode: z5.string().optional(),
1762
+ size: z5.string().optional(),
1763
+ quality: z5.string().optional(),
1764
+ // Upscale: o preço não sai de uma chave fixa — vem das medidas do
1765
+ // arquivo (megapixel de saída na imagem, bloco de 10s no vídeo). Sem
1766
+ // elas o backend responde pelo padrão de 1024²/10s, que não é o preço
1767
+ // que vai ser cobrado.
1768
+ file: z5.string().optional().describe(
1769
+ "[upscale] Arquivo a medir (caminho local ou URL) \u2014 preenche sozinho as medidas abaixo. Use com model=topaz/upscale/image/generative ou .../video/generative."
1770
+ ),
1771
+ upscale_factor: z5.number().int().min(1).max(4).optional().describe("[upscale] Fator de aumento (1 a 4). Padr\xE3o 2."),
1772
+ model_variant: z5.string().optional().describe("[upscale] Variante do Topaz (ex.: Wonder 3, Starlight Fast 2) \u2014 muda o pre\xE7o."),
1773
+ source_width: z5.number().int().positive().optional().describe("[upscale] Largura do arquivo."),
1774
+ source_height: z5.number().int().positive().optional().describe("[upscale] Altura do arquivo."),
1775
+ source_duration: z5.number().positive().optional().describe("[upscale] Dura\xE7\xE3o em segundos (v\xEDdeo)."),
1776
+ source_fps: z5.number().positive().optional().describe("[upscale] Quadros por segundo do arquivo (v\xEDdeo).")
1339
1777
  }
1340
1778
  },
1341
1779
  async (args) => {
1342
1780
  try {
1781
+ let width = args.source_width;
1782
+ let height = args.source_height;
1783
+ let duration = args.source_duration;
1784
+ let fps = args.source_fps;
1785
+ if (args.file && (!width || !height)) {
1786
+ try {
1787
+ const probed = await probeMedia(args.file);
1788
+ width = width ?? probed.width;
1789
+ height = height ?? probed.height;
1790
+ duration = duration ?? probed.duration;
1791
+ fps = fps ?? probed.fps;
1792
+ } catch (e) {
1793
+ if (e instanceof ProbeError) return errorResult(e.message);
1794
+ throw e;
1795
+ }
1796
+ }
1343
1797
  const c = await client.calculateCost(
1344
1798
  compact({
1345
1799
  model: args.model,
@@ -1347,7 +1801,13 @@ Limites simult\xE2neos por usu\xE1rio: imagem=${m.limits.perUserMaxImage}, v\xED
1347
1801
  duration: args.duration,
1348
1802
  mode: args.mode,
1349
1803
  size: args.size,
1350
- quality: args.quality
1804
+ quality: args.quality,
1805
+ upscale_factor: args.upscale_factor,
1806
+ model_variant: args.model_variant,
1807
+ source_width: width,
1808
+ source_height: height,
1809
+ source_duration: duration,
1810
+ source_fps: fps
1351
1811
  })
1352
1812
  );
1353
1813
  const note = c.fallback ? " [modelo desconhecido \u2014 custo m\xEDnimo]" : "";
@@ -1360,7 +1820,7 @@ Limites simult\xE2neos por usu\xE1rio: imagem=${m.limits.perUserMaxImage}, v\xED
1360
1820
  }
1361
1821
 
1362
1822
  // src/tools/analyze.ts
1363
- import { z as z5 } from "zod";
1823
+ import { z as z6 } from "zod";
1364
1824
  function registerAnalyzeTools(server, client, cfg) {
1365
1825
  server.registerTool(
1366
1826
  "analyze_media",
@@ -1368,12 +1828,12 @@ function registerAnalyzeTools(server, client, cfg) {
1368
1828
  title: "Analisar m\xEDdia (Gemini)",
1369
1829
  description: "Envia um prompt + m\xEDdia (imagem, v\xEDdeo, \xE1udio ou PDF) para o Gemini e devolve uma resposta em texto. Use para descrever/analisar/transcrever/resumir/extrair informa\xE7\xE3o de m\xEDdia, ou perguntar algo sobre ela. A m\xEDdia entra como `media_urls` (URLs p\xFAblicas \u2014 tudo que o Google aceita: imagens, v\xEDdeos, \xE1udios, PDFs). V\xEDdeo/\xE1udio grandes funcionam (v\xE3o pela Files API do Google). Consome diamantes (reembolsados se falhar). Veja custo/limites com list_models.",
1370
1830
  inputSchema: {
1371
- prompt: z5.string().min(1).max(5e4).describe('O que voc\xEA quer do modelo sobre a m\xEDdia (ex.: "descreva este v\xEDdeo", "transcreva este \xE1udio").'),
1372
- media_urls: z5.array(z5.string().min(1)).max(50).optional().describe(
1831
+ prompt: z6.string().min(1).max(5e4).describe('O que voc\xEA quer do modelo sobre a m\xEDdia (ex.: "descreva este v\xEDdeo", "transcreva este \xE1udio").'),
1832
+ media_urls: z6.array(z6.string().min(1)).max(50).optional().describe(
1373
1833
  "M\xEDdia (imagem/v\xEDdeo/\xE1udio/PDF): URLs p\xFAblicas OU caminhos de arquivo local (o MCP sobe pro R2 automaticamente). Pode misturar tipos. Opcional (prompt s\xF3-texto tamb\xE9m funciona)."
1374
1834
  ),
1375
- model: z5.enum(["gemini-flash", "gemini-pro"]).default("gemini-flash").optional().describe("gemini-flash = r\xE1pido/barato (Gemini 3.5 Flash); gemini-pro = mais capaz (Gemini 3.1 Pro)."),
1376
- project: z5.string().optional().describe(
1835
+ model: z6.enum(["gemini-flash", "gemini-pro"]).default("gemini-flash").optional().describe("gemini-flash = r\xE1pido/barato (Gemini 3.5 Flash); gemini-pro = mais capaz (Gemini 3.1 Pro)."),
1836
+ project: z6.string().optional().describe(
1377
1837
  'Projeto (uuid ou code curto, ex.: "ICONS") de cujo budget cobrar. Omitido \u2192 usa o projeto ativo da conta.'
1378
1838
  )
1379
1839
  }
@@ -1402,7 +1862,7 @@ function registerAnalyzeTools(server, client, cfg) {
1402
1862
  }
1403
1863
 
1404
1864
  // src/tools/projects.ts
1405
- import { z as z6 } from "zod";
1865
+ import { z as z7 } from "zod";
1406
1866
  function projectLine(p, active) {
1407
1867
  const marker = active ? "\u25CF " : "\u25CB ";
1408
1868
  const counts = `${p.generation_count ?? 0} gera\xE7\xF5es`;
@@ -1446,11 +1906,11 @@ ${lines.join("\n")}`);
1446
1906
  title: "Criar projeto",
1447
1907
  description: "Cria um projeto novo no IA Lab (workspace compartilhado), SEMPRE dentro de um cliente/marca, e por padr\xE3o j\xE1 o deixa ativo \u2014 gera\xE7\xF5es seguintes entram nele. S\xF3 membros da equipe podem criar. O campo `client` \xE9 obrigat\xF3rio: passe o code/uuid de um cliente existente (veja list_clients) OU o NOME de um cliente novo (ele \xE9 criado automaticamente). Projetos s\xE3o globais: todos da equipe usam.",
1448
1908
  inputSchema: {
1449
- name: z6.string().min(1).max(256).describe('Nome do projeto (ex.: "Ladeira Abaixo").'),
1450
- client: z6.string().min(1).max(256).describe('Cliente/marca dono: code/uuid existente (ex.: "RDBL") OU nome novo (ex.: "Red Bull"). Obrigat\xF3rio.'),
1451
- description: z6.string().max(4e3).optional().describe("Descri\xE7\xE3o do projeto (opcional)."),
1452
- color: z6.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe("Cor em hex #RRGGBB para o badge (opcional)."),
1453
- set_active: z6.boolean().optional().describe("Deixar o novo projeto como ativo (padr\xE3o true).")
1909
+ name: z7.string().min(1).max(256).describe('Nome do projeto (ex.: "Ladeira Abaixo").'),
1910
+ client: z7.string().min(1).max(256).describe('Cliente/marca dono: code/uuid existente (ex.: "RDBL") OU nome novo (ex.: "Red Bull"). Obrigat\xF3rio.'),
1911
+ description: z7.string().max(4e3).optional().describe("Descri\xE7\xE3o do projeto (opcional)."),
1912
+ color: z7.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe("Cor em hex #RRGGBB para o badge (opcional)."),
1913
+ set_active: z7.boolean().optional().describe("Deixar o novo projeto como ativo (padr\xE3o true).")
1454
1914
  }
1455
1915
  },
1456
1916
  async (args) => {
@@ -1475,7 +1935,7 @@ ${lines.join("\n")}`);
1475
1935
  title: "Definir projeto ativo",
1476
1936
  description: 'Define o projeto ativo da conta (por uuid OU code curto, ex.: "VRAO-26"). A partir da\xED, gera\xE7\xF5es sem `project` expl\xEDcito s\xE3o vinculadas a ele \u2014 espelha o seletor de projeto da web. Passe project=null para limpar.',
1477
1937
  inputSchema: {
1478
- project: z6.string().max(64).nullable().describe("uuid OU code curto do projeto. null para nenhum projeto.")
1938
+ project: z7.string().max(64).nullable().describe("uuid OU code curto do projeto. null para nenhum projeto.")
1479
1939
  }
1480
1940
  },
1481
1941
  async ({ project }) => {
@@ -1493,7 +1953,7 @@ ${budget}` : ""));
1493
1953
  }
1494
1954
 
1495
1955
  // src/tools/clients.ts
1496
- import { z as z7 } from "zod";
1956
+ import { z as z8 } from "zod";
1497
1957
  function clientLine(c, active) {
1498
1958
  const marker = active ? "\u25CF " : "\u25CB ";
1499
1959
  const counts = `${c.project_count ?? 0} projetos`;
@@ -1532,7 +1992,7 @@ ${lines.join("\n")}`);
1532
1992
  title: "Criar cliente",
1533
1993
  description: 'Cria um cliente/marca novo (ex.: "Red Bull"). S\xF3 membros da equipe. Clientes s\xE3o globais: qualquer colega passa a v\xEA-lo e us\xE1-lo. Depois crie projetos dentro dele com create_project.',
1534
1994
  inputSchema: {
1535
- name: z7.string().min(1).max(256).describe('Nome do cliente/marca (ex.: "Red Bull").')
1995
+ name: z8.string().min(1).max(256).describe('Nome do cliente/marca (ex.: "Red Bull").')
1536
1996
  }
1537
1997
  },
1538
1998
  async (args) => {
@@ -1550,7 +2010,7 @@ ${lines.join("\n")}`);
1550
2010
  title: "Definir cliente ativo",
1551
2011
  description: 'Define o cliente ativo da conta (por uuid OU code curto, ex.: "RDBL"). Trocar de cliente limpa o projeto ativo se ele pertencia a outro cliente. Passe client=null para limpar.',
1552
2012
  inputSchema: {
1553
- client: z7.string().max(64).nullable().describe("uuid OU code curto do cliente. null para nenhum.")
2013
+ client: z8.string().max(64).nullable().describe("uuid OU code curto do cliente. null para nenhum.")
1554
2014
  }
1555
2015
  },
1556
2016
  async ({ client: ref }) => {
@@ -1566,7 +2026,7 @@ ${lines.join("\n")}`);
1566
2026
  }
1567
2027
 
1568
2028
  // src/tools/calls.ts
1569
- import { z as z8 } from "zod";
2029
+ import { z as z9 } from "zod";
1570
2030
  var VIS_LABEL = {
1571
2031
  team: "equipe",
1572
2032
  private: "privada",
@@ -1632,14 +2092,14 @@ function registerCallTools(server, client, _cfg) {
1632
2092
  title: "Buscar calls",
1633
2093
  description: 'Busca nas calls (reuni\xF5es gravadas e transcritas) vis\xEDveis pra conta \u2014 suas, da equipe e compartilhadas. Todos os filtros s\xE3o opcionais e combin\xE1veis: `query` (texto no t\xEDtulo/projeto/cliente/falante), `project` (code/uuid/nome), `client` (code/nome), `company` (empresa/equipe do dono, ex.: "BePlus"), `visibility` (team|private|workspace|link), `speaker` (nome do falante), `mine` (s\xF3 as que EU gravei). Retorna uma lista resumida \u2014 use get_call com o id pra ler a transcri\xE7\xE3o completa.',
1634
2094
  inputSchema: {
1635
- query: z8.string().max(200).optional().describe("Texto livre \u2014 casa t\xEDtulo, projeto, cliente ou falante."),
1636
- project: z8.string().max(64).optional().describe('Projeto: code (ex.: "CONTEUDO-INS"), uuid ou parte do nome.'),
1637
- client: z8.string().max(128).optional().describe("Cliente: code ou parte do nome."),
1638
- company: z8.string().max(128).optional().describe('Empresa/equipe do dono da call (ex.: "BePlus"). Filtra por parte do nome.'),
1639
- visibility: z8.enum(["team", "private", "workspace", "link"]).optional().describe("Privacidade: team (equipe), private (privada), workspace (projeto) ou link."),
1640
- speaker: z8.string().max(128).optional().describe("Nome do falante."),
1641
- mine: z8.boolean().optional().describe('true = s\xF3 as calls que EU gravei (sou o dono). Use pra "minhas grava\xE7\xF5es".'),
1642
- limit: z8.number().int().min(1).max(50).optional().describe("M\xE1ximo de resultados (padr\xE3o 20).")
2095
+ query: z9.string().max(200).optional().describe("Texto livre \u2014 casa t\xEDtulo, projeto, cliente ou falante."),
2096
+ project: z9.string().max(64).optional().describe('Projeto: code (ex.: "CONTEUDO-INS"), uuid ou parte do nome.'),
2097
+ client: z9.string().max(128).optional().describe("Cliente: code ou parte do nome."),
2098
+ company: z9.string().max(128).optional().describe('Empresa/equipe do dono da call (ex.: "BePlus"). Filtra por parte do nome.'),
2099
+ visibility: z9.enum(["team", "private", "workspace", "link"]).optional().describe("Privacidade: team (equipe), private (privada), workspace (projeto) ou link."),
2100
+ speaker: z9.string().max(128).optional().describe("Nome do falante."),
2101
+ mine: z9.boolean().optional().describe('true = s\xF3 as calls que EU gravei (sou o dono). Use pra "minhas grava\xE7\xF5es".'),
2102
+ limit: z9.number().int().min(1).max(50).optional().describe("M\xE1ximo de resultados (padr\xE3o 20).")
1643
2103
  }
1644
2104
  },
1645
2105
  async (args) => {
@@ -1661,7 +2121,7 @@ ${calls.map(callLine).join("\n\n")}`);
1661
2121
  title: "Ler call",
1662
2122
  description: "Retorna o conte\xFAdo completo de uma call pelo id (use search_calls pra achar): t\xEDtulo, resumo, falantes, projeto/cliente, links da grava\xE7\xE3o e a TRANSCRI\xC7\xC3O inteira por falante. Respeita a visibilidade \u2014 s\xF3 retorna o que a conta pode ver.",
1663
2123
  inputSchema: {
1664
- id: z8.string().min(1).max(64).describe("id (uuid) da call, obtido via search_calls.")
2124
+ id: z9.string().min(1).max(64).describe("id (uuid) da call, obtido via search_calls.")
1665
2125
  }
1666
2126
  },
1667
2127
  async ({ id }) => {
@@ -1693,6 +2153,7 @@ function buildServer(cfg) {
1693
2153
  const server = new McpServer({ name: "beplus", version: VERSION });
1694
2154
  const client = new BeplusClient(cfg);
1695
2155
  registerGenerationTools(server, client, cfg);
2156
+ registerUpscaleTools(server, client, cfg);
1696
2157
  registerAudioTools(server, client, cfg);
1697
2158
  registerMusicTools(server, client, cfg);
1698
2159
  registerAccountTools(server, client, cfg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beplus-mcp",
3
- "version": "0.18.1",
3
+ "version": "0.20.0",
4
4
  "description": "Conector MCP da equipe BePlus — gere imagens/vídeos/áudio e consulte calls, projetos e clientes pela sua conta BePlus (diamantes, limites e histórico aplicados pela plataforma).",
5
5
  "type": "module",
6
6
  "bin": {