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 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
- const pedido = i === 0 ? passos[i].pedido
554
- : `${passos[i].pedido}\n\nO QUE JÁ FOI FEITO ANTES (use estes fatos, não pesquise de novo):\n${jaFeito}`;
555
-
556
- r = await runAct(pedido, {
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
- jaFeito = String((r.assistantMessage && r.assistantMessage.content) || '').slice(0, 1200);
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
- return { assistantMessage: { role: 'assistant', content: amigavel } };
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
- let precisa = true;
88
- try {
89
- const alvo = fs.statSync(para);
90
- precisa = alvo.size !== st.size || alvo.mtimeMs < st.mtimeMs;
91
- } catch {}
92
- if (precisa) fs.copyFileSync(de, para);
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()) return { ok: true, url: BASE, jaEstava: true };
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
- const ler = (id) => comStudio(async () => ({ ok: true, arquivo: await pedir('GET', `/api/arquivos/${encodeURIComponent(id)}`) }));
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)) return { ok: false, error: `Arquivo não existe: ${arquivo}` };
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.0",
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())