primocode 9.2.1 → 9.2.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v9.2.1
1
+ # PrimoCode v9.2.2
2
2
 
3
3
  Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
4
4
  Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
9
9
  está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
10
10
 
11
11
  ```
12
- ▐▛███▜▌ Primo Code v9.2.1
12
+ ▐▛███▜▌ Primo Code v9.2.2
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
package/lib/act.js CHANGED
@@ -541,13 +541,29 @@ async function runAct(userPrompt, opts) {
541
541
  if (!subagent && profundidade === 0) {
542
542
  const producao = require('./estudio/diretor.js').precisaDeProducao(pedido);
543
543
  if (producao.producao) {
544
+ /* "SE ESTIVER NA SUA LISTA" não é hesitação — é a diferença entre
545
+ a tarefa seguir e a tarefa morrer.
546
+
547
+ A primeira versão mandava chamar `planejar_producao` sem
548
+ ressalva. Só que quem declara as ferramentas ao modelo é o
549
+ SERVIDOR, e este texto é do cliente: com o servidor
550
+ desatualizado, o modelo obedecia a uma ordem para chamar algo
551
+ que não estava na lista dele, e o provedor recusava com
552
+ "attempted to call tool 'planejar_producao' which was not in
553
+ request.tools" — erro FATAL, em inglês, na cara do usuário, com
554
+ a tarefa perdida antes da primeira ação.
555
+
556
+ Um modelo não chama tool fora da lista por conta própria. Só
557
+ chamou porque este parágrafo mandou. */
544
558
  messages.push({ role: 'system', content:
545
- 'ESTE PEDIDO É DE PRODUÇÃO AUDIOVISUAL. Antes de criar qualquer peça, chame '
546
- + `planejar_producao com o pedido do usuário (formato "${producao.formato}"). `
547
- + 'Ele devolve a pauta — arco, narração falada, voz, direção de arte e a mídia a buscar — '
548
- + 'e é ela que você produz, cena a cena, compondo com as primitivas. Não comece pelo '
549
- + 'estudio_plano: o roteiro escrito no meio do JSON da cena vira legenda de slide, '
550
- + 'que é exatamente o que se quer evitar.' });
559
+ 'ESTE PEDIDO É DE PRODUÇÃO AUDIOVISUAL. Se `planejar_producao` estiver na sua lista de '
560
+ + `ferramentas, chame-o ANTES de criar qualquer peça (formato "${producao.formato}"): `
561
+ + 'ele devolve a pauta — arco, narração falada, voz, direção de arte e a mídia a buscar — '
562
+ + 'e é ela que você produz, cena a cena, compondo com as primitivas. '
563
+ + 'Se ele NÃO estiver na sua lista, não o chame e não procure equivalente: siga com as '
564
+ + 'ferramentas que você tem e escreva o roteiro antes das cenas, não dentro do JSON delas. '
565
+ + 'Nunca chame uma ferramenta que não está na sua lista — a chamada é recusada e a tarefa '
566
+ + 'se perde.' });
551
567
  }
552
568
  }
553
569
 
@@ -569,6 +585,11 @@ async function runAct(userPrompt, opts) {
569
585
  // usuário — que não tem prompt nenhum para ajustar. É o modelo tropeçando
570
586
  // ao escrever o JSON da chamada, e quase sempre acerta na segunda.
571
587
  let funcoesQuebradas = 0;
588
+ /* As ferramentas que o provedor recusou por não estarem na lista do
589
+ pedido. Guardadas para não entrar em laço: tira a instrução, tenta de
590
+ novo UMA vez, e se voltar a acontecer com a mesma ferramenta é porque a
591
+ causa é outra. */
592
+ const toolsRecusadas = new Set();
572
593
  let respostasVazias = 0;
573
594
  // O teto de tokens é POR MINUTO, somando TODAS as chamadas — o plano, o
574
595
  // resumo e cada rodada do loop dividem a mesma cota. Um pedido perfeito
@@ -674,6 +695,37 @@ async function runAct(userPrompt, opts) {
674
695
  continue;
675
696
  }
676
697
 
698
+ /* FERRAMENTA FORA DA LISTA DO PEDIDO — recuperável, e antes não era.
699
+ O provedor recusa com "attempted to call tool 'X' which was not in
700
+ request.tools" e a rodada morria ali: erro cru em inglês, tarefa
701
+ perdida antes da primeira ação. Visto em produção num "crie um
702
+ vídeo para a Paris Group".
703
+
704
+ Quem mandou chamar aquilo foi uma instrução NOSSA, de sistema (o
705
+ gatilho do diretor). Então a cura é tirar a instrução que cita a
706
+ ferramenta recusada e repetir a rodada: o modelo segue com o que
707
+ ele realmente tem, e a pessoa recebe o trabalho em vez de um erro.
708
+ Uma vez por ferramenta — insistindo, o problema é outro. */
709
+ const foraDaLista = streamError && String(streamError)
710
+ .match(/attempted to call tool '([^']+)' which was not in request\.tools/i);
711
+ if (foraDaLista && !PARAR && !toolsRecusadas.has(foraDaLista[1])) {
712
+ toolsRecusadas.add(foraDaLista[1]);
713
+ const nome = foraDaLista[1];
714
+ for (let i = messages.length - 1; i >= 0; i--) {
715
+ if (messages[i].role === 'system' && String(messages[i].content || '').includes(nome)) {
716
+ messages.splice(i, 1);
717
+ }
718
+ }
719
+ messages.push({ role: 'system', content:
720
+ `A ferramenta "${nome}" NÃO está disponível nesta sessão. Não a chame e não procure `
721
+ + 'equivalente com outro nome: siga a tarefa com as ferramentas da sua lista. '
722
+ + 'Se nenhuma delas servir para o que foi pedido, diga isso ao usuário em uma frase, '
723
+ + 'em vez de desistir sem explicar.' });
724
+ if (fluxo) fluxo.fechar();
725
+ iteration--;
726
+ continue;
727
+ }
728
+
677
729
  if (streamError && !PARAR && (ehTetoDeToken(streamError) || ehTransitorio(streamError))) {
678
730
  if (fluxo) fluxo.fechar();
679
731
  // Conversa longa: encolher resolve, porque o peso é o histórico.
package/lib/ui.js CHANGED
@@ -349,6 +349,22 @@ function friendlyError(raw) {
349
349
  }
350
350
  } catch {}
351
351
 
352
+ /* FERRAMENTA QUE NÃO ESTÁ NA LISTA DO PEDIDO.
353
+ O provedor recusa a chamada e devolve, em inglês:
354
+ "tool call validation failed: attempted to call tool 'X' which was not
355
+ in request.tools". Isso saía cru na tela e a tarefa morria ali.
356
+
357
+ A causa é sempre a mesma e não é do usuário: o servidor declara um
358
+ conjunto de ferramentas e o cliente esperava outro. Então a frase diz o
359
+ que É — em português — e aponta para o que resolve. */
360
+ const foraDaLista = s.match(/attempted to call tool '([^']+)' which was not in request\.tools/i);
361
+ if (foraDaLista) {
362
+ return `O servidor não ofereceu a ferramenta "${foraDaLista[1]}" nesta chamada, `
363
+ + 'então ela foi recusada. Isso acontece quando o servidor está numa versão '
364
+ + 'anterior à do aplicativo. Confira em /status; se persistir, o servidor precisa '
365
+ + 'ser atualizado.';
366
+ }
367
+
352
368
  // Falta de saldo: nem bug, nem espera. O provedor diz quanto a conta paga,
353
369
  // e o servidor já refaz o pedido com esse teto — se ainda assim chegou aqui,
354
370
  // é porque nem o mínimo cabe.
package/lib/voz.js CHANGED
@@ -125,7 +125,13 @@ function conferir() {
125
125
 
126
126
  const pip = falta.filter((f) => f.pacote).map((f) => f.pacote);
127
127
  const linhas = [];
128
- if (pip.length) linhas.push(`${py} -m pip install ${pip.join(' ')}`);
128
+ /* O comando que a pessoa vai COPIAR precisa funcionar na máquina dela.
129
+ Mandar `pip install` seco é mandar ela bater na PEP 668 de novo,
130
+ exatamente no mesmo lugar onde o botão "Instalar agora?" já bateu. */
131
+ if (pip.length) {
132
+ linhas.push(`${py} -m pip install --user ${pip.join(' ')}`);
133
+ linhas.push(`(se recusar com "externally-managed-environment", acrescente --break-system-packages)`);
134
+ }
129
135
  if (falta.some((f) => f.modulo === 'sounddevice' && f.sistema)) {
130
136
  linhas.push(COMO_SISTEMA.portaudio[process.platform] || COMO_SISTEMA.portaudio.linux);
131
137
  }
@@ -146,16 +152,61 @@ function conferir() {
146
152
  }
147
153
 
148
154
  /** Instala o que dá para instalar com pip. O tkinter não passa por aqui. */
155
+ /* AS TENTATIVAS, EM ORDEM — e por que uma só não serve.
156
+ *
157
+ * O Python do Homebrew e o do Debian/Ubuntu RECUSAM `pip install` no ambiente
158
+ * do sistema: é a PEP 668, "externally-managed-environment". Uma tentativa
159
+ * só, sem `--break-system-packages`, falha sempre nessas máquinas — e foi o
160
+ * que aconteceu com o dono, no Mac dele, ao aceitar "Instalar agora?".
161
+ *
162
+ * A ordem vai da menos invasiva para a mais: `--user` primeiro, que não toca
163
+ * no Python do sistema; a quebra do bloqueio só quando o resto falhou.
164
+ */
165
+ const TENTATIVAS_PIP = [
166
+ (pacotes) => ['-m', 'pip', 'install', '--user', '--quiet', ...pacotes],
167
+ (pacotes) => ['-m', 'pip', 'install', '--user', '--quiet', '--break-system-packages', ...pacotes],
168
+ (pacotes) => ['-m', 'pip', 'install', '--quiet', ...pacotes],
169
+ (pacotes) => ['-m', 'pip', 'install', '--quiet', '--break-system-packages', ...pacotes],
170
+ ];
171
+
172
+ /** A linha que interessa de um despejo do pip. */
173
+ function porqueOPipFalhou(saida) {
174
+ const texto = String(saida || '').trim();
175
+ if (!texto) return '';
176
+ if (/externally-managed-environment/i.test(texto)) {
177
+ return 'o Python desta máquina não deixa instalar no ambiente do sistema (PEP 668)';
178
+ }
179
+ /* A linha que começa com "error:" é a que o pip escreveu para ser lida.
180
+ Cortar os últimos 400 caracteres — o que se fazia antes — pega o MEIO
181
+ de uma frase: o dono viu "não deu: n result in a broken Homebrew
182
+ installation", que não é português nem inglês. */
183
+ const erro = texto.split('\n').reverse().find((l) => /^\s*error:/i.test(l));
184
+ if (erro) return erro.replace(/^\s*error:\s*/i, '').trim().slice(0, 200);
185
+ const ultima = texto.split('\n').filter((l) => l.trim()).pop() || '';
186
+ return ultima.trim().slice(0, 200);
187
+ }
188
+
149
189
  function instalar(py, pacotes, aoFalar = () => {}) {
150
190
  return new Promise((resolve) => {
151
191
  aoFalar(`instalando ${pacotes.join(', ')} (uma vez, alguns minutos)`);
152
- const p = spawn(py, ['-m', 'pip', 'install', '--quiet', ...pacotes]);
153
- let erro = '';
154
- p.stderr.on('data', (d) => { erro += d.toString(); });
155
- p.on('close', (code) => resolve(code === 0
156
- ? { ok: true }
157
- : { ok: false, error: erro.slice(-400).trim() || `pip saiu com ${code}` }));
158
- p.on('error', (e) => resolve({ ok: false, error: e.message }));
192
+ let ultimoErro = '';
193
+
194
+ const tentar = (i) => {
195
+ if (i >= TENTATIVAS_PIP.length) {
196
+ return resolve({ ok: false, error: ultimoErro || 'o pip não conseguiu instalar' });
197
+ }
198
+ const p = spawn(py, TENTATIVAS_PIP[i](pacotes));
199
+ let saida = '';
200
+ p.stderr.on('data', (d) => { saida += d.toString(); });
201
+ p.stdout.on('data', (d) => { saida += d.toString(); });
202
+ p.on('close', (code) => {
203
+ if (code === 0) return resolve({ ok: true });
204
+ ultimoErro = porqueOPipFalhou(saida) || `pip saiu com ${code}`;
205
+ tentar(i + 1);
206
+ });
207
+ p.on('error', (e) => { ultimoErro = e.message; tentar(i + 1); });
208
+ };
209
+ tentar(0);
159
210
  });
160
211
  }
161
212
 
@@ -336,4 +387,4 @@ print(json.dumps(desperta.baixar_modelo()))
336
387
  });
337
388
  }
338
389
 
339
- module.exports = { conferir, instalar, abrir, conferirEspera, baixarEspera, python, COMO_FALAR, PASTA };
390
+ module.exports = { conferir, instalar, porqueOPipFalhou, abrir, conferirEspera, baixarEspera, python, COMO_FALAR, PASTA };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "9.2.1",
3
+ "version": "9.2.2",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). 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/edge.py CHANGED
@@ -120,9 +120,19 @@ def disponivel():
120
120
  def _rotina_instalar():
121
121
  global _instalando, _verificado
122
122
  try:
123
- subprocess.run(
124
- [sys.executable, "-m", "pip", "install", "--user", "--quiet", "edge-tts"],
125
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=180)
123
+ # As mesmas tentativas do narrar.py: `--user` sozinho é recusado pelo
124
+ # Python do Homebrew e do Debian/Ubuntu (PEP 668), e uma tentativa só
125
+ # falha sempre nessas máquinas — calada, porque a saída vai para o
126
+ # DEVNULL. A narração então cai na voz de reserva e o vídeo sai com
127
+ # outra voz, sem ninguém saber por quê.
128
+ for args in (["-m", "pip", "install", "--user", "--quiet", "edge-tts"],
129
+ ["-m", "pip", "install", "--user", "--quiet", "--break-system-packages", "edge-tts"],
130
+ ["-m", "pip", "install", "--quiet", "edge-tts"],
131
+ ["-m", "pip", "install", "--quiet", "--break-system-packages", "edge-tts"]):
132
+ r = subprocess.run([sys.executable] + args,
133
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=180)
134
+ if r.returncode == 0:
135
+ break
126
136
  except Exception as e:
127
137
  sys.stderr.write("[edge] pip install falhou: %s\n" % e)
128
138
  finally:
package/studio/pessoa.py CHANGED
@@ -72,12 +72,27 @@ def faltando():
72
72
 
73
73
 
74
74
  def instalar(pacotes):
75
- try:
76
- subprocess.run([sys.executable, "-m", "pip", "install", "--quiet", *pacotes],
77
- capture_output=True, timeout=900, check=True)
78
- return not faltando()
79
- except Exception:
80
- return False
75
+ """Traz os pacotes, tentando na ordem da menos invasiva para a mais.
76
+
77
+ Uma tentativa só não serve: o Python do Homebrew e o do Debian/Ubuntu
78
+ recusam instalar no ambiente do sistema (PEP 668,
79
+ "externally-managed-environment"), e nessas máquinas a instalação falharia
80
+ sempre — aqui, calada, porque a saída é capturada e o except engole.
81
+ """
82
+ tentativas = (
83
+ ["-m", "pip", "install", "--user", "--quiet", *pacotes],
84
+ ["-m", "pip", "install", "--user", "--quiet", "--break-system-packages", *pacotes],
85
+ ["-m", "pip", "install", "--quiet", *pacotes],
86
+ ["-m", "pip", "install", "--quiet", "--break-system-packages", *pacotes],
87
+ )
88
+ for args in tentativas:
89
+ try:
90
+ r = subprocess.run([sys.executable] + args, capture_output=True, timeout=900)
91
+ if r.returncode == 0 and not faltando():
92
+ return True
93
+ except Exception:
94
+ continue
95
+ return False
81
96
 
82
97
 
83
98
  def recortar(entrada, saida, suavizar=7, encolher=2, limiar=0.6):
@@ -176,7 +191,9 @@ def main():
176
191
 
177
192
  if args.comando == "conferir":
178
193
  saida = {"ok": not falta, "falta": falta,
179
- "comoResolver": f"{sys.executable} -m pip install " + " ".join(falta) if falta else None}
194
+ "comoResolver": (f"{sys.executable} -m pip install --user " + " ".join(falta)
195
+ + ' (se recusar com "externally-managed-environment",'
196
+ " acrescente --break-system-packages)") if falta else None}
180
197
  else:
181
198
  if falta:
182
199
  # A instalação é pesada (o MediaPipe passa de 100 MB) e demora.