primocode 9.8.0-beta.0 → 9.8.0-beta.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.
@@ -0,0 +1,332 @@
1
+ /**
2
+ * orcamento-entrada.js — o que ENTRA no pedido, e quanto isso custa.
3
+ *
4
+ * ── O NÚMERO QUE MOTIVOU ESTE ARQUIVO ────────────────────────────────────
5
+ * Medido no servidor, com o pedido VAZIO — antes de o usuário digitar
6
+ * qualquer coisa:
7
+ *
8
+ * schema das 45 ferramentas ....... 4.326 tokens
9
+ * prompt do sistema ............... 2.365 tokens
10
+ * ────────────────────────────────────────────
11
+ * piso de qualquer pedido ......... 6.691 tokens
12
+ * teto do plano grátis ............ 6.000 tokens por minuto
13
+ *
14
+ * Ou seja: TODO pedido do modo primocode já nascia estourado. Não era a
15
+ * conversa que crescia demais — era o custo fixo que não cabia. O usuário via
16
+ * "a conversa passou do teto" no meio de uma tarefa e não tinha o que encurtar.
17
+ *
18
+ * ── A IDEIA ──────────────────────────────────────────────────────────────
19
+ * Ninguém precisa das 45 ferramentas para fazer um slide. Quem pede um vídeo
20
+ * não usa `desktop_click`; quem conserta um bug não usa `studio_criar`. Então
21
+ * cada pedido leva só o que aquele pedido pode usar — ferramentas E seções do
22
+ * prompt — e o que sobra do orçamento vira espaço para a conversa.
23
+ *
24
+ * O núcleo (ler, escrever, procurar, rodar, terminar) vai SEMPRE: é o que
25
+ * qualquer tarefa usa, e adivinhar errado aqui custaria a tarefa inteira.
26
+ *
27
+ * Puro de propósito: sem I/O e sem rede, para o teste medir o orçamento sem
28
+ * chave de API nenhuma.
29
+ */
30
+
31
+ 'use strict';
32
+
33
+ /**
34
+ * A MESMA conta do orcamento.js — e isso não era verdade antes.
35
+ *
36
+ * Aqui era 4 caracteres por token e lá 2,6. Para o mesmo prompt e as mesmas
37
+ * ferramentas as duas discordavam em 2.070 tokens, e quem decidia o orçamento
38
+ * de entrada era a conta OTIMISTA: achava que 3.844 cabiam em 5.200 quando o
39
+ * custo real era ~5.900. JSON de schema é o pior caso — pontuação, aspas e
40
+ * identificador curto tokenizam muito pior que texto corrido.
41
+ */
42
+ const CHARS_POR_TOKEN = 2.6;
43
+ function tokensDe(x) {
44
+ const texto = typeof x === 'string' ? x : JSON.stringify(x || '');
45
+ return Math.ceil(texto.length / CHARS_POR_TOKEN);
46
+ }
47
+
48
+ /**
49
+ * O mesmo schema, com a descrição cortada na primeira frase.
50
+ *
51
+ * A primeira frase é a que diz o que a tool FAZ; o resto é conselho de uso,
52
+ * que também está no prompt. Num teto apertado isso vale 768 tokens (3.130 ->
53
+ * 2.362) sem tirar nenhuma ferramenta da mão do agente — e ferramenta a menos
54
+ * é tarefa que não sai.
55
+ */
56
+ function enxugarSchema(tools) {
57
+ return (tools || []).map((t) => {
58
+ const d = t && t.function && t.function.description;
59
+ if (!d) return t;
60
+ const primeira = String(d).split(/(?<=\.)\s/)[0].slice(0, 160);
61
+ if (primeira.length >= String(d).length) return t;
62
+ return { ...t, function: { ...t.function, description: primeira } };
63
+ });
64
+ }
65
+
66
+ // ── Os grupos ────────────────────────────────────────────────────────────
67
+ // `sempre: true` é o núcleo — vai em todo pedido, sem discussão.
68
+ const GRUPOS = [
69
+ {
70
+ nome: 'nucleo',
71
+ sempre: true,
72
+ tools: ['write_file', 'edit_file', 'multi_edit', 'read_file', 'list_dir',
73
+ 'glob', 'grep', 'run_shell', 'write_todos', 'finish'],
74
+ },
75
+ {
76
+ nome: 'estudio',
77
+ tools: ['studio_manual', 'studio_spec', 'studio_criar', 'studio_ler',
78
+ 'studio_atualizar', 'studio_versoes', 'studio_listar', 'studio_midia'],
79
+ // O que o estúdio produz, e os apps que ele substitui.
80
+ gatilho: /\b(slides?|apresenta[çc][ãa]o|deck|v[íi]deo|reels?|anima[çc][ãa]o|post|banner|capa|thumbnail|arte|cartaz|logo|planilha|tabela|or[çc]amento|documento|relat[óo]rio|artigo|proposta|contrato|diagrama|fluxograma|organograma|mapa mental|narra[çc][ãa]o|trilha|est[úu]dio|canva|powerpoint|keynote|word|docs|excel|sheets|photoshop|figma|capcut|premiere|miro)\b/i,
81
+ },
82
+ {
83
+ nome: 'navegador',
84
+ tools: ['browser_open', 'browser_click', 'browser_type', 'browser_key',
85
+ 'browser_read', 'browser_screenshot', 'browser_scroll', 'browser_wait',
86
+ 'browser_eval', 'open_in_browser'],
87
+ gatilho: /\b(site|p[áa]gina|navegador|browser|chrome|url|https?:|www\.|link|formul[áa]rio|login|clicar no bot[ãa]o|testar? (a p[áa]gina|no navegador)|pesquis|busca|internet|not[íi]cia|empresa|mercado)\b/i,
88
+ },
89
+ {
90
+ nome: 'computador',
91
+ tools: ['desktop_open_app', 'desktop_windows', 'desktop_focus', 'desktop_elements',
92
+ 'desktop_click_element', 'desktop_click', 'desktop_double_click', 'desktop_move',
93
+ 'desktop_drag', 'desktop_type', 'desktop_key', 'desktop_scroll', 'desktop_size',
94
+ 'desktop_screenshot'],
95
+ // Só quando a tarefa é MESMO no computador. É o grupo mais caro
96
+ // (1.141 tokens) e o menos usado no dia a dia.
97
+ gatilho: /\b(janela|[áa]rea de trabalho|desktop|abrir? o (app|programa|aplicativo)|clica(r)? (na tela|no [íi]cone)|[íi]cone|mouse|teclado|atalho|print da tela|screenshot|alt\+tab|ctrl\+|bloco de notas|calculadora|explorer|finder)\b/i,
98
+ },
99
+ {
100
+ nome: 'pesquisa',
101
+ tools: ['pesquisar_referencia'],
102
+ gatilho: /\b(pesquis|busca|procur|internet|documenta[çc][ãa]o|refer[êe]ncia|empresa|mercado|not[íi]cia|quem [ée]|sobre a |sobre o |dados de|hist[óo]ria d)\b/i,
103
+ },
104
+ {
105
+ /* O PIPELINE DE QUATRO PAPÉIS, e ele é caro de duas formas: o schema
106
+ dele é longo, e chamá-lo custa seis a oito agentes. Por isso o
107
+ gatilho pede o PAR — um verbo de construir colado num substantivo de
108
+ coisa inteira. "Melhore o site" e "conserte o app" não passam de
109
+ propósito: o pipeline constrói do zero, e mandá-lo mexer no que já
110
+ existe é pagar oito agentes para refazer o que estava pronto. */
111
+ nome: 'pipeline',
112
+ tools: ['construir_app'],
113
+ gatilho: /\b(fa[çc]a|fa[çc]er|faz|cria|crie|criar|monta|monte|montar|constr[óo]i|construa|construir|desenvolv[ae]|quero|preciso de)\b[^.!?]{0,24}\b(app|aplicativo|site|p[áa]gina|landing|jogo|game|ferramenta|dashboard|painel|calculadora|sistema|portf[óo]lio|loja|formul[áa]rio|cron[ôo]metro|agenda|lista de tarefas|to-?do)\b/i,
114
+ },
115
+ {
116
+ /* O ARQUIVO do vídeo. Quem pede "faz um vídeo" recebe o link e o
117
+ botão de baixar; a ferramenta que gera o mp4 sem clique só entra
118
+ quando o pedido fala do arquivo — é o que mantém o pedido de vídeo
119
+ dentro do teto do minuto (medido: ela custa ~180 tokens). */
120
+ nome: 'exportar',
121
+ tools: ['studio_exportar'],
122
+ gatilho: /\b(mp4|webm|baix[ae]r?|download|exporta\w*|salv[ae]r? (o )?v[íi]deo|arquivo (do|de) v[íi]deo|me manda o v[íi]deo|manda o arquivo|renderiza\w*)\b/i,
123
+ },
124
+ {
125
+ nome: 'delegar',
126
+ tools: ['spawn_agent'],
127
+ // Delegar só compensa em tarefa grande; em pedido curto é peso morto.
128
+ gatilho: /\b(v[áa]rios|v[áa]rias|todos os|todas as|em paralelo|projeto inteiro|refator|migra[çc][ãa]o|auditor)\b/i,
129
+ },
130
+ {
131
+ nome: 'terminal',
132
+ tools: ['run_in_new_terminal'],
133
+ gatilho: /\b(servidor|dev|watch|npm run|yarn|vite|next|rodar? em segundo plano|localhost|porta \d+)\b/i,
134
+ },
135
+ {
136
+ // Sem tool nova — só a seção de prompt com as regras de qualidade de
137
+ // código. Nasceu de reclamação real do dono: "ele faz coisas
138
+ // horríveis em relação a código". As regras custam ~200 tokens e só
139
+ // entram quando a tarefa É de código.
140
+ nome: 'codigo',
141
+ tools: [],
142
+ gatilho: /\b(c[óo]digo|app|site|jogo|fun[çc][ãa]o|bug|erro|script|html|css|javascript|typescript|python|node|api|backend|frontend|componente|refator|classe|m[óo]dulo|landing)\b/i,
143
+ },
144
+ ];
145
+
146
+ /**
147
+ * Quais grupos esta tarefa pode usar.
148
+ *
149
+ * Olha o pedido do usuário — não a conversa inteira: o que o modelo escreveu
150
+ * no meio do caminho não deve mudar o que ele pode usar depois. `forcar`
151
+ * existe para o que já foi decidido fora daqui (o esforço alto sempre pesquisa).
152
+ */
153
+ function gruposDe(texto, forcar = []) {
154
+ const t = String(texto || '');
155
+ const escolhidos = new Set(['nucleo', ...forcar]);
156
+ for (const g of GRUPOS) {
157
+ if (g.sempre) { escolhidos.add(g.nome); continue; }
158
+ if (g.gatilho && g.gatilho.test(t)) escolhidos.add(g.nome);
159
+ }
160
+ return escolhidos;
161
+ }
162
+
163
+ /** As ferramentas dos grupos escolhidos, na ordem original da lista. */
164
+ function escolherFerramentas(todas, texto, opts = {}) {
165
+ const grupos = gruposDe(texto, opts.forcar);
166
+ const permitidas = new Set();
167
+ for (const g of GRUPOS) {
168
+ if (grupos.has(g.nome)) for (const t of g.tools) permitidas.add(t);
169
+ }
170
+ const escolhidas = (todas || []).filter((t) => permitidas.has(t.function && t.function.name));
171
+ // Rede de segurança: se a seleção falhar (nome novo que ninguém mapeou),
172
+ // é melhor mandar tudo do que mandar um agente sem ferramenta.
173
+ return escolhidas.length >= 5 ? escolhidas : todas;
174
+ }
175
+
176
+ /**
177
+ * O que a conversa mostra que o agente JÁ APRENDEU.
178
+ *
179
+ * A seção "O ESTÚDIO" do prompt ensina o que a spec entrega — e a spec, quando
180
+ * já foi lida, está inteira na conversa. Manter as duas é pagar duas vezes
181
+ * pelo mesmo ensinamento, e são 1.113 tokens que fazem falta justamente na
182
+ * hora de escrever o documento.
183
+ *
184
+ * O mesmo vale para pesquisa: com as fontes já lidas, o texto sobre COMO
185
+ * pesquisar não muda mais nada.
186
+ */
187
+ function jaAprendeu(messages) {
188
+ const nomePorId = new Map();
189
+ for (const m of messages || []) {
190
+ for (const tc of (m && m.tool_calls) || []) {
191
+ nomePorId.set(tc.id, (tc.function && tc.function.name) || tc.name);
192
+ }
193
+ }
194
+ const feitas = new Set();
195
+ for (const m of messages || []) {
196
+ if (m && m.role === 'tool' && nomePorId.has(m.tool_call_id)) feitas.add(nomePorId.get(m.tool_call_id));
197
+ }
198
+ const dispensaveis = new Set();
199
+ if (feitas.has('studio_spec') || feitas.has('studio_manual')) dispensaveis.add('estudio');
200
+ if (feitas.has('pesquisar_referencia')) dispensaveis.add('pesquisa');
201
+ return dispensaveis;
202
+ }
203
+
204
+ /**
205
+ * O prompt, montado só com as seções que a tarefa usa.
206
+ *
207
+ * `secoes` é { nome: texto }. O núcleo entra sempre; as outras entram quando o
208
+ * grupo correspondente entrou.
209
+ */
210
+ function montarPrompt(nucleo, secoes, texto, opts = {}) {
211
+ const grupos = gruposDe(texto, opts.forcar);
212
+ const dispensaveis = opts.jaAprendeu || new Set();
213
+ const partes = [nucleo];
214
+ for (const [nome, corpo] of Object.entries(secoes || {})) {
215
+ if (grupos.has(nome) && corpo && !dispensaveis.has(nome)) partes.push(corpo);
216
+ }
217
+ return partes.join('\n\n');
218
+ }
219
+
220
+ /**
221
+ * O relatório do que este pedido vai custar, para o servidor registrar e para
222
+ * o teste provar que a economia existe.
223
+ */
224
+ function relatorio({ prompt, ferramentas, messages }) {
225
+ const p = tokensDe(prompt);
226
+ const f = tokensDe(ferramentas);
227
+ const m = tokensDe(messages);
228
+ return { prompt: p, ferramentas: f, conversa: m, total: p + f + m, tools: (ferramentas || []).length };
229
+ }
230
+
231
+ /**
232
+ * Faz a CONVERSA caber no que sobrou do orçamento.
233
+ *
234
+ * A seleção de ferramentas resolve o custo fixo; isto resolve o variável. Numa
235
+ * tarefa de pesquisa o histórico cresce rápido (cada página lida são milhares
236
+ * de caracteres), e sem um teto ela volta a estourar no meio do trabalho —
237
+ * justamente depois de todo o esforço já gasto.
238
+ *
239
+ * Ordem das concessões, da menos dolorosa para a mais:
240
+ * 1. encurta os resultados de tool ANTIGOS (os novos são os que importam);
241
+ * 2. encurta mais ainda, se preciso;
242
+ * 3. em último caso, descarta as rodadas mais VELHAS — sempre o par
243
+ * inteiro (o turno do assistente com tool_calls + as respostas dele),
244
+ * porque a API recusa um role:'tool' órfão.
245
+ *
246
+ * O pedido do usuário e a última rodada nunca são descartados: sem eles o
247
+ * modelo não sabe mais o que estava fazendo.
248
+ */
249
+ // Tools cuja resposta é DOCUMENTO FIXO e que o agente precisa ter em mãos na
250
+ // hora de escrever. Encolher isto é o que criou o loop original: sem o formato,
251
+ // o modelo pede a spec de novo, e de novo.
252
+ //
253
+ // O CLI já protege as mesmas mensagens antes de mandar (lib/repeticao.js), mas
254
+ // quem falava por último aqui era `caberNoTeto`, que não sabia disso e cortava
255
+ // assim mesmo. Dois compactadores brigando, e o do servidor ganhava.
256
+ const REFERENCIAS = new Set(['studio_manual', 'studio_spec']);
257
+
258
+ // Referência tem teto GENEROSO, não infinito.
259
+ //
260
+ // Proteger a spec inteira parecia certo — sem o formato o agente não escreve o
261
+ // documento — mas a spec do `corte` são 3.270 tokens, e com o prompt e as 17
262
+ // tools isso dá 7.126: estoura o minuto SOZINHO, e nenhum corte na conversa
263
+ // resolve. Protegida demais, ela deixava de caber. O começo do documento é o
264
+ // formato (que o agente precisa); o fim são exemplos e opções avançadas.
265
+ const LIMITE_REFERENCIA = 3500;
266
+
267
+ /** Os índices de role:'tool' que não podem encolher: a cópia mais nova de cada referência. */
268
+ function referenciasProtegidas(messages) {
269
+ const nomePorId = new Map();
270
+ for (const m of messages || []) {
271
+ for (const tc of (m && m.tool_calls) || []) {
272
+ nomePorId.set(tc.id, (tc.function && tc.function.name) || tc.name);
273
+ }
274
+ }
275
+ const ultima = new Map();
276
+ (messages || []).forEach((m, i) => {
277
+ if (!m || m.role !== 'tool') return;
278
+ const nome = nomePorId.get(m.tool_call_id);
279
+ if (REFERENCIAS.has(nome)) ultima.set(nome, i);
280
+ });
281
+ return new Set(ultima.values());
282
+ }
283
+
284
+ function caberNoTeto(messages, sobra) {
285
+ const conta = (m) => tokensDe(m);
286
+ if (conta(messages) <= sobra) return { messages, cortou: false };
287
+
288
+ let atual = messages.map((m) => ({ ...m }));
289
+
290
+ // 1 e 2: encurtar resultados antigos, em dois apertos.
291
+ const protegidas = referenciasProtegidas(atual);
292
+ for (const limite of [400, 120]) {
293
+ const indices = atual.map((m, i) => (m.role === 'tool' ? i : -1)).filter((i) => i >= 0);
294
+ const manterInteiros = new Set(indices.slice(-2));
295
+ atual = atual.map((m, i) => {
296
+ const texto = String(m.content || '');
297
+ // Referência: teto próprio, muito maior que o dos outros — o
298
+ // formato vem no começo do documento.
299
+ if (protegidas.has(i)) {
300
+ if (texto.length <= LIMITE_REFERENCIA) return m;
301
+ return { ...m, content: texto.slice(0, LIMITE_REFERENCIA) + '…(spec cortada no limite do plano; o formato essencial está acima)' };
302
+ }
303
+ if (m.role !== 'tool' || manterInteiros.has(i)) return m;
304
+ if (texto.length <= limite) return m;
305
+ return { ...m, content: texto.slice(0, limite) + '…(encurtado para caber no limite do plano)' };
306
+ });
307
+ if (conta(atual) <= sobra) return { messages: atual, cortou: true, como: 'encurtou resultados antigos' };
308
+ }
309
+
310
+ // 3: descartar as rodadas mais velhas, em pares completos.
311
+ while (conta(atual) > sobra) {
312
+ const aindaProtegidas = referenciasProtegidas(atual);
313
+ const primeiroAssistente = atual.findIndex((m, i) => {
314
+ if (!(m.role === 'assistant' && m.tool_calls)) return false;
315
+ // Descartar a rodada que trouxe a spec é recriar o loop original:
316
+ // sem o formato o agente pede a spec de novo, e de novo.
317
+ let f = i + 1;
318
+ while (f < atual.length && atual[f].role === 'tool') { if (aindaProtegidas.has(f)) return false; f++; }
319
+ return true;
320
+ });
321
+ if (primeiroAssistente === -1) break;
322
+ let fim = primeiroAssistente + 1;
323
+ while (fim < atual.length && atual[fim].role === 'tool') fim++;
324
+ // Nunca engolir tudo: para se sobrar só o pedido do usuário.
325
+ if (fim >= atual.length - 1) break;
326
+ atual = [...atual.slice(0, primeiroAssistente), ...atual.slice(fim)];
327
+ }
328
+
329
+ return { messages: atual, cortou: true, como: 'descartou as rodadas mais antigas' };
330
+ }
331
+
332
+ module.exports = { GRUPOS, CHARS_POR_TOKEN, REFERENCIAS, LIMITE_REFERENCIA, tokensDe, enxugarSchema, referenciasProtegidas, jaAprendeu, gruposDe, escolherFerramentas, montarPrompt, relatorio, caberNoTeto };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * motor-cli.js — o Claude Code ou o Codex da pessoa como cérebro do PrimoCode.
3
+ *
4
+ * "Coloca o /claude de novo, mas dentro do /api: aí coloca /claude e
5
+ * /codex." (24/09/2026)
6
+ *
7
+ * Sem chave: usa a assinatura que a pessoa já tem nessas ferramentas. O
8
+ * agente (o loop, as ferramentas, a tela) continua sendo o do PrimoCode; a
9
+ * ferramenta de fora só decide o próximo passo, pelo protocolo de bloco
10
+ * marcado de lib/claude-engine.js (o Codex usa o mesmo).
11
+ *
12
+ * Os motores só sabem o `requestAct`. Chat e resumo, aqui, são uma pergunta
13
+ * de texto simples ao mesmo CLI; o plano não divide (uma ida a mais a um CLI
14
+ * lento custaria mais do que a divisão poupa) e segue em passo único.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const { execFileSync } = require('child_process');
20
+ const cerebro = require('./cerebro.json');
21
+
22
+ const SEM_APARECER = { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true };
23
+
24
+ function motorDe(id) {
25
+ if (id === 'codex') {
26
+ /* O padrão antigo do motor (gpt-5-mini) é recusado por conta de
27
+ assinatura do ChatGPT — medido: "o Codex não aceitou o modelo", e
28
+ uma segunda chamada em TODA rodada. Pelo /api, vai o modelo da
29
+ conta. Quem quiser outro ainda usa PRIMOCODE_CODEX_MODELO. */
30
+ if (process.env.PRIMOCODE_CODEX_MODELO === undefined) process.env.PRIMOCODE_CODEX_MODELO = '';
31
+ return require('../codex-engine.js');
32
+ }
33
+ return require('../claude-engine.js');
34
+ }
35
+
36
+ /** Uma pergunta de texto, resposta de texto. Sem protocolo, sem ferramenta. */
37
+ function perguntar(id, texto) {
38
+ if (id === 'codex') return String(motorDe('codex').chamarCodex(texto) || '').trim();
39
+ const claude = motorDe('claude-code');
40
+ const saida = execFileSync('claude', ['-p', '--output-format', 'json', ...claude.argsDoModelo()], {
41
+ encoding: 'utf-8', timeout: 120000, input: texto, maxBuffer: 8 * 1024 * 1024, ...SEM_APARECER,
42
+ });
43
+ try { return String(JSON.parse(saida).result || '').trim(); } catch { return String(saida || '').trim(); }
44
+ }
45
+
46
+ function criar(id) {
47
+ const motor = motorDe(id);
48
+ return {
49
+ requestAct: (servidor, payload, emit) => motor.requestAct(servidor, payload, emit),
50
+
51
+ async requestChat(_servidor, { messages, prompt, history } = {}, emit) {
52
+ const turnos = Array.isArray(messages) ? messages
53
+ : [...(Array.isArray(history) ? history : []), ...(prompt ? [{ role: 'user', content: prompt }] : [])];
54
+ const conversa = turnos.filter((m) => m && m.role && typeof m.content === 'string')
55
+ .map((m) => `${m.role === 'user' ? 'PESSOA' : 'VOCÊ'}: ${m.content}`).join('\n');
56
+ try {
57
+ const resposta = perguntar(id, `${cerebro.SYSTEM_CHAT}\n\nResponda só à última fala da PESSOA.\n\n${conversa}`);
58
+ if (resposta) emit({ delta: resposta });
59
+ } catch (e) {
60
+ emit({ erro: e.message });
61
+ }
62
+ emit({ done: true });
63
+ },
64
+
65
+ async requestResumo(_servidor, { resumo, mensagens } = {}) {
66
+ const novas = (mensagens || []).filter((m) => m && m.role);
67
+ if (!novas.length) return { resumo: String(resumo || '') };
68
+ const corpo = (resumo ? `RESUMO ANTERIOR:\n${resumo}\n\n` : '') + 'MENSAGENS NOVAS:\n'
69
+ + novas.map((m) => `${m.role === 'user' ? 'USUÁRIO' : m.role === 'assistant' ? 'AGENTE' : 'FERRAMENTA'}: `
70
+ + String(typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '')).slice(0, 1200)).join('\n');
71
+ try {
72
+ return { resumo: perguntar(id, `${cerebro.SYSTEM_RESUMO}\n\n${corpo}`) || String(resumo || '') };
73
+ } catch (e) {
74
+ return { resumo: String(resumo || ''), erro: e.message };
75
+ }
76
+ },
77
+
78
+ async requestPlano(_servidor, { prompt } = {}) {
79
+ return { passos: [{ titulo: 'tarefa', pedido: String(prompt || '').trim() }], dividiu: false };
80
+ },
81
+ };
82
+ }
83
+
84
+ /** A ferramenta está instalada? (sem rodar nada caro) */
85
+ function instalado(id) {
86
+ if (id === 'codex') return motorDe('codex').temCodex();
87
+ return motorDe('claude-code').temClaudeCode();
88
+ }
89
+
90
+ module.exports = { criar, instalado, perguntar };