beplus-mcp 0.31.0 → 0.32.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
@@ -140,10 +140,13 @@ Falam com `/api/v1/canvas`. Montam e editam o fluxo e **não geram mídia**, com
140
140
  | `canvas_reopen_comment` | Reabre, com motivo opcional. |
141
141
  | `canvas_comment` | Cria comentário raiz ancorado (nó, versão, minutagem, ponto ou board), para deixar pergunta na tela da pessoa. |
142
142
  | `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. |
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. Resposta grande vem cortada ("… e mais N"); o andamento sem aba de um pedido cortado avisa que é parcial. |
144
+ | `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
145
 
145
146
  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
147
 
148
+ 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 "×".
149
+
147
150
  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
151
 
149
152
  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
@@ -24087,6 +24087,7 @@ import { createHash } from "crypto";
24087
24087
  // src/canvas/api.ts
24088
24088
  var PINNED_CORE_VERSION = "ca7b3b406184c7f679f555c3a008013197182b23";
24089
24089
  var RUN_REQUEST_TIMEOUT_MS = 4e4;
24090
+ var FOCUS_REQUEST_TIMEOUT_MS = 12e3;
24090
24091
  function errorRaw(err) {
24091
24092
  if (!(err instanceof ApiError)) return void 0;
24092
24093
  const raw = err.body?.error?.raw;
@@ -24221,6 +24222,11 @@ var CanvasApi = class {
24221
24222
  runStatus(boardId4, requestId) {
24222
24223
  return this.req("GET", `/boards/${boardId4}/run-requests/${requestId}`);
24223
24224
  }
24225
+ // ── enquadrar a aba do Canvas (CF1) ───────────────────────────────────────
24226
+ /** Pede para a aba do Canvas da pessoa enquadrar nós. O servidor espera a aba até 5 s. */
24227
+ focusRequest(boardId4, body2) {
24228
+ return this.req("POST", `/boards/${boardId4}/focus-requests`, { body: body2, timeoutMs: FOCUS_REQUEST_TIMEOUT_MS });
24229
+ }
24224
24230
  /** Só para os testes conferirem o aprendizado. */
24225
24231
  get currentCoreVersion() {
24226
24232
  return this.coreVersion;
@@ -24551,13 +24557,13 @@ function makeEdge(init) {
24551
24557
  }
24552
24558
  var setValue = (value) => ({ kind: "value", value });
24553
24559
  var unsetValue = () => ({ kind: "unset" });
24554
- function removeNodeOps(doc, nodeId2) {
24560
+ function removeNodeOps(doc, nodeId3) {
24555
24561
  const ops = [];
24556
- const edgeIds = Object.keys(doc.edges).filter((id) => doc.edges[id].source === nodeId2 || doc.edges[id].target === nodeId2).sort();
24562
+ const edgeIds = Object.keys(doc.edges).filter((id) => doc.edges[id].source === nodeId3 || doc.edges[id].target === nodeId3).sort();
24557
24563
  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 });
24564
+ const history = Object.hasOwn(doc.nodeHistory, nodeId3) ? doc.nodeHistory[nodeId3] : void 0;
24565
+ if (history) for (const resultId of [...history].reverse()) ops.push({ type: "history.remove", nodeId: nodeId3, resultId });
24566
+ ops.push({ type: "node.remove", nodeId: nodeId3 });
24561
24567
  return ops;
24562
24568
  }
24563
24569
  function batchOf(ops) {
@@ -24958,8 +24964,8 @@ function mmss(t) {
24958
24964
  const pad = (n) => String(n).padStart(2, "0");
24959
24965
  return h ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
24960
24966
  }
24961
- function nodeRef(doc, nodeId2) {
24962
- const node = doc.nodes[nodeId2];
24967
+ function nodeRef(doc, nodeId3) {
24968
+ const node = doc.nodes[nodeId3];
24963
24969
  const kind = nodeKind(node) ?? node.type;
24964
24970
  return `"${oneLine(nodeName(node), 60)}" (${kind}, ${node.id})`;
24965
24971
  }
@@ -24983,13 +24989,13 @@ function outputKeyOf(entry) {
24983
24989
  if (typeof entry.url === "string" && entry.url !== "") return `url-${hashString(entry.url)}`;
24984
24990
  return null;
24985
24991
  }
24986
- function versionOf(doc, nodeId2, outputKey) {
24987
- const ids = doc.nodeHistory?.[nodeId2];
24992
+ function versionOf(doc, nodeId3, outputKey) {
24993
+ const ids = doc.nodeHistory?.[nodeId3];
24988
24994
  if (Array.isArray(ids)) {
24989
24995
  const i = ids.indexOf(outputKey);
24990
24996
  if (i >= 0) return i + 1;
24991
24997
  }
24992
- const history = doc.nodes[nodeId2]?.data.history;
24998
+ const history = doc.nodes[nodeId3]?.data.history;
24993
24999
  if (Array.isArray(history)) {
24994
25000
  const i = history.findIndex(
24995
25001
  (h) => typeof h === "object" && h !== null && !Array.isArray(h) && outputKeyOf(h) === outputKey
@@ -25459,17 +25465,17 @@ function holdsOne(doc, node) {
25459
25465
  if (node.data.holds === "many") return false;
25460
25466
  return itemsOf(node).length === 1 && Object.values(doc.edges).some((e) => e.source === node.id);
25461
25467
  }
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.`);
25468
+ function needMediaNode(doc, nodeId3) {
25469
+ const node = doc.nodes[nodeId3];
25470
+ if (!node) throw new CanvasToolError(`N\xF3 ${nodeId3} n\xE3o existe no board. Releia com canvas_read_board.`);
25465
25471
  if (nodeKind(node) !== "media") {
25466
25472
  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.`
25473
+ `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
25474
  );
25469
25475
  }
25470
25476
  return node;
25471
25477
  }
25472
- function payload(u, nodeId2) {
25478
+ function payload(u, nodeId3) {
25473
25479
  const out = { url: u.url, mime: u.mime, bytes: u.bytes };
25474
25480
  if (u.meta.duration !== void 0) out.duration = u.meta.duration;
25475
25481
  if (u.meta.width !== void 0) out.width = u.meta.width;
@@ -25478,14 +25484,14 @@ function payload(u, nodeId2) {
25478
25484
  out.media_item = mediaItem(u);
25479
25485
  if (u.kind !== "file") {
25480
25486
  out.sequence_asset = sequenceAsset(u);
25481
- out.libraryItemId = libraryItemId(u, nodeId2);
25487
+ out.libraryItemId = libraryItemId(u, nodeId3);
25482
25488
  }
25483
25489
  return JSON.stringify(out);
25484
25490
  }
25485
- function sequenceHint(u, nodeId2) {
25491
+ function sequenceHint(u, nodeId3) {
25486
25492
  if (u.kind === "file") return "Documento: entra no n\xF3 M\xEDdia, n\xE3o na timeline da Montagem.";
25487
25493
  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.")
25494
+ "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
25495
  ];
25490
25496
  if (u.meta.duration !== void 0) {
25491
25497
  const at30 = Math.floor(u.meta.duration * 30 + 1e-9);
@@ -25543,13 +25549,13 @@ URL: ${u.url}`;
25543
25549
  return [head, "", payload(u), "", sequenceHint(u)].join("\n");
25544
25550
  }
25545
25551
  const boardId4 = args.board_id;
25546
- let nodeId2 = args.node_id;
25552
+ let nodeId3 = args.node_id;
25547
25553
  let done;
25548
25554
  try {
25549
25555
  done = await edit(boardId4, async (doc) => {
25550
25556
  const item = mediaItem(u);
25551
- if (nodeId2) {
25552
- const node = needMediaNode(doc, nodeId2);
25557
+ if (nodeId3) {
25558
+ const node = needMediaNode(doc, nodeId3);
25553
25559
  const replacing = args.replace ?? holdsOne(doc, node);
25554
25560
  const items = replacing ? [item] : [...itemsOf(node), item];
25555
25561
  if (items.length > MAX_MEDIA_ITEMS) {
@@ -25571,7 +25577,7 @@ URL: ${u.url}`;
25571
25577
  zIndex: Object.values(doc.nodes).reduce((t, n) => Math.max(t, n.zIndex), 0) + 1,
25572
25578
  createdBy: await api.me()
25573
25579
  });
25574
- nodeId2 = created.id;
25580
+ nodeId3 = created.id;
25575
25581
  return {
25576
25582
  ops: [{ type: "node.add", nodeId: created.id, node: created }],
25577
25583
  targets: [],
@@ -25587,7 +25593,7 @@ N\xE3o suba de novo: releia com canvas_read_board e grave media_item em data.ite
25587
25593
  ${payload(u)}`
25588
25594
  );
25589
25595
  }
25590
- return [head, done, "", payload(u, nodeId2), "", sequenceHint(u, nodeId2)].join("\n");
25596
+ return [head, done, "", payload(u, nodeId3), "", sequenceHint(u, nodeId3)].join("\n");
25591
25597
  })
25592
25598
  );
25593
25599
  }
@@ -25615,12 +25621,16 @@ function estimateLines(estimate, titleOf) {
25615
25621
  const cost = n.error ? `estimativa falhou (${n.error})` : dia(n.diamonds);
25616
25622
  lines.push(`- ${n.title ?? titleOf(n.nodeId)} (${n.nodeId}): ${cost}`);
25617
25623
  }
25618
- const cached2 = nodes.filter((n) => !n.willRun && !n.afterApproval).length;
25624
+ if (estimate.omitted) {
25625
+ 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).`);
25626
+ }
25627
+ const cached2 = estimate.cached ?? nodes.filter((n) => !n.willRun && !n.afterApproval).length;
25619
25628
  if (cached2) lines.push(`Em cache (n\xE3o rodam): ${cached2}.`);
25620
- const later = estimate.later?.nodes?.length ?? 0;
25629
+ const later = estimate.counts?.later ?? estimate.later?.nodes?.length ?? 0;
25621
25630
  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).`);
25631
+ const unknown2 = estimate.counts?.unknown ?? estimate.unknown?.length ?? 0;
25632
+ if (unknown2) {
25633
+ lines.push(`Sem estimativa: ${unknown2} n\xF3(s) (texto cobra por token; o total \xE9 piso).`);
25624
25634
  }
25625
25635
  return lines;
25626
25636
  }
@@ -25648,7 +25658,7 @@ function registerCanvasRunTools(server, api, run) {
25648
25658
  return (id) => id;
25649
25659
  }
25650
25660
  }
25651
- function explain(e, requestId) {
25661
+ function explain2(e, requestId) {
25652
25662
  if (!(e instanceof ApiError)) throw e;
25653
25663
  const reason = errorRaw(e)?.reason;
25654
25664
  const unknownFate = requestId && (e.httpStatus === 0 || e.httpStatus === 408 || reason === "tab-timeout" || reason === "tab-bad-answer" || e.httpStatus >= 500 && e.httpStatus !== 501);
@@ -25730,7 +25740,7 @@ function registerCanvasRunTools(server, api, run) {
25730
25740
  label
25731
25741
  });
25732
25742
  } catch (e) {
25733
- explain(e, requestId);
25743
+ explain2(e, requestId);
25734
25744
  }
25735
25745
  const titleOf = await titles(args.board_id);
25736
25746
  if (answer.ok !== true) {
@@ -25743,7 +25753,8 @@ function registerCanvasRunTools(server, api, run) {
25743
25753
  if (answer.status === "started") {
25744
25754
  remember(requestId, {
25745
25755
  boardId: args.board_id,
25746
- nodeIds: nodeIds.length ? nodeIds : target.kind === "nodes" ? target.nodeIds : target.kind === "from" ? [target.nodeId] : []
25756
+ nodeIds: nodeIds.length ? nodeIds : target.kind === "nodes" ? target.nodeIds : target.kind === "from" ? [target.nodeId] : [],
25757
+ ...answer.estimate?.omitted ? { omitted: answer.estimate.omitted } : {}
25747
25758
  });
25748
25759
  }
25749
25760
  return answerText(answer, requestId, args.max_diamonds, titleOf);
@@ -25768,14 +25779,16 @@ function registerCanvasRunTools(server, api, run) {
25768
25779
  return statusText(await api.runStatus(board_id, requestId), await titles(board_id));
25769
25780
  } catch (e) {
25770
25781
  const reason = errorRaw(e)?.reason;
25771
- if (!(e instanceof ApiError) || reason !== "unknown-request" && reason !== "no-tab") explain(e);
25782
+ if (!(e instanceof ApiError) || reason !== "unknown-request" && reason !== "no-tab") explain2(e);
25772
25783
  why = reason === "no-tab" ? "Nenhuma aba do Canvas sua est\xE1 aberta neste board." : "Nenhuma aba aberta conhece este pedido.";
25773
25784
  }
25774
- const ids = node_ids ?? remembered.get(requestId)?.nodeIds;
25785
+ const known = node_ids ? void 0 : remembered.get(requestId);
25786
+ const ids = node_ids ?? known?.nodeIds;
25775
25787
  if (!ids?.length) {
25776
25788
  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
25789
  }
25778
- return `${why} Pelo estado de execu\xE7\xE3o do board:
25790
+ 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)` : "";
25791
+ return `${why} Pelo estado de execu\xE7\xE3o do board${partial2}:
25779
25792
  ${fromRuns(await api.snapshot(board_id), ids)}`;
25780
25793
  })
25781
25794
  );
@@ -25815,6 +25828,7 @@ function statusText(a, titleOf) {
25815
25828
  const outs = typeof n.outputs === "number" && n.outputs > 0 ? ` (${n.outputs} sa\xEDda(s))` : "";
25816
25829
  lines.push(`- ${n.title ?? titleOf(n.nodeId)} (${n.nodeId}): ${NODE_TEXT[n.status] ?? n.status}${outs}${extra}`);
25817
25830
  }
25831
+ 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
25832
  if (a.state === "done" || a.state === "failed") lines.push("Leia o resultado com canvas_read_board (node_ids).");
25819
25833
  return lines.join("\n");
25820
25834
  }
@@ -25834,6 +25848,112 @@ function fromRuns(snap, ids) {
25834
25848
  }).join("\n");
25835
25849
  }
25836
25850
 
25851
+ // src/tools/canvas-focus.ts
25852
+ var MAX_IDS = 100;
25853
+ var nodeId2 = external_exports.string().min(1).max(128);
25854
+ var fmtZoom = (z) => new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 2 }).format(z);
25855
+ function explain(e, usesPat) {
25856
+ if (!(e instanceof ApiError)) throw e;
25857
+ const reason = errorRaw(e)?.reason;
25858
+ if (reason === "no-tab") {
25859
+ throw new CanvasToolError(
25860
+ "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."
25861
+ );
25862
+ }
25863
+ if (reason === "tab-timeout") {
25864
+ throw new CanvasToolError("A aba do Canvas n\xE3o respondeu em 5 s. Ela pode ter enquadrado; chamar de novo n\xE3o faz mal.");
25865
+ }
25866
+ if (reason === "tab-bad-answer") {
25867
+ throw new CanvasToolError("A aba do Canvas respondeu fora do contrato. Pe\xE7a para recarregar a aba e tente de novo.");
25868
+ }
25869
+ if (e.httpStatus === 404) {
25870
+ if (!/board/i.test(e.message)) {
25871
+ throw new CanvasToolError("O servidor do Canvas ainda n\xE3o tem o enquadramento pelo MCP (academy-backend sem a CF1). Nada mudou.");
25872
+ }
25873
+ throw new CanvasToolError("Board n\xE3o encontrado, ou voc\xEA n\xE3o \xE9 membro dele. Confira o id em canvas_list_boards.");
25874
+ }
25875
+ if (e.httpStatus === 403 && (!usesPat || /ialab:/.test(e.message))) {
25876
+ 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.");
25877
+ }
25878
+ if (e.httpStatus === 400) throw new CanvasToolError(`O servidor recusou o pedido: ${e.message}.`);
25879
+ if (e.httpStatus === 429) throw new CanvasToolError("Limite de pedidos de enquadramento (60 por minuto). Espere um minuto.");
25880
+ throw e;
25881
+ }
25882
+ function focusText(a) {
25883
+ const tab = a.tab;
25884
+ const name = tab?.title ? `"${tab.title}"` : "aberta";
25885
+ const found = a.found ?? 0;
25886
+ const selected = a.selected ?? 0;
25887
+ const zoom = typeof a.zoom === "number" ? ` Zoom ${fmtZoom(a.zoom)}.` : "";
25888
+ const lines = [
25889
+ `Enquadrado na aba do Canvas ${name}: ${found} n\xF3(s)${selected > 0 ? `, ${selected} selecionado(s)` : ", sem mexer na sele\xE7\xE3o"}.${zoom}`
25890
+ ];
25891
+ if (tab?.human !== void 0 || tab?.relayed !== void 0) {
25892
+ 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).";
25893
+ const via = tab.relayed ? " Repassado pela aba que o servidor escolheu." : "";
25894
+ lines.push(`${who2}${via}`);
25895
+ }
25896
+ if (a.mode === "follow") {
25897
+ lines.push("Modo seguir: a aba acompanha essa \xE1rea e fica presa nela at\xE9 o usu\xE1rio clicar no \xD7 da p\xEDlula.");
25898
+ } else if (a.mode === "light") {
25899
+ lines.push("Uma vez s\xF3: a aba n\xE3o fica presa; o usu\xE1rio segue mexendo \xE0 vontade.");
25900
+ }
25901
+ const missing = a.missing ?? [];
25902
+ if (missing.length > 0) {
25903
+ const more = a.missingMore ? ` e mais ${a.missingMore}` : "";
25904
+ lines.push(`N\xE3o existem no board (ou est\xE3o arquivados): ${missing.join(", ")}${more}.`);
25905
+ }
25906
+ 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.");
25907
+ 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.");
25908
+ if (tab?.wasFramed) lines.push("Essa aba estava enquadrada em outra \xE1rea (talvez a de outro agente) e agora mostra esta.");
25909
+ return lines.join("\n");
25910
+ }
25911
+ function registerCanvasFocusTools(server, api, run) {
25912
+ server.registerTool(
25913
+ "canvas_focus",
25914
+ {
25915
+ title: "Canvas: enquadrar a aba do usu\xE1rio",
25916
+ 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.',
25917
+ inputSchema: {
25918
+ board_id: external_exports.string().uuid().describe("Id (uuid) do board."),
25919
+ node_ids: external_exports.array(nodeId2).min(1).max(MAX_IDS).optional().describe("Os n\xF3s a mostrar (e selecionar)."),
25920
+ section_id: nodeId2.optional().describe("Uma se\xE7\xE3o: enquadra a se\xE7\xE3o e o que est\xE1 dentro dela."),
25921
+ 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.'),
25922
+ 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.'),
25923
+ select: external_exports.boolean().optional().describe("false: n\xE3o mexe na sele\xE7\xE3o. Padr\xE3o: seleciona os n\xF3s."),
25924
+ 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.")
25925
+ },
25926
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
25927
+ },
25928
+ async (args) => run(async () => {
25929
+ const nodeIds = args.node_ids ? [...new Set(args.node_ids)] : [];
25930
+ if (nodeIds.length === 0 && !args.section_id) {
25931
+ throw new CanvasToolError("Diga o que mostrar: node_ids (os n\xF3s), section_id (uma se\xE7\xE3o) ou os dois.");
25932
+ }
25933
+ if (nodeIds.length + (args.section_id ? 1 : 0) > MAX_IDS) {
25934
+ throw new CanvasToolError(`At\xE9 ${MAX_IDS} ids por pedido (node_ids mais a se\xE7\xE3o).`);
25935
+ }
25936
+ let answer;
25937
+ try {
25938
+ answer = await api.focusRequest(args.board_id, {
25939
+ ...nodeIds.length ? { nodeIds } : {},
25940
+ ...args.section_id ? { sectionId: args.section_id } : {},
25941
+ ...args.titulo ? { title: args.titulo } : {},
25942
+ ...args.zoom !== void 0 ? { zoom: args.zoom } : {},
25943
+ ...args.select !== void 0 ? { select: args.select } : {},
25944
+ ...args.follow !== void 0 ? { follow: args.follow } : {}
25945
+ });
25946
+ } catch (e) {
25947
+ explain(e, api.usesPat);
25948
+ }
25949
+ if (answer.ok !== true) {
25950
+ throw new CanvasToolError(`A aba recusou, nada mudou (${answer.reason ?? "sem motivo"}): ${answer.message ?? ""}`.trim());
25951
+ }
25952
+ return focusText(answer);
25953
+ })
25954
+ );
25955
+ }
25956
+
25837
25957
  // src/canvas/mentions.ts
25838
25958
  var VIA_MENTION = "viaMention";
25839
25959
  var PROMPT_REFS_INPUT = "refs";
@@ -26124,9 +26244,9 @@ ${lines.join("\n")}
26124
26244
  return `\u2705 ${done} em ${envs.length} envelope(s) (seq at\xE9 ${last}).
26125
26245
  ${lines.join("\n")}${note}`;
26126
26246
  }
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.`);
26247
+ function needNode(doc, nodeId3) {
26248
+ const node = doc.nodes[nodeId3];
26249
+ if (!node) throw new CanvasToolError(`N\xF3 ${nodeId3} n\xE3o existe no board. Releia com canvas_read_board.`);
26130
26250
  return node;
26131
26251
  }
26132
26252
  function portWarning(doc, source, sourcePort, target, targetPort) {
@@ -26740,13 +26860,14 @@ N\xF3s: ${Object.entries(ids).map(([k, v]) => `${k}=${v}`).join(" \xB7 ")}` : ""
26740
26860
  registerCanvasMediaTools(server, { api, cfg, run, edit, seen, placeFor });
26741
26861
  registerCanvasLayoutTools(server, { api, run, edit });
26742
26862
  registerCanvasRunTools(server, api, run);
26863
+ registerCanvasFocusTools(server, api, run);
26743
26864
  }
26744
26865
 
26745
26866
  // src/version.ts
26746
26867
  import { createRequire } from "module";
26747
26868
  var require2 = createRequire(import.meta.url);
26748
26869
  function readVersion() {
26749
- if ("0.31.0") return "0.31.0";
26870
+ if ("0.32.0") return "0.32.0";
26750
26871
  try {
26751
26872
  const pkg = require2("../package.json");
26752
26873
  return pkg.version ?? "0.0.0-unknown";
@@ -26762,6 +26883,7 @@ var SERVER_INSTRUCTIONS = [
26762
26883
  '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
26884
  "Calls: search_calls acha, get_call traz a transcri\xE7\xE3o.",
26764
26885
  "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.",
26886
+ '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
26887
  '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
26888
  '"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
26889
  ].join("\n");