primocode 8.10.5 → 8.11.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/bin/primocode.js CHANGED
@@ -531,6 +531,13 @@ async function sendPrompt(userText) {
531
531
  }
532
532
 
533
533
  if (modo === 'primocode') {
534
+ // "baixa o vídeo" / "exporta em mp4" não precisa de IA NENHUMA: é
535
+ // abrir a última criação do Studio com #gravar — o navegador pede a
536
+ // permissão de tela (a aprovação), grava a passada e baixa no fim.
537
+ // Antes isso virava uma tarefa inteira de modelo (id inventado,
538
+ // mídia inventada, limite estourado) para chegar no mesmo lugar.
539
+ if (await baixarCriacaoDireto(userText)) { refreshPrompt(); return; }
540
+
534
541
  const plan = prepararPasta(userText);
535
542
 
536
543
  // Antes de gastar com a tarefa, dobra no resumo o que já passou. A
@@ -632,6 +639,30 @@ async function sendPrompt(userText) {
632
639
  const RE_DESCOBRIR = /\b(pesquis|busca|procur|descubr|investig|analis|leia|estud|olh[ae] (o site|a p[áa]gina)|ver o que)\w*/i;
633
640
  const RE_CRIAR_DEPOIS = /\b(cri[ae]|faz|faça|mont[ae]|ger[ae]|escrev[ae]|produz|desenvolv)\w*/i;
634
641
 
642
+ /** "baixa/exporta o vídeo" resolvido na hora, sem gastar modelo. */
643
+ const RE_BAIXAR = /\b(baixa\w*|exporta\w*|download|salva\w*)\b/i;
644
+ const RE_PECA_STUDIO = /\b(v[ií]deo|mp4|mp3|apresenta[çc][ãa]o|slides?|reels?)\b/i;
645
+
646
+ async function baixarCriacaoDireto(texto) {
647
+ if (!(RE_BAIXAR.test(texto) && RE_PECA_STUDIO.test(texto))) return false;
648
+ try {
649
+ const studio = require('../lib/studio');
650
+ const r = await studio.listar();
651
+ const lista = (r && r.arquivos) || [];
652
+ // a criação mais nova que é vídeo; sem vídeo, a mais nova de todas
653
+ const alvo = lista.find((a) => a.ferramenta === 'corte') || lista[0];
654
+ if (!alvo) return false;
655
+ const url = `http://localhost:7777/v/${alvo.id}#gravar`;
656
+ await tools.execute('open_in_browser', { target: url }, { projectDir: state.workDir || os.homedir() });
657
+ console.log(c.ok(` ⎿ abrindo "${alvo.titulo}" para gravar`));
658
+ console.log(c.muted(' O navegador vai pedir permissão de tela — aceite (marque "compartilhar áudio da aba"'));
659
+ console.log(c.muted(' para levar a narração junto). O vídeo roda sozinho e baixa no fim.'));
660
+ return true;
661
+ } catch {
662
+ return false; // Studio fora do ar ou sem criações: segue o caminho normal
663
+ }
664
+ }
665
+
635
666
  async function dividirTarefa(pedido) {
636
667
  const inteiro = [{ titulo: 'tarefa', pedido }];
637
668
  if (!pedido || pedido.trim().length < 12) return inteiro;
package/lib/act.js CHANGED
@@ -295,9 +295,11 @@ function encolherParaContinuar(messages) {
295
295
 
296
296
  /** Quanto esperar quando o minuto estourou. O provedor costuma dizer
297
297
  * ("Please try again in 7.664s"); sem isso, meio minuto é seguro. */
298
- function esperaDoErro(e) {
298
+ function esperaDoErro(e, tentativa = 0) {
299
299
  const m = /try again in ([0-9.]+)s/i.exec(String((e && e.message) || e || ''));
300
- const s = m ? Math.ceil(parseFloat(m[1])) + 2 : 30;
300
+ // Sem o tempo do provedor, cresce a passada a cada tentativa: bater na
301
+ // mesma porta a cada 30s por horas só esquenta o rate limit.
302
+ const s = m ? Math.ceil(parseFloat(m[1])) + 2 : Math.min(70, 30 + tentativa * 10);
301
303
  return Math.min(70, Math.max(5, s)) * 1000;
302
304
  }
303
305
 
@@ -381,7 +383,12 @@ async function runAct(userPrompt, opts) {
381
383
  // isso (num caso real "encolheu" de 2 para 2 mensagens); esperar a janela
382
384
  // virar, sim.
383
385
  let esperas = 0;
384
- const MAX_ESPERAS = 3;
386
+ // Ordem do dono: limite de token NUNCA mais encerra a tarefa. Ele espera
387
+ // o quanto precisar — Esc Esc continua sendo a saída na mão do usuário.
388
+ // 200 esperas de até 70s são horas: na prática, para quem não apertou
389
+ // Esc, é "espera até dar".
390
+ // O teste derruba para 2 via env; em produção são 200.
391
+ const MAX_ESPERAS = Number(process.env.PRIMOCODE_MAX_ESPERAS || 200);
385
392
 
386
393
  let voltas = 0;
387
394
  for (let iteration = 0; iteration < MAX_ITERATIONS && !PARAR; iteration++) {
@@ -424,7 +431,7 @@ async function runAct(userPrompt, opts) {
424
431
  }
425
432
  if (esperas < MAX_ESPERAS) {
426
433
  esperas++;
427
- const ms = esperaDoErro(e);
434
+ const ms = esperaDoErro(e, esperas);
428
435
  if (!subagent) {
429
436
  const sp2 = new Spinner(`limite do minuto — continuando em ${Math.round(ms / 1000)}s`).start();
430
437
  await new Promise((r) => setTimeout(r, ms));
@@ -451,7 +458,7 @@ async function runAct(userPrompt, opts) {
451
458
  // adjust your prompt.") caía na tela de quem não tem prompt nenhum
452
459
  // para ajustar.
453
460
  if (streamError && /failed to call a function|failed_generation|tool_use_failed/i.test(String(streamError))
454
- && funcoesQuebradas < 2 && !PARAR) {
461
+ && funcoesQuebradas < 4 && !PARAR) {
455
462
  funcoesQuebradas++;
456
463
  if (!subagent) console.log(ident(profundidade) + c.dim(' ⎿ o modelo tropeçou na chamada — tentando de novo'));
457
464
  iteration--;
@@ -474,7 +481,7 @@ async function runAct(userPrompt, opts) {
474
481
  // anteriores gastaram a cota. Espera a janela virar e repete.
475
482
  if (esperas < MAX_ESPERAS) {
476
483
  esperas++;
477
- const ms = esperaDoErro(streamError);
484
+ const ms = esperaDoErro(streamError, esperas);
478
485
  if (!subagent) {
479
486
  const sp2 = new Spinner(`limite do minuto — continuando em ${Math.round(ms / 1000)}s`).start();
480
487
  await new Promise((r) => setTimeout(r, ms));
@@ -51,12 +51,35 @@ function extrairDecisao(texto) {
51
51
  return null;
52
52
  }
53
53
 
54
+ /** JSON com quebra de linha CRUA dentro das aspas — o jeito mais comum de o
55
+ * motor falhar. O Claude escreve o content de um write_file com linhas de
56
+ * verdade em vez de \n, o JSON.parse recusa, e o turno inteiro morria com
57
+ * "decisão que não consegui ler". O conserto é mecânico: dentro de string,
58
+ * controle vira escape. */
59
+ function consertarControles(s) {
60
+ let saida = '';
61
+ let dentro = false, escapa = false;
62
+ for (const c of String(s || '')) {
63
+ if (dentro && !escapa && (c === '\n' || c === '\r' || c === '\t')) {
64
+ saida += c === '\n' ? '\\n' : c === '\r' ? '\\r' : '\\t';
65
+ continue;
66
+ }
67
+ if (escapa) { escapa = false; saida += c; continue; }
68
+ if (c === '\\') { escapa = true; saida += c; continue; }
69
+ if (c === '"') dentro = !dentro;
70
+ saida += c;
71
+ }
72
+ return saida;
73
+ }
74
+
54
75
  /** O JSON do bloco, tolerando o que o modelo costuma acrescentar sem querer. */
55
76
  function lerJSON(bruto) {
56
77
  let s = String(bruto || '').trim();
57
78
  // Cerca de markdown em volta do JSON — o modelo põe por hábito.
58
79
  s = s.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '').trim();
59
80
  try { return JSON.parse(s); } catch {}
81
+ try { return JSON.parse(consertarControles(s)); } catch {}
82
+ s = consertarControles(s);
60
83
  // Sobrou texto depois do objeto? Corta no fecha-chaves que casa com o primeiro.
61
84
  const abre = s.indexOf('{');
62
85
  if (abre === -1) return null;
@@ -69,7 +92,9 @@ function lerJSON(bruto) {
69
92
  if (dentro) continue;
70
93
  if (c === '{') nivel++;
71
94
  else if (c === '}' && --nivel === 0) {
72
- try { return JSON.parse(s.slice(abre, k + 1)); } catch { return null; }
95
+ const trecho = s.slice(abre, k + 1);
96
+ try { return JSON.parse(trecho); } catch {}
97
+ try { return JSON.parse(consertarControles(trecho)); } catch { return null; }
73
98
  }
74
99
  }
75
100
  return null;
@@ -268,15 +293,17 @@ function ultimaMensagem(messages) {
268
293
  function emitirDecisao(textoLido, onEvent) {
269
294
  const decisao = extrairDecisao(textoLido);
270
295
  if (!decisao) {
271
- // Resposta sem bloco legível. Se ela AINDA ASSIM traz a marca do
272
- // protocolo, o motor falhou: mostrar isso cru — com "tool_call" e
273
- // JSON na cara do usuário — é pior do que dizer que falhou.
274
- let conteudo = String(textoLido || '').trim();
275
- if (conteudo.includes(MARCA_INICIO) || conteudo.includes(MARCA_FIM)) {
276
- conteudo = conteudo.split(MARCA_INICIO)[0].trim()
277
- || 'O motor Claude Code devolveu uma decisão que não consegui ler. Tente de novo, ou volte ao motor padrão com /claude off.';
296
+ const cru = String(textoLido || '').trim();
297
+ // Traz a marca do protocolo mas o bloco não dá para ler? O motor
298
+ // TENTOU chamar ferramenta e errou a sintaxe. Antes isso virava
299
+ // desistência na tela; agora vira o mesmo erro que o loop já sabe
300
+ // repetir ("Failed to call a function" dispara retry da rodada).
301
+ if (cru.includes(MARCA_INICIO) || cru.includes(MARCA_FIM)) {
302
+ onEvent({ erro: 'Failed to call a function: o motor Claude devolveu um bloco ilegível — repetindo a rodada.' });
303
+ return;
278
304
  }
279
- onEvent({ assistant_message: { content: conteudo } });
305
+ // Sem marca nenhuma: é resposta em texto, e texto é resposta válida.
306
+ onEvent({ assistant_message: { content: cru } });
280
307
  return;
281
308
  }
282
309
  if (decisao.tool_call) {
@@ -361,7 +388,14 @@ function linhaDoHistorico(m) {
361
388
 
362
389
  async function requestActViaHeadless(payload, onEvent) {
363
390
  const historico = (payload.messages || []).map(linhaDoHistorico).join('\n\n');
364
- const prompt = 'Use a skill primocode-engine para esta chamada.\n\n' + historico;
391
+ // O protocolo vai INTEIRO no prompt. A versão anterior dizia "use a skill
392
+ // primocode-engine" e confiava que o headless a carregaria — quando não
393
+ // carregava, o Claude respondia sem bloco nenhum e o turno morria. Prompt
394
+ // maior, mas o Claude Code tem contexto de sobra, e o formato SEMPRE chega.
395
+ const prompt = textoBootstrap()
396
+ + '\n\nIMPORTANTE: no JSON do bloco, escreva quebras de linha como \\n (escapadas), nunca linha de verdade.'
397
+ + '\n\n# A conversa até aqui\n\n' + historico
398
+ + '\n\nResponda com exatamente UM bloco marcado.';
365
399
  // --dangerously-skip-permissions: mesma razão do caminho ORCA acima —
366
400
  // a skill já proíbe o Claude Code de usar suas próprias ferramentas
367
401
  // nativas; a flag só evita que ele pare pedindo permissão pra si mesmo
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.10.5",
3
+ "version": "8.11.0",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. 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": {
@@ -71,6 +71,16 @@
71
71
  montarTransporte();
72
72
  prepararTrilha();
73
73
  tocar();
74
+ // /v/<id>#gravar: o CLI abre assim quando o usuário pede "baixa o
75
+ // vídeo". O navegador pede a permissão de tela (a "aprovação"), o
76
+ // usuário aceita, o vídeo roda do zero gravando e baixa no fim —
77
+ // nenhuma chamada de IA no caminho inteiro.
78
+ if (location.hash.indexOf("gravar") !== -1) {
79
+ setTimeout(function () {
80
+ aviso("aceite a permissão para gravar — o download sai no fim", 4000);
81
+ iniciarGravacao();
82
+ }, 700);
83
+ }
74
84
  }
75
85
  }
76
86
 
@@ -400,8 +410,11 @@
400
410
  }
401
411
  var misto = new MediaStream(faixas);
402
412
  pedacos = [];
403
- var tipo = MediaRecorder.isTypeSupported("video/webm;codecs=vp9,opus")
404
- ? "video/webm;codecs=vp9,opus" : "video/webm";
413
+ // MP4 primeiro, quando o navegador sabe gravar mp4 (Safari sabe;
414
+ // Chrome grava webm). O usuário pediu "baixar em mp4": onde dá, dá.
415
+ var tipo = MediaRecorder.isTypeSupported("video/mp4") ? "video/mp4"
416
+ : MediaRecorder.isTypeSupported("video/webm;codecs=vp9,opus")
417
+ ? "video/webm;codecs=vp9,opus" : "video/webm";
405
418
  gravador = new MediaRecorder(misto, { mimeType: tipo, videoBitsPerSecond: 8000000 });
406
419
  gravador.ondataavailable = function (ev) { if (ev.data.size) pedacos.push(ev.data); };
407
420
  gravador.onstop = baixarGravacao;
@@ -424,14 +437,16 @@
424
437
  }
425
438
 
426
439
  function baixarGravacao() {
427
- var blob = new Blob(pedacos, { type: "video/webm" });
440
+ var mime = (gravador && gravador.mimeType) || "video/webm";
441
+ var ext = /mp4/.test(mime) ? ".mp4" : ".webm";
442
+ var blob = new Blob(pedacos, { type: mime.split(";")[0] });
428
443
  var url = URL.createObjectURL(blob);
429
444
  var a = document.createElement("a");
430
445
  a.href = url;
431
- a.download = (arq.titulo || "video").replace(/[^\w\-]+/g, "-").toLowerCase() + ".webm";
446
+ a.download = (arq.titulo || "video").replace(/[^\w\-]+/g, "-").toLowerCase() + ext;
432
447
  a.click();
433
448
  setTimeout(function () { URL.revokeObjectURL(url); }, 4000);
434
- aviso("vídeo salvo em .webm", 2600);
449
+ aviso("vídeo salvo em " + ext, 2600);
435
450
  }
436
451
 
437
452
  /* -------------------------------------------------------------- print -- */