primocode 8.42.1 → 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 +599 -327
- 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
package/lib/skills.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skills.js — o que o PrimoCode sabe fazer bem, escrito em texto.
|
|
3
|
+
*
|
|
4
|
+
* "O Primo Code já vem com várias skills instaladas por padrão. Comandos:
|
|
5
|
+
* /skills, /install-skill e /marketplace."
|
|
6
|
+
*
|
|
7
|
+
* ── O QUE É UMA SKILL, E O QUE ELA NÃO É ─────────────────────────────────
|
|
8
|
+
* Uma skill é uma pasta com um `SKILL.md` dentro: um cabeçalho que diz quem
|
|
9
|
+
* ela é e quando serve, e um corpo que é o prompt dela. Nada de código, nada
|
|
10
|
+
* de instalar dependência, nada que rode. É conhecimento, não programa.
|
|
11
|
+
*
|
|
12
|
+
* Isso é decisão, não limitação. Uma skill que executa código é uma porta
|
|
13
|
+
* para qualquer pessoa mandar um arquivo e rodar o que quiser na máquina de
|
|
14
|
+
* quem instalou — e um marketplace de programas executáveis exige assinatura,
|
|
15
|
+
* revisão e sandbox, que é outro produto. Skill é texto: o pior que um texto
|
|
16
|
+
* mal-intencionado faz é dar um conselho ruim ao modelo, e o usuário vê o
|
|
17
|
+
* agente agindo, passo a passo, na tela.
|
|
18
|
+
*
|
|
19
|
+
* ── POR QUE NÃO É A MESMA COISA QUE OS ESPECIALISTAS ─────────────────────
|
|
20
|
+
* `especialistas.js` são sete briefings FIXOS, escolhidos pelo modelo quando
|
|
21
|
+
* ele delega uma tarefa a um subagente. Eles são do produto: ninguém instala,
|
|
22
|
+
* ninguém desliga, e a lista não cresce.
|
|
23
|
+
*
|
|
24
|
+
* Skill é do USUÁRIO. Ele instala, desliga, escreve a dele. É a resposta para
|
|
25
|
+
* "o Primo sempre esquece de rodar o lint antes de terminar" — que não se
|
|
26
|
+
* resolve pedindo de novo a cada conversa, e sim escrevendo uma vez.
|
|
27
|
+
*
|
|
28
|
+
* ── COMO ELA CHEGA AO MODELO ─────────────────────────────────────────────
|
|
29
|
+
* O corpo NÃO vai em todo pedido. São oito skills; mandar todas custaria
|
|
30
|
+
* milhares de tokens por mensagem num plano limitado por token, para que
|
|
31
|
+
* sete delas não tenham nada a ver com o pedido.
|
|
32
|
+
*
|
|
33
|
+
* O que decide é o `quando:` do cabeçalho — as palavras que fazem aquela
|
|
34
|
+
* skill valer a pena. Bateu, o corpo dela entra na frente da tarefa; não
|
|
35
|
+
* bateu, ela não custa nada. É o mesmo desenho dos especialistas, e pelo
|
|
36
|
+
* mesmo motivo: o teto de tokens é o recurso escasso deste produto.
|
|
37
|
+
*
|
|
38
|
+
* ── ONDE ELAS MORAM ──────────────────────────────────────────────────────
|
|
39
|
+
* Em `~/.primocode/skills/`, fora do pacote — pelo mesmo motivo do Studio:
|
|
40
|
+
* instalado global, a pasta do pacote costuma ser de sistema, e um `npm
|
|
41
|
+
* update` apagaria o que o usuário escreveu. As que vêm com o produto são
|
|
42
|
+
* copiadas na primeira vez; as dele ficam lá, a salvo de atualização.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
'use strict';
|
|
46
|
+
|
|
47
|
+
const fs = require('fs');
|
|
48
|
+
const os = require('os');
|
|
49
|
+
const path = require('path');
|
|
50
|
+
|
|
51
|
+
const ORIGEM = path.join(__dirname, '..', 'skills');
|
|
52
|
+
const CASA = path.join(os.homedir(), '.primocode', 'skills');
|
|
53
|
+
const ESTADO = path.join(CASA, 'estado.json');
|
|
54
|
+
|
|
55
|
+
/* Teto do que entra num pedido. Duas skills longas já são mais texto que a
|
|
56
|
+
maioria das tarefas — e cada caractere aqui é um caractere a menos de
|
|
57
|
+
arquivo lido. Estourou, entram as que couberam, das que casaram melhor. */
|
|
58
|
+
const MAX_CARACTERES = 6000;
|
|
59
|
+
|
|
60
|
+
// ── leitura ──────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* O cabeçalho e o corpo de um SKILL.md.
|
|
64
|
+
*
|
|
65
|
+
* O cabeçalho fica entre duas linhas de `---`, uma chave por linha. Formato
|
|
66
|
+
* escolhido por ser o que qualquer pessoa já viu, e por caber num parser de
|
|
67
|
+
* dez linhas — YAML de verdade seria uma dependência para ler quatro campos.
|
|
68
|
+
*/
|
|
69
|
+
function separar(texto) {
|
|
70
|
+
const bruto = String(texto || '');
|
|
71
|
+
const m = bruto.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
72
|
+
if (!m) return { cabecalho: {}, corpo: bruto.trim() };
|
|
73
|
+
const cabecalho = {};
|
|
74
|
+
for (const linha of m[1].split(/\r?\n/)) {
|
|
75
|
+
const par = linha.match(/^\s*([a-zA-Z_][\w-]*)\s*:\s*(.*)$/);
|
|
76
|
+
if (par) cabecalho[par[1].toLowerCase()] = par[2].trim().replace(/^["']|["']$/g, '');
|
|
77
|
+
}
|
|
78
|
+
return { cabecalho, corpo: m[2].trim() };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function lerEstado() {
|
|
82
|
+
try { return JSON.parse(fs.readFileSync(ESTADO, 'utf8')); } catch { return { desligadas: [] }; }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function salvarEstado(estado) {
|
|
86
|
+
try {
|
|
87
|
+
fs.mkdirSync(CASA, { recursive: true });
|
|
88
|
+
fs.writeFileSync(ESTADO, JSON.stringify(estado, null, 2), 'utf8');
|
|
89
|
+
return true;
|
|
90
|
+
} catch { return false; }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* As que vêm com o produto são copiadas na primeira vez. Copiar SEMPRE por
|
|
94
|
+
cima seria desfazer a edição de quem melhorou uma delas; copiar só o que
|
|
95
|
+
falta deixa o usuário dono do que ele mexeu. */
|
|
96
|
+
function prepararCasa() {
|
|
97
|
+
fs.mkdirSync(CASA, { recursive: true });
|
|
98
|
+
let nascidas = 0;
|
|
99
|
+
let origens = [];
|
|
100
|
+
try { origens = fs.readdirSync(ORIGEM, { withFileTypes: true }); } catch { return 0; }
|
|
101
|
+
for (const item of origens) {
|
|
102
|
+
if (!item.isDirectory()) continue;
|
|
103
|
+
const destino = path.join(CASA, item.name);
|
|
104
|
+
if (fs.existsSync(destino)) continue;
|
|
105
|
+
try {
|
|
106
|
+
fs.mkdirSync(destino, { recursive: true });
|
|
107
|
+
fs.copyFileSync(path.join(ORIGEM, item.name, 'SKILL.md'), path.join(destino, 'SKILL.md'));
|
|
108
|
+
nascidas++;
|
|
109
|
+
} catch { /* uma que falha não impede as outras */ }
|
|
110
|
+
}
|
|
111
|
+
return nascidas;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Uma skill pelo nome da pasta, ou null. */
|
|
115
|
+
function ler(nome) {
|
|
116
|
+
const arquivo = path.join(CASA, nome, 'SKILL.md');
|
|
117
|
+
let texto;
|
|
118
|
+
try { texto = fs.readFileSync(arquivo, 'utf8'); } catch { return null; }
|
|
119
|
+
const { cabecalho, corpo } = separar(texto);
|
|
120
|
+
const estado = lerEstado();
|
|
121
|
+
return {
|
|
122
|
+
nome,
|
|
123
|
+
titulo: cabecalho.nome || cabecalho.titulo || nome,
|
|
124
|
+
descricao: cabecalho.descricao || '',
|
|
125
|
+
quando: String(cabecalho.quando || '').split(',').map((s) => s.trim().toLowerCase()).filter(Boolean),
|
|
126
|
+
autor: cabecalho.autor || '',
|
|
127
|
+
corpo,
|
|
128
|
+
ligada: !estado.desligadas.includes(nome),
|
|
129
|
+
doProduto: fs.existsSync(path.join(ORIGEM, nome, 'SKILL.md')),
|
|
130
|
+
caminho: arquivo,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Todas as instaladas, em ordem alfabética. */
|
|
135
|
+
function listar() {
|
|
136
|
+
prepararCasa();
|
|
137
|
+
let nomes = [];
|
|
138
|
+
try {
|
|
139
|
+
nomes = fs.readdirSync(CASA, { withFileTypes: true })
|
|
140
|
+
.filter((d) => d.isDirectory())
|
|
141
|
+
.map((d) => d.name)
|
|
142
|
+
.sort();
|
|
143
|
+
} catch { return []; }
|
|
144
|
+
return nomes.map(ler).filter(Boolean);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── escrita ──────────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/* O nome vira pasta, então precisa ser um nome de pasta: sem barra, sem
|
|
150
|
+
`..`, sem acento. Um nome com `../` escreveria fora de ~/.primocode. */
|
|
151
|
+
function apelidar(bruto) {
|
|
152
|
+
return String(bruto || '')
|
|
153
|
+
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
154
|
+
.toLowerCase()
|
|
155
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
156
|
+
.replace(/^-+|-+$/g, '')
|
|
157
|
+
.slice(0, 48);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Cria (ou reescreve) uma skill.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} nome como a pessoa a chamou
|
|
164
|
+
* @param {string} corpo o prompt inteiro
|
|
165
|
+
* @param {object} extra { descricao, quando, autor }
|
|
166
|
+
*/
|
|
167
|
+
function criar(nome, corpo, extra = {}) {
|
|
168
|
+
const apelido = apelidar(nome);
|
|
169
|
+
if (!apelido) return { ok: false, error: 'Dê um nome à skill.' };
|
|
170
|
+
if (!String(corpo || '').trim()) return { ok: false, error: 'A skill precisa de um conteúdo — é ele que vai para o modelo.' };
|
|
171
|
+
|
|
172
|
+
const cabecalho = [
|
|
173
|
+
'---',
|
|
174
|
+
`nome: ${String(nome).trim()}`,
|
|
175
|
+
`descricao: ${String(extra.descricao || '').replace(/\n/g, ' ').trim()}`,
|
|
176
|
+
`quando: ${String(extra.quando || '').replace(/\n/g, ' ').trim()}`,
|
|
177
|
+
`autor: ${String(extra.autor || 'você').trim()}`,
|
|
178
|
+
'---',
|
|
179
|
+
'',
|
|
180
|
+
].join('\n');
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const pasta = path.join(CASA, apelido);
|
|
184
|
+
fs.mkdirSync(pasta, { recursive: true });
|
|
185
|
+
fs.writeFileSync(path.join(pasta, 'SKILL.md'), cabecalho + String(corpo).trim() + '\n', 'utf8');
|
|
186
|
+
// Skill recém-criada nasce LIGADA, mesmo que uma de mesmo nome tenha
|
|
187
|
+
// sido desligada antes: quem acabou de escrevê-la quer usá-la.
|
|
188
|
+
const estado = lerEstado();
|
|
189
|
+
estado.desligadas = estado.desligadas.filter((x) => x !== apelido);
|
|
190
|
+
salvarEstado(estado);
|
|
191
|
+
return { ok: true, nome: apelido, caminho: path.join(pasta, 'SKILL.md') };
|
|
192
|
+
} catch (e) {
|
|
193
|
+
return { ok: false, error: `Não deu para gravar: ${e.message}` };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function ligar(nome, ligada) {
|
|
198
|
+
const estado = lerEstado();
|
|
199
|
+
estado.desligadas = estado.desligadas.filter((x) => x !== nome);
|
|
200
|
+
if (!ligada) estado.desligadas.push(nome);
|
|
201
|
+
salvarEstado(estado);
|
|
202
|
+
return { ok: true, nome, ligada: !!ligada };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Apaga uma skill.
|
|
207
|
+
*
|
|
208
|
+
* As do produto voltam no próximo `listar()` — a cópia nasce de novo a partir
|
|
209
|
+
* do pacote. Por isso apagar uma delas não é apagar, é DESLIGAR: prometer
|
|
210
|
+
* remoção e ver a skill reaparecer amanhã é pior que dizer a verdade agora.
|
|
211
|
+
*/
|
|
212
|
+
function apagar(nome) {
|
|
213
|
+
const s = ler(nome);
|
|
214
|
+
if (!s) return { ok: false, error: `Não achei a skill "${nome}".` };
|
|
215
|
+
if (s.doProduto) {
|
|
216
|
+
ligar(nome, false);
|
|
217
|
+
return { ok: true, desligada: true,
|
|
218
|
+
aviso: 'Esta skill vem com o PrimoCode, então ela foi DESLIGADA em vez de apagada — '
|
|
219
|
+
+ 'apagada, ela voltaria na próxima atualização.' };
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
fs.rmSync(path.join(CASA, nome), { recursive: true, force: true });
|
|
223
|
+
return { ok: true, apagada: true };
|
|
224
|
+
} catch (e) { return { ok: false, error: e.message }; }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Instala de um arquivo ou de uma pasta que já está no disco.
|
|
229
|
+
*
|
|
230
|
+
* Não busca na rede de propósito: baixar e executar texto que virou prompt do
|
|
231
|
+
* agente é uma decisão que merece o caminho explícito de quem salvou o
|
|
232
|
+
* arquivo antes. O `/marketplace` traz as do catálogo; `/install-skill
|
|
233
|
+
* <caminho>` traz as que a pessoa escreveu ou recebeu.
|
|
234
|
+
*/
|
|
235
|
+
function instalarDe(caminho) {
|
|
236
|
+
const alvo = String(caminho || '').replace(/^~(?=$|[/\\])/, os.homedir());
|
|
237
|
+
let arquivo = alvo;
|
|
238
|
+
try {
|
|
239
|
+
if (fs.statSync(alvo).isDirectory()) arquivo = path.join(alvo, 'SKILL.md');
|
|
240
|
+
} catch { return { ok: false, error: `Não achei ${caminho}.` }; }
|
|
241
|
+
|
|
242
|
+
let texto;
|
|
243
|
+
try { texto = fs.readFileSync(arquivo, 'utf8'); }
|
|
244
|
+
catch { return { ok: false, error: `Não consegui ler ${arquivo}. Uma skill é um arquivo SKILL.md.` }; }
|
|
245
|
+
|
|
246
|
+
const { cabecalho, corpo } = separar(texto);
|
|
247
|
+
const nome = cabecalho.nome || path.basename(path.dirname(arquivo));
|
|
248
|
+
if (!corpo.trim()) return { ok: false, error: 'O arquivo não tem conteúdo depois do cabeçalho.' };
|
|
249
|
+
return criar(nome, corpo, cabecalho);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── o que entra no pedido ────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
const semAcento = (v) => String(v || '').toLowerCase()
|
|
255
|
+
.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Quantas palavras do `quando:` aparecem no texto do pedido.
|
|
259
|
+
*
|
|
260
|
+
* ── PALAVRA INTEIRA, NUNCA PEDACO ────────────────────────────────────────
|
|
261
|
+
* O primeiro jeito era `texto.includes(palavra)`, e estava errado de um jeito
|
|
262
|
+
* que so apareceu no ensaio: "faz uma landing page PRO meu curso" puxou a
|
|
263
|
+
* skill de Git, porque o gatilho `pr` mora dentro de "pro". Gatilhos curtos
|
|
264
|
+
* — pr, api, seo, css — casam com metade da lingua portuguesa se a borda nao
|
|
265
|
+
* for exigida.
|
|
266
|
+
*
|
|
267
|
+
* Uma skill errada nao e ruido inofensivo: ela ocupa o teto de tokens do
|
|
268
|
+
* pedido e da ao modelo instrucoes sobre um assunto que ninguem pediu.
|
|
269
|
+
*/
|
|
270
|
+
function pontuar(skill, texto) {
|
|
271
|
+
const t = semAcento(texto);
|
|
272
|
+
let pontos = 0;
|
|
273
|
+
for (const palavra of skill.quando) {
|
|
274
|
+
const p = semAcento(palavra).trim();
|
|
275
|
+
if (!p) continue;
|
|
276
|
+
const escapado = p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
277
|
+
// A borda e escrita a mao em vez de `\b`: o gatilho pode comecar ou
|
|
278
|
+
// terminar em simbolo ("e-mail"), e ali o `\b` nao vale.
|
|
279
|
+
const re = new RegExp('(^|[^a-z0-9])' + escapado + '($|[^a-z0-9])');
|
|
280
|
+
if (re.test(t)) pontos++;
|
|
281
|
+
}
|
|
282
|
+
return pontos;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* As skills que valem a pena para ESTE pedido, das mais ao menos relevantes.
|
|
287
|
+
*
|
|
288
|
+
* Só as ligadas, só as que casaram com alguma palavra, e só até o teto de
|
|
289
|
+
* caracteres. Skill que não casou não entra: mandar as oito em todo pedido
|
|
290
|
+
* custaria milhares de tokens para sete delas não terem nada a ver com o que
|
|
291
|
+
* foi pedido.
|
|
292
|
+
*/
|
|
293
|
+
function paraOPedido(texto, { teto = MAX_CARACTERES, todas = null } = {}) {
|
|
294
|
+
const lista = (todas || listar()).filter((s) => s.ligada && s.corpo);
|
|
295
|
+
const casadas = lista
|
|
296
|
+
.map((s) => ({ skill: s, pontos: pontuar(s, texto) }))
|
|
297
|
+
.filter((x) => x.pontos > 0)
|
|
298
|
+
.sort((a, b) => b.pontos - a.pontos);
|
|
299
|
+
|
|
300
|
+
const escolhidas = [];
|
|
301
|
+
let tamanho = 0;
|
|
302
|
+
for (const { skill } of casadas) {
|
|
303
|
+
if (tamanho + skill.corpo.length > teto) continue;
|
|
304
|
+
escolhidas.push(skill);
|
|
305
|
+
tamanho += skill.corpo.length;
|
|
306
|
+
}
|
|
307
|
+
return escolhidas;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* O texto que vai na frente da tarefa.
|
|
312
|
+
*
|
|
313
|
+
* Vem antes do pedido, e não depois, pelo mesmo motivo do briefing dos
|
|
314
|
+
* especialistas: é contexto para ler o pedido, não um adendo a ele.
|
|
315
|
+
*/
|
|
316
|
+
function montar(texto, op = {}) {
|
|
317
|
+
const escolhidas = paraOPedido(texto, op);
|
|
318
|
+
if (!escolhidas.length) return { texto, skills: [] };
|
|
319
|
+
const blocos = escolhidas.map((s) => `### ${s.titulo}\n${s.corpo}`);
|
|
320
|
+
const cabeca = 'O QUE VOCÊ SABE SOBRE ESTE TIPO DE TAREFA (siga ao trabalhar):\n\n'
|
|
321
|
+
+ blocos.join('\n\n') + '\n\n---\n\nPEDIDO:\n';
|
|
322
|
+
return { texto: cabeca + texto, skills: escolhidas };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = {
|
|
326
|
+
CASA, ORIGEM, MAX_CARACTERES,
|
|
327
|
+
separar, listar, ler, criar, ligar, apagar, instalarDe, apelidar,
|
|
328
|
+
paraOPedido, montar, pontuar, prepararCasa,
|
|
329
|
+
};
|
package/lib/tools.js
CHANGED
|
@@ -14,8 +14,7 @@ const { exec, execFile } = require('child_process');
|
|
|
14
14
|
const browser = require('./browser');
|
|
15
15
|
const desktop = require('./desktop');
|
|
16
16
|
const referencia = require('./referencia');
|
|
17
|
-
const
|
|
18
|
-
const acervo = require('./acervo');
|
|
17
|
+
const estudio = require('./estudio');
|
|
19
18
|
const cfg = require('./config');
|
|
20
19
|
|
|
21
20
|
const IGNORAR = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.cache', 'vendor']);
|
|
@@ -171,11 +170,6 @@ async function runShell(command, timeout = 60000, cwd) {
|
|
|
171
170
|
// execFile com array de argumentos nunca passa por shell nenhum: "target" vira
|
|
172
171
|
// UM argumento, não texto que um shell reinterpreta — sem isso, um valor tipo
|
|
173
172
|
// `https://x/"; touch /tmp/pwned; echo "` injetava comando (achado de segurança).
|
|
174
|
-
// Quando o auto-abrir do Studio abriu pela última vez — o freio das mil abas.
|
|
175
|
-
let ultimaAberturaStudio = 0;
|
|
176
|
-
// Uma rodada de melhoria por tarefa (ver o aviso em studio_criar).
|
|
177
|
-
let melhorouStudio = false;
|
|
178
|
-
|
|
179
173
|
function openInBrowser(target) {
|
|
180
174
|
const p = os.platform();
|
|
181
175
|
const [cmd, args, opener] = p === 'darwin' ? ['open', [target], 'open']
|
|
@@ -270,6 +264,13 @@ function editFile({ path: rawPath, old_string, new_string, replace_all }, projec
|
|
|
270
264
|
substituicoes: replace_all ? ocorrencias : 1,
|
|
271
265
|
linhasAntes: original.split('\n').length,
|
|
272
266
|
linhasDepois: atualizado.split('\n').length,
|
|
267
|
+
// As duas versões, para a TELA desenhar o diff. O `_` na frente marca
|
|
268
|
+
// o que é só nosso: o act.js apaga estas chaves antes de a conversa
|
|
269
|
+
// seguir, porque mandar o arquivo de volta ao modelo custaria
|
|
270
|
+
// centenas de tokens para ensinar a ele o texto que ele acabou de
|
|
271
|
+
// escrever.
|
|
272
|
+
_antes: original,
|
|
273
|
+
_depois: atualizado,
|
|
273
274
|
};
|
|
274
275
|
}
|
|
275
276
|
|
|
@@ -304,6 +305,7 @@ function multiEdit({ path: rawPath, edits }, projectDir) {
|
|
|
304
305
|
if (!Array.isArray(edits) || !edits.length) return { ok: false, error: 'edits (lista) é obrigatório' };
|
|
305
306
|
|
|
306
307
|
let conteudo = fs.readFileSync(filePath, 'utf-8');
|
|
308
|
+
const original = conteudo;
|
|
307
309
|
const linhasAntes = conteudo.split('\n').length;
|
|
308
310
|
let substituicoes = 0;
|
|
309
311
|
|
|
@@ -329,6 +331,8 @@ function multiEdit({ path: rawPath, edits }, projectDir) {
|
|
|
329
331
|
substituicoes,
|
|
330
332
|
linhasAntes,
|
|
331
333
|
linhasDepois: conteudo.split('\n').length,
|
|
334
|
+
_antes: original,
|
|
335
|
+
_depois: conteudo,
|
|
332
336
|
};
|
|
333
337
|
}
|
|
334
338
|
|
|
@@ -401,9 +405,20 @@ async function despachar(name, args, ctx) {
|
|
|
401
405
|
const filePath = resolveInProject(args.path, projectDir);
|
|
402
406
|
const content = args.content || '';
|
|
403
407
|
const created = !fs.existsSync(filePath);
|
|
408
|
+
// Reescrever um arquivo que já existe é a operação mais
|
|
409
|
+
// perigosa do agente: o conteúdo antigo some inteiro. Ler o
|
|
410
|
+
// que estava lá ANTES de escrever é o que permite mostrar na
|
|
411
|
+
// tela o que foi perdido.
|
|
412
|
+
let antes = '';
|
|
413
|
+
if (!created) { try { antes = fs.readFileSync(filePath, 'utf-8'); } catch { antes = ''; } }
|
|
404
414
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
405
415
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
406
|
-
return {
|
|
416
|
+
return {
|
|
417
|
+
ok: true, created,
|
|
418
|
+
path: path.relative(projectDir, filePath).replace(/\\/g, '/'),
|
|
419
|
+
bytes: content.length,
|
|
420
|
+
...(created ? {} : { _antes: antes, _depois: content }),
|
|
421
|
+
};
|
|
407
422
|
}
|
|
408
423
|
case 'edit_file':
|
|
409
424
|
return editFile(args, projectDir);
|
|
@@ -441,16 +456,6 @@ async function despachar(name, args, ctx) {
|
|
|
441
456
|
paginas: Math.max(1, Math.min(5, Number(args.paginas) || 3)),
|
|
442
457
|
});
|
|
443
458
|
|
|
444
|
-
// Música e efeito sonoro da internet, com licença e crédito. O
|
|
445
|
-
// arquivo vem para a pasta do projeto e sobe para o estúdio;
|
|
446
|
-
// volta a URL "/midia/…" pronta para trilha.src ou som.arquivo.
|
|
447
|
-
case 'buscar_audio':
|
|
448
|
-
return require('./audio-livre.js').buscarAudio(args, { projectDir, studio });
|
|
449
|
-
|
|
450
|
-
// A logo e a identidade visual da empresa, lidas do site dela.
|
|
451
|
-
case 'marca_da_empresa':
|
|
452
|
-
return require('./marca.js').marcaDaEmpresa(args, { projectDir, studio });
|
|
453
|
-
|
|
454
459
|
// memory-geral.md fica FORA da pasta do projeto — write_file/edit_file
|
|
455
460
|
// não alcançam por design (resolveInProject bloqueia ../ de propósito).
|
|
456
461
|
// Esta é a única porta para lá, sem reabrir a sandbox.
|
|
@@ -514,171 +519,36 @@ async function despachar(name, args, ctx) {
|
|
|
514
519
|
return desktop.scroll(args.dy != null ? args.dy : 300);
|
|
515
520
|
case 'desktop_size':
|
|
516
521
|
return desktop.getScreenSize();
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
case 'studio_criar': {
|
|
523
|
-
const r = await studio.criar({ ferramenta: args.ferramenta, titulo: args.titulo, doc: args.doc });
|
|
524
|
-
// Criou? ABRE NO NAVEGADOR, sozinho. O dono: "quando ele faz um
|
|
525
|
-
// vídeo, uma apresentação, abre automático — a pessoa não vai
|
|
526
|
-
// saber abrir". E cada "abre pra mim" digitado depois custava
|
|
527
|
-
// uma tarefa inteira de tokens para o modelo redescobrir o
|
|
528
|
-
// link. Abrir é determinístico: não se gasta modelo com isso.
|
|
529
|
-
// Fica anotado na memória geral: é assim que "lembra do
|
|
530
|
-
// vídeo tal" funciona na conversa seguinte. O id é o que
|
|
531
|
-
// permite reabrir ou ajustar sem recriar.
|
|
532
|
-
if (r && r.ok !== false && r.id) {
|
|
533
|
-
cfg.lembrarCriacao({ ferramenta: r.ferramenta || args.ferramenta,
|
|
534
|
-
titulo: r.titulo || args.titulo, id: r.id });
|
|
535
|
-
}
|
|
536
|
-
if (r && r.ok !== false && r.ver) {
|
|
537
|
-
// UMA aba por tarefa. O modelo às vezes cria três versões
|
|
538
|
-
// seguidas, e abrir todas enchia o navegador do usuário de
|
|
539
|
-
// abas — "abre 1000 versões". A primeira abre; as outras
|
|
540
|
-
// são avisadas a ATUALIZAR a que já existe.
|
|
541
|
-
const agora = Date.now();
|
|
542
|
-
if (agora - ultimaAberturaStudio > 45000) {
|
|
543
|
-
ultimaAberturaStudio = agora;
|
|
544
|
-
try { openInBrowser(r.ver); } catch {}
|
|
545
|
-
r.abertoNoNavegador = true;
|
|
546
|
-
// O estúdio revisa a peça e devolve o que ficou
|
|
547
|
-
// monótono (mesmo fundo em tudo, transição repetida,
|
|
548
|
-
// sem trilha). Sem este ramo o aviso mandaria "chame
|
|
549
|
-
// finish, não ajuste" — e a revisão morreria ali, que
|
|
550
|
-
// é justamente o "fica tudo genérico".
|
|
551
|
-
//
|
|
552
|
-
// UMA rodada de conserto, e acabou: mais que isso
|
|
553
|
-
// vira o agente polindo sozinho enquanto o usuário
|
|
554
|
-
// espera.
|
|
555
|
-
if (Array.isArray(r.podeMelhorar) && r.podeMelhorar.length && !melhorouStudio) {
|
|
556
|
-
melhorouStudio = true;
|
|
557
|
-
r.aviso = 'JÁ ABERTO no navegador. Antes de finalizar, faça UMA correção: '
|
|
558
|
-
+ `use studio_atualizar no id "${r.id}" mandando SÓ as chaves que mudam, `
|
|
559
|
-
+ 'para resolver os pontos de "podeMelhorar". Depois chame finish. '
|
|
560
|
-
+ 'Não crie outro arquivo e não repita esta correção.';
|
|
561
|
-
} else {
|
|
562
|
-
r.aviso = 'JÁ ABERTO no navegador do usuário. Chame finish AGORA dizendo o que foi criado — não abra de novo, não leia de volta, não ajuste sem pedido.';
|
|
563
|
-
}
|
|
564
|
-
} else {
|
|
565
|
-
r.aviso = `Você JÁ criou uma versão nesta tarefa, e ela está aberta no navegador. NÃO crie outra: `
|
|
566
|
-
+ `para mudar algo, use studio_atualizar com o id "${r.id}". Chame finish AGORA.`;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
return r;
|
|
570
|
-
}
|
|
571
|
-
case 'studio_ler':
|
|
572
|
-
return studio.ler(args.id);
|
|
573
|
-
case 'studio_atualizar':
|
|
574
|
-
return studio.atualizar({ id: args.id, titulo: args.titulo, doc: args.doc });
|
|
575
|
-
case 'studio_versoes':
|
|
576
|
-
return studio.versoesDe({ id: args.id, restaurar: args.restaurar });
|
|
577
|
-
case 'studio_listar':
|
|
578
|
-
return studio.listar();
|
|
579
|
-
case 'studio_midia':
|
|
580
|
-
// Sem arquivo, lista o que já foi enviado; com arquivo, envia.
|
|
581
|
-
return args.arquivo
|
|
582
|
-
? studio.midiaEnviar({ arquivo: resolveInProject(args.arquivo, projectDir), nome: args.nome })
|
|
583
|
-
: studio.midiaListar();
|
|
584
|
-
/* ── MÍDIA DE VERDADE, VINDA DA INTERNET ──────────────────
|
|
585
|
-
*
|
|
586
|
-
* Buscar, baixar, recortar o fundo e já deixar na galeria do
|
|
587
|
-
* estúdio. O caminho inteiro é uma tool só do ponto de vista do
|
|
588
|
-
* agente (procurar → baixar), e o `paraEstudio` é o que evita a
|
|
589
|
-
* volta clássica: baixar num canto do disco e depois descobrir
|
|
590
|
-
* que a cena precisa de uma URL "/midia/...".
|
|
522
|
+
/* ── O ESTÚDIO ─────────────────────────────────────────────────
|
|
523
|
+
* Sete ferramentas, e nenhuma delas fala com servidor nenhum: o
|
|
524
|
+
* Studio antigo subia um Python em localhost:7777 porque tinha
|
|
525
|
+
* uma interface web para servir. A interface morreu — a IA faz
|
|
526
|
+
* tudo e abre o vídeo pronto — e o servidor morreu com ela.
|
|
591
527
|
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
|
|
595
|
-
case '
|
|
596
|
-
return
|
|
597
|
-
|
|
598
|
-
|
|
528
|
+
* Cada peça é uma PASTA, cada render é o Remotion. Ver
|
|
529
|
+
* lib/estudio/ para o porquê de cada peça.
|
|
530
|
+
*/
|
|
531
|
+
case 'estudio_projeto':
|
|
532
|
+
return estudio.ferramentaProjeto(args);
|
|
533
|
+
case 'estudio_plano':
|
|
534
|
+
return estudio.ferramentaPlano(args);
|
|
535
|
+
case 'estudio_midia':
|
|
536
|
+
return estudio.ferramentaMidia(args);
|
|
537
|
+
case 'estudio_narrar':
|
|
538
|
+
return estudio.ferramentaNarrar(args);
|
|
539
|
+
case 'estudio_video':
|
|
540
|
+
// A transcrição precisa do servidor (a chave é dele), então o
|
|
541
|
+
// contexto do pedido atravessa até aqui.
|
|
542
|
+
return estudio.ferramentaVideo(args, ctx);
|
|
543
|
+
case 'estudio_render':
|
|
544
|
+
return estudio.ferramentaRender(args, {
|
|
545
|
+
...ctx,
|
|
546
|
+
// O vídeo pronto abre para a pessoa assistir. É a única
|
|
547
|
+
// coisa que abre: foi o vídeo que ela pediu.
|
|
548
|
+
abrir: (arquivo) => { try { openInBrowser(arquivo); } catch {} },
|
|
599
549
|
});
|
|
600
|
-
case '
|
|
601
|
-
|
|
602
|
-
autor: args.autor || null, licenca: args.licenca || null,
|
|
603
|
-
fonte: args.fonte || null, pagina: args.pagina || null,
|
|
604
|
-
} });
|
|
605
|
-
if (!r.ok) return r;
|
|
606
|
-
let arquivo = r.arquivo;
|
|
607
|
-
if (args.semFundo) {
|
|
608
|
-
const corte = await acervo.tirarFundo(arquivo);
|
|
609
|
-
if (corte.ok) { arquivo = corte.arquivo; r.recorte = corte.metodo; r.avisoRecorte = corte.aviso; }
|
|
610
|
-
else r.avisoRecorte = corte.error;
|
|
611
|
-
}
|
|
612
|
-
r.arquivo = arquivo;
|
|
613
|
-
if (args.paraEstudio !== false) {
|
|
614
|
-
const g = await studio.midiaEnviar({ arquivo, nome: args.nome });
|
|
615
|
-
// O "/midia/..." é o que vai em `src` e em `fundo.imagem`.
|
|
616
|
-
if (g && g.ok !== false) r.src = g.url || g.src || null;
|
|
617
|
-
else r.avisoGaleria = (g && g.error) || 'não deu para mandar para a galeria';
|
|
618
|
-
}
|
|
619
|
-
return r;
|
|
620
|
-
}
|
|
621
|
-
case 'midia_sem_fundo':
|
|
622
|
-
return acervo.tirarFundo(resolveInProject(args.arquivo, projectDir), { destino: args.destino });
|
|
623
|
-
case 'midia_creditos':
|
|
624
|
-
return { ok: true, creditos: acervo.creditos() };
|
|
625
|
-
|
|
626
|
-
case 'studio_analisar': {
|
|
627
|
-
// Ouvir o vídeo para achar as pausas. A conta acontece num
|
|
628
|
-
// navegador (é quem sabe decodificar MP4 sem instalar nada) —
|
|
629
|
-
// e ESCONDIDO: a conta não é da pessoa, e uma aba piscando no
|
|
630
|
-
// navegador dela é encanamento aparecendo. Sem Chrome nem
|
|
631
|
-
// Chromium nesta máquina, cai no navegador dela, como antes.
|
|
632
|
-
const visivel = process.env.PRIMOCODE_ANALISE_VISIVEL === '1';
|
|
633
|
-
const abrir = visivel ? openInBrowser : async (url) => {
|
|
634
|
-
try { return await browser.abrirEscondido(url); }
|
|
635
|
-
catch { openInBrowser(url); return null; }
|
|
636
|
-
};
|
|
637
|
-
return studio.analisarAudio({
|
|
638
|
-
arquivo: args.arquivo, pausa: args.pausa, manter: args.manter,
|
|
639
|
-
repetida: args.repetida, abrir, escondido: visivel ? null : browser.rodarEscondido,
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
case 'studio_exportar':
|
|
643
|
-
// O mp4 sem clique: a página do player roda escondida e gera
|
|
644
|
-
// pelo mesmo caminho do botão. Ver lib/studio.js.
|
|
645
|
-
// Com o Remotion mesclado: o vídeo quadro a quadro dele, o
|
|
646
|
-
// áudio do Studio. PRIMOCODE_SEM_REMOTION=1 desliga.
|
|
647
|
-
return studio.exportarVideo({
|
|
648
|
-
id: args.id, destino: args.destino, fps: args.fps,
|
|
649
|
-
escondido: browser.rodarEscondido, pasta: projectDir || process.cwd(),
|
|
650
|
-
remotion: process.env.PRIMOCODE_SEM_REMOTION !== '1',
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
/* A MESMA peça, virando projeto Remotion.
|
|
654
|
-
*
|
|
655
|
-
* O Studio continua sendo o caminho normal: exporta em segundos,
|
|
656
|
-
* sem instalar nada. Isto é para quando o usuário pede Remotion
|
|
657
|
-
* pelo nome, ou quando a peça precisa do que só ele dá — render
|
|
658
|
-
* determinístico, vídeo longo, fps e codec escolhidos, ou o
|
|
659
|
-
* projeto em React para a pessoa mexer.
|
|
660
|
-
*
|
|
661
|
-
* Parte do documento que já existe: nada de um segundo
|
|
662
|
-
* vocabulário para o agente aprender. */
|
|
663
|
-
case 'remotion_projeto': {
|
|
664
|
-
const remotion = require('./remotion.js');
|
|
665
|
-
const peca = await studio.ler(args.id);
|
|
666
|
-
if (!peca || peca.ok === false) {
|
|
667
|
-
return peca || { ok: false, error: `não achei a peça ${args.id}` };
|
|
668
|
-
}
|
|
669
|
-
const arq = peca.arquivo || peca;
|
|
670
|
-
const base = projectDir || cfg.ROOT_DIR;
|
|
671
|
-
const nome = cfg.slugify(args.pasta || arq.titulo || 'video') + '-remotion';
|
|
672
|
-
const destino = path.join(base, nome);
|
|
673
|
-
const r = remotion.montar({
|
|
674
|
-
destino, titulo: arq.titulo, doc: arq.doc, fps: args.fps,
|
|
675
|
-
});
|
|
676
|
-
if (!r.ok || !args.renderizar) return r;
|
|
677
|
-
// Só renderiza quando pedido: o `npm install` demora minutos e
|
|
678
|
-
// é escolha do usuário, não efeito colateral de criar a pasta.
|
|
679
|
-
const feito = await remotion.renderizar({ pasta: destino });
|
|
680
|
-
return Object.assign(r, { render: feito });
|
|
681
|
-
}
|
|
550
|
+
case 'estudio_site':
|
|
551
|
+
return estudio.ferramentaSite(args);
|
|
682
552
|
|
|
683
553
|
case 'desktop_double_click':
|
|
684
554
|
return desktop.doubleClick(args.x, args.y);
|
package/lib/ui.js
CHANGED
|
@@ -160,9 +160,10 @@ const TIPS = [
|
|
|
160
160
|
];
|
|
161
161
|
|
|
162
162
|
const NEWS = [
|
|
163
|
-
'Vídeo com DIRETOR: roteiro, narração
|
|
164
|
-
'Ele busca e baixa imagem, logo e música — com a licença junto',
|
|
165
|
-
'
|
|
163
|
+
'Vídeo com DIRETOR: roteiro, narração e voz decididos antes de produzir',
|
|
164
|
+
'Ele busca e baixa imagem, logo e música — com a licença junto do arquivo',
|
|
165
|
+
'/voice abre o Primo Code Voice · /marketplace instala skills',
|
|
166
|
+
'Do "high" para cima ele PESQUISA referências antes de escrever',
|
|
166
167
|
'Esc Esc a qualquer momento para o agente no meio do trabalho',
|
|
167
168
|
];
|
|
168
169
|
|
|
@@ -313,6 +314,9 @@ function actionHeader(name, args) {
|
|
|
313
314
|
const label = ROTULOS[name] || name;
|
|
314
315
|
let argStr = args ? (args.consulta || args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.app || args.window || args.name || args.ferramenta || args.titulo || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || args.arquivo || args.id || JSON.stringify(args).slice(0,60)) : '';
|
|
315
316
|
argStr = String(argStr || '').replace(/\s+/g, ' ').trim();
|
|
317
|
+
// Uma tool sem argumento nenhum caía no JSON.stringify e desenhava "{}"
|
|
318
|
+
// ao lado do nome — ruído que não diz nada.
|
|
319
|
+
if (argStr === '{}' || argStr === 'null' || argStr === 'undefined') argStr = '';
|
|
316
320
|
if (argStr.length > 96) argStr = argStr.slice(0, 95) + '…';
|
|
317
321
|
return ` ${c.brand('●')} ${c.bold(label)}${argStr ? ' '+c.muted(argStr) : ''}`;
|
|
318
322
|
}
|