primocode 8.0.1 → 8.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.
package/bin/primocode.js CHANGED
@@ -20,10 +20,23 @@ const api = require('../lib/api');
20
20
  const tools = require('../lib/tools');
21
21
  const cfg = require('../lib/config');
22
22
  const { runAct, interromper } = require('../lib/act');
23
+ const claudeEngine = require('../lib/claude-engine');
23
24
  const pasta = require('../lib/pasta');
24
25
  const catalogo = require('../lib/catalogo');
25
26
  const effort = require('../lib/effort');
26
27
 
28
+ // Rede de segurança de último recurso: sem isto, uma exceção fora de um
29
+ // try/catch (ex.: dentro de um callback de socket, como o parser de SSE) não
30
+ // era pega por ninguém e derrubava o processo INTEIRO em silêncio — o usuário
31
+ // via a resposta parar no meio e cair de volta pro shell, sem mensagem
32
+ // nenhuma. Agora vira um erro visível, e o processo continua de pé.
33
+ process.on('uncaughtException', (e) => {
34
+ console.log('\n' + c.err(ui.friendlyError(e)));
35
+ });
36
+ process.on('unhandledRejection', (e) => {
37
+ console.log('\n' + c.err(ui.friendlyError(e instanceof Error ? e : new Error(String(e)))));
38
+ });
39
+
27
40
  // A pasta de onde o comando foi dado. Não é mais onde ele trabalha (o padrão é
28
41
  // ~/primocode), mas continua servindo de ponto de partida para achar pasta que
29
42
  // o usuário mencionar por nome relativo.
@@ -41,6 +54,7 @@ const state = {
41
54
  currentProject: null,
42
55
  workDir: null, // pasta onde o agente trabalha nesta sessão
43
56
  workDirOrigem: null, // 'padrao' | '/dir' | 'detectada' | 'projeto' — para o /dir explicar
57
+ engine: 'server', // 'server' (OpenRouter/Groq) | 'claude-code' (/claude)
44
58
  };
45
59
 
46
60
  const HELP = `
@@ -49,6 +63,7 @@ ${c.bold('Comandos')}
49
63
  ${c.brand('/project')} lista projetos ou cria em ~/primocode: /project <nome>
50
64
  ${c.brand('/mode')} /mode primocode|chat|auto
51
65
  ${c.brand('/effort')} esforço do agente — do rápido ao mais esperto
66
+ ${c.brand('/claude')} liga o Claude Code local como motor (sem API paga) · /claude off volta ao padrão
52
67
  ${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
53
68
  ${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
54
69
  ${c.brand('/tudo')} lista tudo que ele sabe fazer
@@ -87,6 +102,7 @@ const SLASH_COMANDOS = [
87
102
  ['/dir', 'trabalhar numa pasta sua'],
88
103
  ['/mode', 'primocode | chat | auto'],
89
104
  ['/effort', 'esforço: low → ultracode (←/→ para ajustar)'],
105
+ ['/claude', 'liga o Claude Code local como motor · /claude off volta ao padrão'],
90
106
  ['/attach', 'anexa arquivo ao pedido'],
91
107
  ['/detach', 'remove os anexos'],
92
108
  ['/files', 'lista os anexos'],
@@ -522,6 +538,7 @@ async function sendPrompt(userText) {
522
538
  // Só chamado se o agente for mesmo MEXER EM ARQUIVO — abrir o Chrome
523
539
  // não é motivo para criar pasta de projeto.
524
540
  ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
541
+ motor: state.engine === 'claude-code' ? claudeEngine : api,
525
542
  });
526
543
  state.history.push({ role: 'user', content: userText });
527
544
  state.history.push({ role: 'assistant', content: r.assistantMessage.content });
@@ -670,6 +687,27 @@ async function handleSlash(text) {
670
687
  break;
671
688
  }
672
689
 
690
+ case 'claude': {
691
+ if (rest.toLowerCase() === 'off') {
692
+ if (state.engine !== 'claude-code') { console.log(c.muted(' Já estava no motor padrão.')); break; }
693
+ state.engine = 'server';
694
+ console.log(c.ok(' Motor: OpenRouter/Groq (padrão)'));
695
+ break;
696
+ }
697
+ console.log(c.dim(' Checando Claude Code e ORCA…'));
698
+ const r = await claudeEngine.garantirEngine();
699
+ if (!r.ok) {
700
+ console.log(c.err(` ${r.error}`));
701
+ if (r.comoResolver) console.log(c.muted(` ${r.comoResolver}`));
702
+ break;
703
+ }
704
+ state.engine = 'claude-code';
705
+ const onde = r.caminho === 'A' ? 'sessão contínua via ORCA' : 'chamada avulsa (claude -p, sem ORCA)';
706
+ console.log(c.ok(` Motor: Claude Code (${onde})`));
707
+ console.log(c.muted(' /claude off volta pro motor padrão.'));
708
+ break;
709
+ }
710
+
673
711
  // O /model virou /effort: você escolhe o ESFORÇO num slider (←/→,
674
712
  // Enter confirma, Esc cancela) e o modelo sai daí. Com argumento
675
713
  // ("/effort high") aplica direto, para quem já sabe o que quer.
package/lib/act.js CHANGED
@@ -165,7 +165,7 @@ function buildMemoryContext(projectDir, projectName) {
165
165
  const geral = cfg.readGeneralMemory();
166
166
  if (geral.trim()) parts.push(`[memory-geral.md]\n${geral}`);
167
167
  const projeto = cfg.readProjectMemory(projectDir);
168
- if (projeto.trim()) parts.push(`[${projectName}/memory.md]\n${projeto}`);
168
+ if (projeto.trim()) parts.push(`[${projectName || 'projeto'}/memory.md]\n${projeto}`);
169
169
  return parts.join('\n\n');
170
170
  }
171
171
 
@@ -173,6 +173,7 @@ async function runAct(userPrompt, opts) {
173
173
  const {
174
174
  server, model, token, history, projectDir, projectName,
175
175
  ensureWorkDir, profundidade = 0, subagent = false, effort,
176
+ motor = api,
176
177
  } = opts || {};
177
178
 
178
179
  // A pasta de projeto só nasce quando o agente for mesmo MEXER EM ARQUIVO.
@@ -209,7 +210,7 @@ async function runAct(userPrompt, opts) {
209
210
  let streaming = false;
210
211
 
211
212
  try {
212
- await api.requestAct(server, { messages, model, effort, token, projectDir, projectName, subagent }, (evt) => {
213
+ await motor.requestAct(server, { messages, model, effort, token, projectDir, projectName, subagent }, (evt) => {
213
214
  if (evt.erro) { streamError = evt.erro; return; }
214
215
  if (evt.tool_call) { toolCalls.push(evt.tool_call); return; }
215
216
  if (evt.delta) {
@@ -282,7 +283,7 @@ async function runAct(userPrompt, opts) {
282
283
 
283
284
  if (tc.name === 'spawn_agent') {
284
285
  const r = await runSubagent(tc.arguments || {}, {
285
- server, model, token, projectDir: pastaAtual, projectName, profundidade,
286
+ server, model, token, projectDir: pastaAtual, projectName, profundidade, motor,
286
287
  });
287
288
  messages.push({ role: 'tool', tool_call_id: tc.id, content: r });
288
289
  continue;
@@ -342,7 +343,12 @@ async function runAct(userPrompt, opts) {
342
343
  // Nada de arquivo mexido = nada para lembrar. E grava em `pastaAtual`, não em
343
344
  // `projectDir`: quando a pasta do projeto nasceu no meio do caminho, a memória
344
345
  // precisa ir para ela, não para a raiz onde a sessão começou.
345
- if (!subagent && projectName && temPasta) {
346
+ //
347
+ // `projectName` NÃO entra na condição: ele só rotula a memória no prompt
348
+ // (buildMemoryContext), e sessão sem projeto nomeado (ex.: primeiro pedido
349
+ // antes de /project) tem `projectName` undefined mesmo já tendo mexido em
350
+ // arquivo de verdade — a memória era descartada nesse caso, sempre.
351
+ if (!subagent && temPasta) {
346
352
  cfg.appendProjectMemory(pastaAtual, `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
347
353
  }
348
354
 
@@ -353,7 +359,7 @@ async function runAct(userPrompt, opts) {
353
359
  }
354
360
 
355
361
  async function runSubagent(args, ctx) {
356
- const { server, model, token, projectDir, projectName, profundidade } = ctx;
362
+ const { server, model, token, projectDir, projectName, profundidade, motor } = ctx;
357
363
  const descricao = args.description || 'sub-tarefa';
358
364
 
359
365
  if (profundidade + 1 >= MAX_PROFUNDIDADE) {
@@ -368,6 +374,7 @@ async function runSubagent(args, ctx) {
368
374
  history: [],
369
375
  profundidade: profundidade + 1,
370
376
  subagent: true,
377
+ motor,
371
378
  });
372
379
 
373
380
  const resumo = r.assistantMessage.content || 'sem resultado';
package/lib/api.js CHANGED
@@ -90,7 +90,19 @@ function requestStream(serverUrl, endpoint, payload, onEvent, token) {
90
90
  if (!line.startsWith('data: ')) continue;
91
91
  let evt;
92
92
  try { evt = JSON.parse(line.slice(6)); } catch { continue; }
93
- onEvent(evt);
93
+ // onEvent roda dentro de um callback de socket, fora de
94
+ // qualquer try/catch de quem chamou requestStream. Uma
95
+ // exceção aqui não seria pega por ninguém — sem process.on
96
+ // ('uncaughtException') no CLI, isso derrubava o processo
97
+ // inteiro em silêncio (era o "cai fora" sem mensagem
98
+ // nenhuma). Agora vira rejeição normal da Promise.
99
+ try {
100
+ onEvent(evt);
101
+ } catch (e) {
102
+ settled = true;
103
+ reject(e);
104
+ return;
105
+ }
94
106
  if (evt.done) finish();
95
107
  }
96
108
  });
package/lib/catalogo.js CHANGED
@@ -20,80 +20,121 @@ const GRUPOS = [
20
20
  nome: 'Arquivos',
21
21
  resumo: 'ler, criar e mudar arquivo na pasta de trabalho',
22
22
  itens: [
23
- { tool: 'write_file', escreve: true, faz: 'cria arquivo, ou reescreve inteiro' },
24
- { tool: 'edit_file', escreve: true, faz: 'troca um trecho exato, sem reescrever o resto' },
25
- { tool: 'multi_edit', escreve: true, faz: 'várias trocas no mesmo arquivo, tudo ou nada' },
26
- { tool: 'read_file', escreve: false, faz: 'lê arquivo, inteiro ou por pedaço' },
27
- { tool: 'list_dir', escreve: false, faz: 'mostra a árvore de pastas' },
28
- { tool: 'glob', escreve: false, faz: 'acha arquivo pelo nome (**/*.js)' },
29
- { tool: 'grep', escreve: false, faz: 'acha texto dentro dos arquivos' },
23
+ { tool: 'write_file', escreve: true, faz: 'cria arquivo, ou reescreve inteiro',
24
+ args: 'path: caminho relativo, ex "src/app.js". content: conteúdo completo do arquivo.' },
25
+ { tool: 'edit_file', escreve: true, faz: 'troca um trecho exato, sem reescrever o resto',
26
+ args: 'path. old_string: trecho exato a substituir, precisa ser único no arquivo. new_string: texto que entra no lugar. replace_all (opcional, bool, padrão false): troca todas as ocorrências.' },
27
+ { tool: 'multi_edit', escreve: true, faz: 'várias trocas no mesmo arquivo, tudo ou nada',
28
+ args: 'path. edits: lista de {old_string, new_string, replace_all (opcional)}, aplicadas em ordem.' },
29
+ { tool: 'read_file', escreve: false, faz: 'lê arquivo, inteiro ou por pedaço',
30
+ args: 'path. offset (opcional, número): primeira linha a ler. limit (opcional, número): quantas linhas.' },
31
+ { tool: 'list_dir', escreve: false, faz: 'mostra a árvore de pastas',
32
+ args: 'path (opcional, padrão "."). depth (opcional, número, padrão 2).' },
33
+ { tool: 'glob', escreve: false, faz: 'acha arquivo pelo nome (**/*.js)',
34
+ args: 'pattern: padrão de nome, ex "**/*.js" ou "src/**/*.{ts,tsx}".' },
35
+ { tool: 'grep', escreve: false, faz: 'acha texto dentro dos arquivos',
36
+ args: 'pattern: expressão regular. glob (opcional): restringe aos arquivos que casam. ignoreCase (opcional, bool). contexto (opcional, número): linhas ao redor do achado.' },
30
37
  ],
31
38
  },
32
39
  {
33
40
  nome: 'Terminal',
34
41
  resumo: 'rodar comando na sua máquina',
35
42
  itens: [
36
- { tool: 'run_shell', escreve: false, faz: 'roda comando e devolve a saída' },
37
- { tool: 'run_in_new_terminal', escreve: false, faz: 'abre outro terminal para o que fica de pé (servidor, watch)' },
43
+ { tool: 'run_shell', escreve: false, faz: 'roda comando e devolve a saída',
44
+ args: 'command. timeout_ms (opcional, número, padrão 60000).' },
45
+ { tool: 'run_in_new_terminal', escreve: false, faz: 'abre outro terminal para o que fica de pé (servidor, watch)',
46
+ args: 'command. cwd (opcional): pasta onde rodar.' },
38
47
  ],
39
48
  },
40
49
  {
41
50
  nome: 'Navegador',
42
51
  resumo: 'controla um navegador de verdade (Chrome, Edge, Brave, Arc…) — clica, digita, lê, fotografa',
43
52
  itens: [
44
- { tool: 'browser_open', escreve: false, faz: 'abre uma página ou um arquivo seu' },
45
- { tool: 'browser_click', escreve: false, faz: 'clica por texto, por seletor ou por coordenada' },
46
- { tool: 'browser_type', escreve: false, faz: 'digita num campo' },
47
- { tool: 'browser_key', escreve: false, faz: 'aperta tecla (Enter, Tab, Escape)' },
48
- { tool: 'browser_read', escreve: false, faz: 'lê o texto da página para conferir o resultado' },
49
- { tool: 'browser_screenshot', escreve: true, faz: 'tira foto da página' },
50
- { tool: 'browser_scroll', escreve: false, faz: 'rola a página' },
51
- { tool: 'browser_wait', escreve: false, faz: 'espera um elemento aparecer' },
52
- { tool: 'browser_eval', escreve: false, faz: 'roda JavaScript na página' },
53
- { tool: 'open_in_browser', escreve: false, faz: 'abre no seu navegador normal, sem controlar' },
53
+ { tool: 'browser_open', escreve: false, faz: 'abre uma página ou um arquivo seu',
54
+ args: 'url: http(s) ou caminho de arquivo do projeto.' },
55
+ { tool: 'browser_click', escreve: false, faz: 'clica por texto, por seletor ou por coordenada',
56
+ args: 'text (opcional): texto visível do elemento. selector (opcional): seletor CSS. x, y (opcional, número): último recurso.' },
57
+ { tool: 'browser_type', escreve: false, faz: 'digita num campo',
58
+ args: 'text. selector (opcional): foca esse campo antes de digitar. clear (opcional, bool, padrão true).' },
59
+ { tool: 'browser_key', escreve: false, faz: 'aperta tecla (Enter, Tab, Escape)',
60
+ args: 'key: nome da tecla (Enter, Tab, Escape, ArrowDown, ArrowUp, ArrowLeft, ArrowRight, Backspace).' },
61
+ { tool: 'browser_read', escreve: false, faz: 'lê o texto da página para conferir o resultado',
62
+ args: 'selector (opcional): lê só esse elemento; sem isso, a página inteira.' },
63
+ { tool: 'browser_screenshot', escreve: true, faz: 'tira foto da página',
64
+ args: 'path (opcional): onde salvar, ex "print.png". Sem path, usa um nome padrão.' },
65
+ { tool: 'browser_scroll', escreve: false, faz: 'rola a página',
66
+ args: 'dy (opcional, número, padrão 600).' },
67
+ { tool: 'browser_wait', escreve: false, faz: 'espera um elemento aparecer',
68
+ args: 'selector: seletor CSS a esperar. timeout_ms (opcional, número, padrão 15000).' },
69
+ { tool: 'browser_eval', escreve: false, faz: 'roda JavaScript na página',
70
+ args: 'js: trecho de JavaScript a rodar na página, devolve o resultado.' },
71
+ { tool: 'open_in_browser', escreve: false, faz: 'abre no seu navegador normal, sem controlar',
72
+ args: 'target: arquivo do projeto ou URL.' },
54
73
  ],
55
74
  },
56
75
  {
57
76
  nome: 'Estúdio',
58
77
  resumo: 'a suíte de criação do PrimoCode — sobe sozinha, não precisa de app externo',
59
78
  itens: [
60
- { tool: 'studio_manual', escreve: false, faz: 'lê o manual do estúdio e as seis ferramentas' },
61
- { tool: 'studio_spec', escreve: false, faz: 'lê a spec de uma ferramenta (prisma, tela, prosa…)' },
62
- { tool: 'studio_criar', escreve: false, faz: 'cria slides, arte, documento, planilha, vídeo ou diagrama' },
63
- { tool: 'studio_ler', escreve: false, faz: 'lê algo que já foi criado' },
64
- { tool: 'studio_atualizar', escreve: false, faz: 'ajusta o que já existe, sem refazer do zero' },
65
- { tool: 'studio_listar', escreve: false, faz: 'lista o que já existe no estúdio' },
79
+ { tool: 'studio_manual', escreve: false, faz: 'lê o manual do estúdio e as seis ferramentas', args: null },
80
+ { tool: 'studio_spec', escreve: false, faz: 'lê a spec de uma ferramenta (prisma, tela, prosa…)',
81
+ args: 'ferramenta: prisma | tela | prosa | grade | corte | traco.' },
82
+ { tool: 'studio_criar', escreve: false, faz: 'cria slides, arte, documento, planilha, vídeo ou diagrama',
83
+ args: 'ferramenta: prisma | tela | prosa | grade | corte | traco. titulo. doc: objeto no formato que studio_spec daquela ferramenta descreve.' },
84
+ { tool: 'studio_ler', escreve: false, faz: 'lê algo que já foi criado', args: 'id.' },
85
+ { tool: 'studio_atualizar', escreve: false, faz: 'ajusta o que já existe, sem refazer do zero',
86
+ args: 'id. titulo (opcional). doc (opcional).' },
87
+ { tool: 'studio_listar', escreve: false, faz: 'lista o que já existe no estúdio', args: null },
66
88
  ],
67
89
  },
68
90
  {
69
91
  nome: 'Computador',
70
92
  resumo: 'janelas, botões, mouse e teclado — age em qualquer programa e na área de trabalho',
71
93
  itens: [
72
- { tool: 'desktop_open_app', escreve: false, faz: 'abre um programa pelo nome (Chrome, Bloco de Notas…)' },
73
- { tool: 'desktop_windows', escreve: false, faz: 'lista as janelas abertas, com título' },
74
- { tool: 'desktop_focus', escreve: false, faz: 'traz uma janela para a frente, pelo título' },
75
- { tool: 'desktop_elements', escreve: false, faz: 'lê os botões, campos e ícones da janela, com posição' },
76
- { tool: 'desktop_click_element', escreve: false, faz: 'clica num botão ou ícone pelo NOME, sem coordenada' },
77
- { tool: 'desktop_click', escreve: false, faz: 'clica na tela por coordenada' },
78
- { tool: 'desktop_double_click', escreve: false, faz: 'clique duplo — abre ícone da área de trabalho' },
79
- { tool: 'desktop_move', escreve: false, faz: 'move o mouse' },
80
- { tool: 'desktop_drag', escreve: false, faz: 'arrasta de um ponto a outro' },
81
- { tool: 'desktop_type', escreve: false, faz: 'digita no programa que está na frente' },
82
- { tool: 'desktop_key', escreve: false, faz: 'aperta tecla ou atalho (Enter, ctrl+c, alt+tab)' },
83
- { tool: 'desktop_scroll', escreve: false, faz: 'rola a tela' },
84
- { tool: 'desktop_size', escreve: false, faz: 'mede a tela, para se orientar antes de clicar' },
85
- { tool: 'desktop_screenshot', escreve: true, faz: 'tira foto da sua tela' },
94
+ { tool: 'desktop_open_app', escreve: false, faz: 'abre um programa pelo nome (Chrome, Bloco de Notas…)',
95
+ args: 'app: nome do programa, como o usuário fala.' },
96
+ { tool: 'desktop_windows', escreve: false, faz: 'lista as janelas abertas, com título', args: null },
97
+ { tool: 'desktop_focus', escreve: false, faz: 'traz uma janela para a frente, pelo título',
98
+ args: 'window: título (ou pedaço único) da janela.' },
99
+ { tool: 'desktop_elements', escreve: false, faz: 'lê os botões, campos e ícones da janela, com posição',
100
+ args: 'window (opcional): título da janela alvo; sem isso, a janela em foco. No Windows, "Program Manager" é a área de trabalho.' },
101
+ { tool: 'desktop_click_element', escreve: false, faz: 'clica num botão ou ícone pelo NOME, sem coordenada',
102
+ args: 'name: nome (ou pedaço) do botão/campo/ícone. window (opcional). double (opcional, bool, padrão false): clique duplo.' },
103
+ { tool: 'desktop_click', escreve: false, faz: 'clica na tela por coordenada',
104
+ args: 'x, y (número). button (opcional: left|right, padrão left).' },
105
+ { tool: 'desktop_double_click', escreve: false, faz: 'clique duplo — abre ícone da área de trabalho',
106
+ args: 'x, y (número).' },
107
+ { tool: 'desktop_move', escreve: false, faz: 'move o mouse', args: 'x, y (número).' },
108
+ { tool: 'desktop_drag', escreve: false, faz: 'arrasta de um ponto a outro',
109
+ args: 'from_x, from_y, to_x, to_y (número).' },
110
+ { tool: 'desktop_type', escreve: false, faz: 'digita no programa que está na frente', args: 'text.' },
111
+ { tool: 'desktop_key', escreve: false, faz: 'aperta tecla ou atalho (Enter, ctrl+c, alt+tab)',
112
+ args: 'key: tecla ou atalho, ex "Enter", "ctrl+c", "ctrl+shift+t", "alt+tab", "win".' },
113
+ { tool: 'desktop_scroll', escreve: false, faz: 'rola a tela', args: 'dy (opcional, número, padrão 300).' },
114
+ { tool: 'desktop_size', escreve: false, faz: 'mede a tela, para se orientar antes de clicar', args: null },
115
+ { tool: 'desktop_screenshot', escreve: true, faz: 'tira foto da sua tela',
116
+ args: 'path (opcional): onde salvar. Sem path, usa um nome padrão.' },
86
117
  ],
87
118
  },
88
119
  {
89
120
  nome: 'Organização',
90
121
  resumo: 'como ele se organiza numa tarefa grande',
91
122
  itens: [
92
- { tool: 'write_todos', escreve: false, faz: 'monta o plano e vai marcando o que terminou' },
93
- { tool: 'pesquisar_referencia', escreve: false, faz: 'lê docs e exemplos antes de escrever — sobe a qualidade' },
123
+ { tool: 'write_todos', escreve: false, faz: 'monta o plano e vai marcando o que terminou',
124
+ args: 'todos: lista de {content: a tarefa curta no imperativo, status: pending|in_progress|completed}. Reescreve a lista inteira a cada chamada; só uma "in_progress" por vez.' },
125
+ { tool: 'pesquisar_referencia', escreve: false, faz: 'lê docs e exemplos antes de escrever — sobe a qualidade',
126
+ args: 'consulta: o que pesquisar, de preferência em inglês. paginas (opcional, número, padrão 3, máximo 5).' },
127
+ // Vai para memory-geral.md, FORA da pasta do projeto — por isso não
128
+ // é write_file/edit_file (a sandbox de resolveInProject bloqueia
129
+ // qualquer caminho fora do projeto, e é assim mesmo que tem que
130
+ // ser para os outros casos). Esta tool é a única porta pra lá.
131
+ { tool: 'remember_general', escreve: false, faz: 'grava algo sobre o usuário que vale para todo projeto (memory-geral.md)',
132
+ args: 'fato: o que lembrar, em uma frase.' },
94
133
  // Estas duas o loop do agente atende (lib/act.js), não o tools.execute.
95
- { tool: 'spawn_agent', escreve: false, faz: 'chama um subagente para uma parte da tarefa', noLoop: true },
96
- { tool: 'finish', escreve: false, faz: 'encerra dizendo o que fez', noLoop: true },
134
+ { tool: 'spawn_agent', escreve: false, faz: 'chama um subagente para uma parte da tarefa', noLoop: true,
135
+ args: 'description: 3 a 5 palavras, aparece na tela do usuário. prompt: instrução completa e autossuficiente — o subagente não vê esta conversa.' },
136
+ { tool: 'finish', escreve: false, faz: 'encerra dizendo o que fez', noLoop: true,
137
+ args: 'summary (opcional): uma frase do que foi feito.' },
97
138
  ],
98
139
  },
99
140
  ];
@@ -0,0 +1,354 @@
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
+ const MARCA_INICIO = '<<<PRIMOCODE>>>';
18
+ const MARCA_FIM = '<<<FIM>>>';
19
+
20
+ /**
21
+ * Acha o ÚLTIMO bloco marcado no texto (pode vir ruído de terminal — prompt
22
+ * ANSI, texto solto — antes e depois) e devolve o JSON de dentro dele.
23
+ * null quando não achou bloco válido: quem chama decide o que fazer.
24
+ */
25
+ function extrairDecisao(texto) {
26
+ const t = String(texto || '');
27
+ const inicio = t.lastIndexOf(MARCA_INICIO);
28
+ if (inicio === -1) return null;
29
+ const fim = t.indexOf(MARCA_FIM, inicio);
30
+ if (fim === -1) return null;
31
+ const bloco = t.slice(inicio + MARCA_INICIO.length, fim).trim();
32
+ try {
33
+ return JSON.parse(bloco);
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ const catalogo = require('./catalogo');
40
+
41
+ /** Serializa lib/catalogo.js em texto — única fonte de verdade, sem
42
+ * duplicar a lista de ferramentas num arquivo separado. */
43
+ function catalogoParaTexto() {
44
+ const linhas = [];
45
+ for (const grupo of catalogo.GRUPOS) {
46
+ linhas.push(`## ${grupo.nome} — ${grupo.resumo}`);
47
+ for (const item of grupo.itens) {
48
+ const args = item.args ? ` | args: ${item.args}` : ' | sem args';
49
+ linhas.push(`- ${item.tool}: ${item.faz}${args}`);
50
+ }
51
+ linhas.push('');
52
+ }
53
+ return linhas.join('\n').trim();
54
+ }
55
+
56
+ /** Texto completo que ensina o Claude Code a se comportar como motor do
57
+ * PrimoCode. Vira o conteúdo de ~/.claude/skills/primocode-engine/SKILL.md
58
+ * (Task 4) e é referenciado na primeira mensagem de cada sessão. */
59
+ function textoBootstrap() {
60
+ return `---
61
+ name: primocode-engine
62
+ description: Skill instalada pelo PrimoCode para o modo /claude — motor de decisão do agente, não um agente autônomo.
63
+ ---
64
+
65
+ # PrimoCode — modo motor
66
+
67
+ Você está sendo usado como o "cérebro" do PrimoCode, um CLI que executa
68
+ tarefas de engenharia. Não use suas próprias ferramentas nativas (Read,
69
+ Write, Edit, Bash, Glob, Grep, WebFetch, etc.) — quem executa é o
70
+ PrimoCode, não você. Sua única saída é dizer QUAL ferramenta do PrimoCode
71
+ chamar, com quais argumentos, ou a resposta final.
72
+
73
+ ## Ferramentas disponíveis
74
+
75
+ ${catalogoParaTexto()}
76
+
77
+ ## Formato de resposta — obrigatório
78
+
79
+ Termine toda resposta com exatamente um bloco, sem nada depois dele.
80
+
81
+ Para chamar uma ferramenta:
82
+
83
+ ${MARCA_INICIO}
84
+ {"tool_call": {"name": "write_file", "arguments": {"path": "index.html", "content": "..."}}}
85
+ ${MARCA_FIM}
86
+
87
+ Para terminar a tarefa (sem mais ferramenta pra chamar):
88
+
89
+ ${MARCA_INICIO}
90
+ {"assistant_message": "frase curta do que foi feito"}
91
+ ${MARCA_FIM}
92
+
93
+ Nunca omita o bloco. Nunca escreva mais de um bloco por resposta.`;
94
+ }
95
+
96
+ const SKILL_DIR = () => path.join(os.homedir(), '.claude', 'skills', 'primocode-engine');
97
+ const SKILL_PATH = () => path.join(SKILL_DIR(), 'SKILL.md');
98
+
99
+ /** Grava (ou regrava) a skill em ~/.claude/skills/primocode-engine/SKILL.md.
100
+ * Sempre reescreve com o texto atual — barato, e garante que uma versão
101
+ * nova do PrimoCode (com ferramenta nova em catalogo.js) atualiza a skill
102
+ * sozinha na próxima vez que /claude ligar. */
103
+ function garantirSkill() {
104
+ const dir = SKILL_DIR();
105
+ fs.mkdirSync(dir, { recursive: true });
106
+ const caminho = SKILL_PATH();
107
+ fs.writeFileSync(caminho, textoBootstrap(), 'utf-8');
108
+ return { ok: true, caminho };
109
+ }
110
+
111
+ /** executor(cmd) roda um comando e devolve a saída, ou lança. Default:
112
+ * execSync de verdade. Testes injetam uma função fake — sem mock framework. */
113
+ function temClaudeCode(executor = (cmd) => execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] })) {
114
+ try { executor('claude --version'); return true; }
115
+ catch { return false; }
116
+ }
117
+
118
+ function temOrca(executor = (cmd) => execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] })) {
119
+ try { executor('orca --version'); return true; }
120
+ catch { return false; }
121
+ }
122
+
123
+ /** executor aqui devolve TEXTO (o JSON cru de `orca status --json`), não
124
+ * só "roda ou lança" — por isso um default diferente do de cima. */
125
+ function orcaAlcancavel(executor = (cmd) => execSync(cmd, { encoding: 'utf-8' })) {
126
+ try {
127
+ const saida = executor('orca status --json');
128
+ const j = JSON.parse(saida);
129
+ return !!(j && j.result && j.result.runtime && j.result.runtime.reachable);
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+
135
+ /** Função pura — nenhum I/O aqui, só decide. */
136
+ function escolherCaminho({ temClaude, temOrcaCli, orcaOk }) {
137
+ if (!temClaude) return 'sem-claude';
138
+ if (temOrcaCli && orcaOk) return 'A';
139
+ return 'B';
140
+ }
141
+
142
+ // Handle do terminal ORCA desta execução do processo — sessão contínua
143
+ // enquanto o CLI ficar de pé. Cursor de leitura por handle, pra "terminal
144
+ // read --cursor" trazer só o que é novo desde o turno anterior.
145
+ let sessao = { handle: null, cursor: '0', bootstrapEnviado: false };
146
+
147
+ function rodarOrca(args, opts = {}) {
148
+ return execFileSync('orca', args, { encoding: 'utf-8', timeout: opts.timeout || 15000 });
149
+ }
150
+
151
+ let contadorId = 0;
152
+ /** Id sintético pro tool_call — o bloco marcado do Claude Code não traz um
153
+ * (só existe UM tool_call por resposta, pela própria regra do bootstrap),
154
+ * mas lib/act.js precisa de tc.id pra parear com a mensagem role:'tool' que
155
+ * vem depois. Único dentro do processo já basta. */
156
+ function proximoId() {
157
+ contadorId += 1;
158
+ return `claude-engine-${contadorId}`;
159
+ }
160
+
161
+ // Caminho A (sessão contínua via ORCA) tem um problema arquitetural real,
162
+ // achado rodando de verdade na verificação manual (Task 9 do plano): o
163
+ // Claude Code interativo desenha a TUI em tela alternada, e
164
+ // `orca terminal read` (scrollback linha a linha) nunca enxerga esse
165
+ // conteúdo — o tail fica preso no eco do comando de lançamento. `orca
166
+ // terminal show`'s preview mostra a tela renderizada de verdade, mas é só
167
+ // um recorte das últimas linhas visíveis, sem garantia de conter o bloco
168
+ // marcado inteiro numa resposta mais longa que o painel. Até esse
169
+ // mecanismo de leitura ser resolvido, o Caminho B (testado de ponta a
170
+ // ponta, funciona) é o único confiável — força ele sempre, mesmo com ORCA
171
+ // disponível. O código do Caminho A fica pronto pra quando isso for
172
+ // resolvido, só não é escolhido.
173
+ const CAMINHO_A_HABILITADO = false;
174
+
175
+ async function garantirEngine() {
176
+ // execFileSync('claude', …) no caminho headless não resolve o shim
177
+ // .cmd/.bat do Windows sem shell:true — quebraria em EVERY turno, mesmo
178
+ // com a detecção acima (que usa execSync com shell) passando. Recusa
179
+ // antes de instalar a skill: honesto e seguro, em vez de "sucesso" que
180
+ // quebra na primeira mensagem real.
181
+ if (process.platform === 'win32') {
182
+ return {
183
+ ok: false,
184
+ error: 'O motor Claude Code ainda não funciona no Windows.',
185
+ comoResolver: 'Aguarde uma próxima versão, ou use o motor padrão (OpenRouter/Groq).',
186
+ };
187
+ }
188
+ if (!temClaudeCode()) {
189
+ return {
190
+ ok: false,
191
+ error: 'Claude Code não está instalado nesta máquina.',
192
+ comoResolver: 'Instale o Claude Code (veja https://docs.claude.com/claude-code) e rode /claude de novo.',
193
+ };
194
+ }
195
+ garantirSkill();
196
+ const orcaCli = CAMINHO_A_HABILITADO && temOrca();
197
+ const caminho = escolherCaminho({
198
+ temClaude: true,
199
+ temOrcaCli: orcaCli,
200
+ orcaOk: orcaCli ? orcaAlcancavel() : false,
201
+ });
202
+ return { ok: true, caminho };
203
+ }
204
+
205
+ function abrirTerminalOrca() {
206
+ // --dangerously-skip-permissions aqui não dá poder novo ao Claude Code:
207
+ // a skill (textoBootstrap) já instrui ele a nunca usar suas próprias
208
+ // ferramentas nativas. A flag só pula os PRÓPRIOS prompts de permissão
209
+ // dele, que travariam a sessão sem ninguém pra responder.
210
+ const saida = rodarOrca(['terminal', 'create', '--command', 'claude --dangerously-skip-permissions', '--title', 'PrimoCode', '--json']);
211
+ const r = JSON.parse(saida);
212
+ // `orca terminal create --json` aninha o handle em result.terminal.handle,
213
+ // não em result.handle — confirmado rodando o comando de verdade (Task 9).
214
+ return r.result.terminal.handle;
215
+ }
216
+
217
+ function esperarIdleOrca(handle, timeoutMs) {
218
+ rodarOrca(['terminal', 'wait', '--terminal', handle, '--for', 'tui-idle', '--timeout-ms', String(timeoutMs)], { timeout: timeoutMs + 5000 });
219
+ }
220
+
221
+ function ultimaMensagem(messages) {
222
+ const m = (messages || [])[messages.length - 1];
223
+ return m && m.content ? String(m.content) : '';
224
+ }
225
+
226
+ /** Devolve a decisão pro onEvent — tool_call, ou assistant_message (com
227
+ * degradação pra texto puro quando o bloco marcado não veio, em vez de
228
+ * travar o turno em silêncio). */
229
+ function emitirDecisao(textoLido, onEvent) {
230
+ const decisao = extrairDecisao(textoLido);
231
+ if (!decisao) {
232
+ onEvent({ assistant_message: { content: String(textoLido || '').trim() } });
233
+ return;
234
+ }
235
+ if (decisao.tool_call) {
236
+ // lib/act.js usa tc.id pra parear a mensagem role:'tool' com este
237
+ // tool_call (messages.push({ tool_call_id: tc.id, ... })) — sem id
238
+ // aqui, esse pareamento quebra.
239
+ onEvent({ tool_call: { id: proximoId(), name: decisao.tool_call.name, arguments: decisao.tool_call.arguments || {} } });
240
+ }
241
+ else if (decisao.assistant_message) {
242
+ // O modelo pode confundir a forma com a do tool_call (que É um
243
+ // objeto) e emitir {"assistant_message": {"content": "..."}} em vez
244
+ // de string pura. Sem essa coerção, `content` vira um objeto
245
+ // aninhado e lib/act.js quebra com TypeError no `.trim()` — a
246
+ // travessia direta que este arquivo inteiro existe pra evitar.
247
+ const am = decisao.assistant_message;
248
+ const conteudo = typeof am === 'string'
249
+ ? am
250
+ : (am && typeof am.content === 'string' ? am.content : JSON.stringify(am));
251
+ onEvent({ assistant_message: { content: conteudo } });
252
+ }
253
+ else onEvent({ assistant_message: { content: JSON.stringify(decisao) } });
254
+ }
255
+
256
+ async function requestActViaOrca(payload, onEvent) {
257
+ if (!sessao.handle) {
258
+ try {
259
+ sessao.handle = abrirTerminalOrca();
260
+ sessao.cursor = '0';
261
+ sessao.bootstrapEnviado = false;
262
+ esperarIdleOrca(sessao.handle, 30000);
263
+ } catch (e) {
264
+ // Mesmo tratamento do try/catch abaixo: se o terminal não
265
+ // chegou a ficar pronto (timeout do tui-idle, por exemplo), o
266
+ // handle nunca foi confirmado usável — zera pra próxima
267
+ // chamada abrir um terminal novo do zero, em vez de deixar
268
+ // sessao.handle setado com algo que nunca funcionou.
269
+ sessao.handle = null;
270
+ throw e;
271
+ }
272
+ }
273
+ let texto = ultimaMensagem(payload.messages);
274
+ if (!sessao.bootstrapEnviado) {
275
+ texto = 'Use a skill primocode-engine para esta sessão inteira — ela está em ~/.claude/skills/primocode-engine/SKILL.md.\n\n' + texto;
276
+ sessao.bootstrapEnviado = true;
277
+ }
278
+ try {
279
+ rodarOrca(['terminal', 'send', '--terminal', sessao.handle, '--text', texto, '--enter']);
280
+ esperarIdleOrca(sessao.handle, 120000);
281
+ const lida = rodarOrca(['terminal', 'read', '--terminal', sessao.handle, '--cursor', sessao.cursor, '--json']);
282
+ const leitura = JSON.parse(lida);
283
+ // Mesma correção do handle: `terminal read --json` também aninha em
284
+ // result.terminal — e o texto vem como ARRAY de linhas (`tail`), não
285
+ // uma string única (`text`). Confirmado rodando de verdade (Task 9).
286
+ const t = leitura.result.terminal || {};
287
+ sessao.cursor = t.nextCursor != null ? String(t.nextCursor) : sessao.cursor;
288
+ emitirDecisao((t.tail || []).join('\n'), onEvent);
289
+ } catch (e) {
290
+ // Handle obsoleto (terminal fechado, ORCA reiniciou) — documentado
291
+ // pelo próprio `orca agent-context` como terminal_handle_stale. Zera
292
+ // a sessão: a PRÓXIMA chamada abre um terminal novo do zero. Esta
293
+ // chamada ainda falha (o usuário vê o erro e pode só pedir de novo).
294
+ sessao.handle = null;
295
+ throw e;
296
+ }
297
+ }
298
+
299
+ /** Sem sessão viva (caminho B), o histórico inteiro vira texto a cada
300
+ * chamada — mas `content` sozinho perde a mensagem quando é um turno de
301
+ * tool_calls (content vem null ali, o que importa está em tool_calls) ou
302
+ * uma resposta de ferramenta (role:'tool'). Reconstrói os dois casos em
303
+ * vez de deixar "assistant: null" no texto. */
304
+ function linhaDoHistorico(m) {
305
+ if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
306
+ const chamadas = m.tool_calls
307
+ .map((tc) => `chamou ${tc.function.name} com ${tc.function.arguments}`)
308
+ .join('; ');
309
+ return `assistant: ${chamadas}`;
310
+ }
311
+ if (m.role === 'tool') return `resultado da ferramenta (${m.tool_call_id}): ${m.content}`;
312
+ return `${m.role}: ${m.content}`;
313
+ }
314
+
315
+ async function requestActViaHeadless(payload, onEvent) {
316
+ const historico = (payload.messages || []).map(linhaDoHistorico).join('\n\n');
317
+ const prompt = 'Use a skill primocode-engine para esta chamada.\n\n' + historico;
318
+ // --dangerously-skip-permissions: mesma razão do caminho ORCA acima —
319
+ // a skill já proíbe o Claude Code de usar suas próprias ferramentas
320
+ // nativas; a flag só evita que ele pare pedindo permissão pra si mesmo
321
+ // numa chamada headless sem ninguém pra responder.
322
+ const saida = execFileSync('claude', [
323
+ '-p', prompt,
324
+ '--output-format', 'json',
325
+ '--dangerously-skip-permissions',
326
+ ], { encoding: 'utf-8', timeout: 180000 });
327
+ const r = JSON.parse(saida);
328
+ emitirDecisao(r.result || '', onEvent);
329
+ }
330
+
331
+ /** Mesma forma de api.requestAct(server, payload, onEvent) — `server` não
332
+ * é usado aqui (não existe servidor neste motor), fica só pra manter a
333
+ * assinatura uniforme com o outro lado da escolha em lib/act.js. */
334
+ async function requestAct(server, payload, onEvent) {
335
+ try {
336
+ const engineInfo = await garantirEngine();
337
+ if (!engineInfo.ok) {
338
+ onEvent({ erro: `${engineInfo.error} ${engineInfo.comoResolver || ''}`.trim() });
339
+ return;
340
+ }
341
+ if (engineInfo.caminho === 'A') await requestActViaOrca(payload, onEvent);
342
+ else await requestActViaHeadless(payload, onEvent);
343
+ } catch (e) {
344
+ onEvent({ erro: e.message });
345
+ }
346
+ }
347
+
348
+ module.exports = {
349
+ MARCA_INICIO, MARCA_FIM, extrairDecisao,
350
+ catalogoParaTexto, textoBootstrap,
351
+ SKILL_DIR, SKILL_PATH, garantirSkill,
352
+ temClaudeCode, temOrca, orcaAlcancavel, escolherCaminho,
353
+ garantirEngine, requestAct, emitirDecisao,
354
+ };
package/lib/config.js CHANGED
@@ -151,6 +151,18 @@ function appendProjectMemory(dir, entry) {
151
151
  } catch {}
152
152
  }
153
153
 
154
+ // A contraparte de appendProjectMemory, para o que vale em TODOS os projetos
155
+ // (nome do usuário, preferência, decisão de longo prazo). Existia a promessa
156
+ // no prompt — "escreve aqui o que descobrir sobre você" — mas nenhuma função
157
+ // gravava de fato: só ensureStructure() criava o template uma vez.
158
+ function appendGeneralMemory(entry) {
159
+ if (!entry) return;
160
+ try {
161
+ ensureStructure();
162
+ fs.appendFileSync(GENERAL_MEMORY, `\n### ${today()}\n${entry}\n`, 'utf-8');
163
+ } catch {}
164
+ }
165
+
154
166
  function loadConfig() {
155
167
  ensureStructure();
156
168
  let file = {};
@@ -198,5 +210,5 @@ module.exports = {
198
210
  CONFIG_DIR, CONFIG_FILE, HISTORY_FILE, ROOT_DIR, GENERAL_MEMORY,
199
211
  ensureStructure, slugify,
200
212
  createProjectDir, getProjectDir, listProjects, findSimilarProject,
201
- readGeneralMemory, readProjectMemory, appendProjectMemory,
213
+ readGeneralMemory, readProjectMemory, appendProjectMemory, appendGeneralMemory,
202
214
  };
package/lib/modo.js CHANGED
@@ -37,6 +37,13 @@ const VERBOS = [
37
37
  ];
38
38
  const RE_VERBO = new RegExp(`(^|\\s)(${VERBOS.join('|')})\\b`, 'i');
39
39
 
40
+ // Pedido sem verbo de comando, só desejo: "quero um jogo da velha", "preciso
41
+ // de uma landing page". Muito comum em português — e sem isto caía direto no
42
+ // "sem pista de tarefa" porque não tem 'criar'/'fazer' escrito. A diferença
43
+ // para RE_CONCEITUAL é que aqui não vem pergunta ("como", "por que", "o que
44
+ // é") — só o pedido do que a pessoa quer que exista.
45
+ const RE_DESEJO = /\b(quero|queria|querendo|gostaria de|necessito de|precis[ao] de|precisava de)\b/i;
46
+
40
47
  // Coisas que só existem quando há trabalho de arquivo/máquina envolvido.
41
48
  const RE_ARTEFATO = /\.(js|ts|tsx|jsx|html|css|json|md|py|sh|yml|yaml|txt|csv|env)\b|[\w-]+\/[\w-]/i;
42
49
  const RE_FERRAMENTA = /\b(navegador|browser|desktop|terminal|shell|git|npm|node|deploy|servidor|localhost|porta \d+|screenshot|print da tela|mouse|teclado|janela|[áa]rea de trabalho|aplicativo|[íi]cone)\b/i;
@@ -65,6 +72,7 @@ function escolherModo(texto) {
65
72
 
66
73
  if (RE_ARTEFATO.test(t)) return { modo: 'primocode', motivo: 'cita arquivo ou caminho' };
67
74
  if (RE_VERBO.test(t)) return { modo: 'primocode', motivo: 'pede uma ação' };
75
+ if (RE_DESEJO.test(t)) return { modo: 'primocode', motivo: 'expressa o que quer que exista' };
68
76
  if (RE_FERRAMENTA.test(t)) return { modo: 'primocode', motivo: 'menciona ferramenta ou máquina' };
69
77
  if (RE_PASTA.test(t)) return { modo: 'primocode', motivo: 'fala de arquivo ou projeto' };
70
78
 
package/lib/tools.js CHANGED
@@ -15,6 +15,7 @@ const browser = require('./browser');
15
15
  const desktop = require('./desktop');
16
16
  const referencia = require('./referencia');
17
17
  const studio = require('./studio');
18
+ const cfg = require('./config');
18
19
 
19
20
  const IGNORAR = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.cache', 'vendor']);
20
21
  const MAX_ARQUIVOS_VARREDURA = 5000;
@@ -399,6 +400,16 @@ async function despachar(name, args, ctx) {
399
400
  paginas: Math.max(1, Math.min(5, Number(args.paginas) || 3)),
400
401
  });
401
402
 
403
+ // memory-geral.md fica FORA da pasta do projeto — write_file/edit_file
404
+ // não alcançam por design (resolveInProject bloqueia ../ de propósito).
405
+ // Esta é a única porta para lá, sem reabrir a sandbox.
406
+ case 'remember_general': {
407
+ const entry = String(args.fato || args.entry || '').trim();
408
+ if (!entry) return { ok: false, error: 'Diga o que lembrar (fato).' };
409
+ cfg.appendGeneralMemory(entry);
410
+ return { ok: true, gravado: entry };
411
+ }
412
+
402
413
  // ── Navegador controlado (CDP) — o agente age numa página real ──
403
414
  case 'browser_open': {
404
415
  const alvo = /^https?:\/\//i.test(args.url || '')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.0.1",
3
+ "version": "8.1.0",
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": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/primocode.js",
11
- "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"