sapiens-mcp 1.80.0 → 1.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,7 +49,7 @@ O Claude avisa o custo antes de gastar, e geração que falha é estornada. Publ
49
49
 
50
50
  ## Gerar com a sua chave (0 Sinapse)
51
51
 
52
- Tem crédito na fal ou na Kie? Ponha a chave no `env` do servidor, no config do seu cliente, e a `sapiens_pontes` gera no provedor com ela e traz a peça pro seu acervo. Você paga o preço do provedor; a casa cobra zero em cima.
52
+ Tem crédito na fal, na Kie, na WaveSpeed ou no Replicate? Ponha a chave no `env` do servidor, no config do seu cliente (só as que você tem), e a `sapiens_pontes` gera no provedor com ela e traz a peça pro seu acervo. Você paga o preço do provedor; a casa cobra zero em cima.
53
53
 
54
54
  ```json
55
55
  {
@@ -59,16 +59,18 @@ Tem crédito na fal ou na Kie? Ponha a chave no `env` do servidor, no config do
59
59
  "args": ["-y", "sapiens-mcp"],
60
60
  "env": {
61
61
  "FAL_KEY": "cole-aqui-a-chave-da-fal",
62
- "KIE_API_KEY": "cole-aqui-a-chave-da-kie"
62
+ "KIE_API_KEY": "cole-aqui-a-chave-da-kie",
63
+ "WAVESPEED_API_KEY": "cole-aqui-a-chave-da-wavespeed",
64
+ "REPLICATE_API_TOKEN": "cole-aqui-o-token-do-replicate"
63
65
  }
64
66
  }
65
67
  }
66
68
  }
67
69
  ```
68
70
 
69
- No Claude Code: `claude mcp add sapiens --env FAL_KEY=sua-chave -- npx -y sapiens-mcp`. Depois é só pedir: "gera um retrato da Vera na fal com a minha chave".
71
+ No Claude Code: `claude mcp add sapiens --env FAL_KEY=sua-chave -- npx -y sapiens-mcp` (um `--env` por chave). Depois é só pedir: "gera um retrato da Vera na fal com a minha chave".
70
72
 
71
- No Claude Desktop tem um caminho sem config: a extensão [sapiens.mcpb](https://sapiensinteticos.b-cdn.net/mcpb/sapiens.mcpb). Abra o arquivo e o Claude pede a chave da fal e da Kie em campos próprios, que ficam guardados no cofre do sistema. Versão nova é baixar o arquivo de novo e abrir.
73
+ No Claude Desktop tem um caminho sem config: a extensão [sapiens.mcpb](https://sapiensinteticos.b-cdn.net/mcpb/sapiens.mcpb). Abra o arquivo e o Claude pede cada chave num campo próprio, que fica guardado no cofre do sistema. Versão nova é baixar o arquivo de novo e abrir.
72
74
 
73
75
  A chave fica na sua máquina e sai dela só pro provedor: não vai em argumento, não volta em resultado, não passa pelo servidor do Sapiens. Por isso a `sapiens_pontes` existe só aqui, no servidor instalado; o conector remoto (claude.ai, ChatGPT) não recebe chave. Os jobs ficam em `~/.sapiens-mcp/pontes-jobs.json`, e o mesmo pedido repetido em 30 minutos devolve o job que já existe em vez de cobrar de novo.
74
76
 
@@ -81,7 +83,7 @@ A chave fica na sua máquina e sai dela só pro provedor: não vai em argumento,
81
83
 
82
84
  ## Privacidade
83
85
 
84
- O servidor conversa com o backend público do Sapiens (Convex) e, só quando você gera com a sua chave, com o provedor dela (fal ou Kie), direto da sua máquina. Sua identidade vem sempre do token de login, nunca de parâmetros soltos. Cada conta só mexe no que é dela.
86
+ O servidor conversa com o backend público do Sapiens (Convex) e, só quando você gera com a sua chave, com o provedor dela (fal, Kie, WaveSpeed ou Replicate), direto da sua máquina. Sua identidade vem sempre do token de login, nunca de parâmetros soltos. Cada conta só mexe no que é dela.
85
87
 
86
88
  ## Sobre o Sapiens Sintéticos
87
89
 
package/dist/canal.js CHANGED
@@ -1,16 +1,34 @@
1
- /**
2
- * Por onde este processo chegou na máquina da pessoa. Muda como se atualiza:
3
- *
4
- * - "npm" (padrão): `npx -y sapiens-mcp` no config do cliente. Versão nova =
5
- * limpar o cache do npx ou pinar a versão.
6
- * - "mcpb": a extensão do Claude Desktop (tools/mcp-sapiens/scripts/mcpb.mjs).
7
- * O manifesto dela põe SAPIENS_MCP_CANAL=mcpb no ambiente. Versão nova =
8
- * baixar o arquivo de novo e abrir: o Claude Desktop troca a extensão.
9
- */
10
- export const CANAL = process.env.SAPIENS_MCP_CANAL === "mcpb" ? "mcpb" : "npm";
1
+ const CANAIS = ["npm", "mcpb", "plugin"];
2
+ const pedido = process.env.SAPIENS_MCP_CANAL;
3
+ export const CANAL = pedido && CANAIS.includes(pedido) ? pedido : "npm";
11
4
  /**
12
5
  * Endereço estável da extensão no CDN da casa. O scripts/mcpb.mjs sobe aqui a
13
6
  * cada release e purga o cache da borda; a página /conectar-claude aponta pro
14
7
  * mesmo arquivo.
15
8
  */
16
9
  export const MCPB_URL = "https://sapiensinteticos.b-cdn.net/mcpb/sapiens.mcpb";
10
+ /** plugin@marketplace, do jeito que os comandos do Claude Code pedem. */
11
+ export const PLUGIN_ID = "sapiens-sinteticos@sapiens-sinteticos";
12
+ export const ENDERECO_SUAS_CHAVES = "https://www.sapiensinteticos.com/conectar-claude#suas-chaves";
13
+ /** Onde a pessoa põe ou troca a chave de cada balcão, no canal deste processo. */
14
+ export function ondeFicaAChave() {
15
+ switch (CANAL) {
16
+ case "mcpb":
17
+ return "No Claude Desktop: Configurações, Extensões, Sapiens Sintéticos. Os campos de chave (fal, Kie, WaveSpeed, Replicate) ficam ali, só os que você tem, e o que você colar fica guardado no seu computador.";
18
+ case "plugin":
19
+ return 'No Claude Code: /plugin, aba Installed, Sapiens Sintéticos, "Configure options". Os campos de chave (fal, Kie, WaveSpeed, Replicate) ficam ali, só os que você tem, e vão pro cofre do sistema; depois, /reload-plugins.';
20
+ default:
21
+ return `No config do cliente MCP, no bloco do sapiens, só as que você tem: "env": { "FAL_KEY": "...", "KIE_API_KEY": "...", "WAVESPEED_API_KEY": "...", "REPLICATE_API_TOKEN": "..." }, e reinicie o cliente. No Claude Code o plugin põe a chave num campo, e no Claude Desktop a extensão (.mcpb), sem editar config. Passo a passo: ${ENDERECO_SUAS_CHAVES}`;
22
+ }
23
+ }
24
+ /** Como sair de uma versão atrasada, no canal deste processo. */
25
+ export function comoAtualizar(latest) {
26
+ switch (CANAL) {
27
+ case "mcpb":
28
+ return `Baixar a extensão nova (${MCPB_URL}) e abrir o arquivo: o Claude Desktop troca a versão da extensão e as chaves já guardadas ficam.`;
29
+ case "plugin":
30
+ return `No Claude Code: /plugin, aba Installed, Sapiens Sintéticos, "Update now" (ou \`claude plugin update ${PLUGIN_ID}\` no terminal) e depois /reload-plugins; as chaves já guardadas ficam. Pra não ficar pra trás de novo: /plugin, aba Marketplaces, sapiens-sinteticos, "Enable auto-update".`;
31
+ default:
32
+ return `Pinar sapiens-mcp@${latest ?? "latest"} na config do MCP, ou limpar o cache do npx e reiniciar o cliente (restart sozinho não basta, o cache sobrevive).`;
33
+ }
34
+ }
package/dist/registry.js CHANGED
@@ -88,7 +88,7 @@ export const TOOLS = {
88
88
  handler: gallery,
89
89
  },
90
90
  sapiens_pontes: {
91
- description: "GERAR COM A CHAVE DA PESSOA (só no MCP instalado): a outra porta ao lado da Sinapse. Chama o provedor com a chave que mora no ambiente DESTA máquina (o env do config do cliente MCP: FAL_KEY, KIE_API_KEY) e traz a peça pro acervo como nativa, custo 0 em Sinapses: a pessoa paga o preço do provedor, sem margem da casa. A chave NUNCA vai em arg, prompt ou chat, nunca volta no resultado e nunca passa pelo Sapiens: a tool lê do ambiente e o pedido sai daqui direto pro provedor. No conector remoto (claude.ai, ChatGPT) esta tool não existe; lá é Sinapse (sapiens_image, sapiens_video) ou o harness da pessoa + sapiens_gallery action=ingest. Sub-actions: 'balcoes' (quais chaves estão configuradas, pelo NOME, e como configurar; grátis, sem rede), 'gerar' (provedor 'fal' ou 'kie' + modelo = o slug NO PROVEDOR, igual à página do modelo (fal: o id do endpoint, ex 'fal-ai/flux/dev'; Kie: o 'model' do Market) + input = o corpo que a doc do provedor descreve pra esse modelo, com o prompt DENTRO. COBRA NA CONTA DA PESSOA no provedor: confirme modelo e custo com ela antes. O job fica gravado nesta máquina ANTES de esperar o render, e o pedido idêntico em 30 minutos devolve o job que já existe em vez de cobrar de novo (forcar=true repete de propósito). Espera até aguardarSegundos (padrão 45) e, pronto, ingere sozinho com characterId, referenceImageIds, prompt verbatim e o custo quando o provedor diz), 'status' (jobId: consulta o provedor e, se terminou, traz pro acervo; é o que se chama depois de um gerar que voltou 'rodando', NUNCA gerar de novo), 'jobs' (os jobs desta máquina, sem rede). A FICHA DO PERSONAGEM entra no input: pegue passportPrompt e mainImageUrl/imageUrls em sapiens_character action=get, ponha o descritor no começo do prompt e as imagens no campo de referência que o modelo aceita (image_url, image_urls, conforme a doc dele); o bloco NEGATIVE do passaporte vai no campo negative_prompt quando o modelo tem um. O prompt passa pelo piso de idade da casa antes de sair (18 é o piso). Peça de fal entra com o carimbo +18 por classe (peso aberto), igual ao ingest manual; Kie entra sem. Passo a passo na skill 'pontes'.",
91
+ description: "GERAR COM A CHAVE DA PESSOA (só no MCP instalado): a outra porta ao lado da Sinapse. Chama o provedor com a chave que mora no ambiente DESTA máquina (o campo do plugin ou da extensão, ou o env do config do cliente MCP: FAL_KEY, KIE_API_KEY, WAVESPEED_API_KEY, REPLICATE_API_TOKEN) e traz a peça pro acervo como nativa, custo 0 em Sinapses: a pessoa paga o preço do provedor, sem margem da casa. A chave NUNCA vai em arg, prompt ou chat, nunca volta no resultado e nunca passa pelo Sapiens: a tool lê do ambiente e o pedido sai daqui direto pro provedor. No conector remoto (claude.ai, ChatGPT) esta tool não existe; lá é Sinapse (sapiens_image, sapiens_video) ou o harness da pessoa + sapiens_gallery action=ingest. Sub-actions: 'balcoes' (quais chaves estão configuradas, pelo NOME, e como configurar; grátis, sem rede), 'gerar' (provedor 'fal', 'kie', 'wavespeed' ou 'replicate' + modelo = o slug NO PROVEDOR, igual à página do modelo (fal: o id do endpoint, ex 'fal-ai/flux/dev'; Kie: o 'model' do Market; WaveSpeed: o slug da página, ex 'wavespeed-ai/flux-2-klein-9b/text-to-image'; Replicate: 'dono/nome', ou 'dono/nome:<versão>' pra modelo de comunidade) + input = o corpo que a doc do provedor descreve pra esse modelo, com o prompt DENTRO. COBRA NA CONTA DA PESSOA no provedor: confirme modelo e custo com ela antes. O job fica gravado nesta máquina ANTES de esperar o render, e o pedido idêntico em 30 minutos devolve o job que já existe em vez de cobrar de novo (forcar=true repete de propósito). Espera até aguardarSegundos (padrão 45) e, pronto, ingere sozinho com characterId, referenceImageIds, prompt verbatim e o custo quando o provedor diz), 'status' (jobId: consulta o provedor e, se terminou, traz pro acervo; é o que se chama depois de um gerar que voltou 'rodando', NUNCA gerar de novo), 'jobs' (os jobs desta máquina, sem rede). A FICHA DO PERSONAGEM entra no input: pegue passportPrompt e mainImageUrl/imageUrls em sapiens_character action=get, ponha o descritor no começo do prompt e as imagens no campo de referência que o modelo aceita (image_url, image_urls, conforme a doc dele); o bloco NEGATIVE do passaporte vai no campo negative_prompt quando o modelo tem um. O prompt passa pelo piso de idade da casa antes de sair (18 é o piso). Peça de fal, WaveSpeed ou Replicate entra com o carimbo +18 por classe (peso aberto), igual ao ingest manual; Kie entra sem. Passo a passo na skill 'pontes'.",
92
92
  schema: pontesSchema,
93
93
  handler: pontes,
94
94
  },
@@ -218,7 +218,7 @@ export const TOOLS = {
218
218
  handler: trilhas,
219
219
  },
220
220
  sapiens_distribution: {
221
- description: "Despachar peças (Distribution Workflow) — a peça pronta entra na FILA com canal, copy e a aba de quem ela é. ADMIN-ONLY: despacho depende de rede conectada e hoje só a casa tem. NADA aqui posta em rede nenhuma; quem despacha é a tela em /experimentos/distribution-workflow, que é onde a conta conectada mora. A ABA (`voice`) responde de quem é a peça: sapiens (a casa), helen-ailith, borderless, ou o slug de um personagem seu — slug livre, então personagem novo ganha aba sozinho na primeira peça dele, sem deploy. Em queue, aba ausente = o servidor deriva do personagem da peça e cai em sapiens quando não há. Sub-actions: queue (põe na fila: title e channel obrigatórios, mais copy/hashtags/assetUrl/notes; replyText é o comentário que sai junto do post, onde vai o link, e só o X (resposta no fio) e a Página do LinkedIn (primeiro comentário) publicam, o resto recusa; num canal do Instagram, format instagram_story faz a peça sair como STORY: uma mídia só, sem legenda nem link, some em 24h). Em queue, DOIS campos que parecem opcionais e não são: `lang` diz em que língua a copy já está, e sem ele canal de rede internacional reescreve a sua copy sozinho, dois segundos depois, na voz da casa; `assetPageAssetId` é o imageId da obra da casa, e sem ele a peça só encontra a obra se a assetUrl bater letra a letra, então versão web de um master nasce órfã de ficha, list (o que está na fila, filtrável por status e por aba), voices (as abas que existem hoje com a contagem de cada uma), status (move a peça de lane: fila, agendado, postado, descartado; postado aceita postUrl e fecha o rastro, agendado aceita scheduledFor em ms), sign (liga ou desliga a ASSINATURA da peça, imagem e vídeo do CDN da casa, com o carimbo da LINHAGEM de quem assina: sint.fyi/<endereço dela> no canto e o fecho de 2s no vídeo, ou a marca própria dela sem nada da casa; itemId, e signed com true de default; o carimbo da casa é recusado em peça +18 e praça adulta, a marca própria passa; o carimbo leva uns 30s e até lá a peça não sai; em queue, signed:true já enfileira assinada), bio (lê ou troca a BIO de uma conta da casa no Bluesky, channel bluesky ou bluesky-2 a bluesky-5: sem texto só lê; bioLine acrescenta uma linha no fim sem mexer no resto e não repete linha que já está lá; bio reescreve a inteira; teto de 256; ESTA sub-action muda o perfil público na hora, e bio não aceita hiperlink, o endereço aparece escrito), lineages (o CADASTRO das linhagens, o mesmo da aba Teia: de cada personagem, produto e da casa, a mãe no Bluesky onde a peça nasce, as contas próprias, as galerias que a mãe alimenta sozinha, as fichas e o post do dia, mais as regras que a teia desdobra e as vagas de conta ainda sem dona), lineage-new (nasce uma linhagem: nome obrigatório, grupo, artigo, lang; nasce sem mãe, então nada viaja sozinho até o lineage-save dar a mãe), lineage-save (slug e só o que muda: mae, contas rede->canal, galerias, fichaIds, rotina, contaNaCasa, assinatura {estilo casa|propria|nenhuma, marca, padrao}; o servidor recusa conta que já é de outra linhagem e canal de outra rede, e a vaga assumida ganha o nome da linhagem), lineage-archive (slug + arquivada), garimpo (o ACERVO em famílias pra escolher o que vai pra fila: só as candidatas saem, a versão velha de um corte ou upscale vem marcada como substituída, as irmãs da mesma ideia vêm agrupadas pra escolha no olho, o painel cortado vem em recortes (o original e os quadros em ordem, que só saem juntos em carrossel), e cada família diz por onde já saiu; paraCanal tira o que já foi pro canal pedido, soResumo conta sem listar, página por cursor em antesDe; o passo a passo é a skill distribuir). Sem custo em Sinapses: isto organiza, não gera. A copy segue a voz de QUEM ASSINA a peça, que não é sempre a voz da casa: peça da Helen fala como a Helen. E a peça sai pela CONTA de quem assina ela: conta extra da mesma rede é um canal próprio (bluesky-2 é outra conta no Bluesky, instagram-pro outra no Instagram), então case o canal com a aba antes de despachar.",
221
+ description: "Despachar peças (Distribution Workflow) — a peça pronta entra na FILA com canal, copy e a aba de quem ela é. ADMIN-ONLY: despacho depende de rede conectada e hoje só a casa tem. NADA aqui posta em rede nenhuma; quem despacha é a tela em /experimentos/distribution-workflow, que é onde a conta conectada mora. A ABA (`voice`) responde de quem é a peça: sapiens (a casa), helen-ailith, borderless, ou o slug de um personagem seu — slug livre, então personagem novo ganha aba sozinho na primeira peça dele, sem deploy. Em queue, aba ausente = o servidor deriva do personagem da peça e cai em sapiens quando não há. Sub-actions: queue (põe na fila: title e channel obrigatórios, mais copy/hashtags/assetUrl/notes; replyText é o comentário que sai junto do post, onde vai o link, e só o X (resposta no fio) e a Página do LinkedIn (primeiro comentário) publicam, o resto recusa; num canal do Instagram, format instagram_story faz a peça sair como STORY: uma mídia só, sem legenda nem link, some em 24h). Em queue, DOIS campos que parecem opcionais e não são: `lang` diz em que língua a copy já está, e sem ele canal de rede internacional reescreve a sua copy sozinho, dois segundos depois, na voz da casa; `assetPageAssetId` é o imageId da obra da casa, e sem ele a peça só encontra a obra se a assetUrl bater letra a letra, então versão web de um master nasce órfã de ficha, list (o que está na fila, filtrável por status e por aba), voices (as abas que existem hoje com a contagem de cada uma), status (move a peça de lane: fila, agendado, postado, descartado; postado aceita postUrl e fecha o rastro, agendado aceita scheduledFor em ms), sign (liga ou desliga a ASSINATURA da peça, imagem e vídeo do CDN da casa, com o carimbo da LINHAGEM de quem assina: sint.fyi/<endereço dela> no canto e o fecho de 2s no vídeo, ou a marca própria dela sem nada da casa; itemId, e signed com true de default; o carimbo da casa é recusado em peça +18 e praça adulta, a marca própria passa; o carimbo leva uns 30s e até lá a peça não sai; em queue, signed:true já enfileira assinada), bio (lê ou troca a BIO de uma conta da casa no Bluesky, channel bluesky ou bluesky-2 a bluesky-5: sem texto só lê; bioLine acrescenta uma linha no fim sem mexer no resto e não repete linha que já está lá; bio reescreve a inteira; teto de 256; ESTA sub-action muda o perfil público na hora, e bio não aceita hiperlink, o endereço aparece escrito), lineages (o CADASTRO das linhagens, o mesmo da aba Teia: de cada personagem, produto e da casa, a mãe no Bluesky onde a peça nasce, as contas próprias, as galerias que a mãe alimenta sozinha, as fichas e o post do dia, mais as regras que a teia desdobra e as vagas de conta ainda sem dona), lineage-new (nasce uma linhagem: nome obrigatório, grupo, artigo, lang; nasce sem mãe, então nada viaja sozinho até o lineage-save dar a mãe), lineage-save (slug e só o que muda: mae, contas rede->canal, galerias, fichaIds, rotina, contaNaCasa, assinatura {estilo casa|propria|nenhuma, marca, padrao}; o servidor recusa conta que já é de outra linhagem e canal de outra rede, e a vaga assumida ganha o nome da linhagem), lineage-archive (slug + arquivada), tree (a ÁRVORE DE CONTAS, o formulário de quem existe onde: linhagem × rede, com o email, o @, o canal e o estado de cada conta, ligada, desligada, fora, falta ou dispensa, mais o passo pra ligar cada rede; slug filtra uma linhagem), tree-mark (slug + celulas [{rede, handle, canal, dispensada}]: registra o email ou o @ que existe lá fora, associa a conta da casa ou dispensa rede opcional, a linhagem inteira de uma vez e tudo ou nada; X, Instagram, Bluesky, email e Telegram são obrigatórias e não se dispensam), garimpo (o ACERVO em famílias pra escolher o que vai pra fila: só as candidatas saem, a versão velha de um corte ou upscale vem marcada como substituída, as irmãs da mesma ideia vêm agrupadas pra escolha no olho, o painel cortado vem em recortes (o original e os quadros em ordem, que só saem juntos em carrossel), e cada família diz por onde já saiu; paraCanal tira o que já foi pro canal pedido, soResumo conta sem listar, página por cursor em antesDe; o passo a passo é a skill distribuir). Sem custo em Sinapses: isto organiza, não gera. A copy segue a voz de QUEM ASSINA a peça, que não é sempre a voz da casa: peça da Helen fala como a Helen. E a peça sai pela CONTA de quem assina ela: conta extra da mesma rede é um canal próprio (bluesky-2 é outra conta no Bluesky, instagram-pro outra no Instagram), então case o canal com a aba antes de despachar.",
222
222
  schema: distributionSchema,
223
223
  handler: distribution,
224
224
  },
@@ -262,7 +262,7 @@ REGRA DE OURO:
262
262
  - PRIMEIRO CONTATO ou "o que você faz?"/"como começo?"/"o que dá pra fazer?": chame sapiens_meta action=start e MOSTRE o resultado na sua voz. Sem login, ele ensina a conectar; logado, traz saldo + primeiros poderes com exemplos. É a porta de entrada: não despeje a lista inteira de tools, deixe o start guiar.
263
263
  - LOGO APÓS UM LOGIN BEM-SUCEDIDO (action=login retornou ok): chame action=start na sequência e mostre a porta de entrada. O recém-chegado não sabe o que pedir; não o deixe na tela em branco, guie a primeira jogada sem ele precisar perguntar.
264
264
  - Antes de gerar algo caro (imagem/música/vídeo), cheque saldo: sapiens_meta action=credits (ou subscription). Saldo baixo, avise o usuário antes. Vídeo é o mais caro da casa: confirme com ele antes de disparar.
265
- - DUAS PORTAS, lado a lado: Sinapse (sapiens_image, sapiens_video: nada pra configurar) ou a CHAVE DA PESSOA num provedor (fal, Kie, Sogni, a própria placa), que custa zero aqui. No MCP instalado, sapiens_pontes gera com a chave que mora no config dela e traz a peça pro acervo; no remoto, ela gera no harness dela e traz por sapiens_gallery action=ingest. sapiens_meta action=pontes diz o que ela já tem configurado: se tiver chave, ofereça as duas portas antes de gastar Sinapse. A chave NUNCA passa pelo Sapiens (nem em arg, nem no chat). Skill 'pontes'.
265
+ - DUAS PORTAS, lado a lado: Sinapse (sapiens_image, sapiens_video: nada pra configurar) ou a CHAVE DA PESSOA num provedor (fal, Kie, WaveSpeed, Replicate, Sogni, a própria placa), que custa zero aqui. No MCP instalado, sapiens_pontes gera com a chave que mora na máquina dela (fal, Kie, WaveSpeed, Replicate) e traz a peça pro acervo; no remoto, ela gera no harness dela e traz por sapiens_gallery action=ingest. sapiens_meta action=pontes diz o que ela já tem configurado: se tiver chave, ofereça as duas portas antes de gastar Sinapse. A chave NUNCA passa pelo Sapiens (nem em arg, nem no chat). Skill 'pontes'.
266
266
  - REGRA DO TIMEOUT: geração SÍNCRONA pode estourar o teto de ~120s do cliente e voltar 'Timeout' MESMO tendo gerado e COBRADO. Nunca repita às cegas: confira antes onde o resultado cairia (a tabela de onde conferir está na skill 'primeiros-passos').
267
267
  - "sessionToken expirado" = refaça login: sapiens_meta action=login com o código de sapiensinteticos.com/conectar-claude.
268
268
  - sapiens_meta action=formats devolve os schemas por formato; action=whoami diz tier (user/admin) + saldo. sapiens_image e sapiens_video action=models trazem o catálogo com o preço ATUAL: consulte em vez de chutar custo.
package/dist/skills.js CHANGED
@@ -735,14 +735,14 @@ A foto NUNCA vai pra banco nenhum, nem da casa nem de terceiro:
735
735
  {
736
736
  name: "pontes",
737
737
  title: "Pontes: gerar com a sua chave e trazer pro acervo",
738
- description: "A pessoa tem crédito na Kie, na fal, na Magnific, na Sogni ou na própria placa e quer usar o personagem dela lá, quer gerar com a chave dela em vez de Sinapse, ou está sem Sinapse pra vídeo. Como levar a ficha, gerar no balcão dela (no MCP instalado, fal e Kie saem pela sapiens_pontes; a chave nunca passa pelo Sapiens) e a peça voltar pro acervo com a ficha inteira. Puxe quando ouvir 'gera com a minha chave', 'tenho FAL_KEY', 'tem Kie aí?', 'uso a fal', 'gastei minhas Sinapses', 'gero na Magnific', 'tenho ComfyUI'.",
738
+ description: "A pessoa tem crédito na Kie, na fal, na WaveSpeed, no Replicate, na Magnific, na Sogni ou na própria placa e quer usar o personagem dela lá, quer gerar com a chave dela em vez de Sinapse, ou está sem Sinapse pra vídeo. Como levar a ficha, gerar no balcão dela (no MCP instalado, fal, Kie, WaveSpeed e Replicate saem pela sapiens_pontes; a chave nunca passa pelo Sapiens) e a peça voltar pro acervo com a ficha inteira. Puxe quando ouvir 'gera com a minha chave', 'tenho FAL_KEY', 'tem Kie aí?', 'uso a fal', 'tenho WaveSpeed', 'uso o Replicate', 'gastei minhas Sinapses', 'gero na Magnific', 'tenho ComfyUI'.",
739
739
  body: `## O que é uma ponte
740
740
 
741
741
  O Sapiens é uma das pontes, não um muro. A pessoa monta o personagem aqui (ficha, passaporte, referências) e gera a peça ONDE TEM CRÉDITO: Kie, fal, Magnific, Sogni, Krea, Replicate, a própria placa. A peça volta pro acervo dela como NATIVA, com motor, prompt, custo real e personagem na ficha, custo 0 em Sinapses. O portfólio dela cresce aqui; o dinheiro dela sai de onde ela já pôs.
742
742
 
743
743
  São duas portas lado a lado, e nenhuma é a reserva da outra: Sinapse (sapiens_image, sapiens_video) é pra quem não quer configurar nada; a chave própria é pra quem já tem crédito num provedor, e aí a casa não cobra nada em cima: ela paga o preço do provedor, sem margem nossa. Quando a pessoa tem chave configurada, ofereça as duas.
744
744
 
745
- **A regra da chave, sem exceção:** a chave do provedor NUNCA passa pelo Sapiens. Não vai em arg de tool, não vai em prompt, não vai colada no chat. Ela mora no ambiente da máquina da pessoa (o env do config do cliente MCP com \`FAL_KEY\`, \`KIE_API_KEY\`; um conector MCP do provedor ligado na conversa; o SDK dela) e quem chama o motor é um processo DA MÁQUINA DELA: o \`sapiens-mcp\` instalado (sapiens_pontes) ou o harness dela. O Sapiens entra ANTES (a ficha) e DEPOIS (o ingest). Se a pessoa colar a chave no chat por engano, diga pra ela gerar uma chave nova no provedor e pôr a nova no config, e siga sem usar o valor.
745
+ **A regra da chave, sem exceção:** a chave do provedor NUNCA passa pelo Sapiens. Não vai em arg de tool, não vai em prompt, não vai colada no chat. Ela mora na máquina da pessoa (o campo do plugin do Claude Code ou da extensão do Claude Desktop, ou o env do config do cliente MCP com \`FAL_KEY\`, \`KIE_API_KEY\`; um conector MCP do provedor ligado na conversa; o SDK dela) e quem chama o motor é um processo DA MÁQUINA DELA: o \`sapiens-mcp\` instalado (sapiens_pontes) ou o harness dela. O Sapiens entra ANTES (a ficha) e DEPOIS (o ingest). Se a pessoa colar a chave no chat por engano, diga pra ela gerar uma chave nova no provedor e pôr a nova no lugar dela (o \`comoConfigurar\` da \`sapiens_pontes action=balcoes\` diz onde), e siga sem usar o valor.
746
746
 
747
747
  ## Passo 1: descobrir onde a pessoa tem crédito
748
748
 
@@ -766,13 +766,13 @@ Confirme com ela ANTES de disparar em qualquer balcão pago. Geração de fora t
766
766
 
767
767
  Regras que valem fora igual dentro: prompt SEM idade em número; personagem nunca menor (a casa trabalha com 23+ no foco e 18 é piso absoluto); motor citado pelo nome que ele tem no provedor. Prompt novo que funcionar lá fora: grave de volta com \`sapiens_character action=set_passport\` (campo \`recipes\`, prompt verbatim + note com o que provou). É isso que faz a próxima rodada não redescobrir.
768
768
 
769
- ## Passo 3a: gerar pela porta local (fal e Kie, MCP instalado)
769
+ ## Passo 3a: gerar pela porta local (fal, Kie, WaveSpeed e Replicate, MCP instalado)
770
770
 
771
- Com o \`sapiens-mcp\` instalado e a chave no env do config, é a porta mais curta, e funciona até no Claude Desktop, que não tem terminal:
771
+ Com o \`sapiens-mcp\` instalado (plugin do Claude Code, extensão do Claude Desktop ou npx) e a chave configurada, é a porta mais curta:
772
772
 
773
- 1. \`sapiens_pontes action=balcoes\` diz quais chaves estão configuradas (pelo nome, sem ler o valor). Faltando, mande a pessoa pôr a chave no config: sapiensinteticos.com/conectar-claude#suas-chaves.
773
+ 1. \`sapiens_pontes action=balcoes\` diz quais chaves estão configuradas (pelo nome, sem ler o valor). Faltando, repasse o \`comoConfigurar\` da mesma resposta: ele diz onde a chave entra no jeito que o Sapiens foi instalado.
774
774
  2. Confirme com ela o modelo e o custo no provedor. É dinheiro dela.
775
- 3. \`sapiens_pontes action=gerar\` com \`provedor\` ('fal' ou 'kie'), \`modelo\` (o slug como está na página do modelo no provedor) e \`input\` (o corpo que a doc do modelo descreve, com o prompt DENTRO), mais \`characterId\` e \`referenceImageIds\` pra ficha. O descritor do passaporte vai no começo do prompt; o bloco NEGATIVE vai no campo \`negative_prompt\` quando o modelo tem um (no corpo do prompt, negação acende o que proíbe, e o piso de idade da casa recusa palavra de menor ali). As imagens da ficha vão no campo de referência do modelo (\`image_url\`, \`image_urls\`).
775
+ 3. \`sapiens_pontes action=gerar\` com \`provedor\` ('fal', 'kie', 'wavespeed' ou 'replicate'), \`modelo\` (o slug como está na página do modelo no provedor; no Replicate, 'dono/nome' ou 'dono/nome:<versão>' pra modelo de comunidade) e \`input\` (o corpo que a doc do modelo descreve, com o prompt DENTRO), mais \`characterId\` e \`referenceImageIds\` pra ficha. O descritor do passaporte vai no começo do prompt; o bloco NEGATIVE vai no campo \`negative_prompt\` quando o modelo tem um (no corpo do prompt, negação acende o que proíbe, e o piso de idade da casa recusa palavra de menor ali). As imagens da ficha vão no campo de referência do modelo (\`image_url\`, \`image_urls\`).
776
776
  4. Voltou \`rodando\`? Chame \`sapiens_pontes action=status jobId=...\`, nunca \`gerar\` de novo: o job está gravado na máquina dela, e o pedido idêntico em 30 minutos devolve o job que existe em vez de cobrar outra vez.
777
777
  5. Terminado, a peça entra sozinha no acervo, privada, custo 0 em Sinapses, com motor, prompt verbatim e custo na ficha. Mostre com \`sapiens_gallery action=list\`.
778
778
 
@@ -33,6 +33,11 @@ import { convexQuery, convexMutation, convexAction, describeConvexError, getSess
33
33
  * a mãe no Bluesky, as contas próprias, as galerias que a mãe alimenta, as
34
34
  * fichas e o post do dia. É ele que decide por qual conta cada peça sai e
35
35
  * o que viaja sozinho depois.
36
+ * - tree / tree-mark: a ÁRVORE DE CONTAS, o formulário de quem existe onde.
37
+ * Linhagem × rede, com o email e o @ de cada conta, o canal que ela ocupa e
38
+ * o que falta criar. tree lê; tree-mark registra o email ou o @ que existe
39
+ * lá fora, associa a conta da casa ou dispensa a rede opcional, a linhagem
40
+ * inteira de uma vez, tudo ou nada. A mesma função da aba Teia > Contas.
36
41
  * - garimpo: o ACERVO em famílias, pra escolher o que vai pra fila. Versão
37
42
  * velha (o original de um corte, o take de um vídeo montado) sai marcada
38
43
  * como substituída; irmãs (a mesma ideia gerada de novo) vêm agrupadas pra
@@ -147,6 +152,8 @@ export const distributionSchema = z.object({
147
152
  "lineage-new",
148
153
  "lineage-save",
149
154
  "lineage-archive",
155
+ "tree",
156
+ "tree-mark",
150
157
  "garimpo",
151
158
  ]),
152
159
  title: z
@@ -166,6 +173,21 @@ export const distributionSchema = z.object({
166
173
  .optional()
167
174
  .describe("queue: o COMENTÁRIO que sai junto do post, pra onde vai o link (a copy fica sem ele e fecha apontando pro comentário). Só dois lugares publicam: o X pelo Buffer (x-buffer, x-buffer-2..4) e o X da API própria (x), como resposta no mesmo fio, teto 280; e a Página do LinkedIn pelo Buffer (linkedin-page, linkedin-page-2), como primeiro comentário, teto 1250. Threads (threads, threads-pro, threads-3..5), LinkedIn Perfil (linkedin) e o resto NÃO publicam comentário: lá o link mora na própria copy. O servidor recusa a peça com replyText num canal que não publica, e recusa o comentário acima do teto com a contagem; nada é cortado calado."),
168
175
  hashtags: z.array(z.string()).optional().describe("queue: sem o #, o servidor limpa."),
176
+ publicTitle: z
177
+ .string()
178
+ .max(100)
179
+ .optional()
180
+ .describe("queue: o título PÚBLICO, o que vai ao ar (o YouTube põe no vídeo, o Reddit no post, o LinkedIn embaixo do player). Ausente, o servidor deriva da 1a linha da copy. No YouTube passe sempre: a descrição costuma abrir com crédito e link, e isso não é título."),
181
+ captionTracks: z
182
+ .array(z.object({ lang: z.string(), url: z.string(), name: z.string().optional() }))
183
+ .max(4)
184
+ .optional()
185
+ .describe("queue, só YouTube (youtube, youtube-2..5): as faixas de LEGENDA que sobem logo depois do vídeo, UM .srt por língua no CDN da casa (https, *.b-cdn.net). lang no código do YouTube: en, pt-BR. name é o rótulo no seletor do player (default English / Português (Brasil)). A legenda só sobe nas vagas youtube-2..5 (as contas de linhagem, que pedem a permissão de legenda ao conectar; o youtube principal fica só com upload, de propósito) e com a conexão feita a partir de set/2026: conexão antiga ou o youtube principal sobem o vídeo e a peça avisa que a legenda ficou de fora. Outro canal recusa na fila."),
186
+ localizations: z
187
+ .array(z.object({ lang: z.string(), title: z.string().max(100), description: z.string().optional() }))
188
+ .max(4)
189
+ .optional()
190
+ .describe("queue, só YouTube: título e descrição em OUTRA língua, que o YouTube mostra pra quem assiste nela. A língua do vídeo é a da peça (lang en vira en, pt vira pt-BR), e localização na mesma língua do vídeo é ignorada. Ex: [{lang:'pt-BR', title:'...', description:'...'}]."),
169
191
  assetUrl: z
170
192
  .string()
171
193
  .optional()
@@ -244,7 +266,16 @@ export const distributionSchema = z.object({
244
266
  slug: z
245
267
  .string()
246
268
  .optional()
247
- .describe("lineage-save/lineage-archive: a linhagem (o slug que lineages devolve: sapiens, borderless, helen-inbt, aria, arak, giants...). lineage-new: opcional, sai do nome quando ausente."),
269
+ .describe("lineage-save/lineage-archive/tree-mark: a linhagem (o slug que lineages devolve: sapiens, borderless, helen-inbt, aria, arak, giants...). lineage-new: opcional, sai do nome quando ausente. tree: opcional, filtra uma linhagem só."),
270
+ celulas: z
271
+ .array(z.object({
272
+ rede: z.string(),
273
+ handle: z.string().optional(),
274
+ canal: z.string().optional(),
275
+ dispensada: z.boolean().optional(),
276
+ }))
277
+ .optional()
278
+ .describe("tree-mark: as células da linhagem em `slug`, uma por rede da árvore (x, instagram, bluesky, email, telegram, tumblr, deviantart, threads, tiktok, youtube, fanvue; a lista viva sai em tree, campo redes). handle = o @ (ou a URL do perfil, que vira @) ou, na rede email, o endereço: é a conta que existe lá fora; string vazia limpa. canal = a conta da casa que é desta linhagem nessa rede (instagram-6, x-buffer-3), só em rede de conta própria e nunca no bluesky, que é a mãe (lineage-save). dispensada = true quando a linhagem decidiu não ter a rede; X, Instagram, Bluesky, email e Telegram são obrigatórias e o servidor recusa. Tudo ou nada: uma célula recusada não grava nenhuma."),
248
279
  nome: z.string().optional().describe("lineage-new: o nome da linhagem (obrigatório). lineage-save: renomeia."),
249
280
  grupo: z
250
281
  .enum(["casa", "personagens", "produtos"])
@@ -368,6 +399,9 @@ export async function distribution(args) {
368
399
  copy: args.copy,
369
400
  replyText: args.replyText,
370
401
  hashtags: args.hashtags,
402
+ publicTitle: args.publicTitle,
403
+ captionTracks: args.captionTracks,
404
+ localizations: args.localizations,
371
405
  assetUrl: args.assetUrl,
372
406
  assetKind: args.assetKind,
373
407
  assets: args.assets,
@@ -554,5 +588,46 @@ export async function distribution(args) {
554
588
  });
555
589
  return { ok: true, slug: args.slug, arquivada: args.arquivada };
556
590
  }
591
+ if (args.action === "tree") {
592
+ const r = await convexQuery("distributionMcp:mcpArvore", { sessionToken });
593
+ const linhas = r.linhas
594
+ .filter((l) => !args.slug || l.tema === args.slug)
595
+ .map((l) => ({
596
+ tema: l.tema,
597
+ nome: l.nome,
598
+ ficha: l.ficha,
599
+ obrigatorias: `${l.obrigatoriasLigadas}/${l.obrigatorias}`,
600
+ // `declarado` repete o que a célula já diz; o cadastro inteiro sai em lineages.
601
+ celulas: Object.fromEntries(Object.entries(l.celulas).map(([rede, { declarado: _d, ...celula }]) => [rede, celula])),
602
+ }));
603
+ if (args.slug && !linhas.length) {
604
+ throw new Error(`${args.slug} não está na árvore. As linhagens saem em action=lineages; linhagem nova nasce em lineage-new.`);
605
+ }
606
+ return {
607
+ resumo: r.resumo,
608
+ linhas,
609
+ redes: r.redes.map(({ id, nome, forma, obrigatoria, comoLigar }) => ({ id, nome, forma, obrigatoria, comoLigar })),
610
+ ...(args.slug ? {} : { globais: r.globais, livres: r.livres }),
611
+ instruction: "A árvore de contas: linhagem × rede. estado: ligada (a casa tem a conta e ela responde; no email, o endereço está registrado), desligada (a vaga existe, falta credencial ou o token venceu), fora (a conta existe lá fora com o @ e ainda não tem vaga), falta (é criar), dispensa (a linhagem decidiu não ter). origem diz de onde a casa sabe: codigo, ficha (a Presença da personagem) ou declarada. comoLigar, em redes, é o passo que falta em cada rede; livres são contas vivas que nenhuma linhagem ocupa. Conta que nasceu agora se registra com tree-mark, e o email e o @ moram no banco, nunca em arquivo do repo.",
612
+ };
613
+ }
614
+ if (args.action === "tree-mark") {
615
+ if (!args.slug)
616
+ throw new Error("action=tree-mark exige slug (a linhagem).");
617
+ if (!args.celulas?.length) {
618
+ throw new Error("action=tree-mark exige celulas: [{ rede, handle?, canal?, dispensada? }].");
619
+ }
620
+ await convexMutation("distributionMcp:mcpMarcarNaArvore", {
621
+ sessionToken,
622
+ tema: args.slug,
623
+ celulas: args.celulas,
624
+ });
625
+ return {
626
+ ok: true,
627
+ slug: args.slug,
628
+ marcadas: args.celulas.map((c) => c.rede),
629
+ instruction: "Marcado na árvore. Mostre ao dono a linha com action=tree e o slug: o @ sem vaga fica em 'fora' até a conta ganhar vaga e credencial (comoLigar diz o passo de cada rede).",
630
+ };
631
+ }
557
632
  throw new Error(`action desconhecida: ${args.action}`);
558
633
  }
@@ -2,8 +2,13 @@ import { z } from "zod";
2
2
  import { convexQuery, convexMutation, convexAction, getSessionToken, saveSessionToken, clearSessionToken, describeConvexError, isRemoteContext, } from "../convexClient.js";
3
3
  import { getMcpVersion } from "../version.js";
4
4
  import { setTierFromIsAdmin } from "../tier.js";
5
- import { CANAL, MCPB_URL } from "../canal.js";
5
+ import { CANAL, comoAtualizar, ondeFicaAChave } from "../canal.js";
6
6
  import { valorDeChave } from "./pontes.js";
7
+ const ORIGEM = {
8
+ npm: "pelo npm (provável cache do npx)",
9
+ mcpb: "pela extensão do Claude Desktop",
10
+ plugin: "pelo plugin do Claude Code",
11
+ };
7
12
  /**
8
13
  * Versão do MCP realmente rodando (fonte única em ../version.js, lê o
9
14
  * package.json em runtime). Serve pra flagrar client preso em cache antigo do npx.
@@ -210,7 +215,7 @@ const PONTES = [
210
215
  },
211
216
  ];
212
217
  /** Os balcões que a sapiens_pontes dirige daqui (os outros vão pelo harness + ingest). */
213
- const PORTA_LOCAL = ["fal", "kie"];
218
+ const PORTA_LOCAL = ["fal", "kie", "wavespeed", "replicate"];
214
219
  const FIRST_POWERS = [
215
220
  {
216
221
  icon: "📚",
@@ -317,11 +322,15 @@ export async function meta(args) {
317
322
  clientOutdated: {
318
323
  running: ver.version,
319
324
  latest: ver.latest,
325
+ // No plugin, somar o remoto por cima duplicaria as tools: lá só
326
+ // vale atualizar o plugin.
320
327
  howToFix: [
321
- CANAL === "mcpb"
322
- ? `Baixar a extensão nova (${MCPB_URL}) e abrir o arquivo: o Claude Desktop troca a versão da extensão e as chaves já guardadas ficam.`
323
- : `Atualizar o pacote: pinar sapiens-mcp@${ver.latest} na config do MCP, ou limpar o cache do npx e reiniciar o agent (restart sozinho não basta, o cache sobrevive).`,
324
- "Ou conectar pelo MCP remoto em sapiensinteticos.com/conectar-claude: ele sai do site, então está sempre na última versão e nunca desatualiza.",
328
+ comoAtualizar(ver.latest),
329
+ ...(CANAL === "plugin"
330
+ ? []
331
+ : [
332
+ "Ou conectar pelo MCP remoto em sapiensinteticos.com/conectar-claude: ele sai do site, então está sempre na última versão e nunca desatualiza.",
333
+ ]),
325
334
  ],
326
335
  note: "AVISE O USUÁRIO já na primeira resposta. Client atrasado chama rotas antigas e falha de um jeito que parece erro do Sapiens (ex: filme que 'foi adicionado' e não entrou no Repertório).",
327
336
  },
@@ -397,7 +406,7 @@ export async function meta(args) {
397
406
  balance,
398
407
  ...(lowBalance
399
408
  ? {
400
- balanceWarning: "Saldo abaixo de 500 Sinapses: o que é grátis roda tranquilo, mas pra imagem/música/vídeo talvez precise recarregar. Outra saída é a outra porta: com a chave dela num provedor (Kie, fal, Magnific, Sogni), gera lá com a ficha do personagem daqui e a peça volta pro acervo, custo zero em Sinapses. No MCP instalado, fal e Kie saem daqui mesmo por sapiens_pontes action=gerar; o resto volta por sapiens_gallery action=ingest (skill 'pontes'; sapiens_meta action=pontes diz o que ela já tem configurado).",
409
+ balanceWarning: "Saldo abaixo de 500 Sinapses: o que é grátis roda tranquilo, mas pra imagem/música/vídeo talvez precise recarregar. Outra saída é a outra porta: com a chave dela num provedor (Kie, fal, Magnific, Sogni), gera lá com a ficha do personagem daqui e a peça volta pro acervo, custo zero em Sinapses. No MCP instalado, fal, Kie, WaveSpeed e Replicate saem daqui mesmo por sapiens_pontes action=gerar; o resto volta por sapiens_gallery action=ingest (skill 'pontes'; sapiens_meta action=pontes diz o que ela já tem configurado).",
401
410
  }
402
411
  : {}),
403
412
  firstPowers: FIRST_POWERS,
@@ -454,9 +463,7 @@ export async function meta(args) {
454
463
  canal: CANAL,
455
464
  runtime: `node ${process.version}`,
456
465
  note: latest && upToDate === false
457
- ? CANAL === "mcpb"
458
- ? `Rodando ${version} pela extensão do Claude Desktop, e a última é ${latest}. Pra atualizar: baixar a extensão nova (${MCPB_URL}) e abrir o arquivo; as chaves já guardadas ficam.`
459
- : `Rodando ${version}, mas a última no npm é ${latest}: o client está ATRASADO (provável cache do npx). Pra atualizar: pinar sapiens-mcp@${latest} na config do MCP ou recriar o container (restart sozinho não basta).`
466
+ ? `Rodando ${version} ${ORIGEM[CANAL]}, e a última é ${latest}: o client está ATRASADO. Pra atualizar: ${comoAtualizar(latest)}`
460
467
  : latest && upToDate
461
468
  ? `Na última versão (${version}).`
462
469
  : `Versão do binário rodando agora: ${version}. Não consegui consultar o npm pra comparar (offline/timeout).`,
@@ -570,7 +577,7 @@ export async function meta(args) {
570
577
  ? "No MCP remoto não dá pra olhar o ambiente da pessoa, e gerar com a chave dela pede o MCP instalado (a chave não sobe pro conector remoto). Pergunte onde ela tem crédito (Kie, fal, Magnific, Sogni, Krea...) ou olhe os conectores que já estão nesta conversa."
571
578
  : detectadas.length
572
579
  ? `Achei chave configurada (pelo NOME da variável, sem ler o valor) pra: ${detectadas.join(", ")}. Ofereça as duas portas antes de gastar Sinapse.${naPortaLocal.length ? ` ${naPortaLocal.join(" e ")} a casa gera daqui mesmo: sapiens_pontes action=gerar.` : ""}`
573
- : "Nenhuma variável de provedor no ambiente deste MCP. Pergunte onde a pessoa tem crédito, ou olhe os conectores já ligados nesta conversa (Magnific, Krea, Sogni). Pra fal e Kie, a chave vai no env do config do cliente MCP: sapiensinteticos.com/conectar-claude#suas-chaves.",
580
+ : `Nenhuma variável de provedor no ambiente deste MCP. Pergunte onde a pessoa tem crédito, ou olhe os conectores já ligados nesta conversa (Magnific, Krea, Sogni). Pra fal, Kie, WaveSpeed e Replicate: ${ondeFicaAChave()}`,
574
581
  portaLocal: {
575
582
  tool: "sapiens_pontes",
576
583
  balcoes: PORTA_LOCAL,
@@ -5,7 +5,7 @@ import path from "node:path";
5
5
  import { createHash, randomBytes } from "node:crypto";
6
6
  import { describeConvexError, getSessionToken, isRemoteContext, } from "../convexClient.js";
7
7
  import { findMinorMarkers } from "../pisoDeIdade.js";
8
- import { CANAL } from "../canal.js";
8
+ import { CANAL, ondeFicaAChave } from "../canal.js";
9
9
  import { gallery } from "./gallery.js";
10
10
  /**
11
11
  * A PORTA LOCAL DAS PONTES (27/09/2026): gerar no provedor com a chave da
@@ -18,8 +18,8 @@ import { gallery } from "./gallery.js";
18
18
  * quem tem terminal. O Claude Desktop não tem.
19
19
  *
20
20
  * A regra da chave (doc docs/infra/pontes-traga-seu-motor.md): ela mora no
21
- * ambiente DESTA máquina (o "env" do config do cliente MCP) e sai daqui só pro
22
- * provedor. Não entra em arg, não volta em resultado, não vai pro Convex, não
21
+ * ambiente DESTA máquina (o campo do plugin ou da extensão, ou o "env" do config
22
+ * do cliente MCP) e sai daqui só pro provedor. Não entra em arg, não volta em resultado, não vai pro Convex, não
23
23
  * fica no arquivo de jobs. No transporte remoto a tool nem aparece no
24
24
  * tools/list, e a chamada é recusada antes de ler qualquer coisa: lá o processo
25
25
  * é o servidor do site, e chave de membro não sobe.
@@ -32,10 +32,27 @@ import { gallery } from "./gallery.js";
32
32
  * 4. A casa passar a ser quem gera: o prompt passa pelo piso de idade antes
33
33
  * de sair, e a peça entra pelo ingest com o carimbo da classe do provedor.
34
34
  */
35
+ /**
36
+ * Os balcões que esta tool dirige. Cada um é um par enviar/ler (ADAPTADORES,
37
+ * lá embaixo) e uma chave que só volta pro host do próprio provedor: é isso que
38
+ * impede um texto de fora de fazer o Claude mandar a chave pra outro endereço,
39
+ * e é por isso que balcão novo entra por código, nunca por campo livre.
40
+ * 28/09/2026: WaveSpeed e Replicate entraram ("tem que ter liberdade de colocar
41
+ * várias como wavespeed", o dono).
42
+ */
43
+ const PROVEDORES = ["fal", "kie", "wavespeed", "replicate"];
35
44
  const BALCOES = {
36
- fal: { nome: "fal.ai", envVar: "FAL_KEY" },
37
- kie: { nome: "Kie", envVar: "KIE_API_KEY" },
45
+ fal: { nome: "fal.ai", envVar: "FAL_KEY", artigo: "a" },
46
+ kie: { nome: "Kie", envVar: "KIE_API_KEY", artigo: "a" },
47
+ wavespeed: { nome: "WaveSpeed", envVar: "WAVESPEED_API_KEY", artigo: "a" },
48
+ replicate: { nome: "Replicate", envVar: "REPLICATE_API_TOKEN", artigo: "o" },
38
49
  };
50
+ /** "na fal.ai", "do Replicate": a preposição já com o artigo do balcão. */
51
+ function balcaoCom(p, prep) {
52
+ const { nome, artigo } = BALCOES[p];
53
+ const forma = { em: { a: "na", o: "no" }, de: { a: "da", o: "do" }, para: { a: "pra", o: "pro" }, "": { a: "A", o: "O" } };
54
+ return `${forma[prep][artigo]} ${nome}`;
55
+ }
39
56
  /**
40
57
  * Toda variável de chave de provedor que a casa conhece, dirigida por esta tool
41
58
  * ou não: é a lista que o `semChave` limpa. Chave de balcão que a tool não usa
@@ -49,6 +66,20 @@ const NOMES_DE_CHAVE = [
49
66
  "KREA_API_KEY",
50
67
  "REPLICATE_API_TOKEN",
51
68
  ];
69
+ /**
70
+ * No plugin do Claude Code e na extensão do Claude Desktop, o campo de chave
71
+ * chega com nome próprio, e não como FAL_KEY: o env do app sobrescreve o do
72
+ * processo, e campo deixado em branco não pode apagar a FAL_KEY que a pessoa já
73
+ * exporta no terminal. Preenchido, o campo ganha. Os nomes saem de
74
+ * CAMPOS_DE_CHAVE em scripts/servidor.mjs, e o smoke de lá confere que cada
75
+ * campo acende o balcão certo.
76
+ */
77
+ const CAMPO_DO_APP = {
78
+ FAL_KEY: "SAPIENS_FAL_KEY",
79
+ KIE_API_KEY: "SAPIENS_KIE_API_KEY",
80
+ WAVESPEED_API_KEY: "SAPIENS_WAVESPEED_API_KEY",
81
+ REPLICATE_API_TOKEN: "SAPIENS_REPLICATE_API_TOKEN",
82
+ };
52
83
  const JANELA_REPETIDO_MS = 30 * 60 * 1000;
53
84
  const MAX_JOBS_GUARDADOS = 200;
54
85
  const MAX_PECAS_POR_JOB = 4;
@@ -59,18 +90,17 @@ const ESPERA_MAX_S = 75;
59
90
  // leva segundos a dezenas de segundos, então o ingest só começa na mesma
60
91
  // chamada se ainda sobra folga; senão o job volta "pronto" e o status traz.
61
92
  const INGEST_SO_ATE_MS = 60_000;
62
- const ENDERECO_SUAS_CHAVES = "https://www.sapiensinteticos.com/conectar-claude#suas-chaves";
63
93
  export const pontesSchema = z.object({
64
94
  action: z.enum(["balcoes", "gerar", "status", "jobs"]),
65
95
  provedor: z
66
- .enum(["fal", "kie"])
96
+ .enum(PROVEDORES)
67
97
  .optional()
68
- .describe("action=gerar: o balcão. 'fal' lê FAL_KEY; 'kie' lê KIE_API_KEY. As duas moram no env do config do cliente MCP, nesta máquina."),
98
+ .describe("action=gerar: o balcão. 'fal' (FAL_KEY), 'kie' (KIE_API_KEY), 'wavespeed' (WAVESPEED_API_KEY) ou 'replicate' (REPLICATE_API_TOKEN). A chave mora nesta máquina: no campo do plugin ou da extensão, ou no env do config do cliente MCP. action=balcoes diz quais estão configuradas."),
69
99
  modelo: z
70
100
  .string()
71
101
  .max(200)
72
102
  .optional()
73
- .describe("action=gerar: o slug do modelo NO PROVEDOR, igual à página dele. fal: o id do endpoint (ex: 'fal-ai/flux/dev'), que vai no caminho de queue.fal.run. Kie: o campo 'model' do Market (ex: 'kling/v2-1-standard'), que vai no corpo do createTask. Não invente: confira na página do modelo."),
103
+ .describe("action=gerar: o slug do modelo NO PROVEDOR, igual à página dele. fal: o id do endpoint (ex: 'fal-ai/flux/dev'), que vai no caminho de queue.fal.run. Kie: o campo 'model' do Market (ex: 'kling/v2-1-standard'), que vai no corpo do createTask. WaveSpeed: o slug da página do modelo (ex: 'wavespeed-ai/flux-2-klein-9b/text-to-image'), que vai no caminho de api.wavespeed.ai/api/v3. Replicate: 'dono/nome' pra modelo oficial (ex: 'black-forest-labs/flux-schnell') ou 'dono/nome:<versão de 64 caracteres>' pra modelo de comunidade. Não invente: confira na página do modelo."),
74
104
  input: z
75
105
  .union([z.record(z.any()), z.string()])
76
106
  .optional()
@@ -108,7 +138,7 @@ export const pontesSchema = z.object({
108
138
  .string()
109
139
  .max(200)
110
140
  .optional()
111
- .describe("action=gerar: o custo pra ficha, quando você sabe ('US$ 0,35 na fal'). Omitido, a Kie informa os créditos consumidos e a fal fica como 'pago na fal com a sua chave'."),
141
+ .describe("action=gerar: o custo pra ficha, quando você sabe ('US$ 0,35 na fal'). Omitido, a Kie informa os créditos consumidos e os outros balcões ficam como 'pago na <balcão> com a sua chave'."),
112
142
  characterId: z
113
143
  .string()
114
144
  .optional()
@@ -138,12 +168,16 @@ export const pontesSchema = z.object({
138
168
  // ---------------------------------------------------------------------------
139
169
  /**
140
170
  * O valor de uma variável de chave, ou undefined quando não há chave de verdade.
141
- * Na extensão do Claude Desktop (.mcpb) a chave chega pelo manifesto como
142
- * "${user_config.fal_key}": campo opcional deixado em branco pode chegar vazio
171
+ * Na extensão do Claude Desktop (.mcpb) e no plugin do Claude Code a chave chega
172
+ * como "${user_config.fal_key}": campo opcional deixado em branco pode chegar vazio
143
173
  * OU como o próprio texto do molde, e o molde não é chave (mandar ele pro
144
174
  * provedor seria um 401 com cara de chave errada).
145
175
  */
146
176
  export function valorDeChave(nome) {
177
+ const campo = CAMPO_DO_APP[nome];
178
+ return (campo && lerChave(campo)) || lerChave(nome);
179
+ }
180
+ function lerChave(nome) {
147
181
  const valor = process.env[nome]?.trim();
148
182
  if (!valor || valor.length < 8)
149
183
  return undefined;
@@ -153,8 +187,9 @@ export function valorDeChave(nome) {
153
187
  }
154
188
  function segredosConfigurados() {
155
189
  const out = new Set();
156
- for (const nome of NOMES_DE_CHAVE) {
157
- const valor = valorDeChave(nome);
190
+ // As duas fontes de cada chave saem do texto, até a que perdeu a precedência.
191
+ for (const nome of [...NOMES_DE_CHAVE, ...Object.values(CAMPO_DO_APP)]) {
192
+ const valor = lerChave(nome);
158
193
  if (!valor)
159
194
  continue;
160
195
  out.add(valor);
@@ -178,12 +213,10 @@ function limpo(valor) {
178
213
  return JSON.parse(semChave(JSON.stringify(valor)));
179
214
  }
180
215
  function chaveDo(provedor) {
181
- const { nome, envVar } = BALCOES[provedor];
216
+ const { envVar } = BALCOES[provedor];
182
217
  const valor = valorDeChave(envVar);
183
218
  if (!valor) {
184
- throw new Error(CANAL === "mcpb"
185
- ? `A extensão do Sapiens está sem a chave da ${nome}. No Claude Desktop, abra Configurações, Extensões, Sapiens Sintéticos, cole a chave no campo da ${nome} e salve. Ela fica guardada no seu computador; nunca mande ela no chat.`
186
- : `Não achei a ${envVar} nesta máquina. Ponha a chave da ${nome} no "env" do sapiens, no config do seu cliente MCP, e reinicie o cliente: ${ENDERECO_SUAS_CHAVES}. A chave fica no seu computador; nunca mande ela no chat.`);
219
+ throw new Error(`Falta a chave ${balcaoCom(provedor, "de")} (${envVar}) nesta máquina. ${ondeFicaAChave()} A chave fica no seu computador; nunca mande ela no chat.`);
187
220
  }
188
221
  return valor;
189
222
  }
@@ -209,7 +242,7 @@ function confereSemChaveNoPedido(input, prompt) {
209
242
  const vazou = segredosConfigurados().some((s) => corpo.includes(s));
210
243
  const campo = campoDeChave(input);
211
244
  if (vazou || campo) {
212
- throw new Error(`${campo ? `O input tem um campo de chave ("${campo}")` : "Tem uma chave de API dentro do input ou do prompt"}. A chave não vai no pedido: ela mora no env do config do seu cliente MCP e esta tool lê de lá. Tire do input e, se a chave foi colada no chat, gere uma nova no provedor.`);
245
+ throw new Error(`${campo ? `O input tem um campo de chave ("${campo}")` : "Tem uma chave de API dentro do input ou do prompt"}. A chave não vai no pedido: ela mora nesta máquina (campo do plugin, da extensão ou env do cliente MCP) e esta tool lê de lá. Tire do input e, se a chave foi colada no chat, gere uma nova no provedor.`);
213
246
  }
214
247
  }
215
248
  // ---------------------------------------------------------------------------
@@ -275,26 +308,30 @@ function canonico(v) {
275
308
  function hashDoPedido(provedor, modelo, input) {
276
309
  return createHash("sha256").update(`${provedor}\n${modelo}\n${canonico(input)}`).digest("hex");
277
310
  }
278
- // Slug que vai no caminho da URL (fal) ou no corpo (Kie): letras, números,
279
- // ponto, hífen, sublinhado e barra entre partes. Sem "..", sem esquema, sem
280
- // barra na frente: o host é fixo e o caminho não passeia.
311
+ // Slug que vai no caminho da URL (fal, WaveSpeed, Replicate) ou no corpo (Kie):
312
+ // letras, números, ponto, hífen, sublinhado e barra entre partes. Sem "..", sem
313
+ // esquema, sem barra na frente: o host é fixo e o caminho não passeia.
281
314
  const MODELO_VALIDO = /^[a-z0-9][a-z0-9._-]*(\/[a-z0-9][a-z0-9._-]*)*$/i;
282
- function confereModelo(modelo) {
283
- if (!MODELO_VALIDO.test(modelo) || modelo.includes("..")) {
284
- throw new Error(`modelo inválido: "${modelo.slice(0, 80)}". Use o slug como está na página do modelo no provedor (ex: 'fal-ai/flux/dev', 'kling/v2-1-standard').`);
315
+ // Replicate: "dono/nome" (modelo oficial) ou "dono/nome:<versão>" (comunidade).
316
+ const MODELO_REPLICATE = /^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*(:[a-f0-9]{64})?$/i;
317
+ function confereModelo(provedor, modelo) {
318
+ const forma = provedor === "replicate" ? MODELO_REPLICATE : MODELO_VALIDO;
319
+ if (!forma.test(modelo) || modelo.includes("..")) {
320
+ throw new Error(`modelo inválido: "${modelo.slice(0, 80)}". Use o slug como está na página do modelo no provedor (ex: 'fal-ai/flux/dev', 'kling/v2-1-standard', 'wavespeed-ai/flux-2-klein-9b/text-to-image', 'black-forest-labs/flux-schnell').`);
285
321
  }
286
322
  }
287
323
  /** O nome do motor na ficha: sempre com o prefixo do provedor de verdade. */
288
324
  function motorDaFicha(provedor, modelo, pedido) {
289
325
  const limpa = (s) => s
290
326
  .toLowerCase()
291
- .replace(/^fal-ai\//, "")
327
+ .replace(/^(fal-ai|wavespeed-ai)\//, "")
292
328
  .replace(/[\s/]+/g, "-")
293
329
  .replace(/[^a-z0-9._-]/g, "")
294
330
  .replace(/-{2,}/g, "-")
295
331
  .replace(/^-|-$/g, "")
296
332
  .slice(0, 80);
297
- const base = pedido?.trim() ? limpa(pedido) : limpa(modelo);
333
+ // A versão do Replicate (64 caracteres) identifica, mas não é nome de motor.
334
+ const base = pedido?.trim() ? limpa(pedido) : limpa(modelo.split(":")[0]);
298
335
  if (base.startsWith(`${provedor}-`))
299
336
  return base;
300
337
  return `${provedor}-${base || "modelo"}`;
@@ -384,18 +421,27 @@ function urlsDeMidia(v, out = [], fundo = 0) {
384
421
  }
385
422
  // --- fal: fila em queue.fal.run ---
386
423
  const FAL_FILA = "https://queue.fal.run";
387
- /** Só manda a chave de volta pro host da fila da fal, nunca pra URL de outro host. */
388
- function urlDaFilaFal(u) {
424
+ /**
425
+ * A URL de consulta que o provedor devolveu, só se for https no host dele: a
426
+ * chave vai junto nessa consulta, então URL de outro host nunca é seguida.
427
+ */
428
+ function urlNoHost(u, host) {
389
429
  if (typeof u !== "string")
390
430
  return null;
391
431
  try {
392
432
  const url = new URL(u);
393
- return url.protocol === "https:" && url.hostname === "queue.fal.run" ? url.toString() : null;
433
+ return url.protocol === "https:" && url.hostname === host ? url.toString() : null;
394
434
  }
395
435
  catch {
396
436
  return null;
397
437
  }
398
438
  }
439
+ const urlDaFilaFal = (u) => urlNoHost(u, "queue.fal.run");
440
+ /** As URLs https de uma saída que é string solta ou lista de strings. */
441
+ function urlsSoltas(v) {
442
+ const lista = Array.isArray(v) ? v : [v];
443
+ return lista.filter((u) => typeof u === "string" && u.startsWith("https://"));
444
+ }
399
445
  async function falEnviar(chave, modelo, input) {
400
446
  const res = await fetch(`${FAL_FILA}/${modelo}`, {
401
447
  method: "POST",
@@ -532,6 +578,113 @@ async function kieLer(chave, job) {
532
578
  }
533
579
  return { estado: d.state === "generating" ? "rodando" : "fila" };
534
580
  }
581
+ // --- WaveSpeed: POST no slug + predictions/<id>/result (o par do sapiens_video) ---
582
+ const WAVESPEED_API = "https://api.wavespeed.ai/api/v3";
583
+ async function wavespeedEnviar(chave, modelo, input) {
584
+ const res = await fetch(`${WAVESPEED_API}/${modelo}`, {
585
+ method: "POST",
586
+ headers: { Authorization: `Bearer ${chave}`, "Content-Type": "application/json" },
587
+ body: JSON.stringify(input),
588
+ signal: AbortSignal.timeout(30_000),
589
+ });
590
+ const json = await res.json().catch(() => null);
591
+ const id = json?.data?.id;
592
+ if (!res.ok || typeof id !== "string") {
593
+ throw new Error(`A WaveSpeed recusou o pedido (HTTP ${res.status}): ${detalheDoErro(json) || "resposta sem detalhe"}. ` +
594
+ (res.ok ? "Confira no painel da WaveSpeed se o pedido entrou antes de repetir." : "Nada entrou na fila."));
595
+ }
596
+ return {
597
+ requestId: id,
598
+ statusUrl: urlNoHost(json.data.urls?.get, "api.wavespeed.ai") ??
599
+ `${WAVESPEED_API}/predictions/${encodeURIComponent(id)}/result`,
600
+ };
601
+ }
602
+ async function wavespeedLer(chave, job) {
603
+ const aindaNao = { estado: job.estado === "rodando" ? "rodando" : "fila" };
604
+ const statusUrl = urlNoHost(job.statusUrl, "api.wavespeed.ai");
605
+ if (!statusUrl)
606
+ return { estado: "falhou", erro: "job sem endereço de consulta da WaveSpeed" };
607
+ let res;
608
+ try {
609
+ res = await fetch(statusUrl, {
610
+ headers: { Authorization: `Bearer ${chave}` },
611
+ signal: AbortSignal.timeout(20_000),
612
+ });
613
+ }
614
+ catch {
615
+ return aindaNao;
616
+ }
617
+ if (!res.ok)
618
+ return aindaNao;
619
+ const json = await res.json().catch(() => null);
620
+ const d = json?.data ?? json;
621
+ if (d?.status === "failed") {
622
+ return { estado: "falhou", erro: detalheDoErro(d) || "a WaveSpeed marcou falha sem motivo" };
623
+ }
624
+ if (d?.status !== "completed")
625
+ return { estado: d?.status === "processing" ? "rodando" : "fila" };
626
+ const urls = urlsSoltas(d.outputs);
627
+ if (!urls.length)
628
+ return { estado: "falhou", erro: "a WaveSpeed terminou mas não devolveu arquivo" };
629
+ return { estado: "pronto", urls, custo: "pago na WaveSpeed com a sua chave" };
630
+ }
631
+ // --- Replicate: predictions; modelo oficial pelo nome, comunidade pela versão ---
632
+ const REPLICATE_API = "https://api.replicate.com/v1";
633
+ async function replicateEnviar(chave, modelo, input) {
634
+ // Modelo de comunidade só roda por /predictions com a versão completa: o
635
+ // atalho /models/<dono>/<nome>/predictions serve só modelo oficial (404).
636
+ const [nome, versao] = modelo.split(":");
637
+ const res = await fetch(versao ? `${REPLICATE_API}/predictions` : `${REPLICATE_API}/models/${nome}/predictions`, {
638
+ method: "POST",
639
+ headers: { Authorization: `Bearer ${chave}`, "Content-Type": "application/json" },
640
+ body: JSON.stringify(versao ? { version: versao, input } : { input }),
641
+ signal: AbortSignal.timeout(30_000),
642
+ });
643
+ const json = await res.json().catch(() => null);
644
+ if (!res.ok || typeof json?.id !== "string") {
645
+ throw new Error(`O Replicate recusou o pedido (HTTP ${res.status}): ${detalheDoErro(json) || "resposta sem detalhe"}. ` +
646
+ (res.ok ? "Confira no painel do Replicate se o pedido entrou antes de repetir." : "Nada entrou na fila."));
647
+ }
648
+ return {
649
+ requestId: json.id,
650
+ statusUrl: urlNoHost(json.urls?.get, "api.replicate.com") ?? `${REPLICATE_API}/predictions/${encodeURIComponent(json.id)}`,
651
+ };
652
+ }
653
+ async function replicateLer(chave, job) {
654
+ const aindaNao = { estado: job.estado === "rodando" ? "rodando" : "fila" };
655
+ const statusUrl = urlNoHost(job.statusUrl, "api.replicate.com");
656
+ if (!statusUrl)
657
+ return { estado: "falhou", erro: "job sem endereço de consulta do Replicate" };
658
+ let res;
659
+ try {
660
+ res = await fetch(statusUrl, {
661
+ headers: { Authorization: `Bearer ${chave}` },
662
+ signal: AbortSignal.timeout(20_000),
663
+ });
664
+ }
665
+ catch {
666
+ return aindaNao;
667
+ }
668
+ if (!res.ok)
669
+ return aindaNao;
670
+ const p = await res.json().catch(() => null);
671
+ if (p?.status === "failed" || p?.status === "canceled") {
672
+ return { estado: "falhou", erro: String(p.error || `o Replicate marcou ${p.status}`).slice(0, 400) };
673
+ }
674
+ if (p?.status !== "succeeded")
675
+ return { estado: p?.status === "processing" ? "rodando" : "fila" };
676
+ // A saída do Replicate expira em 1 hora: o ingest do mesmo turno importa aqui.
677
+ const urls = urlsSoltas(p.output);
678
+ if (!urls.length)
679
+ return { estado: "falhou", erro: "o Replicate terminou mas não devolveu arquivo" };
680
+ return { estado: "pronto", urls, custo: "pago no Replicate com a sua chave" };
681
+ }
682
+ const ADAPTADORES = {
683
+ fal: { enviar: falEnviar, ler: falLer, intervaloMs: 2500 },
684
+ kie: { enviar: kieEnviar, ler: kieLer, intervaloMs: 3000 },
685
+ wavespeed: { enviar: wavespeedEnviar, ler: wavespeedLer, intervaloMs: 2500 },
686
+ replicate: { enviar: replicateEnviar, ler: replicateLer, intervaloMs: 2500 },
687
+ };
535
688
  // ---------------------------------------------------------------------------
536
689
  // O laço: consultar, e quando pronto, trazer pro acervo.
537
690
  // ---------------------------------------------------------------------------
@@ -599,12 +752,11 @@ async function ingerirPecas(job) {
599
752
  async function avancar(job, segundos) {
600
753
  const inicio = Date.now();
601
754
  const prazo = inicio + Math.max(0, Math.min(segundos, ESPERA_MAX_S)) * 1000;
602
- const intervalo = job.provedor === "kie" ? 3000 : 2500;
755
+ const { ler, intervaloMs: intervalo } = ADAPTADORES[job.provedor];
603
756
  let atual = job;
604
757
  for (;;) {
605
758
  if (atual.estado === "fila" || atual.estado === "rodando") {
606
- const chave = chaveDo(atual.provedor);
607
- const leitura = atual.provedor === "fal" ? await falLer(chave, atual) : await kieLer(chave, atual);
759
+ const leitura = await ler(chaveDo(atual.provedor), atual);
608
760
  atual = aplicar(atual, leitura);
609
761
  await salvarJob(atual);
610
762
  }
@@ -624,11 +776,11 @@ async function avancar(job, segundos) {
624
776
  // O que volta pro agente.
625
777
  // ---------------------------------------------------------------------------
626
778
  function proximoPasso(job) {
627
- const nome = BALCOES[job.provedor].nome;
779
+ const p = job.provedor;
628
780
  switch (job.estado) {
629
781
  case "fila":
630
782
  case "rodando":
631
- return `Ainda renderizando na ${nome}. Chame sapiens_pontes action=status jobId=${job.id} (aguardarSegundos até ${ESPERA_MAX_S}). NÃO chame gerar de novo: isso cria outro job e cobra outra vez na conta da pessoa.`;
783
+ return `Ainda renderizando ${balcaoCom(p, "em")}. Chame sapiens_pontes action=status jobId=${job.id} (aguardarSegundos até ${ESPERA_MAX_S}). NÃO chame gerar de novo: isso cria outro job e cobra outra vez na conta da pessoa.`;
632
784
  case "pronto":
633
785
  if (!job.ingerir) {
634
786
  return "Pronto no provedor, fora do acervo (ingerir=false). A URL do render expira: pra trazer, chame action=status com ingerir=true.";
@@ -639,7 +791,7 @@ function proximoPasso(job) {
639
791
  case "ingerido":
640
792
  return "No acervo, privada e com custo 0 em Sinapses. Mostre com sapiens_gallery action=list (kind=video pra vídeo). Publicar é ato da pessoa (sapiens_gallery action=publish).";
641
793
  case "falhou":
642
- return `A ${nome} marcou falha. Confira o painel da ${nome} antes de tentar de novo (a regra de cobrança de job que falha é deles).`;
794
+ return `${balcaoCom(p, "")} marcou falha. Confira o painel ${balcaoCom(p, "de")} antes de tentar de novo (a regra de cobrança de job que falha é deles).`;
643
795
  }
644
796
  }
645
797
  function resumo(job, extra = {}) {
@@ -653,7 +805,7 @@ function resumo(job, extra = {}) {
653
805
  resultUrls: job.estado === "pronto" ? job.resultUrls : undefined,
654
806
  custo: job.custo,
655
807
  erro: job.erro,
656
- chave: `A chave ficou nesta máquina: o pedido saiu daqui direto pra ${BALCOES[job.provedor].nome}. A casa não viu a chave.`,
808
+ chave: `A chave ficou nesta máquina: o pedido saiu daqui direto ${balcaoCom(job.provedor, "para")}. A casa não viu a chave.`,
657
809
  proximoPasso: proximoPasso(job),
658
810
  ...extra,
659
811
  };
@@ -664,12 +816,12 @@ function resumo(job, extra = {}) {
664
816
  async function gerar(args) {
665
817
  const provedor = args.provedor;
666
818
  if (!provedor)
667
- throw new Error("action=gerar exige provedor: 'fal' ou 'kie'.");
819
+ throw new Error(`action=gerar exige provedor: ${PROVEDORES.map((p) => `'${p}'`).join(", ")}.`);
668
820
  const modelo = args.modelo?.trim();
669
821
  if (!modelo) {
670
822
  throw new Error("action=gerar exige modelo: o slug do modelo no provedor, igual à página dele.");
671
823
  }
672
- confereModelo(modelo);
824
+ confereModelo(provedor, modelo);
673
825
  const chave = chaveDo(provedor);
674
826
  const input = lerInput(args.input);
675
827
  confereSemChaveNoPedido(input, args.prompt);
@@ -700,7 +852,7 @@ async function gerar(args) {
700
852
  }
701
853
  const promptDaFicha = args.prompt?.trim() ||
702
854
  (typeof input.prompt === "string" && input.prompt.trim() ? input.prompt : JSON.stringify(input).slice(0, 4000));
703
- const enviado = provedor === "fal" ? await falEnviar(chave, modelo, input) : await kieEnviar(chave, modelo, input);
855
+ const enviado = await ADAPTADORES[provedor].enviar(chave, modelo, input);
704
856
  const agora = Date.now();
705
857
  const job = {
706
858
  id: novoId(),
@@ -745,18 +897,17 @@ async function status(args) {
745
897
  }
746
898
  function balcoes() {
747
899
  return {
748
- regra: "A chave mora no env do config do seu cliente MCP, nesta máquina, e sai daqui só pro provedor. A casa não recebe, não guarda e não vê. Gerar com a sua chave custa zero em Sinapses: você paga o preço do provedor, sem margem da casa.",
749
- balcoes: Object.keys(BALCOES).map((k) => ({
900
+ regra: "A chave mora nesta máquina e sai daqui só pro provedor. A casa não recebe, não guarda e não vê. Gerar com a sua chave custa zero em Sinapses: você paga o preço do provedor, sem margem da casa.",
901
+ balcoes: PROVEDORES.map((k) => ({
750
902
  provedor: k,
751
903
  nome: BALCOES[k].nome,
752
904
  variavel: BALCOES[k].envVar,
753
905
  configurada: Boolean(valorDeChave(BALCOES[k].envVar)),
754
906
  })),
755
- comoConfigurar: CANAL === "mcpb"
756
- ? "Na extensão do Claude Desktop: Configurações, Extensões, Sapiens Sintéticos. Os campos da fal e da Kie ficam ali, e o que você colar fica guardado no seu computador."
757
- : `No config do cliente MCP, no bloco do sapiens: "env": { "FAL_KEY": "<sua chave da fal>", "KIE_API_KEY": "<sua chave da Kie>" }. Depois reinicie o cliente. No Claude Desktop, a extensão (.mcpb) põe a chave num campo, sem editar config. Passo a passo por cliente: ${ENDERECO_SUAS_CHAVES}`,
758
- ondeAcharOModelo: "fal: o id do endpoint na página do modelo em fal.ai/models (ex: 'fal-ai/flux/dev'). Kie: o 'model' da página do modelo no Market da Kie. O input é o corpo que a mesma página descreve.",
759
- outrosBalcoes: "Sogni, Krea, Magnific, WaveSpeed, Replicate e a própria placa seguem pelo seu harness + sapiens_gallery action=ingest (catálogo em sapiens_meta action=pontes).",
907
+ canal: CANAL,
908
+ comoConfigurar: ondeFicaAChave(),
909
+ ondeAcharOModelo: "fal: o id do endpoint na página do modelo em fal.ai/models (ex: 'fal-ai/flux/dev'). Kie: o 'model' da página do modelo no Market da Kie. WaveSpeed: o slug da página do modelo em wavespeed.ai/models (ex: 'wavespeed-ai/flux-2-klein-9b/text-to-image'). Replicate: 'dono/nome' do modelo oficial (ex: 'black-forest-labs/flux-schnell'), ou 'dono/nome:<versão>' pra modelo de comunidade. O input é o corpo que a mesma página descreve.",
910
+ outrosBalcoes: "Sogni, Krea, Magnific e a própria placa seguem pelo seu harness + sapiens_gallery action=ingest (catálogo em sapiens_meta action=pontes). Balcão novo na porta local entra por código, não por campo livre: cada chave só vai pro endereço do próprio provedor.",
760
911
  };
761
912
  }
762
913
  function jobs(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapiens-mcp",
3
- "version": "1.80.0",
3
+ "version": "1.82.0",
4
4
  "mcpName": "com.sapiensinteticos/sapiens",
5
5
  "description": "MCP server pra operar o Sapiens Sintéticos (sapiensinteticos.com) pelo Claude Code: gerar imagem, escrever artigo, voz, música e mais, na sua conta. Login pelo código de sapiensinteticos.com/conectar-claude.",
6
6
  "type": "module",