primocode 8.10.0 → 8.10.2
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 +26 -5
- package/lib/act.js +29 -2
- package/lib/studio.js +92 -11
- package/package.json +1 -1
- package/studio/server.py +27 -0
package/bin/primocode.js
CHANGED
|
@@ -550,10 +550,19 @@ async function sendPrompt(userText) {
|
|
|
550
550
|
let jaFeito = '';
|
|
551
551
|
for (let i = 0; i < passos.length; i++) {
|
|
552
552
|
if (i > 0) console.log(c.dim(` ⎿ ${passos[i].titulo}`));
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
553
|
+
// Só fato de passo CONCLUÍDO entra aqui. Num pedido real o passo de
|
|
554
|
+
// pesquisa morreu e o texto "Parei sem concluir..." foi injetado
|
|
555
|
+
// como "use estes fatos" — e o passo seguinte obedeceu: criou o
|
|
556
|
+
// vídeo sem fato nenhum, proibido de pesquisar.
|
|
557
|
+
const contexto = !jaFeito ? ''
|
|
558
|
+
: `\n\nO QUE JÁ FOI FEITO ANTES (use estes fatos, não pesquise de novo):\n${jaFeito}`;
|
|
559
|
+
const semFatos = i > 0 && !jaFeito
|
|
560
|
+
? '\n\nATENÇÃO: o passo anterior não trouxe fatos. Trabalhe só com o que está no pedido. '
|
|
561
|
+
+ 'É PROIBIDO inventar dado de empresa, data, preço ou pessoa — na dúvida, não afirme.'
|
|
562
|
+
: '';
|
|
563
|
+
const pedido = passos[i].pedido + contexto + semFatos;
|
|
564
|
+
|
|
565
|
+
const rodarPasso = () => runAct(pedido, {
|
|
557
566
|
server: state.config.server,
|
|
558
567
|
model: state.config.model,
|
|
559
568
|
effort: state.config.effort,
|
|
@@ -568,7 +577,19 @@ async function sendPrompt(userText) {
|
|
|
568
577
|
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
569
578
|
motor: state.engine === 'claude-code' ? claudeEngine : api,
|
|
570
579
|
});
|
|
571
|
-
|
|
580
|
+
|
|
581
|
+
r = await rodarPasso();
|
|
582
|
+
// Passo que desistiu (modelo mudo, voltas esgotadas) ganha UMA
|
|
583
|
+
// segunda chance — costuma ser tropeço transitório do modelo
|
|
584
|
+
// grátis. Falhou de novo: o plano segue com o aviso de "sem
|
|
585
|
+
// fatos", nunca com o texto da falha fantasiado de resultado.
|
|
586
|
+
if (!r.parado && r.concluiu === false && passos.length > 1) {
|
|
587
|
+
console.log(c.dim(' ⎿ o passo não concluiu — tentando de novo'));
|
|
588
|
+
r = await rodarPasso();
|
|
589
|
+
}
|
|
590
|
+
jaFeito = (r.concluiu !== false)
|
|
591
|
+
? String((r.assistantMessage && r.assistantMessage.content) || '').slice(0, 1200)
|
|
592
|
+
: '';
|
|
572
593
|
if (r.parado) break; // Esc Esc no meio do plano para o plano inteiro
|
|
573
594
|
}
|
|
574
595
|
state.history.push({ role: 'user', content: userText });
|
package/lib/act.js
CHANGED
|
@@ -362,6 +362,12 @@ async function runAct(userPrompt, opts) {
|
|
|
362
362
|
let compactacoes = 0;
|
|
363
363
|
const MAX_COMPACTACOES = 2;
|
|
364
364
|
|
|
365
|
+
// O provedor recusa chamada de função malformada com um 400 que dizia
|
|
366
|
+
// "Failed to call a function. Please adjust your prompt." NA CARA do
|
|
367
|
+
// usuário — que não tem prompt nenhum para ajustar. É o modelo tropeçando
|
|
368
|
+
// ao escrever o JSON da chamada, e quase sempre acerta na segunda.
|
|
369
|
+
let funcoesQuebradas = 0;
|
|
370
|
+
|
|
365
371
|
let voltas = 0;
|
|
366
372
|
for (let iteration = 0; iteration < MAX_ITERATIONS && !PARAR; iteration++) {
|
|
367
373
|
voltas++;
|
|
@@ -409,6 +415,19 @@ async function runAct(userPrompt, opts) {
|
|
|
409
415
|
emVoo = null;
|
|
410
416
|
if (PARAR) break;
|
|
411
417
|
|
|
418
|
+
// Chamada de função malformada: o modelo errou a SINTAXE da chamada,
|
|
419
|
+
// não a tarefa. Repetir a mesma rodada quase sempre resolve — e sem
|
|
420
|
+
// isto o 400 cru do provedor ("Failed to call a function. Please
|
|
421
|
+
// adjust your prompt.") caía na tela de quem não tem prompt nenhum
|
|
422
|
+
// para ajustar.
|
|
423
|
+
if (streamError && /failed to call a function|failed_generation|tool_use_failed/i.test(String(streamError))
|
|
424
|
+
&& funcoesQuebradas < 2 && !PARAR) {
|
|
425
|
+
funcoesQuebradas++;
|
|
426
|
+
if (!subagent) console.log(ident(profundidade) + c.dim(' ⎿ o modelo tropeçou na chamada — tentando de novo'));
|
|
427
|
+
iteration--;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
412
431
|
if (streamError && ehTetoDeToken(streamError) && compactacoes < MAX_COMPACTACOES && !PARAR) {
|
|
413
432
|
compactacoes++;
|
|
414
433
|
const antes = messages.length;
|
|
@@ -422,9 +441,11 @@ async function runAct(userPrompt, opts) {
|
|
|
422
441
|
|
|
423
442
|
if (streamError) {
|
|
424
443
|
const amigavel = friendlyError(streamError);
|
|
425
|
-
if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${amigavel}` } };
|
|
444
|
+
if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${amigavel}` }, concluiu: false };
|
|
426
445
|
console.log('\n' + c.err(amigavel));
|
|
427
|
-
|
|
446
|
+
// concluiu: false — o plano de passos não pode tratar um erro de
|
|
447
|
+
// provedor como resultado e passá-lo adiante como "fato".
|
|
448
|
+
return { assistantMessage: { role: 'assistant', content: amigavel }, concluiu: false };
|
|
428
449
|
}
|
|
429
450
|
|
|
430
451
|
// Sem tool_calls: o modelo respondeu e acabou.
|
|
@@ -561,6 +582,11 @@ async function runAct(userPrompt, opts) {
|
|
|
561
582
|
}
|
|
562
583
|
}
|
|
563
584
|
|
|
585
|
+
// O que o modelo entregou por vontade própria, ANTES dos textos de
|
|
586
|
+
// desistência. É esta a diferença que o plano de passos precisa: um passo
|
|
587
|
+
// que "parou sem concluir" não pode virar fato para o passo seguinte.
|
|
588
|
+
const concluiu = Boolean(finalText) && !PARAR;
|
|
589
|
+
|
|
564
590
|
if (PARAR && !finalText) {
|
|
565
591
|
finalText = 'Parado por você (Esc Esc). O que já foi feito, ficou feito.';
|
|
566
592
|
if (!subagent) console.log('\n' + c.warn('⏹ ' + finalText));
|
|
@@ -594,6 +620,7 @@ async function runAct(userPrompt, opts) {
|
|
|
594
620
|
// INTEIRO, não só o passo — senão o próximo passo começa logo depois
|
|
595
621
|
// de o usuário ter mandado parar.
|
|
596
622
|
parado: PARAR,
|
|
623
|
+
concluiu,
|
|
597
624
|
};
|
|
598
625
|
}
|
|
599
626
|
|
package/lib/studio.js
CHANGED
|
@@ -84,12 +84,12 @@ function copiar(de, para) {
|
|
|
84
84
|
for (const item of fs.readdirSync(de)) copiar(path.join(de, item), path.join(para, item));
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
// Copia SEMPRE. A comparação esperta (tamanho + data) deixava passar
|
|
88
|
+
// arquivo alterado de mesmo tamanho: o npm instala com data antiga
|
|
89
|
+
// (1985, de propósito, por reprodutibilidade) e o teste de mtime nunca
|
|
90
|
+
// disparava. O que o usuário criou não corre risco — arquivos/ e midia/
|
|
91
|
+
// não estão em COPIAVEIS.
|
|
92
|
+
fs.copyFileSync(de, para);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
function prepararCasa() {
|
|
@@ -139,10 +139,64 @@ async function estaNoAr() {
|
|
|
139
139
|
// Uma subida por vez: sem esta trava, duas tools chamadas no mesmo turno
|
|
140
140
|
// disparariam dois servidores na mesma porta e o segundo morreria com
|
|
141
141
|
// "address in use" — barulho por nada.
|
|
142
|
+
/** A assinatura do server.py que está NO DISCO — a versão que deveria rodar. */
|
|
143
|
+
function assinaturaDoDisco() {
|
|
144
|
+
try {
|
|
145
|
+
const corpo = fs.readFileSync(path.join(CASA, 'server.py'));
|
|
146
|
+
return require('crypto').createHash('sha1').update(corpo).digest('hex').slice(0, 12);
|
|
147
|
+
} catch { return null; }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A assinatura do processo que está NO AR — a versão que roda de fato. */
|
|
151
|
+
async function assinaturaNoAr() {
|
|
152
|
+
try { return (await pedir('GET', '/api/versao', null, 1500)).assinatura || null; }
|
|
153
|
+
catch { return null; } // servidor antigo não tem a rota — também é "desatualizado"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Derruba o Studio que está no ar. Primeiro pelo caminho limpo (/api/sair);
|
|
158
|
+
* se for um servidor antigo, que não tem a rota, pelo sistema.
|
|
159
|
+
*/
|
|
160
|
+
async function derrubar() {
|
|
161
|
+
try { await pedir('POST', '/api/sair', {}, 1500); } catch {}
|
|
162
|
+
for (let i = 0; i < 10; i++) {
|
|
163
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
164
|
+
if (!(await estaNoAr())) return true;
|
|
165
|
+
}
|
|
166
|
+
// Servidor antigo: sem /api/sair. Mata pelo padrão da linha de comando.
|
|
167
|
+
try {
|
|
168
|
+
const { execSync } = require('child_process');
|
|
169
|
+
if (os.platform() === 'win32') {
|
|
170
|
+
execSync('powershell -NoProfile -Command "Get-CimInstance Win32_Process | ' +
|
|
171
|
+
"Where-Object {$_.CommandLine -like '*primocode*studio*server.py*'} | " +
|
|
172
|
+
'ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"', { stdio: 'ignore' });
|
|
173
|
+
} else {
|
|
174
|
+
execSync("pkill -f '[.]primocode/studio/server.py'", { stdio: 'ignore' });
|
|
175
|
+
}
|
|
176
|
+
} catch {}
|
|
177
|
+
for (let i = 0; i < 10; i++) {
|
|
178
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
179
|
+
if (!(await estaNoAr())) return true;
|
|
180
|
+
}
|
|
181
|
+
return !(await estaNoAr());
|
|
182
|
+
}
|
|
183
|
+
|
|
142
184
|
let subindo = null;
|
|
143
185
|
|
|
144
186
|
async function garantirNoAr() {
|
|
145
|
-
if (await estaNoAr())
|
|
187
|
+
if (await estaNoAr()) {
|
|
188
|
+
// O npm pode ter trocado o código no disco DEPOIS de este processo
|
|
189
|
+
// subir — e aí o Studio no ar é o antigo, servindo o bug que o
|
|
190
|
+
// update corrigiu. Atualiza o disco e compara as assinaturas; se
|
|
191
|
+
// divergirem, reinicia. Na véspera de uma apresentação, "atualizei
|
|
192
|
+
// mas continua igual" é o pior modo de falha que existe.
|
|
193
|
+
prepararCasa();
|
|
194
|
+
const doDisco = assinaturaDoDisco();
|
|
195
|
+
const noAr = await assinaturaNoAr();
|
|
196
|
+
if (!doDisco || (noAr && noAr === doDisco)) return { ok: true, url: BASE, jaEstava: true };
|
|
197
|
+
await derrubar();
|
|
198
|
+
// cai para o caminho de subida normal, já com o código novo
|
|
199
|
+
}
|
|
146
200
|
if (subindo) return subindo;
|
|
147
201
|
|
|
148
202
|
subindo = (async () => {
|
|
@@ -202,7 +256,26 @@ const manual = () => comStudio(async () => ({ ok: true, ...(await pedir('GET', '
|
|
|
202
256
|
const ferramentas = () => comStudio(async () => ({ ok: true, ferramentas: await pedir('GET', '/api/ferramentas') }));
|
|
203
257
|
const spec = (slug) => comStudio(async () => ({ ok: true, spec: await pedir('GET', `/api/ferramentas/${encodeURIComponent(slug)}`) }));
|
|
204
258
|
const listar = () => comStudio(async () => ({ ok: true, arquivos: await pedir('GET', '/api/arquivos') }));
|
|
205
|
-
|
|
259
|
+
// 404 de id é quase sempre id INVENTADO: num pedido real o agente criou o
|
|
260
|
+
// arquivo, recebeu "corte-founders-ea74b" de volta, e chamou studio_ler com
|
|
261
|
+
// "imersao-foulders-ai" — um id que ele imaginou. O erro seco ("não
|
|
262
|
+
// encontrado") não dizia isso, e ele insistia. O erro que ensina, diz.
|
|
263
|
+
async function comIdReal(id, fn) {
|
|
264
|
+
try {
|
|
265
|
+
return await fn();
|
|
266
|
+
} catch (e) {
|
|
267
|
+
if (!/HTTP 404/.test(String(e && e.message))) throw e;
|
|
268
|
+
let existem = [];
|
|
269
|
+
try { existem = (await pedir('GET', '/api/arquivos')).map((a) => a.id).slice(0, 8); } catch {}
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
error: `O id "${id}" não existe. NÃO invente id: use EXATAMENTE o campo "id" que studio_criar devolveu.`
|
|
273
|
+
+ (existem.length ? ` Os que existem agora: ${existem.join(', ')}.` : ''),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const ler = (id) => comStudio(() => comIdReal(id, async () => ({ ok: true, arquivo: await pedir('GET', `/api/arquivos/${encodeURIComponent(id)}`) })));
|
|
206
279
|
|
|
207
280
|
const criar = ({ ferramenta, titulo, doc }) => comStudio(async () => {
|
|
208
281
|
if (!ferramenta) return { ok: false, error: 'Diga a ferramenta: prisma, tela, prosa, grade, corte ou traco.' };
|
|
@@ -211,14 +284,14 @@ const criar = ({ ferramenta, titulo, doc }) => comStudio(async () => {
|
|
|
211
284
|
return { ok: true, ...r };
|
|
212
285
|
});
|
|
213
286
|
|
|
214
|
-
const atualizar = ({ id, titulo, doc }) => comStudio(async () => {
|
|
287
|
+
const atualizar = ({ id, titulo, doc }) => comStudio(() => comIdReal(id, async () => {
|
|
215
288
|
if (!id) return { ok: false, error: 'Falta o id do arquivo.' };
|
|
216
289
|
const corpo = {};
|
|
217
290
|
if (titulo != null) corpo.titulo = titulo;
|
|
218
291
|
if (doc != null) corpo.doc = doc;
|
|
219
292
|
const r = await pedir('PUT', `/api/arquivos/${encodeURIComponent(id)}`, corpo);
|
|
220
293
|
return { ok: true, ...r };
|
|
221
|
-
});
|
|
294
|
+
}));
|
|
222
295
|
|
|
223
296
|
// ── Mídia: a imagem de fundo e a música do usuário ───────────────────────
|
|
224
297
|
// Sem isto o agente não tinha como pôr foto de fundo nem trilha própria: a
|
|
@@ -234,7 +307,15 @@ const midiaListar = () => comStudio(async () => ({ ok: true, midia: await pedir(
|
|
|
234
307
|
const midiaEnviar = ({ arquivo, nome }) => comStudio(async () => {
|
|
235
308
|
if (!arquivo) return { ok: false, error: 'Diga o caminho do arquivo (arquivo: "foto.jpg").' };
|
|
236
309
|
const caminho = path.resolve(arquivo);
|
|
237
|
-
if (!fs.existsSync(caminho))
|
|
310
|
+
if (!fs.existsSync(caminho)) {
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
error: `Arquivo não existe: ${arquivo}. NÃO invente arquivo de mídia — num pedido real o agente `
|
|
314
|
+
+ 'tentou fundo.jpg, trilha.mp3 e logo.png que nunca existiram. Sem arquivo do usuário, siga sem mídia: '
|
|
315
|
+
+ 'fundo pode ser "aurora"/"raios"/"brilho" (gerados), e trilha pode ser gerada — '
|
|
316
|
+
+ '"trilha":{"tipo":"gerada","estilo":"epico"}. O vídeo fica pronto do mesmo jeito.',
|
|
317
|
+
};
|
|
318
|
+
}
|
|
238
319
|
|
|
239
320
|
const ext = path.extname(caminho).toLowerCase();
|
|
240
321
|
if (!TIPOS_MIDIA[ext]) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.10.
|
|
3
|
+
"version": "8.10.2",
|
|
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": {
|
package/studio/server.py
CHANGED
|
@@ -9,6 +9,7 @@ Só biblioteca padrão do Python. Nada para instalar.
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import base64
|
|
12
|
+
import hashlib
|
|
12
13
|
import functools
|
|
13
14
|
import json
|
|
14
15
|
import mimetypes
|
|
@@ -30,6 +31,19 @@ FERRAMENTAS = os.path.join(RAIZ, "ferramentas")
|
|
|
30
31
|
MIDIA = os.path.join(RAIZ, "midia")
|
|
31
32
|
PORTA = int(sys.argv[1]) if len(sys.argv) > 1 else 7777
|
|
32
33
|
|
|
34
|
+
def _assinatura():
|
|
35
|
+
"""SHA-1 do próprio arquivo, lido NA SUBIDA. Se o npm trocar o código no
|
|
36
|
+
disco depois, o processo no ar continua respondendo a assinatura antiga —
|
|
37
|
+
e é exatamente essa diferença que o CLI usa para saber que precisa
|
|
38
|
+
reiniciar o Studio em vez de servir a versão velha para sempre."""
|
|
39
|
+
try:
|
|
40
|
+
with open(os.path.abspath(__file__), "rb") as fh:
|
|
41
|
+
return hashlib.sha1(fh.read()).hexdigest()[:12]
|
|
42
|
+
except Exception:
|
|
43
|
+
return "?"
|
|
44
|
+
|
|
45
|
+
ASSINATURA = _assinatura()
|
|
46
|
+
|
|
33
47
|
# 60 MB por arquivo: cobre faixa longa em mp3 e foto de câmera sem susto
|
|
34
48
|
LIMITE_MIDIA = 60 * 1024 * 1024
|
|
35
49
|
|
|
@@ -506,6 +520,9 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
506
520
|
texto = fh.read()
|
|
507
521
|
return self.txt(texto or json.dumps(info, ensure_ascii=False, indent=2))
|
|
508
522
|
|
|
523
|
+
if p == "/api/versao":
|
|
524
|
+
return self.js({"assinatura": ASSINATURA})
|
|
525
|
+
|
|
509
526
|
if p == "/api/voz":
|
|
510
527
|
return self.voz(urlparse(self.path).query)
|
|
511
528
|
if p == "/api/voz/status":
|
|
@@ -752,6 +769,16 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
752
769
|
corpo, status = guardar_midia(self.corpo())
|
|
753
770
|
return self.js(corpo, status)
|
|
754
771
|
|
|
772
|
+
if rota == "/api/sair":
|
|
773
|
+
# Só o CLI chama, e só quando o código no disco mudou (update do
|
|
774
|
+
# npm). Sem isto o processo antigo ficava no ar servindo o bug que
|
|
775
|
+
# o update tinha acabado de corrigir. Responde antes de morrer,
|
|
776
|
+
# para quem pediu não ver erro de conexão.
|
|
777
|
+
self.js({"ok": True, "tchau": ASSINATURA})
|
|
778
|
+
import threading
|
|
779
|
+
threading.Thread(target=lambda: (time.sleep(0.3), os._exit(0)), daemon=True).start()
|
|
780
|
+
return
|
|
781
|
+
|
|
755
782
|
if rota == "/api/arquivos":
|
|
756
783
|
try:
|
|
757
784
|
a = criar(self.corpo())
|