primocode 8.38.0 → 8.39.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/app/app.js CHANGED
@@ -316,12 +316,16 @@
316
316
  marcarEscolha("cores", "cor", cor);
317
317
  }
318
318
 
319
+ /* A LOGO — a mesma do ícone do sistema e da barra lateral do Conecta. Onde
320
+ o produto se apresenta vai ela; o mascote em pixel fica para onde ele é
321
+ personagem. */
319
322
  function desenharMarca() {
320
323
  if (!window.PrimoMarca) return;
321
324
  var t = document.getElementById("marcaTopo");
322
325
  var g = document.getElementById("marcaGrande");
323
- if (t) t.innerHTML = window.PrimoMarca.svg({ tamanho: 22 });
324
- if (g) g.innerHTML = window.PrimoMarca.svg({ tamanho: 82, rotulo: "PrimoCode" });
326
+ var desenha = window.PrimoMarca.logo || window.PrimoMarca.svg;
327
+ if (t) t.innerHTML = desenha({ tamanho: 22 });
328
+ if (g) g.innerHTML = desenha({ tamanho: 82, rotulo: "PrimoCode" });
325
329
  }
326
330
 
327
331
  function quando(iso) {
@@ -1,12 +1,6 @@
1
1
  /**
2
2
  * primo-marca.js — o mascote do PrimoCode, desenhado em pixel.
3
3
  *
4
- * CÓPIA de conecta-primo-ai/frontend/primo-marca.js. São repositórios
5
- * diferentes e não há como importar de um para o outro; o que dá para fazer é
6
- * manter os dois IDÊNTICOS e o teste conferir isso — um mascote que muda num
7
- * lado e não no outro é a marca do produto se partindo em duas sem ninguém
8
- * ver.
9
- *
10
4
  * ── POR QUE MATRIZ, E NÃO O PNG ──────────────────────────────────────────
11
5
  * O logo veio como imagem. Guardar o PNG funcionaria, mas ele já é pixel art:
12
6
  * redesenhá-lo como MATRIZ dá o mesmo desenho nítido em qualquer tamanho —
@@ -103,11 +97,39 @@
103
97
  + '" height="' + lado + '"' + acessivel + ">" + partes.join("") + "</svg>";
104
98
  }
105
99
 
106
- /** O mesmo bicho como favicon, sem pedir arquivo ao servidor. */
100
+ /* A LOGO — a que o dono mandou, e a mesma do ícone do sistema
101
+ (`app/icone.svg` no PrimoCode): quadrado escuro arredondado, o chevron do
102
+ prompt e o sublinhado do cursor, em ciano. Onde o produto se apresenta
103
+ (botão da barra lateral, cabeçalho, favicon) vai ELA; o mascote em pixel
104
+ acima fica para onde ele é personagem, não marca. Foi por confundir os
105
+ dois que a barra lateral mostrou o bicho onde devia estar a logo. */
106
+ var LOGO_FUNDO = "#0a0f16";
107
+ var LOGO_TRACO = "#22d3ee";
108
+
109
+ /**
110
+ * A logo como SVG.
111
+ * @param {object} op
112
+ * @param {number} [op.tamanho] lado em px
113
+ * @param {string} [op.rotulo] texto alternativo; vazio = decorativo
114
+ */
115
+ function logo(op) {
116
+ op = op || {};
117
+ var lado = op.tamanho || 32;
118
+ var acessivel = op.rotulo
119
+ ? ' role="img" aria-label="' + String(op.rotulo).replace(/"/g, "&quot;") + '"'
120
+ : ' aria-hidden="true"';
121
+ return '<svg viewBox="0 0 32 32" width="' + lado + '" height="' + lado + '"' + acessivel + ">"
122
+ + '<rect width="32" height="32" rx="7" fill="' + LOGO_FUNDO + '"/>'
123
+ + '<path d="M8 10.5 13 16l-5 5.5" fill="none" stroke="' + LOGO_TRACO
124
+ + '" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>'
125
+ + '<path d="M16.5 22H24" fill="none" stroke="' + LOGO_TRACO + '" stroke-width="2.6" stroke-linecap="round"/>'
126
+ + "</svg>";
127
+ }
128
+
129
+ /** A logo como favicon, sem pedir arquivo ao servidor. */
107
130
  function favicon() {
108
- var s = svg({ tamanho: 32, fundo: "#0a0f16" });
109
- return "data:image/svg+xml," + encodeURIComponent(s);
131
+ return "data:image/svg+xml," + encodeURIComponent(logo({ tamanho: 32 }));
110
132
  }
111
133
 
112
- raiz.PrimoMarca = { svg: svg, favicon: favicon, MATRIZ: MATRIZ, CORPO: CORPO, DETALHE: DETALHE };
134
+ raiz.PrimoMarca = { svg: svg, logo: logo, favicon: favicon, MATRIZ: MATRIZ, CORPO: CORPO, DETALHE: DETALHE };
113
135
  })(typeof window !== "undefined" ? window : globalThis);
package/bin/primocode.js CHANGED
@@ -1269,26 +1269,33 @@ async function handleSlash(text) {
1269
1269
  abrir: (u) => tools.openInBrowser(u),
1270
1270
  contar: (info) => {
1271
1271
  sp.stop();
1272
- /* O código continua aparecendo — grande, partido em dois,
1273
- porque é o que se lê de uma tela e se confere noutra. Mas
1274
- agora ele já FOI para a página: o CLI serve o código em
1275
- 127.0.0.1 e a página pergunta ali; e foi para a área de
1276
- transferência, para o "Colar" de um toque onde o
1277
- navegador bloqueia o 127.0.0.1 (Safari). Digitar virou a
1278
- terceira saída, não a primeira. */
1279
1272
  const meio = info.codigo.length === 8
1280
1273
  ? info.codigo.slice(0, 4) + '-' + info.codigo.slice(4) : info.codigo;
1281
1274
  console.log(c.muted(' Abrindo no navegador:'));
1282
1275
  console.log(' ' + c.brand(info.url.replace(/[?&]porta=\d+/, '')));
1283
1276
  console.log('');
1284
- console.log(' ' + c.bold(c.brand(meio.split('').join(' '))));
1285
- console.log('');
1286
- const como = [];
1287
- if (info.porta) como.push('a página preenche sozinha');
1288
- if (info.copiado) como.push('e o código já está copiado');
1289
- console.log(c.dim(' ' + (como.length ? como.join(', ').replace(/^e /, '') + '.'
1290
- : 'Digite o código na página.')));
1291
- console.log(c.dim(' Esperando você aprovar…'));
1277
+ if (info.porta) {
1278
+ /* COM O SERVIDOR LOCAL DE PÉ, NÃO HÁ CÓDIGO PARA LER. A
1279
+ página pergunta ao 127.0.0.1 desta máquina, recebe o
1280
+ código e aprova sozinha assim que a pessoa entra com a
1281
+ conta — "não precisa colocar código, não precisa pra
1282
+ nada, porque ele já libera". Mostrar as oito letras
1283
+ grandes aqui era pedir para ler o que ninguém precisa
1284
+ mais; elas ficam numa linha apagada, só para o caso
1285
+ de o navegador bloquear o 127.0.0.1 (Safari) e a
1286
+ página voltar a pedir. */
1287
+ console.log(' ' + c.white('Entre com a sua conta na página que abriu — o resto é automático.'));
1288
+ console.log(c.dim(` Se a página pedir um código: ${meio}${info.copiado ? ' (já copiado)' : ''}.`));
1289
+ } else {
1290
+ /* Sem porta local (ocupadas, ou bloqueadas), o código
1291
+ volta a ser o caminho — grande, partido em dois,
1292
+ porque é o que se lê de uma tela e se confere noutra. */
1293
+ console.log(' ' + c.bold(c.brand(meio.split('').join(' '))));
1294
+ console.log('');
1295
+ console.log(c.dim(' ' + (info.copiado ? 'O código já está copiado — cole na página.'
1296
+ : 'Digite o código na página.')));
1297
+ }
1298
+ console.log(c.dim(' Esperando o navegador…'));
1292
1299
  },
1293
1300
  });
1294
1301
  sp.stop();
package/lib/act.js CHANGED
@@ -19,7 +19,13 @@ const cfg = require('./config');
19
19
  const catalogo = require('./catalogo');
20
20
  const repeticao = require('./repeticao');
21
21
 
22
- const MAX_ITERATIONS = 15;
22
+ // 40, não 15: com o freio de repetição (repeticao.js) e o de rodada parada,
23
+ // volta a mais não é volta em círculo — é trabalho. Quinze acabavam no meio
24
+ // de um vídeo de dez cenas com pesquisa, e a pessoa lia "gastei as voltas".
25
+ const MAX_ITERATIONS = Number(process.env.PRIMOCODE_MAX_VOLTAS || 40);
26
+ // Quantas vezes o agente é EMPURRADO quando para no meio para narrar
27
+ // ("agora vou criar o arquivo…") em vez de fazer. Ver `pareceParadaNoMeio`.
28
+ const MAX_EMPURROES = 3;
23
29
  // Voltas seguidas sem executar nada antes de desistir.
24
30
  const MAX_RODADAS_PARADAS = 3;
25
31
  const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
@@ -182,6 +188,20 @@ function logResult(kind, result, profundidade) {
182
188
  }
183
189
  return;
184
190
  }
191
+ case 'buscar_audio': {
192
+ console.log(p + c.muted(`${result.tipo === 'efeito' ? 'efeito' : 'música'}: `) + c.white(`${result.titulo}`)
193
+ + c.muted(` — ${result.autor || 'autor desconhecido'}${result.duracao ? ` · ${result.duracao}s` : ''} · ${result.licenca}`));
194
+ console.log(p + c.dim(`${result.url}`));
195
+ return;
196
+ }
197
+ case 'marca_da_empresa': {
198
+ const cores = (result.cores && result.cores.paleta || []).slice(0, 4).join(' ');
199
+ console.log(p + c.white(result.nome || '') + c.muted(` · ${result.site || ''}`));
200
+ console.log(p + c.dim(`cor ${(result.cores && result.cores.principal) || '—'}${cores ? ` · paleta ${cores}` : ''}`
201
+ + `${(result.fontes || []).length ? ` · fonte ${result.fontes[0]}` : ''}`
202
+ + `${result.logo ? ` · logo ${result.logo.formato} (${result.logo.fonte})` : ' · sem logo'}`));
203
+ return;
204
+ }
185
205
  case 'desktop_windows': {
186
206
  console.log(p + c.muted(`${result.total} janela${result.total === 1 ? '' : 's'}`));
187
207
  for (const j of (result.janelas || []).slice(0, 5)) console.log(p + c.dim(`${j.app ? j.app + ' · ' : ''}${j.titulo}`.slice(0, 120)));
@@ -338,6 +358,39 @@ async function pausar(ms, subagent) {
338
358
  if (sp) sp.stop();
339
359
  }
340
360
 
361
+ /**
362
+ * O modelo devolveu TEXTO, sem tool e sem finish. Isso é resposta — ou é o
363
+ * agente parando no meio para narrar o que ia fazer?
364
+ *
365
+ * Reclamação do dono: "ele tá parando sem concluir, ele deve continuar até o
366
+ * fim". Medido: o modelo escreve "Agora vou criar o vídeo com as cenas…" e
367
+ * para, porque texto sem tool encerra a rodada. Quem lê vê uma promessa no
368
+ * lugar da entrega. Três sinais dizem que foi parada, não resposta:
369
+ *
370
+ * 1. Há um plano (write_todos) com item por fazer — ele mesmo disse que
371
+ * não acabou.
372
+ * 2. O texto é INTENÇÃO: começa ou termina anunciando o próximo passo
373
+ * ("vou", "em seguida", "próximo passo", dois-pontos no fim, reticências).
374
+ * 3. É curto. Uma resposta de verdade a "explique este erro" tem corpo; um
375
+ * "vou abrir o arquivo agora" não tem.
376
+ *
377
+ * Pergunta ao usuário NÃO conta como parada: o prompt proíbe perguntar, mas
378
+ * se o modelo perguntou, empurrá-lo faria ele decidir sozinho o que talvez
379
+ * não devesse — e o texto de conversa ("oi, sou o PrimoCode") também não é
380
+ * parada. Só intenção declarada e plano aberto.
381
+ */
382
+ const INTENCAO = /(^|[.!\n]\s*)(agora\s+)?(vou|irei|deixa eu|deixe-me|preciso (agora )?(de )?(criar|ler|abrir|rodar|ver|conferir|pesquisar|mandar|fazer|gerar|escrever|ajustar)|primeiro (vou|eu)|em seguida|pr[óo]ximo passo|a seguir,?\s+(vou|eu)|vamos (criar|ler|abrir|rodar|ver|conferir|come[çc]ar|fazer|gerar|escrever))\b/i;
383
+ function pareceParadaNoMeio(texto, { todosPendentes = 0, rodouTool = false } = {}) {
384
+ const t = String(texto || '').trim();
385
+ if (!t) return false;
386
+ if (todosPendentes > 0) return true;
387
+ if (t.length > 700) return false; // resposta com corpo
388
+ if (/\?\s*$/.test(t)) return false; // perguntou: não é parada
389
+ if (INTENCAO.test(t)) return true;
390
+ if (rodouTool && /[:…]\s*$|\.\.\.\s*$/.test(t)) return true; // "Criando as cenas:" e nada depois
391
+ return false;
392
+ }
393
+
341
394
  function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
342
395
  if (falhasPendentes.length) {
343
396
  return `Parei sem concluir. A última falha foi: ${falhasPendentes[falhasPendentes.length - 1].texto}`;
@@ -391,6 +444,13 @@ async function runAct(userPrompt, opts) {
391
444
  // assim que "crie um vídeo para a tostudy" morreu sem entregar nada.
392
445
  let pesquisasSecas = 0;
393
446
  let rodadasParadas = 0;
447
+ // Para o `pareceParadaNoMeio`: alguma tool já rodou nesta tarefa, e o
448
+ // último plano que o modelo escreveu (write_todos) — desta tarefa, não da
449
+ // sessão: um plano velho de um pedido interrompido não pode empurrar o
450
+ // pedido seguinte.
451
+ let rodouTool = false;
452
+ let todosDaTarefa = [];
453
+ let empurroes = 0;
394
454
 
395
455
  const messages = [];
396
456
  if (!subagent) {
@@ -579,6 +639,29 @@ async function runAct(userPrompt, opts) {
579
639
  iteration--;
580
640
  continue;
581
641
  }
642
+ // PAROU NO MEIO? Texto de intenção ("agora vou criar…") ou plano
643
+ // com item aberto não é entrega. Em vez de encerrar, a tarefa é
644
+ // devolvida: "isso foi uma promessa — faça". Três empurrões; se
645
+ // depois disso ele insistir em só falar, a resposta é dele.
646
+ const pendentes = todosDaTarefa.filter((t) => t.status !== 'completed').length;
647
+ if (finalText && empurroes < MAX_EMPURROES && !PARAR
648
+ && pareceParadaNoMeio(finalText, { todosPendentes: pendentes, rodouTool })) {
649
+ empurroes++;
650
+ if (!subagent) {
651
+ if (fluxo) fluxo.fechar();
652
+ else fala.pensamento(finalText, profundidade);
653
+ }
654
+ messages.push({ role: 'assistant', content: finalText });
655
+ messages.push({
656
+ role: 'user',
657
+ content: 'Isso foi uma intenção, não uma entrega: você parou no meio. Não narre o próximo '
658
+ + 'passo — CHAME a tool agora e siga até o fim'
659
+ + (pendentes ? `, o seu plano ainda tem ${pendentes} item(ns) por fazer` : '')
660
+ + '. Só pare quando tudo estiver feito, com finish("o que foi feito").',
661
+ });
662
+ finalText = '';
663
+ continue;
664
+ }
582
665
  if (!subagent) {
583
666
  if (fluxo) fluxo.fechar();
584
667
  else fala.resposta(finalText);
@@ -711,9 +794,11 @@ async function runAct(userPrompt, opts) {
711
794
  }
712
795
 
713
796
  algoRodou = true;
797
+ rodouTool = true;
714
798
  fala.acao(tc.name, tc.arguments, profundidade);
715
799
  const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
716
800
  logResult(tc.name, result, profundidade);
801
+ if (tc.name === 'write_todos' && result && Array.isArray(result.todos)) todosDaTarefa = result.todos;
717
802
 
718
803
  // Uma escrita bem-sucedida limpa a dívida — mas só a do MESMO
719
804
  // arquivo. Escrever algo sem relação (ou um run_shell qualquer)
@@ -910,4 +995,4 @@ async function rodarPipeline(args, ctx) {
910
995
  }
911
996
 
912
997
  module.exports = { runAct, interromper, compactar, motivoDaParada, ehTetoDeToken, ehTransitorio, esperaDoErro,
913
- anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
998
+ anotacaoDoQueJaFoi, encolherParaContinuar, pareceParadaNoMeio, MAX_ITERATIONS, MAX_EMPURROES };
@@ -0,0 +1,172 @@
1
+ /**
2
+ * audio-livre.js — música e efeitos sonoros da internet, com licença.
3
+ *
4
+ * Pedido do dono: "selecione músicas da internet, efeitos sonoros". O que
5
+ * existia era a trilha SINTETIZADA (quatro osciladores e um ruído) e o
6
+ * arquivo do próprio usuário. Faltava o meio: uma faixa de verdade, tocada
7
+ * por gente, que ele não precise ter no disco.
8
+ *
9
+ * ── DE ONDE VEM ──────────────────────────────────────────────────────────
10
+ * Da API do Openverse (openverse.org, mantida pela WordPress Foundation):
11
+ * ela indexa áudio com licença Creative Commons de fontes abertas — música
12
+ * do Jamendo, efeitos do Freesound, arquivos da Wikimedia — e devolve, para
13
+ * cada faixa, a URL do arquivo, a licença e o crédito pronto. Sem chave, sem
14
+ * conta. Não é YouTube nem Spotify: aqui a licença permite usar, e o crédito
15
+ * vai junto para a peça não sair devendo.
16
+ *
17
+ * ── O QUE ELA DEVOLVE ────────────────────────────────────────────────────
18
+ * O arquivo já BAIXADO para a pasta do projeto e ENVIADO ao estúdio: a URL
19
+ * "/midia/…" que entra direto em `trilha.src` ou em `som: {arquivo}`, mais o
20
+ * crédito ("Título — Autor, CC BY 4.0"). A licença decide o que o agente
21
+ * tem de fazer: BY pede o crédito na peça; CC0/PDM não pede nada. Licença
22
+ * NC (não comercial) não entra por padrão — a peça pode ser um anúncio.
23
+ *
24
+ * Zero dependências: o `pegar` do referencia.js, em modo binário.
25
+ */
26
+
27
+ 'use strict';
28
+
29
+ const fs = require('fs');
30
+ const path = require('path');
31
+ const referencia = require('./referencia.js');
32
+
33
+ const API = 'https://api.openverse.org/v1/audio/';
34
+
35
+ // Licenças que permitem uso comercial sem pedir nada além do crédito.
36
+ const LIVRES = new Set(['by', 'by-sa', 'cc0', 'pdm']);
37
+ const NOME_LICENCA = {
38
+ by: 'CC BY', 'by-sa': 'CC BY-SA', 'by-nc': 'CC BY-NC', 'by-nd': 'CC BY-ND',
39
+ 'by-nc-sa': 'CC BY-NC-SA', 'by-nc-nd': 'CC BY-NC-ND', cc0: 'CC0', pdm: 'Domínio público',
40
+ };
41
+
42
+ function slug(t) {
43
+ return String(t || '').normalize('NFD').replace(/[̀-ͯ]/g, '')
44
+ .toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'audio';
45
+ }
46
+
47
+ function credito(f) {
48
+ const lic = NOME_LICENCA[f.license] || String(f.license || '').toUpperCase();
49
+ const versao = f.license_version && !/cc0|pdm/.test(f.license) ? ' ' + f.license_version : '';
50
+ return `${f.title || 'Sem título'} — ${f.creator || 'autor desconhecido'} (${lic}${versao}, via ${f.provider || 'Openverse'})`;
51
+ }
52
+
53
+ /**
54
+ * Pergunta ao Openverse. `tipo` "musica" restringe à categoria music; "efeito"
55
+ * pede coisa curta (até ~12 s). `dur` em segundos aproxima a duração desejada
56
+ * da trilha — uma faixa de 4 min para um reels de 30 s é desperdício, mas
57
+ * uma de 20 s para um vídeo de 60 s acaba antes; a nota pesa isso.
58
+ */
59
+ async function procurar({ consulta, tipo = 'musica', dur = 0, comercial = true, pagina = 1 }) {
60
+ const q = new URLSearchParams({ q: consulta, page_size: '20', page: String(pagina) });
61
+ if (tipo === 'musica') q.set('category', 'music');
62
+ if (comercial) q.set('license_type', 'commercial');
63
+ const bruto = await referencia.pegar(`${API}?${q}`, { timeout: 12000 });
64
+ if (!bruto) return { ok: false, error: 'o Openverse não respondeu (rede?)' };
65
+ let j;
66
+ try { j = JSON.parse(bruto); } catch { return { ok: false, error: 'resposta ilegível do Openverse' }; }
67
+ const itens = (j.results || []).filter((f) => f.url && LIVRES.has(String(f.license || '').toLowerCase()) || !comercial);
68
+ const alvoMs = tipo === 'efeito' ? 2500 : (dur ? dur * 1000 : 90000);
69
+ const notas = itens.map((f) => {
70
+ let n = 0;
71
+ const d = Number(f.duration) || 0;
72
+ if (tipo === 'efeito') {
73
+ if (d > 15000) n -= 50; // não é efeito, é faixa
74
+ else n += 20 - Math.abs(d - alvoMs) / 500;
75
+ } else {
76
+ if (d && d < alvoMs * 0.6) n -= 20; // acaba antes do vídeo
77
+ n -= Math.abs(d - alvoMs) / 20000; // perto da duração pedida
78
+ if (f.category === 'music') n += 5;
79
+ }
80
+ if (/cc0|pdm/.test(f.license)) n += 3; // sem crédito obrigatório
81
+ if (/mp3/i.test(f.filetype || '') || /\.mp3(\?|$)/i.test(f.url)) n += 4; // toca em qualquer lugar
82
+ // As palavras da consulta no título/tags contam.
83
+ const alvo = `${f.title} ${(f.tags || []).map((t) => t.name).join(' ')} ${(f.genres || []).join(' ')}`.toLowerCase();
84
+ for (const p of consulta.toLowerCase().split(/\s+/)) if (p.length > 2 && alvo.includes(p)) n += 3;
85
+ return { f, n };
86
+ }).sort((a, b) => b.n - a.n);
87
+ return {
88
+ ok: true,
89
+ total: j.result_count || itens.length,
90
+ itens: notas.map(({ f }) => ({
91
+ id: f.id, titulo: f.title, autor: f.creator, licenca: f.license, licencaUrl: f.license_url,
92
+ duracao: f.duration ? Math.round(f.duration / 1000) : null, url: f.url, pagina: f.foreign_landing_url,
93
+ provedor: f.provider, tipoArquivo: f.filetype || null, generos: f.genres || [], credito: credito(f),
94
+ })),
95
+ };
96
+ }
97
+
98
+ /** Baixa a faixa para <projeto>/midia/ e manda ao estúdio. */
99
+ async function trazer(item, { projectDir, studio }) {
100
+ const bytes = await referencia.pegar(item.url, { timeout: 45000, binario: true });
101
+ if (!bytes || bytes.length < 2000) return { ok: false, error: `não consegui baixar "${item.titulo}" de ${item.provedor}` };
102
+ // A extensão pelo conteúdo, não pela URL: o Jamendo entrega mp3 numa URL
103
+ // sem extensão ("?trackid=…&format=mp32").
104
+ const cab = bytes.subarray(0, 12);
105
+ const ext = cab.subarray(0, 3).toString() === 'ID3' || (cab[0] === 0xff && (cab[1] & 0xe0) === 0xe0) ? '.mp3'
106
+ : cab.subarray(0, 4).toString() === 'RIFF' ? '.wav'
107
+ : cab.subarray(0, 4).toString() === 'OggS' ? '.ogg'
108
+ : cab.subarray(0, 4).toString() === 'fLaC' ? '.flac'
109
+ : (item.tipoArquivo ? '.' + item.tipoArquivo.replace(/^\./, '') : '.mp3');
110
+ const pasta = path.join(projectDir || process.cwd(), 'midia');
111
+ fs.mkdirSync(pasta, { recursive: true });
112
+ const nome = `${slug(item.titulo)}-${String(item.id || '').slice(0, 6)}${ext}`;
113
+ const caminho = path.join(pasta, nome);
114
+ fs.writeFileSync(caminho, bytes);
115
+ // FLAC/OGG do Freesound: o navegador toca, mas o MP4 exportado quer AAC —
116
+ // o estúdio cuida disso na exportação; aqui só entregamos o arquivo.
117
+ const envio = await studio.midiaEnviar({ arquivo: caminho, nome });
118
+ if (!envio || envio.ok === false) return { ok: false, error: (envio && envio.error) || 'o estúdio não aceitou o arquivo', arquivoLocal: caminho };
119
+ return { ok: true, url: envio.url, arquivoLocal: path.relative(projectDir || process.cwd(), caminho), bytes: bytes.length };
120
+ }
121
+
122
+ /**
123
+ * A tool: procura, escolhe a melhor, baixa e devolve a URL pronta — e as
124
+ * alternativas, para o agente trocar sem pesquisar de novo.
125
+ */
126
+ async function buscarAudio(args, { projectDir, studio }) {
127
+ const consulta = String(args.consulta || args.query || '').trim();
128
+ if (!consulta) return { ok: false, error: 'Diga o que procurar: o clima da música ("upbeat corporate", "piano calmo") ou o efeito ("whoosh", "ding", "click").' };
129
+ const tipo = /^(efeito|som|sfx|sound)/i.test(String(args.tipo || '')) ? 'efeito' : 'musica';
130
+ const dur = Number(args.duracao) || 0;
131
+ const busca = await procurar({ consulta, tipo, dur, comercial: args.comercial !== false });
132
+ if (!busca.ok) return busca;
133
+ if (!busca.itens.length) {
134
+ return {
135
+ ok: false,
136
+ error: `Nenhuma faixa livre para "${consulta}".`,
137
+ comoCorrigir: tipo === 'efeito'
138
+ ? 'Use um som SINTETIZADO do estúdio, que não precisa de arquivo: som: "whoosh" | "impacto" | "click" | "pop" | "ding" | "subida" | "tique" | "brilho".'
139
+ : 'Tente a consulta em inglês e mais genérica ("upbeat electronic", "calm piano", "epic cinematic"), ou use a trilha GERADA: "trilha": {"tipo":"gerada","estilo":"pulso"}.',
140
+ };
141
+ }
142
+ // Baixa a melhor; se ela falhar, a próxima. Três tentativas.
143
+ let trazido = null;
144
+ let escolhida = null;
145
+ for (const item of busca.itens.slice(0, 3)) {
146
+ trazido = await trazer(item, { projectDir, studio });
147
+ if (trazido.ok) { escolhida = item; break; }
148
+ }
149
+ if (!trazido || !trazido.ok) return { ok: false, error: (trazido && trazido.error) || 'não consegui baixar nenhuma faixa', alternativas: busca.itens.slice(0, 5) };
150
+ const precisaCredito = !/cc0|pdm/.test(escolhida.licenca);
151
+ return {
152
+ ok: true,
153
+ tipo,
154
+ url: trazido.url,
155
+ titulo: escolhida.titulo,
156
+ autor: escolhida.autor,
157
+ duracao: escolhida.duracao,
158
+ licenca: NOME_LICENCA[escolhida.licenca] || escolhida.licenca,
159
+ licencaUrl: escolhida.licencaUrl,
160
+ credito: escolhida.credito,
161
+ pagina: escolhida.pagina,
162
+ arquivoLocal: trazido.arquivoLocal,
163
+ comoUsar: tipo === 'efeito'
164
+ ? `No elemento ou na cena: "som": {"arquivo": "${trazido.url}", "volume": 0.8}.`
165
+ : `No documento: "trilha": {"tipo":"arquivo","src":"${trazido.url}","volume":0.55,"repetir":true}.`
166
+ + (precisaCredito ? ` A licença pede CRÉDITO: ponha "${escolhida.credito}" na assinatura ou numa legenda da última cena.` : ' Licença sem exigência de crédito.'),
167
+ alternativas: busca.itens.filter((i) => i.id !== escolhida.id).slice(0, 4)
168
+ .map((i) => ({ titulo: i.titulo, autor: i.autor, duracao: i.duracao, licenca: i.licenca, id: i.id })),
169
+ };
170
+ }
171
+
172
+ module.exports = { buscarAudio, procurar, trazer, credito, LIVRES, NOME_LICENCA };
@@ -157,13 +157,13 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
157
157
  mostrar('');
158
158
  mostrar(c.bold(c.brand(' Bem-vindo ao PrimoCode.')));
159
159
  mostrar(c.muted(' Primeiro, entre na sua conta do Conecta Primo AI.'));
160
- mostrar(c.dim(' Vou abrir o navegador; é só confirmar por lá.'));
160
+ mostrar(c.dim(' Vou abrir o navegador; é só entrar com a conta por lá — o resto é automático.'));
161
161
  mostrar('');
162
162
  const r = await conectar();
163
163
  if (!r || r.ok === false) {
164
164
  mostrar('');
165
165
  mostrar(c.err(' Não deu para conectar' + (r && r.error ? `: ${r.error}` : '.')));
166
- mostrar(c.muted(' Tente de novo com ') + c.brand('/conectar') + c.muted('.'));
166
+ mostrar(c.muted(' Tente de novo com ') + c.brand('/connect') + c.muted('.'));
167
167
  return { liberado: false, motivo: 'sem-login' };
168
168
  }
169
169
  credencial = conectado ? conectado() : { plano: r.plano };
@@ -181,7 +181,7 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
181
181
  } else if (!s || !s.ok) {
182
182
  mostrar('');
183
183
  mostrar(c.err(' Não consegui confirmar sua conta' + (s && s.error ? `: ${s.error}` : '.')));
184
- mostrar(c.muted(' Entre de novo com ') + c.brand('/conectar') + c.muted('.'));
184
+ mostrar(c.muted(' Entre de novo com ') + c.brand('/connect') + c.muted('.'));
185
185
  return { liberado: false, motivo: 'sessao-invalida' };
186
186
  }
187
187
 
@@ -195,7 +195,7 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
195
195
  mostrar('');
196
196
  mostrar(c.dim(' Assine em ') + c.brand('conecta-primo-ai.vercel.app')
197
197
  + c.dim(' e rode ') + c.brand('primo') + c.dim(' de novo.'));
198
- mostrar(c.dim(' Já assinou? ') + c.brand('/conectar')
198
+ mostrar(c.dim(' Já assinou? ') + c.brand('/connect')
199
199
  + c.dim(' atualiza o plano nesta máquina.'));
200
200
  mostrar('');
201
201
  return { liberado: false, motivo: 'sem-plano', plano };
package/lib/catalogo.js CHANGED
@@ -143,6 +143,13 @@ const GRUPOS = [
143
143
  args: 'todos: lista de {content: a tarefa curta no imperativo, status: pending|in_progress|completed}. Reescreve a lista inteira a cada chamada; só uma "in_progress" por vez.' },
144
144
  { tool: 'pesquisar_referencia', escreve: false, faz: 'lê docs e exemplos antes de escrever — sobe a qualidade',
145
145
  args: 'consulta: o que pesquisar, de preferência em inglês. paginas (opcional, número, padrão 3, máximo 5).' },
146
+ // Baixa o arquivo para <projeto>/midia/ — por isso `escreve: true`.
147
+ { tool: 'buscar_audio', escreve: true, faz: 'acha música ou efeito sonoro livre na internet (Creative Commons), baixa e manda para o estúdio, com o crédito',
148
+ args: 'consulta: o clima ou o som, em inglês ("upbeat corporate", "calm piano", "whoosh", "ding"). tipo (opcional): "musica" (padrão) ou "efeito". '
149
+ + 'duracao (opcional, segundos): quanto o vídeo dura, para a faixa combinar. Devolve url para trilha.src ou som.arquivo, e o crédito a pôr na peça quando a licença pedir.' },
150
+ { tool: 'marca_da_empresa', escreve: true, faz: 'puxa do site da empresa a logo, a cor principal, a paleta, as fontes e a frase — pronto para `marca` no estúdio',
151
+ args: 'nome: a empresa como o usuário falou. site (opcional): o endereço, se souber — sem ele, o site é descoberto. '
152
+ + 'Devolve marca:{cor, logo} para colar no documento, mais cores.paleta, fontes e frase. Chame ANTES de criar uma peça de marca.' },
146
153
  // Vai para memory-geral.md, FORA da pasta do projeto — por isso não
147
154
  // é write_file/edit_file (a sandbox de resolveInProject bloqueia
148
155
  // qualquer caminho fora do projeto, e é assim mesmo que tem que