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/README.md +24 -0
- package/dist/index.js +1411 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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(
|
|
919
|
+
block(body2, nodeCount) {
|
|
920
920
|
this._blockStarts.push(this._nodes.length);
|
|
921
|
-
if (
|
|
922
|
-
this.code(
|
|
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 },
|
|
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(
|
|
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(
|
|
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,
|
|
21159
|
+
constructor(httpStatus, message, body2) {
|
|
21159
21160
|
super(message);
|
|
21160
21161
|
this.name = "ApiError";
|
|
21161
21162
|
this.httpStatus = httpStatus;
|
|
21162
|
-
this.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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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}${
|
|
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.
|
|
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
|
|
25115
|
+
let body2;
|
|
23736
25116
|
try {
|
|
23737
|
-
|
|
25117
|
+
body2 = text ? JSON.parse(text) : void 0;
|
|
23738
25118
|
} catch {
|
|
23739
25119
|
}
|
|
23740
25120
|
if (!res.ok) {
|
|
23741
|
-
const msg =
|
|
25121
|
+
const msg = body2?.message ?? body2?.error_description ?? body2?.error ?? `HTTP ${res.status}`;
|
|
23742
25122
|
throw new Error(`${msg} (${url})`);
|
|
23743
25123
|
}
|
|
23744
|
-
return
|
|
25124
|
+
return body2;
|
|
23745
25125
|
}
|
|
23746
25126
|
async function discover(apiUrl2) {
|
|
23747
25127
|
const meta = await json(`${apiUrl2}/.well-known/oauth-authorization-server`);
|