primocode 8.42.2 → 9.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 +41 -37
- package/bin/primocode.js +598 -360
- package/lib/acervo.js +8 -1
- package/lib/act.js +205 -114
- package/lib/api.js +35 -4
- package/lib/boas-vindas.js +31 -192
- package/lib/catalogo.js +60 -60
- package/lib/config.js +1 -1
- package/lib/conta.js +3 -24
- package/lib/diff.js +161 -0
- package/lib/diretor.js +37 -47
- package/lib/entrada.js +545 -0
- package/lib/especialistas.js +2 -2
- package/lib/estudio/assets.js +343 -0
- package/lib/estudio/identidade.js +560 -0
- package/lib/estudio/index.js +565 -0
- package/lib/estudio/plano.js +327 -0
- package/lib/estudio/projeto.js +246 -0
- package/lib/estudio/remotion.js +606 -0
- package/lib/estudio/slides.js +190 -0
- package/lib/estudio/video.js +296 -0
- package/lib/fala.js +13 -1
- package/lib/marca.js +4 -1
- package/lib/marketplace.js +189 -0
- package/lib/permissao.js +137 -0
- package/lib/registro.js +229 -0
- package/lib/skills.js +329 -0
- package/lib/tools.js +51 -181
- package/lib/ui.js +7 -3
- package/lib/voz.js +239 -0
- package/package.json +12 -3
- package/skills/acessibilidade/SKILL.md +28 -0
- package/skills/api-que-nao-quebra/SKILL.md +27 -0
- package/skills/depurar-de-verdade/SKILL.md +29 -0
- package/skills/editar-meu-video/SKILL.md +59 -0
- package/skills/escrever-em-portugues/SKILL.md +28 -0
- package/skills/git-limpo/SKILL.md +28 -0
- package/skills/motion-de-primeira/SKILL.md +62 -0
- package/skills/pagina-que-converte/SKILL.md +28 -0
- package/skills/performance-web/SKILL.md +28 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
- package/skills/slides-que-prendem/SKILL.md +40 -0
- package/studio/edge.py +16 -66
- package/studio/narrar.py +274 -0
- package/studio/pessoa.py +167 -0
- package/voz/primo_voz/__init__.py +0 -0
- package/voz/primo_voz/__main__.py +325 -0
- package/voz/primo_voz/escuta.py +235 -0
- package/voz/primo_voz/orbe.py +216 -0
- package/lib/audio-livre.js +0 -177
- package/lib/chaves.js +0 -149
- package/lib/claude-engine.js +0 -631
- package/lib/codex-engine.js +0 -208
- package/lib/remotion.js +0 -663
- package/lib/studio.js +0 -960
- package/studio/PRIMOCODE.md +0 -58
- package/studio/README.md +0 -154
- package/studio/__pycache__/edge.cpython-311.pyc +0 -0
- package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
- package/studio/ferramentas/00-protocolo.md +0 -125
- package/studio/ferramentas/01-modelos.md +0 -270
- package/studio/ferramentas/README.md +0 -32
- package/studio/ferramentas/corte.md +0 -919
- package/studio/ferramentas/grade.md +0 -92
- package/studio/ferramentas/prisma.md +0 -107
- package/studio/ferramentas/prosa.md +0 -86
- package/studio/ferramentas/tela.md +0 -93
- package/studio/ferramentas/traco.md +0 -79
- package/studio/server.py +0 -2254
- package/studio/studio.log +0 -57
- package/studio/voz.py +0 -388
- package/studio/web/analise.html +0 -52
- package/studio/web/core.css +0 -1167
- package/studio/web/fontes/bebas.woff2 +0 -0
- package/studio/web/fontes/grotesk-700.woff2 +0 -0
- package/studio/web/fontes/inter-400.woff2 +0 -0
- package/studio/web/fontes/inter-700.woff2 +0 -0
- package/studio/web/fontes/jetbrains-400.woff2 +0 -0
- package/studio/web/fontes/playfair-700.woff2 +0 -0
- package/studio/web/fontes.css +0 -23
- package/studio/web/home.html +0 -217
- package/studio/web/js/analise.js +0 -510
- package/studio/web/js/audio.js +0 -607
- package/studio/web/js/editor.js +0 -1802
- package/studio/web/js/exportar.js +0 -222
- package/studio/web/js/ferramentas.js +0 -108
- package/studio/web/js/icones.js +0 -120
- package/studio/web/js/midia.js +0 -157
- package/studio/web/js/narracao.js +0 -325
- package/studio/web/js/player.js +0 -903
- package/studio/web/js/render-grade.js +0 -243
- package/studio/web/js/render-prosa.js +0 -104
- package/studio/web/js/render.js +0 -1852
- package/studio/web/js/sfx.js +0 -233
- package/studio/web/js/video.js +0 -464
- package/studio/web/js/voz-neural.js +0 -152
- package/studio/web/render.css +0 -1352
- package/studio/web/tf.html +0 -15
- package/studio/web/tool.html +0 -78
- package/studio/web/view.html +0 -27
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* marketplace.js — a tela das skills, navegável pelas setas.
|
|
3
|
+
*
|
|
4
|
+
* "/marketplace abre uma tela navegável no terminal, com navegação pelas
|
|
5
|
+
* setas, seções (uma delas é a seção de Skills) e rolagem para baixo. O
|
|
6
|
+
* usuário navega, ativa e instala skills ali. Dentro do marketplace existe
|
|
7
|
+
* a opção 'New Skill': seleciona com as setas, dá Enter, e o terminal pede
|
|
8
|
+
* em sequência o nome e o conteúdo da skill."
|
|
9
|
+
*
|
|
10
|
+
* ── O DESENHO É FUNÇÃO PURA, DE NOVO ─────────────────────────────────────
|
|
11
|
+
* `montar()` recebe um estado e devolve linhas. `andar()` recebe uma tecla e
|
|
12
|
+
* devolve o estado seguinte. Nenhuma das duas toca no terminal nem no disco.
|
|
13
|
+
*
|
|
14
|
+
* É o mesmo desenho do `/effort` e da caixa de digitar, e pelo mesmo motivo:
|
|
15
|
+
* uma tela de terminal que só existe enquanto alguém aperta seta é uma tela
|
|
16
|
+
* que ninguém consegue testar. Assim o teste percorre a lista inteira, abre e
|
|
17
|
+
* fecha seção, rola até o fim e confere o que apareceu — sem nada na tela.
|
|
18
|
+
*
|
|
19
|
+
* ── POR QUE A LISTA É PLANA POR DENTRO ───────────────────────────────────
|
|
20
|
+
* A tela tem seções, mas a navegação é UMA lista: título de seção é item que
|
|
21
|
+
* não se escolhe, e a seta pula por cima dele. Tentar navegar em árvore (seta
|
|
22
|
+
* para entrar, outra para sair) é o tipo de coisa que exige explicação — e
|
|
23
|
+
* uma tela que precisa de manual não serve para quem abriu o terminal para
|
|
24
|
+
* fazer outra coisa.
|
|
25
|
+
*
|
|
26
|
+
* ── O QUE ESTE MARKETPLACE NÃO É ─────────────────────────────────────────
|
|
27
|
+
* Não há loja na internet. O catálogo é o que vem no produto mais o que
|
|
28
|
+
* estiver instalado na máquina, e é isso que a tela mostra — sem inventar um
|
|
29
|
+
* "1.248 skills disponíveis" que não existe. O dia em que existir um índice
|
|
30
|
+
* remoto, ele entra como mais uma seção.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
const { c, truncate, padRight, visibleWidth } = require('./ui.js');
|
|
36
|
+
|
|
37
|
+
// ── o estado ─────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {Array} skills o que `skills.listar()` devolveu
|
|
41
|
+
*/
|
|
42
|
+
function novo(skills) {
|
|
43
|
+
// A seleção nasce no primeiro item ESCOLHÍVEL, não no zero — o zero é um
|
|
44
|
+
// título de seção, e uma tela que abre sem nada marcado parece travada.
|
|
45
|
+
const base = { skills: skills || [], sel: 0, topo: 0, saiu: null };
|
|
46
|
+
const lista = itens(base);
|
|
47
|
+
base.sel = escolhivel(lista[0]) ? 0 : proximo(lista, 0, 1);
|
|
48
|
+
return base;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A lista plana: cabeçalhos de seção e itens, na ordem em que aparecem.
|
|
53
|
+
*
|
|
54
|
+
* Cada item diz o que é e o que fazer com ele. A tela e as teclas leem daqui,
|
|
55
|
+
* então não existe a chance de uma delas achar que o terceiro item é outro.
|
|
56
|
+
*/
|
|
57
|
+
function itens(estado) {
|
|
58
|
+
const lista = [];
|
|
59
|
+
const ligadas = estado.skills.filter((s) => s.ligada);
|
|
60
|
+
const desligadas = estado.skills.filter((s) => !s.ligada);
|
|
61
|
+
|
|
62
|
+
lista.push({ tipo: 'secao', texto: 'Skills ativas', conta: ligadas.length });
|
|
63
|
+
if (!ligadas.length) lista.push({ tipo: 'vazio', texto: 'nenhuma ativa' });
|
|
64
|
+
for (const s of ligadas) lista.push({ tipo: 'skill', skill: s });
|
|
65
|
+
|
|
66
|
+
lista.push({ tipo: 'secao', texto: 'Desligadas', conta: desligadas.length });
|
|
67
|
+
if (!desligadas.length) lista.push({ tipo: 'vazio', texto: 'nenhuma desligada' });
|
|
68
|
+
for (const s of desligadas) lista.push({ tipo: 'skill', skill: s });
|
|
69
|
+
|
|
70
|
+
lista.push({ tipo: 'secao', texto: 'Criar' });
|
|
71
|
+
lista.push({ tipo: 'nova', texto: 'New Skill', detalhe: 'escreva a sua — o nome e o prompt' });
|
|
72
|
+
lista.push({ tipo: 'instalar', texto: 'Instalar de um arquivo', detalhe: 'um SKILL.md que você tem no disco' });
|
|
73
|
+
|
|
74
|
+
return lista;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Dá para parar em cima deste item? Título de seção e vazio, não. */
|
|
78
|
+
const escolhivel = (item) => item && item.tipo !== 'secao' && item.tipo !== 'vazio';
|
|
79
|
+
|
|
80
|
+
/** O primeiro item escolhível a partir de `de`, andando em `passo`. */
|
|
81
|
+
function proximo(lista, de, passo) {
|
|
82
|
+
let i = de;
|
|
83
|
+
for (let n = 0; n < lista.length; n++) {
|
|
84
|
+
i += passo;
|
|
85
|
+
if (i < 0) i = lista.length - 1;
|
|
86
|
+
if (i >= lista.length) i = 0;
|
|
87
|
+
if (escolhivel(lista[i])) return i;
|
|
88
|
+
}
|
|
89
|
+
return de;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Uma tecla → o estado seguinte.
|
|
94
|
+
*
|
|
95
|
+
* Devolve `{ estado, acao }`. A ação é o que o mundo lá fora precisa fazer —
|
|
96
|
+
* ligar uma skill, abrir o diálogo de criar, sair. Este módulo não faz nada
|
|
97
|
+
* disso: ele não sabe gravar arquivo nem perguntar nada.
|
|
98
|
+
*/
|
|
99
|
+
function andar(estado, tecla, { altura = 14 } = {}) {
|
|
100
|
+
const lista = itens(estado);
|
|
101
|
+
const e = { ...estado };
|
|
102
|
+
|
|
103
|
+
// O primeiro pouso: se a seleção caiu num título, desce até um item.
|
|
104
|
+
if (!escolhivel(lista[e.sel])) e.sel = proximo(lista, e.sel, 1);
|
|
105
|
+
|
|
106
|
+
const nome = (tecla && tecla.name) || '';
|
|
107
|
+
|
|
108
|
+
if (nome === 'down' || nome === 'j') e.sel = proximo(lista, e.sel, 1);
|
|
109
|
+
else if (nome === 'up' || nome === 'k') e.sel = proximo(lista, e.sel, -1);
|
|
110
|
+
else if (nome === 'pagedown') { for (let i = 0; i < 5; i++) e.sel = proximo(lista, e.sel, 1); }
|
|
111
|
+
else if (nome === 'pageup') { for (let i = 0; i < 5; i++) e.sel = proximo(lista, e.sel, -1); }
|
|
112
|
+
else if (nome === 'home') { e.sel = proximo(lista, lista.length - 1, 1); }
|
|
113
|
+
else if (nome === 'end') { e.sel = proximo(lista, 0, -1); }
|
|
114
|
+
else if (nome === 'escape' || nome === 'q') return { estado: { ...e, saiu: 'fechar' }, acao: { tipo: 'fechar' } };
|
|
115
|
+
else if (nome === 'return' || nome === 'enter' || (tecla && tecla.sequence === ' ')) {
|
|
116
|
+
const item = lista[e.sel];
|
|
117
|
+
if (!item) return { estado: e, acao: null };
|
|
118
|
+
if (item.tipo === 'skill') {
|
|
119
|
+
return { estado: e, acao: { tipo: 'alternar', skill: item.skill } };
|
|
120
|
+
}
|
|
121
|
+
if (item.tipo === 'nova') return { estado: { ...e, saiu: 'nova' }, acao: { tipo: 'nova' } };
|
|
122
|
+
if (item.tipo === 'instalar') return { estado: { ...e, saiu: 'instalar' }, acao: { tipo: 'instalar' } };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// A janela acompanha a seleção: sem isto, descer faz o item escolhido
|
|
126
|
+
// sumir embaixo e a seta parece ter parado de funcionar.
|
|
127
|
+
const visiveis = Math.max(4, altura);
|
|
128
|
+
if (e.sel < e.topo) e.topo = e.sel;
|
|
129
|
+
if (e.sel >= e.topo + visiveis) e.topo = e.sel - visiveis + 1;
|
|
130
|
+
e.topo = Math.max(0, Math.min(e.topo, Math.max(0, lista.length - visiveis)));
|
|
131
|
+
|
|
132
|
+
return { estado: e, acao: null };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── o desenho ────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
function montar(estado, { colunas = 80, altura = 14 } = {}) {
|
|
138
|
+
const larg = Math.max(40, Math.min(colunas - 1, 110));
|
|
139
|
+
const lista = itens(estado);
|
|
140
|
+
const visiveis = Math.max(4, altura);
|
|
141
|
+
const topo = Math.max(0, Math.min(estado.topo || 0, Math.max(0, lista.length - visiveis)));
|
|
142
|
+
|
|
143
|
+
const out = [];
|
|
144
|
+
out.push('');
|
|
145
|
+
out.push(' ' + c.bold(c.brand('Marketplace')) + c.dim(' · as skills do Primo Code'));
|
|
146
|
+
out.push(' ' + c.dim('─'.repeat(larg - 4)));
|
|
147
|
+
|
|
148
|
+
for (let i = topo; i < Math.min(lista.length, topo + visiveis); i++) {
|
|
149
|
+
const item = lista[i];
|
|
150
|
+
const escolhido = i === estado.sel;
|
|
151
|
+
const seta = escolhido ? c.brand('❯ ') : ' ';
|
|
152
|
+
|
|
153
|
+
if (item.tipo === 'secao') {
|
|
154
|
+
// Linha em branco antes da seção, menos na primeira: é o que
|
|
155
|
+
// separa os grupos sem precisar de moldura.
|
|
156
|
+
if (out.length > 3) out.push('');
|
|
157
|
+
out.push(' ' + c.bold(c.white(item.texto))
|
|
158
|
+
+ (item.conta != null ? c.dim(` ${item.conta}`) : ''));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (item.tipo === 'vazio') { out.push(' ' + c.dim(item.texto)); continue; }
|
|
162
|
+
|
|
163
|
+
if (item.tipo === 'skill') {
|
|
164
|
+
const s = item.skill;
|
|
165
|
+
const marca = s.ligada ? c.ok('●') : c.dim('○');
|
|
166
|
+
const titulo = escolhido ? c.bold(c.white(s.titulo)) : c.white(s.titulo);
|
|
167
|
+
const selo = s.doProduto ? c.dim(' do produto') : c.brandDim(' sua');
|
|
168
|
+
out.push(' ' + seta + marca + ' ' + truncate(titulo + selo, larg - 8));
|
|
169
|
+
// A descrição só aparece no que está selecionado: com ela em
|
|
170
|
+
// todas, a lista dobra de altura e ninguém lê nenhuma.
|
|
171
|
+
if (escolhido && s.descricao) {
|
|
172
|
+
out.push(' ' + c.dim(truncate(s.descricao, larg - 8)));
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const titulo = escolhido ? c.bold(c.brand(item.texto)) : c.brandDim(item.texto);
|
|
178
|
+
out.push(' ' + seta + titulo);
|
|
179
|
+
if (escolhido && item.detalhe) out.push(' ' + c.dim(item.detalhe));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
out.push('');
|
|
183
|
+
out.push(' ' + c.dim('─'.repeat(larg - 4)));
|
|
184
|
+
out.push(' ' + c.dim('↑↓ navega · Enter liga e desliga · Esc fecha'));
|
|
185
|
+
out.push('');
|
|
186
|
+
return out.join('\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { novo, itens, andar, montar, escolhivel, proximo };
|
package/lib/permissao.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* permissao.js — o portão do controle do computador.
|
|
3
|
+
*
|
|
4
|
+
* "Implementar controle do computador do usuário. Sempre pedir permissão
|
|
5
|
+
* antes: aparece o pedido, o usuário autoriza, e só então o agente age. O
|
|
6
|
+
* usuário assiste o agente mexendo na máquina em tempo real."
|
|
7
|
+
*
|
|
8
|
+
* ── O QUE FALTAVA ────────────────────────────────────────────────────────
|
|
9
|
+
* O controle do computador já existia inteiro (lib/desktop.js): mouse,
|
|
10
|
+
* teclado, janelas, clique em botão pelo nome. O que não existia era o
|
|
11
|
+
* pedido. O agente decidia mexer e mexia — o ponteiro saía andando sozinho
|
|
12
|
+
* sem ninguém ter dito que podia.
|
|
13
|
+
*
|
|
14
|
+
* E a diferença entre isso e as outras ferramentas é real, não cerimônia:
|
|
15
|
+
* escrever arquivo acontece DENTRO da pasta do projeto, e o que der errado se
|
|
16
|
+
* desfaz. Mover o mouse e apertar teclas acontece no computador inteiro, no
|
|
17
|
+
* programa que estiver na frente — e um `alt+tab` no momento errado digita o
|
|
18
|
+
* que o agente ia escrever dentro da sua conversa aberta no WhatsApp.
|
|
19
|
+
*
|
|
20
|
+
* ── AS TRÊS RESPOSTAS ────────────────────────────────────────────────────
|
|
21
|
+
* "Sim, agora" · "Sim, esta sessão inteira" · "Não".
|
|
22
|
+
*
|
|
23
|
+
* A do meio existe porque uma tarefa de desktop são dez, vinte ações. Pedir
|
|
24
|
+
* a cada clique transformaria a permissão num botão que a pessoa aperta sem
|
|
25
|
+
* ler — que é o mesmo que não pedir, com uma etapa a mais. Pedir UMA vez, com
|
|
26
|
+
* o que ele vai fazer escrito, é a pergunta que ainda é lida.
|
|
27
|
+
*
|
|
28
|
+
* A sessão é a sessão: fechar o PrimoCode esquece tudo. Não há arquivo de
|
|
29
|
+
* "sempre permitir" de propósito — uma autorização permanente é exatamente o
|
|
30
|
+
* que ninguém lembra de ter dado.
|
|
31
|
+
*
|
|
32
|
+
* ── O QUE NÃO PASSA POR AQUI ─────────────────────────────────────────────
|
|
33
|
+
* Ler a tela (`desktop_screenshot`, `desktop_windows`, `desktop_elements`,
|
|
34
|
+
* `desktop_size`) não mexe em nada — é o agente ABRINDO O OLHO, e é o que ele
|
|
35
|
+
* precisa fazer para saber onde clicar. Exigir autorização para olhar
|
|
36
|
+
* obrigaria a pessoa a responder duas vezes a mesma pergunta: uma para ele
|
|
37
|
+
* ver, outra para ele agir.
|
|
38
|
+
*
|
|
39
|
+
* O print merece uma linha à parte: ele vê a sua tela inteira, e isso é
|
|
40
|
+
* sério. Mas quem decide se o conteúdo da tela sai da máquina é o modelo, e
|
|
41
|
+
* modelo de texto não enxerga imagem — o print vira arquivo no disco, para o
|
|
42
|
+
* agente medir onde as coisas estão. Ler é olhar; o portão é para AGIR.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
'use strict';
|
|
46
|
+
|
|
47
|
+
// Ferramentas que MEXEM na máquina. Todo o resto do `desktop_*` é olhar.
|
|
48
|
+
const AGEM = new Set([
|
|
49
|
+
'desktop_click', 'desktop_double_click', 'desktop_click_element',
|
|
50
|
+
'desktop_move', 'desktop_drag', 'desktop_type', 'desktop_key',
|
|
51
|
+
'desktop_scroll', 'desktop_open_app',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/* O que cada uma faz, em português de gente. Vai na pergunta: "autoriza
|
|
55
|
+
desktop_click_element" não é uma pergunta que alguém consiga responder. */
|
|
56
|
+
const EM_PORTUGUES = {
|
|
57
|
+
desktop_click: 'clicar na sua tela',
|
|
58
|
+
desktop_double_click: 'dar um clique duplo na sua tela',
|
|
59
|
+
desktop_click_element: 'clicar num botão da janela aberta',
|
|
60
|
+
desktop_move: 'mover o seu mouse',
|
|
61
|
+
desktop_drag: 'arrastar com o seu mouse',
|
|
62
|
+
desktop_type: 'digitar no programa que está na frente',
|
|
63
|
+
desktop_key: 'apertar teclas no programa que está na frente',
|
|
64
|
+
desktop_scroll: 'rolar a tela',
|
|
65
|
+
desktop_open_app: 'abrir um programa no seu computador',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function novo() {
|
|
69
|
+
return { liberado: false, negado: false, pedidos: 0 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Esta ferramenta mexe na máquina? */
|
|
73
|
+
function precisa(nome) { return AGEM.has(nome); }
|
|
74
|
+
|
|
75
|
+
/** A frase do que ele quer fazer, com o alvo quando há um. */
|
|
76
|
+
function descrever(nome, args) {
|
|
77
|
+
const a = args || {};
|
|
78
|
+
const base = EM_PORTUGUES[nome] || nome;
|
|
79
|
+
const alvo = a.name || a.app || a.window || a.text || a.key
|
|
80
|
+
|| (a.x != null ? `${a.x},${a.y}` : '');
|
|
81
|
+
const curto = String(alvo || '').replace(/\s+/g, ' ').trim();
|
|
82
|
+
return curto ? `${base} — ${curto.length > 60 ? curto.slice(0, 59) + '…' : curto}` : base;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* O portão.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} estado o estado da sessão (de `novo()`)
|
|
89
|
+
* @param {string} nome a ferramenta
|
|
90
|
+
* @param {object} args os argumentos dela
|
|
91
|
+
* @param {function} perguntar async (frase) => 'agora' | 'sessao' | 'nao'
|
|
92
|
+
* @returns {Promise<{ok: boolean, error?: string, primeira?: boolean}>}
|
|
93
|
+
*/
|
|
94
|
+
async function pedir(estado, nome, args, perguntar) {
|
|
95
|
+
if (!precisa(nome)) return { ok: true };
|
|
96
|
+
if (estado.liberado) return { ok: true };
|
|
97
|
+
|
|
98
|
+
/* Negar uma vez vale para a sessão. Sem isto, quem disse não leva a mesma
|
|
99
|
+
pergunta na ação seguinte, e na seguinte — o agente tem vinte ações
|
|
100
|
+
para tentar, e a pessoa acabaria dizendo sim de cansaço. */
|
|
101
|
+
if (estado.negado) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
error: 'O usuário NÃO autorizou o controle do computador nesta sessão. '
|
|
105
|
+
+ 'Não tente de novo com outra ferramenta de desktop: a resposta é a mesma. '
|
|
106
|
+
+ 'Resolva por outro caminho (arquivos, terminal, navegador) ou diga a ele '
|
|
107
|
+
+ 'o que precisaria ser feito à mão.',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Sem quem responda (pipe, CI, subagente), a resposta é NÃO. O padrão
|
|
112
|
+
// seguro de uma pergunta que ninguém ouviu não é "pode".
|
|
113
|
+
if (typeof perguntar !== 'function') {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
error: 'Controle do computador precisa de autorização, e não há ninguém '
|
|
117
|
+
+ 'para autorizar neste modo. Use outro caminho.',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const primeira = estado.pedidos === 0;
|
|
122
|
+
estado.pedidos++;
|
|
123
|
+
const resposta = await perguntar(descrever(nome, args), { primeira });
|
|
124
|
+
|
|
125
|
+
if (resposta === 'sessao') { estado.liberado = true; return { ok: true, primeira }; }
|
|
126
|
+
if (resposta === 'agora') return { ok: true, primeira };
|
|
127
|
+
|
|
128
|
+
estado.negado = true;
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
error: 'O usuário recusou o controle do computador. Não insista com outra '
|
|
132
|
+
+ 'ferramenta de desktop — resolva por outro caminho ou explique a ele '
|
|
133
|
+
+ 'o que precisaria ser feito à mão.',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { novo, precisa, pedir, descrever, AGEM, EM_PORTUGUES };
|
package/lib/registro.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* registro.js — o que este pedido fez, para dizer no fim.
|
|
3
|
+
*
|
|
4
|
+
* "Precisa existir um pipeline visível de execução: o que está fazendo, o
|
|
5
|
+
* que já fez, o que falhou. Exibir sempre: arquivos alterados, arquivos
|
|
6
|
+
* criados, links abertos, comandos rodados, resultados dos testes."
|
|
7
|
+
*
|
|
8
|
+
* ── O QUE FALTAVA ────────────────────────────────────────────────────────
|
|
9
|
+
* Tudo isso JÁ aparecia — uma linha por vez, conforme acontecia. O problema é
|
|
10
|
+
* que uma tarefa de verdade são trinta ações, e no fim delas a primeira já
|
|
11
|
+
* saiu da tela. Quem acompanhou viu tudo; quem voltou do café viu o fim de um
|
|
12
|
+
* rolo de texto e não sabe se o agente criou três arquivos ou trinta, nem se
|
|
13
|
+
* o teste que ele rodou passou.
|
|
14
|
+
*
|
|
15
|
+
* Então, além de narrar, ele ANOTA. No fim sai um balanço do pedido inteiro:
|
|
16
|
+
* o que nasceu, o que mudou, o que rodou, o que abriu, o que falhou.
|
|
17
|
+
*
|
|
18
|
+
* ── POR QUE ANOTAR AQUI E NÃO OLHAR O DISCO ──────────────────────────────
|
|
19
|
+
* A tentação é perguntar ao git no fim. Não serve: a pasta pode não ser um
|
|
20
|
+
* repositório, pode ter mudanças anteriores ao pedido, e o `git status` não
|
|
21
|
+
* sabe qual comando o agente rodou nem qual link ele abriu. O que interessa é
|
|
22
|
+
* o que o AGENTE fez neste pedido, e quem sabe isso é quem executou.
|
|
23
|
+
*
|
|
24
|
+
* ── O QUE CONTA COMO TESTE ───────────────────────────────────────────────
|
|
25
|
+
* Um `run_shell` cujo comando parece rodar teste. A distinção importa porque
|
|
26
|
+
* "rodei o teste" é a frase que mais se diz sem prova: com o registro, o que
|
|
27
|
+
* sai na tela é o comando REAL e o código de saída REAL dele. Quando a saída
|
|
28
|
+
* traz contagem reconhecível ("12 passed, 1 failed"), ela entra também — mas
|
|
29
|
+
* o código de saída é o que decide, porque é o que não dá para escrever por
|
|
30
|
+
* engano.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
const { c, truncate, padRight } = require('./ui.js');
|
|
36
|
+
|
|
37
|
+
/* Comando que é teste. Deliberadamente restrito: `npm run build` não é
|
|
38
|
+
teste, e chamar de teste o que não é seria pior que não contar nada. */
|
|
39
|
+
const EH_TESTE = /(^|\s|&&|\|\|)(npm\s+(run\s+)?tests?\b|npm\s+t\b|yarn\s+tests?\b|pnpm\s+tests?\b|pytest\b|python\s+-m\s+pytest\b|jest\b|vitest\b|mocha\b|go\s+test\b|cargo\s+test\b|mvn\s+test\b|rspec\b|phpunit\b)/i;
|
|
40
|
+
|
|
41
|
+
/* As contagens que os corredores de teste escrevem. Nenhuma é obrigatória:
|
|
42
|
+
quando nada casa, vale o código de saída, que é o que não mente. */
|
|
43
|
+
const CONTAGENS = [
|
|
44
|
+
/(\d+)\s+pass(?:ed|ing)\b/i,
|
|
45
|
+
/(\d+)\s+fail(?:ed|ing|ures?)\b/i,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function novo() {
|
|
49
|
+
return {
|
|
50
|
+
criados: [], // caminhos que nasceram neste pedido
|
|
51
|
+
alterados: [], // caminhos que já existiam e mudaram
|
|
52
|
+
comandos: [], // { comando, ok, code }
|
|
53
|
+
testes: [], // { comando, ok, code, resumo }
|
|
54
|
+
links: [], // URLs e arquivos abertos no navegador
|
|
55
|
+
falhas: [], // { acao, motivo }
|
|
56
|
+
pesquisas: [], // consultas que ele foi olhar antes de escrever
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const jaTem = (lista, valor) => lista.includes(valor);
|
|
61
|
+
|
|
62
|
+
/** Guarda sem repetir, e sem deixar a lista crescer sem fim. */
|
|
63
|
+
function guardar(lista, valor, teto = 60) {
|
|
64
|
+
if (!valor || jaTem(lista, valor)) return;
|
|
65
|
+
if (lista.length < teto) lista.push(valor);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A linha de resumo de um teste, quando dá para ler a contagem da saída. */
|
|
69
|
+
function resumoDeTeste(saida) {
|
|
70
|
+
const texto = String(saida || '');
|
|
71
|
+
const passou = (texto.match(CONTAGENS[0]) || [])[1];
|
|
72
|
+
const falhou = (texto.match(CONTAGENS[1]) || [])[1];
|
|
73
|
+
if (!passou && !falhou) return '';
|
|
74
|
+
const partes = [];
|
|
75
|
+
if (passou) partes.push(`${passou} passaram`);
|
|
76
|
+
if (falhou && Number(falhou) > 0) partes.push(`${falhou} falharam`);
|
|
77
|
+
return partes.join(', ');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Anota uma ação que acabou de acontecer.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} reg o registro deste pedido
|
|
84
|
+
* @param {string} nome o nome da tool
|
|
85
|
+
* @param {object} args os argumentos com que ela foi chamada
|
|
86
|
+
* @param {object} r o resultado que ela devolveu
|
|
87
|
+
*/
|
|
88
|
+
function anotar(reg, nome, args, r) {
|
|
89
|
+
if (!reg || !r) return reg;
|
|
90
|
+
const a = args || {};
|
|
91
|
+
|
|
92
|
+
// Ação que falhou entra sempre, seja qual for a tool: é justamente o que
|
|
93
|
+
// a pessoa precisa ver, e o que some mais rápido do rolo de texto.
|
|
94
|
+
if (r.ok === false) {
|
|
95
|
+
const motivo = String(r.error || `saiu com ${r.code}`).replace(/\s+/g, ' ').trim();
|
|
96
|
+
/* QUAL arquivo, QUAL comando. Sem isso a linha vira
|
|
97
|
+
"run_shell: saiu com 1" — que diz que algo falhou e não diz o quê,
|
|
98
|
+
que é o mesmo que não dizer nada. */
|
|
99
|
+
const alvo = String(a.path || a.command || a.target || a.url || a.consulta || '')
|
|
100
|
+
.replace(/\s+/g, ' ').trim();
|
|
101
|
+
if (reg.falhas.length < 20) reg.falhas.push({ acao: nome, alvo, motivo });
|
|
102
|
+
// Uma tool que falhou não criou nem alterou nada. Continuar daqui
|
|
103
|
+
// faria um write_file recusado aparecer na lista de criados.
|
|
104
|
+
return reg;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
switch (nome) {
|
|
108
|
+
case 'write_file':
|
|
109
|
+
// `created` vem do próprio tools.js: ele sabe se o arquivo existia.
|
|
110
|
+
guardar(r.created ? reg.criados : reg.alterados, r.path || a.path);
|
|
111
|
+
return reg;
|
|
112
|
+
|
|
113
|
+
case 'edit_file':
|
|
114
|
+
case 'multi_edit':
|
|
115
|
+
guardar(reg.alterados, r.path || a.path);
|
|
116
|
+
return reg;
|
|
117
|
+
|
|
118
|
+
case 'run_shell': {
|
|
119
|
+
const comando = String(a.command || '').replace(/\s+/g, ' ').trim();
|
|
120
|
+
if (!comando) return reg;
|
|
121
|
+
const item = { comando, ok: r.ok !== false, code: r.code ?? 0 };
|
|
122
|
+
if (EH_TESTE.test(comando)) {
|
|
123
|
+
item.resumo = resumoDeTeste(r.stdout || r.saida || r.output || '');
|
|
124
|
+
if (reg.testes.length < 20) reg.testes.push(item);
|
|
125
|
+
} else if (reg.comandos.length < 40) {
|
|
126
|
+
reg.comandos.push(item);
|
|
127
|
+
}
|
|
128
|
+
return reg;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case 'run_in_new_terminal':
|
|
132
|
+
if (reg.comandos.length < 40) {
|
|
133
|
+
reg.comandos.push({ comando: String(a.command || '').trim(), ok: true, code: 0, solto: true });
|
|
134
|
+
}
|
|
135
|
+
return reg;
|
|
136
|
+
|
|
137
|
+
case 'open_in_browser':
|
|
138
|
+
case 'browser_open':
|
|
139
|
+
guardar(reg.links, r.url || a.target || a.url);
|
|
140
|
+
return reg;
|
|
141
|
+
|
|
142
|
+
case 'pesquisar_referencia':
|
|
143
|
+
guardar(reg.pesquisas, a.consulta || a.query, 12);
|
|
144
|
+
return reg;
|
|
145
|
+
|
|
146
|
+
default:
|
|
147
|
+
return reg;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Junta o livro de um subagente no de quem o chamou.
|
|
153
|
+
*
|
|
154
|
+
* Sem isto o balanço mentiria por omissão: o especialista de frontend
|
|
155
|
+
* escreveria cinco arquivos e a tela diria "nenhum arquivo". As conversas
|
|
156
|
+
* seguem separadas — o que se junta é o que aconteceu no disco.
|
|
157
|
+
*/
|
|
158
|
+
function juntar(destino, outro) {
|
|
159
|
+
if (!destino || !outro) return destino;
|
|
160
|
+
for (const campo of ['criados', 'alterados', 'links', 'pesquisas']) {
|
|
161
|
+
for (const v of outro[campo] || []) guardar(destino[campo], v);
|
|
162
|
+
}
|
|
163
|
+
for (const campo of ['comandos', 'testes', 'falhas']) {
|
|
164
|
+
for (const v of outro[campo] || []) {
|
|
165
|
+
if (destino[campo].length < 40) destino[campo].push(v);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return destino;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Alguma coisa aconteceu? Um pedido de conversa pura não tem balanço. */
|
|
172
|
+
function vazio(reg) {
|
|
173
|
+
if (!reg) return true;
|
|
174
|
+
return !reg.criados.length && !reg.alterados.length && !reg.comandos.length
|
|
175
|
+
&& !reg.testes.length && !reg.links.length && !reg.falhas.length;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* O balanço, em texto pronto para a tela.
|
|
180
|
+
*
|
|
181
|
+
* Ordem deliberada: primeiro o que mudou no computador da pessoa (arquivos),
|
|
182
|
+
* depois o que ele rodou, depois a prova (testes), e por último o que falhou
|
|
183
|
+
* — que é o que ela precisa ler ANTES de ir embora, então fica onde o olho
|
|
184
|
+
* para, no fim.
|
|
185
|
+
*/
|
|
186
|
+
function resumo(reg, { colunas = 80 } = {}) {
|
|
187
|
+
if (vazio(reg)) return '';
|
|
188
|
+
const larg = Math.max(40, Math.min(colunas, 120));
|
|
189
|
+
const out = [];
|
|
190
|
+
|
|
191
|
+
const secao = (rotulo, itens, pinta) => {
|
|
192
|
+
if (!itens.length) return;
|
|
193
|
+
out.push(' ' + c.dim(padRight(rotulo, 12)) + pinta(itens[0]));
|
|
194
|
+
for (const item of itens.slice(1)) out.push(' ' + ' '.repeat(12) + pinta(item));
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
out.push('');
|
|
198
|
+
out.push(' ' + c.dim('─'.repeat(larg - 4)));
|
|
199
|
+
|
|
200
|
+
secao('criados', reg.criados, (f) => c.ok('+ ') + c.white(f));
|
|
201
|
+
secao('alterados', reg.alterados, (f) => c.brandDim('~ ') + c.white(f));
|
|
202
|
+
secao('comandos', reg.comandos.map((x) => x), (x) => {
|
|
203
|
+
const marca = x.solto ? c.dim('▸ ') : x.ok ? c.dim('$ ') : c.err('$ ');
|
|
204
|
+
return marca + truncate(x.ok ? c.muted(x.comando) : c.warn(x.comando), larg - 18);
|
|
205
|
+
});
|
|
206
|
+
secao('links', reg.links, (u) => c.dim('↗ ') + c.brandDim(truncate(u, larg - 18)));
|
|
207
|
+
|
|
208
|
+
// Os testes ganham seção própria porque são a PROVA. "Rodei os testes" é
|
|
209
|
+
// a frase que mais se diz sem prova; aqui sai o comando real e o código
|
|
210
|
+
// de saída real, que é o que não dá para escrever por engano.
|
|
211
|
+
if (reg.testes.length) {
|
|
212
|
+
secao('testes', reg.testes, (t) => {
|
|
213
|
+
const marca = t.ok ? c.ok('✔ ') : c.err('✗ ');
|
|
214
|
+
const cauda = t.resumo ? c.muted(' ' + t.resumo)
|
|
215
|
+
: t.ok ? c.dim(' passou') : c.warn(` saiu com ${t.code}`);
|
|
216
|
+
return marca + c.white(truncate(t.comando, larg - 30)) + cauda;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (reg.falhas.length) {
|
|
221
|
+
secao('falhou', reg.falhas, (f) => c.warn('! ')
|
|
222
|
+
+ c.muted(truncate(`${f.alvo || f.acao}${f.alvo ? '' : ''} — ${f.motivo}`, larg - 18)));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
out.push('');
|
|
226
|
+
return out.join('\n');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = { novo, anotar, juntar, resumo, vazio, resumoDeTeste, EH_TESTE };
|