primocode 9.8.0-beta.4 → 9.8.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +15 -4
  2. package/app/ansi.js +55 -0
  3. package/app/ide.css +345 -0
  4. package/app/ide.html +183 -0
  5. package/app/ide.js +639 -0
  6. package/bin/primocode.js +315 -83
  7. package/kits/acolhedor/index.html +130 -0
  8. package/kits/acolhedor/script.js +25 -0
  9. package/kits/acolhedor/style.css +146 -0
  10. package/kits/editorial/index.html +115 -0
  11. package/kits/editorial/script.js +19 -0
  12. package/kits/editorial/style.css +112 -0
  13. package/kits/premium/index.html +83 -0
  14. package/kits/premium/script.js +11 -0
  15. package/kits/premium/style.css +87 -0
  16. package/kits/vitrine/index.html +108 -0
  17. package/kits/vitrine/script.js +20 -0
  18. package/kits/vitrine/style.css +117 -0
  19. package/lib/app.js +41 -6
  20. package/lib/browser.js +14 -1
  21. package/lib/checkpoint-git.js +218 -0
  22. package/lib/checkpoint.js +159 -44
  23. package/lib/comandos-extra.js +481 -0
  24. package/lib/config.js +5 -2
  25. package/lib/ide.js +337 -0
  26. package/lib/kits.js +165 -54
  27. package/lib/skills.js +6 -1
  28. package/lib/ui.js +171 -17
  29. package/package.json +2 -2
  30. package/skills/apresentacao-pptx/SKILL.md +52 -0
  31. package/skills/cartaz-de-arte/LICENSE.txt +202 -0
  32. package/skills/cartaz-de-arte/SKILL.md +47 -0
  33. package/skills/criar-skill/LICENSE.txt +202 -0
  34. package/skills/criar-skill/SKILL.md +49 -0
  35. package/skills/depurar-de-verdade/LICENSE.txt +21 -0
  36. package/skills/depurar-de-verdade/SKILL.md +14 -5
  37. package/skills/documento-word/SKILL.md +57 -0
  38. package/skills/interface-revisada/SKILL.md +47 -0
  39. package/skills/me-ensina/LICENSE.txt +21 -0
  40. package/skills/me-ensina/SKILL.md +27 -0
  41. package/skills/me-entrevista/LICENSE.txt +21 -0
  42. package/skills/me-entrevista/SKILL.md +27 -0
  43. package/skills/pdf-bonito/SKILL.md +48 -0
  44. package/skills/planilha-excel/SKILL.md +47 -0
  45. package/skills/react-sem-dor/SKILL.md +48 -0
  46. package/skills/revisao-antes-de-entregar/LICENSE.txt +21 -0
  47. package/skills/revisao-antes-de-entregar/SKILL.md +6 -1
  48. package/skills/temas-prontos/LICENSE.txt +202 -0
  49. package/skills/temas-prontos/SKILL.md +50 -0
  50. package/skills/testar-o-site/LICENSE.txt +202 -0
  51. package/skills/testar-o-site/SKILL.md +46 -0
  52. package/skills/video-no-remotion/SKILL.md +48 -0
package/bin/primocode.js CHANGED
@@ -43,6 +43,16 @@ const conta = require('../lib/conta');
43
43
  const nuvem = require('../lib/nuvem');
44
44
  const compat = require('../lib/compatibilidade');
45
45
  const mencoesLib = require('../lib/mencoes');
46
+ const extra = require('../lib/comandos-extra');
47
+
48
+ /* O /custo: toda conversa com o modelo passa pelo lib/api.js, e o contador
49
+ embrulha as funções dele uma vez, aqui. Quem atende é o provedor do /api
50
+ quando há um, senão o servidor — é por eles que o /custo separa. */
51
+ const contadorDaSessao = extra.novoContador();
52
+ extra.instalarContador(api, contadorDaSessao, () => {
53
+ const p = provedorLocal.atual();
54
+ return p ? p.nome : 'Servidor PrimoCode';
55
+ });
46
56
 
47
57
  // Rede de segurança de último recurso: sem isto, uma exceção fora de um
48
58
  // try/catch (ex.: dentro de um callback de socket, como o parser de SSE) não
@@ -97,10 +107,34 @@ const state = {
97
107
  // existe para a frente em vez de subir outro servidor ao lado.
98
108
  let janelaAberta = null;
99
109
 
110
+ /* O que a janela do /rc, a IDE do /ide e o `primo --app` passam ao
111
+ lib/app.js. Uma função só porque os três sobem o MESMO servidor: se cada um
112
+ montasse o seu, o /ide aberto depois do /rc acharia um servidor sem a pasta
113
+ do projeto — e a IDE nasceria sem árvore. */
114
+ function opcoesDaJanela(extra) {
115
+ return Object.assign({
116
+ versao: VERSION,
117
+ enviar: (t) => sendPrompt(t),
118
+ barra: (t) => handleSlash(t),
119
+ interromper,
120
+ estado: () => ({
121
+ pasta: state.workDir ? shortPath(state.workDir) : null,
122
+ projeto: state.currentProject,
123
+ modo: state.mode,
124
+ esforco: state.config.effort,
125
+ }),
126
+ // Lida na hora, não guardada: o /dir e o /project trocam a pasta
127
+ // com a IDE aberta, e ela tem de acompanhar.
128
+ pastaProjeto: () => projectDir(),
129
+ }, extra || {});
130
+ }
131
+
100
132
  const HELP = `
101
133
  ${c.bold('Comandos')}
102
134
  ${c.brand('/dir')} <caminho> trabalha numa pasta que já existe (onde você quiser)
103
135
  ${c.brand('/project')} lista projetos ou cria em ~/primocode: /project <nome>
136
+ ${c.brand('/projetos')} [n] projetos do mais recente, com tamanho · /projetos 2 entra
137
+ ${c.brand('/novo')} <descrição> começa um projeto NOVO do zero, em pasta própria
104
138
  ${c.brand('/mode')} /mode primocode|chat|auto
105
139
  ${c.brand('/effort')} esforço do agente — do rápido ao mais esperto
106
140
  ${c.brand('/permissoes')} quando ele pergunta antes de agir: auto|perguntar|livre
@@ -114,14 +148,21 @@ ${c.bold('Comandos')}
114
148
  ${c.brand('/read')} <arquivo> mostra um arquivo
115
149
  ${c.brand('/run')} <comando> executa no shell do projeto
116
150
  ${c.brand('/open')} <arq|url> abre no navegador
151
+ ${c.brand('/abrir')} abre o projeto: o servidor de pé, ou a index.html
152
+ ${c.brand('/copiar')} copia a última resposta
153
+ ${c.brand('/historico')} [termo] procura nos seus pedidos antigos
117
154
  ${c.brand('/rc')} abre a janela do PrimoCode (chat, sem terminal)
155
+ ${c.brand('/ide')} abre a IDE: arquivos, editor, prévia do site e o chat
118
156
  ${c.brand('/login')} entra na sua conta · ${c.brand('/account')} mostra o plano
119
157
  ${c.brand('/chats')} [n] suas conversas — as mesmas do app e do celular
120
158
  ${c.brand('/logout')} desconecta este computador da conta
121
159
  ${c.brand('/comandos')} seus comandos (/revisar, /explicar…) · /comandos novo <nome>
122
160
  ${c.brand('/status')} checa o servidor
161
+ ${c.brand('/doctor')} confere tudo (servidor, /api, Chrome, disco…) e diz como consertar
162
+ ${c.brand('/custo')} tokens e pedidos desta sessão, por provedor
163
+ ${c.brand('/atalhos')} as teclas que ajudam
123
164
  ${c.brand('/name')} <nome> como o Primo te chama
124
- ${c.brand('/mascot')} <estilo> mascote: primo|fox|cat
165
+ ${c.brand('/mascot')} [nome|n] galeria dos mascotes · /mascot reacoes on|off
125
166
  ${c.brand('/config')} mostra suas preferências
126
167
  ${c.brand('/clear')} limpa a conversa ${c.brand('/cls')} limpa a tela
127
168
  ${c.brand('/uninstall')} remove o PrimoCode
@@ -147,6 +188,8 @@ ${c.bold('Onde ele trabalha')}
147
188
  const SLASH_COMANDOS = [
148
189
  ['/help', 'todos os comandos'],
149
190
  ['/project', 'lista/cria projetos em ~/primocode'],
191
+ ['/projetos', 'projetos do mais recente, com tamanho · /projetos 2 entra'],
192
+ ['/novo', 'começa um projeto novo do zero: /novo site da padaria'],
150
193
  ['/dir', 'trabalhar numa pasta sua'],
151
194
  ['/mode', 'primocode | chat | auto'],
152
195
  ['/effort', 'esforço: low → ultracode (←/→ para ajustar)'],
@@ -162,7 +205,11 @@ const SLASH_COMANDOS = [
162
205
  ['/read', 'mostra um arquivo'],
163
206
  ['/run', 'roda um comando'],
164
207
  ['/open', 'abre no navegador'],
208
+ ['/abrir', 'abre o projeto atual: o servidor de pé, ou a index.html'],
209
+ ['/copiar', 'copia a última resposta'],
210
+ ['/historico', 'procura nos seus pedidos antigos: /historico login'],
165
211
  ['/rc', 'abre a janela do PrimoCode'],
212
+ ['/ide', 'abre a IDE: arquivos, editor, prévia e o chat, lado a lado'],
166
213
  ['/login', 'entra na sua conta Conecta Primo'],
167
214
  ['/account', 'sua conta, plano e computadores conectados'],
168
215
  ['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
@@ -176,9 +223,12 @@ const SLASH_COMANDOS = [
176
223
  ['/comandos', 'seus comandos: um .md vira /nome · /comandos novo <nome>'],
177
224
  ['/api', 'usa a SUA chave (Gemini, ChatGPT, Claude…) direto daqui, sem o limite do servidor'],
178
225
  ['/status', 'checa o servidor'],
226
+ ['/doctor', 'confere se está tudo certo e diz como consertar'],
227
+ ['/custo', 'tokens e pedidos desta sessão, por provedor'],
228
+ ['/atalhos', 'as teclas que ajudam'],
179
229
  ['/connectors', 'quem está atendendo os seus pedidos'],
180
230
  ['/name', 'como ele te chama'],
181
- ['/mascot', 'primo | fox | cat'],
231
+ ['/mascot', 'galeria dos mascotes · escolha por nome ou número'],
182
232
  ['/config', 'suas preferências'],
183
233
  ['/clear', 'limpa a conversa'],
184
234
  ['/cls', 'limpa a tela'],
@@ -654,6 +704,8 @@ function ehSoEstudio(texto) {
654
704
  }
655
705
 
656
706
  function planWorkDir(userText) {
707
+ // O /novo já criou a pasta e entrou nela: nenhuma heurística decide outra.
708
+ if (state.projetoFixo) return { dir: state.workDir, pending: false, name: state.currentProject };
657
709
  const escolhida = ['/dir', 'detectada', 'projeto'].includes(state.workDirOrigem);
658
710
  // Pasta escolhida a dedo (/dir, ou dita no pedido) manda sempre. Projeto
659
711
  // em andamento manda também — a menos que o pedido abra outra coisa.
@@ -675,36 +727,27 @@ function planWorkDir(userText) {
675
727
  paralelo com o trabalho: a pasta só nasce na primeira escrita, e até lá a
676
728
  resposta já chegou. Só pergunta quando o pedido é de CRIAR algo — pedir
677
729
  para abrir o Chrome não gasta chamada com nome de pasta. */
730
+ async function perguntarNome(texto) {
731
+ let resposta = '';
732
+ await api.requestChat(state.config.server, {
733
+ prompt: texto, history: [], model: state.config.model, token: state.config.token,
734
+ }, (evt) => { if (evt.delta) resposta += evt.delta; });
735
+ return resposta;
736
+ }
737
+
678
738
  function batizarDepois(plan, userText) {
679
739
  if (!plan.pending || !(RE_CRIAR_UM.test(semAcento(userText)) || PEDIDO_DE_CODIGO.test(userText))) return;
680
- const perguntar = async (texto) => {
681
- let resposta = '';
682
- await api.requestChat(state.config.server, {
683
- prompt: texto, history: [], model: state.config.model, token: state.config.token,
684
- }, (evt) => { if (evt.delta) resposta += evt.delta; });
685
- return resposta;
686
- };
687
- plan.batizando = nomeProjeto.escolher({ pedido: userText, nomes: cfg.listProjects(), perguntar })
740
+ plan.batizando = nomeProjeto.escolher({ pedido: userText, nomes: cfg.listProjects(), perguntar: perguntarNome })
688
741
  .then((nome) => { if (nome) plan.nomeEscolhido = nome; });
689
742
  }
690
743
 
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. */
744
+ /* A inspiração visual (lib/kits.js): pedido de landing leva a ficha da
745
+ direção que combina com o negócio. Nada é copiado para a pasta — o agente
746
+ escreve a página dele, inspirado, e não troca texto num molde. */
694
747
  async function prepararKit(plan, userText) {
695
748
  const kit = kits.escolher(userText, { provedor: provedorLocal.atual() });
696
749
  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`));
750
+ console.log(c.dim(` ⎿ inspiração visual: ${kit.titulo}`));
708
751
  return kit.instrucao;
709
752
  }
710
753
 
@@ -1067,7 +1110,9 @@ async function sendPrompt(userText) {
1067
1110
  balanços, quer saber o que existe no disco quando ele terminar. */
1068
1111
  const livro = registro.novo();
1069
1112
  // Um checkpoint por PEDIDO (não por passo): é o que o /desfazer volta.
1070
- checkpoint.iniciar(userText);
1113
+ // Com a pasta: a foto do projeto inteiro sai AGORA, antes de qualquer
1114
+ // tool — é ela que dá volta ao `rm`/`mv` que o run_shell fizer.
1115
+ checkpoint.iniciar(userText, projectDir());
1071
1116
  for (let i = 0; i < passos.length; i++) {
1072
1117
  if (i > 0) console.log(c.dim(` ⎿ ${passos[i].titulo}`));
1073
1118
  // Só fato de passo CONCLUÍDO entra aqui. Num pedido real o passo de
@@ -1153,6 +1198,12 @@ async function sendPrompt(userText) {
1153
1198
  arquivos é a conferência, e conferência se lê por último. */
1154
1199
  const balanco = registro.resumo(livro, { colunas: process.stdout.columns || 80 });
1155
1200
  if (balanco) console.log(balanco);
1201
+ // O rostinho do mascote, por último e numa linha só. O humor sai do
1202
+ // que aconteceu, não de adivinhar pelo texto da resposta.
1203
+ if (state.config.reacoes !== false && r) {
1204
+ const humor = r.parado ? 'parado' : r.concluiu === false ? 'preocupado' : 'feliz';
1205
+ console.log(ui.reacao(state.config.mascot, humor));
1206
+ }
1156
1207
  state.history.push({ role: 'user', content: userText });
1157
1208
  state.history.push({ role: 'assistant', content: r.assistantMessage.content });
1158
1209
  resumirDepois();
@@ -1526,7 +1577,10 @@ const RENOMEADOS = {
1526
1577
  conta: 'account',
1527
1578
  sair: 'exit', // sair é SAIR; desconectar a conta é /logout
1528
1579
  // o histórico
1529
- conversas: 'chats', historico: 'chats', histórico: 'chats',
1580
+ // /historico deixou de ser apelido do /chats: agora procura nos pedidos
1581
+ // guardados nesta máquina (lib/comandos-extra.js). /chats segue sendo a
1582
+ // conversa inteira, da conta.
1583
+ conversas: 'chats',
1530
1584
  // o resto
1531
1585
  pasta: 'dir',
1532
1586
  esforco: 'effort', esforço: 'effort',
@@ -1590,16 +1644,33 @@ async function handleSlash(text) {
1590
1644
  switch (nome) {
1591
1645
  case 'help': console.log(HELP); break;
1592
1646
 
1593
- case 'project': {
1594
- if (!rest) {
1595
- const projects = cfg.listProjects();
1596
- if (!projects.length) { console.log(c.muted(' Nenhum projeto ainda. Peça algo e eu crio o primeiro.')); break; }
1597
- console.log('');
1598
- for (const p of projects) {
1599
- const mark = p === state.currentProject ? c.brand(' ●') : ' ';
1600
- console.log(`${mark} ${p === state.currentProject ? c.brand(p) : c.white(p)}`);
1647
+ /* /projetos e /project sem nome são a MESMA lista: antes o /project só
1648
+ listava os nomes, em ordem alfabética, e quem procurava "o de ontem"
1649
+ tinha de abrir pasta por pasta. Agora vem do mais recente, com
1650
+ quando mexeu e o tamanho, e um número para entrar. */
1651
+ case 'projetos': case 'project': {
1652
+ const lista = extra.listarProjetos(cfg.ROOT_DIR);
1653
+ const numero = /^\d+$/.test(rest) ? Number(rest) : 0;
1654
+ if (!rest || (numero && nome === 'projetos') || (numero && numero <= lista.length)) {
1655
+ if (!lista.length) { console.log(c.muted(' Nenhum projeto ainda. Peça algo e eu crio o primeiro.')); break; }
1656
+ if (numero) {
1657
+ const alvo = lista[numero - 1];
1658
+ if (!alvo) { console.log(c.err(` Não há projeto ${numero}. São ${lista.length}.`)); break; }
1659
+ entrarNoProjeto(alvo.dir, 'projeto');
1660
+ console.log(c.ok(` Trabalhando em ${alvo.nome}`) + c.muted(` (${shortPath(alvo.dir)})`));
1661
+ refreshPrompt();
1662
+ break;
1601
1663
  }
1602
- console.log(c.muted(`\n ${cfg.ROOT_DIR}`));
1664
+ console.log('');
1665
+ const largura = Math.min(34, Math.max(...lista.map((x) => x.nome.length)));
1666
+ lista.forEach((x, i) => {
1667
+ const aqui = x.nome === state.currentProject;
1668
+ const nomeCol = ui.padRight(ui.truncate(aqui ? c.brand(x.nome) : c.white(x.nome), largura), largura);
1669
+ console.log(` ${aqui ? c.brand('●') : ' '} ${c.dim(String(i + 1).padStart(2))} ${nomeCol} `
1670
+ + c.muted(ui.padRight(extra.quandoFoi(x.modificado), 11))
1671
+ + c.dim(`${extra.fmtTamanho(x.bytes)}${x.cortado ? '+' : ''} · ${x.arquivos} arq.`));
1672
+ });
1673
+ console.log(c.dim(`\n ${shortPath(cfg.ROOT_DIR)} · /projetos 1 entra no primeiro · /novo <descrição> começa outro`));
1603
1674
  break;
1604
1675
  }
1605
1676
  const dir = cfg.createProjectDir(rest);
@@ -1794,25 +1865,31 @@ async function handleSlash(text) {
1794
1865
 
1795
1866
  /* ── /desfazer ────────────────────────────────────────────────
1796
1867
  * 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. */
1868
+ * cada pedido tirou uma foto do projeto (com git) ou guardou o
1869
+ * original dos arquivos que mexeu (sem git). */
1798
1870
  case 'desfazer': case 'undo': case 'rewind': {
1799
1871
  const cps = checkpoint.lista(projectDir());
1800
1872
  if (!cps.length) { console.log(c.muted(' Nada para desfazer nesta pasta.')); break; }
1801
1873
  const quando = (cp) => new Date(cp.quando).toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' });
1802
1874
  if (/^(lista|list|ls)$/i.test(rest)) {
1803
1875
  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.')}`));
1876
+ + `${c.white(cp.pedido.slice(0, 50) + (cp.pedido.length > 50 ? '…' : ''))} `
1877
+ + c.muted(`${cp.mudou.length} ${cp.mudou.length === 1 ? 'arquivo mudou' : 'arquivos mudaram'}`)));
1805
1878
  console.log(c.muted('\n /desfazer N volta os N últimos pedidos.'));
1879
+ if (checkpoint.modo(projectDir()) === 'arquivo') {
1880
+ console.log(c.muted(' Sem git aqui: só volta o que o agente escreveu com as ferramentas de arquivo (não o que um comando apagou).'));
1881
+ }
1806
1882
  break;
1807
1883
  }
1808
1884
  const n = rest ? parseInt(rest, 10) : 1;
1809
1885
  if (!(n >= 1)) { console.log(c.err('Uso: /desfazer · /desfazer lista · /desfazer N')); break; }
1810
1886
  const alvo = cps.slice(0, n);
1811
1887
  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
- }
1888
+ for (const cp of alvo) console.log(` ${c.dim(quando(cp))} ${c.white(cp.pedido.slice(0, 60))}`);
1889
+ const previa = checkpoint.previa(projectDir(), n);
1890
+ if (!previa.length) console.log(c.muted(' (o disco já está como estava antes)'));
1891
+ for (const a of previa.slice(0, 40)) console.log(c.muted(` ${a.acao} `) + shortPath(a.caminho));
1892
+ if (previa.length > 40) console.log(c.muted(` … e mais ${previa.length - 40}`));
1816
1893
  if (!(await confirm('Desfazer?'))) { console.log(c.muted('Cancelado.')); break; }
1817
1894
  for (const f of checkpoint.desfazer(projectDir(), n)) {
1818
1895
  const rotulo = f.acao === 'falhou' ? c.err(` falhou: ${f.erro} `) : c.ok(` ${f.acao} `);
@@ -1843,8 +1920,57 @@ async function handleSlash(text) {
1843
1920
  break;
1844
1921
  }
1845
1922
 
1846
- case 'open': {
1847
- if (!rest) { console.log(c.err('Uso: /open <arquivo|url>')); break; }
1923
+ /* /novo: projeto novo por ORDEM da pessoa. Sem ele, "crie um site de
1924
+ receitas" pode cair dentro de "site-receitas-velho" porque o nome
1925
+ parece (findSimilarProject) — e quem quer começar do zero não tem
1926
+ como dizer isso. Aqui a pasta nasce antes, com nome que não colide,
1927
+ e o pedido roda preso nela. */
1928
+ case 'novo': case 'new': {
1929
+ if (!rest) { console.log(c.err('Uso: /novo <o que você quer criar>') + c.dim(' ex.: /novo site da padaria')); break; }
1930
+ const nomes = cfg.listProjects();
1931
+ const sp = new Spinner('escolhendo o nome da pasta').start();
1932
+ const sugerido = await nomeProjeto.escolher({ pedido: rest, nomes, perguntar: perguntarNome }).catch(() => null);
1933
+ sp.stop();
1934
+ const nomeNovo = extra.nomeDoNovo({
1935
+ descricao: rest, nomes, sugerido, derivar: deriveProjectName,
1936
+ slugify: cfg.slugify, semColisao: nomeProjeto.semColisao,
1937
+ });
1938
+ const dir = cfg.createProjectDir(nomeNovo);
1939
+ entrarNoProjeto(dir, 'projeto');
1940
+ // Conversa nova junto: o que se falou do projeto anterior puxaria o
1941
+ // agente de volta para ele.
1942
+ state.history = []; state.memoria = memoria.nova(); state.gravador.zerar();
1943
+ console.log(c.ok(` Projeto novo: ${state.currentProject}`) + c.muted(` (${shortPath(dir)})`));
1944
+ refreshPrompt();
1945
+ const modoAntes = state.mode;
1946
+ state.mode = 'primocode';
1947
+ state.projetoFixo = true;
1948
+ try { await sendPrompt(extra.pedidoDoNovo(rest, state.currentProject)); }
1949
+ finally { state.mode = modoAntes; state.projetoFixo = false; }
1950
+ break;
1951
+ }
1952
+
1953
+ /* /open sem nada (e /abrir) acha sozinho o que abrir: o servidor do
1954
+ projeto de pé, ou a index.html. Antes pedia o caminho, e quem não
1955
+ sabe que a página se chama index.html ficava parado aqui. */
1956
+ case 'abrir': case 'open': {
1957
+ if (!rest) {
1958
+ const pastaAtual = projectDir();
1959
+ if (path.resolve(pastaAtual) === path.resolve(cfg.ROOT_DIR)) {
1960
+ console.log(c.muted(' Nenhum projeto aberto.') + c.dim(' /projetos para escolher um'));
1961
+ break;
1962
+ }
1963
+ const alvo = await extra.acharAlvoAbrir(pastaAtual);
1964
+ if (!alvo) {
1965
+ console.log(c.muted(` Não achei página nem servidor em ${shortPath(pastaAtual)}.`));
1966
+ console.log(c.dim(' Peça "roda o projeto" ao Primo, ou /open <arquivo>.'));
1967
+ break;
1968
+ }
1969
+ const r = await tools.openInBrowser(alvo.url || alvo.caminho);
1970
+ if (r && r.ok === false) { console.log(c.err(` Não consegui abrir: ${r.error}`)); break; }
1971
+ console.log(c.ok(` Aberto: ${alvo.url || shortPath(alvo.caminho)}`) + c.dim(` (${alvo.porque})`));
1972
+ break;
1973
+ }
1848
1974
  const target = /^https?:\/\//i.test(rest) ? rest : path.resolve(projectDir(), rest);
1849
1975
  await tools.openInBrowser(target);
1850
1976
  console.log(c.ok(`Aberto: ${rest}`));
@@ -1859,32 +1985,47 @@ async function handleSlash(text) {
1859
1985
  break;
1860
1986
  }
1861
1987
 
1862
- case 'mascot': {
1863
- const estilos = ['primo', 'fox', 'cat'];
1864
- const m = rest.toLowerCase();
1865
- if (!m) {
1988
+ /* A galeria: todos lado a lado, número e nome em cima. Escolhe por
1989
+ nome ou número — na hora, se há teclado; senão pelo /mascot <n>.
1990
+ Num pipe não pergunta: a "resposta" seria o próximo comando. */
1991
+ case 'mascot': case 'mascote': {
1992
+ const reac = rest.match(/^(reac(?:oes|ões)?|rea[cç][aã]o|reactions?)\s*(on|off|liga[r]?|desliga[r]?|sim|n[aã]o)?$/i);
1993
+ if (reac) {
1994
+ const quer = reac[2] ? /^(on|liga|ligar|sim)$/i.test(reac[2]) : state.config.reacoes === false;
1995
+ state.config.reacoes = quer;
1996
+ cfg.saveConfig({ reacoes: quer });
1997
+ console.log(c.ok(` Reações do mascote: ${quer ? 'ligadas' : 'desligadas'}`)
1998
+ + (quer ? ' ' + ui.reacao(state.config.mascot, 'feliz').trim() : ''));
1999
+ break;
2000
+ }
2001
+ let pedido = rest;
2002
+ if (!pedido) {
1866
2003
  console.log('');
1867
- for (const e of estilos) {
1868
- console.log(` ${e === state.config.mascot ? c.brand('● ' + e) : c.muted('○ ' + e)}`);
1869
- ui.mascot(e).forEach((l) => console.log(' ' + l));
1870
- console.log('');
2004
+ console.log(ui.galeriaMascotes(state.config.mascot || 'primo', process.stdout.columns || 80));
2005
+ if (!process.stdin.isTTY || state.semTerminal) {
2006
+ console.log(c.muted(' Escolha: /mascot <nome ou número>') + c.dim(' · /mascot reacoes off desliga o rostinho'));
2007
+ break;
1871
2008
  }
1872
- console.log(c.muted(' Escolha: /mascot <estilo>'));
1873
- break;
2009
+ pedido = String(await ask(` ${c.brand('?')} Nome ou número ${c.dim('(Enter mantém)')}: `)).trim();
2010
+ if (!pedido) { console.log(c.muted(' Nada mudou.')); break; }
1874
2011
  }
1875
- if (!estilos.includes(m)) { console.log(c.err(`Uso: /mascot ${estilos.join('|')}`)); break; }
2012
+ const m = ui.resolverMascote(pedido);
2013
+ if (!m) { console.log(c.err(` Não conheço "${pedido}".`) + c.dim(` São: ${ui.MASCOTES.join(', ')}`)); break; }
1876
2014
  state.config.mascot = m;
1877
2015
  cfg.saveConfig({ mascot: m });
2016
+ const info = ui.mascoteInfo(m);
1878
2017
  console.log('');
1879
- ui.mascot(m).forEach((l) => console.log(' ' + l));
1880
- console.log(c.ok(`\nMascote: ${m}`));
2018
+ const arte = ui.mascot(m);
2019
+ arte.forEach((l, i) => console.log(' ' + l + (i === 0 ? ' ' + c.bold(c.white(info.nome))
2020
+ : i === 1 ? ' ' + c.dim(`"${info.frase}"`) : '')));
2021
+ console.log(c.ok(`\n Mascote: ${m}`));
1881
2022
  break;
1882
2023
  }
1883
2024
 
1884
2025
  case 'config': {
1885
2026
  console.log('');
1886
2027
  console.log(` ${c.muted('nome')} ${c.white(state.config.userName || '(nenhum)')}`);
1887
- console.log(` ${c.muted('mascote')} ${c.white(state.config.mascot || 'primo')}`);
2028
+ console.log(` ${c.muted('mascote')} ${c.white(state.config.mascot || 'primo')}${c.dim(state.config.reacoes === false ? ' · sem reações' : ' · com reações')}`);
1888
2029
  console.log(` ${c.muted('esforço')} ${c.white(state.config.effort)}${c.dim(' · /effort muda')}`);
1889
2030
  console.log(` ${c.muted('modelo')} ${c.white(state.config.model)}${c.dim(' (vem do esforço)')}`);
1890
2031
  console.log(` ${c.muted('modo')} ${c.white(state.mode)}`);
@@ -1923,6 +2064,78 @@ async function handleSlash(text) {
1923
2064
  state.gravador.zerar(); console.log(c.ok('Conversa limpa.')); break;
1924
2065
  case 'cls': process.stdout.write('\x1Bc'); break;
1925
2066
 
2067
+ /* O /status olha só o servidor. O /doctor olha tudo o que costuma
2068
+ ser o motivo de "não funciona" — e cada ✗ diz como consertar. */
2069
+ case 'doctor': case 'medico': case 'médico': {
2070
+ const sp = new Spinner('conferindo').start();
2071
+ const linhas = await extra.diagnosticar({
2072
+ saude: () => api.getHealth(state.config.server, state.config.token),
2073
+ provedor: () => provedorLocal.atual(),
2074
+ conferirChave: (id, chave) => provedorLocal.conferir(id, chave),
2075
+ cliInstalado: (id) => require('../lib/local/motor-cli').instalado(id),
2076
+ acharChrome: () => require('../lib/browser').findChrome(),
2077
+ raiz: cfg.ROOT_DIR,
2078
+ arquivoConfig: cfg.CONFIG_FILE,
2079
+ });
2080
+ sp.stop();
2081
+ console.log('');
2082
+ for (const l of linhas) {
2083
+ const marca = l.ok === true ? c.ok('✓') : l.ok === false ? c.err('✗') : c.dim('·');
2084
+ console.log(` ${marca} ${ui.padRight(c.white(l.item), 18)}${ui.truncate(c.muted(l.detalhe || ''), Math.max(20, (process.stdout.columns || 80) - 23))}`);
2085
+ if (l.conserto) console.log(` ${c.dim('→ ' + l.conserto)}`);
2086
+ }
2087
+ const ruins = linhas.filter((l) => l.ok === false).length;
2088
+ console.log(ruins ? c.warn(`\n ${ruins} ${ruins === 1 ? 'coisa para arrumar' : 'coisas para arrumar'}.`) : c.ok('\n Tudo certo.'));
2089
+ break;
2090
+ }
2091
+
2092
+ case 'custo': case 'cost': case 'tokens': {
2093
+ const { linhas, total } = extra.resumoCusto(contadorDaSessao);
2094
+ if (!linhas.length) { console.log(c.muted(' Nenhum pedido ao modelo nesta sessão ainda.')); break; }
2095
+ console.log('');
2096
+ for (const l of linhas) {
2097
+ console.log(` ${c.white(ui.padRight(l.provedor, 22))}${c.muted(ui.padRight(l.pedidos + (l.pedidos === 1 ? ' pedido' : ' pedidos'), 12))}`
2098
+ + `${c.dim('enviou ')}${c.white(extra.fmtNumero(l.enviados))}${c.dim(' · recebeu ')}${c.white(extra.fmtNumero(l.recebidos))}${c.dim(' tokens' + (l.exato ? '' : ' (estimado)'))}`);
2099
+ }
2100
+ if (linhas.length > 1) {
2101
+ console.log(` ${c.white(ui.padRight('Total', 22))}${c.muted(ui.padRight(total.pedidos + ' pedidos', 12))}`
2102
+ + `${c.dim('enviou ')}${c.white(extra.fmtNumero(total.enviados))}${c.dim(' · recebeu ')}${c.white(extra.fmtNumero(total.recebidos))}`);
2103
+ }
2104
+ if (linhas.some((l) => !l.exato)) console.log(c.dim('\n Estimado = caracteres ÷ 4. Conta cada pedido inteiro, com a conversa que vai junto.'));
2105
+ break;
2106
+ }
2107
+
2108
+ case 'copiar': case 'copy': {
2109
+ const r = extra.copiar(state.lastResponse);
2110
+ if (r.ok) console.log(c.ok(' Copiado.') + c.dim(` ${state.lastResponse.length} caracteres · é só colar`));
2111
+ else console.log(c.muted(` Não copiei: ${r.error}.`));
2112
+ break;
2113
+ }
2114
+
2115
+ case 'historico': case 'histórico': {
2116
+ const achados = extra.buscarHistorico(cfg.HISTORY_FILE, rest, 20);
2117
+ if (!achados.length) {
2118
+ console.log(c.muted(rest ? ` Nada com "${rest}" nos seus pedidos.` : ' Nenhum pedido guardado ainda.'));
2119
+ console.log(c.dim(' /chats mostra as conversas inteiras da sua conta.'));
2120
+ break;
2121
+ }
2122
+ console.log('');
2123
+ const largura = Math.max(30, (process.stdout.columns || 80) - 34);
2124
+ for (const e of achados) {
2125
+ const quando = new Date(e.ts).toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' });
2126
+ console.log(` ${c.dim(quando)} ${c.muted(ui.padRight(ui.truncate(e.project || e.mode || '', 14), 14))} `
2127
+ + ui.truncate(c.white(String(e.prompt).replace(/\s+/g, ' ')), largura));
2128
+ }
2129
+ console.log(c.dim(`\n ${achados.length === 20 ? 'Os 20 mais recentes' : achados.length + ' pedido(s)'}${rest ? ` com "${rest}"` : ''} · /historico <termo> filtra · /chats abre conversas`));
2130
+ break;
2131
+ }
2132
+
2133
+ case 'atalhos': case 'shortcuts': case 'teclas': {
2134
+ console.log('');
2135
+ for (const [tecla, faz] of extra.ATALHOS) console.log(` ${c.brand(ui.padRight(tecla, 20))}${c.muted(faz)}`);
2136
+ break;
2137
+ }
2138
+
1926
2139
  case 'status': {
1927
2140
  const sp = new Spinner('verificando').start();
1928
2141
  try {
@@ -2093,7 +2306,8 @@ async function handleSlash(text) {
2093
2306
  return resposta;
2094
2307
  }
2095
2308
  const plan = prepararPasta(texto);
2096
- checkpoint.iniciar(texto);
2309
+ // Pasta pendente (projeto que ainda vai nascer) é o ~/primocode: sem foto.
2310
+ checkpoint.iniciar(texto, plan.pending ? null : plan.dir);
2097
2311
  const resultado = await runAct(texto, {
2098
2312
  instrucao: vozModo.COMO_FALAR,
2099
2313
  server: state.config.server,
@@ -2108,6 +2322,7 @@ async function handleSlash(text) {
2108
2322
  controle: state.controle,
2109
2323
  permissoes: state.config.permissoes,
2110
2324
  });
2325
+ checkpoint.encerrar();
2111
2326
  const resposta = (resultado.assistantMessage && resultado.assistantMessage.content) || '';
2112
2327
  // A conversa é a MESMA do terminal: sair do modo voz e
2113
2328
  // continuar digitando não pode começar do zero.
@@ -2217,18 +2432,7 @@ async function handleSlash(text) {
2217
2432
  }
2218
2433
  const sp = new Spinner('abrindo a janela').start();
2219
2434
  try {
2220
- janelaAberta = await appLib.abrir({
2221
- versao: VERSION,
2222
- enviar: (t) => sendPrompt(t),
2223
- barra: (t) => handleSlash(t),
2224
- interromper,
2225
- estado: () => ({
2226
- pasta: state.workDir ? shortPath(state.workDir) : null,
2227
- projeto: state.currentProject,
2228
- modo: state.mode,
2229
- esforco: state.config.effort,
2230
- }),
2231
- });
2435
+ janelaAberta = await appLib.abrir(opcoesDaJanela());
2232
2436
  sp.stop();
2233
2437
  const j = janelaAberta.janela || {};
2234
2438
  console.log(c.ok(' Janela aberta.')
@@ -2252,6 +2456,37 @@ async function handleSlash(text) {
2252
2456
  break;
2253
2457
  }
2254
2458
 
2459
+ /* A IDE: "um app ao invés de CLI, aí fica uma IDE e uma CLI".
2460
+ Mora no MESMO servidor do /rc (lib/app.js + lib/ide.js): com a
2461
+ janela do chat já aberta, o /ide só abre outra tela nele — mesma
2462
+ chave, mesmo agente, a mesma conversa nas duas. */
2463
+ case 'ide': {
2464
+ if (janelaAberta && janelaAberta.urlIde) {
2465
+ await appLib.abrirJanela(janelaAberta.urlIde);
2466
+ console.log(c.ok(' IDE aberta.') + c.muted(' (no mesmo servidor da janela que já estava aberta)'));
2467
+ console.log(c.dim(` ${janelaAberta.urlIde}`));
2468
+ break;
2469
+ }
2470
+ const sp = new Spinner('abrindo a IDE').start();
2471
+ try {
2472
+ janelaAberta = await appLib.abrir(opcoesDaJanela({ abrirEm: 'ide' }));
2473
+ sp.stop();
2474
+ const j = janelaAberta.janela || {};
2475
+ console.log(c.ok(' IDE aberta.')
2476
+ + c.muted(j.modo === 'janela' ? ` (${j.navegador})`
2477
+ : j.modo === 'aba' ? ' (aba do navegador padrão)' : ''));
2478
+ console.log(c.dim(` ${janelaAberta.urlIde}`));
2479
+ if (j.modo === 'nenhum') {
2480
+ console.log(c.warn(' Não consegui abrir sozinho — copie o endereço acima no navegador.'));
2481
+ }
2482
+ console.log(c.muted(` Pasta: ${shortPath(projectDir())} · o terminal continua funcionando, os dois mostram a mesma coisa.`));
2483
+ } catch (e) {
2484
+ sp.stop();
2485
+ console.log(c.err(` Não subiu: ${e.message}`));
2486
+ }
2487
+ break;
2488
+ }
2489
+
2255
2490
  /* A conta do Conecta Primo, sem navegador: e-mail e senha aqui
2256
2491
  mesmo. O login inteiro mora em lib/conta.js; aqui é a tela dele. */
2257
2492
  case 'login': {
@@ -2555,18 +2790,7 @@ async function main() {
2555
2790
  process.exit(r.ok ? 0 : 1);
2556
2791
  }
2557
2792
 
2558
- const janela = await appLib.abrir({
2559
- versao: VERSION,
2560
- enviar: (t) => sendPrompt(t),
2561
- barra: (t) => handleSlash(t),
2562
- interromper,
2563
- estado: () => ({
2564
- pasta: state.workDir ? shortPath(state.workDir) : null,
2565
- projeto: state.currentProject,
2566
- modo: state.mode,
2567
- esforco: state.config.effort,
2568
- }),
2569
- });
2793
+ const janela = await appLib.abrir(opcoesDaJanela());
2570
2794
  janelaAberta = janela;
2571
2795
  console.log(c.brand(` PrimoCode`) + c.muted(` ${janela.url.split('?')[0]}`));
2572
2796
  // Sem readline não há nada segurando o laço de eventos além do
@@ -2577,8 +2801,13 @@ async function main() {
2577
2801
  return;
2578
2802
  }
2579
2803
 
2804
+ /* `primo ide`: o terminal de sempre, com a IDE já aberta ao lado.
2805
+ Só a palavra SOZINHA: `primo ide para gatos` continua sendo um pedido
2806
+ ao agente (modo um-tiro), como sempre foi. */
2807
+ const abrirIde = args.length === 1 && args[0].toLowerCase() === 'ide';
2808
+
2580
2809
  // Modo one-shot: primocode "crie um jogo de carro"
2581
- if (args.length && !args[0].startsWith('-')) {
2810
+ if (args.length && !args[0].startsWith('-') && !abrirIde) {
2582
2811
  const fileFlag = args.indexOf('-f');
2583
2812
  const words = fileFlag >= 0 ? args.slice(0, fileFlag) : args;
2584
2813
  const promptText = words.join(' ');
@@ -2757,6 +2986,9 @@ async function main() {
2757
2986
  // O que sobrou de adiantado não era resposta de nada: vira comando,
2758
2987
  // na ordem em que foi digitado. Nada se perde, só esperou a vez.
2759
2988
  segurando = false;
2989
+ // Depois da abertura (e do login dela): a IDE sem conta abriria
2990
+ // mandando pedidos que o servidor recusa.
2991
+ if (abrirIde) queue.push('/ide');
2760
2992
  while (adiantadas.length) queue.push(adiantadas.shift());
2761
2993
  if (queue.length) drain();
2762
2994
  else if (!closed) rl.prompt();