primocode 9.8.0-beta.16 → 9.8.0-beta.17

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
@@ -1,4 +1,4 @@
1
- # PrimoCode v9.8.0-beta.16
1
+ # PrimoCode v9.8.0-beta.17
2
2
 
3
3
  Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
4
4
  Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
9
9
  está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
10
10
 
11
11
  ```
12
- ▐▛███▜▌ Primo Code v9.8.0-beta.16
12
+ ▐▛███▜▌ Primo Code v9.8.0-beta.17
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
@@ -120,7 +120,41 @@ const COLECOES = [
120
120
  * O SVG vem com `currentColor` no traço para a cor ser decidida na cena — um
121
121
  * ícone com cor cravada no arquivo obriga a baixar de novo para mudar de cor.
122
122
  */
123
+ /* ── EMOJI 3D ─────────────────────────────────────────────────────────
124
+ * "Os emojis, quero emojis da Apple." (25/09/2026) Os da Apple têm direito
125
+ * autoral e não podem ir num vídeo publicado; os Fluent 3D da Microsoft têm
126
+ * licença MIT e o mesmo visual brilhante e com volume. "fluent-3d:robot" é o
127
+ * PNG do repositório oficial (pelo jsDelivr), embutido como <img> — o resto
128
+ * do render trata igual a um ícone SVG. Sem o 3D, cai no emoji plano. */
129
+ const cache3d = new Map();
130
+ async function emoji3d(nome, { pasta = null } = {}) {
131
+ if (cache3d.has(nome)) return cache3d.get(nome);
132
+ const palavras = nome.split('-');
133
+ const pastas = [
134
+ palavras.join(' ').replace(/^./, (c) => c.toUpperCase()),
135
+ palavras.map((w) => w[0].toUpperCase() + w.slice(1)).join(' '),
136
+ ];
137
+ for (const pastaGit of pastas) {
138
+ const url = `https://cdn.jsdelivr.net/gh/microsoft/fluentui-emoji@main/assets/${encodeURIComponent(pastaGit)}/3D/${palavras.join('_')}_3d.png`;
139
+ try {
140
+ const png = await pegar(url, { binario: true });
141
+ if (png && png.length > 1000 && png[0] === 0x89) {
142
+ if (pasta) guardar(path.join(pasta, 'midia', 'icones', `fluent-3d-${nome}.png`), png, { licenca: 'MIT (Microsoft Fluent UI Emoji)', origem: url });
143
+ const r = { ok: true, nome: 'fluent-3d:' + nome,
144
+ svg: `<img src="data:image/png;base64,${png.toString('base64')}" style="width:100%;height:100%;object-fit:contain;display:block" />` };
145
+ cache3d.set(nome, r);
146
+ return r;
147
+ }
148
+ } catch { /* a próxima grafia */ }
149
+ }
150
+ const plano = await icone('fluent-emoji-flat:' + nome, { pasta });
151
+ const r = plano.ok ? { ...plano, nome: 'fluent-3d:' + nome } : plano;
152
+ cache3d.set(nome, r);
153
+ return r;
154
+ }
155
+
123
156
  async function icone(nome, { pasta = null } = {}) {
157
+ if (/^fluent-3d:/i.test(String(nome))) return emoji3d(String(nome).split(':')[1], { pasta });
124
158
  const m = String(nome || '').match(/^([a-z0-9-]+):([a-z0-9-]+)$/i);
125
159
  if (!m) return { ok: false, error: 'o ícone é "coleção:nome", como "lucide:zap"' };
126
160
  const url = `https://api.iconify.design/${m[1]}/${m[2]}.svg?color=currentColor`;
@@ -64,7 +64,8 @@ function paleta(marca = {}) {
64
64
  /* ── a letra que cabe ─────────────────────────────────────────────────────
65
65
  * Largura média do caractere em "em" por família. Não é exata — é o
66
66
  * bastante para escolher o maior tamanho que cabe em N linhas, com folga. */
67
- const LARGURA = { condensada: 0.43, display: 0.56, texto: 0.52, serifa: 0.5 };
67
+ // Com folga: medido em 25/09, 0,52 deixava "Fundadores" quebrar no meio no card.
68
+ const LARGURA = { condensada: 0.46, display: 0.6, texto: 0.58, serifa: 0.54 };
68
69
  const FAMILIA_TIPO = (f) => (/condensed|narrow|anton|oswald|bebas|teko|league gothic/i.test(f) ? 'condensada'
69
70
  : /serif|fraunces|playfair|garamond|lora|newsreader|spectral|baskerville|dm serif/i.test(f) ? 'serifa'
70
71
  : /black|bricolage|archivo|sora|grotesk|syne|unbounded/i.test(f) ? 'display' : 'texto');
@@ -183,7 +184,7 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
183
184
  const fotoFundo = fundos[i];
184
185
  if (fotoFundo && p.tipo !== 'imagem') {
185
186
  const ehV = fotoFundo.tipo === 'video';
186
- els.push({ tipo: ehV ? 'video' : 'imagem', src: fotoFundo.src, ...(ehV ? { volume: 0 } : {}), x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover',
187
+ els.push({ tipo: ehV ? 'video' : 'imagem', src: fotoFundo.src, ...(ehV ? { volume: 0 } : {}), x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover', foco: 'center 28%',
187
188
  anima: { escala: [{ t: 0, v: variante ? 1.14 : 1.02 }, { t: dur, v: variante ? 1.02 : 1.14, e: 'suave' }] } });
188
189
  els.push({ tipo: 'forma', forma: 'retangulo', x: 0, y: 0, largura: '100%', altura: '100%',
189
190
  fundo: { tipo: 'gradiente', cores: [alfa(cor.fundo, 0.93), alfa(cor.fundo, 0.72), alfa(cor.fundo, 0.55)], angulo: 90 } });
@@ -192,7 +193,8 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
192
193
  const canto = [[W - R * 0.55, -R * 0.45], [-R * 0.45, H - R * 0.5], [W - R * 0.4, H - R * 0.55], [-R * 0.5, -R * 0.4]][i % 4];
193
194
  els.push({
194
195
  tipo: 'forma', forma: 'circulo', largura: R, altura: R, x: Math.round(canto[0]), y: Math.round(canto[1]),
195
- cor: p.tipo === 'numero' ? cor.sobreDestaque : cor.destaque, opacidade: p.tipo === 'numero' ? 0.12 : (cor.escuro ? 0.22 : 0.16),
196
+ // O brilho alterna entre o destaque e a cor de apoio: as duas cores da marca aparecem.
197
+ cor: p.tipo === 'numero' ? cor.sobreDestaque : (variante && marca.cores && marca.cores.apoio ? cor.apoio : cor.destaque), opacidade: p.tipo === 'numero' ? 0.12 : (cor.escuro ? 0.26 : 0.16),
196
198
  anima: {
197
199
  desfoque: [{ t: 0, v: Math.round(R * 0.22) }],
198
200
  x: [{ t: 0, v: 0 }, { t: dur, v: variante ? 60 : -60, e: 'suave' }],
@@ -293,7 +295,7 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
293
295
  vazia embaixo. */
294
296
  const fT = p.titulo ? caber(p.titulo, { largura: W0, linhas: 2, max: H * 0.1, fonte: F.titulo }) : { tamanho: 0, linhas: 0 };
295
297
  const hTit = fT.tamanho * 1.04 * fT.linhas;
296
- const hCard0 = colunas > 1 ? (p.tipo === 'passos' ? H * 0.26 : H * 0.44) : H * 0.14;
298
+ const hCard0 = colunas > 1 ? (p.tipo === 'passos' ? H * 0.26 : H * 0.44) : H * (deitado ? 0.14 : 0.13);
297
299
  const hItens = colunas > 1 ? hCard0 : n * hCard0 + (n - 1) * gap;
298
300
  const bloco = hTit + (hTit ? H * 0.07 : 0) + hItens;
299
301
  const yTit = Math.max(M, (H - bloco) / 2);
@@ -301,6 +303,23 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
301
303
  const topo = yTit + hTit + (hTit ? H * 0.07 : 0);
302
304
  const hCard = Math.min(hCard0, colunas > 1 ? H - topo - M : (H - topo - M - gap * (n - 1)) / n);
303
305
  const dur0 = vid ? Math.max(1, (tp.ate || dur) - (tp.de || 0)) : 1;
306
+ /* Um tamanho de letra para TODOS os itens: o do mais apertado. Cada
307
+ item com o seu tamanho deixava um card com a letra maior que os
308
+ outros, e esse texto saía do card (medido em 25/09). */
309
+ const padG = Math.round(Math.min(wCard, hCard) * 0.14);
310
+ const tamIG = Math.round(Math.min(hCard * (colunas > 1 ? 0.28 : 0.5), H * 0.09));
311
+ const lwG = colunas > 1 ? wCard - 2 * padG : wCard - (tamIG + 3 * padG);
312
+ // No vertical a linha é estreita: 3 linhas por item, para a letra caber grande.
313
+ const linhasG = colunas > 1 ? 4 : (deitado ? 2 : 3);
314
+ const hTexto = colunas > 1 ? hCard - (padG * 1.8 + tamIG + padG) : hCard - padG;
315
+ const maxItem = deitado ? Math.min(H * 0.05, hCard * 0.3) : Math.min(W * 0.062, hCard * 0.26);
316
+ const fits = itens.map((it) => caber(it.texto, { largura: lwG, linhas: linhasG, max: maxItem, fonte: F.texto }));
317
+ let tamItens = Math.min(...fits.map((f) => f.tamanho));
318
+ // Só na lista o texto mora DENTRO do card: ali a altura também manda.
319
+ if (p.tipo === 'lista') {
320
+ const linhasReais = Math.max(...itens.map((it) => caber(it.texto, { largura: lwG, linhas: linhasG, max: tamItens, min: tamItens, fonte: F.texto }).linhas));
321
+ tamItens = Math.min(tamItens, Math.floor(hTexto / (1.2 * linhasReais)));
322
+ }
304
323
  itens.forEach((it, k) => {
305
324
  const cx = colunas > 1 ? M + k * (wCard + gap) : M;
306
325
  const cy = colunas > 1 ? topo : topo + k * (hCard + gap);
@@ -327,7 +346,8 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
327
346
  const tx = colunas > 1 ? cx + pad : cx + pad + tamI + pad;
328
347
  const ty = colunas > 1 ? cy + pad + tamI + pad * 0.8 : cy + hCard / 2;
329
348
  const lw = colunas > 1 ? wCard - 2 * pad : wCard - (tamI + 3 * pad);
330
- const fit = caber(it.texto, { largura: lw, linhas: colunas > 1 ? 4 : 2, max: Math.min(H * 0.05, hCard * 0.3), fonte: F.texto });
349
+ const fit0 = caber(it.texto, { largura: lw, linhas: linhasG, max: tamItens, min: Math.min(18, tamItens), fonte: F.texto });
350
+ const fit = { tamanho: Math.min(fit0.tamanho, tamItens), linhas: fit0.linhas };
331
351
  els.push({
332
352
  tipo: 'texto', texto: it.texto, fonte: F.texto, peso: 650, tamanho: fit.tamanho, cor: tinta, entrelinha: 1.2,
333
353
  x: Math.round(tx), y: Math.round(colunas > 1 ? ty : ty - fit.tamanho * 1.2 * fit.linhas / 2), largura: Math.round(lw), anima: entra,
@@ -365,13 +385,19 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
365
385
  const src = m && typeof m === 'object' ? m.src : m;
366
386
  if (src) {
367
387
  const ehVideo = m && typeof m === 'object' && m.tipo === 'video';
368
- els.push({ tipo: ehVideo ? 'video' : 'imagem', src, ...(ehVideo ? { volume: 0 } : {}), x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover', anima: { escala: [{ t: 0, v: 1.12 }, { t: dur, v: 1, e: 'suave' }] } });
388
+ // O enquadramento puxa para cima: é onde ficam os rostos.
389
+ els.push({ tipo: ehVideo ? 'video' : 'imagem', src, ...(ehVideo ? { volume: 0 } : {}), x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover', foco: 'center 28%', anima: { escala: [{ t: 0, v: 1.12 }, { t: dur, v: 1, e: 'suave' }] } });
369
390
  els.push({ tipo: 'forma', forma: 'retangulo', x: 0, y: 0, largura: '100%', altura: '100%', fundo: { tipo: 'gradiente', cores: ['rgba(0,0,0,0)', 'rgba(0,0,0,0.25)', 'rgba(0,0,0,0.85)'], angulo: 180 } });
370
391
  }
371
- const larg = W0 * 0.8;
372
- const fit = caber(p.titulo || '', { largura: larg, linhas: 2, max: H * 0.1, fonte: F.titulo });
392
+ const larg = W0 * (deitado ? 0.8 : 1);
393
+ const fit = caber(p.titulo || '', { largura: larg, linhas: 2, max: H * (deitado ? 0.1 : 0.06), fonte: F.titulo });
373
394
  const hT = fit.tamanho * 1.04 * fit.linhas;
374
- const yT = H - M - hT - (p.sub ? H * 0.08 : 0);
395
+ /* Vídeo da marca (depoimento, bastidor) costuma ter o nome da
396
+ pessoa gravado embaixo: o título vai para o alto, com o véu
397
+ de cima, para não escrever por cima dele. */
398
+ const noTopo = Boolean(m && typeof m === 'object' && m.tipo === 'video');
399
+ if (noTopo) els.push({ tipo: 'forma', forma: 'retangulo', x: 0, y: 0, largura: '100%', altura: '100%', fundo: { tipo: 'gradiente', cores: ['rgba(0,0,0,0.8)', 'rgba(0,0,0,0.2)', 'rgba(0,0,0,0)'], angulo: 180 } });
400
+ const yT = noTopo ? M * 1.2 : H - M - hT - (p.sub ? H * 0.08 : 0);
375
401
  if (p.titulo) titulo(p.titulo, { y: yT, larg, max: fit.tamanho, cor: '#FFFFFF', de: 0.3 });
376
402
  if (p.sub) linhaFina(p.sub, { y: yT + hT + H * 0.02, larg, c: 'rgba(255,255,255,0.8)', de: 0.7 });
377
403
  break;
@@ -116,7 +116,7 @@ function iconeDe(palavra) {
116
116
  if (!w || VAZIAS.has(w)) return null;
117
117
  for (const [raizes, nome] of ICONES) {
118
118
  for (const r of raizes) {
119
- if (r.length <= 3 ? w === r : w.startsWith(r)) return 'fluent-emoji-flat:' + nome;
119
+ if (r.length <= 3 ? w === r : w.startsWith(r)) return 'fluent-3d:' + nome;
120
120
  }
121
121
  }
122
122
  return null;
@@ -306,6 +306,8 @@ async function ferramentaPlano(args) {
306
306
  return { ok: false, error: 'o roteiro virou um plano com problemas (defeito do PrimoCode, não seu).', problemas: conferida.problemas.slice(0, 12) };
307
307
  }
308
308
  projeto.salvarPlano(args.id, r.plano, args.motivo || 'montado do roteiro');
309
+ // O roteiro fica guardado: ajustar uma frase é mandar o roteiro de novo, não reescrever tudo.
310
+ try { fs.writeFileSync(path.join(p.pasta, 'roteiro.json'), JSON.stringify(args.roteiro, null, 2)); } catch { /* segue */ }
309
311
  projeto.salvar(args.id, { duracao: conferida.duracao });
310
312
  return {
311
313
  ok: true, duracao: conferida.duracao, ...r.resumo, resumo: plano.resumo(r.plano),
@@ -959,6 +961,36 @@ async function ferramentaRender(args, ctx = {}) {
959
961
 
960
962
  // ── estudio_site ─────────────────────────────────────────────────────────
961
963
 
964
+ /* A PALETA INTEIRA do site, não só a primeira cor. O fundo é o neutro
965
+ escuro que o site mais usa (#121218 na Paris Group); o destaque é a cor da
966
+ marca que mais aparece sobre ele (o laranja, não o azul escuro que some); o
967
+ apoio é a seguinte. A fonte serifada, quando existe, vai para citação. */
968
+ function paletaDoSite(r) {
969
+ const hexRgb = (h) => { const n = parseInt(String(h).slice(1).padEnd(6, '0').slice(0, 6), 16); return [n >> 16, (n >> 8) & 255, n & 255]; };
970
+ const luz = (h) => { const [a, b, c] = hexRgb(h); return (0.299 * a + 0.587 * b + 0.114 * c) / 255; };
971
+ const sat = (h) => { const v = hexRgb(h); return (Math.max(...v) - Math.min(...v)) / 255; };
972
+ const neutras = ((r.cores && (r.cores.neutras || r.cores.fundo)) || []).map((x) => x.cor || x).filter((c) => /^#/.test(c));
973
+ const fundo = neutras.find((c) => luz(c) < 0.15 && luz(c) > 0.02) || neutras.find((c) => luz(c) < 0.2) || undefined;
974
+ const base = fundo || '#121218';
975
+ const marca = ((r.cores && r.cores.marca) || []).map((x) => x.cor).filter(Boolean);
976
+ const nota = (c) => sat(c) * 1.2 + Math.abs(luz(c) - luz(base));
977
+ const ordenadas = [...marca].sort((a, b) => nota(b) - nota(a));
978
+ const destaque = ordenadas[0];
979
+ // O apoio é OUTRA cor, de outro matiz: dois laranjas não fazem paleta.
980
+ const matiz = (h) => { const [a, b, c] = hexRgb(h).map((v) => v / 255); const mx = Math.max(a, b, c), mn = Math.min(a, b, c), d = mx - mn;
981
+ if (!d) return 0; const x = mx === a ? ((b - c) / d) % 6 : mx === b ? (c - a) / d + 2 : (a - b) / d + 4; return (x * 60 + 360) % 360; };
982
+ const longe = (a, b) => { const d = Math.abs(matiz(a) - matiz(b)); return Math.min(d, 360 - d) > 60; };
983
+ const apoio = ordenadas.find((c) => c !== destaque && longe(c, destaque) && sat(c) > 0.3) || ordenadas[1];
984
+ const fontes = ((r.fontes && r.fontes.declaradas) || []).map((x) => x.fonte).filter((f) => !/mono|menlo|courier|system|arial|helvetica/i.test(f));
985
+ return {
986
+ cores: { ...(fundo ? { fundo } : {}), ...(destaque ? { destaque } : {}), ...(apoio ? { apoio } : {}) },
987
+ fontes: {
988
+ ...(fontes[0] ? { titulo: fontes.find((f) => !/serif/i.test(f) || /sans/i.test(f)) || fontes[0], texto: fontes.find((f) => !/serif/i.test(f) || /sans/i.test(f)) || fontes[0] } : {}),
989
+ ...(fontes.find((f) => /serif/i.test(f) && !/sans/i.test(f)) ? { serifa: fontes.find((f) => /serif/i.test(f) && !/sans/i.test(f)) } : {}),
990
+ },
991
+ };
992
+ }
993
+
962
994
  async function ferramentaSite(args) {
963
995
  let alvo = String(args.url || args.site || '').trim();
964
996
 
@@ -989,10 +1021,7 @@ async function ferramentaSite(args) {
989
1021
  const html = r.html;
990
1022
  delete r.html; // o HTML cru não vai para o modelo
991
1023
  if (r.ok) {
992
- const marcaLida = {
993
- cores: { destaque: ((r.cores && r.cores.marca) || [])[0] ? r.cores.marca[0].cor : undefined },
994
- fontes: { titulo: ((r.fontes && r.fontes.declaradas) || [])[0] ? r.fontes.declaradas[0].fonte : undefined },
995
- };
1024
+ const marcaLida = paletaDoSite(r);
996
1025
  ultimoSite = { url: r.url, html, quando: Date.now(), marca: marcaLida };
997
1026
  }
998
1027
  if (r.ok && args.id && args.logo !== false) {
@@ -323,7 +323,7 @@ function Elemento({ el, t, dados }) {
323
323
  startFrom={el.deOrigem ? Math.round(el.deOrigem * dados.fps) : undefined}
324
324
  endAt={el.ateOrigem ? Math.round(el.ateOrigem * dados.fps) : undefined}
325
325
  volume={el.volume ?? 0}
326
- style={{ width: '100%', height: '100%', objectFit: el.ajuste || 'cover' }}
326
+ style={{ width: '100%', height: '100%', objectFit: el.ajuste || 'cover', objectPosition: el.foco || 'center' }}
327
327
  />
328
328
  </div>
329
329
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "9.8.0-beta.16",
3
+ "version": "9.8.0-beta.17",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {