primocode 9.8.0-beta.2 → 9.8.0-beta.4

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
@@ -21,6 +21,7 @@ const api = require('../lib/api');
21
21
  const tools = require('../lib/tools');
22
22
  const cfg = require('../lib/config');
23
23
  const nomeProjeto = require('../lib/nome-projeto');
24
+ const kits = require('../lib/kits');
24
25
  const provedorLocal = require('../lib/local/provedor');
25
26
  const { runAct, interromper, ehTetoDeToken, ehTransitorio, esperaDoErro } = require('../lib/act');
26
27
  const fala = require('../lib/fala');
@@ -31,13 +32,17 @@ const effort = require('../lib/effort');
31
32
  const memoria = require('../lib/memoria-conversa');
32
33
  const registro = require('../lib/registro');
33
34
  const permissao = require('../lib/permissao');
35
+ const perigo = require('../lib/perigo');
34
36
  const skills = require('../lib/skills');
37
+ const checkpoint = require('../lib/checkpoint');
38
+ const comandosLib = require('../lib/comandos');
35
39
  const marketplace = require('../lib/marketplace');
36
40
  const vozModo = require('../lib/voz');
37
41
  const appLib = require('../lib/app');
38
42
  const conta = require('../lib/conta');
39
43
  const nuvem = require('../lib/nuvem');
40
44
  const compat = require('../lib/compatibilidade');
45
+ const mencoesLib = require('../lib/mencoes');
41
46
 
42
47
  // Rede de segurança de último recurso: sem isto, uma exceção fora de um
43
48
  // try/catch (ex.: dentro de um callback de socket, como o parser de SSE) não
@@ -98,10 +103,13 @@ ${c.bold('Comandos')}
98
103
  ${c.brand('/project')} lista projetos ou cria em ~/primocode: /project <nome>
99
104
  ${c.brand('/mode')} /mode primocode|chat|auto
100
105
  ${c.brand('/effort')} esforço do agente — do rápido ao mais esperto
106
+ ${c.brand('/permissoes')} quando ele pergunta antes de agir: auto|perguntar|livre
101
107
  ${c.brand('/compact')} resume a conversa e devolve o contexto
102
- ${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
108
+ ${c.brand('/init')} estuda o projeto e escreve o PRIMO.md
109
+ ${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido · ou @arquivo no pedido
103
110
  ${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
104
111
  ${c.brand('/all')} lista tudo que ele sabe fazer
112
+ ${c.brand('/desfazer')} [lista|N] desfaz o que o último pedido (ou os N últimos) mudou
105
113
  ${c.brand('/tree')} [n] árvore do projeto
106
114
  ${c.brand('/read')} <arquivo> mostra um arquivo
107
115
  ${c.brand('/run')} <comando> executa no shell do projeto
@@ -110,6 +118,7 @@ ${c.bold('Comandos')}
110
118
  ${c.brand('/login')} entra na sua conta · ${c.brand('/account')} mostra o plano
111
119
  ${c.brand('/chats')} [n] suas conversas — as mesmas do app e do celular
112
120
  ${c.brand('/logout')} desconecta este computador da conta
121
+ ${c.brand('/comandos')} seus comandos (/revisar, /explicar…) · /comandos novo <nome>
113
122
  ${c.brand('/status')} checa o servidor
114
123
  ${c.brand('/name')} <nome> como o Primo te chama
115
124
  ${c.brand('/mascot')} <estilo> mascote: primo|fox|cat
@@ -141,11 +150,14 @@ const SLASH_COMANDOS = [
141
150
  ['/dir', 'trabalhar numa pasta sua'],
142
151
  ['/mode', 'primocode | chat | auto'],
143
152
  ['/effort', 'esforço: low → ultracode (←/→ para ajustar)'],
153
+ ['/permissoes', 'auto | perguntar | livre — quando ele pede licença'],
144
154
  ['/compact', 'resume a conversa e devolve o contexto'],
155
+ ['/init', 'estuda o projeto e escreve o PRIMO.md (as instruções dele)'],
145
156
  ['/attach', 'anexa arquivo ao pedido'],
146
157
  ['/detach', 'remove os anexos'],
147
158
  ['/files', 'lista os anexos'],
148
159
  ['/all', 'tudo que ele sabe fazer'],
160
+ ['/desfazer', 'desfaz o que o último pedido mudou nos arquivos (lista · N)'],
149
161
  ['/tree', 'árvore do projeto'],
150
162
  ['/read', 'mostra um arquivo'],
151
163
  ['/run', 'roda um comando'],
@@ -161,6 +173,7 @@ const SLASH_COMANDOS = [
161
173
  ['/skills', 'o que ele sabe fazer bem — ligue e desligue'],
162
174
  ['/marketplace', 'a tela das skills: navegue, ligue, crie'],
163
175
  ['/install-skill', 'instala uma skill de um arquivo'],
176
+ ['/comandos', 'seus comandos: um .md vira /nome · /comandos novo <nome>'],
164
177
  ['/api', 'usa a SUA chave (Gemini, ChatGPT, Claude…) direto daqui, sem o limite do servidor'],
165
178
  ['/status', 'checa o servidor'],
166
179
  ['/connectors', 'quem está atendendo os seus pedidos'],
@@ -312,9 +325,34 @@ function aplicarEffort(nome) {
312
325
  // Enquanto o slider está no ar, o fantasma do autocompletar não desenha.
313
326
  let sliderAtivo = false;
314
327
 
328
+ /* Os comandos da pessoa (lib/comandos.js), com o do projeto atual por cima.
329
+ A caixa de digitar pede a lista a cada tecla para desenhar o fantasma; um
330
+ cache de dois segundos poupa ler três pastas por letra digitada, e ainda
331
+ pega na hora um arquivo que a pessoa acabou de salvar. */
332
+ let cacheComandos = { em: 0, dir: null, lista: [] };
333
+ function comandosDaPessoa() {
334
+ const agora = Date.now();
335
+ if (agora - cacheComandos.em > 2000 || cacheComandos.dir !== state.workDir) {
336
+ let lista = [];
337
+ try { lista = comandosLib.carregar({ projectDir: state.workDir }); } catch { /* disco estranho não trava a caixa */ }
338
+ cacheComandos = { em: agora, dir: state.workDir, lista };
339
+ }
340
+ return cacheComandos.lista;
341
+ }
342
+
343
+ /* O menu e o fantasma enxergam os dois tipos. O embutido vem primeiro e,
344
+ com o mesmo nome, o da pessoa nem aparece: quem ganha é sempre o embutido. */
345
+ function comandosDoMenu() {
346
+ const embutidos = new Set(SLASH_COMANDOS.map(([n]) => n));
347
+ const deles = comandosDaPessoa()
348
+ .filter((x) => !embutidos.has('/' + x.nome))
349
+ .map((x) => ['/' + x.nome, x.descricao || `seu comando (${x.origem})`]);
350
+ return SLASH_COMANDOS.concat(deles);
351
+ }
352
+
315
353
  function mostrarMenuSlash() {
316
354
  console.log('');
317
- for (const [nome, desc] of SLASH_COMANDOS) {
355
+ for (const [nome, desc] of comandosDoMenu()) {
318
356
  console.log(` ${c.brand(ui.padRight(nome, 12))}${c.muted(desc)}`);
319
357
  }
320
358
  console.log(c.dim('\n Tab completa · metade do comando já serve: /eff vira /effort'));
@@ -554,14 +592,18 @@ const RE_PROJETO_NOVO = /\b(novo projeto|projeto novo|outro projeto|do zero|come
554
592
  const RE_CRIAR_UM = /\b(crie|cria|criar|fa[cç]a|faz|fazer|monte|monta|montar|gere|gera|desenvolva|construa|programe|quero|preciso de)\s+(um|uma|outro|outra|novo|nova)\b/;
555
593
  const RE_REFERE_ATUAL = /\b(isso|isto|esse|essa|este|esta|nele|nela|dele|dela|no projeto|nesse|nessa|neste|nesta|continua|continue|edita|edite|muda|mude|melhora|melhore|adiciona|adicione|coloca|coloque|p[oõ]e|corrige|corrija|arruma|arrume|ajusta|ajuste)\b/;
556
594
 
595
+ /* "crie uma nova seção", "cria um botão", "faz uma tela de login": é um
596
+ PEDAÇO do projeto em que se está, não outro projeto. */
597
+ const RE_PEDACO = /\b(um|uma|outro|outra|novo|nova)\s+(nov[ao]\s+)?(secao|secoes|parte|botao|botoes|tela|aba|menu|rodape|cabecalho|formulario|fase|funcao|funcionalidade|campo|imagem|animacao|efeito|cor|fonte|pagina interna|area|bloco|card|modal|link)\b/;
598
+
557
599
  function pedeProjetoNovo(texto) {
558
600
  const t = semAcento(texto);
559
601
  if (RE_PROJETO_NOVO.test(t)) return true;
560
- if (!RE_CRIAR_UM.test(t) || RE_REFERE_ATUAL.test(t)) return false;
602
+ if (!RE_CRIAR_UM.test(t) || RE_REFERE_ATUAL.test(t) || RE_PEDACO.test(t)) return false;
561
603
  // Mesmo assunto do projeto atual ("crie uma fase nova pro jogo de carro")
562
604
  // é continuação, não projeto novo.
563
- const atual = new Set(cfg.slugify(state.currentProject || '').split('-').filter((x) => x.length > 2));
564
- const pedido = cfg.slugify(deriveProjectName(texto)).split('-').filter((x) => x.length > 2);
605
+ const atual = new Set(cfg.tokensDoAssunto(state.currentProject || ''));
606
+ const pedido = cfg.tokensDoAssunto(deriveProjectName(texto));
565
607
  return !pedido.some((x) => atual.has(x));
566
608
  }
567
609
 
@@ -642,10 +684,30 @@ function batizarDepois(plan, userText) {
642
684
  }, (evt) => { if (evt.delta) resposta += evt.delta; });
643
685
  return resposta;
644
686
  };
645
- nomeProjeto.escolher({ pedido: userText, nomes: cfg.listProjects(), perguntar })
687
+ plan.batizando = nomeProjeto.escolher({ pedido: userText, nomes: cfg.listProjects(), perguntar })
646
688
  .then((nome) => { if (nome) plan.nomeEscolhido = nome; });
647
689
  }
648
690
 
691
+ /* O kit de partida (lib/kits.js): pedido de landing numa pasta nova começa
692
+ de uma página pronta, e o agente só adapta. A pasta nasce AGORA, e não na
693
+ primeira escrita, porque o kit precisa estar lá antes de o agente ler. */
694
+ async function prepararKit(plan, userText) {
695
+ const kit = kits.escolher(userText, { provedor: provedorLocal.atual() });
696
+ if (!kit) return '';
697
+ let dir = plan.dir;
698
+ if (plan.pending) {
699
+ await Promise.race([plan.batizando, new Promise((r) => setTimeout(r, 8000))]);
700
+ dir = createPendingProject(plan);
701
+ plan.pending = false;
702
+ plan.dir = dir;
703
+ }
704
+ if (!dir || path.resolve(dir) === path.resolve(cfg.ROOT_DIR)) return '';
705
+ const copiados = kits.aplicar(kit, dir);
706
+ if (!copiados.length) return '';
707
+ console.log(c.dim(` ⎿ começando do kit de ${kit.nome} (${copiados.join(', ')}) — o agente só adapta`));
708
+ return kit.instrucao;
709
+ }
710
+
649
711
  function createPendingProject(plan) {
650
712
  const dir = cfg.createProjectDir(plan.nomeEscolhido || plan.name);
651
713
  entrarNoProjeto(dir, 'projeto');
@@ -726,8 +788,15 @@ function prepararPasta(userText) {
726
788
  * Enter sozinho é NÃO. O padrão de quem apertou Enter sem ler não pode ser
727
789
  * entregar o mouse e o teclado.
728
790
  */
729
- async function autorizarControle(oQueVaiFazer, { primeira } = {}) {
730
- if (primeira) {
791
+ async function autorizarControle(oQueVaiFazer, { primeira, tipo } = {}) {
792
+ /* O mesmo portão serve ao /permissoes (lib/perigo.js): a pergunta muda de
793
+ cabeçalho, as três respostas são as mesmas. */
794
+ if (tipo) {
795
+ console.log('');
796
+ console.log(' ' + c.warn('▸▸') + ' ' + c.bold(c.white(tipo === 'perigo'
797
+ ? 'Isto não tem volta. O Primo quer:'
798
+ : 'O Primo quer (você escolheu /permissoes perguntar):')));
799
+ } else if (primeira) {
731
800
  console.log('');
732
801
  console.log(' ' + c.warn('▸▸') + ' ' + c.bold(c.white('O Primo quer usar o seu computador.')));
733
802
  console.log(' ' + c.muted('Ele vai mexer no mouse e no teclado de verdade, no programa que'));
@@ -758,7 +827,15 @@ async function autorizarControle(oQueVaiFazer, { primeira } = {}) {
758
827
  * simplesmente travava. Abrir o /voice e PEDIR em voz para ele mexer no
759
828
  * computador já é a autorização — é o produto funcionando como o Jarvis. Fica
760
829
  * dito no terminal, a cada ação, o que ele fez; e fechar o PrimoCode esquece. */
761
- async function autorizarPorVoz(oQueVaiFazer, { primeira } = {}) {
830
+ async function autorizarPorVoz(oQueVaiFazer, { primeira, tipo } = {}) {
831
+ /* Pedir em voz é autorizar o que foi pedido — mas não o que não tem volta.
832
+ Ninguém está lendo o terminal para ver um `rm -rf` passar: o perigoso
833
+ é recusado, e o agente conta em voz o que ficou por fazer. */
834
+ if (tipo === 'perigo') {
835
+ console.log(' ' + c.warn('▸▸ recusado no modo voz: ') + c.muted(oQueVaiFazer));
836
+ return 'nao';
837
+ }
838
+ if (tipo) return 'agora';
762
839
  if (primeira) {
763
840
  console.log(' ' + c.warn('▸▸') + ' ' + c.muted('Modo voz: o Primo vai usar o mouse e o teclado. Você vê o cursor dele na tela.'));
764
841
  }
@@ -936,8 +1013,12 @@ async function sendPrompt(userText) {
936
1013
  }
937
1014
 
938
1015
  let prompt = userText;
939
- if (state.attachedFiles.length) {
940
- const parts = state.attachedFiles.map((f) => `Arquivo: ${f.path}\n\`\`\`\n${f.content}\n\`\`\``);
1016
+ // "@index.html" no pedido é um /attach só deste pedido: vai junto dos
1017
+ // anexos, mas não fica em state.attachedFiles para o próximo.
1018
+ const citados = mencoesDoPedido(userText);
1019
+ const anexos = [...state.attachedFiles, ...citados];
1020
+ if (anexos.length) {
1021
+ const parts = anexos.map((f) => `Arquivo: ${f.path}\n\`\`\`\n${f.content}\n\`\`\``);
941
1022
  prompt = parts.join('\n\n') + '\n\n' + userText;
942
1023
  }
943
1024
 
@@ -976,6 +1057,7 @@ async function sendPrompt(userText) {
976
1057
  // num plano limitado por REQUISIÇÕES (o grátis da Groq são 30 por
977
1058
  // minuto) cada chamada extra custa tanto quanto token.
978
1059
  const passos = await dividirTarefa(prompt);
1060
+ const instrucaoKit = await prepararKit(plan, userText);
979
1061
 
980
1062
  let r = null;
981
1063
  let jaFeito = '';
@@ -984,6 +1066,8 @@ async function sendPrompt(userText) {
984
1066
  interessa é o do pedido inteiro: quem pediu um vídeo não quer três
985
1067
  balanços, quer saber o que existe no disco quando ele terminar. */
986
1068
  const livro = registro.novo();
1069
+ // Um checkpoint por PEDIDO (não por passo): é o que o /desfazer volta.
1070
+ checkpoint.iniciar(userText);
987
1071
  for (let i = 0; i < passos.length; i++) {
988
1072
  if (i > 0) console.log(c.dim(` ⎿ ${passos[i].titulo}`));
989
1073
  // Só fato de passo CONCLUÍDO entra aqui. Num pedido real o passo de
@@ -1013,11 +1097,15 @@ async function sendPrompt(userText) {
1013
1097
  ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
1014
1098
  // O portão do computador: quem pergunta é o terminal, quem
1015
1099
  // lembra da resposta é a sessão.
1016
- autorizar: autorizarControle,
1100
+ // No pedido único (`primo "…"`) não há ninguém para responder:
1101
+ // sem `autorizar`, o que pede licença é recusado (lib/perigo.js).
1102
+ autorizar: state.semTerminal ? undefined : autorizarControle,
1017
1103
  controle: state.controle,
1104
+ permissoes: state.config.permissoes,
1018
1105
  // Pedido de mexer no computador leva a dica que libera as
1019
1106
  // ferramentas desktop_* no servidor (ver DICA_COMPUTADOR).
1020
- instrucao: i === 0 && querComputador(userText) ? vozModo.DICA_COMPUTADOR : '',
1107
+ instrucao: [instrucaoKit, i === 0 && querComputador(userText) ? vozModo.DICA_COMPUTADOR : '']
1108
+ .filter(Boolean).join('\n\n'),
1021
1109
  });
1022
1110
 
1023
1111
  r = await rodarPasso();
@@ -1058,6 +1146,7 @@ async function sendPrompt(userText) {
1058
1146
  : '';
1059
1147
  if (r.parado) break; // Esc Esc no meio do plano para o plano inteiro
1060
1148
  }
1149
+ checkpoint.encerrar();
1061
1150
 
1062
1151
  /* O balanço, depois da resposta. Depois, e não antes, de propósito: o
1063
1152
  que a pessoa quer ler primeiro é o que ele TEM A DIZER; a lista de
@@ -1251,6 +1340,31 @@ async function esperarResumo(tetoMs = 15000) {
1251
1340
  /* A linha que vai junto do resumo: em que projeto se está. É o que faz
1252
1341
  "continua o jogo" e "muda a cor" caírem no mesmo projeto em vez de inventar
1253
1342
  outro. */
1343
+ function mencoesDoPedido(userText) {
1344
+ const { arquivos, ignorados } = mencoesLib.mencoes(userText, projectDir());
1345
+ const lidos = [];
1346
+ for (const a of arquivos) {
1347
+ try {
1348
+ lidos.push({ path: a.rel, content: fs.readFileSync(a.abs, 'utf-8') });
1349
+ console.log(c.dim(` ⎿ anexado: ${a.rel} (${tools.fmtBytes(a.bytes)})`));
1350
+ } catch { console.log(c.dim(` ⎿ não consegui ler: ${a.rel}`)); }
1351
+ }
1352
+ for (const i of ignorados) console.log(c.dim(` ⎿ não anexei ${i.rel}: ${i.motivo}`));
1353
+ return lidos;
1354
+ }
1355
+
1356
+ /* O pedido do /init. Passa pelo MESMO caminho de um pedido digitado
1357
+ (sendPrompt → runAct), então usa as ferramentas e o provedor de agora — um
1358
+ /init que falasse direto com o modelo não enxergaria os arquivos. */
1359
+ const PEDIDO_INIT = 'Estude este projeto (a pasta atual) e escreva um arquivo PRIMO.md na raiz dela. '
1360
+ + 'Primeiro olhe de verdade: liste os arquivos, leia o README, o package.json (ou o equivalente) e os arquivos principais. '
1361
+ + 'Depois escreva o PRIMO.md em português, curto (menos de 60 linhas), com estas seções: '
1362
+ + 'o que é o projeto (2 ou 3 frases); como rodar (os comandos exatos que você viu, de instalar, rodar e testar); '
1363
+ + 'a stack (linguagem, frameworks, banco); as convenções que o código segue (nomes, estilo, onde ficam os testes); '
1364
+ + 'e onde está cada coisa (as pastas e arquivos que importam, uma linha cada). '
1365
+ + 'Só escreva o que você conferiu nos arquivos — nada de inventar comando ou dependência. '
1366
+ + 'Esse arquivo vai ser lido no começo de todo pedido neste projeto, então cada linha precisa valer o espaço.';
1367
+
1254
1368
  function contextoDoProjeto() {
1255
1369
  if (!state.currentProject) return '';
1256
1370
  return `Projeto atual: ${state.currentProject} (pasta ${shortPath(projectDir())}). `
@@ -1258,6 +1372,14 @@ function contextoDoProjeto() {
1258
1372
  + 'trabalhe nos arquivos que já existem lá, não crie outro projeto.';
1259
1373
  }
1260
1374
 
1375
+ /* O chat não passa pelo runAct, e é no runAct (buildMemoryContext) que as
1376
+ instruções do projeto entram. Sem isto o PRIMO.md valeria para o agente e
1377
+ seria ignorado quando o modo auto manda o pedido para a conversa. */
1378
+ function comInstrucoes(history) {
1379
+ const texto = mencoesLib.instrucoesDoProjeto(state.currentProject || state.workDir ? projectDir() : null);
1380
+ return texto ? [{ role: 'system', content: texto }, ...history] : history;
1381
+ }
1382
+
1261
1383
  // O chat também precisa parar no Esc Esc. Sem isto o texto continuava saindo
1262
1384
  // na tela depois de o usuário ter mandado parar.
1263
1385
  let chatEmVoo = null;
@@ -1284,7 +1406,7 @@ async function runChat(promptForModel, userTextForMemory) {
1284
1406
  try {
1285
1407
  await api.requestChat(state.config.server, {
1286
1408
  prompt: promptForModel,
1287
- history: memoria.paraEnviar(state.memoria, state.history, contextoDoProjeto()),
1409
+ history: comInstrucoes(memoria.paraEnviar(state.memoria, state.history, contextoDoProjeto())),
1288
1410
  model: state.config.model,
1289
1411
  token: state.config.token,
1290
1412
  sinal: chatEmVoo.signal,
@@ -1411,6 +1533,49 @@ const RENOMEADOS = {
1411
1533
  tudo: 'all', capacidades: 'all',
1412
1534
  };
1413
1535
 
1536
+ /* Abrir uma conversa salva e seguir dela. Uma função só, para o /chats n e o
1537
+ `primo -c` carregarem a conversa do MESMO jeito: dois caminhos divergiriam
1538
+ no primeiro detalhe (a memória, o gravador) e um deles gravaria errado. */
1539
+ async function abrirConversa(alvo) {
1540
+ const sp = new Spinner('abrindo').start();
1541
+ const conv = await nuvem.lerConversa(alvo.id);
1542
+ sp.stop();
1543
+ if (!conv.ok) return conv;
1544
+ // A memória entra ZERADA: o resumo antigo não veio junto, e um
1545
+ // resumo de outra conversa colado aqui faria o modelo responder
1546
+ // sobre coisa que não foi dita nesta.
1547
+ state.history = conv.mensagens.map((m) => ({
1548
+ role: m.papel, content: m.texto,
1549
+ }));
1550
+ state.memoria = memoria.nova();
1551
+ state.gravador.retomar(alvo.id, alvo.titulo, conv.mensagens.length);
1552
+ return conv;
1553
+ }
1554
+
1555
+ /* `primo --continue` / `primo -c`: volta na última conversa, sem passar pelo
1556
+ /chats. A lista do servidor já vem da mais recente para a mais antiga, então
1557
+ a última é a primeira. Sem conta ou sem conversa, começa do zero e diz. */
1558
+ async function continuarUltima() {
1559
+ if (!conta.atual()) {
1560
+ console.log(c.dim(' ⎿ sem conta conectada, não há conversa para continuar — começando do zero'));
1561
+ return false;
1562
+ }
1563
+ const l = await nuvem.listarConversas({ limite: 1 });
1564
+ const alvo = l.ok && l.conversas[0];
1565
+ if (!alvo) {
1566
+ console.log(c.dim(' ⎿ nenhuma conversa salva ainda — começando do zero'));
1567
+ return false;
1568
+ }
1569
+ const conv = await abrirConversa(alvo);
1570
+ if (!conv.ok) {
1571
+ console.log(c.dim(` ⎿ não consegui abrir a última conversa (${conv.error}) — começando do zero`));
1572
+ return false;
1573
+ }
1574
+ const quando = alvo.atualizada ? new Date(alvo.atualizada).toLocaleString('pt-BR') : 'data desconhecida';
1575
+ console.log(c.dim(` ⎿ continuando: ${alvo.titulo.slice(0, 60)} (${quando})`));
1576
+ return true;
1577
+ }
1578
+
1414
1579
  async function handleSlash(text) {
1415
1580
  const [cmd] = text.trim().slice(1).split(/\s+/);
1416
1581
  const rest = text.trim().slice(1 + cmd.length).trim();
@@ -1496,6 +1661,38 @@ async function handleSlash(text) {
1496
1661
  break;
1497
1662
  }
1498
1663
 
1664
+ /* Quando ele pergunta antes de agir. Os três modos e o porquê de cada
1665
+ um estão em lib/perigo.js. */
1666
+ case 'permissoes':
1667
+ case 'permissions': {
1668
+ const DIZ = {
1669
+ auto: 'roda tudo, mas pergunta antes do que não tem volta (apagar fora do projeto, push --force, sudo…)',
1670
+ perguntar: 'pergunta antes de todo comando e de toda escrita de arquivo',
1671
+ livre: 'nunca pergunta — nem antes do que não tem volta',
1672
+ };
1673
+ const atual = perigo.normalizarModo(state.config.permissoes);
1674
+ if (!rest) {
1675
+ console.log(c.muted(' Agora: ') + c.brand(atual) + c.muted(' — ' + DIZ[atual]));
1676
+ for (const m of perigo.MODOS) console.log(` ${m === atual ? c.brand('● ' + m) : c.muted('○ ' + m)} ${c.dim(DIZ[m])}`);
1677
+ console.log(c.dim(' Use /permissoes auto|perguntar|livre'));
1678
+ break;
1679
+ }
1680
+ const pedido = rest.toLowerCase();
1681
+ if (!perigo.MODOS.includes(perigo.normalizarModo(pedido)) || (perigo.normalizarModo(pedido) === 'auto' && pedido !== 'auto')) {
1682
+ console.log(c.err('Uso: /permissoes auto|perguntar|livre'));
1683
+ break;
1684
+ }
1685
+ const m = perigo.normalizarModo(pedido);
1686
+ state.config.permissoes = m;
1687
+ cfg.saveConfig({ permissoes: m });
1688
+ console.log(c.ok(` Permissões: ${m}`) + c.muted(' — ' + DIZ[m]));
1689
+ if (m === 'livre') {
1690
+ console.log(' ' + c.warn('▸▸ Atenção: ele vai apagar, forçar push e usar sudo sem te perguntar.'));
1691
+ console.log(' ' + c.warn(' O que der errado pode não ter volta. /permissoes auto desfaz.'));
1692
+ }
1693
+ break;
1694
+ }
1695
+
1499
1696
  case 'mode': {
1500
1697
  const m = rest.toLowerCase();
1501
1698
  if (['primocode', 'chat', 'auto'].includes(m)) {
@@ -1556,6 +1753,20 @@ async function handleSlash(text) {
1556
1753
  break;
1557
1754
  }
1558
1755
 
1756
+ case 'init': {
1757
+ const alvo = path.join(projectDir(), 'PRIMO.md');
1758
+ if (fs.existsSync(alvo) && !(await confirm(`Já existe um PRIMO.md em ${shortPath(projectDir())}. Escrever outro por cima?`))) {
1759
+ console.log(c.muted(' Deixei o PRIMO.md como estava.'));
1760
+ break;
1761
+ }
1762
+ // Escrever arquivo pede o agente: no modo chat (ou num auto que
1763
+ // achasse que é conversa) o /init só responderia um texto.
1764
+ const modoAntes = state.mode;
1765
+ state.mode = 'primocode';
1766
+ try { await sendPrompt(PEDIDO_INIT); } finally { state.mode = modoAntes; }
1767
+ break;
1768
+ }
1769
+
1559
1770
  case 'detach': state.attachedFiles = []; console.log(c.ok('Anexos removidos.')); refreshPrompt(); break;
1560
1771
 
1561
1772
  case 'files':
@@ -1581,6 +1792,35 @@ async function handleSlash(text) {
1581
1792
  break;
1582
1793
  }
1583
1794
 
1795
+ /* ── /desfazer ────────────────────────────────────────────────
1796
+ * Quem não sabe git não tem como voltar uma edição ruim. Aqui volta:
1797
+ * cada pedido guardou o original dos arquivos que mexeu. */
1798
+ case 'desfazer': case 'undo': case 'rewind': {
1799
+ const cps = checkpoint.lista(projectDir());
1800
+ if (!cps.length) { console.log(c.muted(' Nada para desfazer nesta pasta.')); break; }
1801
+ const quando = (cp) => new Date(cp.quando).toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' });
1802
+ if (/^(lista|list|ls)$/i.test(rest)) {
1803
+ cps.forEach((cp, i) => console.log(` ${c.brand(String(i + 1).padStart(2))} ${c.dim(quando(cp))} `
1804
+ + `${c.white(cp.pedido.slice(0, 50) + (cp.pedido.length > 50 ? '…' : ''))} ${c.muted(cp.arquivos.length + ' arq.')}`));
1805
+ console.log(c.muted('\n /desfazer N volta os N últimos pedidos.'));
1806
+ break;
1807
+ }
1808
+ const n = rest ? parseInt(rest, 10) : 1;
1809
+ if (!(n >= 1)) { console.log(c.err('Uso: /desfazer · /desfazer lista · /desfazer N')); break; }
1810
+ const alvo = cps.slice(0, n);
1811
+ console.log(c.muted(`\n Volta ${alvo.length === 1 ? 'o último pedido' : `os ${alvo.length} últimos pedidos`}:`));
1812
+ for (const cp of alvo) {
1813
+ console.log(` ${c.dim(quando(cp))} ${c.white(cp.pedido.slice(0, 60))}`);
1814
+ for (const a of cp.arquivos) console.log(c.muted(` ${a.existia ? 'volta ao original' : 'apaga (era novo)'} `) + shortPath(a.caminho));
1815
+ }
1816
+ if (!(await confirm('Desfazer?'))) { console.log(c.muted('Cancelado.')); break; }
1817
+ for (const f of checkpoint.desfazer(projectDir(), n)) {
1818
+ const rotulo = f.acao === 'falhou' ? c.err(` falhou: ${f.erro} `) : c.ok(` ${f.acao} `);
1819
+ console.log(rotulo + shortPath(f.caminho));
1820
+ }
1821
+ break;
1822
+ }
1823
+
1584
1824
  case 'tree': console.log(tools.treeDir(projectDir(), parseInt(rest, 10) || 2)); break;
1585
1825
 
1586
1826
  case 'read': {
@@ -1853,6 +2093,7 @@ async function handleSlash(text) {
1853
2093
  return resposta;
1854
2094
  }
1855
2095
  const plan = prepararPasta(texto);
2096
+ checkpoint.iniciar(texto);
1856
2097
  const resultado = await runAct(texto, {
1857
2098
  instrucao: vozModo.COMO_FALAR,
1858
2099
  server: state.config.server,
@@ -1865,6 +2106,7 @@ async function handleSlash(text) {
1865
2106
  ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
1866
2107
  autorizar: autorizarPorVoz,
1867
2108
  controle: state.controle,
2109
+ permissoes: state.config.permissoes,
1868
2110
  });
1869
2111
  const resposta = (resultado.assistantMessage && resultado.assistantMessage.content) || '';
1870
2112
  // A conversa é a MESMA do terminal: sair do modo voz e
@@ -2067,18 +2309,8 @@ async function handleSlash(text) {
2067
2309
  const n = Number(rest);
2068
2310
  const alvo = l.conversas[n - 1];
2069
2311
  if (!alvo) { console.log(c.err(` Não achei a conversa ${rest}.`)); break; }
2070
- const sp2 = new Spinner('abrindo').start();
2071
- const conv = await nuvem.lerConversa(alvo.id);
2072
- sp2.stop();
2312
+ const conv = await abrirConversa(alvo);
2073
2313
  if (!conv.ok) { console.log(c.err(` ${conv.error}`)); break; }
2074
- // A memória entra ZERADA: o resumo antigo não veio junto, e um
2075
- // resumo de outra conversa colado aqui faria o modelo responder
2076
- // sobre coisa que não foi dita nesta.
2077
- state.history = conv.mensagens.map((m) => ({
2078
- role: m.papel, content: m.texto,
2079
- }));
2080
- state.memoria = memoria.nova();
2081
- state.gravador.retomar(alvo.id, alvo.titulo, conv.mensagens.length);
2082
2314
  console.log(c.ok(` ${alvo.titulo}`)
2083
2315
  + c.muted(` · ${conv.mensagens.length} mensagens carregadas`));
2084
2316
  console.log(c.dim(' Pode continuar daqui — o que você disser volta pro app e pro celular.'));
@@ -2154,6 +2386,29 @@ async function handleSlash(text) {
2154
2386
  break;
2155
2387
  }
2156
2388
 
2389
+ /* Os comandos da pessoa: lista, e explica como fazer o dela. */
2390
+ case 'comandos': {
2391
+ const novo = rest.match(/^(novo|new|criar)\s+(\S+)$/i);
2392
+ if (novo) {
2393
+ const r = comandosLib.criar(novo[2]);
2394
+ if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
2395
+ cacheComandos.em = 0;
2396
+ console.log(c.ok(` Criado: /${path.basename(r.arquivo, '.md')}`) + c.muted(' abra e escreva o pedido:'));
2397
+ console.log(' ' + c.white(r.arquivo));
2398
+ break;
2399
+ }
2400
+ const lista = comandosDaPessoa();
2401
+ console.log(c.white('\n Seus comandos'));
2402
+ for (const x of lista) {
2403
+ console.log(` ${c.brand(ui.padRight('/' + x.nome, 14))}${c.dim(ui.padRight(x.origem, 11))}${c.muted(x.descricao)}`);
2404
+ }
2405
+ console.log(c.dim('\n Um arquivo .md vira um comando: revisar.md é /revisar.'));
2406
+ console.log(c.dim(' ~/.primocode/comandos/ vale em todo projeto · <projeto>/.primocode/comandos/ só naquele.'));
2407
+ console.log(c.dim(' $ARGUMENTOS no texto recebe o que você digitar depois: /revisar o login.'));
2408
+ console.log(c.dim(' /comandos novo <nome> cria um para você editar.'));
2409
+ break;
2410
+ }
2411
+
2157
2412
  case 'claude': case 'gpt': case 'codex': case 'key': case 'keys': {
2158
2413
  console.log(c.muted(' Não existe mais provedor para configurar.'));
2159
2414
  console.log(c.muted(' A API já vem com o PrimoCode: entrou na conta, pode usar.'));
@@ -2165,7 +2420,18 @@ async function handleSlash(text) {
2165
2420
  // "/" sozinho abre o menu; um prefixo único autopreenche e executa
2166
2421
  // ("/eff" → /effort); prefixo ambíguo mostra as opções.
2167
2422
  if (!cmd) { mostrarMenuSlash(); break; }
2168
- const casam = SLASH_COMANDOS.map(([nome]) => nome.slice(1)).filter((n) => n.startsWith(cmd.toLowerCase()));
2423
+ // Chegou aqui, não é embutido: o switch já atendeu todos eles.
2424
+ // Por isso um /help.md da pessoa nunca troca o /help de verdade.
2425
+ const proprio = comandosDaPessoa().find((x) => x.nome === nome);
2426
+ if (proprio) {
2427
+ console.log(c.dim(` ⎿ /${proprio.nome} · ${proprio.origem}`));
2428
+ // O MESMO caminho de um pedido digitado: modo, pasta, plano,
2429
+ // tudo. Um atalho que pulasse o sendPrompt pularia junto o
2430
+ // portão do plano e a escolha da pasta.
2431
+ await sendPrompt(comandosLib.expandir(proprio, rest));
2432
+ break;
2433
+ }
2434
+ const casam = comandosDoMenu().map(([nome]) => nome.slice(1)).filter((n) => n.startsWith(cmd.toLowerCase()));
2169
2435
  if (casam.length === 1) {
2170
2436
  console.log(c.dim(` → /${casam[0]}`));
2171
2437
  await handleSlash('/' + casam[0] + (rest ? ' ' + rest : ''));
@@ -2174,7 +2440,7 @@ async function handleSlash(text) {
2174
2440
  if (casam.length > 1) {
2175
2441
  console.log(c.muted(` /${cmd}… pode ser:`));
2176
2442
  for (const n of casam) {
2177
- const [, desc] = SLASH_COMANDOS.find(([nome]) => nome === '/' + n);
2443
+ const [, desc] = comandosDoMenu().find(([nome]) => nome === '/' + n);
2178
2444
  console.log(` ${c.brand(ui.padRight('/' + n, 12))}${c.dim(desc)}`);
2179
2445
  }
2180
2446
  break;
@@ -2247,7 +2513,11 @@ async function main() {
2247
2513
  // mesmo que uma refatoração.
2248
2514
  state.mode = ['primocode', 'chat', 'auto'].includes(state.config.mode) ? state.config.mode : 'auto';
2249
2515
 
2250
- const args = process.argv.slice(2);
2516
+ // `-c` / `--continue` pode vir em qualquer posição e sai da lista: o
2517
+ // resto (one-shot, -f) segue sendo lido do jeito de sempre.
2518
+ const todos = process.argv.slice(2);
2519
+ const continuar = todos.some((a) => a === '-c' || a === '--continue');
2520
+ const args = todos.filter((a) => a !== '-c' && a !== '--continue');
2251
2521
 
2252
2522
  // Flags padrão de CLI — funcionam em qualquer máquina, úteis em scripts/CI.
2253
2523
  if (args[0] === '--version' || args[0] === '-v' || args[0] === '-V') {
@@ -2312,28 +2582,23 @@ async function main() {
2312
2582
  const fileFlag = args.indexOf('-f');
2313
2583
  const words = fileFlag >= 0 ? args.slice(0, fileFlag) : args;
2314
2584
  const promptText = words.join(' ');
2315
- let fullPrompt = promptText;
2316
2585
 
2317
2586
  if (fileFlag >= 0 && args[fileFlag + 1]) {
2318
2587
  try {
2319
2588
  const fc = fs.readFileSync(path.resolve(args[fileFlag + 1]), 'utf-8');
2320
- fullPrompt = `Arquivo: ${args[fileFlag + 1]}\n\`\`\`\n${fc}\n\`\`\`\n\n${promptText}`;
2589
+ state.attachedFiles.push({ path: args[fileFlag + 1], content: fc });
2321
2590
  } catch { console.log(c.err(`Não consegui ler: ${args[fileFlag + 1]}`)); process.exit(1); }
2322
2591
  }
2323
2592
 
2324
- // Mesma função do modo interativo: detecta a pasta, adota projeto que já
2325
- // existe, e só então decide se há projeto novo pendente.
2326
- const plan = prepararPasta(promptText);
2327
- await runAct(fullPrompt, {
2328
- server: state.config.server,
2329
- model: state.config.model,
2330
- effort: state.config.effort,
2331
- token: state.config.token,
2332
- history: [],
2333
- projectDir: plan.dir,
2334
- projectName: plan.name || state.currentProject,
2335
- ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
2336
- });
2593
+ // Com -c, o pedido continua a última conversa em vez de chegar sozinho.
2594
+ if (continuar) await continuarUltima();
2595
+ /* O MESMO caminho do pedido digitado (sendPrompt). Este modo tinha um
2596
+ runAct próprio, e por isso ficava sem tudo o que vive lá: o kit de
2597
+ partida, o plano em passos, a retomada automática, o @arquivo, o
2598
+ checkpoint do /desfazer. Medido em 24/09: `primo "crie uma landing…"`
2599
+ não pegou o kit. */
2600
+ state.semTerminal = true;
2601
+ await sendPrompt(promptText);
2337
2602
  process.exit(0);
2338
2603
  }
2339
2604
 
@@ -2345,7 +2610,7 @@ async function main() {
2345
2610
  }));
2346
2611
  rl = entrada.criar({
2347
2612
  status: barraDeStatus,
2348
- comandos: () => SLASH_COMANDOS,
2613
+ comandos: comandosDoMenu,
2349
2614
  vazio: 'peça alguma coisa, ou / para os comandos',
2350
2615
  });
2351
2616
 
@@ -2484,15 +2749,19 @@ async function main() {
2484
2749
  // o primocode-server, que tem a chave do provedor.
2485
2750
  state.liberado = !r || r.liberado !== false;
2486
2751
  state.motivoBloqueio = r && r.motivo;
2487
- }).catch(() => { state.liberado = true; }).then(() => {
2488
- // O que sobrou de adiantado não era resposta de nada: vira comando,
2489
- // na ordem em que foi digitado. Nada se perde, só esperou a vez.
2490
- segurando = false;
2491
- while (adiantadas.length) queue.push(adiantadas.shift());
2492
- if (queue.length) drain();
2493
- else if (!closed) rl.prompt();
2494
- conferirServidor();
2495
- });
2752
+ }).catch(() => { state.liberado = true; })
2753
+ // Depois da abertura, porque é nela que a pessoa entra na conta — e
2754
+ // sem conta não há conversa para achar.
2755
+ .then(() => (continuar ? continuarUltima().catch(() => false) : null))
2756
+ .then(() => {
2757
+ // O que sobrou de adiantado não era resposta de nada: vira comando,
2758
+ // na ordem em que foi digitado. Nada se perde, só esperou a vez.
2759
+ segurando = false;
2760
+ while (adiantadas.length) queue.push(adiantadas.shift());
2761
+ if (queue.length) drain();
2762
+ else if (!closed) rl.prompt();
2763
+ conferirServidor();
2764
+ });
2496
2765
  }
2497
2766
 
2498
2767
  main().catch((e) => { console.error(c.err(e.message || e)); process.exit(1); });
@@ -0,0 +1,14 @@
1
+ ---
2
+ descricao: explica este projeto (ou um arquivo) para quem não programa
3
+ ---
4
+ Me explique como se eu nunca tivesse programado: $ARGUMENTOS
5
+
6
+ Se eu não disse o que explicar, explique o projeto inteiro desta pasta. Leia os arquivos antes de falar, não chute.
7
+
8
+ Responda nesta ordem, em frases curtas:
9
+ 1. o que isto faz, em uma frase;
10
+ 2. as partes principais e o papel de cada uma (use comparações do dia a dia);
11
+ 3. como eu rodo e vejo funcionando;
12
+ 4. onde eu mexo para mudar o que é mais provável que eu queira mudar.
13
+
14
+ Sem jargão. Se uma palavra técnica for inevitável, explique ela na hora.