beplus-mcp 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,7 +84,8 @@ Reinicie o cliente. Rode a tool **`whoami`** para confirmar o vínculo.
84
84
  | `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). |
85
85
  | `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. |
86
86
  | `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. |
87
- | `generate_audio` | Sintetiza fala (Gemini TTS, ~30 vozes, multi-locutor). Síncrono. |
87
+ | `generate_audio` | Sintetiza fala (Gemini TTS, multi-locutor). Padrão `gemini-3.8-flash-lite-tts`; os modelos 3.8 aceitam vozes da biblioteca, tags vocais (`<laugh>`, `<sigh>`...) e `style` por locutor; os legados só as 30 vozes clássicas. Síncrono. |
88
+ | `list_tts_voices` | Lista as vozes da biblioteca do TTS 3.8 (filtros: idioma, gênero, tom, contexto, busca) com o id para usar em `voice`. Somente leitura. |
88
89
  | `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. |
89
90
  | `check_generation` | Status de uma geração async por id. |
90
91
  | `cancel_generation` | Cancela uma geração em fila/processamento (estorna diamantes). |
@@ -140,10 +141,13 @@ Falam com `/api/v1/canvas`. Montam e editam o fluxo e **não geram mídia**, com
140
141
  | `canvas_reopen_comment` | Reabre, com motivo opcional. |
141
142
  | `canvas_comment` | Cria comentário raiz ancorado (nó, versão, minutagem, ponto ou board), para deixar pergunta na tela da pessoa. |
142
143
  | `canvas_run` | Roda nós pela aba do Canvas aberta da própria pessoa (mesma conta; sem aba, nada roda). `max_diamonds` obrigatório: a aba estima como o botão Rodar e só roda se couber. `dry_run: true` só estima. Alvo: `node_ids` (roda de novo mesmo em cache, como o Rodar do cartão), `from_node_id` (daqui para frente) ou `all: true` (só o que mudou). `simulated: true` exige a aba no Simulado; sem ele, a aba precisa estar no Real. Prompt com PT sem aplicar no EN: recusa e diz qual. Devolve o que vai rodar, o custo e o `request_id`. |
143
- | `canvas_run_status` | Andamento de um `request_id`: estado de cada nó, estimado e gasto, perguntado à aba que rodou. Com a aba fechada, lê o estado de execução do board para os nós do pedido. |
144
+ | `canvas_run_status` | Andamento de um `request_id`: estado de cada nó, estimado e gasto, perguntado à aba que rodou. Com a aba fechada, lê o estado de execução do board para os nós do pedido. Resposta grande vem cortada ("… e mais N"); o andamento sem aba de um pedido cortado avisa que é parcial. |
145
+ | `canvas_focus` | "Olha aqui": leva a aba do Canvas que a pessoa está olhando (a visível que não é aba de agente aberta por link `?frame`; sem nenhuma, a de um agente) até `node_ids` e/ou `section_id`, sem animação, e seleciona os nós (`select: false` não mexe). **Padrão: uma vez só**, a aba não fica presa. `follow: true` segue a área e prende a aba até o "×" da pílula (modo `?frame`, com o `titulo` na pílula e no nome da aba). `zoom`: `"fit"` (padrão) ou número de 0,07 a 4. **Não gasta e não escreve no board.** Devolve qual aba enquadrou (nome, do usuário ou de agente, repassada, escondida, Montagem aberta) e os ids que não existem. Qualquer papel, leitor inclusive; pede só `ialab:read`. |
144
146
 
145
147
  Rodar pelo MCP (`canvas_run`): o servidor (academy-backend, rota `run-requests`) repassa o pedido para **uma** aba da própria pessoa (a aberta por último) e nunca roda nada. Sem resposta em 20 s, o destino fica incerto: a ferramenta manda consultar `canvas_run_status` com o mesmo `request_id`, e repetir `canvas_run` com esse `request_id` é seguro (a aba não roda duas vezes).
146
148
 
149
+ Enquadrar pelo MCP (`canvas_focus`): o servidor (rota `focus-requests`) repassa para **uma** aba da própria pessoa, a de entrada mais recente com a versão do Canvas que enquadra (a aba antiga não conta: recarregue). Essa aba passa o pedido à aba que a pessoa está olhando no mesmo navegador. Sem resposta em 5 s, 504; chamar de novo é seguro. Por padrão a aba só vai até a área uma vez; com `follow: true` ela segue a área até a pessoa clicar no "×".
150
+
147
151
  Concorrência: cada edição relê o board, vai num envelope só (um `seq`) e é recusada, sem aplicar nada, se outra pessoa mexeu nos mesmos nós desde a sua leitura. Recusas do servidor (`node-missing` e afins) voltam em português com "releia". As exceções são o `canvas_patch_node` e o `canvas_update_nodes`: vários envelopes em ordem, cada um julgado sozinho, e o relatório diz qual entrou. A medida que a tela grava sozinha (`data.__measured`) não conta como "outra pessoa mexeu".
148
152
 
149
153
  Tamanho do cartão: o `size` gravado é **piso**; o cartão cresce com o conteúdo (um Prompt de 340 de largura com o `!!ESTILO` resolvido vira uma coluna de 1.800). O Canvas grava o tamanho desenhado em `data.__measured = { w, h, at }` quando o cartão passa do piso; as leituras mostram esse (`medido`) ou o piso (`gravado`). É campo interno: não aparece como dado e nenhuma ferramenta grava nele. `size: {w, h}` muda a caixa; `size: "titulo" | "subtitulo" | "corpo"` é o tamanho do texto do Texto livre (`data.size` do nó `label`).
package/dist/index.js CHANGED
@@ -21621,6 +21621,9 @@ var BeplusClient = class {
21621
21621
  tts(body2) {
21622
21622
  return this.request("POST", "/tts", { body: body2, timeoutMs: 9e4 });
21623
21623
  }
21624
+ ttsVoices(query) {
21625
+ return this.request("GET", "/tts/voices", { query, timeoutMs: 2e4 });
21626
+ }
21624
21627
  models() {
21625
21628
  return this.request("GET", "/models");
21626
21629
  }
@@ -22179,9 +22182,13 @@ var MUSIC_MODELS = [
22179
22182
  ];
22180
22183
  var VOCAL_GENDERS = ["f", "m"];
22181
22184
  var TTS_MODELS = [
22185
+ "gemini-3.8-flash-lite-tts",
22186
+ "gemini-3.8-flash-tts",
22182
22187
  "gemini-3.1-flash-tts-preview",
22183
22188
  "gemini-2.5-pro-preview-tts"
22184
22189
  ];
22190
+ var TTS_MODELS_38 = ["gemini-3.8-flash-lite-tts", "gemini-3.8-flash-tts"];
22191
+ var LIBRARY_VOICE_ID = /^[a-z]{2,3}-[a-z0-9]{2,4}-[a-z0-9-]{1,60}$/;
22185
22192
  var VOICES = [
22186
22193
  "Zephyr",
22187
22194
  "Puck",
@@ -22226,7 +22233,7 @@ var DEFAULTS = {
22226
22233
  videoAspect: "16:9",
22227
22234
  videoResolution: "720p",
22228
22235
  videoDuration: 5,
22229
- ttsModel: "gemini-3.1-flash-tts-preview",
22236
+ ttsModel: "gemini-3.8-flash-lite-tts",
22230
22237
  voice: "Kore",
22231
22238
  musicModel: "suno/v5-5"
22232
22239
  };
@@ -23463,26 +23470,50 @@ ${BUDGET_BLOCKED_GUIDANCE}`);
23463
23470
  }
23464
23471
 
23465
23472
  // src/tools/audio.ts
23473
+ var VOICE_HELP = "Uma das 30 vozes cl\xE1ssicas (Kore, Puck, Charon...) OU o id de uma voz da biblioteca (ex.: pt-br-advisor-10, s\xF3 nos modelos 3.8; descubra ids com list_tts_voices).";
23474
+ var voiceSchema = external_exports.string().refine((v) => VOICES.includes(v) || LIBRARY_VOICE_ID.test(v), {
23475
+ message: "Voz inv\xE1lida: use uma das 30 vozes cl\xE1ssicas (ex.: Kore) ou um id da biblioteca (ex.: pt-br-advisor-10)."
23476
+ });
23477
+ function isLibraryVoice(v) {
23478
+ return !VOICES.includes(v) && LIBRARY_VOICE_ID.test(v);
23479
+ }
23480
+ var speakerSchema = external_exports.object({
23481
+ name: external_exports.string().min(1).max(50),
23482
+ voice: voiceSchema.describe(VOICE_HELP),
23483
+ style: external_exports.string().max(500).optional().describe('Entrega deste locutor (s\xF3 modelos 3.8), ex.: "calmo, grave, pausado".')
23484
+ });
23466
23485
  function registerAudioTools(server, client, cfg) {
23467
23486
  server.registerTool(
23468
23487
  "generate_audio",
23469
23488
  {
23470
23489
  title: "Gerar \xE1udio (TTS)",
23471
- 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,
23490
+ description: 'Sintetiza fala a partir de texto (s\xEDncrono \u2014 retorna a URL na hora) usando o IA Lab da BePlus. Modelos Gemini 3.8 (`gemini-3.8-flash-lite-tts`, padr\xE3o, r\xE1pido e barato; `gemini-3.8-flash-tts`, qualidade de est\xFAdio, melhor para di\xE1logo e narra\xE7\xE3o longa) aceitam: vozes da biblioteca (milhares, por idioma; liste com `list_tts_voices` e passe o id em `voice`), tags vocais no texto como <laugh> <chuckle> <sigh> <breath> <gasp> <cough> <sneeze> <throat-clearing> <yawn> <groan> <scream> <whimper> <argh> <short pause> <long pause> (viram som de verdade), e estilo por locutor (`style` em cada speaker). No 3.8, `style_instruction` \xE9 dire\xE7\xE3o e N\xC3O \xE9 lida em voz alta. Os modelos legados (`gemini-3.1-flash-tts-preview`, `gemini-2.5-pro-preview-tts`) s\xF3 aceitam as 30 vozes cl\xE1ssicas (Kore, Puck, Charon...) e n\xE3o t\xEAm voz da biblioteca. Opcional `style_instruction` (ex.: "fale animado e pausado") e di\xE1logo com 2 locutores via `multi_speaker`. Consome diamantes por segundo de \xE1udio.' + TEAM_GATING_NOTE,
23472
23491
  inputSchema: {
23473
23492
  text: external_exports.string().min(1).max(1e4).describe("Texto a ser falado."),
23474
- voice: external_exports.enum(VOICES).default(DEFAULTS.voice).describe("Voz (ex.: Kore, Puck, Charon)."),
23493
+ voice: voiceSchema.default(DEFAULTS.voice).describe(VOICE_HELP),
23475
23494
  model: external_exports.enum(TTS_MODELS).default(DEFAULTS.ttsModel).describe("Modelo TTS."),
23476
23495
  style_instruction: external_exports.string().max(2e3).optional().describe("Instru\xE7\xE3o de estilo/tom aplicada \xE0 fala."),
23477
23496
  multi_speaker: external_exports.object({
23478
- speaker1: external_exports.object({ name: external_exports.string().min(1).max(50), voice: external_exports.enum(VOICES) }),
23479
- speaker2: external_exports.object({ name: external_exports.string().min(1).max(50), voice: external_exports.enum(VOICES) })
23497
+ speaker1: speakerSchema,
23498
+ speaker2: speakerSchema
23480
23499
  }).optional().describe('Di\xE1logo de 2 locutores. Marque as falas no texto como "Nome: ...".'),
23481
23500
  project: external_exports.string().max(64).optional().describe('Projeto (uuid OU code curto, ex.: "VRAO-26"). Omita para usar o projeto ativo da conta.')
23482
23501
  }
23483
23502
  },
23484
23503
  async (args) => {
23485
23504
  try {
23505
+ const ms = args.multi_speaker;
23506
+ const voices = [args.voice, ms?.speaker1.voice, ms?.speaker2.voice].filter((v) => !!v);
23507
+ if (!TTS_MODELS_38.includes(args.model)) {
23508
+ if (voices.some(isLibraryVoice)) {
23509
+ return errorResult(
23510
+ `Vozes da biblioteca s\xF3 funcionam nos modelos Gemini 3.8 (gemini-3.8-flash-lite-tts, gemini-3.8-flash-tts). O modelo ${args.model} s\xF3 aceita as 30 vozes cl\xE1ssicas (Kore, Puck, Charon...).`
23511
+ );
23512
+ }
23513
+ if (ms?.speaker1.style || ms?.speaker2.style) {
23514
+ return errorResult("O estilo por locutor (`style`) s\xF3 funciona nos modelos Gemini 3.8.");
23515
+ }
23516
+ }
23486
23517
  const body2 = compact({
23487
23518
  text: args.text,
23488
23519
  voice: args.voice,
@@ -23516,6 +23547,45 @@ ${BUDGET_BLOCKED_GUIDANCE}`);
23516
23547
  }
23517
23548
  }
23518
23549
  );
23550
+ server.registerTool(
23551
+ "list_tts_voices",
23552
+ {
23553
+ title: "Listar vozes da biblioteca TTS",
23554
+ description: 'Lista vozes da biblioteca do Gemini 3.8 TTS (n\xE3o inclui as 30 cl\xE1ssicas como Kore e Puck). Filtre por idioma (`language`, BCP-47, padr\xE3o pt-BR; "all" = todos), `gender`, `pitch`, `context` (ex.: Audiobook) e `search` (texto livre em nome, persona, descri\xE7\xE3o, sotaque). Passe o `id` da voz em `voice` do generate_audio com um modelo 3.8 (gemini-3.8-flash-lite-tts ou gemini-3.8-flash-tts). Somente leitura, sem custo.',
23555
+ inputSchema: {
23556
+ language: external_exports.string().max(12).optional().describe('Idioma BCP-47, ex.: pt-BR, en-US. Padr\xE3o pt-BR. "all" = sem filtro.'),
23557
+ gender: external_exports.enum(["female", "male", "neutral"]).optional(),
23558
+ pitch: external_exports.enum(["low", "medium", "high"]).optional(),
23559
+ context: external_exports.string().max(60).optional().describe("Uso da voz (trecho), ex.: Audiobook, Enterprise Agent."),
23560
+ search: external_exports.string().max(100).optional().describe("Texto livre: nome, persona, descri\xE7\xE3o, sotaque."),
23561
+ limit: external_exports.number().int().min(1).max(200).optional().describe("Padr\xE3o 60."),
23562
+ offset: external_exports.number().int().min(0).optional()
23563
+ }
23564
+ },
23565
+ async (args) => {
23566
+ try {
23567
+ const res = await client.ttsVoices(compact(args));
23568
+ const offset = args.offset ?? 0;
23569
+ if (!res.voices.length) {
23570
+ return textResult(`Nenhuma voz encontrada (total ${res.total}). Idiomas dispon\xEDveis: ${res.languages.join(", ")}.`);
23571
+ }
23572
+ const lines = res.voices.map((v) => {
23573
+ const desc = v.description ? ` | ${v.description.length > 110 ? `${v.description.slice(0, 107)}...` : v.description}` : "";
23574
+ return `${v.id} | ${v.name} | ${v.gender} | ${v.pitch}${desc}`;
23575
+ });
23576
+ const more = res.total > offset + res.voices.length ? ` Use offset=${offset + res.voices.length} para ver mais.` : "";
23577
+ return textResult(
23578
+ `Vozes ${offset + 1}-${offset + res.voices.length} de ${res.total} (id | nome | g\xEAnero | tom | descri\xE7\xE3o):
23579
+ ${lines.join("\n")}
23580
+
23581
+ Use o id em voice (modelos 3.8).${more}
23582
+ Contextos: ${res.contexts.join(", ")}.`
23583
+ );
23584
+ } catch (e) {
23585
+ return errorResult(apiErrorToUserMessage(e));
23586
+ }
23587
+ }
23588
+ );
23519
23589
  }
23520
23590
 
23521
23591
  // src/tools/music.ts
@@ -24087,6 +24157,7 @@ import { createHash } from "crypto";
24087
24157
  // src/canvas/api.ts
24088
24158
  var PINNED_CORE_VERSION = "ca7b3b406184c7f679f555c3a008013197182b23";
24089
24159
  var RUN_REQUEST_TIMEOUT_MS = 4e4;
24160
+ var FOCUS_REQUEST_TIMEOUT_MS = 12e3;
24090
24161
  function errorRaw(err) {
24091
24162
  if (!(err instanceof ApiError)) return void 0;
24092
24163
  const raw = err.body?.error?.raw;
@@ -24221,6 +24292,11 @@ var CanvasApi = class {
24221
24292
  runStatus(boardId4, requestId) {
24222
24293
  return this.req("GET", `/boards/${boardId4}/run-requests/${requestId}`);
24223
24294
  }
24295
+ // ── enquadrar a aba do Canvas (CF1) ───────────────────────────────────────
24296
+ /** Pede para a aba do Canvas da pessoa enquadrar nós. O servidor espera a aba até 5 s. */
24297
+ focusRequest(boardId4, body2) {
24298
+ return this.req("POST", `/boards/${boardId4}/focus-requests`, { body: body2, timeoutMs: FOCUS_REQUEST_TIMEOUT_MS });
24299
+ }
24224
24300
  /** Só para os testes conferirem o aprendizado. */
24225
24301
  get currentCoreVersion() {
24226
24302
  return this.coreVersion;
@@ -24315,7 +24391,7 @@ var KINDS = {
24315
24391
  tts: {
24316
24392
  title: "Locu\xE7\xE3o",
24317
24393
  size: { w: 320, h: 212 },
24318
- data: { model: "gemini-3.1-flash-tts-preview", voice: "Algieba", speed: "normal" },
24394
+ data: { model: "gemini-3.8-flash-lite-tts", voice: "Algieba", speed: "normal" },
24319
24395
  inputs: ["text"],
24320
24396
  outputs: ["audio", "approved"],
24321
24397
  dynamic: "style conforme o modelo"
@@ -24551,13 +24627,13 @@ function makeEdge(init) {
24551
24627
  }
24552
24628
  var setValue = (value) => ({ kind: "value", value });
24553
24629
  var unsetValue = () => ({ kind: "unset" });
24554
- function removeNodeOps(doc, nodeId2) {
24630
+ function removeNodeOps(doc, nodeId3) {
24555
24631
  const ops = [];
24556
- const edgeIds = Object.keys(doc.edges).filter((id) => doc.edges[id].source === nodeId2 || doc.edges[id].target === nodeId2).sort();
24632
+ const edgeIds = Object.keys(doc.edges).filter((id) => doc.edges[id].source === nodeId3 || doc.edges[id].target === nodeId3).sort();
24557
24633
  for (const edgeId of edgeIds) ops.push({ type: "edge.remove", edgeId });
24558
- const history = Object.hasOwn(doc.nodeHistory, nodeId2) ? doc.nodeHistory[nodeId2] : void 0;
24559
- if (history) for (const resultId of [...history].reverse()) ops.push({ type: "history.remove", nodeId: nodeId2, resultId });
24560
- ops.push({ type: "node.remove", nodeId: nodeId2 });
24634
+ const history = Object.hasOwn(doc.nodeHistory, nodeId3) ? doc.nodeHistory[nodeId3] : void 0;
24635
+ if (history) for (const resultId of [...history].reverse()) ops.push({ type: "history.remove", nodeId: nodeId3, resultId });
24636
+ ops.push({ type: "node.remove", nodeId: nodeId3 });
24561
24637
  return ops;
24562
24638
  }
24563
24639
  function batchOf(ops) {
@@ -24958,8 +25034,8 @@ function mmss(t) {
24958
25034
  const pad = (n) => String(n).padStart(2, "0");
24959
25035
  return h ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
24960
25036
  }
24961
- function nodeRef(doc, nodeId2) {
24962
- const node = doc.nodes[nodeId2];
25037
+ function nodeRef(doc, nodeId3) {
25038
+ const node = doc.nodes[nodeId3];
24963
25039
  const kind = nodeKind(node) ?? node.type;
24964
25040
  return `"${oneLine(nodeName(node), 60)}" (${kind}, ${node.id})`;
24965
25041
  }
@@ -24983,13 +25059,13 @@ function outputKeyOf(entry) {
24983
25059
  if (typeof entry.url === "string" && entry.url !== "") return `url-${hashString(entry.url)}`;
24984
25060
  return null;
24985
25061
  }
24986
- function versionOf(doc, nodeId2, outputKey) {
24987
- const ids = doc.nodeHistory?.[nodeId2];
25062
+ function versionOf(doc, nodeId3, outputKey) {
25063
+ const ids = doc.nodeHistory?.[nodeId3];
24988
25064
  if (Array.isArray(ids)) {
24989
25065
  const i = ids.indexOf(outputKey);
24990
25066
  if (i >= 0) return i + 1;
24991
25067
  }
24992
- const history = doc.nodes[nodeId2]?.data.history;
25068
+ const history = doc.nodes[nodeId3]?.data.history;
24993
25069
  if (Array.isArray(history)) {
24994
25070
  const i = history.findIndex(
24995
25071
  (h) => typeof h === "object" && h !== null && !Array.isArray(h) && outputKeyOf(h) === outputKey
@@ -25459,17 +25535,17 @@ function holdsOne(doc, node) {
25459
25535
  if (node.data.holds === "many") return false;
25460
25536
  return itemsOf(node).length === 1 && Object.values(doc.edges).some((e) => e.source === node.id);
25461
25537
  }
25462
- function needMediaNode(doc, nodeId2) {
25463
- const node = doc.nodes[nodeId2];
25464
- if (!node) throw new CanvasToolError(`N\xF3 ${nodeId2} n\xE3o existe no board. Releia com canvas_read_board.`);
25538
+ function needMediaNode(doc, nodeId3) {
25539
+ const node = doc.nodes[nodeId3];
25540
+ if (!node) throw new CanvasToolError(`N\xF3 ${nodeId3} n\xE3o existe no board. Releia com canvas_read_board.`);
25465
25541
  if (nodeKind(node) !== "media") {
25466
25542
  throw new CanvasToolError(
25467
- `O n\xF3 ${nodeId2} ("${oneLine(nodeName(node), 40)}") \xE9 ${nodeKind(node) ?? node.type}, n\xE3o M\xEDdia. Passe o id de um n\xF3 media, ou omita node_id para criar um n\xF3 M\xEDdia novo.`
25543
+ `O n\xF3 ${nodeId3} ("${oneLine(nodeName(node), 40)}") \xE9 ${nodeKind(node) ?? node.type}, n\xE3o M\xEDdia. Passe o id de um n\xF3 media, ou omita node_id para criar um n\xF3 M\xEDdia novo.`
25468
25544
  );
25469
25545
  }
25470
25546
  return node;
25471
25547
  }
25472
- function payload(u, nodeId2) {
25548
+ function payload(u, nodeId3) {
25473
25549
  const out = { url: u.url, mime: u.mime, bytes: u.bytes };
25474
25550
  if (u.meta.duration !== void 0) out.duration = u.meta.duration;
25475
25551
  if (u.meta.width !== void 0) out.width = u.meta.width;
@@ -25478,14 +25554,14 @@ function payload(u, nodeId2) {
25478
25554
  out.media_item = mediaItem(u);
25479
25555
  if (u.kind !== "file") {
25480
25556
  out.sequence_asset = sequenceAsset(u);
25481
- out.libraryItemId = libraryItemId(u, nodeId2);
25557
+ out.libraryItemId = libraryItemId(u, nodeId3);
25482
25558
  }
25483
25559
  return JSON.stringify(out);
25484
25560
  }
25485
- function sequenceHint(u, nodeId2) {
25561
+ function sequenceHint(u, nodeId3) {
25486
25562
  if (u.kind === "file") return "Documento: entra no n\xF3 M\xEDdia, n\xE3o na timeline da Montagem.";
25487
25563
  const lines = [
25488
- "Num clipe da timeline da Montagem (data.timeline; data.sequence s\xF3 em n\xF3 que ainda n\xE3o migrou): use sequence_asset como `asset` e libraryItemId como est\xE1" + (nodeId2 ? "." : ", trocando <id do n\xF3 M\xEDdia> pelo id do n\xF3 onde o item ficou.")
25564
+ "Num clipe da timeline da Montagem (data.timeline; data.sequence s\xF3 em n\xF3 que ainda n\xE3o migrou): use sequence_asset como `asset` e libraryItemId como est\xE1" + (nodeId3 ? "." : ", trocando <id do n\xF3 M\xEDdia> pelo id do n\xF3 onde o item ficou.")
25489
25565
  ];
25490
25566
  if (u.meta.duration !== void 0) {
25491
25567
  const at30 = Math.floor(u.meta.duration * 30 + 1e-9);
@@ -25543,13 +25619,13 @@ URL: ${u.url}`;
25543
25619
  return [head, "", payload(u), "", sequenceHint(u)].join("\n");
25544
25620
  }
25545
25621
  const boardId4 = args.board_id;
25546
- let nodeId2 = args.node_id;
25622
+ let nodeId3 = args.node_id;
25547
25623
  let done;
25548
25624
  try {
25549
25625
  done = await edit(boardId4, async (doc) => {
25550
25626
  const item = mediaItem(u);
25551
- if (nodeId2) {
25552
- const node = needMediaNode(doc, nodeId2);
25627
+ if (nodeId3) {
25628
+ const node = needMediaNode(doc, nodeId3);
25553
25629
  const replacing = args.replace ?? holdsOne(doc, node);
25554
25630
  const items = replacing ? [item] : [...itemsOf(node), item];
25555
25631
  if (items.length > MAX_MEDIA_ITEMS) {
@@ -25571,7 +25647,7 @@ URL: ${u.url}`;
25571
25647
  zIndex: Object.values(doc.nodes).reduce((t, n) => Math.max(t, n.zIndex), 0) + 1,
25572
25648
  createdBy: await api.me()
25573
25649
  });
25574
- nodeId2 = created.id;
25650
+ nodeId3 = created.id;
25575
25651
  return {
25576
25652
  ops: [{ type: "node.add", nodeId: created.id, node: created }],
25577
25653
  targets: [],
@@ -25587,7 +25663,7 @@ N\xE3o suba de novo: releia com canvas_read_board e grave media_item em data.ite
25587
25663
  ${payload(u)}`
25588
25664
  );
25589
25665
  }
25590
- return [head, done, "", payload(u, nodeId2), "", sequenceHint(u, nodeId2)].join("\n");
25666
+ return [head, done, "", payload(u, nodeId3), "", sequenceHint(u, nodeId3)].join("\n");
25591
25667
  })
25592
25668
  );
25593
25669
  }
@@ -25615,12 +25691,16 @@ function estimateLines(estimate, titleOf) {
25615
25691
  const cost = n.error ? `estimativa falhou (${n.error})` : dia(n.diamonds);
25616
25692
  lines.push(`- ${n.title ?? titleOf(n.nodeId)} (${n.nodeId}): ${cost}`);
25617
25693
  }
25618
- const cached2 = nodes.filter((n) => !n.willRun && !n.afterApproval).length;
25694
+ if (estimate.omitted) {
25695
+ lines.push(`\u2026 e mais ${estimate.omitted} n\xF3(s) fora da lista (a resposta da aba foi cortada pelo tamanho; o total vale para todos).`);
25696
+ }
25697
+ const cached2 = estimate.cached ?? nodes.filter((n) => !n.willRun && !n.afterApproval).length;
25619
25698
  if (cached2) lines.push(`Em cache (n\xE3o rodam): ${cached2}.`);
25620
- const later = estimate.later?.nodes?.length ?? 0;
25699
+ const later = estimate.counts?.later ?? estimate.later?.nodes?.length ?? 0;
25621
25700
  if (later) lines.push(`Esperando aprova\xE7\xE3o no board depois disso: ${later} n\xF3(s), \u2248 ${dia(estimate.later?.total ?? 0)} (fora desta conta).`);
25622
- if (estimate.unknown?.length) {
25623
- lines.push(`Sem estimativa: ${estimate.unknown.length} n\xF3(s) (texto cobra por token; o total \xE9 piso).`);
25701
+ const unknown2 = estimate.counts?.unknown ?? estimate.unknown?.length ?? 0;
25702
+ if (unknown2) {
25703
+ lines.push(`Sem estimativa: ${unknown2} n\xF3(s) (texto cobra por token; o total \xE9 piso).`);
25624
25704
  }
25625
25705
  return lines;
25626
25706
  }
@@ -25648,7 +25728,7 @@ function registerCanvasRunTools(server, api, run) {
25648
25728
  return (id) => id;
25649
25729
  }
25650
25730
  }
25651
- function explain(e, requestId) {
25731
+ function explain2(e, requestId) {
25652
25732
  if (!(e instanceof ApiError)) throw e;
25653
25733
  const reason = errorRaw(e)?.reason;
25654
25734
  const unknownFate = requestId && (e.httpStatus === 0 || e.httpStatus === 408 || reason === "tab-timeout" || reason === "tab-bad-answer" || e.httpStatus >= 500 && e.httpStatus !== 501);
@@ -25730,7 +25810,7 @@ function registerCanvasRunTools(server, api, run) {
25730
25810
  label
25731
25811
  });
25732
25812
  } catch (e) {
25733
- explain(e, requestId);
25813
+ explain2(e, requestId);
25734
25814
  }
25735
25815
  const titleOf = await titles(args.board_id);
25736
25816
  if (answer.ok !== true) {
@@ -25743,7 +25823,8 @@ function registerCanvasRunTools(server, api, run) {
25743
25823
  if (answer.status === "started") {
25744
25824
  remember(requestId, {
25745
25825
  boardId: args.board_id,
25746
- nodeIds: nodeIds.length ? nodeIds : target.kind === "nodes" ? target.nodeIds : target.kind === "from" ? [target.nodeId] : []
25826
+ nodeIds: nodeIds.length ? nodeIds : target.kind === "nodes" ? target.nodeIds : target.kind === "from" ? [target.nodeId] : [],
25827
+ ...answer.estimate?.omitted ? { omitted: answer.estimate.omitted } : {}
25747
25828
  });
25748
25829
  }
25749
25830
  return answerText(answer, requestId, args.max_diamonds, titleOf);
@@ -25768,14 +25849,16 @@ function registerCanvasRunTools(server, api, run) {
25768
25849
  return statusText(await api.runStatus(board_id, requestId), await titles(board_id));
25769
25850
  } catch (e) {
25770
25851
  const reason = errorRaw(e)?.reason;
25771
- if (!(e instanceof ApiError) || reason !== "unknown-request" && reason !== "no-tab") explain(e);
25852
+ if (!(e instanceof ApiError) || reason !== "unknown-request" && reason !== "no-tab") explain2(e);
25772
25853
  why = reason === "no-tab" ? "Nenhuma aba do Canvas sua est\xE1 aberta neste board." : "Nenhuma aba aberta conhece este pedido.";
25773
25854
  }
25774
- const ids = node_ids ?? remembered.get(requestId)?.nodeIds;
25855
+ const known = node_ids ? void 0 : remembered.get(requestId);
25856
+ const ids = node_ids ?? known?.nodeIds;
25775
25857
  if (!ids?.length) {
25776
25858
  return `${why} Sem a aba, o andamento sai do estado de execu\xE7\xE3o do board: chame de novo com node_ids (os n\xF3s que o pedido rodava).`;
25777
25859
  }
25778
- return `${why} Pelo estado de execu\xE7\xE3o do board:
25860
+ const partial2 = known?.omitted ? ` (parcial: s\xF3 os ${ids.length} n\xF3s que a aba listou; outros ${known.omitted} ficaram fora da resposta. Para eles, chame com node_ids)` : "";
25861
+ return `${why} Pelo estado de execu\xE7\xE3o do board${partial2}:
25779
25862
  ${fromRuns(await api.snapshot(board_id), ids)}`;
25780
25863
  })
25781
25864
  );
@@ -25815,6 +25898,7 @@ function statusText(a, titleOf) {
25815
25898
  const outs = typeof n.outputs === "number" && n.outputs > 0 ? ` (${n.outputs} sa\xEDda(s))` : "";
25816
25899
  lines.push(`- ${n.title ?? titleOf(n.nodeId)} (${n.nodeId}): ${NODE_TEXT[n.status] ?? n.status}${outs}${extra}`);
25817
25900
  }
25901
+ if (a.omitted) lines.push(`\u2026 e mais ${a.omitted} n\xF3(s) fora da lista (a resposta da aba foi cortada pelo tamanho).`);
25818
25902
  if (a.state === "done" || a.state === "failed") lines.push("Leia o resultado com canvas_read_board (node_ids).");
25819
25903
  return lines.join("\n");
25820
25904
  }
@@ -25834,6 +25918,112 @@ function fromRuns(snap, ids) {
25834
25918
  }).join("\n");
25835
25919
  }
25836
25920
 
25921
+ // src/tools/canvas-focus.ts
25922
+ var MAX_IDS = 100;
25923
+ var nodeId2 = external_exports.string().min(1).max(128);
25924
+ var fmtZoom = (z) => new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 2 }).format(z);
25925
+ function explain(e, usesPat) {
25926
+ if (!(e instanceof ApiError)) throw e;
25927
+ const reason = errorRaw(e)?.reason;
25928
+ if (reason === "no-tab") {
25929
+ throw new CanvasToolError(
25930
+ "Nenhuma aba do Canvas sua est\xE1 aberta neste board (ou a aba \xE9 de uma vers\xE3o sem esta fun\xE7\xE3o). Pe\xE7a para abrir o board no Canvas com a mesma conta deste MCP (ou recarregar a aba) e tente de novo. Nada mudou."
25931
+ );
25932
+ }
25933
+ if (reason === "tab-timeout") {
25934
+ throw new CanvasToolError("A aba do Canvas n\xE3o respondeu em 5 s. Ela pode ter enquadrado; chamar de novo n\xE3o faz mal.");
25935
+ }
25936
+ if (reason === "tab-bad-answer") {
25937
+ throw new CanvasToolError("A aba do Canvas respondeu fora do contrato. Pe\xE7a para recarregar a aba e tente de novo.");
25938
+ }
25939
+ if (e.httpStatus === 404) {
25940
+ if (!/board/i.test(e.message)) {
25941
+ throw new CanvasToolError("O servidor do Canvas ainda n\xE3o tem o enquadramento pelo MCP (academy-backend sem a CF1). Nada mudou.");
25942
+ }
25943
+ throw new CanvasToolError("Board n\xE3o encontrado, ou voc\xEA n\xE3o \xE9 membro dele. Confira o id em canvas_list_boards.");
25944
+ }
25945
+ if (e.httpStatus === 403 && (!usesPat || /ialab:/.test(e.message))) {
25946
+ throw new CanvasToolError("O login pelo navegador deste MCP n\xE3o tem a permiss\xE3o que o enquadramento exige. Rode `beplus-mcp login` de novo e autorize.");
25947
+ }
25948
+ if (e.httpStatus === 400) throw new CanvasToolError(`O servidor recusou o pedido: ${e.message}.`);
25949
+ if (e.httpStatus === 429) throw new CanvasToolError("Limite de pedidos de enquadramento (60 por minuto). Espere um minuto.");
25950
+ throw e;
25951
+ }
25952
+ function focusText(a) {
25953
+ const tab = a.tab;
25954
+ const name = tab?.title ? `"${tab.title}"` : "aberta";
25955
+ const found = a.found ?? 0;
25956
+ const selected = a.selected ?? 0;
25957
+ const zoom = typeof a.zoom === "number" ? ` Zoom ${fmtZoom(a.zoom)}.` : "";
25958
+ const lines = [
25959
+ `Enquadrado na aba do Canvas ${name}: ${found} n\xF3(s)${selected > 0 ? `, ${selected} selecionado(s)` : ", sem mexer na sele\xE7\xE3o"}.${zoom}`
25960
+ ];
25961
+ if (tab?.human !== void 0 || tab?.relayed !== void 0) {
25962
+ const who2 = tab.human === false ? "Aba de agente (link ?frame): nenhuma aba do usu\xE1rio estava vis\xEDvel neste board." : "Aba do usu\xE1rio (a que ele est\xE1 olhando).";
25963
+ const via = tab.relayed ? " Repassado pela aba que o servidor escolheu." : "";
25964
+ lines.push(`${who2}${via}`);
25965
+ }
25966
+ if (a.mode === "follow") {
25967
+ lines.push("Modo seguir: a aba acompanha essa \xE1rea e fica presa nela at\xE9 o usu\xE1rio clicar no \xD7 da p\xEDlula.");
25968
+ } else if (a.mode === "light") {
25969
+ lines.push("Uma vez s\xF3: a aba n\xE3o fica presa; o usu\xE1rio segue mexendo \xE0 vontade.");
25970
+ }
25971
+ const missing = a.missing ?? [];
25972
+ if (missing.length > 0) {
25973
+ const more = a.missingMore ? ` e mais ${a.missingMore}` : "";
25974
+ lines.push(`N\xE3o existem no board (ou est\xE3o arquivados): ${missing.join(", ")}${more}.`);
25975
+ }
25976
+ if (tab?.visible === false) lines.push("A aba est\xE1 escondida (atr\xE1s de outra): pe\xE7a para o usu\xE1rio traz\xEA-la para a frente.");
25977
+ if (tab?.montage) lines.push("O usu\xE1rio est\xE1 no editor (Montagem aberta nessa aba): o board foi enquadrado por tr\xE1s e aparece quando ele fechar a Montagem.");
25978
+ if (tab?.wasFramed) lines.push("Essa aba estava enquadrada em outra \xE1rea (talvez a de outro agente) e agora mostra esta.");
25979
+ return lines.join("\n");
25980
+ }
25981
+ function registerCanvasFocusTools(server, api, run) {
25982
+ server.registerTool(
25983
+ "canvas_focus",
25984
+ {
25985
+ title: "Canvas: enquadrar a aba do usu\xE1rio",
25986
+ description: 'Mostra "olha aqui" ao usu\xE1rio: leva a aba do Canvas que ele est\xE1 olhando neste board (a vis\xEDvel, que n\xE3o \xE9 aba de agente aberta por link ?frame; sem nenhuma, a de um agente) at\xE9 n\xF3s ou uma se\xE7\xE3o, sem anima\xE7\xE3o, e seleciona os n\xF3s (select: false n\xE3o mexe na sele\xE7\xE3o). Padr\xE3o: enquadra e seleciona UMA vez; a aba n\xE3o fica presa e o usu\xE1rio segue mexendo. follow: true segue a \xE1rea e prende a aba at\xE9 o \xD7 (modo ?frame: acompanha as mudan\xE7as, titulo na p\xEDlula e no nome da aba); use s\xF3 quando ele pedir para acompanhar. N\xC3O gasta e N\xC3O escreve no board. Diga node_ids e/ou section_id (at\xE9 100 ids; com os dois, a sele\xE7\xE3o fica nos node_ids). zoom: "fit" (padr\xE3o, cabe na tela) ou n\xFAmero de 0.07 a 4. Devolve qual aba enquadrou (nome, se \xE9 a do usu\xE1rio ou de agente, se veio repassada, se est\xE1 escondida, se ele est\xE1 no editor da Montagem) e os ids que n\xE3o existem. Sem aba: pe\xE7a para abrir (ou recarregar) o board no Canvas. Chamar de novo com a mesma \xE1rea \xE9 seguro.',
25987
+ inputSchema: {
25988
+ board_id: external_exports.string().uuid().describe("Id (uuid) do board."),
25989
+ node_ids: external_exports.array(nodeId2).min(1).max(MAX_IDS).optional().describe("Os n\xF3s a mostrar (e selecionar)."),
25990
+ section_id: nodeId2.optional().describe("Uma se\xE7\xE3o: enquadra a se\xE7\xE3o e o que est\xE1 dentro dela."),
25991
+ titulo: external_exports.string().trim().min(1).max(80).optional().describe('Com follow: texto da p\xEDlula e do nome da aba ("<titulo> \xB7 <board>"). Sem follow n\xE3o aparece.'),
25992
+ zoom: external_exports.union([external_exports.literal("fit"), external_exports.number().finite().min(0.07).max(4)]).optional().describe('"fit" (padr\xE3o): a \xE1rea cabe na tela, zoom at\xE9 100%. N\xFAmero: zoom fixo, com a \xE1rea no meio.'),
25993
+ select: external_exports.boolean().optional().describe("false: n\xE3o mexe na sele\xE7\xE3o. Padr\xE3o: seleciona os n\xF3s."),
25994
+ follow: external_exports.boolean().optional().describe("true: segue a \xE1rea e prende a aba at\xE9 o \xD7 da p\xEDlula. Padr\xE3o false: enquadra e seleciona uma vez, sem prender.")
25995
+ },
25996
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
25997
+ },
25998
+ async (args) => run(async () => {
25999
+ const nodeIds = args.node_ids ? [...new Set(args.node_ids)] : [];
26000
+ if (nodeIds.length === 0 && !args.section_id) {
26001
+ throw new CanvasToolError("Diga o que mostrar: node_ids (os n\xF3s), section_id (uma se\xE7\xE3o) ou os dois.");
26002
+ }
26003
+ if (nodeIds.length + (args.section_id ? 1 : 0) > MAX_IDS) {
26004
+ throw new CanvasToolError(`At\xE9 ${MAX_IDS} ids por pedido (node_ids mais a se\xE7\xE3o).`);
26005
+ }
26006
+ let answer;
26007
+ try {
26008
+ answer = await api.focusRequest(args.board_id, {
26009
+ ...nodeIds.length ? { nodeIds } : {},
26010
+ ...args.section_id ? { sectionId: args.section_id } : {},
26011
+ ...args.titulo ? { title: args.titulo } : {},
26012
+ ...args.zoom !== void 0 ? { zoom: args.zoom } : {},
26013
+ ...args.select !== void 0 ? { select: args.select } : {},
26014
+ ...args.follow !== void 0 ? { follow: args.follow } : {}
26015
+ });
26016
+ } catch (e) {
26017
+ explain(e, api.usesPat);
26018
+ }
26019
+ if (answer.ok !== true) {
26020
+ throw new CanvasToolError(`A aba recusou, nada mudou (${answer.reason ?? "sem motivo"}): ${answer.message ?? ""}`.trim());
26021
+ }
26022
+ return focusText(answer);
26023
+ })
26024
+ );
26025
+ }
26026
+
25837
26027
  // src/canvas/mentions.ts
25838
26028
  var VIA_MENTION = "viaMention";
25839
26029
  var PROMPT_REFS_INPUT = "refs";
@@ -26124,9 +26314,9 @@ ${lines.join("\n")}
26124
26314
  return `\u2705 ${done} em ${envs.length} envelope(s) (seq at\xE9 ${last}).
26125
26315
  ${lines.join("\n")}${note}`;
26126
26316
  }
26127
- function needNode(doc, nodeId2) {
26128
- const node = doc.nodes[nodeId2];
26129
- if (!node) throw new CanvasToolError(`N\xF3 ${nodeId2} n\xE3o existe no board. Releia com canvas_read_board.`);
26317
+ function needNode(doc, nodeId3) {
26318
+ const node = doc.nodes[nodeId3];
26319
+ if (!node) throw new CanvasToolError(`N\xF3 ${nodeId3} n\xE3o existe no board. Releia com canvas_read_board.`);
26130
26320
  return node;
26131
26321
  }
26132
26322
  function portWarning(doc, source, sourcePort, target, targetPort) {
@@ -26740,13 +26930,14 @@ N\xF3s: ${Object.entries(ids).map(([k, v]) => `${k}=${v}`).join(" \xB7 ")}` : ""
26740
26930
  registerCanvasMediaTools(server, { api, cfg, run, edit, seen, placeFor });
26741
26931
  registerCanvasLayoutTools(server, { api, run, edit });
26742
26932
  registerCanvasRunTools(server, api, run);
26933
+ registerCanvasFocusTools(server, api, run);
26743
26934
  }
26744
26935
 
26745
26936
  // src/version.ts
26746
26937
  import { createRequire } from "module";
26747
26938
  var require2 = createRequire(import.meta.url);
26748
26939
  function readVersion() {
26749
- if ("0.31.0") return "0.31.0";
26940
+ if ("0.33.0") return "0.33.0";
26750
26941
  try {
26751
26942
  const pkg = require2("../package.json");
26752
26943
  return pkg.version ?? "0.0.0-unknown";
@@ -26762,6 +26953,7 @@ var SERVER_INSTRUCTIONS = [
26762
26953
  'M\xEDdia: generate_* gasta diamantes; confira com estimate_cost antes de lotes grandes. Sem project, a gera\xE7\xE3o cai no projeto ativo; billing:"personal" cobra do usu\xE1rio.',
26763
26954
  "Calls: search_calls acha, get_call traz a transcri\xE7\xE3o.",
26764
26955
  "Canvas online (canvas_*): leia com canvas_read_board antes de editar e siga o estado lido. Cada edi\xE7\xE3o vai num passo s\xF3; se outra pessoa mexeu nos mesmos n\xF3s, a ferramenta recusa e pede releitura. As canvas_* n\xE3o geram m\xEDdia, exceto canvas_run: roda n\xF3s pela aba do Canvas aberta do usu\xE1rio e gasta diamantes; max_diamonds \xE9 o teto que o usu\xE1rio autorizou (dry_run s\xF3 estima). D\xEA nome (label) aos n\xF3s, n\xE3o invente tipos nem portas, e confirme antes de canvas_delete.",
26956
+ 'Mostrar algo ao usu\xE1rio ("olha aqui"): canvas_focus leva a aba do Canvas que ele est\xE1 olhando at\xE9 os n\xF3s e os seleciona uma vez, sem prender a aba, sem gasto e sem escrita no board. follow: true prende a aba na \xE1rea at\xE9 o \xD7: s\xF3 se ele pedir.',
26765
26957
  'Arrumar o board: o size gravado \xE9 piso (o cart\xE3o cresce com o conte\xFAdo); use o tamanho "medido" do canvas_read_board. \xC1rea de revis\xE3o: canvas_arrange_rows (uma linha por vers\xE3o); posi\xE7\xE3o/tamanho em lote: canvas_update_nodes com layout_only. N\xE3o afirme que nada se sobrep\xF5e com n\xF3 sem medida.',
26766
26958
  '"Aplica os coment\xE1rios" do board: canvas_list_comments (abertos) \u2192 para cada um, leia o n\xF3 ancorado com canvas_read_board (node_ids), aplique a edi\xE7\xE3o pelas canvas_* e feche com canvas_resolve_comment dizendo em uma frase o que fez. Coment\xE1rio amb\xEDguo ou com alvo sumido: responda com pergunta (canvas_reply_comment) e n\xE3o resolva. D\xFAvida sua sobre um n\xF3: canvas_comment ancorado nele. As escritas j\xE1 saem espa\xE7adas (60/min); se ainda vier limite, pare e diga de onde seguir.'
26767
26959
  ].join("\n");