primocode 9.10.0 → 10.0.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/README.md +30 -6
- package/bin/primocode.js +100 -341
- package/lib/act.js +245 -23
- package/lib/browser.js +17 -0
- package/lib/casar.js +108 -0
- package/lib/catalogo.js +6 -2
- package/lib/config.js +10 -3
- package/lib/cursor.js +10 -1
- package/lib/entrada.js +4 -2
- package/lib/estudio/diretor.js +9 -1
- package/lib/estudio/estilo.js +51 -0
- package/lib/estudio/glifos.js +215 -0
- package/lib/estudio/index.js +41 -18
- package/lib/estudio/kit/base.jsx +285 -0
- package/lib/estudio/kit/index.jsx +11 -0
- package/lib/estudio/kit/marca.jsx +741 -0
- package/lib/estudio/kit/vox.jsx +357 -0
- package/lib/estudio/kit.js +72 -0
- package/lib/estudio/lancamento.js +413 -0
- package/lib/estudio/motion.js +2 -5
- package/lib/estudio/plano.js +28 -3
- package/lib/estudio/remotion.js +87 -43
- package/lib/estudio/ritmo.js +77 -0
- package/lib/estudio/vox.js +385 -0
- package/lib/fala.js +1 -94
- package/lib/pipeline.js +55 -56
- package/lib/repeticao.js +3 -1
- package/lib/skills.js +8 -2
- package/lib/snapshot.js +111 -0
- package/lib/tools.js +79 -20
- package/lib/ui.js +13 -6
- package/lib/video/analise.js +89 -0
- package/lib/video/cli.js +74 -0
- package/lib/video/delta.js +90 -0
- package/lib/video/edits.js +379 -0
- package/lib/video/filters.js +247 -0
- package/lib/video/ops.js +322 -0
- package/lib/video/projeto.js +133 -0
- package/lib/video/render.js +221 -0
- package/lib/video/texto.js +142 -0
- package/lib/video/timeline.js +310 -0
- package/lib/video/transcript.js +124 -0
- package/lib/voz.js +75 -223
- package/package.json +2 -3
- package/skills/motion-de-primeira/SKILL.md +3 -4
- package/studio/texto.py +208 -0
- package/voz/primo_voz/__main__.py +96 -236
- package/voz/primo_voz/transcrever.py +31 -1
- package/app/ansi.js +0 -55
- package/app/app.css +0 -500
- package/app/app.js +0 -1017
- package/app/icone.svg +0 -6
- package/app/index.html +0 -220
- package/app/primo-marca.js +0 -192
- package/lib/app.js +0 -615
- package/lib/icone.js +0 -263
- package/lib/instalar-app.js +0 -231
- package/lib/painel.js +0 -291
- package/voz/primo_voz/desperta.py +0 -195
- package/voz/primo_voz/orbe.py +0 -406
package/lib/act.js
CHANGED
|
@@ -55,6 +55,44 @@ function interromper() {
|
|
|
55
55
|
if (emVoo) { try { emVoo.abort(); } catch {} }
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/* ── A CAIXA DE ENTRADA: o que a pessoa digita enquanto ele trabalha ────
|
|
59
|
+
* Antes, a linha digitada durante uma tarefa ia para a fila do terminal e só
|
|
60
|
+
* rodava DEPOIS — como um pedido novo, que não sabia nada do que estava em
|
|
61
|
+
* curso. "Mesmo quando ela estiver trabalhando poderá enviar mensagem; ela
|
|
62
|
+
* vai à fila e, quando o PrimoCode termina um passo, recebe a mensagem e
|
|
63
|
+
* integra na tarefa." É o que o Claude Code e o Codex fazem: o recado entra
|
|
64
|
+
* na conversa na fronteira seguinte — depois do resultado das ferramentas,
|
|
65
|
+
* antes da próxima ida ao modelo. Nenhuma ferramenta é cortada no meio.
|
|
66
|
+
*
|
|
67
|
+
* Só o pedido PRINCIPAL lê a caixa (o subagente trabalha para ele). O que
|
|
68
|
+
* sobrar quando a tarefa acabar volta para o terminal como pedido normal. */
|
|
69
|
+
const CAIXA = [];
|
|
70
|
+
let TRABALHANDO = false;
|
|
71
|
+
function mandarDuranteATarefa(texto) {
|
|
72
|
+
const t = String(texto || '').trim();
|
|
73
|
+
if (!t || !TRABALHANDO) return false;
|
|
74
|
+
CAIXA.push(t);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
function trabalhando() { return TRABALHANDO; }
|
|
78
|
+
/** O que ficou na caixa sem ser lido — o terminal roda como pedido novo. */
|
|
79
|
+
function sobrasDaCaixa() { return CAIXA.splice(0); }
|
|
80
|
+
/* Esvazia a caixa dentro da conversa. Devolve quantos recados entraram. */
|
|
81
|
+
function entregarRecados(messages, profundidade) {
|
|
82
|
+
if (!CAIXA.length) return 0;
|
|
83
|
+
const recados = CAIXA.splice(0);
|
|
84
|
+
for (const r of recados) fala.pensamento(`recado recebido — entrando na tarefa: ${r}`, profundidade);
|
|
85
|
+
messages.push({
|
|
86
|
+
role: 'user',
|
|
87
|
+
content: (recados.length === 1 ? 'MENSAGEM NOVA do usuário, enviada enquanto você trabalhava:\n'
|
|
88
|
+
: 'MENSAGENS NOVAS do usuário, enviadas enquanto você trabalhava:\n')
|
|
89
|
+
+ recados.map((r) => `- ${r}`).join('\n')
|
|
90
|
+
+ '\n\nIntegre isto na tarefa em curso: se é um ajuste, aplique; se muda o rumo, siga o novo rumo; '
|
|
91
|
+
+ 'se é uma pergunta, responda em uma frase e continue. Não recomece o que já está feito.',
|
|
92
|
+
});
|
|
93
|
+
return recados.length;
|
|
94
|
+
}
|
|
95
|
+
|
|
58
96
|
/**
|
|
59
97
|
* Este erro é "não coube no minuto"?
|
|
60
98
|
*
|
|
@@ -138,9 +176,6 @@ function limparParaOModelo(result) {
|
|
|
138
176
|
|
|
139
177
|
function logResult(kind, result, profundidade) {
|
|
140
178
|
const p = fala.resultado(profundidade);
|
|
141
|
-
// A linha da janela (lib/app.js) fecha aqui: o mesmo resultado que o
|
|
142
|
-
// terminal desenha embaixo, resumido. Ver o canal em lib/fala.js.
|
|
143
|
-
fala.avisar('resultado', fala.resumoDoResultado(kind, result));
|
|
144
179
|
if (!result) return;
|
|
145
180
|
// Uma tool que falhou é informação para o AGENTE, que vai consertar na
|
|
146
181
|
// volta seguinte — não é o erro do usuário. Âmbar, uma linha, sem o
|
|
@@ -221,9 +256,12 @@ function logResult(kind, result, profundidade) {
|
|
|
221
256
|
case 'browser_open':
|
|
222
257
|
console.log(p + c.muted(result.url || 'aberto'));
|
|
223
258
|
return;
|
|
224
|
-
case 'browser_read':
|
|
225
|
-
|
|
259
|
+
case 'browser_read': {
|
|
260
|
+
const refs = (String(result.pagina || '').match(/^@e\d+/gm) || []).length;
|
|
261
|
+
console.log(p + c.muted(refs ? `${refs} elementos com ref · ${(result.text || '').length} caracteres de texto`
|
|
262
|
+
: `${(result.text || '').length} caracteres lidos da página`));
|
|
226
263
|
return;
|
|
264
|
+
}
|
|
227
265
|
case 'pesquisar_referencia': {
|
|
228
266
|
console.log(p + c.muted(`${result.fontes} fonte${result.fontes === 1 ? '' : 's'} lida${result.fontes === 1 ? '' : 's'} sobre "${result.consulta}"`));
|
|
229
267
|
for (const r of (result.referencias || []).slice(0, 4)) {
|
|
@@ -298,6 +336,50 @@ const RESTO_DO_RESULTADO = 300;
|
|
|
298
336
|
const RESTO_DA_PESQUISA = 1500;
|
|
299
337
|
const MATERIA_PRIMA = new Set(['pesquisar_referencia']);
|
|
300
338
|
|
|
339
|
+
/* ── O ARQUIVO JÁ GRAVADO NÃO VIAJA DE NOVO ─────────────────────────────
|
|
340
|
+
* O `compactar` encolhia o resultado das ferramentas e deixava intacto o que
|
|
341
|
+
* o MODELO escreveu — e o que ele escreve num site é o arquivo inteiro, dentro
|
|
342
|
+
* da chamada write_file. Medido em 29/09 ("crie uma landing de barbearia"):
|
|
343
|
+
* depois do index.html (7,6 KB) e do style.css (15 KB), cada ida ao modelo
|
|
344
|
+
* levava os dois de novo, passava do teto de tokens por minuto do plano
|
|
345
|
+
* grátis e ficava esperando a janela virar, em silêncio — "escreve dois
|
|
346
|
+
* arquivos e trava". O conteúdo já está no disco: nas voltas antigas ele vira
|
|
347
|
+
* um ponteiro. As duas últimas ficam inteiras (é nelas que o modelo decide o
|
|
348
|
+
* próximo passo, e é delas que sai a releitura sem disco — ver read_file). */
|
|
349
|
+
const ESCRITAS_INTEIRAS = 2;
|
|
350
|
+
const RESTO_DA_ESCRITA = 400;
|
|
351
|
+
function assistentesAntigas(messages) {
|
|
352
|
+
const idx = [];
|
|
353
|
+
for (let i = 0; i < messages.length; i++) {
|
|
354
|
+
const m = messages[i];
|
|
355
|
+
if (m && m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) idx.push(i);
|
|
356
|
+
}
|
|
357
|
+
return new Set(idx.slice(0, -ESCRITAS_INTEIRAS));
|
|
358
|
+
}
|
|
359
|
+
function encolherEscritas(m) {
|
|
360
|
+
let mudou = false;
|
|
361
|
+
const tool_calls = m.tool_calls.map((tc) => {
|
|
362
|
+
const f = tc && tc.function;
|
|
363
|
+
if (!f || !/^(write_file|edit_file|multi_edit)$/.test(f.name)) return tc;
|
|
364
|
+
const bruto = String(f.arguments || '');
|
|
365
|
+
if (bruto.length <= RESTO_DA_ESCRITA) return tc;
|
|
366
|
+
let a;
|
|
367
|
+
try { a = JSON.parse(bruto); } catch { return tc; }
|
|
368
|
+
const ponteiro = (n) => `(${n} caracteres — já gravado no disco; para mudar, leia com read_file e use edit_file)`;
|
|
369
|
+
if (f.name === 'write_file' && typeof a.content === 'string' && a.content.length > RESTO_DA_ESCRITA) a.content = ponteiro(a.content.length);
|
|
370
|
+
else if (f.name === 'edit_file' && typeof a.new_string === 'string' && a.new_string.length > RESTO_DA_ESCRITA) {
|
|
371
|
+
a.new_string = ponteiro(a.new_string.length);
|
|
372
|
+
if (typeof a.old_string === 'string' && a.old_string.length > 200) a.old_string = a.old_string.slice(0, 200) + '…';
|
|
373
|
+
} else if (f.name === 'multi_edit' && Array.isArray(a.edits)) {
|
|
374
|
+
a.edits = a.edits.map((e) => (e && typeof e.new_string === 'string' && e.new_string.length > RESTO_DA_ESCRITA
|
|
375
|
+
? { ...e, new_string: ponteiro(e.new_string.length), old_string: String(e.old_string || '').slice(0, 200) } : e));
|
|
376
|
+
} else return tc;
|
|
377
|
+
mudou = true;
|
|
378
|
+
return { ...tc, function: { ...f, arguments: JSON.stringify(a) } };
|
|
379
|
+
});
|
|
380
|
+
return mudou ? { ...m, tool_calls } : m;
|
|
381
|
+
}
|
|
382
|
+
|
|
301
383
|
/**
|
|
302
384
|
* Encolhe os resultados de tool ANTIGOS antes de mandar a conversa.
|
|
303
385
|
*
|
|
@@ -322,11 +404,13 @@ function compactar(messages) {
|
|
|
322
404
|
|
|
323
405
|
const indices = [];
|
|
324
406
|
for (let i = 0; i < messages.length; i++) if (messages[i].role === 'tool') indices.push(i);
|
|
325
|
-
if (indices.length <= RESULTADOS_INTEIROS && !duplicadas.size) return messages;
|
|
407
|
+
if (indices.length <= RESULTADOS_INTEIROS && !duplicadas.size && !assistentesAntigas(messages).size) return messages;
|
|
326
408
|
|
|
327
409
|
const chamadas = repeticao.chamadasPorId(messages);
|
|
328
410
|
const encolher = new Set(indices.slice(0, -RESULTADOS_INTEIROS));
|
|
411
|
+
const antigas = assistentesAntigas(messages);
|
|
329
412
|
return messages.map((m, i) => {
|
|
413
|
+
if (antigas.has(i)) return encolherEscritas(m);
|
|
330
414
|
if (duplicadas.has(i)) {
|
|
331
415
|
return { ...m, content: '(a mesma referência aparece inteira mais adiante nesta conversa — use aquela)' };
|
|
332
416
|
}
|
|
@@ -383,10 +467,21 @@ function esperaDoErro(e, tentativa = 0) {
|
|
|
383
467
|
return Math.min(20, 3 * 2 ** Math.max(0, tentativa - 1)) * 1000;
|
|
384
468
|
}
|
|
385
469
|
|
|
386
|
-
/** Espera
|
|
387
|
-
*
|
|
470
|
+
/** Espera a vez no modelo — À VISTA, com os segundos, também no subagente.
|
|
471
|
+
* Antes girava o spinner de "pensando" (e no subagente, nada): eram minutos
|
|
472
|
+
* de tela parada que pareciam trava. "As coisas estão acontecendo, só não
|
|
473
|
+
* estão sendo mostradas." Dizer que é a vez do modelo, sem alarme, é a
|
|
474
|
+
* diferença entre esperar e desistir. */
|
|
475
|
+
/* PRIMOCODE_TRACO=1: uma linha por ida ao modelo no stderr — o tamanho da
|
|
476
|
+
conversa, o erro e a espera. É o que diz ONDE uma tarefa travou quando a
|
|
477
|
+
tela (ou um log sem terminal) só mostra o último arquivo escrito. */
|
|
478
|
+
function traco(texto) {
|
|
479
|
+
if (process.env.PRIMOCODE_TRACO === '1') process.stderr.write(` [traço ${new Date().toTimeString().slice(0, 8)}] ${texto}\n`);
|
|
480
|
+
}
|
|
481
|
+
|
|
388
482
|
async function pausar(ms, subagent) {
|
|
389
|
-
|
|
483
|
+
traco(`esperando ${Math.round(ms / 1000)}s pela vez no modelo${subagent ? ' (agente)' : ''}`);
|
|
484
|
+
const sp = new Spinner(subagent ? 'o agente espera a vez no modelo' : 'esperando a vez no modelo').start();
|
|
390
485
|
await new Promise((r) => setTimeout(r, ms));
|
|
391
486
|
if (sp) sp.stop();
|
|
392
487
|
tempos.somar('espera de cota/rede', ms);
|
|
@@ -487,6 +582,58 @@ function mapaDoProjeto(dir, limite = 80, achados = arquivosDoProjeto(dir, limite
|
|
|
487
582
|
return `Arquivos que JÁ existem neste projeto (edite-os em vez de recriar):\n${achados.join('\n')}${mais}`;
|
|
488
583
|
}
|
|
489
584
|
|
|
585
|
+
/* O comando de shell que só LÊ um arquivo — `cat x`, `sed -n '10,40p' x`,
|
|
586
|
+
`head -50 x`, `tail x`, `nl x`, com ou sem `cd pasta &&` na frente. Medido
|
|
587
|
+
num pedido de site (29/09): depois de escrever o script.js, o agente o
|
|
588
|
+
releu em quatro fatias de `sed -n`, uma volta de modelo cada. Devolve o
|
|
589
|
+
caminho lido (absoluto quando houve cd), ou null. */
|
|
590
|
+
function leituraPeloShell(comando) {
|
|
591
|
+
const m = /^\s*(?:cd\s+("[^"]+"|'[^']+'|\S+)\s*&&\s*)?(?:cat|nl|sed\s+-n\s+(?:'[^']*'|"[^"]*"|\S+)|head(?:\s+-n?\s*\d+)?|tail(?:\s+-n?\s*\d+)?)\s+("[^"]+"|'[^']+'|[^\s|;&><]+)\s*$/.exec(String(comando || ''));
|
|
592
|
+
if (!m) return null;
|
|
593
|
+
const tira = (x) => String(x || '').replace(/^["']|["']$/g, '');
|
|
594
|
+
const arquivo = tira(m[2]);
|
|
595
|
+
return m[1] ? require('path').resolve(tira(m[1]), arquivo) : arquivo;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/* ── O HISTÓRICO QUE VIAJA ─────────────────────────────────────────────
|
|
599
|
+
* Vinte mensagens INTEIRAS iam em toda volta do agente principal. Medido em
|
|
600
|
+
* 29/09: num pedido de site, 28 mil dos 38 mil caracteres da primeira ida
|
|
601
|
+
* eram respostas de pedidos anteriores — sozinhas, acima do teto por minuto
|
|
602
|
+
* do plano grátis. O que a conversa precisa para continuar é o fio (o que
|
|
603
|
+
* foi pedido, o que ficou pronto), não o texto inteiro: cada mensagem entra
|
|
604
|
+
* até um teto, e o total também, das mais novas para as mais velhas. */
|
|
605
|
+
const HISTORICO_POR_MENSAGEM = 1200;
|
|
606
|
+
const HISTORICO_TOTAL = 6000;
|
|
607
|
+
function historicoEnxuto(history) {
|
|
608
|
+
const fora = [];
|
|
609
|
+
let total = 0;
|
|
610
|
+
for (const m of (history || []).slice(-20).reverse()) {
|
|
611
|
+
if (!m || !m.role || !m.content) continue;
|
|
612
|
+
let t = String(m.content);
|
|
613
|
+
if (t.length > HISTORICO_POR_MENSAGEM) t = t.slice(0, HISTORICO_POR_MENSAGEM) + '…(encurtado)';
|
|
614
|
+
if (total + t.length > HISTORICO_TOTAL && fora.length) break;
|
|
615
|
+
total += t.length;
|
|
616
|
+
fora.unshift({ role: m.role, content: t });
|
|
617
|
+
}
|
|
618
|
+
// A API quer a conversa começando pelo usuário.
|
|
619
|
+
while (fora.length && fora[0].role !== 'user') fora.shift();
|
|
620
|
+
return fora;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/* ── O RITMO ─────────────────────────────────────────────────────────
|
|
624
|
+
* "A potência não deve vir do modelo, deve vir do sistema." O prompt do
|
|
625
|
+
* servidor manda "leia antes de editar" e "confira antes de dizer que
|
|
626
|
+
* terminou" — certo para editar código dos outros, e a receita do laço
|
|
627
|
+
* quando o arquivo foi ele mesmo que escreveu um minuto antes. Estas regras
|
|
628
|
+
* vêm do que o opencode e o Codex põem nos prompts deles para o modelo não
|
|
629
|
+
* reler o que fez; entram em TODO pedido, curtas, porque o plano conta token.
|
|
630
|
+
* A conferência de verdade é nossa (a página aberta no Chrome escondido). */
|
|
631
|
+
const RITMO = '[ritmo]\n'
|
|
632
|
+
+ '- O resultado de write_file/edit_file é a verdade. Não releia um arquivo que você acabou de escrever para conferir (nem com cat/sed/head): a conferência é do PrimoCode.\n'
|
|
633
|
+
+ '- Várias ações independentes vão na MESMA resposta (ex.: escrever index.html, style.css e script.js juntos).\n'
|
|
634
|
+
+ '- Leia um arquivo inteiro uma vez, não em pedaços pequenos; não leia de novo o que não mudou.\n'
|
|
635
|
+
+ '- Não narre o próximo passo: faça. Termine com finish("o que foi feito").';
|
|
636
|
+
|
|
490
637
|
/* `pedido` é opcional: com ele, o arquivo citado no pedido é o primeiro a
|
|
491
638
|
entrar já lido (ver lib/pre-leitura.js). */
|
|
492
639
|
function buildMemoryContext(projectDir, projectName, pedido = '') {
|
|
@@ -505,6 +652,7 @@ function buildMemoryContext(projectDir, projectName, pedido = '') {
|
|
|
505
652
|
if (mapa) parts.push(`[${projectName || 'projeto'}/arquivos]\n${mapa}\n\n${preLeitura.JUNTAR_LEITURAS}`);
|
|
506
653
|
const lidos = preLeitura.bloco(preLeitura.escolher(projectDir, arquivos, pedido));
|
|
507
654
|
if (lidos) parts.push(lidos);
|
|
655
|
+
parts.push(RITMO);
|
|
508
656
|
return parts.join('\n\n');
|
|
509
657
|
}
|
|
510
658
|
|
|
@@ -584,10 +732,12 @@ async function runAct(userPrompt, opts) {
|
|
|
584
732
|
mexeuNaPagina = false;
|
|
585
733
|
let medida = null;
|
|
586
734
|
const t = Date.now();
|
|
735
|
+
// Com spinner: eram segundos de tela parada entre o último arquivo e o
|
|
736
|
+
// "pronto", e parecia que tinha travado ali.
|
|
737
|
+
const sp = new Spinner('abrindo a página escondido para conferir').start();
|
|
587
738
|
try {
|
|
588
|
-
fala.pensamento('abrindo a página para conferir antes de entregar', profundidade);
|
|
589
739
|
medida = await medirPagina(pastaAtual);
|
|
590
|
-
} catch { medida = null; }
|
|
740
|
+
} catch { medida = null; } finally { sp.stop(); }
|
|
591
741
|
msConferindo += Date.now() - t;
|
|
592
742
|
tempos.somar('conferir página', Date.now() - t);
|
|
593
743
|
if (!medida || !medida.testou) return null;
|
|
@@ -602,6 +752,9 @@ async function runAct(userPrompt, opts) {
|
|
|
602
752
|
const jaChamado = repeticao.novo();
|
|
603
753
|
// O que read_file/list_dir devolveram, pela chave da época (repeticao.js).
|
|
604
754
|
const leiturasGuardadas = new Map();
|
|
755
|
+
// O que ELE escreveu inteiro nesta tarefa, por caminho absoluto — para
|
|
756
|
+
// não gastar uma volta relendo o próprio texto (ver read_file abaixo).
|
|
757
|
+
const escritosNaTarefa = new Map();
|
|
605
758
|
/* O livro deste pedido: arquivos, comandos, links, testes e falhas. Sai
|
|
606
759
|
no fim como balanço (ver lib/registro.js). O subagente tem o seu, que
|
|
607
760
|
não se mistura com o do principal — mas o que ele fez também precisa
|
|
@@ -653,9 +806,7 @@ async function runAct(userPrompt, opts) {
|
|
|
653
806
|
if (!subagent) {
|
|
654
807
|
const memory = buildMemoryContext(projectDir, projectName, userPrompt);
|
|
655
808
|
if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
|
|
656
|
-
for (const m of (history
|
|
657
|
-
if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
|
|
658
|
-
}
|
|
809
|
+
for (const m of historicoEnxuto(history)) messages.push(m);
|
|
659
810
|
}
|
|
660
811
|
/* O que VAI para o modelo é `pedido` (com as skills na frente); o que
|
|
661
812
|
fica guardado na memória e no histórico é `userPrompt`, o que a pessoa
|
|
@@ -676,8 +827,14 @@ async function runAct(userPrompt, opts) {
|
|
|
676
827
|
* Num subagente não entra: ele já está fazendo uma parte de um plano, e
|
|
677
828
|
* um diretor dentro do diretor é uma volta a mais para o mesmo lugar. */
|
|
678
829
|
if (!subagent && profundidade === 0) {
|
|
679
|
-
const
|
|
680
|
-
|
|
830
|
+
const diretor = require('./estudio/diretor.js');
|
|
831
|
+
const producao = diretor.precisaDeProducao(pedido);
|
|
832
|
+
if (diretor.precisaDeEdicao(pedido)) {
|
|
833
|
+
// A gravação da pessoa: o editor por linha do tempo (lib/video/), pelo run_shell.
|
|
834
|
+
messages.push({ role: 'system', content: 'PARA EDITAR A GRAVAÇÃO DA PESSOA use o editor por linha do tempo via run_shell: '
|
|
835
|
+
+ 'rode `primocode video help` UMA vez (lista as operações), depois import → remove_silence/remove_words → add_captions → inspect (olhe o png) → export. '
|
|
836
|
+
+ 'Cada edição devolve um delta curto e tem undo: edite sem pedir licença a cada passo.' });
|
|
837
|
+
} else if (producao.producao) {
|
|
681
838
|
/* "SE ESTIVER NA SUA LISTA" não é hesitação — é a diferença entre
|
|
682
839
|
a tarefa seguir e a tarefa morrer.
|
|
683
840
|
|
|
@@ -749,8 +906,13 @@ async function runAct(userPrompt, opts) {
|
|
|
749
906
|
const MAX_ESPERAS_REDE = Number(process.env.PRIMOCODE_MAX_ESPERAS_REDE || 6);
|
|
750
907
|
|
|
751
908
|
let voltas = 0;
|
|
909
|
+
if (!subagent) TRABALHANDO = true;
|
|
910
|
+
try {
|
|
752
911
|
for (let iteration = 0; iteration < MAX_ITERATIONS && !PARAR; iteration++) {
|
|
753
912
|
voltas++;
|
|
913
|
+
// Recado digitado durante o passo anterior entra AGORA, antes da ida
|
|
914
|
+
// ao modelo — o resultado das ferramentas já está na conversa.
|
|
915
|
+
if (!subagent) entregarRecados(messages, profundidade);
|
|
754
916
|
const sp = new Spinner(subagent ? 'subagente trabalhando' : PENSANDO).start();
|
|
755
917
|
|
|
756
918
|
const toolCalls = [];
|
|
@@ -769,11 +931,19 @@ async function runAct(userPrompt, opts) {
|
|
|
769
931
|
emVoo = new AbortController();
|
|
770
932
|
const tRodada = Date.now();
|
|
771
933
|
try {
|
|
772
|
-
|
|
934
|
+
const enviadas = compactar(messages);
|
|
935
|
+
traco(`volta ${voltas}${subagent ? ' (agente)' : ''} · ${enviadas.length} mensagens · ${JSON.stringify(enviadas).length} caracteres`);
|
|
936
|
+
await motor.requestAct(server, { messages: enviadas, model, effort, token, projectDir: pastaAtual, projectName, subagent, sinal: emVoo.signal }, (evt) => {
|
|
773
937
|
if (evt.erro) { streamError = evt.erro; return; }
|
|
774
|
-
if (evt.tool_call) {
|
|
938
|
+
if (evt.tool_call) {
|
|
939
|
+
toolCalls.push(evt.tool_call);
|
|
940
|
+
const a = evt.tool_call.arguments || {};
|
|
941
|
+
sp.detalhe = `${toolCalls.length} ${toolCalls.length > 1 ? 'ações prontas' : 'ação pronta'} · ${evt.tool_call.name}${a.path ? ' ' + a.path : ''}`;
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
775
944
|
if (evt.delta) {
|
|
776
945
|
assistantText += evt.delta;
|
|
946
|
+
sp.detalhe = assistantText.length > 200 ? `${Math.round(assistantText.length / 100) / 10} mil caracteres` : '';
|
|
777
947
|
if (!subagent && !toolCalls.length) {
|
|
778
948
|
if (!fluxo && assistantText.length > fala.LIMIAR_PENSAMENTO) {
|
|
779
949
|
sp.stop();
|
|
@@ -787,6 +957,7 @@ async function runAct(userPrompt, opts) {
|
|
|
787
957
|
} catch (e) {
|
|
788
958
|
sp.stop();
|
|
789
959
|
emVoo = null;
|
|
960
|
+
traco(`erro na volta ${voltas}: ${String((e && e.message) || e).slice(0, 200)}`);
|
|
790
961
|
if (fluxo) fluxo.fechar();
|
|
791
962
|
// Estourou o minuto: encolhe o que já foi feito e CONTINUA, em vez
|
|
792
963
|
// de devolver erro e jogar fora o trabalho. O usuário não precisa
|
|
@@ -814,6 +985,7 @@ async function runAct(userPrompt, opts) {
|
|
|
814
985
|
}
|
|
815
986
|
sp.stop();
|
|
816
987
|
emVoo = null;
|
|
988
|
+
traco(`volta ${voltas} voltou em ${Math.round((Date.now() - tRodada) / 1000)}s · ${toolCalls.map((t) => t.name).join(', ') || 'texto'}${streamError ? ' · erro: ' + String(streamError).slice(0, 160) : ''}`);
|
|
817
989
|
tempos.somar(subagent ? 'rodada do subagente' : 'rodada do modelo', Date.now() - tRodada);
|
|
818
990
|
if (PARAR) { if (fluxo) fluxo.fechar(); break; }
|
|
819
991
|
|
|
@@ -956,7 +1128,7 @@ async function runAct(userPrompt, opts) {
|
|
|
956
1128
|
messages.push({
|
|
957
1129
|
role: 'user',
|
|
958
1130
|
content: `AINDA NÃO. Abri a página para conferir e ela tem problema:\n${relato}\n\n`
|
|
959
|
-
+ 'Conserte
|
|
1131
|
+
+ 'Conserte SÓ o que está apontado, com edit_file no arquivo do erro (leia só ele, se precisar), e finalize de novo.',
|
|
960
1132
|
});
|
|
961
1133
|
finalText = '';
|
|
962
1134
|
continue;
|
|
@@ -967,6 +1139,13 @@ async function runAct(userPrompt, opts) {
|
|
|
967
1139
|
if (fluxo) fluxo.fechar();
|
|
968
1140
|
else fala.resposta(finalText);
|
|
969
1141
|
}
|
|
1142
|
+
/* Ia terminar, mas chegou recado no meio: a tarefa não acaba com
|
|
1143
|
+
a pessoa esperando resposta. A volta seguinte entrega o recado. */
|
|
1144
|
+
if (!subagent && CAIXA.length && !PARAR) {
|
|
1145
|
+
messages.push({ role: 'assistant', content: finalText });
|
|
1146
|
+
finalText = '';
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
970
1149
|
break;
|
|
971
1150
|
}
|
|
972
1151
|
respostasVazias = 0;
|
|
@@ -1037,7 +1216,7 @@ async function runAct(userPrompt, opts) {
|
|
|
1037
1216
|
role: 'tool',
|
|
1038
1217
|
tool_call_id: tc.id,
|
|
1039
1218
|
content: `NÃO terminou. Estas tools falharam e você não corrigiu: ${falhasPendentes.map((f) => f.texto).join('; ')}. `
|
|
1040
|
-
+ 'Conserte agora
|
|
1219
|
+
+ 'Conserte agora com edit_file no arquivo que falhou ou, se for impossível, diga claramente ao usuário o que falhou.',
|
|
1041
1220
|
});
|
|
1042
1221
|
continue;
|
|
1043
1222
|
}
|
|
@@ -1117,6 +1296,38 @@ async function runAct(userPrompt, opts) {
|
|
|
1117
1296
|
|
|
1118
1297
|
if (criarPasta && catalogo.criaProjeto(tc.name, tc.arguments)) garantirPasta();
|
|
1119
1298
|
|
|
1299
|
+
/* RELER O QUE ACABOU DE ESCREVER. O modelo grátis escreve o
|
|
1300
|
+
index.html e, na volta seguinte, pede para lê-lo "para
|
|
1301
|
+
conferir" — uma ida ao servidor para receber o texto que ele
|
|
1302
|
+
mesmo mandou, e que continua inteiro na chamada write_file dele
|
|
1303
|
+
(o `compactar` só encolhe resultado de ferramenta, nunca o que
|
|
1304
|
+
o modelo escreveu). Se o arquivo está byte a byte como ele
|
|
1305
|
+
deixou, a resposta é essa, sem abrir nada. Depois de um
|
|
1306
|
+
edit_file não vale: aí o texto na conversa está em pedaços. */
|
|
1307
|
+
const relido = tc.name === 'read_file' && tc.arguments && tc.arguments.path
|
|
1308
|
+
? tc.arguments.path
|
|
1309
|
+
: tc.name === 'run_shell' ? leituraPeloShell(tc.arguments && tc.arguments.command) : null;
|
|
1310
|
+
if (relido && escritosNaTarefa.size) {
|
|
1311
|
+
let igual = false;
|
|
1312
|
+
try {
|
|
1313
|
+
const abs = require('path').isAbsolute(relido) ? relido : tools.resolveInProject(relido, pastaAtual);
|
|
1314
|
+
const e = escritosNaTarefa.get(abs);
|
|
1315
|
+
// Só enquanto a escrita ainda viaja inteira na conversa
|
|
1316
|
+
// (as duas últimas voltas — ver ESCRITAS_INTEIRAS).
|
|
1317
|
+
igual = !!e && voltas - e.volta <= 1 && require('fs').readFileSync(abs, 'utf8') === e.texto;
|
|
1318
|
+
} catch { igual = false; }
|
|
1319
|
+
if (igual) {
|
|
1320
|
+
fala.acao(tc.name, tc.arguments, profundidade);
|
|
1321
|
+
console.log(fala.resultado(profundidade) + c.muted('igual ao que ele escreveu — não precisou ler'));
|
|
1322
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({
|
|
1323
|
+
ok: true, igualAoQueVoceEscreveu: true,
|
|
1324
|
+
aviso: 'Este arquivo está EXATAMENTE como você o escreveu nesta tarefa — o conteúdo é o da sua '
|
|
1325
|
+
+ 'chamada write_file acima. Não precisa ler. Para mudar, use edit_file direto; se terminou, siga.',
|
|
1326
|
+
}) });
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1120
1331
|
if (tc.name === 'pesquisar_referencia' && pesquisasSecas >= 3) {
|
|
1121
1332
|
voltasEmFalso++;
|
|
1122
1333
|
messages.push({
|
|
@@ -1138,7 +1349,6 @@ async function runAct(userPrompt, opts) {
|
|
|
1138
1349
|
const porta = await permissao.pedir(portaoControle, tc.name, tc.arguments, autorizar);
|
|
1139
1350
|
if (!porta.ok) {
|
|
1140
1351
|
fala.acao(tc.name, tc.arguments, profundidade);
|
|
1141
|
-
fala.avisar('resultado', { nome: tc.name, ok: false, resumo: 'não autorizado' });
|
|
1142
1352
|
console.log(fala.resultado(profundidade) + c.warn('não autorizado'));
|
|
1143
1353
|
registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: porta.error });
|
|
1144
1354
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: porta.error }) });
|
|
@@ -1156,7 +1366,6 @@ async function runAct(userPrompt, opts) {
|
|
|
1156
1366
|
});
|
|
1157
1367
|
if (!risco.ok) {
|
|
1158
1368
|
fala.acao(tc.name, tc.arguments, profundidade);
|
|
1159
|
-
fala.avisar('resultado', { nome: tc.name, ok: false, resumo: `não autorizado — ${risco.motivo}` });
|
|
1160
1369
|
console.log(fala.resultado(profundidade) + c.warn(`não autorizado — ${risco.motivo}`));
|
|
1161
1370
|
registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: risco.error });
|
|
1162
1371
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: risco.error }) });
|
|
@@ -1181,6 +1390,14 @@ async function runAct(userPrompt, opts) {
|
|
|
1181
1390
|
leiturasGuardadas.set(eco.chave, summarizeForModel(result));
|
|
1182
1391
|
}
|
|
1183
1392
|
if (tc.name === 'write_todos' && result && Array.isArray(result.todos)) todosDaTarefa = result.todos;
|
|
1393
|
+
if (/^(write_file|edit_file|multi_edit)$/.test(tc.name) && tc.arguments && tc.arguments.path) {
|
|
1394
|
+
try {
|
|
1395
|
+
const abs = tools.resolveInProject(tc.arguments.path, pastaAtual);
|
|
1396
|
+
if (tc.name === 'write_file' && result && result.ok !== false && typeof tc.arguments.content === 'string') {
|
|
1397
|
+
escritosNaTarefa.set(abs, { texto: tc.arguments.content, volta: voltas });
|
|
1398
|
+
} else escritosNaTarefa.delete(abs);
|
|
1399
|
+
} catch { /* caminho inválido: a tool já recusou */ }
|
|
1400
|
+
}
|
|
1184
1401
|
// Além de narrar, ANOTA: no fim do pedido sai o balanço do que
|
|
1185
1402
|
// nasceu, mudou, rodou e falhou. Narrar sozinho não basta — numa
|
|
1186
1403
|
// tarefa de trinta ações, a primeira já saiu da tela.
|
|
@@ -1270,9 +1487,14 @@ async function runAct(userPrompt, opts) {
|
|
|
1270
1487
|
if (finished) {
|
|
1271
1488
|
finalText = finished;
|
|
1272
1489
|
if (!subagent) fala.resposta(finished);
|
|
1490
|
+
// Recado chegou enquanto ele fechava: entrega e segue.
|
|
1491
|
+
if (!subagent && CAIXA.length && !PARAR) { finalText = ''; continue; }
|
|
1273
1492
|
break;
|
|
1274
1493
|
}
|
|
1275
1494
|
}
|
|
1495
|
+
} finally {
|
|
1496
|
+
if (!subagent) TRABALHANDO = false;
|
|
1497
|
+
}
|
|
1276
1498
|
|
|
1277
1499
|
// O que o modelo entregou por vontade própria, ANTES dos textos de
|
|
1278
1500
|
// desistência. É esta a diferença que o plano de passos precisa: um passo
|
|
@@ -1480,5 +1702,5 @@ async function rodarPipeline(args, ctx) {
|
|
|
1480
1702
|
});
|
|
1481
1703
|
}
|
|
1482
1704
|
|
|
1483
|
-
module.exports = { runAct, buildMemoryContext, interromper, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
|
|
1705
|
+
module.exports = { runAct, buildMemoryContext, leituraPeloShell, historicoEnxuto, interromper, mandarDuranteATarefa, trabalhando, sobrasDaCaixa, entregarRecados, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
|
|
1484
1706
|
anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
|
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');
|
|
@@ -658,7 +659,22 @@ async function candidatosNaPagina() {
|
|
|
658
659
|
try { return JSON.parse(await evaluate(js)) || []; } catch { return []; }
|
|
659
660
|
}
|
|
660
661
|
|
|
662
|
+
/* O retrato da página com as refs (lib/snapshot.js), já em texto. */
|
|
663
|
+
async function retrato(max = 80) {
|
|
664
|
+
try {
|
|
665
|
+
const r = await evaluate(snapshot.scriptDoRetrato(max));
|
|
666
|
+
return snapshot.formatar(r, max);
|
|
667
|
+
} catch { return ''; }
|
|
668
|
+
}
|
|
669
|
+
|
|
661
670
|
async function erroDeSeletor(selector) {
|
|
671
|
+
// Ref que não existe mais: a página mudou depois da leitura. Listar os
|
|
672
|
+
// seletores CSS aqui só confundiria — o caminho é ler de novo.
|
|
673
|
+
if (/data-primo-ref/.test(selector)) {
|
|
674
|
+
const err = new Error('essa ref não existe mais nesta página — ela mudou depois da leitura. '
|
|
675
|
+
+ 'Estas são as refs de agora:\n' + await retrato(40));
|
|
676
|
+
return err;
|
|
677
|
+
}
|
|
662
678
|
const candidatos = await candidatosNaPagina();
|
|
663
679
|
const err = new Error(
|
|
664
680
|
`seletor não encontrado: ${selector}`
|
|
@@ -1191,6 +1207,7 @@ function fecharRevisor() {
|
|
|
1191
1207
|
}
|
|
1192
1208
|
|
|
1193
1209
|
module.exports = {
|
|
1210
|
+
retrato,
|
|
1194
1211
|
SEM_KEYCHAIN,
|
|
1195
1212
|
scriptDaSeta, verSeta, findChrome,
|
|
1196
1213
|
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: motion (padrão, 3D contínuo) | 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
|
|
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
|
-
|
|
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
|
-
|
|
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');
|