beplus-mcp 0.25.0 → 0.27.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/dist/index.js CHANGED
@@ -916,10 +916,10 @@ var require_codegen = __commonJS({
916
916
  return this._leafNode(new Throw(error2));
917
917
  }
918
918
  // start self-balancing block
919
- block(body, nodeCount) {
919
+ block(body2, nodeCount) {
920
920
  this._blockStarts.push(this._nodes.length);
921
- if (body)
922
- this.code(body).endBlock(nodeCount);
921
+ if (body2)
922
+ this.code(body2).endBlock(nodeCount);
923
923
  return this;
924
924
  }
925
925
  // end the current self-balancing block
@@ -2344,15 +2344,15 @@ var require_validate = __commonJS({
2344
2344
  validateFunction(it, () => (0, boolSchema_1.topBoolOrEmptySchema)(it));
2345
2345
  }
2346
2346
  exports.validateFunctionCode = validateFunctionCode;
2347
- function validateFunction({ gen, validateName, schema, schemaEnv, opts }, body) {
2347
+ function validateFunction({ gen, validateName, schema, schemaEnv, opts }, body2) {
2348
2348
  if (opts.code.es5) {
2349
2349
  gen.func(validateName, (0, codegen_1._)`${names_1.default.data}, ${names_1.default.valCxt}`, schemaEnv.$async, () => {
2350
2350
  gen.code((0, codegen_1._)`"use strict"; ${funcSourceUrl(schema, opts)}`);
2351
2351
  destructureValCxtES5(gen, opts);
2352
- gen.code(body);
2352
+ gen.code(body2);
2353
2353
  });
2354
2354
  } else {
2355
- gen.func(validateName, (0, codegen_1._)`${names_1.default.data}, ${destructureValCxt(opts)}`, schemaEnv.$async, () => gen.code(funcSourceUrl(schema, opts)).code(body));
2355
+ gen.func(validateName, (0, codegen_1._)`${names_1.default.data}, ${destructureValCxt(opts)}`, schemaEnv.$async, () => gen.code(funcSourceUrl(schema, opts)).code(body2));
2356
2356
  }
2357
2357
  }
2358
2358
  function destructureValCxt(opts) {
@@ -13050,6 +13050,7 @@ function loadConfig() {
13050
13050
  return {
13051
13051
  baseUrl,
13052
13052
  apiBase: `${baseUrl}/api/v1/mcp`,
13053
+ canvasBase: `${baseUrl}/api/v1/canvas`,
13053
13054
  token,
13054
13055
  inlineImages,
13055
13056
  inlineImageMax: 4,
@@ -21155,11 +21156,11 @@ var EMPTY_COMPLETION_RESULT = {
21155
21156
  var ApiError = class extends Error {
21156
21157
  httpStatus;
21157
21158
  body;
21158
- constructor(httpStatus, message, body) {
21159
+ constructor(httpStatus, message, body2) {
21159
21160
  super(message);
21160
21161
  this.name = "ApiError";
21161
21162
  this.httpStatus = httpStatus;
21162
- this.body = body;
21163
+ this.body = body2;
21163
21164
  }
21164
21165
  };
21165
21166
  function apiErrorToUserMessage(err) {
@@ -21200,8 +21201,9 @@ var BeplusClient = class {
21200
21201
  this.cfg = cfg;
21201
21202
  }
21202
21203
  cfg;
21204
+ /** Público para os módulos de rota própria (ex.: Canvas) reaproveitarem auth, timeout e erros. */
21203
21205
  async request(method, path, opts = {}) {
21204
- const url = new URL(`${this.cfg.apiBase}${path}`);
21206
+ const url = new URL(`${opts.base ?? this.cfg.apiBase}${path}`);
21205
21207
  if (opts.query) {
21206
21208
  for (const [k, v] of Object.entries(opts.query)) {
21207
21209
  if (v !== void 0 && v !== null && v !== "") url.searchParams.set(k, String(v));
@@ -21242,14 +21244,14 @@ var BeplusClient = class {
21242
21244
  }
21243
21245
  return json2 && typeof json2 === "object" && "result" in json2 ? json2.result : json2;
21244
21246
  }
21245
- startGeneration(body) {
21246
- return this.request("POST", "/generations", { body });
21247
+ startGeneration(body2) {
21248
+ return this.request("POST", "/generations", { body: body2 });
21247
21249
  }
21248
21250
  listProjects() {
21249
21251
  return this.request("GET", "/projects");
21250
21252
  }
21251
- createProject(body) {
21252
- return this.request("POST", "/projects", { body });
21253
+ createProject(body2) {
21254
+ return this.request("POST", "/projects", { body: body2 });
21253
21255
  }
21254
21256
  getActiveProject() {
21255
21257
  return this.request("GET", "/projects/active");
@@ -21261,8 +21263,8 @@ var BeplusClient = class {
21261
21263
  listClients() {
21262
21264
  return this.request("GET", "/clients");
21263
21265
  }
21264
- createClient(body) {
21265
- return this.request("POST", "/clients", { body });
21266
+ createClient(body2) {
21267
+ return this.request("POST", "/clients", { body: body2 });
21266
21268
  }
21267
21269
  setActiveClient(client) {
21268
21270
  return this.request("PUT", "/clients/active", { body: { client } });
@@ -21284,8 +21286,8 @@ var BeplusClient = class {
21284
21286
  }
21285
21287
  });
21286
21288
  }
21287
- tts(body) {
21288
- return this.request("POST", "/tts", { body, timeoutMs: 9e4 });
21289
+ tts(body2) {
21290
+ return this.request("POST", "/tts", { body: body2, timeoutMs: 9e4 });
21289
21291
  }
21290
21292
  models() {
21291
21293
  return this.request("GET", "/models");
@@ -21300,11 +21302,11 @@ var BeplusClient = class {
21300
21302
  me(billing) {
21301
21303
  return this.request("GET", "/me", { query: { billing } });
21302
21304
  }
21303
- readProvenance(body) {
21304
- return this.request("POST", "/read-provenance", { body, timeoutMs: 3e4 });
21305
+ readProvenance(body2) {
21306
+ return this.request("POST", "/read-provenance", { body: body2, timeoutMs: 3e4 });
21305
21307
  }
21306
- analyzeMedia(body) {
21307
- return this.request("POST", "/analyze-media", { body, timeoutMs: 3e5 });
21308
+ analyzeMedia(body2) {
21309
+ return this.request("POST", "/analyze-media", { body: body2, timeoutMs: 3e5 });
21308
21310
  }
21309
21311
  searchCalls(query) {
21310
21312
  return this.request("GET", "/calls", {
@@ -22872,7 +22874,7 @@ function registerAudioTools(server, client, cfg) {
22872
22874
  },
22873
22875
  async (args) => {
22874
22876
  try {
22875
- const body = compact({
22877
+ const body2 = compact({
22876
22878
  text: args.text,
22877
22879
  voice: args.voice,
22878
22880
  model: args.model,
@@ -22880,7 +22882,7 @@ function registerAudioTools(server, client, cfg) {
22880
22882
  multiSpeaker: args.multi_speaker,
22881
22883
  project: args.project ?? cfg.activeProject ?? void 0
22882
22884
  });
22883
- const res = await client.tts(body);
22885
+ const res = await client.tts(body2);
22884
22886
  const parts = [`Custo: ${res.diamond_cost} \u{1F48E}`];
22885
22887
  if (res.diamonds_refunded) parts.push(`Estorno: ${res.diamonds_refunded} \u{1F48E}`);
22886
22888
  const projLine = res.project_code ? `
@@ -23376,11 +23378,11 @@ function fmtDate(iso) {
23376
23378
  function callLine(c) {
23377
23379
  const company = c.company_name ? ` \xB7 ${c.company_name}` : "";
23378
23380
  const proj = c.project_name ? ` \xB7 ${c.client_name ? c.client_name + " / " : ""}${c.project_name}` : "";
23379
- const who = c.speakers.length ? ` \xB7 ${c.speakers.join(", ")}` : c.participant_count ? ` \xB7 ${c.participant_count} falantes` : "";
23381
+ const who2 = c.speakers.length ? ` \xB7 ${c.speakers.join(", ")}` : c.participant_count ? ` \xB7 ${c.participant_count} falantes` : "";
23380
23382
  const vis = VIS_LABEL[c.visibility] ?? c.visibility;
23381
23383
  const flag = c.status !== "ready" ? ` [${c.status}]` : "";
23382
23384
  return `\u2022 ${c.title ?? "(sem t\xEDtulo)"}${flag}
23383
- ${fmtDate(c.date)} \xB7 ${fmtDuration(c.duration_sec)} \xB7 ${vis}${company}${proj}${who}
23385
+ ${fmtDate(c.date)} \xB7 ${fmtDuration(c.duration_sec)} \xB7 ${vis}${company}${proj}${who2}
23384
23386
  id: ${c.id}`;
23385
23387
  }
23386
23388
  function renderCall(c) {
@@ -23466,11 +23468,1381 @@ ${calls.map(callLine).join("\n\n")}`);
23466
23468
  );
23467
23469
  }
23468
23470
 
23471
+ // src/canvas/api.ts
23472
+ var PINNED_CORE_VERSION = "ca7b3b406184c7f679f555c3a008013197182b23";
23473
+ function errorRaw(err) {
23474
+ if (!(err instanceof ApiError)) return void 0;
23475
+ const raw = err.body?.error?.raw;
23476
+ return raw && typeof raw === "object" ? raw : void 0;
23477
+ }
23478
+ var commentPacing = {
23479
+ perMinute: 50,
23480
+ retryWaitsMs: [5e3, 15e3, 45e3],
23481
+ now: () => Date.now(),
23482
+ sleep: (ms) => new Promise((r) => setTimeout(r, Math.max(0, ms)))
23483
+ };
23484
+ var CanvasApi = class {
23485
+ constructor(client, cfg) {
23486
+ this.client = client;
23487
+ this.cfg = cfg;
23488
+ }
23489
+ client;
23490
+ cfg;
23491
+ coreVersion = PINNED_CORE_VERSION;
23492
+ userId = null;
23493
+ /** Instantes das escritas de comentário no último minuto. */
23494
+ writes = [];
23495
+ req(method, path, opts = {}) {
23496
+ return this.client.request(method, path, { ...opts, base: this.cfg.canvasBase });
23497
+ }
23498
+ /** Token do `beplus-mcp login` (OAuth) não abre `/api/v1/canvas`: só PAT. */
23499
+ get usesPat() {
23500
+ return this.cfg.token.startsWith("pat_");
23501
+ }
23502
+ /** `createdBy` dos nós que o MCP cria. Nulo seria "motor de execução" no Canvas. */
23503
+ async me() {
23504
+ if (!this.userId) this.userId = (await this.client.me()).userId;
23505
+ return this.userId;
23506
+ }
23507
+ listBoards(kind = "board") {
23508
+ return this.req("GET", "/boards", { query: { kind } });
23509
+ }
23510
+ createBoard(body2) {
23511
+ return this.req("POST", "/boards", { body: body2 });
23512
+ }
23513
+ snapshot(boardId3) {
23514
+ return this.req("GET", `/boards/${boardId3}`);
23515
+ }
23516
+ /** Replay depois de `after`. `null` quando o servidor manda reler a foto (410). */
23517
+ async opsAfter(boardId3, after) {
23518
+ try {
23519
+ const { envelopes } = await this.req("GET", `/boards/${boardId3}/ops`, {
23520
+ query: { after }
23521
+ });
23522
+ return envelopes;
23523
+ } catch (e) {
23524
+ if (e instanceof ApiError && e.httpStatus === 410) return null;
23525
+ throw e;
23526
+ }
23527
+ }
23528
+ async commit(boardId3, ops) {
23529
+ const send = () => this.req("POST", `/boards/${boardId3}/ops`, {
23530
+ body: { coreVersion: this.coreVersion, ops }
23531
+ });
23532
+ try {
23533
+ return await send();
23534
+ } catch (e) {
23535
+ const raw = errorRaw(e);
23536
+ if (!(e instanceof ApiError) || e.httpStatus !== 409 || raw?.reason !== "core-mismatch") throw e;
23537
+ const serverVersion = typeof raw.coreVersion === "string" ? raw.coreVersion : "";
23538
+ if (!serverVersion || serverVersion === this.coreVersion) throw new CoreMismatchError(serverVersion);
23539
+ this.coreVersion = serverVersion;
23540
+ try {
23541
+ return await send();
23542
+ } catch (again) {
23543
+ if (again instanceof ApiError && again.httpStatus === 409 && errorRaw(again)?.reason === "core-mismatch") {
23544
+ throw new CoreMismatchError(String(errorRaw(again)?.coreVersion ?? ""));
23545
+ }
23546
+ throw again;
23547
+ }
23548
+ }
23549
+ }
23550
+ members(boardId3) {
23551
+ return this.req("GET", `/boards/${boardId3}/members`);
23552
+ }
23553
+ putMember(boardId3, userId, role) {
23554
+ return this.req("PUT", `/boards/${boardId3}/members/${userId}`, { body: { role } });
23555
+ }
23556
+ removeMember(boardId3, userId) {
23557
+ return this.req("DELETE", `/boards/${boardId3}/members/${userId}`);
23558
+ }
23559
+ // ── comentários (academy-backend #313) ────────────────────────────────────
23560
+ listComments(boardId3, status = "open") {
23561
+ return this.req("GET", `/boards/${boardId3}/comments`, { query: { status } });
23562
+ }
23563
+ /**
23564
+ * Escrita de comentário passa por aqui: o back limita a 60 por minuto por pessoa
23565
+ * (balde fixo, sem Retry-After). O MCP se espaça abaixo do teto e, se ainda
23566
+ * assim vier 429 (a pessoa também comentando no Canvas), espera e tenta de novo.
23567
+ */
23568
+ async write(method, path, body2) {
23569
+ const p = commentPacing;
23570
+ const now = p.now();
23571
+ this.writes = this.writes.filter((t) => now - t < 6e4);
23572
+ if (this.writes.length >= p.perMinute) {
23573
+ await p.sleep(this.writes[0] + 6e4 - now);
23574
+ this.writes.shift();
23575
+ }
23576
+ for (let attempt = 0; ; attempt++) {
23577
+ this.writes.push(p.now());
23578
+ try {
23579
+ return await this.req(method, path, body2 === void 0 ? {} : { body: body2 });
23580
+ } catch (e) {
23581
+ if (!(e instanceof ApiError) || e.httpStatus !== 429 || attempt >= p.retryWaitsMs.length) throw e;
23582
+ await p.sleep(p.retryWaitsMs[attempt]);
23583
+ }
23584
+ }
23585
+ }
23586
+ /** Raiz ou resposta. O back devolve sempre a THREAD DA RAIZ (resposta nova = última de `replies`). */
23587
+ createComment(boardId3, body2) {
23588
+ return this.write("POST", `/boards/${boardId3}/comments`, body2);
23589
+ }
23590
+ resolveComment(boardId3, commentId2) {
23591
+ return this.write("POST", `/boards/${boardId3}/comments/${commentId2}/resolve`);
23592
+ }
23593
+ reopenComment(boardId3, commentId2) {
23594
+ return this.write("POST", `/boards/${boardId3}/comments/${commentId2}/reopen`);
23595
+ }
23596
+ /** Só para os testes conferirem o aprendizado. */
23597
+ get currentCoreVersion() {
23598
+ return this.coreVersion;
23599
+ }
23600
+ };
23601
+ var CoreMismatchError = class extends Error {
23602
+ constructor(serverVersion) {
23603
+ super(
23604
+ "O servidor do Canvas recusou a vers\xE3o do canvas-core deste MCP (409 core-mismatch)" + (serverVersion ? ` e segue exigindo ${serverVersion.slice(0, 12)}` : " sem informar a vers\xE3o esperada") + ". Nada foi aplicado. Atualize o beplus-mcp (npx -y beplus-mcp@latest) ou avise o time."
23605
+ );
23606
+ this.serverVersion = serverVersion;
23607
+ this.name = "CoreMismatchError";
23608
+ }
23609
+ serverVersion;
23610
+ };
23611
+ var CanvasToolError = class extends Error {
23612
+ };
23613
+
23614
+ // src/canvas/catalog.ts
23615
+ var WORKFLOW_NODE_TYPE = "workflow";
23616
+ var KIND_FIELD = "kind";
23617
+ var KINDS = {
23618
+ text: { title: "Texto", size: { w: 300, h: 160 }, data: { text: "" }, inputs: [], outputs: ["text"], main: "text" },
23619
+ block: { title: "Bloco", size: { w: 260, h: 140 }, data: { text: "" }, inputs: [], outputs: ["text"], main: "text" },
23620
+ prompt: {
23621
+ title: "Prompt",
23622
+ size: { w: 340, h: 200 },
23623
+ data: { template: "" },
23624
+ inputs: ["refs"],
23625
+ outputs: ["text", "refs"],
23626
+ dynamic: "uma entrada de texto por {{var}} do template",
23627
+ main: "template"
23628
+ },
23629
+ llm: {
23630
+ title: "Redator",
23631
+ size: { w: 340, h: 300 },
23632
+ data: { model: "gemini-flash", system: "", instruction: "", outputMode: "text" },
23633
+ inputs: ["instruction", "media"],
23634
+ outputs: ["text"],
23635
+ main: "instruction"
23636
+ },
23637
+ brief: {
23638
+ title: "Diretor",
23639
+ size: { w: 420, h: 360 },
23640
+ data: {
23641
+ model: "gemini-flash",
23642
+ sceneCount: 4,
23643
+ aspect_ratio: "16:9",
23644
+ secondsPerScene: 5,
23645
+ tone: "Natural e pr\xF3ximo, como quem conta para um amigo",
23646
+ language: "pt-BR"
23647
+ },
23648
+ inputs: ["briefing", "refs"],
23649
+ outputs: ["script", "narration", "music", "scenes", "motion"]
23650
+ },
23651
+ narration: { title: "Narra\xE7\xE3o", size: { w: 360, h: 360 }, data: { blocks: [] }, inputs: [], outputs: ["text", "blocks"], main: "blocks" },
23652
+ shotlist: {
23653
+ title: "Roteiro",
23654
+ size: { w: 780, h: 410 },
23655
+ data: { rows: [], orphanedRows: [] },
23656
+ inputs: ["blocks"],
23657
+ outputs: ["shotlist"],
23658
+ dynamic: "uma sa\xEDda shot:<ideaId> por ideia",
23659
+ main: "rows"
23660
+ },
23661
+ context: { title: "Contexto", size: { w: 320, h: 220 }, data: { kitId: null }, inputs: [], outputs: ["context"] },
23662
+ copy: {
23663
+ title: "Textos da pe\xE7a",
23664
+ size: { w: 340, h: 320 },
23665
+ data: { title: "", body: "", cta: "", legal: "" },
23666
+ inputs: [],
23667
+ outputs: ["title", "body", "cta", "legal"],
23668
+ main: "title"
23669
+ },
23670
+ media: { title: "M\xEDdia", size: { w: 320, h: 280 }, data: { items: [] }, inputs: [], outputs: ["image"], dynamic: "sa\xEDda por tipo de item (image, video, audio, file)", main: "items" },
23671
+ image: {
23672
+ title: "Gerador de imagem",
23673
+ size: { w: 320, h: 380 },
23674
+ data: { model: "google/nano-banana-2", aspect_ratio: "1:1", count: 1 },
23675
+ inputs: ["prompt"],
23676
+ outputs: ["image"],
23677
+ dynamic: "refs e negative conforme o modelo"
23678
+ },
23679
+ video: {
23680
+ title: "Gerador de v\xEDdeo",
23681
+ size: { w: 360, h: 320 },
23682
+ data: { model: "bytedance/seedance-2.0-mini", aspect_ratio: "16:9", duration: 5, generateAudio: true },
23683
+ inputs: ["prompt"],
23684
+ outputs: ["video"],
23685
+ dynamic: "frame, refs e negative conforme o modelo"
23686
+ },
23687
+ tts: {
23688
+ title: "Locu\xE7\xE3o",
23689
+ size: { w: 320, h: 212 },
23690
+ data: { model: "gemini-3.1-flash-tts-preview", voice: "Algieba", speed: "normal" },
23691
+ inputs: ["text"],
23692
+ outputs: ["audio", "approved"],
23693
+ dynamic: "style conforme o modelo"
23694
+ },
23695
+ music: {
23696
+ title: "Trilha",
23697
+ size: { w: 320, h: 212 },
23698
+ data: { model: "suno/v5-5", instrumental: true, negativeTags: ["vocals", "choir", "whistling", "marching band", "epic orchestral"] },
23699
+ inputs: ["prompt", "negative"],
23700
+ outputs: ["audio", "approved"],
23701
+ dynamic: "lyrics quando n\xE3o \xE9 instrumental"
23702
+ },
23703
+ review: {
23704
+ title: "Revisor",
23705
+ size: { w: 400, h: 420 },
23706
+ data: {
23707
+ model: "gemini-flash",
23708
+ strictness: "normal",
23709
+ criteria: "",
23710
+ checks: { inventedText: true, inventedLogo: true, productFidelity: true, composition: true, artifacts: true }
23711
+ },
23712
+ inputs: ["items", "prompt", "refs"],
23713
+ outputs: ["reviewed"],
23714
+ main: "criteria"
23715
+ },
23716
+ approval: { title: "Aprova\xE7\xE3o", size: { w: 660, h: 580 }, data: {}, inputs: ["items"], outputs: ["approved"] },
23717
+ "audio-demo": { title: "Demo de \xE1udio", size: { w: 460, h: 500 }, data: { chosenKey: null }, inputs: ["voice", "music"], outputs: ["voice", "music"] },
23718
+ compose: {
23719
+ title: "Composi\xE7\xE3o",
23720
+ size: { w: 440, h: 320 },
23721
+ data: { formatIds: [], imageType: "jpg", jpgQuality: 0.92 },
23722
+ inputs: ["background", "title", "body", "cta", "legal", "logo"],
23723
+ outputs: ["pieces"]
23724
+ },
23725
+ montage: {
23726
+ title: "Montagem",
23727
+ size: { w: 460, h: 560 },
23728
+ data: {
23729
+ order: "chegada",
23730
+ musicGainDb: -14,
23731
+ duckingDb: -8,
23732
+ fadeMs: 1500,
23733
+ title: "",
23734
+ renderFilm: true,
23735
+ filmFps: 30,
23736
+ clipAudioGainDb: -12
23737
+ },
23738
+ inputs: ["clips", "voice", "music"],
23739
+ outputs: ["film", "cut", "clips", "voice", "music"],
23740
+ main: "title"
23741
+ },
23742
+ export: {
23743
+ title: "Exporta\xE7\xE3o",
23744
+ size: { w: 380, h: 420 },
23745
+ data: { client: "", campaign: "", version: 1, imageType: "png", jpgQuality: 90, manifest: true, flat: false },
23746
+ inputs: ["items"],
23747
+ outputs: ["package"]
23748
+ },
23749
+ note: { title: "Nota", size: { w: 220, h: 220 }, data: { text: "", color: "yellow", fontSize: "auto" }, inputs: [], outputs: [], annotation: true, main: "text" },
23750
+ label: {
23751
+ title: "Texto livre",
23752
+ size: { w: 320, h: 56 },
23753
+ data: { text: "", size: "titulo", weight: "bold", color: "neutral", align: "left" },
23754
+ inputs: [],
23755
+ outputs: [],
23756
+ annotation: true,
23757
+ main: "text"
23758
+ },
23759
+ section: { title: "Se\xE7\xE3o", size: { w: 880, h: 560 }, data: { title: "Se\xE7\xE3o", color: "violet" }, inputs: [], outputs: [], annotation: true, main: "title" },
23760
+ shape: {
23761
+ title: "Forma",
23762
+ size: { w: 200, h: 140 },
23763
+ data: { shape: "rounded", fill: "blue", stroke: "blue", strokeWidth: 2, text: "" },
23764
+ inputs: [],
23765
+ outputs: [],
23766
+ annotation: true,
23767
+ main: "text"
23768
+ }
23769
+ };
23770
+ function kindList() {
23771
+ return Object.entries(KINDS).map(([k, s]) => `${k} (${s.title})`).join(", ");
23772
+ }
23773
+ function knownPort(kind, side, port) {
23774
+ const spec = kind ? KINDS[kind] : void 0;
23775
+ if (!spec) return void 0;
23776
+ const list = side === "in" ? spec.inputs : spec.outputs;
23777
+ if (list.includes(port)) return true;
23778
+ if (spec.dynamic) {
23779
+ return void 0;
23780
+ }
23781
+ return false;
23782
+ }
23783
+
23784
+ // src/canvas/core.ts
23785
+ import { randomBytes, randomUUID } from "crypto";
23786
+ var SCHEMA_VERSION = 1;
23787
+ var MAX_OP_BYTES = 64 * 1024;
23788
+ var MAX_DATA_URL_BYTES = 8 * 1024;
23789
+ var CLIENT_ID = `mcp-${randomBytes(6).toString("hex")}`;
23790
+ var NODE_KEYS = ["id", "type", "position", "size", "zIndex", "parentId", "data", "createdBy", "createdAt", "updatedAt"];
23791
+ var EDGE_KEYS = ["id", "source", "sourcePort", "target", "targetPort", "data", "createdBy", "createdAt"];
23792
+ var FORBIDDEN_SEGMENTS = ["__proto__", "constructor", "prototype"];
23793
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
23794
+ var isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
23795
+ var isStringOrNull = (v) => v === null || typeof v === "string";
23796
+ var hasOnlyKeys = (r, keys) => Object.keys(r).every((k) => keys.includes(k));
23797
+ function isJson(value) {
23798
+ if (value === null) return true;
23799
+ const kind = typeof value;
23800
+ if (kind === "boolean" || kind === "string") return true;
23801
+ if (kind === "number") return Number.isFinite(value);
23802
+ if (Array.isArray(value)) return value.every(isJson);
23803
+ if (isRecord(value)) return Object.values(value).every(isJson);
23804
+ return false;
23805
+ }
23806
+ var isJsonBag = (v) => isRecord(v) && Object.values(v).every(isJson);
23807
+ var isPoint = (v) => isRecord(v) && hasOnlyKeys(v, ["x", "y"]) && isFiniteNumber(v.x) && isFiniteNumber(v.y);
23808
+ var isSize = (v) => isRecord(v) && hasOnlyKeys(v, ["w", "h"]) && isFiniteNumber(v.w) && isFiniteNumber(v.h);
23809
+ function isValidNode(key, v) {
23810
+ return isRecord(v) && hasOnlyKeys(v, NODE_KEYS) && v.id === key && typeof v.type === "string" && isPoint(v.position) && isSize(v.size) && isFiniteNumber(v.zIndex) && isStringOrNull(v.parentId) && isJsonBag(v.data) && isStringOrNull(v.createdBy) && isFiniteNumber(v.createdAt) && isFiniteNumber(v.updatedAt);
23811
+ }
23812
+ function isValidEdge(key, v) {
23813
+ return isRecord(v) && hasOnlyKeys(v, EDGE_KEYS) && v.id === key && typeof v.source === "string" && typeof v.sourcePort === "string" && typeof v.target === "string" && typeof v.targetPort === "string" && isJsonBag(v.data) && isStringOrNull(v.createdBy) && isFiniteNumber(v.createdAt);
23814
+ }
23815
+ function checkNodePath(path) {
23816
+ const segments = path.split(".");
23817
+ if (segments.some((s) => s.length === 0)) return `caminho inv\xE1lido "${path}"`;
23818
+ if (segments.some((s) => FORBIDDEN_SEGMENTS.includes(s))) return `segmento proibido em "${path}"`;
23819
+ const [root, ...rest] = segments;
23820
+ if (root === "data" && rest.length >= 1) return null;
23821
+ if (root === "position" && rest.length === 1 && ["x", "y"].includes(rest[0]) || root === "size" && rest.length === 1 && ["w", "h"].includes(rest[0])) return null;
23822
+ return `s\xF3 d\xE1 para editar data.*, position.x/y e size.w/h (recebido "${path}")`;
23823
+ }
23824
+ function hasLargeDataUrl(value) {
23825
+ const stack = [value];
23826
+ while (stack.length) {
23827
+ const cur = stack.pop();
23828
+ if (typeof cur === "string") {
23829
+ if (cur.length > MAX_DATA_URL_BYTES && cur.startsWith("data:")) return true;
23830
+ } else if (Array.isArray(cur)) stack.push(...cur);
23831
+ else if (isRecord(cur)) stack.push(...Object.values(cur));
23832
+ }
23833
+ return false;
23834
+ }
23835
+ function checkOp(op) {
23836
+ switch (op.type) {
23837
+ case "node.add":
23838
+ if (op.node.id !== op.nodeId || !isValidNode(op.nodeId, op.node)) return `n\xF3 ${op.nodeId} fora do formato do Canvas`;
23839
+ return null;
23840
+ case "edge.add":
23841
+ if (op.edge.id !== op.edgeId || !isValidEdge(op.edgeId, op.edge)) return `liga\xE7\xE3o ${op.edgeId} fora do formato do Canvas`;
23842
+ return null;
23843
+ case "node.set": {
23844
+ const bad = checkNodePath(op.path);
23845
+ if (bad) return bad;
23846
+ if (op.value.kind === "value") {
23847
+ if (!isJson(op.value.value)) return `valor de ${op.path} n\xE3o \xE9 JSON`;
23848
+ if (!op.path.startsWith("data.") && !isFiniteNumber(op.value.value)) return `${op.path} precisa ser n\xFAmero`;
23849
+ } else if (!op.path.startsWith("data.")) return `${op.path} n\xE3o pode ser apagado`;
23850
+ return null;
23851
+ }
23852
+ case "board.set":
23853
+ if (op.field === "title" && (op.value.kind !== "value" || typeof op.value.value !== "string" || !op.value.value.trim()))
23854
+ return "t\xEDtulo vazio";
23855
+ return null;
23856
+ case "batch":
23857
+ for (const inner of op.ops) {
23858
+ const bad = checkOp(inner);
23859
+ if (bad) return bad;
23860
+ }
23861
+ return null;
23862
+ default:
23863
+ return null;
23864
+ }
23865
+ }
23866
+ function envelope(boardId3, op, actorId) {
23867
+ return { id: randomUUID(), boardId: boardId3, clientId: CLIENT_ID, actorId, seq: null, ts: Date.now(), op };
23868
+ }
23869
+ function checkEnvelope(env) {
23870
+ const bad = checkOp(env.op);
23871
+ if (bad) return bad;
23872
+ if (Buffer.byteLength(JSON.stringify(env)) > MAX_OP_BYTES)
23873
+ return "opera\xE7\xE3o maior que 64 KB: divida em partes menores";
23874
+ if (hasLargeDataUrl(env.op)) return "m\xEDdia embutida (data:) no board: suba o arquivo e use a URL";
23875
+ return null;
23876
+ }
23877
+ function emptyBoard(title, now = Date.now()) {
23878
+ return {
23879
+ id: randomUUID(),
23880
+ schemaVersion: SCHEMA_VERSION,
23881
+ title,
23882
+ ownerId: "",
23883
+ folderId: null,
23884
+ version: 0,
23885
+ nodes: {},
23886
+ edges: {},
23887
+ results: {},
23888
+ nodeHistory: {},
23889
+ createdAt: now,
23890
+ updatedAt: now
23891
+ };
23892
+ }
23893
+ function makeNode(init) {
23894
+ const now = init.now ?? Date.now();
23895
+ return {
23896
+ id: init.id ?? randomUUID(),
23897
+ type: init.type,
23898
+ position: { x: init.position.x, y: init.position.y },
23899
+ size: { w: init.size.w, h: init.size.h },
23900
+ zIndex: init.zIndex,
23901
+ parentId: null,
23902
+ data: init.data,
23903
+ createdBy: init.createdBy,
23904
+ createdAt: now,
23905
+ updatedAt: now
23906
+ };
23907
+ }
23908
+ function makeEdge(init) {
23909
+ return {
23910
+ id: randomUUID(),
23911
+ source: init.source,
23912
+ sourcePort: init.sourcePort,
23913
+ target: init.target,
23914
+ targetPort: init.targetPort,
23915
+ data: {},
23916
+ createdBy: init.createdBy,
23917
+ createdAt: init.now ?? Date.now()
23918
+ };
23919
+ }
23920
+ var setValue = (value) => ({ kind: "value", value });
23921
+ var unsetValue = () => ({ kind: "unset" });
23922
+ function removeNodeOps(doc, nodeId) {
23923
+ const ops = [];
23924
+ const edgeIds = Object.keys(doc.edges).filter((id) => doc.edges[id].source === nodeId || doc.edges[id].target === nodeId).sort();
23925
+ for (const edgeId of edgeIds) ops.push({ type: "edge.remove", edgeId });
23926
+ const history = Object.hasOwn(doc.nodeHistory, nodeId) ? doc.nodeHistory[nodeId] : void 0;
23927
+ if (history) for (const resultId of [...history].reverse()) ops.push({ type: "history.remove", nodeId, resultId });
23928
+ ops.push({ type: "node.remove", nodeId });
23929
+ return ops;
23930
+ }
23931
+ function batchOf(ops) {
23932
+ return ops.length === 1 ? ops[0] : { type: "batch", ops };
23933
+ }
23934
+
23935
+ // src/canvas/summary.ts
23936
+ var MAX_TEXT = 160;
23937
+ var MAX_BLOCKS = 40;
23938
+ var HEAVY_KEYS = /* @__PURE__ */ new Set([
23939
+ "history",
23940
+ "pinned",
23941
+ "main",
23942
+ "lastFrame",
23943
+ "sequence",
23944
+ "points",
23945
+ "extra",
23946
+ "mentions",
23947
+ "mediaMentions",
23948
+ "refs",
23949
+ "fields",
23950
+ "orphanedRows",
23951
+ "holds",
23952
+ "appearance",
23953
+ "settings",
23954
+ "enabledKeys",
23955
+ "checks",
23956
+ "versions"
23957
+ ]);
23958
+ var NAME_KEYS = /* @__PURE__ */ new Set([KIND_FIELD, "label", "nodeTitle"]);
23959
+ function oneLine(text, max = MAX_TEXT) {
23960
+ const flat = text.replace(/\s+/g, " ").trim();
23961
+ return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
23962
+ }
23963
+ function nodeKind(node) {
23964
+ const k = node.data[KIND_FIELD];
23965
+ return typeof k === "string" && k ? k : null;
23966
+ }
23967
+ function nodeName(node) {
23968
+ const { label, nodeTitle } = node.data;
23969
+ if (typeof label === "string" && label.trim()) return label.trim();
23970
+ if (typeof nodeTitle === "string" && nodeTitle.trim()) return nodeTitle.trim();
23971
+ const kind = nodeKind(node);
23972
+ return kind && KINDS[kind]?.title || kind || node.type;
23973
+ }
23974
+ function scalar(v) {
23975
+ if (typeof v === "string") {
23976
+ if (v.startsWith("data:")) return "[data-url]";
23977
+ if (/^https?:\/\//.test(v)) return "[url]";
23978
+ return JSON.stringify(oneLine(v, 60));
23979
+ }
23980
+ if (typeof v === "number" || typeof v === "boolean" || v === null) return String(v);
23981
+ return null;
23982
+ }
23983
+ function blocksLines(blocks) {
23984
+ if (!Array.isArray(blocks)) return [];
23985
+ const out = blocks.slice(0, MAX_BLOCKS).map((b) => {
23986
+ const rec = b ?? {};
23987
+ return ` \xB7 ${String(rec.id ?? "?")}: ${oneLine(String(rec.text ?? ""), 120)}`;
23988
+ });
23989
+ if (blocks.length > MAX_BLOCKS) out.push(` \xB7 \u2026 +${blocks.length - MAX_BLOCKS} blocos`);
23990
+ return out;
23991
+ }
23992
+ function rowsLines(rows) {
23993
+ if (!Array.isArray(rows)) return [];
23994
+ const out = [];
23995
+ for (const r of rows.slice(0, MAX_BLOCKS)) {
23996
+ const rec = r ?? {};
23997
+ const ideas = Array.isArray(rec.ideas) ? rec.ideas : [];
23998
+ const ideaText = ideas.slice(0, 6).map((i) => {
23999
+ const idea = i ?? {};
24000
+ return `${String(idea.id ?? "?")}="${oneLine(String(idea.text ?? ""), 60)}"`;
24001
+ }).join("; ");
24002
+ out.push(` \xB7 linha ${String(rec.id ?? "?")} (bloco ${String(rec.narrationBlockId ?? "?")}): ${ideas.length} ideia(s) ${ideaText}`);
24003
+ }
24004
+ if (rows.length > MAX_BLOCKS) out.push(` \xB7 \u2026 +${rows.length - MAX_BLOCKS} linhas`);
24005
+ return out;
24006
+ }
24007
+ function nodeLines(node, run) {
24008
+ const kind = nodeKind(node);
24009
+ const spec = kind ? KINDS[kind] : void 0;
24010
+ const head = `- ${node.id} \xB7 ${kind ?? node.type} \xB7 "${oneLine(nodeName(node), 60)}" @(${Math.round(node.position.x)},${Math.round(node.position.y)})`;
24011
+ const facts = [];
24012
+ const extra = [];
24013
+ const d = node.data;
24014
+ if (d.archived) facts.push("ARQUIVADO");
24015
+ const approval = d.approval?.state;
24016
+ if (typeof approval === "string") facts.push(`aprova\xE7\xE3o: ${approval}`);
24017
+ if (Array.isArray(d.history)) facts.push(`${d.history.length} gera\xE7\xE3o(\xF5es)`);
24018
+ if (Array.isArray(d.items)) {
24019
+ const names = d.items.slice(0, 5).map((i) => String(i?.name ?? "?"));
24020
+ facts.push(`${d.items.length} arquivo(s)${names.length ? `: ${names.join(", ")}` : ""}`);
24021
+ }
24022
+ if (Array.isArray(d.blocks)) {
24023
+ facts.push(`${d.blocks.length} bloco(s)`);
24024
+ extra.push(...blocksLines(d.blocks));
24025
+ }
24026
+ if (Array.isArray(d.rows)) {
24027
+ facts.push(`${d.rows.length} linha(s)`);
24028
+ extra.push(...rowsLines(d.rows));
24029
+ }
24030
+ const main2 = spec?.main;
24031
+ if (main2 && typeof d[main2] === "string" && d[main2]) facts.push(`${main2}: "${oneLine(d[main2])}"`);
24032
+ for (const [key, value] of Object.entries(d)) {
24033
+ if (NAME_KEYS.has(key) || HEAVY_KEYS.has(key) || key === main2) continue;
24034
+ if (["blocks", "rows", "items", "archived", "approval"].includes(key)) continue;
24035
+ const s = scalar(value);
24036
+ if (s !== null) facts.push(`${key}=${s}`);
24037
+ else if (Array.isArray(value)) facts.push(`${key}=[${value.length}]`);
24038
+ else if (value && typeof value === "object") facts.push(`${key}={\u2026}`);
24039
+ if (facts.length > 12) break;
24040
+ }
24041
+ if (run?.claimedBy) facts.push("rodando agora");
24042
+ return [facts.length ? `${head} \xB7 ${facts.join(" \xB7 ")}` : head, ...extra];
24043
+ }
24044
+ function boardSummary(snap) {
24045
+ const doc = snap.doc;
24046
+ const nodes = Object.values(doc.nodes).sort((a, b) => a.position.x - b.position.x || a.position.y - b.position.y);
24047
+ const edges = Object.values(doc.edges);
24048
+ const runs = new Map(snap.runs.map((r) => [r.nodeId, r]));
24049
+ const lines = [
24050
+ `Board "${snap.title}" \xB7 id ${snap.id} \xB7 seq ${snap.seq} \xB7 seu papel: ${snap.role} \xB7 ${nodes.length} n\xF3(s) \xB7 ${edges.length} liga\xE7\xE3o(\xF5es)`
24051
+ ];
24052
+ const ialab = doc.meta?.ialab;
24053
+ if (ialab === null) lines.push("IA Lab: sem v\xEDnculo (gera\xE7\xF5es saem dos diamantes de quem roda).");
24054
+ else if (ialab && typeof ialab === "object") {
24055
+ const label = ["code", "name", "projectCode", "projectName"].map((k) => ialab[k]).find((v) => typeof v === "string");
24056
+ lines.push(`IA Lab: vinculado${label ? ` a ${String(label)}` : ""}.`);
24057
+ }
24058
+ if (doc.meta?.guide) lines.push(`Guia: ${oneLine(doc.meta.guide)}`);
24059
+ lines.push("", "N\xF3s:");
24060
+ if (!nodes.length) lines.push("(vazio)");
24061
+ for (const n of nodes) lines.push(...nodeLines(n, runs.get(n.id)));
24062
+ lines.push("", "Liga\xE7\xF5es:");
24063
+ if (!edges.length) lines.push("(nenhuma)");
24064
+ for (const e of edges) lines.push(`- ${e.id}: ${e.source}.${e.sourcePort} \u2192 ${e.target}.${e.targetPort}`);
24065
+ return lines.join("\n");
24066
+ }
24067
+ function nodeDetail(node) {
24068
+ const strip = (v, depth) => {
24069
+ if (typeof v === "string") {
24070
+ if (v.startsWith("data:")) return `[data-url ${v.length}b]`;
24071
+ return v.length > 2e3 ? `${v.slice(0, 2e3)}\u2026 [+${v.length - 2e3}]` : v;
24072
+ }
24073
+ if (Array.isArray(v)) {
24074
+ if (depth > 4) return `[${v.length} itens]`;
24075
+ const head = v.slice(0, 60).map((x) => strip(x, depth + 1));
24076
+ return v.length > 60 ? [...head, `\u2026 +${v.length - 60}`] : head;
24077
+ }
24078
+ if (v && typeof v === "object") {
24079
+ if (depth > 4) return "{\u2026}";
24080
+ const out = {};
24081
+ for (const [k, x] of Object.entries(v)) {
24082
+ out[k] = k === "points" || k === "sequence" ? "[omitido]" : strip(x, depth + 1);
24083
+ }
24084
+ return out;
24085
+ }
24086
+ return v;
24087
+ };
24088
+ return `${node.id} \xB7 ${nodeKind(node) ?? node.type} \xB7 "${nodeName(node)}"
24089
+ ${JSON.stringify(strip(node.data, 0))}`;
24090
+ }
24091
+ function changesSince(envelopes, doc) {
24092
+ const count = /* @__PURE__ */ new Map();
24093
+ for (const e of envelopes) {
24094
+ const key = e.op.type;
24095
+ count.set(key, (count.get(key) ?? 0) + 1);
24096
+ }
24097
+ const parts = [...count].map(([t, n]) => `${n}\xD7 ${t}`);
24098
+ const names = [...touchedNodes(envelopes)].map((id) => doc.nodes[id] ? `"${oneLine(nodeName(doc.nodes[id]), 40)}"` : `${id} (apagado)`).slice(0, 8);
24099
+ return `${envelopes.length} opera\xE7\xE3o(\xF5es) de outras pessoas desde a sua leitura (${parts.join(", ")})` + (names.length ? `; n\xF3s tocados: ${names.join(", ")}` : "");
24100
+ }
24101
+ function touchedNodes(envelopes) {
24102
+ const out = /* @__PURE__ */ new Set();
24103
+ const visit = (op) => {
24104
+ if (op.type === "batch" && Array.isArray(op.ops)) {
24105
+ for (const inner of op.ops) visit(inner);
24106
+ return;
24107
+ }
24108
+ if (typeof op.nodeId === "string") out.add(op.nodeId);
24109
+ const edge = op.edge;
24110
+ if (edge && typeof edge.source === "string") out.add(edge.source);
24111
+ if (edge && typeof edge.target === "string") out.add(edge.target);
24112
+ };
24113
+ for (const e of envelopes) visit(e.op);
24114
+ return out;
24115
+ }
24116
+
24117
+ // src/canvas/comments.ts
24118
+ function mmss(t) {
24119
+ const total = Math.max(0, Math.floor(t));
24120
+ const h = Math.floor(total / 3600);
24121
+ const m = Math.floor(total % 3600 / 60);
24122
+ const s = total % 60;
24123
+ const pad = (n) => String(n).padStart(2, "0");
24124
+ return h ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
24125
+ }
24126
+ function nodeRef(doc, nodeId) {
24127
+ const node = doc.nodes[nodeId];
24128
+ const kind = nodeKind(node) ?? node.type;
24129
+ return `"${oneLine(nodeName(node), 60)}" (${kind}, ${node.id})`;
24130
+ }
24131
+ function hashString(text) {
24132
+ let h1 = 3735928559;
24133
+ let h2 = 1103547991;
24134
+ for (let i = 0; i < text.length; i += 1) {
24135
+ const ch = text.charCodeAt(i);
24136
+ h1 = Math.imul(h1 ^ ch, 2654435761);
24137
+ h2 = Math.imul(h2 ^ ch, 1597334677);
24138
+ }
24139
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
24140
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
24141
+ const hex = (n) => (n >>> 0).toString(16).padStart(8, "0");
24142
+ return hex(h2) + hex(h1);
24143
+ }
24144
+ function outputKeyOf(entry) {
24145
+ for (const field of [entry.resultId, entry.id, entry.generationId]) {
24146
+ if (typeof field === "string" && field !== "" && field.length <= 128) return field;
24147
+ }
24148
+ if (typeof entry.url === "string" && entry.url !== "") return `url-${hashString(entry.url)}`;
24149
+ return null;
24150
+ }
24151
+ function versionOf(doc, nodeId, outputKey) {
24152
+ const ids = doc.nodeHistory?.[nodeId];
24153
+ if (Array.isArray(ids)) {
24154
+ const i = ids.indexOf(outputKey);
24155
+ if (i >= 0) return i + 1;
24156
+ }
24157
+ const history = doc.nodes[nodeId]?.data.history;
24158
+ if (Array.isArray(history)) {
24159
+ const i = history.findIndex(
24160
+ (h) => typeof h === "object" && h !== null && !Array.isArray(h) && outputKeyOf(h) === outputKey
24161
+ );
24162
+ if (i >= 0) return history.length - i;
24163
+ }
24164
+ return null;
24165
+ }
24166
+ function describeAnchor(anchor, doc) {
24167
+ if (!anchor || anchor.kind === "point") return { text: "no board" };
24168
+ const exists = !!doc.nodes[anchor.nodeId];
24169
+ if (!exists) {
24170
+ return { text: `no n\xF3 ${anchor.nodeId}`, gone: "o n\xF3 foi apagado" };
24171
+ }
24172
+ const ref = nodeRef(doc, anchor.nodeId);
24173
+ switch (anchor.kind) {
24174
+ case "node":
24175
+ return { text: `no n\xF3 ${ref}`, nodeId: anchor.nodeId };
24176
+ case "output": {
24177
+ const v = versionOf(doc, anchor.nodeId, anchor.outputKey);
24178
+ return v === null ? { text: `numa vers\xE3o de ${ref} (${anchor.outputKey})`, nodeId: anchor.nodeId, gone: "essa vers\xE3o n\xE3o est\xE1 mais no hist\xF3rico do n\xF3" } : { text: `na vers\xE3o ${v} de ${ref}`, nodeId: anchor.nodeId };
24179
+ }
24180
+ case "timecode": {
24181
+ const node = doc.nodes[anchor.nodeId];
24182
+ const kind = nodeKind(node);
24183
+ const t = mmss(Number(anchor.t) || 0);
24184
+ if (kind === "montage") {
24185
+ const name = nodeName(node);
24186
+ const title = name === KINDS.montage.title ? "" : ` "${oneLine(name, 60)}"`;
24187
+ return { text: `em ${t} da Montagem${title} (${node.id})`, nodeId: anchor.nodeId };
24188
+ }
24189
+ return { text: `em ${t} de ${ref}`, nodeId: anchor.nodeId };
24190
+ }
24191
+ default:
24192
+ return { text: "no board" };
24193
+ }
24194
+ }
24195
+ function who(c) {
24196
+ return c.author?.name?.trim() || "algu\xE9m";
24197
+ }
24198
+ function reactionsText(r) {
24199
+ if (Array.isArray(r)) {
24200
+ const parts = r.map((x) => {
24201
+ const rec = x ?? {};
24202
+ const emoji2 = typeof rec.emoji === "string" ? rec.emoji : null;
24203
+ const n = typeof rec.count === "number" ? rec.count : Array.isArray(rec.userIds) ? rec.userIds.length : Array.isArray(rec.users) ? rec.users.length : 1;
24204
+ return emoji2 ? `${emoji2}${n > 1 ? `\xD7${n}` : ""}` : null;
24205
+ }).filter(Boolean);
24206
+ return parts.join(" ");
24207
+ }
24208
+ if (r && typeof r === "object") {
24209
+ return Object.entries(r).map(([emoji2, v]) => {
24210
+ const n = Array.isArray(v) ? v.length : typeof v === "number" ? v : 1;
24211
+ return `${emoji2}${n > 1 ? `\xD7${n}` : ""}`;
24212
+ }).join(" ");
24213
+ }
24214
+ return "";
24215
+ }
24216
+ var MAX_REPLIES = 5;
24217
+ function threadLines(t, doc) {
24218
+ const where = describeAnchor(t.anchor, doc);
24219
+ const state = t.resolvedAt ? " \xB7 RESOLVIDO" : "";
24220
+ const reacts = reactionsText(t.reactions);
24221
+ const lines = t.deleted ? [`- ${t.id} \xB7 ${where.text}${state}`, " (coment\xE1rio raiz apagado: n\xE3o d\xE1 para responder nem resolver)"] : [`- ${t.id} \xB7 ${where.text} \xB7 ${who(t)}${state}${reacts ? ` \xB7 ${reacts}` : ""}`, ` pedido: ${oneLine(t.body ?? "", 400)}`];
24222
+ if (where.gone && !t.deleted) lines.push(` \u26A0 ALVO SUMIU: ${where.gone}. Responda com pergunta, n\xE3o resolva.`);
24223
+ const replies = Array.isArray(t.replies) ? t.replies : [];
24224
+ for (const r of replies.slice(-MAX_REPLIES)) {
24225
+ lines.push(r.deleted ? " \u21B3 (resposta apagada)" : ` \u21B3 ${who(r)}: ${oneLine(r.body ?? "", 140)}`);
24226
+ }
24227
+ if (replies.length > MAX_REPLIES) lines.splice(lines.length - MAX_REPLIES, 0, ` \u21B3 \u2026 ${replies.length - MAX_REPLIES} resposta(s) antes`);
24228
+ return lines;
24229
+ }
24230
+ function commentsSummary(threads, doc, status) {
24231
+ const label = status === "resolved" ? "resolvido(s)" : status === "all" ? "no total" : "aberto(s)";
24232
+ if (!threads.length) return `Nenhum coment\xE1rio ${label === "no total" ? "no board" : label}.`;
24233
+ const out = [`${threads.length} coment\xE1rio(s) ${label}:`];
24234
+ for (const t of threads) out.push(...threadLines(t, doc));
24235
+ return out.join("\n");
24236
+ }
24237
+
24238
+ // src/tools/canvas-comments.ts
24239
+ var boardId = external_exports.string().uuid().describe("Id (uuid) do board.");
24240
+ var commentId = external_exports.string().min(1).max(128).describe("Id do coment\xE1rio raiz, como sai em canvas_list_comments.");
24241
+ var body = external_exports.string().trim().min(1).max(4e3);
24242
+ var mentions = external_exports.array(external_exports.string().uuid()).max(50).optional().describe("userIds a marcar (opcional; quem n\xE3o \xE9 membro \xE9 ignorado).");
24243
+ function registerCanvasCommentTools(server, api, run) {
24244
+ async function onComment(fn) {
24245
+ try {
24246
+ return await fn();
24247
+ } catch (e) {
24248
+ if (!(e instanceof ApiError)) throw e;
24249
+ if (e.httpStatus === 404) {
24250
+ throw new CanvasToolError("Coment\xE1rio (ou board) n\xE3o encontrado ou apagado. Liste de novo com canvas_list_comments.");
24251
+ }
24252
+ if (e.httpStatus === 400) throw new CanvasToolError(`O servidor recusou o coment\xE1rio: ${e.message}.`);
24253
+ if (e.httpStatus === 403 && (!api.usesPat || /ialab:/.test(e.message))) {
24254
+ throw new CanvasToolError(
24255
+ "O login pelo navegador deste MCP n\xE3o tem a permiss\xE3o ialab:spend, que toda escrita de coment\xE1rio exige. Rode `beplus-mcp login` de novo e autorize a permiss\xE3o de gastar/editar."
24256
+ );
24257
+ }
24258
+ if (e.httpStatus === 403) throw new CanvasToolError(`Sem permiss\xE3o: ${e.message}.`);
24259
+ if (e.httpStatus === 429) {
24260
+ throw new CanvasToolError(
24261
+ "Limite de escrita em coment\xE1rios (60 por minuto por pessoa) mesmo depois de esperar. Pare o lote, diga \xE0 pessoa quais j\xE1 foram fechados e siga daqui a 1 minuto a partir do pr\xF3ximo."
24262
+ );
24263
+ }
24264
+ throw e;
24265
+ }
24266
+ }
24267
+ const newReplyId = (thread) => {
24268
+ const last = thread?.replies?.at(-1);
24269
+ return last?.id ?? "?";
24270
+ };
24271
+ server.registerTool(
24272
+ "canvas_list_comments",
24273
+ {
24274
+ title: "Canvas: listar coment\xE1rios",
24275
+ description: "Lista os coment\xE1rios de um board (padr\xE3o: abertos): id, onde est\xE1 (n\xF3, vers\xE3o, minutagem da Montagem ou board), autor, pedido e respostas. Avisa quando o alvo sumiu. S\xF3 leitura.",
24276
+ inputSchema: {
24277
+ board_id: boardId,
24278
+ status: external_exports.enum(["open", "resolved", "all"]).optional().describe("open (padr\xE3o), resolved ou all.")
24279
+ },
24280
+ annotations: { readOnlyHint: true, openWorldHint: false }
24281
+ },
24282
+ async ({ board_id, status }) => run(async () => {
24283
+ const st = status ?? "open";
24284
+ const [snap, threads] = await Promise.all([api.snapshot(board_id), api.listComments(board_id, st)]);
24285
+ const text = commentsSummary(threads, snap.doc, st);
24286
+ const open2 = threads.some((t) => !t.resolvedAt);
24287
+ return open2 ? `${text}
24288
+
24289
+ Para aplicar: leia o n\xF3 com canvas_read_board (node_ids), edite pelas canvas_* e feche com canvas_resolve_comment dizendo o que fez.` : text;
24290
+ })
24291
+ );
24292
+ server.registerTool(
24293
+ "canvas_reply_comment",
24294
+ {
24295
+ title: "Canvas: responder coment\xE1rio",
24296
+ description: "Responde numa thread de coment\xE1rio sem resolver. Use para perguntar quando o pedido \xE9 amb\xEDguo ou o alvo sumiu.",
24297
+ inputSchema: { board_id: boardId, comment_id: commentId, body, mentions },
24298
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
24299
+ },
24300
+ async ({ board_id, comment_id, body: text, mentions: m }) => run(async () => {
24301
+ const thread = await onComment(
24302
+ () => api.createComment(board_id, { body: text, parentId: comment_id, ...m?.length ? { mentions: m } : {} })
24303
+ );
24304
+ const open2 = thread?.resolvedAt ? "A thread est\xE1 resolvida; reabra com canvas_reopen_comment se precisar." : "A thread segue aberta.";
24305
+ return `\u2705 Resposta ${newReplyId(thread)} em ${comment_id}: "${oneLine(text, 120)}". ${open2}`;
24306
+ })
24307
+ );
24308
+ server.registerTool(
24309
+ "canvas_resolve_comment",
24310
+ {
24311
+ title: "Canvas: resolver coment\xE1rio",
24312
+ description: "Fecha um coment\xE1rio depois de aplicar o ajuste. done \xE9 obrigat\xF3rio: uma frase curta dizendo o que foi feito (vira resposta na thread antes de resolver). N\xE3o resolva pedido amb\xEDguo nem com alvo sumido: responda com pergunta.",
24313
+ inputSchema: {
24314
+ board_id: boardId,
24315
+ comment_id: commentId,
24316
+ done: external_exports.string().trim().min(3).max(500).describe('O que foi feito, ex.: "troquei a fala do b2 e encurtei o b3".')
24317
+ },
24318
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
24319
+ },
24320
+ async ({ board_id, comment_id, done }) => run(async () => {
24321
+ const thread = await onComment(() => api.createComment(board_id, { body: done, parentId: comment_id }));
24322
+ const replyId = newReplyId(thread);
24323
+ try {
24324
+ await onComment(() => api.resolveComment(board_id, comment_id));
24325
+ } catch (e) {
24326
+ const why = e instanceof Error ? e.message : String(e);
24327
+ throw new CanvasToolError(
24328
+ `A resposta "${oneLine(done, 80)}" (${replyId}) foi postada em ${comment_id}, mas resolver falhou: ${why} N\xE3o repita canvas_resolve_comment (postaria a resposta de novo): avise a pessoa para resolver no Canvas.`
24329
+ );
24330
+ }
24331
+ return `\u2705 Resolvido ${comment_id}: "${oneLine(done, 120)}" (resposta ${replyId}).`;
24332
+ })
24333
+ );
24334
+ server.registerTool(
24335
+ "canvas_reopen_comment",
24336
+ {
24337
+ title: "Canvas: reabrir coment\xE1rio",
24338
+ description: "Reabre um coment\xE1rio resolvido. why (opcional) vira resposta na thread explicando o motivo.",
24339
+ inputSchema: { board_id: boardId, comment_id: commentId, why: external_exports.string().trim().min(1).max(1e3).optional() },
24340
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
24341
+ },
24342
+ async ({ board_id, comment_id, why }) => run(async () => {
24343
+ await onComment(() => api.reopenComment(board_id, comment_id));
24344
+ if (why) await onComment(() => api.createComment(board_id, { body: why, parentId: comment_id }));
24345
+ return `\u2705 Reaberto ${comment_id}${why ? ` com a resposta "${oneLine(why, 120)}"` : ""}.`;
24346
+ })
24347
+ );
24348
+ server.registerTool(
24349
+ "canvas_comment",
24350
+ {
24351
+ title: "Canvas: comentar",
24352
+ description: "Cria um coment\xE1rio novo (raiz), ancorado para aparecer no lugar certo do Canvas. Use para deixar uma pergunta ou observa\xE7\xE3o para a pessoa. \xC2ncora: node_id (no n\xF3); node_id + output_key (numa vers\xE3o gerada); node_id + t em segundos (minutagem da Montagem ou do v\xEDdeo); x,y (ponto do board); nada = acima do conte\xFAdo do board.",
24353
+ inputSchema: {
24354
+ board_id: boardId,
24355
+ body,
24356
+ node_id: external_exports.string().min(1).max(64).optional(),
24357
+ output_key: external_exports.string().min(1).max(128).optional().describe("Id da vers\xE3o (resultado) do n\xF3."),
24358
+ t: external_exports.number().finite().min(0).optional().describe("Segundos desde o in\xEDcio."),
24359
+ x: external_exports.number().finite().optional(),
24360
+ y: external_exports.number().finite().optional(),
24361
+ mentions
24362
+ },
24363
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
24364
+ },
24365
+ async (args) => run(async () => {
24366
+ if (!args.node_id && (args.output_key !== void 0 || args.t !== void 0)) {
24367
+ throw new CanvasToolError("output_key e t precisam de node_id.");
24368
+ }
24369
+ if (args.output_key !== void 0 && args.t !== void 0) throw new CanvasToolError("Use output_key ou t, n\xE3o os dois.");
24370
+ if (args.x === void 0 !== (args.y === void 0)) throw new CanvasToolError("Informe x e y juntos.");
24371
+ const snap = await api.snapshot(args.board_id);
24372
+ let anchor;
24373
+ if (args.node_id) {
24374
+ if (!snap.doc.nodes[args.node_id]) {
24375
+ throw new CanvasToolError(`N\xF3 ${args.node_id} n\xE3o existe no board. Releia com canvas_read_board.`);
24376
+ }
24377
+ if (args.output_key !== void 0) anchor = { kind: "output", nodeId: args.node_id, outputKey: args.output_key };
24378
+ else if (args.t !== void 0) anchor = { kind: "timecode", nodeId: args.node_id, t: args.t };
24379
+ else anchor = { kind: "node", nodeId: args.node_id, dx: 0, dy: 0 };
24380
+ } else if (args.x !== void 0 && args.y !== void 0) {
24381
+ anchor = { kind: "point", x: args.x, y: args.y };
24382
+ } else {
24383
+ const nodes = Object.values(snap.doc.nodes);
24384
+ anchor = nodes.length ? { kind: "point", x: Math.min(...nodes.map((n) => n.position.x)), y: Math.min(...nodes.map((n) => n.position.y)) - 80 } : { kind: "point", x: 0, y: 0 };
24385
+ }
24386
+ const created = await onComment(
24387
+ () => api.createComment(args.board_id, {
24388
+ body: args.body,
24389
+ anchor,
24390
+ ...args.mentions?.length ? { mentions: args.mentions } : {}
24391
+ })
24392
+ );
24393
+ const where = describeAnchor(anchor, snap.doc).text;
24394
+ const at = anchor.kind === "point" && args.x !== void 0 ? ` em (${args.x}, ${args.y})` : "";
24395
+ return `\u2705 Coment\xE1rio ${created?.id ?? ""} criado ${where}${at}.`.replace(" ", " ");
24396
+ })
24397
+ );
24398
+ }
24399
+
24400
+ // src/tools/canvas.ts
24401
+ var REJECT_TEXT = {
24402
+ "node-missing": "o n\xF3 n\xE3o existe mais (algu\xE9m apagou)",
24403
+ "edge-endpoint-missing": "uma ponta da liga\xE7\xE3o n\xE3o existe mais",
24404
+ "invalid-op": "opera\xE7\xE3o inv\xE1lida ou repetida",
24405
+ "too-large": "opera\xE7\xE3o grande demais (64 KB) ou com m\xEDdia embutida"
24406
+ };
24407
+ var SECTION_Z = -1e3;
24408
+ var GAP = 80;
24409
+ var boardId2 = external_exports.string().uuid().describe("Id (uuid) do board, como sai em canvas_list_boards.");
24410
+ var nodeIdArg = external_exports.string().min(1).max(128);
24411
+ var point = external_exports.object({ x: external_exports.number().finite(), y: external_exports.number().finite() });
24412
+ function registerCanvasTools(server, client, cfg) {
24413
+ const api = new CanvasApi(client, cfg);
24414
+ const seen = /* @__PURE__ */ new Map();
24415
+ function fail(e) {
24416
+ if (e instanceof CanvasToolError || e instanceof CoreMismatchError) return errorResult(e.message);
24417
+ if (e instanceof ApiError && e.httpStatus === 401 && !api.usesPat) {
24418
+ return errorResult(
24419
+ "O acesso pelo navegador expirou ou foi revogado. Rode `beplus-mcp login` de novo e autorize."
24420
+ );
24421
+ }
24422
+ if (e instanceof ApiError && e.httpStatus === 404) {
24423
+ return errorResult("Board n\xE3o encontrado, ou voc\xEA n\xE3o \xE9 membro dele. Confira o id em canvas_list_boards.");
24424
+ }
24425
+ return errorResult(apiErrorToUserMessage(e));
24426
+ }
24427
+ async function run(fn) {
24428
+ try {
24429
+ return textResult(await fn());
24430
+ } catch (e) {
24431
+ return fail(e);
24432
+ }
24433
+ }
24434
+ async function edit(id, build) {
24435
+ const last = seen.get(id);
24436
+ if (last === void 0) {
24437
+ throw new CanvasToolError("Leia o board com canvas_read_board antes de editar (a leitura vale para a sess\xE3o).");
24438
+ }
24439
+ const snap = await api.snapshot(id);
24440
+ if (snap.role === "viewer") throw new CanvasToolError("Voc\xEA \xE9 s\xF3 leitor neste board: pe\xE7a ao dono para te dar papel de editor.");
24441
+ const doc = snap.doc;
24442
+ const { ops, done, targets } = await build(doc, snap);
24443
+ if (!ops.length) return `Nada a mudar. ${done}`;
24444
+ let note = "";
24445
+ if (snap.seq > last) {
24446
+ const since = await api.opsAfter(id, last);
24447
+ const theirs = since?.filter((e) => e.clientId !== CLIENT_ID) ?? null;
24448
+ if (theirs === null) {
24449
+ seen.delete(id);
24450
+ throw new CanvasToolError(
24451
+ `O board mudou muito desde a sua leitura (seq ${last} \u2192 ${snap.seq}). Releia com canvas_read_board antes de editar.`
24452
+ );
24453
+ }
24454
+ const clash = [...touchedNodes(theirs)].filter((n) => targets.includes(n));
24455
+ if (clash.length) {
24456
+ seen.delete(id);
24457
+ const names = clash.map((n) => doc.nodes[n] ? `"${oneLine(nodeName(doc.nodes[n]), 40)}"` : n).join(", ");
24458
+ throw new CanvasToolError(
24459
+ `Outra pessoa mexeu em ${names} depois da sua leitura (seq ${last} \u2192 ${snap.seq}). Nada foi aplicado: releia com canvas_read_board e refa\xE7a a edi\xE7\xE3o em cima do estado atual.`
24460
+ );
24461
+ }
24462
+ if (theirs.length) note = `
24463
+ Aviso: ${changesSince(theirs, doc)}.`;
24464
+ }
24465
+ const env = envelope(id, batchOf(ops), await api.me());
24466
+ const bad = checkEnvelope(env);
24467
+ if (bad) throw new CanvasToolError(`Edi\xE7\xE3o recusada antes de enviar: ${bad}.`);
24468
+ const res = await api.commit(id, [env]);
24469
+ const outcome = res.outcomes.find((o) => o.opId === env.id);
24470
+ if (!outcome || "rejected" in outcome) {
24471
+ seen.delete(id);
24472
+ const why = outcome && "rejected" in outcome ? REJECT_TEXT[outcome.rejected] ?? outcome.rejected : "sem resposta do servidor";
24473
+ throw new CanvasToolError(`O servidor recusou a edi\xE7\xE3o: ${why}. Nada foi aplicado. Releia com canvas_read_board.`);
24474
+ }
24475
+ seen.set(id, res.seq);
24476
+ return `\u2705 ${done} (seq ${res.seq}).${note}`;
24477
+ }
24478
+ function needNode(doc, nodeId) {
24479
+ const node = doc.nodes[nodeId];
24480
+ if (!node) throw new CanvasToolError(`N\xF3 ${nodeId} n\xE3o existe no board. Releia com canvas_read_board.`);
24481
+ return node;
24482
+ }
24483
+ function portWarning(doc, source, sourcePort, target, targetPort) {
24484
+ const warn = [];
24485
+ const s = knownPort(nodeKind(doc.nodes[source]), "out", sourcePort);
24486
+ const t = knownPort(nodeKind(doc.nodes[target]), "in", targetPort);
24487
+ if (s === false) warn.push(`sa\xEDda "${sourcePort}" n\xE3o \xE9 porta conhecida de ${nodeKind(doc.nodes[source])}`);
24488
+ if (t === false) warn.push(`entrada "${targetPort}" n\xE3o \xE9 porta conhecida de ${nodeKind(doc.nodes[target])}`);
24489
+ return warn.length ? ` Aten\xE7\xE3o: ${warn.join("; ")}.` : "";
24490
+ }
24491
+ function edgeOps(doc, links, createdBy, pending) {
24492
+ const ops = [];
24493
+ let warn = "";
24494
+ for (const l of links) {
24495
+ for (const end of [l.source, l.target]) {
24496
+ if (!doc.nodes[end] && !pending.has(end)) throw new CanvasToolError(`N\xF3 ${end} n\xE3o existe no board. Releia com canvas_read_board.`);
24497
+ }
24498
+ const dup = Object.values(doc.edges).some(
24499
+ (e) => e.source === l.source && e.sourcePort === l.sourcePort && e.target === l.target && e.targetPort === l.targetPort
24500
+ );
24501
+ if (dup) continue;
24502
+ if (doc.nodes[l.source] && doc.nodes[l.target]) warn += portWarning(doc, l.source, l.sourcePort, l.target, l.targetPort);
24503
+ const edge = makeEdge({ ...l, createdBy });
24504
+ ops.push({ type: "edge.add", edgeId: edge.id, edge });
24505
+ }
24506
+ return { ops, warn };
24507
+ }
24508
+ function placeFor(doc, size, near, side = "right") {
24509
+ if (near) {
24510
+ const x = side === "right" ? near.position.x + near.size.w + GAP : near.position.x - size.w - GAP;
24511
+ let y = near.position.y;
24512
+ const nodes2 = Object.values(doc.nodes);
24513
+ const hits = (yy) => nodes2.some((n) => x < n.position.x + n.size.w && x + size.w > n.position.x && yy < n.position.y + n.size.h && yy + size.h > n.position.y);
24514
+ for (let i = 0; i < 50 && hits(y); i++) y += GAP;
24515
+ return { x, y };
24516
+ }
24517
+ const nodes = Object.values(doc.nodes).filter((n) => nodeKind(n) !== "section");
24518
+ if (!nodes.length) return { x: 0, y: 0 };
24519
+ return {
24520
+ x: Math.max(...nodes.map((n) => n.position.x + n.size.w)) + GAP,
24521
+ y: Math.min(...nodes.map((n) => n.position.y))
24522
+ };
24523
+ }
24524
+ function parseData(raw) {
24525
+ const out = {};
24526
+ for (const [k, v] of Object.entries(raw ?? {})) {
24527
+ if (k === KIND_FIELD) throw new CanvasToolError("data.kind n\xE3o muda: apague o n\xF3 e crie outro do tipo certo.");
24528
+ if (!isJson(v)) throw new CanvasToolError(`data.${k} n\xE3o \xE9 JSON v\xE1lido.`);
24529
+ out[k] = v;
24530
+ }
24531
+ return out;
24532
+ }
24533
+ server.registerTool(
24534
+ "canvas_list_boards",
24535
+ {
24536
+ title: "Canvas: listar boards",
24537
+ description: "Lista os boards do Canvas online de que voc\xEA \xE9 membro (id, t\xEDtulo, seu papel, seq, \xFAltima mudan\xE7a). S\xF3 leitura.",
24538
+ inputSchema: { kind: external_exports.enum(["board", "template"]).optional().describe("board (padr\xE3o) ou template (moldes).") },
24539
+ annotations: { readOnlyHint: true, openWorldHint: false }
24540
+ },
24541
+ async ({ kind }) => run(async () => {
24542
+ const boards = await api.listBoards(kind ?? "board");
24543
+ if (!boards.length) return "Nenhum board. Crie um com canvas_create_board.";
24544
+ return boards.map((b) => `- ${b.id} \xB7 "${b.title}" \xB7 ${b.role} \xB7 seq ${b.seq} \xB7 ${String(b.updatedAt).slice(0, 16).replace("T", " ")}`).join("\n");
24545
+ })
24546
+ );
24547
+ server.registerTool(
24548
+ "canvas_read_board",
24549
+ {
24550
+ title: "Canvas: ler board",
24551
+ description: "L\xEA um board: n\xF3s (id, tipo, nome, campos principais), liga\xE7\xF5es e v\xEDnculo com o IA Lab, sem m\xEDdia. Obrigat\xF3rio antes de editar (vale para a sess\xE3o). node_ids devolve o data completo desses n\xF3s.",
24552
+ inputSchema: {
24553
+ board_id: boardId2,
24554
+ node_ids: external_exports.array(nodeIdArg).max(20).optional().describe("N\xF3s para ver o data inteiro (sem blobs).")
24555
+ },
24556
+ annotations: { readOnlyHint: true, openWorldHint: false }
24557
+ },
24558
+ async ({ board_id, node_ids }) => run(async () => {
24559
+ const snap = await api.snapshot(board_id);
24560
+ seen.set(board_id, snap.seq);
24561
+ const parts = [boardSummary(snap)];
24562
+ if (node_ids?.length) {
24563
+ parts.push("", "Detalhe:");
24564
+ for (const n of node_ids) parts.push(snap.doc.nodes[n] ? nodeDetail(snap.doc.nodes[n]) : `${n}: n\xE3o existe`);
24565
+ }
24566
+ return parts.join("\n");
24567
+ })
24568
+ );
24569
+ server.registerTool(
24570
+ "canvas_create_board",
24571
+ {
24572
+ title: "Canvas: criar board",
24573
+ description: 'Cria um board online (voc\xEA vira dono). template "narration" j\xE1 cria Contexto, Narra\xE7\xE3o, Roteiro e Locu\xE7\xE3o ligados (Narra\xE7\xE3o.blocks \u2192 Roteiro.blocks, Narra\xE7\xE3o.text \u2192 Locu\xE7\xE3o.text). O kit do Contexto a pessoa escolhe no Canvas. N\xE3o gera m\xEDdia.',
24574
+ inputSchema: {
24575
+ title: external_exports.string().trim().min(1).max(200),
24576
+ template: external_exports.enum(["blank", "narration"]).optional().describe("blank (padr\xE3o) ou narration.")
24577
+ },
24578
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
24579
+ },
24580
+ async ({ title, template }) => run(async () => {
24581
+ const doc = emptyBoard(title);
24582
+ const ids = {};
24583
+ if (template === "narration") {
24584
+ const me = await api.me();
24585
+ let zIndex = 1;
24586
+ const add = (kind, x, y) => {
24587
+ const spec = KINDS[kind];
24588
+ const node = makeNode({
24589
+ type: WORKFLOW_NODE_TYPE,
24590
+ data: { ...spec.data, [KIND_FIELD]: kind },
24591
+ position: { x, y },
24592
+ size: spec.size,
24593
+ zIndex: zIndex++,
24594
+ createdBy: me
24595
+ });
24596
+ doc.nodes[node.id] = node;
24597
+ ids[kind] = node.id;
24598
+ return node.id;
24599
+ };
24600
+ add("context", 200, 200);
24601
+ const narration = add("narration", 650, 200);
24602
+ const shotlist = add("shotlist", 1150, 200);
24603
+ const tts = add("tts", 650, 750);
24604
+ for (const l of [
24605
+ { source: narration, sourcePort: "blocks", target: shotlist, targetPort: "blocks" },
24606
+ { source: narration, sourcePort: "text", target: tts, targetPort: "text" }
24607
+ ]) {
24608
+ const edge = makeEdge({ ...l, createdBy: me });
24609
+ doc.edges[edge.id] = edge;
24610
+ }
24611
+ }
24612
+ const created = await api.createBoard({ title, kind: "board", doc });
24613
+ seen.set(created.id, created.seq);
24614
+ const idLine = Object.keys(ids).length ? `
24615
+ N\xF3s: ${Object.entries(ids).map(([k, v]) => `${k}=${v}`).join(" \xB7 ")}` : "";
24616
+ return `\u2705 Board criado: "${created.title}" \xB7 id ${created.id} \xB7 seq ${created.seq}.${idLine}`;
24617
+ })
24618
+ );
24619
+ server.registerTool(
24620
+ "canvas_add_node",
24621
+ {
24622
+ title: "Canvas: adicionar n\xF3",
24623
+ description: `Adiciona um n\xF3 (n\xE3o executa). kind: ${kindList()}. data completa o padr\xE3o do tipo; label d\xE1 o nome. Sem position, nasce ao lado do n\xF3 ligado ou \xE0 direita do board. inputs/outputs j\xE1 ligam o n\xF3 no mesmo passo.`,
24624
+ inputSchema: {
24625
+ board_id: boardId2,
24626
+ kind: external_exports.string().min(1).max(40),
24627
+ label: external_exports.string().trim().min(1).max(120).optional().describe("Nome do cart\xE3o (data.label)."),
24628
+ data: external_exports.record(external_exports.unknown()).optional().describe("Campos do data. Ex.: narration {blocks:[{id,text}]}, text {text}, prompt {template}."),
24629
+ position: point.optional(),
24630
+ inputs: external_exports.array(external_exports.object({ from: nodeIdArg, from_port: external_exports.string().min(1), to_port: external_exports.string().min(1) })).max(20).optional().describe("Liga\xE7\xF5es que CHEGAM no n\xF3 novo: from.from_port \u2192 novo.to_port."),
24631
+ outputs: external_exports.array(external_exports.object({ from_port: external_exports.string().min(1), to: nodeIdArg, to_port: external_exports.string().min(1) })).max(20).optional().describe("Liga\xE7\xF5es que SAEM do n\xF3 novo: novo.from_port \u2192 to.to_port.")
24632
+ },
24633
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
24634
+ },
24635
+ async (args) => run(
24636
+ () => edit(args.board_id, async (doc) => {
24637
+ const spec = KINDS[args.kind];
24638
+ if (!spec) throw new CanvasToolError(`Tipo "${args.kind}" desconhecido. Tipos: ${kindList()}.`);
24639
+ const me = await api.me();
24640
+ const data = { ...spec.data, ...parseData(args.data), [KIND_FIELD]: args.kind };
24641
+ if (args.label) data.label = args.label;
24642
+ const near = args.inputs?.[0] ? doc.nodes[args.inputs[0].from] : args.outputs?.[0] ? doc.nodes[args.outputs[0].to] : void 0;
24643
+ const position = args.position ?? placeFor(doc, spec.size, near, args.inputs?.[0] ? "right" : "left");
24644
+ const zIndex = args.kind === "section" ? SECTION_Z : Object.values(doc.nodes).reduce((t, n) => Math.max(t, n.zIndex), 0) + 1;
24645
+ const node = makeNode({ type: WORKFLOW_NODE_TYPE, data, position, size: spec.size, zIndex, createdBy: me });
24646
+ const links = [
24647
+ ...(args.inputs ?? []).map((l) => ({ source: l.from, sourcePort: l.from_port, target: node.id, targetPort: l.to_port })),
24648
+ ...(args.outputs ?? []).map((l) => ({ source: node.id, sourcePort: l.from_port, target: l.to, targetPort: l.to_port }))
24649
+ ];
24650
+ for (const l of links) {
24651
+ const warn = knownPort(args.kind, l.source === node.id ? "out" : "in", l.source === node.id ? l.sourcePort : l.targetPort);
24652
+ if (warn === false) {
24653
+ throw new CanvasToolError(
24654
+ `Porta "${l.source === node.id ? l.sourcePort : l.targetPort}" n\xE3o existe em ${args.kind}. Entradas: ${spec.inputs.join(", ") || "\u2014"}; sa\xEDdas: ${spec.outputs.join(", ") || "\u2014"}.`
24655
+ );
24656
+ }
24657
+ }
24658
+ const edges = edgeOps(doc, links, me, /* @__PURE__ */ new Set([node.id]));
24659
+ return {
24660
+ ops: [{ type: "node.add", nodeId: node.id, node }, ...edges.ops],
24661
+ targets: links.map((l) => l.source === node.id ? l.target : l.source),
24662
+ done: `N\xF3 ${args.kind} "${nodeName(node)}" criado: ${node.id}${edges.ops.length ? ` com ${edges.ops.length} liga\xE7\xE3o(\xF5es)` : ""}.${edges.warn}`
24663
+ };
24664
+ })
24665
+ )
24666
+ );
24667
+ server.registerTool(
24668
+ "canvas_update_node",
24669
+ {
24670
+ title: "Canvas: editar n\xF3",
24671
+ description: "Muda campos do data de um n\xF3, chave a chave (null apaga a chave), sem executar. label renomeia. N\xE3o troca o tipo. Leia o n\xF3 com node_ids antes para n\xE3o sobrescrever o que outra pessoa escreveu.",
24672
+ inputSchema: {
24673
+ board_id: boardId2,
24674
+ node_id: nodeIdArg,
24675
+ data: external_exports.record(external_exports.unknown()).optional().describe("Chaves de data a gravar. Valor null apaga a chave."),
24676
+ label: external_exports.string().trim().min(1).max(120).optional(),
24677
+ position: point.optional()
24678
+ },
24679
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
24680
+ },
24681
+ async (args) => run(
24682
+ () => edit(args.board_id, async (doc) => {
24683
+ const node = needNode(doc, args.node_id);
24684
+ const ops = [];
24685
+ const patch = parseData(args.data);
24686
+ if (args.label) patch.label = args.label;
24687
+ for (const [k, v] of Object.entries(patch)) {
24688
+ const path = `data.${k}`;
24689
+ if (v === null) {
24690
+ if (Object.hasOwn(node.data, k)) ops.push({ type: "node.set", nodeId: node.id, path, value: unsetValue() });
24691
+ } else if (JSON.stringify(node.data[k]) !== JSON.stringify(v)) {
24692
+ ops.push({ type: "node.set", nodeId: node.id, path, value: setValue(v) });
24693
+ }
24694
+ }
24695
+ if (args.position) {
24696
+ ops.push({ type: "node.set", nodeId: node.id, path: "position.x", value: setValue(args.position.x) });
24697
+ ops.push({ type: "node.set", nodeId: node.id, path: "position.y", value: setValue(args.position.y) });
24698
+ }
24699
+ const keys = ops.map((o) => o.type === "node.set" ? o.path.replace(/^data\./, "") : "").join(", ");
24700
+ return { ops, targets: [node.id], done: `N\xF3 "${nodeName(node)}" atualizado: ${keys || "nada mudou"}` };
24701
+ })
24702
+ )
24703
+ );
24704
+ server.registerTool(
24705
+ "canvas_connect",
24706
+ {
24707
+ title: "Canvas: ligar portas",
24708
+ description: "Liga a sa\xEDda de um n\xF3 \xE0 entrada de outro (ids e portas de canvas_read_board). Liga\xE7\xE3o repetida \xE9 ignorada.",
24709
+ inputSchema: {
24710
+ board_id: boardId2,
24711
+ source: nodeIdArg,
24712
+ source_port: external_exports.string().min(1).max(80),
24713
+ target: nodeIdArg,
24714
+ target_port: external_exports.string().min(1).max(80)
24715
+ },
24716
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
24717
+ },
24718
+ async (args) => run(
24719
+ () => edit(args.board_id, async (doc) => {
24720
+ if (args.source === args.target) throw new CanvasToolError("Um n\xF3 n\xE3o se liga nele mesmo.");
24721
+ const link = { source: args.source, sourcePort: args.source_port, target: args.target, targetPort: args.target_port };
24722
+ const { ops, warn } = edgeOps(doc, [link], await api.me(), /* @__PURE__ */ new Set());
24723
+ return {
24724
+ ops,
24725
+ targets: [args.source, args.target],
24726
+ done: ops.length ? `Ligado ${nodeName(doc.nodes[args.source])}.${args.source_port} \u2192 ${nodeName(doc.nodes[args.target])}.${args.target_port}.${warn}` : "A liga\xE7\xE3o j\xE1 existia."
24727
+ };
24728
+ })
24729
+ )
24730
+ );
24731
+ server.registerTool(
24732
+ "canvas_delete",
24733
+ {
24734
+ title: "Canvas: apagar n\xF3s/liga\xE7\xF5es",
24735
+ description: "Apaga n\xF3s (com as liga\xE7\xF5es e varia\xE7\xF5es deles) e/ou liga\xE7\xF5es, num passo s\xF3. Irrevers\xEDvel pelo MCP: confirme com o usu\xE1rio antes. N\xE3o apaga o board.",
24736
+ inputSchema: {
24737
+ board_id: boardId2,
24738
+ node_ids: external_exports.array(nodeIdArg).max(200).optional(),
24739
+ edge_ids: external_exports.array(nodeIdArg).max(200).optional()
24740
+ },
24741
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
24742
+ },
24743
+ async (args) => run(
24744
+ () => edit(args.board_id, async (doc) => {
24745
+ const nodeIds = [...new Set(args.node_ids ?? [])];
24746
+ const edgeIds = [...new Set(args.edge_ids ?? [])];
24747
+ if (!nodeIds.length && !edgeIds.length) throw new CanvasToolError("Informe node_ids e/ou edge_ids.");
24748
+ const missing = [...nodeIds.filter((n) => !doc.nodes[n]), ...edgeIds.filter((e) => !doc.edges[e])];
24749
+ if (missing.length) throw new CanvasToolError(`N\xE3o existem no board: ${missing.join(", ")}. Releia com canvas_read_board.`);
24750
+ const ops = [];
24751
+ const gone = /* @__PURE__ */ new Set();
24752
+ for (const e of edgeIds) {
24753
+ const edge = doc.edges[e];
24754
+ if (nodeIds.includes(edge.source) || nodeIds.includes(edge.target)) continue;
24755
+ ops.push({ type: "edge.remove", edgeId: e });
24756
+ gone.add(e);
24757
+ }
24758
+ for (const n of nodeIds) {
24759
+ for (const op of removeNodeOps(doc, n)) {
24760
+ if (op.type === "edge.remove") {
24761
+ if (gone.has(op.edgeId)) continue;
24762
+ gone.add(op.edgeId);
24763
+ }
24764
+ ops.push(op);
24765
+ }
24766
+ }
24767
+ const names = nodeIds.map((n) => `"${oneLine(nodeName(doc.nodes[n]), 40)}"`);
24768
+ return {
24769
+ ops,
24770
+ targets: nodeIds,
24771
+ done: `Apagado: ${nodeIds.length} n\xF3(s)${names.length ? ` (${names.join(", ")})` : ""} e ${gone.size} liga\xE7\xE3o(\xF5es)`
24772
+ };
24773
+ })
24774
+ )
24775
+ );
24776
+ server.registerTool(
24777
+ "canvas_rename_board",
24778
+ {
24779
+ title: "Canvas: renomear board",
24780
+ description: "Troca o t\xEDtulo do board (board.set). Precisa de papel de editor ou dono.",
24781
+ inputSchema: { board_id: boardId2, title: external_exports.string().trim().min(1).max(200) },
24782
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
24783
+ },
24784
+ async ({ board_id, title }) => run(
24785
+ () => edit(board_id, async (doc) => ({
24786
+ ops: doc.title === title ? [] : [{ type: "board.set", field: "title", value: setValue(title) }],
24787
+ targets: [],
24788
+ done: `Board renomeado: "${doc.title}" \u2192 "${title}"`
24789
+ }))
24790
+ )
24791
+ );
24792
+ server.registerTool(
24793
+ "canvas_list_members",
24794
+ {
24795
+ title: "Canvas: membros do board",
24796
+ description: "Lista quem tem acesso ao board (userId, nome, papel). S\xF3 leitura.",
24797
+ inputSchema: { board_id: boardId2 },
24798
+ annotations: { readOnlyHint: true, openWorldHint: false }
24799
+ },
24800
+ async ({ board_id }) => run(async () => {
24801
+ const members = await api.members(board_id);
24802
+ return members.map((m) => {
24803
+ const name = [m.firstName, m.lastName].filter(Boolean).join(" ") || m.nickname || "(sem nome)";
24804
+ return `- ${m.userId} \xB7 ${name} \xB7 ${m.role}`;
24805
+ }).join("\n");
24806
+ })
24807
+ );
24808
+ server.registerTool(
24809
+ "canvas_set_member",
24810
+ {
24811
+ title: "Canvas: adicionar membro",
24812
+ description: "D\xE1 acesso ao board (ou troca o papel) para um usu\xE1rio pelo userId (uuid). S\xF3 o dono pode. Ainda n\xE3o h\xE1 busca por e-mail: pe\xE7a o userId ou copie de canvas_list_members de outro board.",
24813
+ inputSchema: {
24814
+ board_id: boardId2,
24815
+ user_id: external_exports.string().uuid(),
24816
+ role: external_exports.enum(["editor", "viewer"]).describe("editor edita; viewer s\xF3 v\xEA.")
24817
+ },
24818
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
24819
+ },
24820
+ async ({ board_id, user_id, role }) => run(async () => {
24821
+ const r = await api.putMember(board_id, user_id, role);
24822
+ return `\u2705 ${r.userId} agora \xE9 ${r.role} no board.`;
24823
+ })
24824
+ );
24825
+ server.registerTool(
24826
+ "canvas_remove_member",
24827
+ {
24828
+ title: "Canvas: remover membro",
24829
+ description: "Tira o acesso de algu\xE9m ao board (o dono tira qualquer um; quem n\xE3o \xE9 dono s\xF3 sai sozinho). O dono n\xE3o sai.",
24830
+ inputSchema: { board_id: boardId2, user_id: external_exports.string().uuid() },
24831
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
24832
+ },
24833
+ async ({ board_id, user_id }) => run(async () => {
24834
+ await api.removeMember(board_id, user_id);
24835
+ return `\u2705 ${user_id} n\xE3o tem mais acesso ao board.`;
24836
+ })
24837
+ );
24838
+ registerCanvasCommentTools(server, api, run);
24839
+ }
24840
+
23469
24841
  // src/version.ts
23470
24842
  import { createRequire } from "module";
23471
24843
  var require2 = createRequire(import.meta.url);
23472
24844
  function readVersion() {
23473
- if ("0.25.0") return "0.25.0";
24845
+ if ("0.27.0") return "0.27.0";
23474
24846
  try {
23475
24847
  const pkg = require2("../package.json");
23476
24848
  return pkg.version ?? "0.0.0-unknown";
@@ -23481,8 +24853,15 @@ function readVersion() {
23481
24853
  var VERSION = readVersion();
23482
24854
 
23483
24855
  // src/server.ts
24856
+ var SERVER_INSTRUCTIONS = [
24857
+ "BePlus: tudo roda pela conta do usu\xE1rio (diamantes, limites e hist\xF3rico na plataforma).",
24858
+ '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.',
24859
+ "Calls: search_calls acha, get_call traz a transcri\xE7\xE3o.",
24860
+ "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 ferramentas canvas_* n\xE3o geram m\xEDdia nem gastam diamantes: quem roda os n\xF3s \xE9 o usu\xE1rio no Canvas. D\xEA nome (label) aos n\xF3s, n\xE3o invente tipos nem portas, e confirme antes de canvas_delete.",
24861
+ '"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.'
24862
+ ].join("\n");
23484
24863
  function buildServer(cfg) {
23485
- const server = new McpServer({ name: "beplus", version: VERSION });
24864
+ const server = new McpServer({ name: "beplus", version: VERSION }, { instructions: SERVER_INSTRUCTIONS });
23486
24865
  const client = new BeplusClient(cfg);
23487
24866
  registerGenerationTools(server, client, cfg);
23488
24867
  registerUpscaleTools(server, client, cfg);
@@ -23493,6 +24872,7 @@ function buildServer(cfg) {
23493
24872
  registerProjectTools(server, client, cfg);
23494
24873
  registerClientTools(server, client, cfg);
23495
24874
  registerCallTools(server, client, cfg);
24875
+ registerCanvasTools(server, client, cfg);
23496
24876
  return { server, client };
23497
24877
  }
23498
24878
 
@@ -23732,16 +25112,16 @@ async function json(url, init) {
23732
25112
  throw new Error(`Nao consegui falar com ${new URL(url).origin}. Confira a rede e o BEPLUS_API_URL.`);
23733
25113
  }
23734
25114
  const text = await res.text();
23735
- let body;
25115
+ let body2;
23736
25116
  try {
23737
- body = text ? JSON.parse(text) : void 0;
25117
+ body2 = text ? JSON.parse(text) : void 0;
23738
25118
  } catch {
23739
25119
  }
23740
25120
  if (!res.ok) {
23741
- const msg = body?.message ?? body?.error_description ?? body?.error ?? `HTTP ${res.status}`;
25121
+ const msg = body2?.message ?? body2?.error_description ?? body2?.error ?? `HTTP ${res.status}`;
23742
25122
  throw new Error(`${msg} (${url})`);
23743
25123
  }
23744
- return body;
25124
+ return body2;
23745
25125
  }
23746
25126
  async function discover(apiUrl2) {
23747
25127
  const meta = await json(`${apiUrl2}/.well-known/oauth-authorization-server`);