primocode 10.9.0 → 10.9.1

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 v10.9.0
1
+ # PrimoCode v10.9.1
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 v10.9.0
12
+ ▐▛███▜▌ Primo Code v10.9.1
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
package/lib/act.js CHANGED
@@ -684,6 +684,11 @@ const RITMO_AGENTE = '[ritmo]\n'
684
684
 
685
685
  /* `pedido` é opcional: com ele, o arquivo citado no pedido é o primeiro a
686
686
  entrar já lido (ver lib/pre-leitura.js). */
687
+ /** O agente quer o editor de gravação (`primocode video`) num pedido que não é editar gravação? Puro. */
688
+ function videoForaDeEdicao(nome, args, ehEdicao) {
689
+ return nome === 'run_shell' && !ehEdicao && /^\s*(?:cd\s+\S+\s*&&\s*)?(?:npx\s+)?primocode\s+video\b/.test(String((args || {}).command || ''));
690
+ }
691
+
687
692
  function buildMemoryContext(projectDir, projectName, pedido = '', { memoriaDaPasta = true } = {}) {
688
693
  const parts = [];
689
694
  /* NO SERVIDOR O CONTEXTO É CURTO (13/10: "continua lendo e listando
@@ -867,6 +872,10 @@ async function runAct(userPrompt, opts) {
867
872
  * lib/skills.js, que também explica por que mandar todas em todo pedido
868
873
  * não é uma opção num plano limitado por token. */
869
874
  let pedido = userPrompt;
875
+ /* Editar a GRAVAÇÃO da pessoa? Decide pelo que ELA escreveu (userPrompt),
876
+ não pelo `pedido` já com skills e instruções na frente: uma skill com
877
+ "editar… o vídeo" no corpo bastava para virar edição. */
878
+ const ehEdicaoDaGravacao = require('./estudio/diretor.js').precisaDeEdicao(userPrompt);
870
879
  if (!subagent) {
871
880
  const comSkills = skills.cardapio(userPrompt);
872
881
  pedido = comSkills.texto;
@@ -921,8 +930,8 @@ async function runAct(userPrompt, opts) {
921
930
  // Vídeo que o Estúdio fez se edita no Remotion Studio, não no editor por linha do tempo.
922
931
  const EDITAR_DO_ESTUDIO = 'SE ELA QUER EDITAR UM VÍDEO QUE O ESTÚDIO FEZ (não uma gravação dela): '
923
932
  + 'estudio_projeto acao:"editar" com o id do projeto abre o Remotion Studio no navegador; diga a pasta a ela e pare.\n';
924
- const producao = diretor.precisaDeProducao(pedido);
925
- if (diretor.precisaDeEdicao(pedido)) {
933
+ const producao = diretor.precisaDeProducao(userPrompt);
934
+ if (ehEdicaoDaGravacao) {
926
935
  // A gravação da pessoa: o editor por linha do tempo (lib/video/), pelo run_shell.
927
936
  messages.push({ role: 'system', content: EDITAR_DO_ESTUDIO + 'PARA EDITAR A GRAVAÇÃO DA PESSOA use o editor por linha do tempo via run_shell: '
928
937
  + 'SE A PESSOA SÓ DISSE "edite esse vídeo" (sem dizer o quê), O VÍDEO É O PEDIDO: `primocode video import --json \'{"path":"<arquivo>"}\'`, '
@@ -1481,6 +1490,19 @@ async function runAct(userPrompt, opts) {
1481
1490
  /* Só conta o que passou do freio de repetição: a chamada idêntica
1482
1491
  já é barrada lá (e a rodada parada desiste sozinha). Aqui entra
1483
1492
  o que escapa dele — a fatia sobreposta, a volta sem nada novo. */
1493
+ /* VÍDEO SE FAZ NO ESTÚDIO (13/10). Pedida uma animação 3D, o modelo
1494
+ do servidor rodou `primocode video do` e `primocode video help`
1495
+ pelo shell cinco vezes até o freio parar — o editor por linha do
1496
+ tempo é para a GRAVAÇÃO da pessoa, e as ferramentas do Estúdio
1497
+ estavam ali. Fora de edição, a chamada volta com o caminho certo. */
1498
+ if (videoForaDeEdicao(tc.name, tc.arguments, ehEdicaoDaGravacao)) {
1499
+ anotar(tc, 'recusada', 'primocode video fora de edição de gravação');
1500
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false,
1501
+ error: '`primocode video` é só para editar uma GRAVAÇÃO da pessoa. Para criar vídeo, animação ou motion: '
1502
+ + 'estudio_projeto acao:"criar" → estudio_plano com o roteiro → estudio_render.' }) });
1503
+ continue;
1504
+ }
1505
+
1484
1506
  motivoTravou = motivoTravou || travado.observar(circulo, tc.name, tc.arguments, pastaAtual);
1485
1507
 
1486
1508
  if (criarPasta && catalogo.criaProjeto(tc.name, tc.arguments)) garantirPasta();
@@ -1962,5 +1984,5 @@ async function rodarPipeline(args, ctx) {
1962
1984
  });
1963
1985
  }
1964
1986
 
1965
- module.exports = { runAct, entregaDoAgente, lembreteDeParalelo, buildMemoryContext, leituraPeloShell, historicoEnxuto, interromper, mandarDuranteATarefa, trabalhando, sobrasDaCaixa, entregarRecados, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
1987
+ module.exports = { runAct, videoForaDeEdicao, entregaDoAgente, lembreteDeParalelo, buildMemoryContext, leituraPeloShell, historicoEnxuto, interromper, mandarDuranteATarefa, trabalhando, sobrasDaCaixa, entregarRecados, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
1966
1988
  anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
@@ -985,13 +985,19 @@ function vazamentoNoComeco(lista, palavras, direcao = '') {
985
985
  const roteiro = norm(lista.slice(0, 2).map((x) => x.texto).join(' ')).split(' ').filter(Boolean);
986
986
  if (roteiro.length < 2 || !palavras.length) return 0;
987
987
  const pares = new Set(roteiro.slice(0, -1).map((w, i) => w + ' ' + roteiro[i + 1]));
988
- const ditas = palavras.slice(0, 14).map((w) => norm(w.texto));
988
+ /* 40 palavras, não 14 (13/10): a direção do personagem tem ~20 palavras, e
989
+ o roteiro só começava depois da janela — o corte desistia e o vídeo saía
990
+ com "Natural and close. Never like an announcer." na legenda. */
991
+ const ditas = palavras.slice(0, 40).map((w) => norm(w.texto));
989
992
  let i = 0;
990
993
  while (i < ditas.length - 1 && !pares.has(ditas[i] + ' ' + ditas[i + 1])) i++;
991
994
  if (i === 0 || i >= ditas.length - 1) return 0;
992
995
  const daDirecao = new Set(norm(direcao).split(' ').filter(Boolean));
993
996
  const antes = ditas.slice(0, i).join(' ').split(' ').filter(Boolean);
994
- const vazou = !direcao || antes.filter((w) => daDirecao.has(w)).length / antes.length >= 0.6;
997
+ /* A transcrição entorta a direção ("Read aloud in Brazilian Portuguese" vira
998
+ "Lê a alta em Brasília e Português"): basta 35% das palavras, ou 3, serem dela. */
999
+ const daDir = antes.filter((w) => daDirecao.has(w)).length;
1000
+ const vazou = !direcao || daDir / antes.length >= 0.35 || daDir >= 3;
995
1001
  return vazou ? Math.max(0, palavras[i].de - 0.08) : 0;
996
1002
  }
997
1003
 
@@ -1141,7 +1147,7 @@ async function narrarComCache(lista, mp3, { voz, direcao } = {}) {
1141
1147
  /* 'v3' (11/10): as narrações guardadas com a direção antiga (a que tinha
1142
1148
  "RÁPIDO" e "contínuas") ficam para trás — reaproveitar uma delas trazia o
1143
1149
  "Rápido e contínuo." de volta mesmo com a direção nova. */
1144
- const chave = require('crypto').createHash('sha1').update(JSON.stringify(['v3', lista.map((x) => x.texto), voz || '', direcao || ''])).digest('hex').slice(0, 16);
1150
+ const chave = require('crypto').createHash('sha1').update(JSON.stringify(['v4', lista.map((x) => x.texto), voz || '', direcao || ''])).digest('hex').slice(0, 16);
1145
1151
  const dir = path.join(os.homedir(), '.primocode', 'studio', 'cache-voz');
1146
1152
  const cm = path.join(dir, chave + '.mp3'), cj = path.join(dir, chave + '.json');
1147
1153
  if (fs.existsSync(cm) && fs.existsSync(cj)) { fs.copyFileSync(cm, mp3); return { ok: true, ...JSON.parse(fs.readFileSync(cj, 'utf8')) }; }
@@ -293,6 +293,16 @@ async function marcaDaEmpresa(args, { projectDir, studio }) {
293
293
  function marcaEmObjeto(m) {
294
294
  if (!m || typeof m !== 'object') return {};
295
295
  const out = { ...m };
296
+ /* Objeto com OUTROS nomes ({primaria, neutras: […]}, visto em 13/10): sem
297
+ fundo/destaque/tinta, todas as cores dele viram a lista e são lidas por
298
+ luz e saturação, como a lista do agente. */
299
+ const papeis = ['fundo', 'destaque', 'tinta', 'apoio'];
300
+ if (m.cores && typeof m.cores === 'object' && !Array.isArray(m.cores) && !papeis.some((k) => m.cores[k])) {
301
+ const todas = [];
302
+ const juntar = (v) => { if (Array.isArray(v)) v.forEach(juntar); else if (v && typeof v === 'object') Object.values(v).forEach(juntar); else if (typeof v === 'string') todas.push(v); };
303
+ juntar(m.cores);
304
+ m = { ...m, cores: todas };
305
+ }
296
306
  if (Array.isArray(m.cores)) {
297
307
  const rgb = (h) => { const n = parseInt(h.slice(1), 16); return [n >> 16, (n >> 8) & 255, n & 255].map((v) => v / 255); };
298
308
  const luz = (h) => { const [r, g, b] = rgb(h); return 0.2126 * r + 0.7152 * g + 0.0722 * b; };
@@ -128,28 +128,54 @@ function generoDoApresentador(roteiro = {}) {
128
128
  const CABELOS_F = ['longo']; // o coque e o cacheado, de frente, liam como homem (12/10); só quando pedidos
129
129
  const CABELOS_M = ['curto', 'topete', 'careca', 'cacheado'];
130
130
 
131
+ /* A COR PELO NOME (13/10). O agente escreveu pele: "morena", corCabelo:
132
+ "castanho escuro", roupa: "moletom cinza" — e o three.js, sem entender o
133
+ nome, pinta de BRANCO: o humano saiu todo branco, pele, cabelo e roupa.
134
+ Código de cor passa direto; nome em português vira o código pela tabela
135
+ (o nome mais longo primeiro: "castanho escuro" antes de "castanho"); o que
136
+ não se reconhece cai no sorteio de sempre. */
137
+ const CORES_POR_NOME = {
138
+ pele: [['muito clara', '#f6d7bd'], ['clara', '#f2c9a6'], ['branca', '#f2c9a6'], ['bronzeada', '#c08660'], ['parda', '#c98d66'],
139
+ ['morena clara', '#c98d66'], ['morena escura', '#8a5a3c'], ['morena', '#a8704c'], ['oliva', '#c4a07a'], ['negra', '#6b4229'],
140
+ ['preta', '#6b4229'], ['escura', '#6b4229'], ['media', '#c98d66']],
141
+ cabelo: [['castanho escuro', '#3b2618'], ['castanho claro', '#8a5a34'], ['castanho', '#6b4423'], ['preto', '#1d1612'], ['loiro', '#d8c39a'],
142
+ ['ruivo', '#a4471f'], ['grisalho', '#8d8d93'], ['branco', '#e8e6e1'], ['cinza', '#8d8d93']],
143
+ cor: [['azul marinho', '#1f2a5c'], ['azul claro', '#6fa8ff'], ['cinza escuro', '#3a3d45'], ['cinza claro', '#c4c6cc'], ['cinza', '#8a8d96'],
144
+ ['preto', '#24262d'], ['preta', '#24262d'], ['branco', '#e8e2d6'], ['branca', '#e8e2d6'], ['azul', '#2f4cdd'], ['verde', '#1f8a5b'],
145
+ ['vermelho', '#c2452f'], ['vermelha', '#c2452f'], ['amarelo', '#f1b434'], ['amarela', '#f1b434'], ['laranja', '#d9532b'],
146
+ ['roxo', '#7b5cff'], ['roxa', '#7b5cff'], ['rosa', '#e07aa0'], ['bege', '#d8c4a4'], ['marrom', '#6b4423'], ['vinho', '#7a1f2b'], ['dourado', '#c9a24a']],
147
+ };
148
+ function corEmHex(valor, tabela, sorteada) {
149
+ const v = String(valor == null ? '' : valor).trim();
150
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(v)) return v;
151
+ const t = ' ' + v.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z ]+/g, ' ') + ' ';
152
+ for (const [nomeCor, hex] of CORES_POR_NOME[tabela] || []) if (t.includes(' ' + nomeCor + ' ')) return hex;
153
+ return sorteada;
154
+ }
155
+
131
156
  function aparencia(nome, pedido = {}) {
132
157
  const k = (s) => hashDe(nome + ':' + s);
133
158
  const tipo = tipoDe(nome, pedido);
134
159
  const rotulo = String(pedido.rotulo || pedido.nome || nome || '').trim().slice(0, 24);
135
160
  if (tipo === 'animal') {
136
161
  const especie = especieDe(pedido.especie || '') || especieDe(nome) || 'cachorro';
137
- return { nome, rotulo, tipo, especie, ...(pedido.cor ? { cor: pedido.cor } : {}), ...(pedido.cor2 ? { cor2: pedido.cor2 } : {}) };
162
+ const cor = corEmHex(pedido.cor, 'cor', null), cor2 = corEmHex(pedido.cor2, 'cor', null);
163
+ return { nome, rotulo, tipo, especie, ...(cor ? { cor } : {}), ...(cor2 ? { cor2 } : {}) };
138
164
  }
139
165
  if (tipo === 'coisa') {
140
166
  const termo = String(pedido.icone || pedido.coisa || nome);
141
167
  return { nome, rotulo, tipo, icone: termo, ...(pedido.src ? { src: pedido.src } : {}) };
142
168
  }
143
169
  if (tipo === 'robo') {
144
- return { nome, rotulo, tipo, cor: pedido.cor || pegar(ROBOS, k('cor')), olhos: pedido.olhos || null, ...(pedido.bigode ? { bigode: true } : {}) };
170
+ return { nome, rotulo, tipo, cor: corEmHex(pedido.cor, 'cor', pegar(ROBOS, k('cor'))), olhos: pedido.olhos || null, ...(pedido.bigode ? { bigode: true } : {}) };
145
171
  }
146
172
  return {
147
173
  nome, rotulo, tipo,
148
- pele: pedido.pele || pegar(PELES, k('pele')),
174
+ pele: corEmHex(pedido.pele, 'pele', pegar(PELES, k('pele'))),
149
175
  cabelo: CABELOS.includes(pedido.cabelo) ? pedido.cabelo
150
176
  : pegar(pedido.genero === 'f' ? CABELOS_F : pedido.genero === 'm' ? CABELOS_M : CABELOS, k('cabelo')),
151
- corCabelo: pedido.corCabelo || pegar(CORES_CABELO, k('corCabelo')),
152
- roupa: pedido.roupa || pegar(ROUPAS, k('roupa')),
177
+ corCabelo: corEmHex(pedido.corCabelo, 'cabelo', pegar(CORES_CABELO, k('corCabelo'))),
178
+ roupa: corEmHex(pedido.roupa, 'cor', pegar(ROUPAS, k('roupa'))),
153
179
  oculos: pedido.oculos != null ? !!pedido.oculos : k('oculos') < 0.3,
154
180
  barba: !!pedido.barba && pedido.genero !== 'f', // só quando pedida: sorteada, ficava estranha em quem tem cabelo longo
155
181
  bigode: !!pedido.bigode && pedido.genero !== 'f',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "10.9.0",
3
+ "version": "10.9.1",
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": {