primocode 7.15.0 → 7.17.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
@@ -22,7 +22,7 @@ const cfg = require('../lib/config');
22
22
  const { runAct, interromper } = require('../lib/act');
23
23
  const pasta = require('../lib/pasta');
24
24
  const catalogo = require('../lib/catalogo');
25
- const streaming = require('../lib/streaming');
25
+ const effort = require('../lib/effort');
26
26
 
27
27
  // A pasta de onde o comando foi dado. Não é mais onde ele trabalha (o padrão é
28
28
  // ~/primocode), mas continua servindo de ponto de partida para achar pasta que
@@ -48,7 +48,7 @@ ${c.bold('Comandos')}
48
48
  ${c.brand('/dir')} <caminho> trabalha numa pasta que já existe (onde você quiser)
49
49
  ${c.brand('/project')} lista projetos ou cria em ~/primocode: /project <nome>
50
50
  ${c.brand('/mode')} /mode primocode|chat|auto
51
- ${c.brand('/model')} /model top|main|fast — ou um ID do OpenRouter
51
+ ${c.brand('/effort')} esforço do agente — do rápido ao mais esperto
52
52
  ${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
53
53
  ${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
54
54
  ${c.brand('/tudo')} lista tudo que ele sabe fazer
@@ -56,7 +56,6 @@ ${c.bold('Comandos')}
56
56
  ${c.brand('/read')} <arquivo> mostra um arquivo
57
57
  ${c.brand('/run')} <comando> executa no shell do projeto
58
58
  ${c.brand('/open')} <arquivo|url> abre no navegador
59
- ${c.brand('/streaming')} painel ao vivo — veja o agente mexendo no computador
60
59
  ${c.brand('/status')} checa o servidor
61
60
  ${c.brand('/name')} <nome> como o Primo te chama
62
61
  ${c.brand('/mascot')} <estilo> mascote: primo|fox|cat
@@ -80,14 +79,14 @@ ${c.bold('Onde ele trabalha')}
80
79
 
81
80
  // ── Autopreenchimento dos comandos (estilo Claude Code) ────────────────
82
81
  // Metade do comando já serve: Tab completa, e um prefixo único executa
83
- // direto — "/str" vira "/streaming" sozinho. A lista é só a dos nomes
82
+ // direto — "/eff" vira "/effort" sozinho. A lista é só a dos nomes
84
83
  // canônicos; os apelidos (/cd, /stream…) continuam valendo por extenso.
85
84
  const SLASH_COMANDOS = [
86
85
  ['/help', 'todos os comandos'],
87
86
  ['/project', 'lista/cria projetos em ~/primocode'],
88
87
  ['/dir', 'trabalhar numa pasta sua'],
89
88
  ['/mode', 'primocode | chat | auto'],
90
- ['/model', 'top | main | fast'],
89
+ ['/effort', 'esforço: low → ultracode (←/→ para ajustar)'],
91
90
  ['/attach', 'anexa arquivo ao pedido'],
92
91
  ['/detach', 'remove os anexos'],
93
92
  ['/files', 'lista os anexos'],
@@ -96,7 +95,6 @@ const SLASH_COMANDOS = [
96
95
  ['/read', 'mostra um arquivo'],
97
96
  ['/run', 'roda um comando'],
98
97
  ['/open', 'abre no navegador'],
99
- ['/streaming', 'painel ao vivo — vídeo dele trabalhando'],
100
98
  ['/status', 'checa o servidor'],
101
99
  ['/name', 'como ele te chama'],
102
100
  ['/mascot', 'primo | fox | cat'],
@@ -117,12 +115,111 @@ function completarSlash(line) {
117
115
  return [acertos, line];
118
116
  }
119
117
 
118
+ // ── /effort: o slider de esforço (estilo Claude Code) ──────────────────
119
+ // Régua com os níveis, o marcador ▲ embaixo do escolhido, "Faster" à
120
+ // esquerda e "Smarter" à direita. ←/→ move, Enter confirma, Esc cancela.
121
+
122
+ function desenharEffort(idx) {
123
+ const cols = Math.max(52, Math.min(process.stdout.columns || 100, 104));
124
+ const regua = '▔'.repeat(cols);
125
+ const linhas = [];
126
+
127
+ // Cada nível ocupa uma coluna própria; o ultracode fica destacado no fim.
128
+ const rotulos = effort.NOMES;
129
+ const larguras = rotulos.map((n) => Math.max(n.length + 3, 9));
130
+ const posicoes = [];
131
+ let x = 23; // recuo da régua, igual ao do mockup
132
+ for (let i = 0; i < rotulos.length; i++) {
133
+ posicoes.push(x + Math.floor(larguras[i] / 2));
134
+ x += larguras[i];
135
+ }
136
+ const fimRegua = x;
137
+
138
+ linhas.push(c.dim(regua));
139
+ linhas.push(' ' + c.bold(c.white('Effort')));
140
+ linhas.push('');
141
+
142
+ // Faster ....▲.... Smarter — a trilha, com o marcador na posição atual.
143
+ const marca = posicoes[idx];
144
+ let trilha = '';
145
+ for (let k = 23; k < fimRegua; k++) {
146
+ if (k === marca) trilha += c.brand('▲');
147
+ else if (k === fimRegua - 12) trilha += c.dim('┆'); // ultracode é um degrau à parte
148
+ else trilha += c.dim(k < marca ? '─' : '─');
149
+ }
150
+ linhas.push(' ' + c.muted('Faster') + ' '.repeat(Math.max(1, fimRegua - 23 - 6 - 7)) + c.muted('Smarter'));
151
+ linhas.push(' ' + trilha);
152
+
153
+ // Os nomes, o escolhido aceso.
154
+ let linhaNomes = ' '.repeat(23);
155
+ for (let i = 0; i < rotulos.length; i++) {
156
+ const nome = rotulos[i];
157
+ const espaco = larguras[i];
158
+ const pad = Math.floor((espaco - nome.length) / 2);
159
+ const texto = i === idx ? c.bold(c.brand(nome)) : c.muted(nome);
160
+ linhaNomes += ' '.repeat(pad) + texto + ' '.repeat(espaco - nome.length - pad);
161
+ }
162
+ linhas.push(linhaNomes);
163
+
164
+ const nivel = effort.nivelDe(rotulos[idx]);
165
+ linhas.push(' ' + c.dim(nivel.resumo));
166
+ linhas.push('');
167
+ linhas.push(' ' + c.muted('←/→ ajusta · Enter confirma · Esc cancela'));
168
+ return linhas.join('\n');
169
+ }
170
+
171
+ // Desenha, escuta as setas, e devolve o nome escolhido (ou null se cancelou).
172
+ function sliderEffort(atual) {
173
+ return new Promise((resolve) => {
174
+ if (!process.stdin.isTTY) { // sem terminal (pipe, CI) não há slider
175
+ console.log(c.muted(` Esforço atual: ${effort.normalizar(atual)}. Use /effort <${effort.NOMES.join('|')}>.`));
176
+ return resolve(null);
177
+ }
178
+ let idx = effort.indiceDe(atual);
179
+ const ALTURA = 9; // linhas que o desenho ocupa — para redesenhar por cima
180
+
181
+ const pintar = (primeira) => {
182
+ if (!primeira) process.stdout.write(`\x1b[${ALTURA}A\r`);
183
+ process.stdout.write(desenharEffort(idx).split('\n').map((l) => '\x1b[2K' + l).join('\n') + '\n');
184
+ };
185
+ pintar(true);
186
+
187
+ const aoTeclar = (ch, key) => {
188
+ if (!key) return;
189
+ if (key.name === 'right' || key.name === 'l') { idx = Math.min(effort.NOMES.length - 1, idx + 1); pintar(false); return; }
190
+ if (key.name === 'left' || key.name === 'h') { idx = Math.max(0, idx - 1); pintar(false); return; }
191
+ if (key.name === 'return' || key.name === 'enter') { encerrar(effort.NOMES[idx]); return; }
192
+ if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { encerrar(null); return; }
193
+ };
194
+ const encerrar = (valor) => {
195
+ process.stdin.removeListener('keypress', aoTeclar);
196
+ sliderAtivo = false;
197
+ resolve(valor);
198
+ };
199
+ sliderAtivo = true;
200
+ process.stdin.on('keypress', aoTeclar);
201
+ });
202
+ }
203
+
204
+ function aplicarEffort(nome) {
205
+ const nivel = effort.nivelDe(nome);
206
+ state.config.effort = nivel.nome;
207
+ state.config.model = nivel.model;
208
+ cfg.saveConfig({ effort: nivel.nome, model: nivel.model });
209
+ console.log(c.ok(`Esforço: ${nivel.nome}`) + c.muted(` ${nivel.resumo}`));
210
+ if (nivel.pesquisa) console.log(c.dim(' Pesquisa de referência ligada: ele consulta docs e exemplos antes de escrever.'));
211
+ refreshPrompt();
212
+ }
213
+
214
+ // Enquanto o slider está no ar, o fantasma do autocompletar não desenha.
215
+ let sliderAtivo = false;
216
+
120
217
  function mostrarMenuSlash() {
121
218
  console.log('');
122
219
  for (const [nome, desc] of SLASH_COMANDOS) {
123
220
  console.log(` ${c.brand(ui.padRight(nome, 12))}${c.muted(desc)}`);
124
221
  }
125
- console.log(c.dim('\n Tab completa · metade do comando já serve: /str vira /streaming'));
222
+ console.log(c.dim('\n Tab completa · metade do comando já serve: /eff vira /effort'));
126
223
  }
127
224
 
128
225
  let rl = null;
@@ -309,6 +406,7 @@ async function sendPrompt(userText) {
309
406
  const r = await runAct(prompt, {
310
407
  server: state.config.server,
311
408
  model: state.config.model,
409
+ effort: state.config.effort,
312
410
  token: state.config.token,
313
411
  history: state.history,
314
412
  projectDir: plan.dir,
@@ -464,24 +562,38 @@ async function handleSlash(text) {
464
562
  break;
465
563
  }
466
564
 
565
+ // O /model virou /effort: você escolhe o ESFORÇO num slider (←/→,
566
+ // Enter confirma, Esc cancela) e o modelo sai daí. Com argumento
567
+ // ("/effort high") aplica direto, para quem já sabe o que quer.
568
+ case 'effort': case 'esforco': case 'esforço': {
569
+ const pedido = rest.toLowerCase().trim();
570
+ if (pedido) {
571
+ if (!effort.NOMES.includes(pedido)) {
572
+ console.log(c.err(`Esforço inválido. Use: ${effort.NOMES.join(' | ')}`));
573
+ break;
574
+ }
575
+ aplicarEffort(pedido);
576
+ break;
577
+ }
578
+ const escolhido = await sliderEffort(state.config.effort);
579
+ if (escolhido) aplicarEffort(escolhido);
580
+ else console.log(c.muted(' Nada mudou.'));
581
+ break;
582
+ }
583
+
584
+ // Continua existindo para quem fixa um ID do OpenRouter na unha; o
585
+ // caminho normal é o /effort.
467
586
  case 'model': {
468
587
  const m = rest.toLowerCase();
469
- // Todos grátis. A lista viva de cada corrente está no /status.
470
- const nomes = {
471
- top: 'o mais capaz — 550B, 1 milhão de tokens de contexto',
472
- main: 'o padrão — agente de código, o melhor em tarefa de terminal',
473
- fast: 'o mais rápido — resposta curta, alta vazão',
474
- };
475
- if (nomes[m]) {
476
- state.config.model = m;
477
- cfg.saveConfig({ model: m });
478
- console.log(c.ok(`Modelo: ${m}`) + c.muted(` ${nomes[m]}`) + c.dim(' · grátis'));
479
- } else if (m.includes('/')) {
480
- // ID literal do OpenRouter, para quem quer sair dos três.
588
+ if (m.includes('/')) {
481
589
  state.config.model = m;
482
590
  cfg.saveConfig({ model: m });
483
- console.log(c.ok(`Modelo: ${m}`) + c.muted(' precisa aceitar tools para o modo primocode'));
484
- } else console.log(c.err('Uso: /model top|main|fast (ou um ID do OpenRouter, ex: google/gemini-2.5-pro)'));
591
+ console.log(c.ok(`Modelo fixo: ${m}`) + c.muted(' precisa aceitar tools no modo primocode'));
592
+ console.log(c.dim(' Para voltar ao normal, use /effort.'));
593
+ } else {
594
+ console.log(c.muted(' O modelo agora vem do esforço. Use ') + c.brand('/effort') + c.muted('.'));
595
+ console.log(c.dim(` Agora: ${state.config.effort} → modelo ${effort.modeloDe(state.config.effort)}`));
596
+ }
485
597
  break;
486
598
  }
487
599
 
@@ -552,33 +664,6 @@ async function handleSlash(text) {
552
664
  break;
553
665
  }
554
666
 
555
- // Painel ao vivo: um servidorzinho local mostra cada ação do agente na
556
- // tela + a foto tirada logo depois dela. Liga/desliga; /streaming on|off
557
- // também funciona.
558
- case 'streaming': case 'stream': case 'live': {
559
- const pedido = rest.toLowerCase();
560
- const ligar = pedido === 'on' ? true : pedido === 'off' ? false : !streaming.isOn();
561
-
562
- if (!ligar) {
563
- if (streaming.isOn()) { streaming.stop(); console.log(c.ok('Streaming desligado.')); }
564
- else console.log(c.muted('O streaming já estava desligado.'));
565
- break;
566
- }
567
- if (streaming.isOn()) {
568
- console.log(c.muted(`Já está ligado: ${streaming.url()}`));
569
- break;
570
- }
571
- try {
572
- const url = await streaming.start();
573
- console.log(c.ok('Streaming ligado: ') + c.white(url));
574
- console.log(c.muted(' Dê uma instrução e veja cada passo dele aí, ao vivo. /streaming de novo desliga.'));
575
- await tools.openInBrowser(url);
576
- } catch (e) {
577
- console.log(c.err(`Não consegui ligar o streaming: ${e.message}`));
578
- }
579
- break;
580
- }
581
-
582
667
  case 'name': {
583
668
  if (!rest) { console.log(c.muted(` Nome atual: ${state.config.userName || '(nenhum)'}`)); break; }
584
669
  state.config.userName = rest;
@@ -613,7 +698,8 @@ async function handleSlash(text) {
613
698
  console.log('');
614
699
  console.log(` ${c.muted('nome')} ${c.white(state.config.userName || '(nenhum)')}`);
615
700
  console.log(` ${c.muted('mascote')} ${c.white(state.config.mascot || 'primo')}`);
616
- console.log(` ${c.muted('modelo')} ${c.white(state.config.model)}`);
701
+ console.log(` ${c.muted('esforço')} ${c.white(state.config.effort)}${c.dim(' · /effort muda')}`);
702
+ console.log(` ${c.muted('modelo')} ${c.white(state.config.model)}${c.dim(' (vem do esforço)')}`);
617
703
  console.log(` ${c.muted('modo')} ${c.white(state.mode)}`);
618
704
  console.log(` ${c.muted('servidor')} ${c.dim(state.config.server)}`);
619
705
  console.log(c.muted(`\n ${cfg.CONFIG_FILE}`));
@@ -654,7 +740,7 @@ async function handleSlash(text) {
654
740
  case 'exit': case 'quit': if (rl) rl.close(); break;
655
741
  default: {
656
742
  // "/" sozinho abre o menu; um prefixo único autopreenche e executa
657
- // ("/str" → /streaming); prefixo ambíguo mostra as opções.
743
+ // ("/eff" → /effort); prefixo ambíguo mostra as opções.
658
744
  if (!cmd) { mostrarMenuSlash(); break; }
659
745
  const casam = SLASH_COMANDOS.map(([nome]) => nome.slice(1)).filter((n) => n.startsWith(cmd.toLowerCase()));
660
746
  if (casam.length === 1) {
@@ -733,6 +819,7 @@ async function main() {
733
819
  await runAct(fullPrompt, {
734
820
  server: state.config.server,
735
821
  model: state.config.model,
822
+ effort: state.config.effort,
736
823
  token: state.config.token,
737
824
  history: [],
738
825
  projectDir: plan.dir,
@@ -766,7 +853,7 @@ async function main() {
766
853
  if (process.stdin.isTTY && process.stdout.isTTY) {
767
854
  let fantasma = '';
768
855
  const desenharFantasma = () => {
769
- if (busy || pendingAsk || !rl) return;
856
+ if (busy || pendingAsk || sliderAtivo || !rl) return;
770
857
  const linha = rl.line;
771
858
  const noFim = rl.cursor === linha.length;
772
859
  fantasma = '';
@@ -785,6 +872,7 @@ async function main() {
785
872
  // sequence "\x1b\x1b" — vale como os dois.
786
873
  let ultimoEsc = 0;
787
874
  process.stdin.on('keypress', (ch, key) => {
875
+ if (sliderAtivo) return; // o slider é dono do teclado enquanto está no ar
788
876
  if (key && (key.name === 'escape' || key.sequence === '\x1b\x1b')) {
789
877
  if (!busy) { ultimoEsc = 0; return; } // sem agente rodando, Esc é só Esc
790
878
  const agora = Date.now();
package/lib/act.js CHANGED
@@ -15,10 +15,6 @@ const tools = require('./tools');
15
15
  const api = require('./api');
16
16
  const cfg = require('./config');
17
17
  const catalogo = require('./catalogo');
18
- // "painel", não "streaming": dentro do runAct existe um `let streaming` (a
19
- // flag de texto chegando em stream) que SOMBREARIA este módulo — e o step()
20
- // viraria TypeError engolido pelo catch. Foi um bug real, pego em teste.
21
- const painel = require('./streaming');
22
18
 
23
19
  const MAX_ITERATIONS = 15;
24
20
  const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
@@ -105,6 +101,14 @@ function logResult(kind, result, profundidade) {
105
101
  case 'browser_read':
106
102
  console.log(p + c.muted(`${(result.text || '').length} caracteres lidos da página`));
107
103
  return;
104
+ case 'pesquisar_referencia': {
105
+ console.log(p + c.muted(`${result.fontes} fonte${result.fontes === 1 ? '' : 's'} lida${result.fontes === 1 ? '' : 's'} sobre "${result.consulta}"`));
106
+ for (const r of (result.referencias || []).slice(0, 4)) {
107
+ console.log(p + c.dim(`${r.titulo}`.slice(0, 100)));
108
+ console.log(p + c.dim(` ${r.url}`.slice(0, 110)));
109
+ }
110
+ return;
111
+ }
108
112
  case 'desktop_windows': {
109
113
  console.log(p + c.muted(`${result.total} janela${result.total === 1 ? '' : 's'}`));
110
114
  for (const j of (result.janelas || []).slice(0, 5)) console.log(p + c.dim(`${j.app ? j.app + ' · ' : ''}${j.titulo}`.slice(0, 120)));
@@ -168,7 +172,7 @@ function buildMemoryContext(projectDir, projectName) {
168
172
  async function runAct(userPrompt, opts) {
169
173
  const {
170
174
  server, model, token, history, projectDir, projectName,
171
- ensureWorkDir, profundidade = 0, subagent = false,
175
+ ensureWorkDir, profundidade = 0, subagent = false, effort,
172
176
  } = opts || {};
173
177
 
174
178
  // A pasta de projeto só nasce quando o agente for mesmo MEXER EM ARQUIVO.
@@ -193,11 +197,6 @@ async function runAct(userPrompt, opts) {
193
197
 
194
198
  let finalText = '';
195
199
 
196
- // O pedido do usuário aparece na barra de comando do painel, digitado
197
- // letra a letra — só o pedido principal; sub-tarefa de agente não é
198
- // instrução de gente.
199
- if (!subagent) { try { painel.instrucao(userPrompt); } catch {} }
200
-
201
200
  // Pedido novo zera o pedido de parada anterior.
202
201
  if (!subagent) PARAR = false;
203
202
 
@@ -210,7 +209,7 @@ async function runAct(userPrompt, opts) {
210
209
  let streaming = false;
211
210
 
212
211
  try {
213
- await api.requestAct(server, { messages, model, token, projectDir, projectName, subagent }, (evt) => {
212
+ await api.requestAct(server, { messages, model, effort, token, projectDir, projectName, subagent }, (evt) => {
214
213
  if (evt.erro) { streamError = evt.erro; return; }
215
214
  if (evt.tool_call) { toolCalls.push(evt.tool_call); return; }
216
215
  if (evt.delta) {
@@ -300,10 +299,6 @@ async function runAct(userPrompt, opts) {
300
299
  const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
301
300
  logResult(tc.name, result, profundidade);
302
301
 
303
- // Painel ao vivo (/streaming): registra o passo e fotografa a tela.
304
- // Nunca pode derrubar o loop — falha de foto é problema do painel.
305
- try { painel.step(tc.name, tc.arguments, result); } catch {}
306
-
307
302
  // Uma escrita bem-sucedida limpa a dívida: o agente se corrigiu.
308
303
  if (result && result.ok === false) falhasPendentes.push(`${tc.name}(${tc.arguments?.path || ''}): ${result.error}`);
309
304
  else if (['write_file', 'edit_file'].includes(tc.name)) falhasPendentes.length = 0;
package/lib/catalogo.js CHANGED
@@ -78,6 +78,7 @@ const GRUPOS = [
78
78
  resumo: 'como ele se organiza numa tarefa grande',
79
79
  itens: [
80
80
  { tool: 'write_todos', escreve: false, faz: 'monta o plano e vai marcando o que terminou' },
81
+ { tool: 'pesquisar_referencia', escreve: false, faz: 'lê docs e exemplos antes de escrever — sobe a qualidade' },
81
82
  // Estas duas o loop do agente atende (lib/act.js), não o tools.execute.
82
83
  { tool: 'spawn_agent', escreve: false, faz: 'chama um subagente para uma parte da tarefa', noLoop: true },
83
84
  { tool: 'finish', escreve: false, faz: 'encerra dizendo o que fez', noLoop: true },
package/lib/config.js CHANGED
@@ -160,6 +160,9 @@ function loadConfig() {
160
160
  return {
161
161
  server: process.env.PRIMOCODE_SERVER || file.server || 'https://primocode-server.onrender.com',
162
162
  model: process.env.PRIMOCODE_MODEL || file.model || 'main',
163
+ // Esforço, estilo Claude Code (/effort). É o que o usuário escolhe; o
164
+ // modelo é derivado dele. Guardado para durar entre sessões.
165
+ effort: process.env.PRIMOCODE_EFFORT || file.effort || 'medium',
163
166
  token: process.env.PRIMOCODE_TOKEN || file.token || null,
164
167
  autoApply: file.autoApply === true,
165
168
  currentProject: file.currentProject || null,
@@ -177,8 +180,8 @@ function saveConfig(partial) {
177
180
  ensureDir(CONFIG_DIR);
178
181
  const cur = (() => { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; } })();
179
182
  // Só persiste o que faz sentido guardar — nada de campos derivados.
180
- const { server, model, token, autoApply, currentProject, userName, mascot, theme, mode } = { ...cur, ...partial };
181
- const next = { server, model, token, autoApply, currentProject, userName, mascot, theme, mode };
183
+ const { server, model, effort, token, autoApply, currentProject, userName, mascot, theme, mode } = { ...cur, ...partial };
184
+ const next = { server, model, effort, token, autoApply, currentProject, userName, mascot, theme, mode };
182
185
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8');
183
186
  return next;
184
187
  }
package/lib/desktop.js CHANGED
@@ -659,8 +659,8 @@ function clickElement(nome, opts = {}) {
659
659
  }
660
660
 
661
661
  // ── Screenshot ────────────────────────────────────────
662
- // A ferramenta do Linux é resolvida UMA vez: o vídeo do /streaming chama isto
663
- // várias vezes por segundo, e um `which` por frame seria desperdício.
662
+ // A ferramenta do Linux é resolvida UMA vez: um `which` a cada screenshot
663
+ // seria desperdício.
664
664
  let LINUX_SHOT;
665
665
  function linuxShot() {
666
666
  if (LINUX_SHOT === undefined) LINUX_SHOT = firstCmd(['scrot', 'maim', 'gnome-screenshot', 'spectacle', 'import']);
@@ -669,8 +669,8 @@ function linuxShot() {
669
669
 
670
670
  /**
671
671
  * O comando de shell que fotografa a tela para `filename` — ou null quando a
672
- * plataforma não tem como. Existe separado do screenshot() para o painel do
673
- * /streaming capturar em LOOP, assíncrono, sem travar o agente.
672
+ * plataforma não tem como. Existe separado do screenshot() para quem precisa
673
+ * capturar de forma assíncrona, sem travar o processo.
674
674
  */
675
675
  function screenshotCommand(filename) {
676
676
  if (PLATFORM === 'darwin') return `screencapture -x "${filename}"`;
package/lib/effort.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * effort.js — o "esforço" do agente, no espírito do /effort do Claude Code.
3
+ *
4
+ * O usuário não escolhe mais um modelo por nome (/model): escolhe um ESFORÇO
5
+ * num controle deslizante, do mais rápido ao mais esperto. O modelo é derivado
6
+ * daqui. Fica tudo num lugar só, puro (sem I/O), para o teste bater sem rede.
7
+ *
8
+ * A pesquisa de referência (abrir docs/exemplos e aprender com eles antes de
9
+ * escrever) liga a partir do "high": esforço maior = ele pesquisa antes de
10
+ * codar, o que melhora a qualidade. É VISÍVEL no painel — nada escondido.
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ // Ordem = ordem do slider, do mais rápido ao mais esperto.
16
+ const NIVEIS = [
17
+ { nome: 'low', model: 'fast', pesquisa: false, resumo: 'rápido — tarefa simples, resposta curta' },
18
+ { nome: 'medium', model: 'main', pesquisa: false, resumo: 'equilíbrio — o padrão do dia a dia' },
19
+ { nome: 'high', model: 'main', pesquisa: true, resumo: 'capricha no código e pesquisa referências' },
20
+ { nome: 'xhigh', model: 'top', pesquisa: true, resumo: 'o mais capaz — 1M de contexto, pesquisa' },
21
+ { nome: 'max', model: 'top', pesquisa: true, resumo: 'esforço máximo — pesquisa a fundo' },
22
+ { nome: 'ultracode', model: 'top', pesquisa: true, resumo: 'tudo no talo — pesquisa e cruza referências' },
23
+ ];
24
+
25
+ const NOMES = NIVEIS.map((n) => n.nome);
26
+
27
+ function normalizar(nome) {
28
+ const n = String(nome || '').toLowerCase().trim();
29
+ return NOMES.includes(n) ? n : 'medium';
30
+ }
31
+
32
+ function nivelDe(nome) {
33
+ return NIVEIS.find((n) => n.nome === normalizar(nome)) || NIVEIS[1];
34
+ }
35
+
36
+ function indiceDe(nome) {
37
+ const i = NOMES.indexOf(normalizar(nome));
38
+ return i < 0 ? 1 : i;
39
+ }
40
+
41
+ /** O apelido de modelo que este esforço usa. */
42
+ function modeloDe(nome) {
43
+ return nivelDe(nome).model;
44
+ }
45
+
46
+ /** Este esforço pesquisa referências antes de codar? */
47
+ function pesquisaDe(nome) {
48
+ return nivelDe(nome).pesquisa;
49
+ }
50
+
51
+ module.exports = { NIVEIS, NOMES, normalizar, nivelDe, indiceDe, modeloDe, pesquisaDe };