primocode 8.42.2 → 9.1.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.
Files changed (104) hide show
  1. package/README.md +41 -37
  2. package/bin/primocode.js +598 -360
  3. package/lib/acervo.js +8 -1
  4. package/lib/act.js +205 -114
  5. package/lib/api.js +35 -4
  6. package/lib/boas-vindas.js +42 -195
  7. package/lib/catalogo.js +100 -61
  8. package/lib/config.js +1 -1
  9. package/lib/conta.js +3 -24
  10. package/lib/diff.js +161 -0
  11. package/lib/diretor.js +37 -47
  12. package/lib/entrada.js +576 -0
  13. package/lib/especialistas.js +2 -2
  14. package/lib/estudio/assets.js +343 -0
  15. package/lib/estudio/identidade.js +560 -0
  16. package/lib/estudio/index.js +565 -0
  17. package/lib/estudio/plano.js +327 -0
  18. package/lib/estudio/projeto.js +246 -0
  19. package/lib/estudio/remotion.js +644 -0
  20. package/lib/estudio/slides.js +190 -0
  21. package/lib/estudio/video.js +296 -0
  22. package/lib/fala.js +49 -6
  23. package/lib/marca.js +4 -1
  24. package/lib/markdown.js +376 -0
  25. package/lib/marketplace.js +189 -0
  26. package/lib/pasta.js +15 -4
  27. package/lib/permissao.js +137 -0
  28. package/lib/registro.js +229 -0
  29. package/lib/skills.js +329 -0
  30. package/lib/tools.js +106 -187
  31. package/lib/ui.js +7 -3
  32. package/lib/voz.js +239 -0
  33. package/package.json +12 -3
  34. package/skills/acessibilidade/SKILL.md +28 -0
  35. package/skills/api-que-nao-quebra/SKILL.md +27 -0
  36. package/skills/depurar-de-verdade/SKILL.md +29 -0
  37. package/skills/editar-meu-video/SKILL.md +59 -0
  38. package/skills/escrever-em-portugues/SKILL.md +28 -0
  39. package/skills/git-limpo/SKILL.md +28 -0
  40. package/skills/motion-de-primeira/SKILL.md +62 -0
  41. package/skills/pagina-que-converte/SKILL.md +28 -0
  42. package/skills/performance-web/SKILL.md +28 -0
  43. package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
  44. package/skills/slides-que-prendem/SKILL.md +40 -0
  45. package/studio/edge.py +16 -66
  46. package/studio/narrar.py +274 -0
  47. package/studio/pessoa.py +199 -0
  48. package/voz/primo_voz/__init__.py +0 -0
  49. package/voz/primo_voz/__main__.py +383 -0
  50. package/voz/primo_voz/escuta.py +235 -0
  51. package/voz/primo_voz/orbe.py +216 -0
  52. package/lib/audio-livre.js +0 -177
  53. package/lib/chaves.js +0 -149
  54. package/lib/claude-engine.js +0 -631
  55. package/lib/codex-engine.js +0 -208
  56. package/lib/remotion.js +0 -663
  57. package/lib/studio.js +0 -960
  58. package/studio/PRIMOCODE.md +0 -58
  59. package/studio/README.md +0 -154
  60. package/studio/__pycache__/edge.cpython-311.pyc +0 -0
  61. package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
  62. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  63. package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
  64. package/studio/ferramentas/00-protocolo.md +0 -125
  65. package/studio/ferramentas/01-modelos.md +0 -270
  66. package/studio/ferramentas/README.md +0 -32
  67. package/studio/ferramentas/corte.md +0 -919
  68. package/studio/ferramentas/grade.md +0 -92
  69. package/studio/ferramentas/prisma.md +0 -107
  70. package/studio/ferramentas/prosa.md +0 -86
  71. package/studio/ferramentas/tela.md +0 -93
  72. package/studio/ferramentas/traco.md +0 -79
  73. package/studio/server.py +0 -2254
  74. package/studio/studio.log +0 -57
  75. package/studio/voz.py +0 -388
  76. package/studio/web/analise.html +0 -52
  77. package/studio/web/core.css +0 -1167
  78. package/studio/web/fontes/bebas.woff2 +0 -0
  79. package/studio/web/fontes/grotesk-700.woff2 +0 -0
  80. package/studio/web/fontes/inter-400.woff2 +0 -0
  81. package/studio/web/fontes/inter-700.woff2 +0 -0
  82. package/studio/web/fontes/jetbrains-400.woff2 +0 -0
  83. package/studio/web/fontes/playfair-700.woff2 +0 -0
  84. package/studio/web/fontes.css +0 -23
  85. package/studio/web/home.html +0 -217
  86. package/studio/web/js/analise.js +0 -510
  87. package/studio/web/js/audio.js +0 -607
  88. package/studio/web/js/editor.js +0 -1802
  89. package/studio/web/js/exportar.js +0 -222
  90. package/studio/web/js/ferramentas.js +0 -108
  91. package/studio/web/js/icones.js +0 -120
  92. package/studio/web/js/midia.js +0 -157
  93. package/studio/web/js/narracao.js +0 -325
  94. package/studio/web/js/player.js +0 -903
  95. package/studio/web/js/render-grade.js +0 -243
  96. package/studio/web/js/render-prosa.js +0 -104
  97. package/studio/web/js/render.js +0 -1852
  98. package/studio/web/js/sfx.js +0 -233
  99. package/studio/web/js/video.js +0 -464
  100. package/studio/web/js/voz-neural.js +0 -152
  101. package/studio/web/render.css +0 -1352
  102. package/studio/web/tf.html +0 -15
  103. package/studio/web/tool.html +0 -78
  104. package/studio/web/view.html +0 -27
@@ -1,631 +0,0 @@
1
- /**
2
- * claude-engine.js — motor alternativo do agente: Claude Code local em vez
3
- * de OpenRouter/Groq via primocode-server.
4
- *
5
- * Implementa a MESMA forma que lib/api.js (requestAct(server, payload,
6
- * onEvent)) — é o que permite lib/act.js trocar de motor sem saber disso.
7
- * Ver docs/superpowers/specs/2026-08-15-claude-engine-design.md.
8
- */
9
-
10
- 'use strict';
11
-
12
- const fs = require('fs');
13
- const path = require('path');
14
- const os = require('os');
15
- const { execSync, execFileSync } = require('child_process');
16
-
17
- /* ── O MOTOR EXTERNO NÃO APARECE ────────────────────────────────────────
18
- *
19
- * "O Primo Code abre esses processos ocultamente (em segundo plano) e usa
20
- * apenas o modelo dessas ferramentas. Toda a interface, comandos, agentes e
21
- * experiência continuam sendo 100% do Primo Code."
22
- *
23
- * `execFileSync` sem `stdio` HERDA o stderr do pai — é o padrão do Node, e não
24
- * parece nada até o dia em que o motor imprime um aviso de versão, de
25
- * atualização ou de configuração. Aquilo cai no meio da tela do PrimoCode, com
26
- * a cara e o vocabulário de outro produto, no meio de uma tarefa.
27
- *
28
- * Com o stderr CAPTURADO ele some da tela e ganha um segundo uso: `e.stderr` só
29
- * existe quando é canalizado, e o tratamento de erro daqui (e o do
30
- * codex-engine) já lia esse campo — estava lendo `null` desde sempre, e a
31
- * mensagem de falha do motor saía sem o motivo que ele mesmo tinha escrito.
32
- *
33
- * `windowsHide` é a outra metade do "ocultamente": sem ele, cada chamada
34
- * pisca um console preto na frente de quem usa Windows. */
35
- const SEM_APARECER = { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true };
36
-
37
-
38
- const MARCA_INICIO = '<<<PRIMOCODE>>>';
39
- const MARCA_FIM = '<<<FIM>>>';
40
-
41
- /**
42
- * O modelo que o /claude pede.
43
- *
44
- * "Ele usa o modelo Opus 5 [no Claude]."
45
- *
46
- * Vai o APELIDO (`opus`), não o identificador completo: o apelido é o que o
47
- * Claude Code resolve para o Opus do dia, e um identificador fixo aqui
48
- * envelheceria dentro de um pacote publicado no npm — a pessoa atualizaria o
49
- * Claude Code, o modelo sairia de catálogo, e o PrimoCode pediria um modelo
50
- * que não existe mais.
51
- *
52
- * Quem quiser outro põe PRIMOCODE_CLAUDE_MODELO. Vazio manda sem `--model`, e
53
- * aí vale o padrão da conta da pessoa.
54
- */
55
- const MODELO_CLAUDE = process.env.PRIMOCODE_CLAUDE_MODELO !== undefined
56
- ? process.env.PRIMOCODE_CLAUDE_MODELO
57
- : 'opus';
58
-
59
- /** Os argumentos do modelo, ou nenhum quando a escolha é do dono da conta. */
60
- function argsDoModelo() {
61
- return MODELO_CLAUDE ? ['--model', MODELO_CLAUDE] : [];
62
- }
63
-
64
- /**
65
- * Acha o ÚLTIMO bloco marcado no texto (pode vir ruído de terminal — prompt
66
- * ANSI, texto solto — antes e depois) e devolve o JSON de dentro dele.
67
- * null quando não achou bloco válido: quem chama decide o que fazer.
68
- */
69
- function extrairDecisao(texto) {
70
- const t = String(texto || '');
71
-
72
- // Varre TODOS os blocos completos e fica com o último que dá para ler.
73
- //
74
- // A versão anterior fazia lastIndexOf(INICIO) e depois procurava o FIM
75
- // para frente. Bastava o modelo escrever a marca de abertura mais uma vez
76
- // — explicando o formato, ou começando um bloco e desistindo — para o FIM
77
- // não existir depois dela: a extração devolvia null e o protocolo CRU ia
78
- // parar na tela do usuário, com tool_call e tudo. Foi o que aconteceu num
79
- // pedido real de vídeo.
80
- const blocos = [];
81
- let i = 0;
82
- while (true) {
83
- const inicio = t.indexOf(MARCA_INICIO, i);
84
- if (inicio === -1) break;
85
- const fim = t.indexOf(MARCA_FIM, inicio + MARCA_INICIO.length);
86
- if (fim === -1) break; // bloco aberto e não fechado: ignora
87
- blocos.push(t.slice(inicio + MARCA_INICIO.length, fim));
88
- i = fim + MARCA_FIM.length;
89
- }
90
-
91
- for (let k = blocos.length - 1; k >= 0; k--) {
92
- const lido = lerJSON(blocos[k]);
93
- if (lido) return lido;
94
- }
95
- return null;
96
- }
97
-
98
- /** JSON com quebra de linha CRUA dentro das aspas — o jeito mais comum de o
99
- * motor falhar. O Claude escreve o content de um write_file com linhas de
100
- * verdade em vez de \n, o JSON.parse recusa, e o turno inteiro morria com
101
- * "decisão que não consegui ler". O conserto é mecânico: dentro de string,
102
- * controle vira escape. */
103
- function consertarControles(s) {
104
- let saida = '';
105
- let dentro = false, escapa = false;
106
- for (const c of String(s || '')) {
107
- if (dentro && !escapa && (c === '\n' || c === '\r' || c === '\t')) {
108
- saida += c === '\n' ? '\\n' : c === '\r' ? '\\r' : '\\t';
109
- continue;
110
- }
111
- if (escapa) { escapa = false; saida += c; continue; }
112
- if (c === '\\') { escapa = true; saida += c; continue; }
113
- if (c === '"') dentro = !dentro;
114
- saida += c;
115
- }
116
- return saida;
117
- }
118
-
119
- /** O JSON do bloco, tolerando o que o modelo costuma acrescentar sem querer. */
120
- function lerJSON(bruto) {
121
- let s = String(bruto || '').trim();
122
- // Cerca de markdown em volta do JSON — o modelo põe por hábito.
123
- s = s.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '').trim();
124
- try { return JSON.parse(s); } catch {}
125
- try { return JSON.parse(consertarControles(s)); } catch {}
126
- s = consertarControles(s);
127
- // Sobrou texto depois do objeto? Corta no fecha-chaves que casa com o primeiro.
128
- const abre = s.indexOf('{');
129
- if (abre === -1) return null;
130
- let nivel = 0, dentro = false, escapa = false;
131
- for (let k = abre; k < s.length; k++) {
132
- const c = s[k];
133
- if (escapa) { escapa = false; continue; }
134
- if (c === '\\') { escapa = true; continue; }
135
- if (c === '"') { dentro = !dentro; continue; }
136
- if (dentro) continue;
137
- if (c === '{') nivel++;
138
- else if (c === '}' && --nivel === 0) {
139
- const trecho = s.slice(abre, k + 1);
140
- try { return JSON.parse(trecho); } catch {}
141
- try { return JSON.parse(consertarControles(trecho)); } catch { return null; }
142
- }
143
- }
144
- return null;
145
- }
146
-
147
- const catalogo = require('./catalogo');
148
- const chaves = require('./chaves');
149
-
150
- /** Serializa lib/catalogo.js em texto — única fonte de verdade, sem
151
- * duplicar a lista de ferramentas num arquivo separado. */
152
- function catalogoParaTexto() {
153
- const linhas = [];
154
- for (const grupo of catalogo.GRUPOS) {
155
- linhas.push(`## ${grupo.nome} — ${grupo.resumo}`);
156
- for (const item of grupo.itens) {
157
- const args = item.args ? ` | args: ${item.args}` : ' | sem args';
158
- linhas.push(`- ${item.tool}: ${item.faz}${args}`);
159
- }
160
- linhas.push('');
161
- }
162
- return linhas.join('\n').trim();
163
- }
164
-
165
- /** Texto completo que ensina o Claude Code a se comportar como motor do
166
- * PrimoCode. Vira o conteúdo de ~/.claude/skills/primocode-engine/SKILL.md
167
- * (Task 4) e é referenciado na primeira mensagem de cada sessão. */
168
- function textoBootstrap() {
169
- return `---
170
- name: primocode-engine
171
- description: Skill instalada pelo PrimoCode para o modo /claude — motor de decisão do agente, não um agente autônomo.
172
- ---
173
-
174
- # PrimoCode — modo motor
175
-
176
- Você está sendo usado como o "cérebro" do PrimoCode, um CLI que executa
177
- tarefas de engenharia. Não use suas próprias ferramentas nativas (Read,
178
- Write, Edit, Bash, Glob, Grep, WebFetch, etc.) — quem executa é o
179
- PrimoCode, não você. Sua única saída é dizer QUAL ferramenta do PrimoCode
180
- chamar, com quais argumentos, ou a resposta final.
181
-
182
- ## Ferramentas disponíveis
183
-
184
- ${catalogoParaTexto()}
185
-
186
- ## Formato de resposta — obrigatório
187
-
188
- Termine toda resposta com exatamente um bloco, sem nada depois dele.
189
-
190
- Para chamar uma ferramenta:
191
-
192
- ${MARCA_INICIO}
193
- {"tool_call": {"name": "write_file", "arguments": {"path": "index.html", "content": "..."}}}
194
- ${MARCA_FIM}
195
-
196
- Para terminar a tarefa (sem mais ferramenta pra chamar):
197
-
198
- ${MARCA_INICIO}
199
- {"assistant_message": "frase curta do que foi feito"}
200
- ${MARCA_FIM}
201
-
202
- Nunca omita o bloco. Nunca escreva mais de um bloco por resposta.`;
203
- }
204
-
205
- const SKILL_DIR = () => path.join(os.homedir(), '.claude', 'skills', 'primocode-engine');
206
- const SKILL_PATH = () => path.join(SKILL_DIR(), 'SKILL.md');
207
-
208
- /** Grava (ou regrava) a skill em ~/.claude/skills/primocode-engine/SKILL.md.
209
- * Sempre reescreve com o texto atual — barato, e garante que uma versão
210
- * nova do PrimoCode (com ferramenta nova em catalogo.js) atualiza a skill
211
- * sozinha na próxima vez que /claude ligar. */
212
- function garantirSkill() {
213
- const dir = SKILL_DIR();
214
- fs.mkdirSync(dir, { recursive: true });
215
- const caminho = SKILL_PATH();
216
- fs.writeFileSync(caminho, textoBootstrap(), 'utf-8');
217
- return { ok: true, caminho };
218
- }
219
-
220
- /** executor(cmd) roda um comando e devolve a saída, ou lança. Default:
221
- * execSync de verdade. Testes injetam uma função fake — sem mock framework. */
222
- function temClaudeCode(executor = (cmd) => execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] })) {
223
- try { executor('claude --version'); return true; }
224
- catch { return false; }
225
- }
226
-
227
- function temOrca(executor = (cmd) => execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] })) {
228
- try { executor('orca --version'); return true; }
229
- catch { return false; }
230
- }
231
-
232
- /** executor aqui devolve TEXTO (o JSON cru de `orca status --json`), não
233
- * só "roda ou lança" — por isso um default diferente do de cima. */
234
- function orcaAlcancavel(executor = (cmd) => execSync(cmd, { encoding: 'utf-8' })) {
235
- try {
236
- const saida = executor('orca status --json');
237
- const j = JSON.parse(saida);
238
- return !!(j && j.result && j.result.runtime && j.result.runtime.reachable);
239
- } catch {
240
- return false;
241
- }
242
- }
243
-
244
- /** Função pura — nenhum I/O aqui, só decide. */
245
- function escolherCaminho({ temClaude, temOrcaCli, orcaOk }) {
246
- if (!temClaude) return 'sem-claude';
247
- if (temOrcaCli && orcaOk) return 'A';
248
- return 'B';
249
- }
250
-
251
- // Handle do terminal ORCA desta execução do processo — sessão contínua
252
- // enquanto o CLI ficar de pé. Cursor de leitura por handle, pra "terminal
253
- // read --cursor" trazer só o que é novo desde o turno anterior.
254
- let sessao = { handle: null, cursor: '0', bootstrapEnviado: false };
255
-
256
- function rodarOrca(args, opts = {}) {
257
- return execFileSync('orca', args,
258
- { encoding: 'utf-8', timeout: opts.timeout || 15000, ...SEM_APARECER });
259
- }
260
-
261
- let contadorId = 0;
262
- /** Id sintético pro tool_call — o bloco marcado do Claude Code não traz um
263
- * (só existe UM tool_call por resposta, pela própria regra do bootstrap),
264
- * mas lib/act.js precisa de tc.id pra parear com a mensagem role:'tool' que
265
- * vem depois. Único dentro do processo já basta. */
266
- function proximoId() {
267
- contadorId += 1;
268
- return `claude-engine-${contadorId}`;
269
- }
270
-
271
- // Caminho A (sessão contínua via ORCA) tem um problema arquitetural real,
272
- // achado rodando de verdade na verificação manual (Task 9 do plano): o
273
- // Claude Code interativo desenha a TUI em tela alternada, e
274
- // `orca terminal read` (scrollback linha a linha) nunca enxerga esse
275
- // conteúdo — o tail fica preso no eco do comando de lançamento. `orca
276
- // terminal show`'s preview mostra a tela renderizada de verdade, mas é só
277
- // um recorte das últimas linhas visíveis, sem garantia de conter o bloco
278
- // marcado inteiro numa resposta mais longa que o painel. Até esse
279
- // mecanismo de leitura ser resolvido, o Caminho B (testado de ponta a
280
- // ponta, funciona) é o único confiável — força ele sempre, mesmo com ORCA
281
- // disponível. O código do Caminho A fica pronto pra quando isso for
282
- // resolvido, só não é escolhido.
283
- const CAMINHO_A_HABILITADO = false;
284
-
285
- async function garantirEngine() {
286
- // execFileSync('claude', …) no caminho headless não resolve o shim
287
- // .cmd/.bat do Windows sem shell:true — quebraria em EVERY turno, mesmo
288
- // com a detecção acima (que usa execSync com shell) passando. Recusa
289
- // antes de instalar a skill: honesto e seguro, em vez de "sucesso" que
290
- // quebra na primeira mensagem real.
291
- if (process.platform === 'win32') {
292
- return {
293
- ok: false,
294
- error: 'O motor Claude Code ainda não funciona no Windows.',
295
- comoResolver: 'Aguarde uma próxima versão, ou use o motor padrão (OpenRouter/Groq).',
296
- };
297
- }
298
- if (!temClaudeCode()) {
299
- return {
300
- ok: false,
301
- error: 'Claude Code não está instalado nesta máquina.',
302
- comoResolver: 'Instale o Claude Code (veja https://docs.claude.com/claude-code) e rode /claude de novo.',
303
- };
304
- }
305
- garantirSkill();
306
- const orcaCli = CAMINHO_A_HABILITADO && temOrca();
307
- const caminho = escolherCaminho({
308
- temClaude: true,
309
- temOrcaCli: orcaCli,
310
- orcaOk: orcaCli ? orcaAlcancavel() : false,
311
- });
312
- return { ok: true, caminho };
313
- }
314
-
315
- function abrirTerminalOrca() {
316
- // --dangerously-skip-permissions aqui não dá poder novo ao Claude Code:
317
- // a skill (textoBootstrap) já instrui ele a nunca usar suas próprias
318
- // ferramentas nativas. A flag só pula os PRÓPRIOS prompts de permissão
319
- // dele, que travariam a sessão sem ninguém pra responder.
320
- const saida = rodarOrca(['terminal', 'create', '--command', 'claude --dangerously-skip-permissions', '--title', 'PrimoCode', '--json']);
321
- const r = JSON.parse(saida);
322
- // `orca terminal create --json` aninha o handle em result.terminal.handle,
323
- // não em result.handle — confirmado rodando o comando de verdade (Task 9).
324
- return r.result.terminal.handle;
325
- }
326
-
327
- function esperarIdleOrca(handle, timeoutMs) {
328
- rodarOrca(['terminal', 'wait', '--terminal', handle, '--for', 'tui-idle', '--timeout-ms', String(timeoutMs)], { timeout: timeoutMs + 5000 });
329
- }
330
-
331
- function ultimaMensagem(messages) {
332
- const m = (messages || [])[messages.length - 1];
333
- return m && m.content ? String(m.content) : '';
334
- }
335
-
336
- /** Devolve a decisão pro onEvent — tool_call, ou assistant_message (com
337
- * degradação pra texto puro quando o bloco marcado não veio, em vez de
338
- * travar o turno em silêncio). */
339
- /* ─────────────────────────────────────────────── o que não deu para ler ──
340
- O motor às vezes devolve algo que este lado não sabe processar: bloco com
341
- sintaxe torta, ou uma ferramenta que não existe. O usuário via isso na
342
- tela — "O motor Claude Code devolveu uma decisão que não consegui ler" —
343
- e a tarefa parava.
344
-
345
- Erro de forma não é assunto do usuário. Quem errou a forma foi o motor, e
346
- quem sabe consertar é ele. Então o problema volta PARA ELE, dizendo
347
- exatamente o que estava errado, e ele reescreve a ação. Só depois de
348
- algumas tentativas seguidas é que alguém precisa saber.
349
-
350
- `pedirCorrecao` é preenchido por quem chamou (o caminho headless ou o
351
- ORCA), porque só eles sabem como falar com o motor de novo. */
352
- function diagnosticar(cru, nomesValidos) {
353
- if (cru.includes(MARCA_INICIO) && !cru.includes(MARCA_FIM)) {
354
- return 'você abriu ' + MARCA_INICIO + ' e nunca fechou com ' + MARCA_FIM + '.';
355
- }
356
- if (!cru.includes(MARCA_INICIO) && cru.includes(MARCA_FIM)) {
357
- return 'apareceu ' + MARCA_FIM + ' sem o ' + MARCA_INICIO + ' antes.';
358
- }
359
- const dentro = /<<<PRIMOCODE>>>([\s\S]*?)<<<FIM>>>/.exec(cru);
360
- if (dentro) {
361
- try {
362
- JSON.parse(dentro[1].trim());
363
- return 'o JSON até é válido, mas não tem "tool_call" nem "assistant_message" na raiz.';
364
- } catch (e) {
365
- return 'o conteúdo entre as marcas não é JSON válido: ' + e.message
366
- + '. Lembre de escapar quebra de linha como \\n dentro das strings.';
367
- }
368
- }
369
- if (nomesValidos) return 'a ferramenta pedida não existe. As que existem: ' + nomesValidos + '.';
370
- return 'não achei um bloco marcado na sua resposta.';
371
- }
372
-
373
- function textoDeCorrecao(cru, motivo) {
374
- return 'A sua última resposta não pôde ser executada: ' + motivo
375
- + '\n\nO que você mandou foi:\n---\n' + String(cru).slice(0, 1200) + '\n---\n\n'
376
- + 'Reescreva AGORA a mesma intenção numa ação que o sistema reconheça. '
377
- + 'Responda com exatamente UM bloco:\n'
378
- + MARCA_INICIO + '\n{"tool_call":{"name":"<uma ferramenta do catálogo>","arguments":{...}}}\n' + MARCA_FIM
379
- + '\nou, se era só texto para a pessoa, ' + MARCA_INICIO
380
- + '\n{"assistant_message":"..."}\n' + MARCA_FIM
381
- + '\nNada fora do bloco.';
382
- }
383
-
384
- const MAX_CORRECOES = 3;
385
-
386
- async function emitirDecisao(textoLido, onEvent, pedirCorrecao, tentativa, validos) {
387
- tentativa = tentativa || 0;
388
- const decisao = extrairDecisao(textoLido);
389
- if (!decisao) {
390
- const cru = String(textoLido || '').trim();
391
- // Traz a marca do protocolo mas o bloco não dá para ler? O motor
392
- // TENTOU chamar ferramenta e errou a sintaxe. Em vez de mostrar isso
393
- // ao usuário, devolvemos o problema ao motor com o diagnóstico e
394
- // pedimos a mesma intenção escrita direito.
395
- if (cru.includes(MARCA_INICIO) || cru.includes(MARCA_FIM)) {
396
- if (pedirCorrecao && tentativa < MAX_CORRECOES) {
397
- const motivo = diagnosticar(cru);
398
- try {
399
- const novo = await pedirCorrecao(textoDeCorrecao(cru, motivo));
400
- return await emitirDecisao(novo, onEvent, pedirCorrecao, tentativa + 1, validos);
401
- } catch (e) {
402
- // A correção em si falhou (motor fora do ar, tempo
403
- // esgotado). Cai no caminho antigo, que o loop repete.
404
- }
405
- }
406
- onEvent({ erro: 'Failed to call a function: o motor Claude devolveu um bloco ilegível — repetindo a rodada.' });
407
- return;
408
- }
409
- // Sem marca nenhuma: é resposta em texto, e texto é resposta válida.
410
- onEvent({ assistant_message: { content: cru } });
411
- return;
412
- }
413
- if (decisao.tool_call) {
414
- // Ferramenta que não existe é o outro jeito de o motor devolver algo
415
- // que este lado não sabe processar. Antes viraria uma ação falhada
416
- // com "ferramenta desconhecida" — ruído para o usuário por um erro
417
- // que é do motor. Volta para ele, com a lista do que existe.
418
- const nome = decisao.tool_call.name;
419
- if (validos && validos.length && validos.indexOf(nome) === -1) {
420
- if (pedirCorrecao && tentativa < MAX_CORRECOES) {
421
- try {
422
- const novo = await pedirCorrecao(textoDeCorrecao(
423
- JSON.stringify(decisao),
424
- 'a ferramenta "' + nome + '" não existe. As que existem: ' + validos.join(', ') + '.'));
425
- return await emitirDecisao(novo, onEvent, pedirCorrecao, tentativa + 1, validos);
426
- } catch (e) { /* motor mudo: segue e deixa o loop tratar */ }
427
- }
428
- onEvent({ erro: 'Failed to call a function: ferramenta "' + nome + '" não existe — repetindo a rodada.' });
429
- return;
430
- }
431
- // lib/act.js usa tc.id pra parear a mensagem role:'tool' com este
432
- // tool_call (messages.push({ tool_call_id: tc.id, ... })) — sem id
433
- // aqui, esse pareamento quebra.
434
- onEvent({ tool_call: { id: proximoId(), name: nome, arguments: decisao.tool_call.arguments || {} } });
435
- }
436
- else if (decisao.assistant_message) {
437
- // O modelo pode confundir a forma com a do tool_call (que É um
438
- // objeto) e emitir {"assistant_message": {"content": "..."}} em vez
439
- // de string pura. Sem essa coerção, `content` vira um objeto
440
- // aninhado e lib/act.js quebra com TypeError no `.trim()` — a
441
- // travessia direta que este arquivo inteiro existe pra evitar.
442
- const am = decisao.assistant_message;
443
- const conteudo = typeof am === 'string'
444
- ? am
445
- : (am && typeof am.content === 'string' ? am.content : JSON.stringify(am));
446
- onEvent({ assistant_message: { content: conteudo } });
447
- }
448
- else onEvent({ assistant_message: { content: JSON.stringify(decisao) } });
449
- }
450
-
451
- async function requestActViaOrca(payload, onEvent) {
452
- if (!sessao.handle) {
453
- try {
454
- sessao.handle = abrirTerminalOrca();
455
- sessao.cursor = '0';
456
- sessao.bootstrapEnviado = false;
457
- esperarIdleOrca(sessao.handle, 30000);
458
- } catch (e) {
459
- // Mesmo tratamento do try/catch abaixo: se o terminal não
460
- // chegou a ficar pronto (timeout do tui-idle, por exemplo), o
461
- // handle nunca foi confirmado usável — zera pra próxima
462
- // chamada abrir um terminal novo do zero, em vez de deixar
463
- // sessao.handle setado com algo que nunca funcionou.
464
- sessao.handle = null;
465
- throw e;
466
- }
467
- }
468
- let texto = ultimaMensagem(payload.messages);
469
- if (!sessao.bootstrapEnviado) {
470
- texto = 'Use a skill primocode-engine para esta sessão inteira — ela está em ~/.claude/skills/primocode-engine/SKILL.md.\n\n' + texto;
471
- sessao.bootstrapEnviado = true;
472
- }
473
- try {
474
- rodarOrca(['terminal', 'send', '--terminal', sessao.handle, '--text', texto, '--enter']);
475
- esperarIdleOrca(sessao.handle, 120000);
476
- const lida = rodarOrca(['terminal', 'read', '--terminal', sessao.handle, '--cursor', sessao.cursor, '--json']);
477
- const leitura = JSON.parse(lida);
478
- // Mesma correção do handle: `terminal read --json` também aninha em
479
- // result.terminal — e o texto vem como ARRAY de linhas (`tail`), não
480
- // uma string única (`text`). Confirmado rodando de verdade (Task 9).
481
- const t = leitura.result.terminal || {};
482
- sessao.cursor = t.nextCursor != null ? String(t.nextCursor) : sessao.cursor;
483
- // No terminal a correção é só mais uma mensagem na MESMA sessão: o
484
- // motor já tem o protocolo e a conversa toda em contexto, então
485
- // basta dizer o que saiu errado.
486
- const pedirCorrecao = (aviso) => {
487
- rodarOrca(['terminal', 'send', '--terminal', sessao.handle, '--text', aviso, '--enter']);
488
- esperarIdleOrca(sessao.handle, 120000);
489
- const outra = JSON.parse(rodarOrca(['terminal', 'read', '--terminal', sessao.handle,
490
- '--cursor', sessao.cursor, '--json']));
491
- const t2 = outra.result.terminal || {};
492
- sessao.cursor = t2.nextCursor != null ? String(t2.nextCursor) : sessao.cursor;
493
- return (t2.tail || []).join('\n');
494
- };
495
- await emitirDecisao((t.tail || []).join('\n'), onEvent, pedirCorrecao, 0, nomesDasTools(payload));
496
- } catch (e) {
497
- // Handle obsoleto (terminal fechado, ORCA reiniciou) — documentado
498
- // pelo próprio `orca agent-context` como terminal_handle_stale. Zera
499
- // a sessão: a PRÓXIMA chamada abre um terminal novo do zero. Esta
500
- // chamada ainda falha (o usuário vê o erro e pode só pedir de novo).
501
- sessao.handle = null;
502
- throw e;
503
- }
504
- }
505
-
506
- /** Sem sessão viva (caminho B), o histórico inteiro vira texto a cada
507
- * chamada — mas `content` sozinho perde a mensagem quando é um turno de
508
- * tool_calls (content vem null ali, o que importa está em tool_calls) ou
509
- * uma resposta de ferramenta (role:'tool'). Reconstrói os dois casos em
510
- * vez de deixar "assistant: null" no texto. */
511
- function linhaDoHistorico(m) {
512
- if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
513
- const chamadas = m.tool_calls
514
- .map((tc) => `chamou ${tc.function.name} com ${tc.function.arguments}`)
515
- .join('; ');
516
- return `assistant: ${chamadas}`;
517
- }
518
- if (m.role === 'tool') return `resultado da ferramenta (${m.tool_call_id}): ${m.content}`;
519
- return `${m.role}: ${m.content}`;
520
- }
521
-
522
- /**
523
- * O protocolo INTEIRO, mais a conversa, num prompt só.
524
- *
525
- * A versão anterior dizia "use a skill primocode-engine" e confiava que o
526
- * headless a carregaria — quando não carregava, o Claude respondia sem bloco
527
- * nenhum e o turno morria. Prompt maior, mas o motor tem contexto de sobra e
528
- * o formato SEMPRE chega.
529
- *
530
- * SEM o cabeçalho YAML da skill: ele começa com "---", e um prompt que começa
531
- * com traço o parser de argumentos lê como OPÇÃO — "error: unknown option
532
- * '---'" na cara do usuário, com o protocolo inteiro despejado no terminal.
533
- * Foi exatamente o que aconteceu.
534
- *
535
- * Isto é o que o /claude e o /codex têm em comum, e é de propósito que more
536
- * num lugar só: são dois motores, e tem de ser UM protocolo.
537
- */
538
- function corpoDoProtocolo() {
539
- return textoBootstrap().replace(/^---[\s\S]*?---\s*/, '');
540
- }
541
-
542
- /** O pedido de conserto: o protocolo de novo, mais o que saiu errado. Sem a
543
- * conversa — o assunto aqui é a FORMA, e repetir o histórico inteiro só
544
- * gastaria contexto no que não é o problema. */
545
- function promptDeCorrecao(aviso) {
546
- return corpoDoProtocolo() + '\n\n' + aviso;
547
- }
548
-
549
- function promptDoProtocolo(payload) {
550
- const historico = (payload.messages || []).map(linhaDoHistorico).join('\n\n');
551
- return corpoDoProtocolo()
552
- + '\n\nIMPORTANTE: no JSON do bloco, escreva quebras de linha como \\n (escapadas), nunca linha de verdade.'
553
- + '\n\n# A conversa até aqui\n\n' + historico
554
- + '\n\nResponda com exatamente UM bloco marcado.';
555
- }
556
-
557
- async function requestActViaHeadless(payload, onEvent) {
558
- const corpoBootstrap = corpoDoProtocolo();
559
- const prompt = promptDoProtocolo(payload);
560
- // --dangerously-skip-permissions: mesma razão do caminho ORCA acima —
561
- // a skill já proíbe o Claude Code de usar suas próprias ferramentas
562
- // nativas; a flag só evita que ele pare pedindo permissão pra si mesmo
563
- // numa chamada headless sem ninguém pra responder.
564
- // O prompt vai por STDIN, nunca por argumento: argumento tem limite de
565
- // tamanho (a conversa cresce) e qualquer conteúdo começando com traço
566
- // seria lido como opção. stdin não tem nenhum dos dois problemas.
567
- const saida = execFileSync('claude', [
568
- '-p',
569
- '--output-format', 'json',
570
- '--dangerously-skip-permissions',
571
- ...argsDoModelo(),
572
- ], { encoding: 'utf-8', timeout: 180000, input: prompt, maxBuffer: 32 * 1024 * 1024,
573
- env: chaves.ambienteDe('claude'), ...SEM_APARECER });
574
- const r = JSON.parse(saida);
575
- // A correção é uma segunda ida ao motor, com o mesmo protocolo e o
576
- // diagnóstico do que saiu errado. Barata: o prompt é curto, e evita que
577
- // um erro de sintaxe do motor vire erro na cara do usuário.
578
- const pedirCorrecao = (aviso) => {
579
- const s = execFileSync('claude', [
580
- '-p', '--output-format', 'json', '--dangerously-skip-permissions',
581
- ...argsDoModelo(),
582
- ], {
583
- encoding: 'utf-8', timeout: 120000, maxBuffer: 32 * 1024 * 1024,
584
- env: chaves.ambienteDe('claude'),
585
- input: promptDeCorrecao(aviso),
586
- ...SEM_APARECER,
587
- });
588
- return JSON.parse(s).result || '';
589
- };
590
- await emitirDecisao(r.result || '', onEvent, pedirCorrecao, 0, nomesDasTools(payload));
591
- }
592
-
593
- /** Os nomes que o sistema realmente sabe executar, tirados do payload. */
594
- function nomesDasTools(payload) {
595
- return ((payload && payload.tools) || [])
596
- .map((t) => (t && t.function && t.function.name) || t.name)
597
- .filter(Boolean);
598
- }
599
-
600
- /** Mesma forma de api.requestAct(server, payload, onEvent) — `server` não
601
- * é usado aqui (não existe servidor neste motor), fica só pra manter a
602
- * assinatura uniforme com o outro lado da escolha em lib/act.js. */
603
- async function requestAct(server, payload, onEvent) {
604
- try {
605
- const engineInfo = await garantirEngine();
606
- if (!engineInfo.ok) {
607
- onEvent({ erro: `${engineInfo.error} ${engineInfo.comoResolver || ''}`.trim() });
608
- return;
609
- }
610
- if (engineInfo.caminho === 'A') await requestActViaOrca(payload, onEvent);
611
- else await requestActViaHeadless(payload, onEvent);
612
- } catch (e) {
613
- onEvent({ erro: e.message });
614
- }
615
- }
616
-
617
- module.exports = {
618
- MARCA_INICIO, MARCA_FIM, extrairDecisao,
619
- catalogoParaTexto, textoBootstrap,
620
- // O protocolo sai daqui para o lib/codex-engine.js usar o MESMO. Ele é o
621
- // que faz um motor de fora virar cérebro do PrimoCode: o texto que
622
- // ensina o formato, a leitura do bloco marcado e a volta para consertar
623
- // o que veio torto. Reimplementar aquilo no outro motor seria manter dois
624
- // protocolos que divergem na primeira correção de um deles.
625
- linhaDoHistorico, nomesDasTools, promptDoProtocolo, promptDeCorrecao,
626
- MODELO_CLAUDE, argsDoModelo,
627
- SKILL_DIR, SKILL_PATH, garantirSkill,
628
- temClaudeCode, temOrca, orcaAlcancavel, escolherCaminho,
629
- garantirEngine, requestAct, emitirDecisao,
630
- _emitirDecisao: emitirDecisao,
631
- };