primocode 8.1.1 → 8.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/primocode.js +3 -1
- package/lib/act.js +57 -9
- package/lib/catalogo.js +2 -0
- package/lib/claude-engine.js +57 -10
- package/lib/modo.js +70 -12
- package/lib/studio.js +35 -0
- package/lib/tools.js +5 -0
- package/package.json +2 -2
- package/studio/ferramentas/corte.md +33 -0
- package/studio/web/tool.html +4 -0
- package/studio/web/view.html +2 -0
package/bin/primocode.js
CHANGED
|
@@ -516,7 +516,9 @@ async function sendPrompt(userText) {
|
|
|
516
516
|
// tokens). No auto, quem não vai usar ferramenta não paga por ela.
|
|
517
517
|
let modo = state.mode;
|
|
518
518
|
if (modo === 'auto') {
|
|
519
|
-
|
|
519
|
+
// O modo do pedido ANTERIOR entra na decisão: no meio de uma tarefa,
|
|
520
|
+
// "refaz" e "agora põe música" são continuação, não papo novo.
|
|
521
|
+
const escolha = escolherModo(userText, { ultimoModo: state.modoEfetivo });
|
|
520
522
|
modo = escolha.modo;
|
|
521
523
|
state.modoEfetivo = modo;
|
|
522
524
|
const rotulo = modo === 'primocode' ? c.warn('Primo Code') : c.brandDim('Chat');
|
package/lib/act.js
CHANGED
|
@@ -160,6 +160,37 @@ function summarizeForModel(result) {
|
|
|
160
160
|
return json.length > 6000 ? json.slice(0, 6000) + '…(truncado)' : json;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// Quantos resultados de tool ficam INTEIROS na conversa. Os mais novos são o
|
|
164
|
+
// que o modelo precisa para decidir o próximo passo; os velhos viram peso.
|
|
165
|
+
const RESULTADOS_INTEIROS = 3;
|
|
166
|
+
const RESTO_DO_RESULTADO = 300;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Encolhe os resultados de tool ANTIGOS antes de mandar a conversa.
|
|
170
|
+
*
|
|
171
|
+
* Sem isto, cada página lida e cada pesquisa ficava inteira no histórico para
|
|
172
|
+
* sempre. Medido num pedido real ("pesquise sobre a empresa X e faça um
|
|
173
|
+
* vídeo"): cinco pesquisas e seis leituras de página levaram a conversa a
|
|
174
|
+
* 27.454 tokens contra um teto de 6.000 por minuto do plano grátis — a tarefa
|
|
175
|
+
* morreu no meio, depois de todo o trabalho de pesquisa já feito.
|
|
176
|
+
*
|
|
177
|
+
* Nenhuma mensagem é REMOVIDA: a API exige que cada role:'tool' continue
|
|
178
|
+
* pareada com o tool_call que a pediu. Só o texto encolhe.
|
|
179
|
+
*/
|
|
180
|
+
function compactar(messages) {
|
|
181
|
+
const indices = [];
|
|
182
|
+
for (let i = 0; i < messages.length; i++) if (messages[i].role === 'tool') indices.push(i);
|
|
183
|
+
if (indices.length <= RESULTADOS_INTEIROS) return messages;
|
|
184
|
+
|
|
185
|
+
const encolher = new Set(indices.slice(0, -RESULTADOS_INTEIROS));
|
|
186
|
+
return messages.map((m, i) => {
|
|
187
|
+
if (!encolher.has(i)) return m;
|
|
188
|
+
const texto = String(m.content || '');
|
|
189
|
+
if (texto.length <= RESTO_DO_RESULTADO) return m;
|
|
190
|
+
return { ...m, content: texto.slice(0, RESTO_DO_RESULTADO) + `…(resultado antigo encurtado; eram ${texto.length} caracteres)` };
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
163
194
|
function buildMemoryContext(projectDir, projectName) {
|
|
164
195
|
const parts = [];
|
|
165
196
|
const geral = cfg.readGeneralMemory();
|
|
@@ -210,7 +241,7 @@ async function runAct(userPrompt, opts) {
|
|
|
210
241
|
let streaming = false;
|
|
211
242
|
|
|
212
243
|
try {
|
|
213
|
-
await motor.requestAct(server, { messages, model, effort, token, projectDir, projectName, subagent }, (evt) => {
|
|
244
|
+
await motor.requestAct(server, { messages: compactar(messages), model, effort, token, projectDir, projectName, subagent }, (evt) => {
|
|
214
245
|
if (evt.erro) { streamError = evt.erro; return; }
|
|
215
246
|
if (evt.tool_call) { toolCalls.push(evt.tool_call); return; }
|
|
216
247
|
if (evt.delta) {
|
|
@@ -271,7 +302,7 @@ async function runAct(userPrompt, opts) {
|
|
|
271
302
|
messages.push({
|
|
272
303
|
role: 'tool',
|
|
273
304
|
tool_call_id: tc.id,
|
|
274
|
-
content: `NÃO terminou. Estas tools falharam e você não corrigiu: ${falhasPendentes.join('; ')}. `
|
|
305
|
+
content: `NÃO terminou. Estas tools falharam e você não corrigiu: ${falhasPendentes.map((f) => f.texto).join('; ')}. `
|
|
275
306
|
+ 'Conserte agora (leia o arquivo antes de editar) ou, se for impossível, diga claramente ao usuário o que falhou.',
|
|
276
307
|
});
|
|
277
308
|
continue;
|
|
@@ -306,12 +337,29 @@ async function runAct(userPrompt, opts) {
|
|
|
306
337
|
// a lista inteira e o agente conseguia dizer "concluído" com uma
|
|
307
338
|
// falha real ainda pendente.
|
|
308
339
|
if (result && result.ok === false) {
|
|
309
|
-
falhasPendentes.push(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
340
|
+
falhasPendentes.push({
|
|
341
|
+
tool: tc.name,
|
|
342
|
+
alvo: tc.arguments?.path || '',
|
|
343
|
+
texto: `${tc.name}(${tc.arguments?.path || ''}): ${result.error}`,
|
|
344
|
+
});
|
|
345
|
+
} else {
|
|
346
|
+
// Deu certo: some com o que ESTA tool tinha deixado pendente.
|
|
347
|
+
//
|
|
348
|
+
// Antes só write_file/edit_file limpavam, e por caminho. Uma
|
|
349
|
+
// falha já superada continuava bloqueando o finish para sempre:
|
|
350
|
+
// medido num pedido real, o browser_read errou o seletor
|
|
351
|
+
// ("main"), acertou na tentativa seguinte lendo 5.240 chars — e
|
|
352
|
+
// mesmo assim a tarefa morreu com "Parei sem concluir. A última
|
|
353
|
+
// falha foi: browser_read()". Acertar depois de errar é
|
|
354
|
+
// exatamente o que esta guarda quer premiar, não punir.
|
|
355
|
+
const alvo = tc.arguments?.path;
|
|
356
|
+
const sobrou = falhasPendentes.filter((f) => {
|
|
357
|
+
if (f.tool === tc.name) return false; // a mesma tool funcionou
|
|
358
|
+
if (alvo && f.alvo === alvo) return false; // o mesmo arquivo foi resolvido
|
|
359
|
+
return true;
|
|
360
|
+
});
|
|
313
361
|
falhasPendentes.length = 0;
|
|
314
|
-
falhasPendentes.push(...
|
|
362
|
+
falhasPendentes.push(...sobrou);
|
|
315
363
|
}
|
|
316
364
|
|
|
317
365
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: summarizeForModel(result) });
|
|
@@ -335,7 +383,7 @@ async function runAct(userPrompt, opts) {
|
|
|
335
383
|
// depois de a primeira tentativa de seletor falhar.
|
|
336
384
|
if (!finalText) {
|
|
337
385
|
finalText = falhasPendentes.length
|
|
338
|
-
? `Parei sem concluir. A última falha foi: ${falhasPendentes[falhasPendentes.length - 1]}`
|
|
386
|
+
? `Parei sem concluir. A última falha foi: ${falhasPendentes[falhasPendentes.length - 1].texto}`
|
|
339
387
|
: 'Parei sem concluir e sem explicação do modelo. Tente pedir de novo, em partes menores.';
|
|
340
388
|
if (!subagent) console.log('\n' + c.warn(finalText));
|
|
341
389
|
}
|
|
@@ -382,4 +430,4 @@ async function runSubagent(args, ctx) {
|
|
|
382
430
|
return resumo;
|
|
383
431
|
}
|
|
384
432
|
|
|
385
|
-
module.exports = { runAct, interromper };
|
|
433
|
+
module.exports = { runAct, interromper, compactar };
|
package/lib/catalogo.js
CHANGED
|
@@ -85,6 +85,8 @@ const GRUPOS = [
|
|
|
85
85
|
{ tool: 'studio_atualizar', escreve: false, faz: 'ajusta o que já existe, sem refazer do zero',
|
|
86
86
|
args: 'id. titulo (opcional). doc (opcional).' },
|
|
87
87
|
{ tool: 'studio_listar', escreve: false, faz: 'lista o que já existe no estúdio', args: null },
|
|
88
|
+
{ tool: 'studio_midia', escreve: false, faz: 'manda imagem, música ou vídeo seu para usar nas peças',
|
|
89
|
+
args: 'arquivo (opcional): caminho no projeto — sem ele, lista a galeria. nome (opcional).' },
|
|
88
90
|
],
|
|
89
91
|
},
|
|
90
92
|
{
|
package/lib/claude-engine.js
CHANGED
|
@@ -24,16 +24,55 @@ const MARCA_FIM = '<<<FIM>>>';
|
|
|
24
24
|
*/
|
|
25
25
|
function extrairDecisao(texto) {
|
|
26
26
|
const t = String(texto || '');
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
|
|
28
|
+
// Varre TODOS os blocos completos e fica com o último que dá para ler.
|
|
29
|
+
//
|
|
30
|
+
// A versão anterior fazia lastIndexOf(INICIO) e depois procurava o FIM
|
|
31
|
+
// para frente. Bastava o modelo escrever a marca de abertura mais uma vez
|
|
32
|
+
// — explicando o formato, ou começando um bloco e desistindo — para o FIM
|
|
33
|
+
// não existir depois dela: a extração devolvia null e o protocolo CRU ia
|
|
34
|
+
// parar na tela do usuário, com tool_call e tudo. Foi o que aconteceu num
|
|
35
|
+
// pedido real de vídeo.
|
|
36
|
+
const blocos = [];
|
|
37
|
+
let i = 0;
|
|
38
|
+
while (true) {
|
|
39
|
+
const inicio = t.indexOf(MARCA_INICIO, i);
|
|
40
|
+
if (inicio === -1) break;
|
|
41
|
+
const fim = t.indexOf(MARCA_FIM, inicio + MARCA_INICIO.length);
|
|
42
|
+
if (fim === -1) break; // bloco aberto e não fechado: ignora
|
|
43
|
+
blocos.push(t.slice(inicio + MARCA_INICIO.length, fim));
|
|
44
|
+
i = fim + MARCA_FIM.length;
|
|
36
45
|
}
|
|
46
|
+
|
|
47
|
+
for (let k = blocos.length - 1; k >= 0; k--) {
|
|
48
|
+
const lido = lerJSON(blocos[k]);
|
|
49
|
+
if (lido) return lido;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** O JSON do bloco, tolerando o que o modelo costuma acrescentar sem querer. */
|
|
55
|
+
function lerJSON(bruto) {
|
|
56
|
+
let s = String(bruto || '').trim();
|
|
57
|
+
// Cerca de markdown em volta do JSON — o modelo põe por hábito.
|
|
58
|
+
s = s.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '').trim();
|
|
59
|
+
try { return JSON.parse(s); } catch {}
|
|
60
|
+
// Sobrou texto depois do objeto? Corta no fecha-chaves que casa com o primeiro.
|
|
61
|
+
const abre = s.indexOf('{');
|
|
62
|
+
if (abre === -1) return null;
|
|
63
|
+
let nivel = 0, dentro = false, escapa = false;
|
|
64
|
+
for (let k = abre; k < s.length; k++) {
|
|
65
|
+
const c = s[k];
|
|
66
|
+
if (escapa) { escapa = false; continue; }
|
|
67
|
+
if (c === '\\') { escapa = true; continue; }
|
|
68
|
+
if (c === '"') { dentro = !dentro; continue; }
|
|
69
|
+
if (dentro) continue;
|
|
70
|
+
if (c === '{') nivel++;
|
|
71
|
+
else if (c === '}' && --nivel === 0) {
|
|
72
|
+
try { return JSON.parse(s.slice(abre, k + 1)); } catch { return null; }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
37
76
|
}
|
|
38
77
|
|
|
39
78
|
const catalogo = require('./catalogo');
|
|
@@ -229,7 +268,15 @@ function ultimaMensagem(messages) {
|
|
|
229
268
|
function emitirDecisao(textoLido, onEvent) {
|
|
230
269
|
const decisao = extrairDecisao(textoLido);
|
|
231
270
|
if (!decisao) {
|
|
232
|
-
|
|
271
|
+
// Resposta sem bloco legível. Se ela AINDA ASSIM traz a marca do
|
|
272
|
+
// protocolo, o motor falhou: mostrar isso cru — com "tool_call" e
|
|
273
|
+
// JSON na cara do usuário — é pior do que dizer que falhou.
|
|
274
|
+
let conteudo = String(textoLido || '').trim();
|
|
275
|
+
if (conteudo.includes(MARCA_INICIO) || conteudo.includes(MARCA_FIM)) {
|
|
276
|
+
conteudo = conteudo.split(MARCA_INICIO)[0].trim()
|
|
277
|
+
|| 'O motor Claude Code devolveu uma decisão que não consegui ler. Tente de novo, ou volte ao motor padrão com /claude off.';
|
|
278
|
+
}
|
|
279
|
+
onEvent({ assistant_message: { content: conteudo } });
|
|
233
280
|
return;
|
|
234
281
|
}
|
|
235
282
|
if (decisao.tool_call) {
|
package/lib/modo.js
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Escolha automática de modo.
|
|
3
3
|
*
|
|
4
|
-
* O modo primocode manda o schema das
|
|
5
|
-
* tokens antes de você digitar a primeira letra. Um "oi" gasta o mesmo que
|
|
6
|
-
* refatoração, e num plano com teto diário isso é dinheiro jogado fora (foi
|
|
4
|
+
* O modo primocode manda o schema das 45 ferramentas em todo pedido — milhares
|
|
5
|
+
* de tokens antes de você digitar a primeira letra. Um "oi" gasta o mesmo que
|
|
6
|
+
* uma refatoração, e num plano com teto diário isso é dinheiro jogado fora (foi
|
|
7
7
|
* assim que a cota de 100 mil tokens/dia acabou às 15h).
|
|
8
8
|
*
|
|
9
9
|
* O modo chat não manda schema nenhum. Então a regra é: só paga o preço das
|
|
10
10
|
* ferramentas quem vai usar ferramenta.
|
|
11
11
|
*
|
|
12
12
|
* A decisão é local e sem custo — nenhuma chamada ao modelo para descobrir a
|
|
13
|
-
* intenção, senão o remédio custaria como a doença.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* intenção, senão o remédio custaria como a doença.
|
|
14
|
+
*
|
|
15
|
+
* ── POR QUE ELE OLHA O TURNO ANTERIOR ────────────────────────────────────
|
|
16
|
+
* A primeira versão julgava cada frase sozinha, e errava feio no uso real: o
|
|
17
|
+
* usuário pedia um vídeo (tarefa), e o "refaz", o "continua" e o "agora põe
|
|
18
|
+
* música de fundo" logo depois caíam no chat — o agente perdia o fio no meio
|
|
19
|
+
* do trabalho. Medido na conversa do dono: 7 erros em 18 frases, quase todos
|
|
20
|
+
* de continuação.
|
|
21
|
+
*
|
|
22
|
+
* Conversa humana não repete o objeto a cada frase. Então, QUANDO O TURNO
|
|
23
|
+
* ANTERIOR FOI UMA TAREFA, a régua abaixa: o que não é claramente cumprimento
|
|
24
|
+
* ou dúvida conceitual continua sendo trabalho. Fora de uma tarefa, a régua
|
|
25
|
+
* volta a subir — na dúvida, chat.
|
|
17
26
|
*/
|
|
18
27
|
|
|
19
28
|
'use strict';
|
|
@@ -34,6 +43,13 @@ const VERBOS = [
|
|
|
34
43
|
// "pesquisa no google", "aperta enter", "arrasta o arquivo".
|
|
35
44
|
'clic[ao]r?', 'clique', 'digit[ae]r?', 'pesquis[ae]r?', 'apert[ae]r?',
|
|
36
45
|
'arrast[ae]r?', 'rol[ae]r?', 'minimiz[ae]r?', 'maximiz[ae]r?', 'fech[ae]r?',
|
|
46
|
+
// Ajuste do que já foi criado — o vocabulário do estúdio. Sem estes, "agora
|
|
47
|
+
// põe música de fundo" caía no chat, no meio de um vídeo.
|
|
48
|
+
'p[õo]e', 'ponha', 'p[oô]r', 'coloc[ae]r?', 'tir[ae]r?', 'troc[ae]r?',
|
|
49
|
+
'inclu[ai]r?', 'insir[ae]', 'insert', 'aument[ae]r?', 'diminu[ai]r?',
|
|
50
|
+
'grav[ae]r?', 'narr[ae]r?', 'export[ae]r?', 'salv[ae]r?', 'envi[ae]r?',
|
|
51
|
+
'convert[ae]r?', 'transform[ae]r?', 'divid[ai]r?', 'junt[ae]r?',
|
|
52
|
+
'desenh[ae]r?', 'ilustr[ae]r?', 'resum[ae]r?', 'traduz[ai]r?',
|
|
37
53
|
];
|
|
38
54
|
const RE_VERBO = new RegExp(`(^|\\s)(${VERBOS.join('|')})\\b`, 'i');
|
|
39
55
|
|
|
@@ -49,33 +65,75 @@ const RE_ARTEFATO = /\.(js|ts|tsx|jsx|html|css|json|md|py|sh|yml|yaml|txt|csv|en
|
|
|
49
65
|
const RE_FERRAMENTA = /\b(navegador|browser|desktop|terminal|shell|git|npm|node|deploy|servidor|localhost|porta \d+|screenshot|print da tela|mouse|teclado|janela|[áa]rea de trabalho|aplicativo|[íi]cone)\b/i;
|
|
50
66
|
const RE_PASTA = /\b(pasta|diret[óo]rio|arquivo|arquivos|reposit[óo]rio|repo|projeto)\b/i;
|
|
51
67
|
|
|
68
|
+
// O que o estúdio produz. "faz uns slides", "quero um post", "monta a planilha"
|
|
69
|
+
// — o substantivo sozinho já denuncia a tarefa, mesmo sem verbo claro.
|
|
70
|
+
const RE_PECA = /\b(slides?|apresenta[çc][ãa]o|deck|v[íi]deo|reels?|anima[çc][ãa]o|post|banner|capa|thumbnail|arte|cartaz|planilha|tabela|documento|relat[óo]rio|artigo|proposta|contrato|diagrama|fluxograma|organograma|mapa mental|narra[çc][ãa]o|trilha|legenda)\b/i;
|
|
71
|
+
|
|
52
72
|
// Conversa: cumprimento, agradecimento, pergunta conceitual.
|
|
53
|
-
const RE_SAUDACAO = /^\s*(oi+|ol[áa]|e?a[íi]|bom dia|boa tarde|boa noite|tudo bem|beleza|valeu|obrigad[oa]|brigad[oa]|tchau|at
|
|
73
|
+
const RE_SAUDACAO = /^\s*(oi+|ol[áa]|e?a[íi]|bom dia|boa tarde|boa noite|tudo bem|beleza|valeu|obrigad[oa]|brigad[oa]|tchau|at[ée]|ok|okay|entendi|legal|show|massa|top|perfeito|isso|isso a[íi]|ta bom|t[áa] bom|certo)\b[\s!.?]*$/i;
|
|
54
74
|
const RE_CONCEITUAL = /\b(o que [ée]|o q [ée]|que [ée]|qual (a|o|as|os)?\s*(diferen[çc]a|melhor|sentido)|por qu[êe]|pra que serve|para que serve|como funciona|como (fa[çc]o|posso|se faz|fazer)|explica|explique|me explica|significa|vale a pena|devo usar|qual usar|prefere)\b/i;
|
|
75
|
+
// "o que você faz?", "o que vc sabe fazer?" — é pergunta sobre ele, não pedido.
|
|
76
|
+
// Sem esta linha o "faz" batia no verbo e virava tarefa.
|
|
77
|
+
const RE_SOBRE_ELE = /\bo que (voc[êe]|vc|tu)\s+(faz|sabe|consegue|pode|[ée])\b|\bquem [ée] voc[êe]\b|\bcomo voc[êe] funciona\b/i;
|
|
78
|
+
|
|
79
|
+
// Satisfação: "ficou bom", "tá bom assim", "pode parar" — o usuário está
|
|
80
|
+
// dizendo que ACABOU, não pedindo mais trabalho. Sem isto, no meio de uma
|
|
81
|
+
// tarefa a régua abaixada mandava um elogio de volta para o agente.
|
|
82
|
+
const RE_SATISFEITO = /^\s*(t[áa]|ta|ficou|est[áa]|ta[' ]?)?\s*(bom|[óo]timo|legal|show|massa|perfeito|lindo|maneiro|isso)\b(\s+(assim|mesmo|ent[ãa]o|demais))?[\s!.]*$|^\s*(pode parar|chega|s[óo] isso|era isso|nada mais)\b/i;
|
|
83
|
+
|
|
84
|
+
// Continuação forte: sozinhas já significam "siga com o que estava fazendo".
|
|
85
|
+
const RE_CONTINUACAO = /^\s*(refa[çz]a?|refaz|de novo|novamente|outra vez|mais uma vez|continu[ae]|prossiga|segue|siga|vai|manda|tenta( de novo| outra vez)?|tente( de novo| outra vez)?|repete|repita)\b/i;
|
|
86
|
+
|
|
87
|
+
// Continuação fraca: só vale quando o turno anterior foi tarefa.
|
|
88
|
+
// "agora …", "e o …?", "também …", "mais …", "sem …", "com …".
|
|
89
|
+
const RE_CONTINUACAO_FRACA = /^\s*(agora|depois|ent[ãa]o|e (o|a|os|as|no|na)\b|tamb[ée]m|mais|menos|sem|com|s[óo]|apenas|troca|muda)\b/i;
|
|
55
90
|
|
|
56
91
|
/**
|
|
57
92
|
* Decide o modo de um pedido.
|
|
58
|
-
*
|
|
93
|
+
*
|
|
94
|
+
* @param {string} texto o que o usuário digitou
|
|
95
|
+
* @param {{ultimoModo?: string}} ctx contexto da conversa; `ultimoModo` é o
|
|
96
|
+
* modo que o pedido ANTERIOR usou de fato ('primocode' | 'chat').
|
|
97
|
+
* @returns {{modo:'primocode'|'chat', motivo:string}}
|
|
98
|
+
*
|
|
59
99
|
* O motivo existe para o CLI mostrar por que escolheu — automode que decide em
|
|
60
100
|
* silêncio parece defeito na primeira vez que erra.
|
|
61
101
|
*/
|
|
62
|
-
function escolherModo(texto) {
|
|
102
|
+
function escolherModo(texto, ctx = {}) {
|
|
63
103
|
const t = String(texto || '').trim();
|
|
64
104
|
if (!t) return { modo: 'chat', motivo: 'nada a fazer' };
|
|
65
105
|
|
|
106
|
+
const noMeioDeUmaTarefa = (ctx && ctx.ultimoModo) === 'primocode';
|
|
107
|
+
|
|
66
108
|
// Cumprimento é chat mesmo que traga um verbo depois ("oi, tudo bem?").
|
|
67
109
|
if (RE_SAUDACAO.test(t)) return { modo: 'chat', motivo: 'cumprimento' };
|
|
110
|
+
if (RE_SATISFEITO.test(t)) return { modo: 'chat', motivo: 'está satisfeito' };
|
|
68
111
|
|
|
69
|
-
// Pergunta
|
|
70
|
-
// dúvida, não pedido de execução.
|
|
112
|
+
// Pergunta sobre ele mesmo, e pergunta conceitual, ganham do verbo:
|
|
113
|
+
// "como faço para criar um site?" é dúvida, não pedido de execução.
|
|
114
|
+
if (RE_SOBRE_ELE.test(t)) return { modo: 'chat', motivo: 'pergunta sobre ele' };
|
|
71
115
|
if (RE_CONCEITUAL.test(t)) return { modo: 'chat', motivo: 'pergunta conceitual' };
|
|
72
116
|
|
|
117
|
+
// "refaz", "de novo", "continua" — retomada, com ou sem contexto.
|
|
118
|
+
if (RE_CONTINUACAO.test(t)) return { modo: 'primocode', motivo: 'retoma a tarefa' };
|
|
119
|
+
|
|
120
|
+
// Pistas diretas.
|
|
73
121
|
if (RE_ARTEFATO.test(t)) return { modo: 'primocode', motivo: 'cita arquivo ou caminho' };
|
|
74
122
|
if (RE_VERBO.test(t)) return { modo: 'primocode', motivo: 'pede uma ação' };
|
|
123
|
+
if (RE_PECA.test(t)) return { modo: 'primocode', motivo: 'fala de algo para criar' };
|
|
75
124
|
if (RE_DESEJO.test(t)) return { modo: 'primocode', motivo: 'expressa o que quer que exista' };
|
|
76
125
|
if (RE_FERRAMENTA.test(t)) return { modo: 'primocode', motivo: 'menciona ferramenta ou máquina' };
|
|
77
126
|
if (RE_PASTA.test(t)) return { modo: 'primocode', motivo: 'fala de arquivo ou projeto' };
|
|
78
127
|
|
|
128
|
+
// No meio de uma tarefa a régua abaixa: conversa não repete o objeto a cada
|
|
129
|
+
// frase, e o custo de errar aqui é o agente perder o fio.
|
|
130
|
+
if (noMeioDeUmaTarefa) {
|
|
131
|
+
if (RE_CONTINUACAO_FRACA.test(t)) return { modo: 'primocode', motivo: 'continua o que estava fazendo' };
|
|
132
|
+
// Pergunta curta no meio do trabalho costuma ser sobre o trabalho
|
|
133
|
+
// ("e não dá pra deixar rodando?"), não papo novo.
|
|
134
|
+
if (t.split(/\s+/).length <= 12) return { modo: 'primocode', motivo: 'segue a tarefa em andamento' };
|
|
135
|
+
}
|
|
136
|
+
|
|
79
137
|
return { modo: 'chat', motivo: 'sem pista de tarefa' };
|
|
80
138
|
}
|
|
81
139
|
|
package/lib/studio.js
CHANGED
|
@@ -191,8 +191,43 @@ const atualizar = ({ id, titulo, doc }) => comStudio(async () => {
|
|
|
191
191
|
return { ok: true, ...r };
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
+
// ── Mídia: a imagem de fundo e a música do usuário ───────────────────────
|
|
195
|
+
// Sem isto o agente não tinha como pôr foto de fundo nem trilha própria: a
|
|
196
|
+
// API pede base64, e ele não ia montar data URI na mão dentro de um curl.
|
|
197
|
+
const TIPOS_MIDIA = {
|
|
198
|
+
'.png': 1, '.jpg': 1, '.jpeg': 1, '.gif': 1, '.webp': 1, '.svg': 1, '.avif': 1,
|
|
199
|
+
'.mp3': 1, '.m4a': 1, '.wav': 1, '.ogg': 1, '.aac': 1, '.flac': 1, '.opus': 1, '.weba': 1,
|
|
200
|
+
'.mp4': 1, '.webm': 1, '.mov': 1,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const midiaListar = () => comStudio(async () => ({ ok: true, midia: await pedir('GET', '/api/midia') }));
|
|
204
|
+
|
|
205
|
+
const midiaEnviar = ({ arquivo, nome }) => comStudio(async () => {
|
|
206
|
+
if (!arquivo) return { ok: false, error: 'Diga o caminho do arquivo (arquivo: "foto.jpg").' };
|
|
207
|
+
const caminho = path.resolve(arquivo);
|
|
208
|
+
if (!fs.existsSync(caminho)) return { ok: false, error: `Arquivo não existe: ${arquivo}` };
|
|
209
|
+
|
|
210
|
+
const ext = path.extname(caminho).toLowerCase();
|
|
211
|
+
if (!TIPOS_MIDIA[ext]) {
|
|
212
|
+
return { ok: false, error: `Formato não aceito: ${ext}`, aceitos: Object.keys(TIPOS_MIDIA).join(' ') };
|
|
213
|
+
}
|
|
214
|
+
const bytes = fs.statSync(caminho).size;
|
|
215
|
+
if (bytes > 60 * 1024 * 1024) return { ok: false, error: `Arquivo tem ${(bytes / 1048576).toFixed(1)} MB; o limite é 60 MB.` };
|
|
216
|
+
|
|
217
|
+
const tipo = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
|
|
218
|
+
'.webp': 'image/webp', '.svg': 'image/svg+xml', '.avif': 'image/avif', '.mp3': 'audio/mpeg',
|
|
219
|
+
'.m4a': 'audio/mp4', '.wav': 'audio/wav', '.ogg': 'audio/ogg', '.aac': 'audio/aac',
|
|
220
|
+
'.flac': 'audio/flac', '.opus': 'audio/opus', '.weba': 'audio/webm', '.mp4': 'video/mp4',
|
|
221
|
+
'.webm': 'video/webm', '.mov': 'video/quicktime' }[ext] || 'application/octet-stream';
|
|
222
|
+
|
|
223
|
+
const dados = `data:${tipo};base64,` + fs.readFileSync(caminho).toString('base64');
|
|
224
|
+
const r = await pedir('POST', '/api/midia', { nome: nome || path.basename(caminho), dados }, 120000);
|
|
225
|
+
return { ok: true, ...r, comoUsar: `Use "${r.url}" em fundo.imagem, em src de uma imagem, ou em trilha.src.` };
|
|
226
|
+
});
|
|
227
|
+
|
|
194
228
|
module.exports = {
|
|
195
229
|
BASE, PORTA, CASA,
|
|
196
230
|
garantirNoAr, estaNoAr, prepararCasa, pythonCmd,
|
|
197
231
|
manual, ferramentas, spec, listar, ler, criar, atualizar,
|
|
232
|
+
midiaListar, midiaEnviar,
|
|
198
233
|
};
|
package/lib/tools.js
CHANGED
|
@@ -498,6 +498,11 @@ async function despachar(name, args, ctx) {
|
|
|
498
498
|
return studio.atualizar({ id: args.id, titulo: args.titulo, doc: args.doc });
|
|
499
499
|
case 'studio_listar':
|
|
500
500
|
return studio.listar();
|
|
501
|
+
case 'studio_midia':
|
|
502
|
+
// Sem arquivo, lista o que já foi enviado; com arquivo, envia.
|
|
503
|
+
return args.arquivo
|
|
504
|
+
? studio.midiaEnviar({ arquivo: resolveInProject(args.arquivo, projectDir), nome: args.nome })
|
|
505
|
+
: studio.midiaListar();
|
|
501
506
|
|
|
502
507
|
case 'desktop_double_click':
|
|
503
508
|
return desktop.doubleClick(args.x, args.y);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.2.1",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
|
@@ -60,6 +60,39 @@ livre de direitos, ou uma das trilhas geradas acima.
|
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
+
## Narração
|
|
64
|
+
|
|
65
|
+
A voz que fala por cima do vídeo. Vai **na cena**, e o estúdio narra sozinho
|
|
66
|
+
na hora de tocar — sem gravar nada, sem arquivo:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{ "layout": "centro", "duracao": 6,
|
|
70
|
+
"narracao": "O texto que a voz vai falar nesta cena.",
|
|
71
|
+
"elementos": [ ... ] }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Ajuste da voz no **documento** (opcional):
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
"voz": { "velocidade": 1, "tom": 1, "volume": 1, "lang": "pt-BR" }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Regras que fazem diferença:
|
|
81
|
+
|
|
82
|
+
- **Escreva para o ouvido, não para o olho.** A narração NÃO repete o texto da
|
|
83
|
+
tela: a tela dá o título, a voz dá o argumento. Repetir os dois é o erro mais
|
|
84
|
+
comum, e soa a leitura de slide.
|
|
85
|
+
- **Caiba na duração.** Fale ~2,5 palavras por segundo: uma cena de 6s aguenta
|
|
86
|
+
~15 palavras. Texto mais longo que a cena é cortado no meio.
|
|
87
|
+
- **Uma ideia por cena**, em uma ou duas frases curtas. Ponto final ajuda a voz
|
|
88
|
+
a respirar.
|
|
89
|
+
- Para o vídeo ficar mudo de propósito, é só não pôr `narracao` em cena nenhuma.
|
|
90
|
+
|
|
91
|
+
O usuário também pode gravar a própria voz pelo editor; aí a cena ganha
|
|
92
|
+
`narracaoAudio` com o arquivo, e ela tem prioridade sobre a voz sintetizada.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
63
96
|
## Transições
|
|
64
97
|
|
|
65
98
|
Na cena, `transicao` define como ela **entra**:
|
package/studio/web/tool.html
CHANGED
|
@@ -65,6 +65,10 @@
|
|
|
65
65
|
document.getElementById("btn-pdf").innerHTML = svgIcone("tela");
|
|
66
66
|
document.getElementById("btn-baixar").innerHTML = svgIcone("baixar");
|
|
67
67
|
</script>
|
|
68
|
+
<script src="/js/audio.js"></script>
|
|
69
|
+
<script src="/js/narracao.js"></script>
|
|
70
|
+
<script src="/js/midia.js"></script>
|
|
71
|
+
<script src="/js/exportar.js"></script>
|
|
68
72
|
<script src="/js/editor.js"></script>
|
|
69
73
|
</body>
|
|
70
74
|
</html>
|
package/studio/web/view.html
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
<script src="/js/render.js"></script>
|
|
16
16
|
<script src="/js/render-prosa.js"></script>
|
|
17
17
|
<script src="/js/render-grade.js"></script>
|
|
18
|
+
<script src="/js/audio.js"></script>
|
|
19
|
+
<script src="/js/narracao.js"></script>
|
|
18
20
|
<script src="/js/player.js"></script>
|
|
19
21
|
</body>
|
|
20
22
|
</html>
|