primocode 9.10.0 → 10.0.9

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.
Files changed (89) hide show
  1. package/README.md +30 -6
  2. package/bin/primocode.js +169 -342
  3. package/lib/act.js +360 -39
  4. package/lib/browser.js +105 -4
  5. package/lib/casar.js +108 -0
  6. package/lib/catalogo.js +6 -2
  7. package/lib/claude-engine.js +38 -17
  8. package/lib/codex-engine.js +27 -9
  9. package/lib/config.js +10 -3
  10. package/lib/cursor.js +10 -1
  11. package/lib/entrada.js +77 -11
  12. package/lib/estudio/cinema.js +27 -9
  13. package/lib/estudio/diretor.js +21 -1
  14. package/lib/estudio/ende.js +16 -5
  15. package/lib/estudio/estilo.js +66 -0
  16. package/lib/estudio/gerativo.js +1453 -0
  17. package/lib/estudio/glifos.js +215 -0
  18. package/lib/estudio/hibrido.js +165 -0
  19. package/lib/estudio/index.js +284 -53
  20. package/lib/estudio/kit/base.jsx +285 -0
  21. package/lib/estudio/kit/gerativo.jsx +980 -0
  22. package/lib/estudio/kit/index.jsx +13 -0
  23. package/lib/estudio/kit/marca.jsx +741 -0
  24. package/lib/estudio/kit/tres.jsx +308 -0
  25. package/lib/estudio/kit/vox.jsx +357 -0
  26. package/lib/estudio/kit.js +78 -0
  27. package/lib/estudio/lancamento.js +441 -0
  28. package/lib/estudio/motion.js +2 -5
  29. package/lib/estudio/plano.js +28 -3
  30. package/lib/estudio/referencias.js +105 -0
  31. package/lib/estudio/remotion.js +139 -51
  32. package/lib/estudio/ritmo.js +77 -0
  33. package/lib/estudio/sons-leves.js +38 -1
  34. package/lib/estudio/variedade.js +105 -0
  35. package/lib/estudio/vox.js +414 -0
  36. package/lib/fala.js +1 -94
  37. package/lib/mcp.js +156 -0
  38. package/lib/personas.js +67 -0
  39. package/lib/pipeline.js +55 -56
  40. package/lib/repeticao.js +3 -1
  41. package/lib/skills.js +44 -2
  42. package/lib/snapshot.js +111 -0
  43. package/lib/tools.js +100 -22
  44. package/lib/ui.js +50 -8
  45. package/lib/video/analise.js +89 -0
  46. package/lib/video/auto.js +329 -0
  47. package/lib/video/camadas.js +207 -0
  48. package/lib/video/cli.js +74 -0
  49. package/lib/video/delta.js +90 -0
  50. package/lib/video/edits.js +379 -0
  51. package/lib/video/filters.js +280 -0
  52. package/lib/video/fx3d/Camadas.jsx +64 -0
  53. package/lib/video/fx3d/Objetos.jsx +128 -0
  54. package/lib/video/fx3d/Root.jsx +18 -0
  55. package/lib/video/fx3d/modelos.jsx +304 -0
  56. package/lib/video/fx3d.js +108 -0
  57. package/lib/video/gerados.js +119 -0
  58. package/lib/video/objetos.js +111 -0
  59. package/lib/video/ops-fx.js +453 -0
  60. package/lib/video/ops.js +347 -0
  61. package/lib/video/paralelo.js +50 -0
  62. package/lib/video/pedido.js +191 -0
  63. package/lib/video/projeto.js +133 -0
  64. package/lib/video/rastrear.js +95 -0
  65. package/lib/video/rastro.js +276 -0
  66. package/lib/video/render.js +274 -0
  67. package/lib/video/texto.js +144 -0
  68. package/lib/video/timeline.js +318 -0
  69. package/lib/video/transcript.js +124 -0
  70. package/lib/voz.js +75 -223
  71. package/package.json +2 -3
  72. package/skills/motion-de-primeira/SKILL.md +5 -5
  73. package/studio/narrar.py +10 -7
  74. package/studio/rastrear.py +399 -0
  75. package/studio/texto.py +208 -0
  76. package/voz/primo_voz/__main__.py +96 -236
  77. package/voz/primo_voz/transcrever.py +31 -1
  78. package/app/ansi.js +0 -55
  79. package/app/app.css +0 -500
  80. package/app/app.js +0 -1017
  81. package/app/icone.svg +0 -6
  82. package/app/index.html +0 -220
  83. package/app/primo-marca.js +0 -192
  84. package/lib/app.js +0 -615
  85. package/lib/icone.js +0 -263
  86. package/lib/instalar-app.js +0 -231
  87. package/lib/painel.js +0 -291
  88. package/voz/primo_voz/desperta.py +0 -195
  89. package/voz/primo_voz/orbe.py +0 -406
package/lib/browser.js CHANGED
@@ -14,6 +14,7 @@
14
14
 
15
15
  'use strict';
16
16
 
17
+ const snapshot = require('./snapshot.js');
17
18
  const fs = require('fs');
18
19
  const os = require('os');
19
20
  const net = require('net');
@@ -22,9 +23,36 @@ const http = require('http');
22
23
  const crypto = require('crypto');
23
24
  const { spawn, execSync } = require('child_process');
24
25
 
25
- const PROFILE_DIR = path.join(os.homedir(), '.primocode', 'chrome-profile');
26
+ const PERFIL_VISIVEL = path.join(os.homedir(), '.primocode', 'chrome-profile');
26
27
  // Dentro da faixa 3100-3199 do ambiente local, para o start.sh cuidar dela.
27
- const DEBUG_PORT = Number(process.env.PRIMOCODE_CDP_PORT || 3199);
28
+ const PORTA_VISIVEL = Number(process.env.PRIMOCODE_CDP_PORT || 3199);
29
+
30
+ /* ── VISÍVEL SÓ QUANDO A PESSOA PEDIU ────────────────────────────────────
31
+ * "toda vez que eu mando alguma coisa pra ele ele abre umas 10.000 abas
32
+ * do Chrome — ele nem usa — e eu fecho e ele abre de novo" (Joel, 02/10)
33
+ * As ferramentas browser_* vão para o modelo em todo pedido, e ele as usa
34
+ * para PESQUISAR (o motion da Paris Group subiu o Chrome às 09:41 sem
35
+ * ninguém pedir navegador). Agora o terminal diz, por pedido, se a pessoa
36
+ * quer ver (definirVisivel): sem pedido de navegador, as mesmas ferramentas
37
+ * rodam num Chrome --headless de perfil e porta próprios, que nunca abre
38
+ * janela nem aba na tela — e morre com o processo. */
39
+ const PERFIL_ESCONDIDO = path.join(os.homedir(), '.primocode', 'chrome-escondido');
40
+ const PORTA_ESCONDIDA = Number(process.env.PRIMOCODE_CDP_PORT_ESCONDIDO || 3197);
41
+ let visivel = true;
42
+ let DEBUG_PORT = PORTA_VISIVEL;
43
+ let PROFILE_DIR = PERFIL_VISIVEL;
44
+
45
+ function definirVisivel(sim) {
46
+ const quer = sim !== false;
47
+ if (quer === visivel) return visivel;
48
+ visivel = quer;
49
+ DEBUG_PORT = quer ? PORTA_VISIVEL : PORTA_ESCONDIDA;
50
+ PROFILE_DIR = quer ? PERFIL_VISIVEL : PERFIL_ESCONDIDO;
51
+ // A sessão aberta é do outro navegador: a próxima ação conecta no certo.
52
+ if (currentSession) { try { currentSession.close(); } catch { /* já fechou */ } currentSession = null; }
53
+ return visivel;
54
+ }
55
+ function estaVisivel() { return visivel; }
28
56
 
29
57
  // ---- Helpers ---------------------------------------------------
30
58
  /* O Chromium que o Playwright baixa. Ele não fica no PATH nem em /Applications
@@ -413,12 +441,20 @@ async function ensureChromeRunning() {
413
441
  args.push('--no-sandbox');
414
442
  }
415
443
  // Sem interface gráfica (CI, servidor): PRIMOCODE_BROWSER_HEADLESS=1.
416
- if (process.env.PRIMOCODE_BROWSER_HEADLESS === '1' || process.env.PRIMOCODE_BROWSER_HEADLESS === 'true') {
417
- args.push('--headless=new', '--disable-gpu', '--disable-dev-shm-usage');
444
+ // E sempre que a pessoa não pediu para ver (definirVisivel).
445
+ const escondido = !visivel;
446
+ if (escondido || process.env.PRIMOCODE_BROWSER_HEADLESS === '1' || process.env.PRIMOCODE_BROWSER_HEADLESS === 'true') {
447
+ args.push('--headless=new', '--disable-gpu', '--disable-dev-shm-usage', '--mute-audio');
418
448
  }
419
449
  args.push('about:blank');
420
450
  chromeChild = spawn(chrome, args, { detached: true, stdio: 'ignore' });
421
451
  chromeChild.on('error', () => {});
452
+ /* O escondido morre com o processo: ninguém o vê para fechar, e um
453
+ headless órfão por sessão era o que o revisor deixava em 24/09. */
454
+ if (escondido) {
455
+ const filho = chromeChild;
456
+ process.once('exit', () => { try { process.kill(-filho.pid); } catch { try { filho.kill(); } catch { /* já morreu */ } } });
457
+ }
422
458
  chromeChild.unref();
423
459
 
424
460
  // Espera a porta responder
@@ -491,6 +527,55 @@ async function open(url) {
491
527
  return { ok: true, url };
492
528
  }
493
529
 
530
+ /* ── ABRIR À VISTA ────────────────────────────────────────────────────────
531
+ * "Peço pra ele abrir o Chrome e o YouTube: em vez de só abrir, ele mostra
532
+ * o mouse dele abrindo o Chrome, pesquisando…" (30/09)
533
+ * O cursor do PrimoCode (voz/primo_voz/cursor.py, macOS) vai até a barra de
534
+ * endereço da janela de verdade, clica, digita o endereço letra por letra e
535
+ * aperta Enter — quem olha vê o computador sendo usado. A janela vem do CDP
536
+ * (Browser.getWindowForTarget), então o clique cai na barra mesmo com a
537
+ * janela em qualquer lugar da tela. Sem cursor (outro SO, sem permissão),
538
+ * navega direto, como antes. */
539
+ async function abrirVisivel(url) {
540
+ // Escondido não tem janela para o cursor ir: navega direto.
541
+ if (!visivel) return open(url);
542
+ await ensure();
543
+ const cursor = require('./cursor.js');
544
+ if (!verSeta() || !cursor.disponivel()) return open(url);
545
+ try { await currentSession.send('Page.bringToFront'); } catch { /* segue */ }
546
+ let janela = null;
547
+ try {
548
+ const alvo = await currentSession.send('Target.getTargetInfo');
549
+ const w = await currentSession.send('Browser.getWindowForTarget', alvo && alvo.targetInfo ? { targetId: alvo.targetInfo.targetId } : {});
550
+ janela = w && w.bounds;
551
+ } catch { janela = null; }
552
+ if (!janela || janela.left == null) return open(url);
553
+ // A barra fica uns 58 pt abaixo do topo da janela (abas + barra), a ~30% da largura.
554
+ const x = Math.round(janela.left + Math.min(420, janela.width * 0.3));
555
+ const y = Math.round(janela.top + 58);
556
+ const passos = [
557
+ ['clicar', { x, y, botao: 'esquerdo', rotulo: 'barra de endereço' }],
558
+ ['tecla', { combo: 'cmd+a' }],
559
+ ['digitar', { texto: url.replace(/^https?:\/\//, ''), rotulo: 'digitando o endereço' }],
560
+ ['tecla', { combo: 'enter' }],
561
+ ];
562
+ for (const [cmd, args] of passos) {
563
+ const r = await cursor.mandar(cmd, args);
564
+ if (!r || r.ok === false) return open(url); // sem permissão de Acessibilidade: navega direto
565
+ }
566
+ const deadline = Date.now() + 20000;
567
+ await sleep(600);
568
+ while (Date.now() < deadline) {
569
+ try {
570
+ const r = await currentSession.send('Runtime.evaluate', { expression: 'document.readyState + "|" + location.href', returnByValue: true });
571
+ const [estado, href] = String((r.result && r.result.value) || '').split('|');
572
+ if (estado === 'complete' && href && href !== 'about:blank') return { ok: true, url: href, visivel: true };
573
+ } catch { /* a página está trocando */ }
574
+ await sleep(200);
575
+ }
576
+ return { ok: true, url, visivel: true };
577
+ }
578
+
494
579
  async function evaluate(js) {
495
580
  await ensure();
496
581
  const r = await currentSession.send('Runtime.evaluate', {
@@ -658,7 +743,22 @@ async function candidatosNaPagina() {
658
743
  try { return JSON.parse(await evaluate(js)) || []; } catch { return []; }
659
744
  }
660
745
 
746
+ /* O retrato da página com as refs (lib/snapshot.js), já em texto. */
747
+ async function retrato(max = 80) {
748
+ try {
749
+ const r = await evaluate(snapshot.scriptDoRetrato(max));
750
+ return snapshot.formatar(r, max);
751
+ } catch { return ''; }
752
+ }
753
+
661
754
  async function erroDeSeletor(selector) {
755
+ // Ref que não existe mais: a página mudou depois da leitura. Listar os
756
+ // seletores CSS aqui só confundiria — o caminho é ler de novo.
757
+ if (/data-primo-ref/.test(selector)) {
758
+ const err = new Error('essa ref não existe mais nesta página — ela mudou depois da leitura. '
759
+ + 'Estas são as refs de agora:\n' + await retrato(40));
760
+ return err;
761
+ }
662
762
  const candidatos = await candidatosNaPagina();
663
763
  const err = new Error(
664
764
  `seletor não encontrado: ${selector}`
@@ -1191,6 +1291,7 @@ function fecharRevisor() {
1191
1291
  }
1192
1292
 
1193
1293
  module.exports = {
1294
+ retrato, abrirVisivel, definirVisivel, estaVisivel,
1194
1295
  SEM_KEYCHAIN,
1195
1296
  scriptDaSeta, verSeta, findChrome,
1196
1297
  open, click, clickText, clickSelector, type, typeInSelector,
package/lib/casar.js ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * casar.js — acha no arquivo o trecho que o modelo MANDOU trocar, mesmo
3
+ * quando ele não copiou letra por letra.
4
+ *
5
+ * O modelo grátis erra o old_string de jeitos previsíveis: um espaço a mais
6
+ * no fim da linha, a indentação com 2 em vez de 4, o número de linha que o
7
+ * read_file com offset põe na frente ("12\t…"), um "\n" escapado. Cada erro
8
+ * desses virava "trecho não encontrado" → read_file de novo → edit de novo:
9
+ * duas voltas de modelo para um espaço. É o "fica o dia inteiro lendo o que
10
+ * fez". A ideia vem do opencode (MIT), que encadeia vários "casadores" do
11
+ * mais estrito ao mais frouxo; o código é nosso.
12
+ *
13
+ * A regra de ouro: frouxo, mas NUNCA ambíguo. Cada estratégia só vale se
14
+ * achar exatamente UM lugar. Dois lugares parecidos é pior que nenhum —
15
+ * trocar o errado estraga o arquivo calado.
16
+ *
17
+ * achar(conteudo, alvo) → { inicio, fim, como } | { erro: 'nenhum' | 'varios', vezes }
18
+ */
19
+
20
+ 'use strict';
21
+
22
+ // Onde está cada linha no texto: [inicio, fim) de cada uma, sem o \n.
23
+ function linhasComPosicao(texto) {
24
+ const out = [];
25
+ let i = 0;
26
+ for (const l of texto.split('\n')) {
27
+ out.push({ texto: l, inicio: i, fim: i + l.length });
28
+ i += l.length + 1;
29
+ }
30
+ return out;
31
+ }
32
+
33
+ /* Procura um bloco de linhas do alvo no conteúdo comparando cada linha por
34
+ `igual`. Devolve todos os lugares (em caracteres do conteúdo original). */
35
+ function porLinhas(conteudo, alvo, normalizar) {
36
+ const doArquivo = linhasComPosicao(conteudo);
37
+ let doAlvo = alvo.split('\n');
38
+ // Linha vazia no começo ou no fim do alvo é quase sempre sobra do modelo.
39
+ while (doAlvo.length > 1 && !doAlvo[0].trim()) doAlvo = doAlvo.slice(1);
40
+ while (doAlvo.length > 1 && !doAlvo[doAlvo.length - 1].trim()) doAlvo = doAlvo.slice(0, -1);
41
+ const alvoN = doAlvo.map(normalizar);
42
+ if (!alvoN.some(Boolean)) return [];
43
+ const achados = [];
44
+ for (let i = 0; i + alvoN.length <= doArquivo.length; i++) {
45
+ let bate = true;
46
+ for (let j = 0; j < alvoN.length; j++) {
47
+ if (normalizar(doArquivo[i + j].texto) !== alvoN[j]) { bate = false; break; }
48
+ }
49
+ if (bate) achados.push({ inicio: doArquivo[i].inicio, fim: doArquivo[i + alvoN.length - 1].fim });
50
+ }
51
+ return achados;
52
+ }
53
+
54
+ // Tira a indentação que todas as linhas não vazias têm em comum.
55
+ function semIndentacaoComum(linhas) {
56
+ const recuos = linhas.filter((l) => l.trim()).map((l) => l.match(/^[ \t]*/)[0].length);
57
+ const menor = recuos.length ? Math.min(...recuos) : 0;
58
+ return linhas.map((l) => l.slice(Math.min(menor, l.match(/^[ \t]*/)[0].length)));
59
+ }
60
+
61
+ function porIndentacao(conteudo, alvo) {
62
+ const doArquivo = linhasComPosicao(conteudo);
63
+ const doAlvo = semIndentacaoComum(alvo.replace(/^\n+|\n+$/g, '').split('\n')).map((l) => l.trimEnd());
64
+ const n = doAlvo.length;
65
+ if (!doAlvo.some((l) => l.trim())) return [];
66
+ const achados = [];
67
+ for (let i = 0; i + n <= doArquivo.length; i++) {
68
+ const janela = semIndentacaoComum(doArquivo.slice(i, i + n).map((l) => l.texto)).map((l) => l.trimEnd());
69
+ if (janela.every((l, j) => l === doAlvo[j])) {
70
+ achados.push({ inicio: doArquivo[i].inicio, fim: doArquivo[i + n - 1].fim });
71
+ }
72
+ }
73
+ return achados;
74
+ }
75
+
76
+ function ocorrencias(conteudo, alvo) {
77
+ const out = [];
78
+ if (!alvo) return out;
79
+ let i = conteudo.indexOf(alvo);
80
+ while (i !== -1) { out.push({ inicio: i, fim: i + alvo.length }); i = conteudo.indexOf(alvo, i + alvo.length); }
81
+ return out;
82
+ }
83
+
84
+ /* Do mais estrito ao mais frouxo. O primeiro que achar UM lugar vence; o que
85
+ achar vários para a busca ali mesmo (ficar mais frouxo só pioraria). */
86
+ const ESTRATEGIAS = [
87
+ ['exato', (c, a) => ocorrencias(c, a)],
88
+ // "12\t <div>" — o prefixo que o read_file com offset põe em cada linha.
89
+ ['sem número de linha', (c, a) => (/^\s*\d+\t/m.test(a) ? ocorrencias(c, a.replace(/^\s*\d+\t/gm, '')) : [])],
90
+ // "\\n" e "\\t" que vieram escapados de um JSON mal montado.
91
+ ['sem escape', (c, a) => (/\\[nt"]/.test(a) && !/\\[nt"]/.test(c)
92
+ ? ocorrencias(c, a.replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\"/g, '"')) : [])],
93
+ ['espaço no fim da linha', (c, a) => porLinhas(c, a, (l) => l.trimEnd())],
94
+ ['linha aparada', (c, a) => porLinhas(c, a, (l) => l.trim())],
95
+ ['indentação', (c, a) => porIndentacao(c, a)],
96
+ ['espaço normalizado', (c, a) => porLinhas(c, a, (l) => l.replace(/\s+/g, ' ').trim())],
97
+ ];
98
+
99
+ function achar(conteudo, alvo) {
100
+ for (const [como, f] of ESTRATEGIAS) {
101
+ const achados = f(conteudo, alvo);
102
+ if (achados.length === 1) return { ...achados[0], como };
103
+ if (achados.length > 1) return { erro: 'varios', vezes: achados.length, como };
104
+ }
105
+ return { erro: 'nenhum' };
106
+ }
107
+
108
+ module.exports = { achar, ocorrencias };
package/lib/catalogo.js CHANGED
@@ -87,12 +87,16 @@ const GRUPOS = [
87
87
  args: 'id. PREFIRA roteiro: {marca:{cores:{fundo,destaque,tinta},fontes:{titulo,texto},logo}, proporcao, '
88
88
  + 'partes:[{tipo, fala, …}]} — o PrimoCode faz o layout, a voz com entonação, os ícones, a música e os sons. '
89
89
  + 'Tipos: abertura{titulo,sub} titulo{titulo,sub} numero{valor,rotulo,sub} lista{titulo,itens} passos{titulo,itens} '
90
- + 'contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras:[{rotulo,valor}]} cta{titulo,link}. '
90
+ + 'contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras:[{rotulo,valor}]} cta{titulo,link} '
91
+ + 'logo{titulo} cores{cores:[{nome,hex}]} produto{titulo,texto,destaque,selo,barras}. Em qualquer parte: destaque (a palavra-tese), fonte (a fonte citada). '
92
+ + 'estilo: hibrido (PADRÃO: foto e vídeo reais que não param + o motion por cima; dê busca:"termo em inglês" por parte para o b-roll) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som sorteados do roteiro; é o do modo sem referências e o de quem pede motion/animação; com modelo3d o produto 3D entra numa batida) | classico (o motion 3D antigo) | vox (explainer editorial: tinta sobre papel, câmera que conta a frase) | '
93
+ + 'marca (brand reveal: brilho, letras em vetor, marca cromada, padrão) | lancamento (produto: cartões, painel, calendário) | impacto | cinema. '
94
+ + 'marca e lancamento aceitam visual:orbe|vetor|estrela|esfera|grade|site|padrao|cromo|cartoes|painel|cores numa parte. marca.nome dá o nome. '
91
95
  + 'voz (thalita|antonio|francisca). Ajuste fino: plano: {formato:{largura,altura,fps}, cenas:[…], audio:{…}} — ou cenas direto. '
92
96
  + 'cena (opcional): o número de UMA cena para trocar só ela, sem reescrever o resto. '
93
97
  + 'motivo (opcional): fica no caderno. voltarPara (opcional): nome de uma versão. '
94
98
  + 'Sem plano nem cenas, LÊ o plano atual. '
95
- + 'Cada elemento é {tipo: texto|forma|imagem|video|icone|legenda|componente} com x, y, cor, tamanho e '
99
+ + 'Cada elemento é {tipo: texto|forma|imagem|video|icone|legenda|componente|kit} com x, y, cor, tamanho e '
96
100
  + '"anima": {propriedade: [{t,v,e}]} — quadros-chave em segundos. A cena tem "camera": {zoom,x,y,rotacao} e '
97
101
  + '"perspectiva" para 3D. NÃO existe layout pronto nem tema — cada propriedade do vídeo é escolha sua, a cada vídeo.' },
98
102
 
@@ -575,6 +575,40 @@ function promptDoProtocolo(payload) {
575
575
  + '\n\nResponda com exatamente UM bloco marcado.';
576
576
  }
577
577
 
578
+ /* O `claude -p` ASSÍNCRONO. Era execFileSync: enquanto o Claude pensava (um
579
+ minuto, dois), o PrimoCode inteiro parava — sem spinner, sem recado, sem
580
+ Esc Esc. E o teto de 180 s estourava justo quando ele escrevia um arquivo
581
+ grande, e o erro saía cru na tela ("Command failed: claude -p …"). Agora
582
+ roda em paralelo, com 10 min de teto, uma segunda tentativa, e o erro
583
+ diz o que o Claude disse. */
584
+ function rodarClaude(args, input, { timeout = 600000, tentativas = 2 } = {}) {
585
+ const { spawn } = require('child_process');
586
+ const umaVez = () => new Promise((resolve, reject) => {
587
+ const p = spawn('claude', args, { env: process.env, stdio: ['pipe', 'pipe', 'pipe'], ...(SEM_APARECER.windowsHide ? { windowsHide: true } : {}) });
588
+ let out = '', err = '';
589
+ const relogio = setTimeout(() => { try { p.kill(); } catch {} reject(new Error(`o Claude Code não respondeu em ${Math.round(timeout / 60000)} min`)); }, timeout);
590
+ p.stdout.on('data', (d) => { out += d; });
591
+ p.stderr.on('data', (d) => { err += d; });
592
+ p.on('error', (e) => { clearTimeout(relogio); reject(e); });
593
+ p.on('close', (code) => {
594
+ clearTimeout(relogio);
595
+ if (code === 0) return resolve(out);
596
+ // O JSON às vezes vem com is_error e o motivo, mesmo com código != 0.
597
+ let motivo = '';
598
+ try { const j = JSON.parse(out); motivo = j.result || j.error || ''; } catch { /* sem json */ }
599
+ reject(new Error(`o Claude Code saiu com ${code}: ${String(motivo || err || out).replace(/\s+/g, ' ').trim().slice(0, 300)}`));
600
+ });
601
+ p.stdin.end(input);
602
+ });
603
+ return (async () => {
604
+ let ultimo;
605
+ for (let i = 0; i < tentativas; i++) {
606
+ try { return await umaVez(); } catch (e) { ultimo = e; }
607
+ }
608
+ throw ultimo;
609
+ })();
610
+ }
611
+
578
612
  async function requestActViaHeadless(payload, onEvent) {
579
613
  const corpoBootstrap = corpoDoProtocolo();
580
614
  const prompt = promptDoProtocolo(payload);
@@ -585,27 +619,14 @@ async function requestActViaHeadless(payload, onEvent) {
585
619
  // O prompt vai por STDIN, nunca por argumento: argumento tem limite de
586
620
  // tamanho (a conversa cresce) e qualquer conteúdo começando com traço
587
621
  // seria lido como opção. stdin não tem nenhum dos dois problemas.
588
- const saida = execFileSync('claude', [
589
- '-p',
590
- '--output-format', 'json',
591
- '--dangerously-skip-permissions',
592
- ...argsDoMotor(),
593
- ], { encoding: 'utf-8', timeout: 180000, input: prompt, maxBuffer: 32 * 1024 * 1024,
594
- env: process.env, ...SEM_APARECER });
622
+ const saida = await rodarClaude(['-p', '--output-format', 'json', '--dangerously-skip-permissions', ...argsDoMotor()], prompt);
595
623
  const r = JSON.parse(saida);
596
624
  // A correção é uma segunda ida ao motor, com o mesmo protocolo e o
597
625
  // diagnóstico do que saiu errado. Barata: o prompt é curto, e evita que
598
626
  // um erro de sintaxe do motor vire erro na cara do usuário.
599
- const pedirCorrecao = (aviso) => {
600
- const s = execFileSync('claude', [
601
- '-p', '--output-format', 'json', '--dangerously-skip-permissions',
602
- ...argsDoMotor(),
603
- ], {
604
- encoding: 'utf-8', timeout: 120000, maxBuffer: 32 * 1024 * 1024,
605
- env: process.env,
606
- input: promptDeCorrecao(aviso),
607
- ...SEM_APARECER,
608
- });
627
+ const pedirCorrecao = async (aviso) => {
628
+ const s = await rodarClaude(['-p', '--output-format', 'json', '--dangerously-skip-permissions', ...argsDoMotor()],
629
+ promptDeCorrecao(aviso), { timeout: 300000, tentativas: 1 });
609
630
  return JSON.parse(s).result || '';
610
631
  };
611
632
  await emitirDecisao(r.result || '', onEvent, pedirCorrecao, 0, nomesDasTools(payload));
@@ -131,22 +131,40 @@ function recusouOModelo(texto) {
131
131
  * `--model` e avisa — cair para o padrão da conta é melhor que a tarefa
132
132
  * morrer por causa de um nome que mudou de catálogo.
133
133
  */
134
- function chamarCodex(prompt, { executor, avisar = () => {} } = {}) {
134
+ /* O Codex ASSÍNCRONO, pelo mesmo motivo do claude -p (lib/claude-engine.js):
135
+ o execFileSync congelava o PrimoCode inteiro — spinner, recado, Esc Esc —
136
+ enquanto o Codex pensava. O erro mantém stdout/stderr para o diagnóstico. */
137
+ function rodarAssincrono(args, entrada) {
138
+ const { spawn } = require('child_process');
139
+ return new Promise((resolve, reject) => {
140
+ const p = spawn(CODEX, args, { env: process.env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
141
+ let out = '', err = '';
142
+ const relogio = setTimeout(() => { try { p.kill(); } catch {} const e = new Error(`o Codex não respondeu em ${Math.round(TEMPO / 60000)} min`); e.stderr = err; e.stdout = out; reject(e); }, TEMPO);
143
+ p.stdout.on('data', (d) => { out += d; if (out.length > BUFFER) { try { p.kill(); } catch {} } });
144
+ p.stderr.on('data', (d) => { err += d; });
145
+ p.on('error', (e) => { clearTimeout(relogio); reject(e); });
146
+ p.on('close', (code) => {
147
+ clearTimeout(relogio);
148
+ if (code === 0) return resolve(out);
149
+ const e = new Error(`Command failed: codex (saiu com ${code})`); e.stderr = err; e.stdout = out; reject(e);
150
+ });
151
+ p.stdin.end(entrada);
152
+ });
153
+ }
154
+
155
+ async function chamarCodex(prompt, { executor, avisar = () => {} } = {}) {
135
156
  // A chave própria da pessoa, quando ela configurou uma. Sem configuração
136
157
  // o ambiente vai INTACTO — passar a variável vazia apagaria a chave que
137
158
  // ela já tem exportada no shell, e o Codex pararia de funcionar por ter
138
159
  // sido "configurado".
139
- const rodar = executor || ((args, entrada) => execFileSync(CODEX, args, {
140
- encoding: 'utf-8', timeout: TEMPO, input: entrada, maxBuffer: BUFFER,
141
- env: process.env, ...SEM_APARECER,
142
- }));
160
+ const rodar = executor || rodarAssincrono;
143
161
  try {
144
- return rodar(argsDoExec(), prompt);
162
+ return await rodar(argsDoExec(), prompt);
145
163
  } catch (e) {
146
164
  const detalhe = `${e.message || ''}\n${(e.stderr || '')}\n${(e.stdout || '')}`;
147
165
  if (MODELO_CODEX && recusouOModelo(detalhe)) {
148
166
  avisar(`o Codex não aceitou o modelo ${MODELO_CODEX} — indo com o padrão da sua conta`);
149
- return rodar(argsDoExec({ comModelo: false }), prompt);
167
+ return await rodar(argsDoExec({ comModelo: false }), prompt);
150
168
  }
151
169
  throw new Error(primeiraLinhaUtil(detalhe) || e.message);
152
170
  }
@@ -185,12 +203,12 @@ async function requestAct(server, payload, onEvent) {
185
203
  return;
186
204
  }
187
205
  const prompt = claude.promptDoProtocolo(payload);
188
- const saida = chamarCodex(prompt, { avisar: (m) => onEvent({ aviso: m }) });
206
+ const saida = await chamarCodex(prompt, { avisar: (m) => onEvent({ aviso: m }) });
189
207
 
190
208
  // A correção é uma segunda ida ao motor, com o mesmo protocolo e o
191
209
  // diagnóstico do que saiu errado. Barata, e evita que um erro de forma
192
210
  // do motor vire erro na cara de quem pediu a tarefa.
193
- const pedirCorrecao = (aviso) => chamarCodex(claude.promptDeCorrecao(aviso));
211
+ const pedirCorrecao = (aviso) => chamarCodex(claude.promptDeCorrecao(aviso)); // devolve promessa: quem chama dá await
194
212
 
195
213
  await claude.emitirDecisao(saida, onEvent, pedirCorrecao, 0,
196
214
  claude.nomesDasTools(payload));
package/lib/config.js CHANGED
@@ -74,8 +74,11 @@ function ensureDir(dir) {
74
74
  try { fs.mkdirSync(dir, { recursive: true }); } catch {}
75
75
  }
76
76
 
77
+ // O dia LOCAL: toISOString é UTC, e no Brasil depois das 21h a memória saía
78
+ // datada no dia seguinte.
77
79
  function today() {
78
- return new Date().toISOString().split('T')[0];
80
+ const d = new Date();
81
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
79
82
  }
80
83
 
81
84
  function ensureStructure() {
@@ -99,12 +102,16 @@ function slugify(name) {
99
102
  .toLowerCase()
100
103
  .replace(/[^a-z0-9]+/g, '-')
101
104
  .replace(/^-+|-+$/g, '')
102
- .slice(0, 60);
105
+ .slice(0, 60)
106
+ .replace(/-+$/, ''); // o corte pode deixar hífen no fim
103
107
  }
104
108
 
105
109
  function createProjectDir(projectName) {
106
110
  ensureStructure();
107
- const slug = slugify(projectName);
111
+ /* Nome sem letra nem número ("!!!", só emoji, só japonês) dava slug
112
+ VAZIO — e path.join(ROOT_DIR, '') é a própria raiz: o "projeto" virava
113
+ ~/primocode, com memory.md gravado nela. */
114
+ const slug = slugify(projectName) || `projeto-${today()}`;
108
115
  const projectDir = path.join(ROOT_DIR, slug);
109
116
  ensureDir(projectDir);
110
117
  const memoryPath = path.join(projectDir, 'memory.md');
package/lib/cursor.js CHANGED
@@ -95,7 +95,16 @@ function subir() {
95
95
  };
96
96
  proc.on('exit', morreu);
97
97
  proc.on('error', morreu);
98
- setTimeout(() => { if (!subiu) { semCursor = 'o cursor demorou demais para abrir'; resolve(false); } }, 20000);
98
+ // Demorou demais: desiste E mata — senão o Python ficava órfão, e o
99
+ // `pronto` guardado como false deixava o cursor fora a sessão toda.
100
+ const teto = setTimeout(() => {
101
+ if (subiu) return;
102
+ semCursor = 'o cursor demorou demais para abrir';
103
+ try { proc && proc.kill(); } catch { /* já morreu */ }
104
+ pronto = null;
105
+ resolve(false);
106
+ }, 20000);
107
+ if (teto.unref) teto.unref();
99
108
  // O cursor não pode segurar o PrimoCode aberto na saída.
100
109
  proc.unref();
101
110
  proc.stdout.unref && proc.stdout.unref();
package/lib/entrada.js CHANGED
@@ -43,6 +43,10 @@ const { c, visibleWidth, truncate } = require('./ui.js');
43
43
 
44
44
  // ── O desenho ────────────────────────────────────────────────────────────
45
45
 
46
+ /** A etiqueta de colagem em destaque (fininha, na cor da marca), sem mudar a largura. */
47
+ const RE_ETIQUETA = /\[Colado #\d+ · [^\]]+\]/g;
48
+ function pintarEtiquetas(s) { return String(s).replace(RE_ETIQUETA, (m) => c.brand(m) + '\x1b[38;5;254m'); }
49
+
46
50
  const BORDA = { ne: '╭', no: '╮', se: '╰', so: '╯', h: '─', v: '│' };
47
51
 
48
52
  /** Preenche até `w` colunas VISÍVEIS (códigos de cor não ocupam coluna). */
@@ -128,6 +132,12 @@ function montar(e) {
128
132
 
129
133
  const out = [];
130
134
  const regua = (bash ? c.warn : c.dim)(BORDA.h.repeat(largura));
135
+ /* O SPINNER MORA AQUI, na linha logo acima da caixa. Ele escrevia direto
136
+ no terminal com "\r" a cada 80 ms, sem quebra de linha — e a caixa,
137
+ que espera a linha fechar para se redesenhar, ficava apagada enquanto
138
+ ele girasse: o que se digitava não aparecia, e a barra de status
139
+ sumia ("não consigo digitar enquanto ele trabalha"). */
140
+ if (e.spinner) out.push(' ' + truncate(e.spinner, largura - 2));
131
141
  out.push(regua);
132
142
 
133
143
  const vazio = !String(e.texto).length;
@@ -137,13 +147,14 @@ function montar(e) {
137
147
  : c.dim('┆ ');
138
148
  // O "!" some do texto desenhado: quem vê "$ " já sabe que é shell, e
139
149
  // mostrar os dois é mostrar a mesma informação duas vezes.
140
- let conteudo = c.white(i === 0 && bash ? l.texto.replace(/^\s*!/, '') : l.texto);
150
+ let conteudo = pintarEtiquetas(c.white(i === 0 && bash ? l.texto.replace(/^\s*!/, '') : l.texto));
141
151
  // O fantasma só existe na última linha e só com o cursor no fim dela:
142
152
  // desenhado no meio do texto, ele empurraria o que vem depois.
143
153
  if (i === linhas.length - 1 && e.fantasma && cursor === String(e.texto).length) {
144
154
  conteudo += c.dim(e.fantasma);
145
155
  }
146
- if (i === 0 && vazio && e.vazio) conteudo = c.dim(e.vazio);
156
+ if (i === 0 && vazio && e.ocupado) conteudo = c.dim('mande um recado — ele entra na tarefa depois do passo atual');
157
+ else if (i === 0 && vazio && e.vazio) conteudo = c.dim(e.vazio);
147
158
  // Sem `encher`: sem barra à direita não há nada para alinhar, e o
148
159
  // espaço sobrando só apagaria o que estivesse embaixo.
149
160
  out.push(' ' + marca + conteudo);
@@ -315,7 +326,8 @@ class Entrada extends EventEmitter {
315
326
  suspender() { this.suspenso = true; this._apagar(); }
316
327
  retomar() { this.suspenso = false; if (this.ativo && !this.fechado) this._desenhar(); }
317
328
 
318
- /** O agente está trabalhando: a caixa esmaece e o Enter não manda nada. */
329
+ /** O agente está trabalhando: a caixa esmaece, e o Enter manda um RECADO
330
+ * que entra na tarefa em curso (a caixa de entrada de lib/act.js). */
319
331
  trabalhando(sim) {
320
332
  if (this.ocupado === !!sim) return;
321
333
  this.ocupado = !!sim;
@@ -324,6 +336,15 @@ class Entrada extends EventEmitter {
324
336
 
325
337
  prompt() { if (!this.suspenso && this.tty && !this.fechado) this._desenhar(); }
326
338
 
339
+ /** O spinner (lib/ui.js) desenha por aqui quando há caixa: uma linha
340
+ * acima dela, redesenhada junto — nunca um "\r" solto por cima. */
341
+ mostrarSpinner(texto) {
342
+ const novo = texto || '';
343
+ if (novo === (this.spinnerTexto || '')) return;
344
+ this.spinnerTexto = novo;
345
+ if (this.desenhado && !this._meiaLinha) this._redesenhar();
346
+ }
347
+
327
348
  /** Escreve texto ACIMA da caixa — é por onde todo o resto do programa fala. */
328
349
  escreverAcima(texto) {
329
350
  if (!this.desenhado) { this.escreverCru(texto); return; }
@@ -340,7 +361,40 @@ class Entrada extends EventEmitter {
340
361
  this._mudou();
341
362
  }
342
363
 
343
- limpar() { this.line = ''; this.cursor = 0; this.menu = null; this._mudou(); }
364
+ limpar() { this.line = ''; this.cursor = 0; this.menu = null; this.colados = new Map(); this._mudou(); }
365
+
366
+ /* COLAR VIRA ETIQUETA, como no Claude Code: texto grande colado (várias
367
+ linhas, ou mais de 300 caracteres) entra na caixa como
368
+ "[Colado #1 · 42 linhas]" — a caixa não vira um paredão, dá para
369
+ escrever o pedido em volta, e no Enter a etiqueta volta a ser o texto
370
+ inteiro. O que é curto entra como texto mesmo. */
371
+ colar(texto) {
372
+ const t = String(texto || '');
373
+ if (!t) return;
374
+ const linhas = t.split('\n').length;
375
+ if (linhas < 3 && t.length <= 300) return this.write(t);
376
+ if (!this.colados) this.colados = new Map();
377
+ const n = this.colados.size + 1;
378
+ const etiqueta = `[Colado #${n} · ${linhas > 1 ? `${linhas} linhas` : `${t.length} caracteres`}]`;
379
+ this.colados.set(etiqueta, t);
380
+ this.write(etiqueta);
381
+ }
382
+
383
+ /** A linha com cada etiqueta trocada pelo texto que ela guarda. */
384
+ expandir(linha) {
385
+ if (!this.colados || !this.colados.size) return linha;
386
+ let out = linha;
387
+ for (const [et, t] of this.colados) out = out.split(et).join(t);
388
+ return out;
389
+ }
390
+
391
+ /** A etiqueta que termina exatamente no cursor (o backspace apaga ela inteira). */
392
+ _etiquetaAntes() {
393
+ if (!this.colados) return null;
394
+ const antes = this.line.slice(0, this.cursor);
395
+ for (const et of this.colados.keys()) if (antes.endsWith(et)) return et;
396
+ return null;
397
+ }
344
398
 
345
399
  /**
346
400
  * A PRÓXIMA linha é senha: a caixa desenha `•` no lugar das letras, não
@@ -361,6 +415,7 @@ class Entrada extends EventEmitter {
361
415
  status: this.op.status ? this.op.status() : [],
362
416
  menu: this.menu,
363
417
  ocupado: this.ocupado,
418
+ spinner: this.spinnerTexto || '',
364
419
  });
365
420
  }
366
421
 
@@ -434,11 +489,11 @@ class Entrada extends EventEmitter {
434
489
  const seq = (key && key.sequence) || ch || '';
435
490
 
436
491
  // Colagem entre marcas: tudo que vier até a marca de fim é texto.
437
- if (seq.includes('\x1b[200~')) { this.colando = true; }
492
+ if (seq.includes('\x1b[200~')) { this.colando = true; this.bufColagem = ''; }
438
493
  if (this.colando) {
439
494
  const limpo = seq.replace('\x1b[200~', '').replace('\x1b[201~', '');
440
- if (limpo) this.write(limpo.replace(/\r/g, '\n'));
441
- if (seq.includes('\x1b[201~')) this.colando = false;
495
+ if (limpo) this.bufColagem += limpo.replace(/\r\n?/g, '\n');
496
+ if (seq.includes('\x1b[201~')) { this.colando = false; this.colar(this.bufColagem); this.bufColagem = ''; }
442
497
  return;
443
498
  }
444
499
 
@@ -476,7 +531,15 @@ class Entrada extends EventEmitter {
476
531
  return;
477
532
  }
478
533
 
479
- case 'backspace':
534
+ case 'backspace': {
535
+ const et = this._etiquetaAntes();
536
+ if (et) {
537
+ this.line = this.line.slice(0, this.cursor - et.length) + this.line.slice(this.cursor);
538
+ this.cursor -= et.length;
539
+ this.colados.delete(et);
540
+ return this._mudou();
541
+ }
542
+ }
480
543
  if (this.cursor > 0) {
481
544
  this.line = this.line.slice(0, this.cursor - 1) + this.line.slice(this.cursor);
482
545
  this.cursor--;
@@ -583,7 +646,9 @@ class Entrada extends EventEmitter {
583
646
 
584
647
  _enviar() {
585
648
  if (this.menu) return this._escolherDoMenu();
586
- const texto = this.line;
649
+ const visto = this.line;
650
+ const texto = this.expandir(visto);
651
+ this.colados = new Map();
587
652
  const oculto = this.oculto;
588
653
  this.oculto = false;
589
654
  this.line = '';
@@ -597,7 +662,8 @@ class Entrada extends EventEmitter {
597
662
  this._apagar();
598
663
  if (texto.trim() && !oculto) {
599
664
  const marca = c.brand('❯ ');
600
- this.escreverCru(texto.split('\n').map((l, i) => (i ? ' ' : marca) + c.white(l)).join('\n') + '\n');
665
+ // Na tela fica a etiqueta (o que se viu ao mandar); quem vai ao agente é o texto inteiro.
666
+ this.escreverCru(visto.split('\n').map((l, i) => (i ? ' ' : marca) + pintarEtiquetas(c.white(l))).join('\n') + '\n');
601
667
  if (this.historico[this.historico.length - 1] !== texto) this.historico.push(texto);
602
668
  }
603
669
  this._desenhar();
@@ -607,4 +673,4 @@ class Entrada extends EventEmitter {
607
673
 
608
674
  function criar(op) { return new Entrada(op).iniciar(); }
609
675
 
610
- module.exports = { criar, Entrada, montar, quebrar, encher };
676
+ module.exports = { criar, Entrada, montar, quebrar, encher, pintarEtiquetas };