primocode 9.9.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/ende.js +2 -0
- package/lib/estudio/estilo.js +51 -0
- package/lib/estudio/glifos.js +215 -0
- package/lib/estudio/index.js +63 -11
- 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/morfar.js +191 -0
- package/lib/estudio/motion.js +333 -0
- package/lib/estudio/plano.js +54 -7
- package/lib/estudio/remotion.js +167 -52
- package/lib/estudio/ritmo.js +77 -0
- package/lib/estudio/sons-leves.js +62 -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 +21 -1
- 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/pipeline.js
CHANGED
|
@@ -217,8 +217,8 @@ Duas regras que valem mais que o resto:
|
|
|
217
217
|
|
|
218
218
|
Responda com a especificação e mais nada.`;
|
|
219
219
|
|
|
220
|
-
const CONSTRUTOR_1 = (spec, pasta) => `Você é o CONSTRUTOR
|
|
221
|
-
o
|
|
220
|
+
const CONSTRUTOR_1 = (spec, pasta) => `Você é o CONSTRUTOR, e o seu trabalho é
|
|
221
|
+
o PRODUTO que FUNCIONA — de primeira, sem protótipo para alguém refazer depois.
|
|
222
222
|
|
|
223
223
|
Construa na pasta: ${pasta}
|
|
224
224
|
|
|
@@ -243,39 +243,35 @@ Regras:
|
|
|
243
243
|
digitar não mostra nada.
|
|
244
244
|
· Não deixe função pela metade nem "TODO". O que não der para fazer, não
|
|
245
245
|
prometa na tela.
|
|
246
|
+
· Com TELA: hierarquia de tamanhos, espaço para respirar, estados de foco e de
|
|
247
|
+
toque, mensagem que diz o que fazer quando o campo está vazio, e nada de
|
|
248
|
+
rolagem horizontal em 390px. Uma paleta, mantida.
|
|
249
|
+
· RÁPIDO: escreva cada arquivo UMA vez, inteiro, e vários na mesma rodada. O
|
|
250
|
+
resultado do write_file é a verdade — não releia o que acabou de escrever.
|
|
246
251
|
|
|
247
252
|
Escreva os arquivos e diga em uma linha o que ficou pronto.`;
|
|
248
253
|
|
|
249
|
-
const CONSTRUTOR_2 = (spec, pasta, relato) => `Você é o
|
|
250
|
-
|
|
254
|
+
const CONSTRUTOR_2 = (spec, pasta, relato) => `Você é o CORRETOR. O produto já
|
|
255
|
+
está construído na pasta ${pasta}, e alguém ABRIU e achou problema.
|
|
251
256
|
|
|
252
|
-
|
|
253
|
-
LEIA os arquivos que já estão lá antes de mexer. Não recomece do zero: o que
|
|
254
|
-
está funcionando fica.
|
|
255
|
-
|
|
256
|
-
Especificação:
|
|
257
|
+
O que foi pedido (resumo):
|
|
257
258
|
|
|
258
259
|
"""
|
|
259
260
|
${spec}
|
|
260
261
|
"""
|
|
261
262
|
|
|
262
|
-
|
|
263
|
-
O que você faz:
|
|
263
|
+
O que está errado:
|
|
264
264
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
· REDESENHA: hierarquia de tamanhos, espaço para respirar, estados de foco e de
|
|
269
|
-
toque, e o que aparece quando não há dados. Escolha uma paleta e mantenha.
|
|
270
|
-
· CUIDA DO ERRO: campo vazio, valor impossível, lista vazia. Cada um com uma
|
|
271
|
-
mensagem que diz o que fazer, não "erro".
|
|
272
|
-
· RESPONSIVO de verdade: nada de rolagem horizontal em 390px de largura.
|
|
265
|
+
"""
|
|
266
|
+
${relato || '(sem relato)'}
|
|
267
|
+
"""
|
|
273
268
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
269
|
+
· Conserte SÓ o que está apontado. Nada de recurso novo, nada de recomeçar.
|
|
270
|
+
· Leia apenas o arquivo citado no erro — não a pasta inteira — e troque o trecho
|
|
271
|
+
com edit_file. Reescrever o arquivo inteiro só se ele estiver vazio ou em
|
|
272
|
+
branco.
|
|
273
|
+
· Não releia o arquivo depois de editar: o resultado do edit_file é a verdade.
|
|
277
274
|
|
|
278
|
-
Não escreva "pronto" sem ter aberto os arquivos e mudado o que precisa.
|
|
279
275
|
Diga em uma linha o que mudou.`;
|
|
280
276
|
|
|
281
277
|
const REVISOR = (spec, pasta, arquivos, relato) => `Você é o REVISADOR, e você
|
|
@@ -327,11 +323,12 @@ const APROVOU = /^\s*APROVAD[OA]\b/im;
|
|
|
327
323
|
* @param {(prompt:string,rotulo:string)=>Promise<string>} op.correr roda um agente
|
|
328
324
|
* @param {(linha:string)=>void} op.mostrar escreve na tela do usuário
|
|
329
325
|
* @param {(alvo:string)=>any} [op.abrir] abre no navegador da pessoa
|
|
330
|
-
* @param {number} [op.rodadas]
|
|
326
|
+
* @param {number} [op.rodadas] conferências, contando a primeira (padrão 3)
|
|
331
327
|
*/
|
|
332
328
|
async function construir(op) {
|
|
333
329
|
const { pedido, pasta, correr, mostrar = () => {}, abrir } = op;
|
|
334
|
-
|
|
330
|
+
// Quantas CONFERÊNCIAS (a primeira + os consertos). 3 = até dois consertos.
|
|
331
|
+
const rodadasMax = Math.max(1, Math.min(4, op.rodadas == null ? 3 : op.rodadas));
|
|
335
332
|
if (!pedido) return { ok: false, error: 'Diga o que construir.' };
|
|
336
333
|
if (!pasta) return { ok: false, error: 'Diga em que pasta construir.' };
|
|
337
334
|
fs.mkdirSync(pasta, { recursive: true });
|
|
@@ -345,45 +342,47 @@ async function construir(op) {
|
|
|
345
342
|
return { ok: false, error: 'O engenheiro de prompt não devolveu especificação. Faça você mesmo.' };
|
|
346
343
|
}
|
|
347
344
|
|
|
348
|
-
mostrar('construtor
|
|
349
|
-
await correr(CONSTRUTOR_1(spec, pasta), 'construtor
|
|
350
|
-
etapas.push({ papel: 'construtor
|
|
351
|
-
|
|
345
|
+
mostrar('construtor');
|
|
346
|
+
await correr(CONSTRUTOR_1(spec, pasta), 'construtor');
|
|
347
|
+
etapas.push({ papel: 'construtor', arquivos: listar(pasta).length });
|
|
348
|
+
|
|
349
|
+
/* ── CONFERIR: a máquina primeiro, o modelo só quando ela não sabe ──
|
|
350
|
+
* Antes eram quatro agentes no mínimo: engenheiro, protótipo, um
|
|
351
|
+
* construtor 2 que RELIA tudo para refazer como MVP, e um revisor que
|
|
352
|
+
* relia de novo para opinar. Era o "escreve dois arquivos e fica o dia
|
|
353
|
+
* lendo o que fez". Agora: a página abriu limpa no Chrome escondido?
|
|
354
|
+
* Está pronta — a medida é fato, e opinião por cima dela só custava.
|
|
355
|
+
* Abriu com erro? O corretor recebe o erro e troca o trecho. O revisor
|
|
356
|
+
* de modelo só entra quando não há página para medir (um app de
|
|
357
|
+
* terminal), porque aí "não sei" precisa de alguém que leia. */
|
|
352
358
|
let relato = null;
|
|
353
359
|
let aprovado = false;
|
|
354
360
|
let ultimoRevisor = '';
|
|
355
361
|
|
|
356
362
|
for (let volta = 1; volta <= rodadasMax; volta++) {
|
|
357
|
-
mostrar(
|
|
358
|
-
await correr(CONSTRUTOR_2(spec, pasta, relato), `construtor 2 (${volta})`);
|
|
359
|
-
|
|
360
|
-
mostrar('revisador — testando escondido');
|
|
363
|
+
mostrar(volta === 1 ? 'conferindo — abrindo a página escondido' : `conferindo o conserto ${volta - 1}`);
|
|
361
364
|
const medida = await medirNoNavegador(pasta);
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
porque o projeto não é uma página, ou porque não há navegador
|
|
377
|
-
nesta máquina — o que a máquina tem a dizer é "não sei", e "não
|
|
378
|
-
sei" não reprova. Sem esta distinção, um app de linha de comando
|
|
379
|
-
nunca seria aprovado: gastaria as voltas todas e terminaria
|
|
380
|
-
dizendo que não está pronto, sobre um trabalho que está. */
|
|
381
|
-
aprovado = APROVOU.test(ultimoRevisor || '') && !(medida.testou && !medida.limpo);
|
|
365
|
+
let veredito;
|
|
366
|
+
if (medida.testou) {
|
|
367
|
+
aprovado = medida.limpo;
|
|
368
|
+
veredito = medida.limpo ? 'APROVADO pela medida' : 'REPROVADO pela medida';
|
|
369
|
+
relato = medida.limpo ? null : medida.relato;
|
|
370
|
+
} else {
|
|
371
|
+
mostrar('revisador — lendo o que foi feito');
|
|
372
|
+
ultimoRevisor = await correr(
|
|
373
|
+
REVISOR(spec, pasta, listar(pasta), medida.relato), `revisador (${volta})`);
|
|
374
|
+
aprovado = APROVOU.test(ultimoRevisor || '');
|
|
375
|
+
veredito = (ultimoRevisor || '').split('\n')[0];
|
|
376
|
+
relato = aprovado ? null : ultimoRevisor;
|
|
377
|
+
}
|
|
378
|
+
etapas.push({ papel: `conferência ${volta}`, testou: medida.testou, limpo: medida.limpo, veredito });
|
|
382
379
|
if (aprovado) { mostrar('aprovado'); break; }
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
380
|
+
if (volta === rodadasMax) break;
|
|
381
|
+
if (!ultimoRevisor) ultimoRevisor = relato || '';
|
|
382
|
+
mostrar(`corretor — consertando (${volta})`);
|
|
383
|
+
await correr(CONSTRUTOR_2(spec, pasta, relato), `corretor (${volta})`);
|
|
386
384
|
}
|
|
385
|
+
if (!ultimoRevisor && relato) ultimoRevisor = relato;
|
|
387
386
|
|
|
388
387
|
const entrada = acharEntrada(pasta);
|
|
389
388
|
let abriu = false;
|
package/lib/repeticao.js
CHANGED
|
@@ -62,7 +62,9 @@ function palavras(texto) {
|
|
|
62
62
|
/** Quanto duas buscas se sobrepõem, de 0 a 1. */
|
|
63
63
|
function semelhanca(a, b) {
|
|
64
64
|
const A = palavras(a), B = palavras(b);
|
|
65
|
-
|
|
65
|
+
// Sem palavra que conte dos dois lados ("go" e "js"): só é a mesma busca
|
|
66
|
+
// se o texto for o mesmo.
|
|
67
|
+
if (!A.size || !B.size) return normalizar(a).trim() === normalizar(b).trim() ? 1 : 0;
|
|
66
68
|
let juntos = 0;
|
|
67
69
|
for (const p of A) if (B.has(p)) juntos++;
|
|
68
70
|
return juntos / (A.size + B.size - juntos);
|
package/lib/skills.js
CHANGED
|
@@ -80,7 +80,13 @@ function separar(texto) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function lerEstado() {
|
|
83
|
-
|
|
83
|
+
let e;
|
|
84
|
+
try { e = JSON.parse(fs.readFileSync(ESTADO, 'utf8')); } catch { e = null; }
|
|
85
|
+
// Um estado.json válido sem `desligadas` (um `{}`) derrubava /skills,
|
|
86
|
+
// /marketplace e a entrada das skills em todo pedido.
|
|
87
|
+
if (!e || typeof e !== 'object') e = {};
|
|
88
|
+
if (!Array.isArray(e.desligadas)) e.desligadas = [];
|
|
89
|
+
return e;
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
function salvarEstado(estado) {
|
|
@@ -200,7 +206,7 @@ function ler(nome) {
|
|
|
200
206
|
quando: String(cabecalho.quando || '').split(',').map((s) => s.trim().toLowerCase()).filter(Boolean),
|
|
201
207
|
autor: cabecalho.autor || '',
|
|
202
208
|
corpo,
|
|
203
|
-
ligada: !estado.desligadas.includes(nome),
|
|
209
|
+
ligada: !(Array.isArray(estado.desligadas) && estado.desligadas.includes(nome)),
|
|
204
210
|
doProduto: fs.existsSync(path.join(ORIGEM, nome, 'SKILL.md')),
|
|
205
211
|
caminho: arquivo,
|
|
206
212
|
};
|
package/lib/snapshot.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* snapshot.js — a página como o agente precisa dela: o que dá para clicar e
|
|
3
|
+
* digitar, cada coisa com uma REF curta.
|
|
4
|
+
*
|
|
5
|
+
* @e1 [link] "Preços" → /precos
|
|
6
|
+
* @e2 [textbox] "E-mail" (seu@email.com)
|
|
7
|
+
* @e3 [button] "Entrar"
|
|
8
|
+
*
|
|
9
|
+
* Antes o agente lia o innerText da página (6.000 caracteres de texto corrido,
|
|
10
|
+
* sem dizer o que é botão) e chutava um seletor CSS — `input[name="q"]` no
|
|
11
|
+
* Google, que é `textarea`. Errava, lia de novo, chutava de novo. Com a ref,
|
|
12
|
+
* clicar é `browser_click selector:"@e3"`: sem CSS, sem chute. A ideia é a do
|
|
13
|
+
* agent-browser da Vercel e do Playwright MCP (árvore de acessibilidade com
|
|
14
|
+
* refs); o código é nosso, roda no mesmo CDP que o PrimoCode já usa e não pede
|
|
15
|
+
* instalar nada.
|
|
16
|
+
*
|
|
17
|
+
* A ref mora no próprio elemento (data-primo-ref). Navegou ou a página se
|
|
18
|
+
* redesenhou, a ref some — e o erro diz para ler de novo, em vez de clicar no
|
|
19
|
+
* lugar errado.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const ATRIBUTO = 'data-primo-ref';
|
|
25
|
+
|
|
26
|
+
/** O script que roda DENTRO da página e devolve { titulo, url, linhas, total }. */
|
|
27
|
+
function scriptDoRetrato(max = 80) {
|
|
28
|
+
return `(function(){
|
|
29
|
+
var MAX = ${Number(max) || 80};
|
|
30
|
+
var ATR = ${JSON.stringify(ATRIBUTO)};
|
|
31
|
+
var velhos = document.querySelectorAll('[' + ATR + ']');
|
|
32
|
+
for (var i = 0; i < velhos.length; i++) velhos[i].removeAttribute(ATR);
|
|
33
|
+
function visivel(el) {
|
|
34
|
+
var r = el.getBoundingClientRect();
|
|
35
|
+
if (r.width < 2 || r.height < 2) return false;
|
|
36
|
+
var s = getComputedStyle(el);
|
|
37
|
+
return s.visibility !== 'hidden' && s.display !== 'none' && Number(s.opacity) > 0.05;
|
|
38
|
+
}
|
|
39
|
+
function limpo(t, n) { return String(t || '').replace(/\\s+/g, ' ').trim().slice(0, n || 60); }
|
|
40
|
+
function papel(el) {
|
|
41
|
+
var r = el.getAttribute('role');
|
|
42
|
+
if (r) return r;
|
|
43
|
+
var tag = el.tagName.toLowerCase();
|
|
44
|
+
if (tag === 'a') return 'link';
|
|
45
|
+
if (tag === 'button' || tag === 'summary') return 'button';
|
|
46
|
+
if (tag === 'select') return 'select';
|
|
47
|
+
if (tag === 'textarea') return 'textbox';
|
|
48
|
+
if (tag === 'input') {
|
|
49
|
+
var t = (el.type || 'text').toLowerCase();
|
|
50
|
+
if (t === 'checkbox' || t === 'radio') return t;
|
|
51
|
+
if (t === 'submit' || t === 'button' || t === 'reset') return 'button';
|
|
52
|
+
return 'textbox';
|
|
53
|
+
}
|
|
54
|
+
if (el.isContentEditable) return 'textbox';
|
|
55
|
+
return 'button';
|
|
56
|
+
}
|
|
57
|
+
function nome(el) {
|
|
58
|
+
var n = el.getAttribute('aria-label') || el.getAttribute('title') || '';
|
|
59
|
+
if (!n && el.id) { var l = document.querySelector('label[for="' + el.id + '"]'); if (l) n = l.innerText; }
|
|
60
|
+
if (!n && el.closest && el.closest('label')) n = el.closest('label').innerText;
|
|
61
|
+
// O innerText de um select são TODAS as opções — o nome dele é o rótulo.
|
|
62
|
+
if (!n && el.tagName !== 'SELECT') n = el.innerText || el.value || el.getAttribute('alt') || '';
|
|
63
|
+
if (!n) { var img = el.querySelector && el.querySelector('img[alt]'); if (img) n = img.alt; }
|
|
64
|
+
return limpo(n);
|
|
65
|
+
}
|
|
66
|
+
var SEL = 'a[href],button,input:not([type=hidden]),textarea,select,summary,[contenteditable=""],[contenteditable=true],'
|
|
67
|
+
+ '[role=button],[role=link],[role=checkbox],[role=radio],[role=tab],[role=menuitem],[role=option],[role=textbox],'
|
|
68
|
+
+ '[role=combobox],[role=switch],[role=searchbox],[onclick],h1,h2,h3';
|
|
69
|
+
var linhas = [], n = 0, total = 0;
|
|
70
|
+
var todos = document.querySelectorAll(SEL);
|
|
71
|
+
for (var j = 0; j < todos.length; j++) {
|
|
72
|
+
var el = todos[j];
|
|
73
|
+
if (!visivel(el)) continue;
|
|
74
|
+
var tag = el.tagName.toLowerCase();
|
|
75
|
+
if (/^h[1-3]$/.test(tag)) { var h = limpo(el.innerText, 80); if (h) linhas.push('#'.repeat(Number(tag[1])) + ' ' + h); continue; }
|
|
76
|
+
total++;
|
|
77
|
+
if (n >= MAX) continue;
|
|
78
|
+
n++;
|
|
79
|
+
var ref = 'e' + n;
|
|
80
|
+
el.setAttribute(ATR, ref);
|
|
81
|
+
var p = papel(el), txt = nome(el), extra = '';
|
|
82
|
+
if (p === 'link') { var href = el.getAttribute('href') || ''; if (href && href.indexOf('javascript:') !== 0) extra = ' → ' + href.slice(0, 60); }
|
|
83
|
+
if (p === 'textbox' && el.placeholder && el.placeholder !== txt) extra = ' (' + limpo(el.placeholder, 40) + ')';
|
|
84
|
+
if ((p === 'textbox' || p === 'select') && el.value) extra += ' = "' + limpo(el.value, 40) + '"';
|
|
85
|
+
if ((p === 'checkbox' || p === 'radio') && el.checked) extra += ' [marcado]';
|
|
86
|
+
if (el.disabled) extra += ' [desativado]';
|
|
87
|
+
linhas.push('@' + ref + ' [' + p + '] "' + txt + '"' + extra);
|
|
88
|
+
}
|
|
89
|
+
return { titulo: document.title, url: location.href, linhas: linhas, total: total };
|
|
90
|
+
})()`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Texto do retrato, pronto para o modelo. */
|
|
94
|
+
function formatar(r, max = 80) {
|
|
95
|
+
if (!r) return '';
|
|
96
|
+
const cab = `${r.titulo || '(sem título)'} — ${r.url || ''}`;
|
|
97
|
+
const mais = r.total > max ? `\n… e mais ${r.total - max} elementos (role a página e leia de novo)` : '';
|
|
98
|
+
return `${cab}\n${(r.linhas || []).join('\n')}${mais}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** "@e3" ou "e3" → o seletor CSS da ref; qualquer outra coisa passa igual. */
|
|
102
|
+
function seletorDaRef(s) {
|
|
103
|
+
const m = /^@?(e\d+)$/.exec(String(s || '').trim());
|
|
104
|
+
return m ? `[${ATRIBUTO}="${m[1]}"]` : s;
|
|
105
|
+
}
|
|
106
|
+
function ehRef(s) { return /^@?e\d+$/.test(String(s || '').trim()); }
|
|
107
|
+
|
|
108
|
+
const COMO_USAR = 'Para agir, use a ref como selector: browser_click selector:"@e3", '
|
|
109
|
+
+ 'browser_type selector:"@e2" text:"…". A ref vale até a página mudar; depois de navegar, leia de novo.';
|
|
110
|
+
|
|
111
|
+
module.exports = { scriptDoRetrato, formatar, seletorDaRef, ehRef, COMO_USAR, ATRIBUTO };
|
package/lib/tools.js
CHANGED
|
@@ -61,6 +61,8 @@ async function clicarPeloCursor(x, y, { botao = 'left', duplo = false, rotulo }
|
|
|
61
61
|
() => (duplo ? desktop.doubleClick(nx, ny) : desktop.click(nx, ny, botao)));
|
|
62
62
|
}
|
|
63
63
|
const referencia = require('./referencia');
|
|
64
|
+
const snapshot = require('./snapshot');
|
|
65
|
+
const casar = require('./casar');
|
|
64
66
|
const estudio = require('./estudio');
|
|
65
67
|
const cfg = require('./config');
|
|
66
68
|
|
|
@@ -85,7 +87,8 @@ function resolveInProject(rawPath, projectDir) {
|
|
|
85
87
|
}
|
|
86
88
|
const target = path.resolve(base, rawPath || '.');
|
|
87
89
|
const rel = path.relative(base, target);
|
|
88
|
-
|
|
90
|
+
// `..` como SEGMENTO, não como começo de nome: "..rascunho" é legítimo.
|
|
91
|
+
if (rel === '..' || rel.startsWith('..' + path.sep) || rel.startsWith('../') || path.isAbsolute(rel)) {
|
|
89
92
|
throw new Error(`Caminho fora da pasta de trabalho: ${rawPath}`);
|
|
90
93
|
}
|
|
91
94
|
return target;
|
|
@@ -100,7 +103,10 @@ function treeDir(dir, maxDepth = 2, currentDepth = 0, prefix = '') {
|
|
|
100
103
|
for (const item of items) {
|
|
101
104
|
if (item.startsWith('.') && item !== '.env') continue;
|
|
102
105
|
if (IGNORAR.has(item)) continue;
|
|
103
|
-
|
|
106
|
+
// Link quebrado não derruba a listagem inteira: entra como arquivo.
|
|
107
|
+
let ehPasta = false;
|
|
108
|
+
try { ehPasta = fs.statSync(path.join(dir, item)).isDirectory(); } catch { ehPasta = false; }
|
|
109
|
+
(ehPasta ? dirs : files).push(item);
|
|
104
110
|
}
|
|
105
111
|
dirs.sort(); files.sort();
|
|
106
112
|
const all = [...dirs, ...files];
|
|
@@ -109,7 +115,7 @@ function treeDir(dir, maxDepth = 2, currentDepth = 0, prefix = '') {
|
|
|
109
115
|
const isLast = i === all.length - 1;
|
|
110
116
|
result += prefix + (isLast ? '└── ' : '├── ') + item + '\n';
|
|
111
117
|
const full = path.join(dir, item);
|
|
112
|
-
if (
|
|
118
|
+
if (dirs.includes(item) && currentDepth < maxDepth) {
|
|
113
119
|
result += treeDir(full, maxDepth, currentDepth + 1, prefix + (isLast ? ' ' : '│ '));
|
|
114
120
|
}
|
|
115
121
|
}
|
|
@@ -262,7 +268,19 @@ async function runInNewTerminal(command, cwd) {
|
|
|
262
268
|
como ~/Documents/Meus Projetos virava dois argumentos e o cd falhava
|
|
263
269
|
calado: o terminal abria na pasta errada e o comando rodava no lugar
|
|
264
270
|
errado, que é pior que não abrir. */
|
|
265
|
-
|
|
271
|
+
/* Sem shell no meio: o script vai como UM argumento do osascript
|
|
272
|
+
(execFile), e dentro dele a string do AppleScript é escapada (\\ e
|
|
273
|
+
\"). Antes o comando entrava cru entre aspas duplas dentro de aspas
|
|
274
|
+
simples — `node server.js --name="x"` quebrava o AppleScript. A
|
|
275
|
+
pasta vai entre aspas simples para o shell do Terminal (a aspa
|
|
276
|
+
simples está barrada acima). */
|
|
277
|
+
const doTerminal = `cd '${pastaAlvo}' && ${command}`;
|
|
278
|
+
const script = `tell application "Terminal" to do script "${doTerminal.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
279
|
+
return new Promise((resolve) => {
|
|
280
|
+
execFile('osascript', ['-e', script], { timeout: 60000 }, (erro, _o, stderr) => {
|
|
281
|
+
resolve(erro ? { ok: false, error: String(stderr || erro.message).trim() || 'falhou' } : { ok: true, opener: 'osascript' });
|
|
282
|
+
});
|
|
283
|
+
});
|
|
266
284
|
} else if (p === 'win32') {
|
|
267
285
|
// Aspas em volta do caminho: "C:\Users\Joel Silva\..." é comum, e sem
|
|
268
286
|
// elas o cmd corta no espaço. A aspa dupla já está barrada acima, então
|
|
@@ -273,7 +291,8 @@ async function runInNewTerminal(command, cwd) {
|
|
|
273
291
|
o caminho não precisa entrar na string de shell nenhuma. É a solução
|
|
274
292
|
mais segura das três: o que não é interpolado não pode ser mal
|
|
275
293
|
escapado. */
|
|
276
|
-
|
|
294
|
+
// A aspa simples do comando fecharia o `bash -c '…'`: vira '\''.
|
|
295
|
+
cmd = `x-terminal-emulator -e "bash -c '${String(command).replace(/'/g, "'\\''")}; exec bash'"`;
|
|
277
296
|
pastaDoProcesso = pastaAlvo;
|
|
278
297
|
}
|
|
279
298
|
const r = await runShell(cmd, 60000, pastaDoProcesso);
|
|
@@ -315,7 +334,19 @@ function editFile({ path: rawPath, old_string, new_string, replace_all }, projec
|
|
|
315
334
|
const original = fs.readFileSync(filePath, 'utf-8');
|
|
316
335
|
const ocorrencias = original.split(old_string).length - 1;
|
|
317
336
|
|
|
337
|
+
// Não bateu letra por letra: o casador frouxo (lib/casar.js) acha o
|
|
338
|
+
// trecho com o espaço, a indentação ou o número de linha errados — desde
|
|
339
|
+
// que seja UM lugar só. Poupa a volta "não achei → relê → edita de novo".
|
|
340
|
+
let casado = null;
|
|
318
341
|
if (ocorrencias === 0) {
|
|
342
|
+
const r = casar.achar(original, old_string);
|
|
343
|
+
if (r.erro === 'varios') {
|
|
344
|
+
return { ok: false, error: `Trecho aparece ${r.vezes} vezes (${r.como}). Inclua mais linhas em volta para ficar único.` };
|
|
345
|
+
}
|
|
346
|
+
if (!r.erro) casado = r;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (ocorrencias === 0 && !casado) {
|
|
319
350
|
// Erro seco faz o modelo chutar de novo. Devolvendo o que EXISTE no
|
|
320
351
|
// arquivo, ele acerta na segunda tentativa.
|
|
321
352
|
return {
|
|
@@ -329,15 +360,18 @@ function editFile({ path: rawPath, old_string, new_string, replace_all }, projec
|
|
|
329
360
|
return { ok: false, error: `Trecho aparece ${ocorrencias} vezes. Inclua mais linhas em volta para ficar único, ou use replace_all.` };
|
|
330
361
|
}
|
|
331
362
|
|
|
332
|
-
const atualizado =
|
|
333
|
-
? original.
|
|
334
|
-
:
|
|
363
|
+
const atualizado = casado
|
|
364
|
+
? original.slice(0, casado.inicio) + (new_string ?? '') + original.slice(casado.fim)
|
|
365
|
+
: replace_all
|
|
366
|
+
? original.split(old_string).join(new_string ?? '')
|
|
367
|
+
: original.replace(old_string, () => new_string ?? '');
|
|
335
368
|
|
|
336
369
|
fs.writeFileSync(filePath, atualizado, 'utf-8');
|
|
337
370
|
return {
|
|
338
371
|
ok: true,
|
|
339
372
|
path: path.relative(projectDir, filePath).replace(/\\/g, '/'),
|
|
340
|
-
substituicoes: replace_all ? ocorrencias : 1,
|
|
373
|
+
substituicoes: replace_all && !casado ? ocorrencias : 1,
|
|
374
|
+
casouPor: casado ? casado.como : undefined,
|
|
341
375
|
linhasAntes: original.split('\n').length,
|
|
342
376
|
linhasDepois: atualizado.split('\n').length,
|
|
343
377
|
// As duas versões, para a TELA desenhar o diff. O `_` na frente marca
|
|
@@ -391,12 +425,21 @@ function multiEdit({ path: rawPath, edits }, projectDir) {
|
|
|
391
425
|
if (old_string === new_string) return { ok: false, error: `edit ${i + 1}: old_string e new_string são iguais` };
|
|
392
426
|
const ocorrencias = conteudo.split(old_string).length - 1;
|
|
393
427
|
if (ocorrencias === 0) {
|
|
394
|
-
|
|
428
|
+
const r = casar.achar(conteudo, old_string);
|
|
429
|
+
if (r.erro === 'varios') {
|
|
430
|
+
return { ok: false, error: `edit ${i + 1}: trecho aparece ${r.vezes}x (${r.como}) — inclua mais contexto para torná-lo único` };
|
|
431
|
+
}
|
|
432
|
+
if (r.erro) {
|
|
433
|
+
return { ok: false, error: `edit ${i + 1}: trecho não encontrado`, trechosParecidos: trechosParecidos(conteudo, old_string) };
|
|
434
|
+
}
|
|
435
|
+
conteudo = conteudo.slice(0, r.inicio) + (new_string ?? '') + conteudo.slice(r.fim);
|
|
436
|
+
substituicoes++;
|
|
437
|
+
continue;
|
|
395
438
|
}
|
|
396
439
|
if (ocorrencias > 1 && !replace_all) {
|
|
397
440
|
return { ok: false, error: `edit ${i + 1}: trecho aparece ${ocorrencias}x — use replace_all ou inclua mais contexto para torná-lo único` };
|
|
398
441
|
}
|
|
399
|
-
conteudo = replace_all ? conteudo.split(old_string).join(new_string ?? '') : conteudo.replace(old_string, new_string ?? '');
|
|
442
|
+
conteudo = replace_all ? conteudo.split(old_string).join(new_string ?? '') : conteudo.replace(old_string, () => new_string ?? '');
|
|
400
443
|
substituicoes++;
|
|
401
444
|
}
|
|
402
445
|
|
|
@@ -544,24 +587,40 @@ async function despachar(name, args, ctx) {
|
|
|
544
587
|
}
|
|
545
588
|
|
|
546
589
|
// ── Navegador controlado (CDP) — o agente age numa página real ──
|
|
590
|
+
/* O navegador fala em REFS (lib/snapshot.js): abrir e clicar já
|
|
591
|
+
devolvem o retrato da página com "@e3 [button] Entrar", e o
|
|
592
|
+
selector aceita "@e3". Uma leitura a menos por passo, e nenhum
|
|
593
|
+
seletor CSS chutado. */
|
|
547
594
|
case 'browser_open': {
|
|
548
595
|
const alvo = /^https?:\/\//i.test(args.url || '')
|
|
549
596
|
? args.url
|
|
550
597
|
: 'file://' + resolveInProject(args.url, projectDir);
|
|
551
|
-
|
|
598
|
+
const r = await browser.open(alvo);
|
|
599
|
+
return r && r.ok !== false ? { ...r, pagina: await browser.retrato(40), comoUsar: snapshot.COMO_USAR } : r;
|
|
600
|
+
}
|
|
601
|
+
case 'browser_click': {
|
|
602
|
+
let r;
|
|
603
|
+
if (args.selector || snapshot.ehRef(args.text)) r = await browser.clickSelector(snapshot.seletorDaRef(args.selector || args.text));
|
|
604
|
+
else if (args.text) r = await browser.clickText(args.text);
|
|
605
|
+
else if (args.x != null && args.y != null) r = await browser.click(args.x, args.y);
|
|
606
|
+
else return { ok: false, error: 'browser_click precisa de selector (ou a ref "@e3"), text ou x/y' };
|
|
607
|
+
// O clique quase sempre muda a página: o retrato novo vai junto.
|
|
608
|
+
await new Promise((ok) => setTimeout(ok, 500));
|
|
609
|
+
return r && r.ok !== false ? { ...r, pagina: await browser.retrato(40) } : r;
|
|
552
610
|
}
|
|
553
|
-
case 'browser_click':
|
|
554
|
-
if (args.selector) return browser.clickSelector(args.selector);
|
|
555
|
-
if (args.text) return browser.clickText(args.text);
|
|
556
|
-
if (args.x != null && args.y != null) return browser.click(args.x, args.y);
|
|
557
|
-
return { ok: false, error: 'browser_click precisa de selector, text ou x/y' };
|
|
558
611
|
case 'browser_type':
|
|
559
|
-
if (args.selector) return browser.typeInSelector(args.selector, args.text || '', args.clear !== false);
|
|
612
|
+
if (args.selector) return browser.typeInSelector(snapshot.seletorDaRef(args.selector), args.text || '', args.clear !== false);
|
|
560
613
|
return browser.type(args.text || '');
|
|
561
614
|
case 'browser_key':
|
|
562
615
|
return browser.key(args.key);
|
|
563
616
|
case 'browser_read': {
|
|
564
|
-
|
|
617
|
+
if (!args.selector) {
|
|
618
|
+
const pagina = await browser.retrato(80);
|
|
619
|
+
const texto = await browser.evaluate('(function(){return document.body?document.body.innerText.slice(0,2500):null;})()');
|
|
620
|
+
if (!pagina && texto == null) return { ok: false, error: 'nada na página — ela pode não ter carregado' };
|
|
621
|
+
return { ok: true, pagina, text: texto || '', comoUsar: snapshot.COMO_USAR };
|
|
622
|
+
}
|
|
623
|
+
const sel = `document.querySelector(${JSON.stringify(snapshot.seletorDaRef(args.selector))})`;
|
|
565
624
|
const texto = await browser.evaluate(`(function(){const e=${sel};return e?e.innerText.slice(0,6000):null;})()`);
|
|
566
625
|
return texto == null ? { ok: false, error: 'nada na página (ou seletor não encontrado)' } : { ok: true, text: texto };
|
|
567
626
|
}
|
|
@@ -573,7 +632,7 @@ async function despachar(name, args, ctx) {
|
|
|
573
632
|
case 'browser_scroll':
|
|
574
633
|
return browser.scroll(args.dy != null ? args.dy : 600);
|
|
575
634
|
case 'browser_wait':
|
|
576
|
-
return browser.waitFor(args.selector, args.timeout_ms || 15000);
|
|
635
|
+
return browser.waitFor(snapshot.seletorDaRef(args.selector), args.timeout_ms || 15000);
|
|
577
636
|
case 'browser_eval':
|
|
578
637
|
return { ok: true, value: await browser.evaluate(args.js || 'null') };
|
|
579
638
|
|
package/lib/ui.js
CHANGED
|
@@ -310,10 +310,10 @@ const TIPS = [
|
|
|
310
310
|
];
|
|
311
311
|
|
|
312
312
|
const NEWS = [
|
|
313
|
-
'
|
|
314
|
-
'
|
|
315
|
-
'
|
|
316
|
-
'
|
|
313
|
+
'Digite enquanto ele trabalha: o recado entra na tarefa depois do passo atual',
|
|
314
|
+
'Mais rápido: não relê o que escreveu, e a espera aparece com os segundos',
|
|
315
|
+
'primocode video help — o editor de vídeo que o agente opera sozinho',
|
|
316
|
+
'Motion novo: vox, marca e lancamento · /voice: fale, e ele responde em voz',
|
|
317
317
|
'Esc Esc a qualquer momento para o agente no meio do trabalho',
|
|
318
318
|
];
|
|
319
319
|
|
|
@@ -413,6 +413,7 @@ class Spinner {
|
|
|
413
413
|
this.text = this.textos[0];
|
|
414
414
|
this.frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
415
415
|
this.i = 0; this.timer = null; this.desde = 0; this.largura = 0;
|
|
416
|
+
this.detalhe = ''; // o que está chegando agora (act.js atualiza)
|
|
416
417
|
}
|
|
417
418
|
start() {
|
|
418
419
|
// Fora de um terminal (pipe, CI) a animação só sujaria a saída.
|
|
@@ -423,10 +424,16 @@ class Spinner {
|
|
|
423
424
|
// curta que a anterior: o resto da linha velha ficaria na tela, então
|
|
424
425
|
// a linha é sempre preenchida até a maior largura já escrita.
|
|
425
426
|
if (this.textos.length > 1) this.text = this.textos[Math.floor((Date.now() - this.desde) / 4000) % this.textos.length];
|
|
426
|
-
|
|
427
|
+
/* O relógio e o que está chegando. "Parece travado" era isto: a
|
|
428
|
+
mesma frase girando por um minuto inteiro enquanto o modelo
|
|
429
|
+
escrevia um arquivo grande. Com os segundos contando (e o que
|
|
430
|
+
já chegou, quando há), quem olha sabe que está andando. */
|
|
431
|
+
const seg = Math.floor((Date.now() - this.desde) / 1000);
|
|
432
|
+
const extra = (seg >= 3 ? ` · ${seg}s` : '') + (this.detalhe ? ` · ${this.detalhe}` : '');
|
|
433
|
+
const cru = ` ${this.frames[this.i]} ${this.text}${extra}`;
|
|
427
434
|
const falta = Math.max(0, this.largura - cru.length);
|
|
428
435
|
this.largura = Math.max(this.largura, cru.length);
|
|
429
|
-
process.stdout.write(`\r ${c.muted(this.frames[this.i])} ${c.muted(this.text)}${' '.repeat(falta)}`);
|
|
436
|
+
process.stdout.write(`\r ${c.muted(this.frames[this.i])} ${c.muted(this.text)}${c.dim(extra)}${' '.repeat(falta)}`);
|
|
430
437
|
this.i = (this.i+1) % this.frames.length;
|
|
431
438
|
}, 80);
|
|
432
439
|
return this;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* analise.js — silêncio e batida, como conta pura sobre números medidos.
|
|
3
|
+
*
|
|
4
|
+
* O ffmpeg mede (onde o áudio fica abaixo do limiar; as amostras cruas), e
|
|
5
|
+
* as decisões ficam aqui, testáveis sem áudio nenhum:
|
|
6
|
+
*
|
|
7
|
+
* · o SILÊNCIO vira trecho da linha do tempo com `padding` de respiro em
|
|
8
|
+
* cada ponta — cortar no exato instante em que a voz volta é o que faz
|
|
9
|
+
* corte automático soar como máquina (lib/estudio/video.js conta a mesma
|
|
10
|
+
* história). O silêncio no começo e no fim do arquivo sai inteiro;
|
|
11
|
+
* · a BATIDA é o pico de subida de energia (onset), com limiar que se
|
|
12
|
+
* adapta ao volume da música em volta. Não é um detector de tempo de
|
|
13
|
+
* estúdio; é o bastante para cortar no ritmo.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const T = require('./timeline.js');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Os trechos (quadros da linha do tempo) a apagar, dados os silêncios de
|
|
22
|
+
* cada mídia em segundos da origem: {assetId: [{de, ate}]}.
|
|
23
|
+
*/
|
|
24
|
+
function trechosDeSilencio(tl, clipes, silencios, { padding = 0.15, minimo = 2 } = {}) {
|
|
25
|
+
const ranges = [];
|
|
26
|
+
for (const c of clipes) {
|
|
27
|
+
const lista = silencios[c.asset] || [];
|
|
28
|
+
const dur = (tl.assets[c.asset] || {}).seconds || Infinity;
|
|
29
|
+
const { de, ate } = T.janelaOrigem(tl, c);
|
|
30
|
+
for (const s of lista) {
|
|
31
|
+
const a = s.de <= 0.05 ? s.de : s.de + padding;
|
|
32
|
+
const b = s.ate >= dur - 0.05 ? s.ate : s.ate - padding;
|
|
33
|
+
const ia = Math.max(a, de);
|
|
34
|
+
const ib = Math.min(b, ate);
|
|
35
|
+
if (ib <= ia) continue;
|
|
36
|
+
const fa = T.origemParaQuadro(tl, c, ia);
|
|
37
|
+
const fb = T.origemParaQuadro(tl, c, ib);
|
|
38
|
+
if (fb - fa >= minimo) ranges.push([fa, fb]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return ranges;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Amostras (Float32, mono) → {bpm, beats: [segundos]}. */
|
|
45
|
+
function batidas(amostras, taxa, { sensibilidade = 1.4, minimoEntre = 0.22 } = {}) {
|
|
46
|
+
const hop = Math.max(64, Math.round(taxa * 0.0116)); // ~11,6 ms
|
|
47
|
+
const janela = hop * 2;
|
|
48
|
+
const energia = [];
|
|
49
|
+
for (let i = 0; i + janela <= amostras.length; i += hop) {
|
|
50
|
+
let s = 0;
|
|
51
|
+
for (let j = i; j < i + janela; j++) s += amostras[j] * amostras[j];
|
|
52
|
+
energia.push(Math.log(1e-9 + s / janela));
|
|
53
|
+
}
|
|
54
|
+
const onset = energia.map((e, i) => (i ? Math.max(0, e - energia[i - 1]) : 0));
|
|
55
|
+
const porSeg = taxa / hop;
|
|
56
|
+
const raio = Math.round(porSeg * 0.5);
|
|
57
|
+
const beats = [];
|
|
58
|
+
let ultimo = -Infinity;
|
|
59
|
+
for (let i = 1; i < onset.length - 1; i++) {
|
|
60
|
+
const a = Math.max(0, i - raio);
|
|
61
|
+
const b = Math.min(onset.length, i + raio);
|
|
62
|
+
let soma = 0;
|
|
63
|
+
let soma2 = 0;
|
|
64
|
+
for (let j = a; j < b; j++) { soma += onset[j]; soma2 += onset[j] * onset[j]; }
|
|
65
|
+
const n = b - a;
|
|
66
|
+
const media = soma / n;
|
|
67
|
+
const dp = Math.sqrt(Math.max(0, soma2 / n - media * media));
|
|
68
|
+
const limiar = media + sensibilidade * dp;
|
|
69
|
+
let pico = onset[i] > limiar && onset[i] > 0.3;
|
|
70
|
+
for (let j = Math.max(0, i - 3); pico && j <= Math.min(onset.length - 1, i + 3); j++) if (onset[j] > onset[i]) pico = false;
|
|
71
|
+
const t = (i * hop) / taxa;
|
|
72
|
+
if (pico && t - ultimo >= minimoEntre) { beats.push(Math.round(t * 1000) / 1000); ultimo = t; }
|
|
73
|
+
}
|
|
74
|
+
return { bpm: bpmDe(beats), beats };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function bpmDe(beats) {
|
|
78
|
+
if (beats.length < 4) return null;
|
|
79
|
+
const iv = [];
|
|
80
|
+
for (let i = 1; i < beats.length; i++) iv.push(beats[i] - beats[i - 1]);
|
|
81
|
+
iv.sort((a, b) => a - b);
|
|
82
|
+
let med = iv[Math.floor(iv.length / 2)];
|
|
83
|
+
let bpm = 60 / med;
|
|
84
|
+
while (bpm < 70) bpm *= 2;
|
|
85
|
+
while (bpm > 180) bpm /= 2;
|
|
86
|
+
return Math.round(bpm);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { trechosDeSilencio, batidas, bpmDe };
|