primocode 8.34.1 → 8.36.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/app/index.html CHANGED
@@ -40,7 +40,7 @@
40
40
  <div class="rotulo">Conversas</div>
41
41
  <div class="recentes" id="recentes">
42
42
  <div class="vazio-lista" id="listaVazia">
43
- As conversas aparecem aqui quando você entra na sua conta com <code>/conectar</code>.
43
+ As conversas aparecem aqui quando você entra na sua conta com <code>/connect</code>.
44
44
  </div>
45
45
  </div>
46
46
 
@@ -69,7 +69,7 @@
69
69
  <p>Escreva do jeito que você falaria. Ele lê a pasta, escreve o código,
70
70
  roda o que precisar e conta o que fez.</p>
71
71
  <div class="sugestoes">
72
- <button class="sugestao" data-manda="/tudo">o que ele sabe fazer</button>
72
+ <button class="sugestao" data-manda="/all">o que ele sabe fazer</button>
73
73
  <button class="sugestao" data-manda="/project">meus projetos</button>
74
74
  <button class="sugestao" data-manda="/effort">esforço</button>
75
75
  <button class="sugestao" data-manda="/help">comandos</button>
package/bin/primocode.js CHANGED
@@ -82,31 +82,32 @@ let janelaAberta = null;
82
82
 
83
83
  const HELP = `
84
84
  ${c.bold('Comandos')}
85
- ${c.brand('/dir')} <caminho> trabalha numa pasta que já existe (onde você quiser)
86
- ${c.brand('/project')} lista projetos ou cria em ~/primocode: /project <nome>
87
- ${c.brand('/mode')} /mode primocode|chat|auto
88
- ${c.brand('/effort')} esforço do agente — do rápido ao mais esperto
89
- ${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
90
- ${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
91
- ${c.brand('/tudo')} lista tudo que ele sabe fazer
92
- ${c.brand('/tree')} [n] árvore do projeto
93
- ${c.brand('/read')} <arquivo> mostra um arquivo
94
- ${c.brand('/run')} <comando> executa no shell do projeto
95
- ${c.brand('/open')} <arquivo|url> abre no navegador
96
- ${c.brand('/rc')} abre a janela do PrimoCode (chat, sem terminal)
97
- ${c.brand('/claude')} usa o Claude Code da SUA conta como motor (Opus)
98
- ${c.brand('/gpt')} usa o GPT (Codex) da SUA conta como motor
99
- ${c.brand('/chave')} <provedor> sua própria chave de API (Claude, Codex)
100
- ${c.brand('/conectar')} entra na sua conta Conecta Primo · ${c.brand('/conta')} mostra o plano
101
- ${c.brand('/conversas')} [n] suas conversas salvas — as mesmas do app e do celular
102
- ${c.brand('/sair')} desconecta este computador
103
- ${c.brand('/status')} checa o servidor
104
- ${c.brand('/name')} <nome> como o Primo te chama
105
- ${c.brand('/mascot')} <estilo> mascote: primo|fox|cat
106
- ${c.brand('/config')} mostra suas preferências
107
- ${c.brand('/clear')} limpa a conversa ${c.brand('/cls')} limpa a tela
108
- ${c.brand('/uninstall')} remove o PrimoCode
109
- ${c.brand('/exit')} sair
85
+ ${c.brand('/dir')} <caminho> trabalha numa pasta que já existe (onde você quiser)
86
+ ${c.brand('/project')} lista projetos ou cria em ~/primocode: /project <nome>
87
+ ${c.brand('/mode')} /mode primocode|chat|auto
88
+ ${c.brand('/effort')} esforço do agente — do rápido ao mais esperto
89
+ ${c.brand('/compact')} resume a conversa e devolve o contexto
90
+ ${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
91
+ ${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
92
+ ${c.brand('/all')} lista tudo que ele sabe fazer
93
+ ${c.brand('/tree')} [n] árvore do projeto
94
+ ${c.brand('/read')} <arquivo> mostra um arquivo
95
+ ${c.brand('/run')} <comando> executa no shell do projeto
96
+ ${c.brand('/open')} <arq|url> abre no navegador
97
+ ${c.brand('/rc')} abre a janela do PrimoCode (chat, sem terminal)
98
+ ${c.brand('/claude')} usa o Claude Code da SUA conta como motor (Opus)
99
+ ${c.brand('/gpt')} usa o GPT (Codex) da SUA conta como motor
100
+ ${c.brand('/key')} <provedor> sua própria chave de API (Claude, GPT)
101
+ ${c.brand('/connect')} entra na sua conta · ${c.brand('/account')} mostra o plano
102
+ ${c.brand('/chats')} [n] suas conversas — as mesmas do app e do celular
103
+ ${c.brand('/logout')} desconecta este computador da conta
104
+ ${c.brand('/status')} checa o servidor
105
+ ${c.brand('/name')} <nome> como o Primo te chama
106
+ ${c.brand('/mascot')} <estilo> mascote: primo|fox|cat
107
+ ${c.brand('/config')} mostra suas preferências
108
+ ${c.brand('/clear')} limpa a conversa ${c.brand('/cls')} limpa a tela
109
+ ${c.brand('/uninstall')} remove o PrimoCode
110
+ ${c.brand('/exit')} sai do PrimoCode
110
111
 
111
112
  ${c.bold('Modos')}
112
113
  ${c.warn('Primo Code')} ${c.muted('Agente completo — cria arquivos, roda comandos, abre o navegador')}
@@ -131,10 +132,11 @@ const SLASH_COMANDOS = [
131
132
  ['/dir', 'trabalhar numa pasta sua'],
132
133
  ['/mode', 'primocode | chat | auto'],
133
134
  ['/effort', 'esforço: low → ultracode (←/→ para ajustar)'],
135
+ ['/compact', 'resume a conversa e devolve o contexto'],
134
136
  ['/attach', 'anexa arquivo ao pedido'],
135
137
  ['/detach', 'remove os anexos'],
136
138
  ['/files', 'lista os anexos'],
137
- ['/tudo', 'tudo que ele sabe fazer'],
139
+ ['/all', 'tudo que ele sabe fazer'],
138
140
  ['/tree', 'árvore do projeto'],
139
141
  ['/read', 'mostra um arquivo'],
140
142
  ['/run', 'roda um comando'],
@@ -142,11 +144,11 @@ const SLASH_COMANDOS = [
142
144
  ['/rc', 'abre a janela do PrimoCode'],
143
145
  ['/claude', 'usa o Claude Code da sua conta como motor'],
144
146
  ['/gpt', 'usa o GPT (Codex) da sua conta como motor'],
145
- ['/chave', 'sua própria chave de API (Claude, Codex)'],
146
- ['/conectar', 'entra na sua conta Conecta Primo'],
147
- ['/conta', 'sua conta, plano e computadores conectados'],
148
- ['/conversas', 'suas conversas salvas, as mesmas do app e do celular'],
149
- ['/sair', 'desconecta este computador'],
147
+ ['/key', 'sua própria chave de API (Claude, Codex)'],
148
+ ['/connect', 'entra na sua conta Conecta Primo'],
149
+ ['/account', 'sua conta, plano e computadores conectados'],
150
+ ['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
151
+ ['/logout', 'desconecta este computador da conta'],
150
152
  ['/status', 'checa o servidor'],
151
153
  ['/name', 'como ele te chama'],
152
154
  ['/mascot', 'primo | fox | cat'],
@@ -259,8 +261,22 @@ function makePrompt() {
259
261
  modeTag = state.mode === 'primocode' ? c.warn('Primo Code') : c.brandDim('Chat');
260
262
  }
261
263
  const proj = state.currentProject ? c.dim(` ${state.currentProject}`) : '';
264
+
265
+ /* O CONTEXTO na barra, como o pedido pede. Só aparece depois que há
266
+ conversa: um "ctx 0%" na abertura é ruído, e ruído que aparece sempre é
267
+ o que a pessoa aprende a não enxergar — inclusive quando marcar 90%.
268
+ Fica âmbar a partir de 70% e vermelho a partir de 100%, que é quando
269
+ `/compact` deixa de ser conselho e vira o que fazer. */
270
+ const uso = memoria.usoDoContexto(state.memoria, state.history);
271
+ const ctx = uso.pct < 5 ? '' : c.dim(' · ')
272
+ + (uso.pct >= 100 ? c.err(`ctx ${uso.pct}%`)
273
+ : uso.pct >= 70 ? c.warn(`ctx ${uso.pct}%`)
274
+ : c.dim(`ctx ${uso.pct}%`));
275
+ // O modelo ativo: qual motor está pensando. No padrão é o do servidor.
276
+ const motor = state.engine === 'claude-code' ? c.dim(' · ') + c.dim('claude')
277
+ : state.engine === 'codex' ? c.dim(' · ') + c.dim('gpt') : '';
262
278
  const att = state.attachedFiles.length ? c.muted(` +${state.attachedFiles.length}`) : '';
263
- return `\n${c.muted(modeTag)}${proj}${att} ${c.brand('❯')} `;
279
+ return `\n${c.muted(modeTag)}${motor}${ctx}${proj}${att} ${c.brand('❯')} `;
264
280
  }
265
281
  function refreshPrompt() { if (rl) rl.setPrompt(makePrompt()); }
266
282
 
@@ -458,7 +474,7 @@ async function sendPrompt(userText) {
458
474
  if (state.liberado === false) {
459
475
  console.log(c.err(' O PrimoCode pede o plano Premium ou Super.'));
460
476
  console.log(c.muted(' Assine em ') + c.brand('conecta-primo-ai.vercel.app')
461
- + c.muted(' · já assinou? ') + c.brand('/conectar'));
477
+ + c.muted(' · já assinou? ') + c.brand('/connect'));
462
478
  return;
463
479
  }
464
480
 
@@ -649,8 +665,12 @@ async function dividirTarefa(pedido) {
649
665
  * Silencioso quando não há o que fazer: nas primeiras rodadas a conversa é
650
666
  * curta e o resumo não pagaria a chamada.
651
667
  */
652
- async function comprimirConversa() {
653
- if (!memoria.precisaResumir(state.memoria, state.history)) return;
668
+ /* `forcado` é o `/compact`: a pessoa mandou resumir, e mandar resumir tem de
669
+ resumir. Sem isso o comando responderia "nada a fazer" justamente quando
670
+ ela está vendo a IA se perder — e "não fiz nada" é a pior resposta possível
671
+ para quem acabou de pedir socorro. */
672
+ async function comprimirConversa(forcado = false) {
673
+ if (!forcado && !memoria.precisaResumir(state.memoria, state.history)) return;
654
674
  const sp = new Spinner('resumindo a conversa').start();
655
675
  try {
656
676
  state.memoria = await memoria.atualizar(state.memoria, state.history, ({ resumo, mensagens }) =>
@@ -720,11 +740,49 @@ function motorDaVez() {
720
740
 
721
741
  function isSlash(text) { return text.trim().startsWith('/'); }
722
742
 
743
+ /* ── OS NOMES ANTIGOS ────────────────────────────────────────────────────
744
+ * Os comandos passaram a ser todos em inglês. Metade já era; a outra metade
745
+ * estava em português, e a mistura era o que a especificação chamou de
746
+ * bagunça — `/run` ao lado de `/pasta`, `/tree` ao lado de `/tudo`.
747
+ *
748
+ * O nome antigo NÃO some: ele responde e ensina o novo. Sumir de vez faz quem
749
+ * tem o dedo viciado bater num "comando não existe" e concluir que a
750
+ * atualização quebrou alguma coisa — e essa pessoa não vai procurar no /help,
751
+ * vai desinstalar.
752
+ *
753
+ * Uma tabela, e não um `if` dentro de cada `case`: com dezoito apelidos, o
754
+ * `if` espalhado é o jeito garantido de esquecer um. Aqui, esquecer é não
755
+ * escrever a linha, e a linha some da lista.
756
+ */
757
+ const RENOMEADOS = {
758
+ // o que abre a janela
759
+ app: 'rc',
760
+ // a conta
761
+ entrar: 'connect', login: 'connect', conectar: 'connect',
762
+ conta: 'account',
763
+ sair: 'exit', // sair é SAIR; desconectar a conta é /logout
764
+ // as chaves de API
765
+ chave: 'key', chaves: 'key',
766
+ // o histórico
767
+ conversas: 'chats', historico: 'chats', histórico: 'chats',
768
+ // o resto
769
+ pasta: 'dir',
770
+ esforco: 'effort', esforço: 'effort',
771
+ tudo: 'all', capacidades: 'all',
772
+ };
773
+
723
774
  async function handleSlash(text) {
724
775
  const [cmd] = text.trim().slice(1).split(/\s+/);
725
776
  const rest = text.trim().slice(1 + cmd.length).trim();
726
777
 
727
- switch (cmd.toLowerCase()) {
778
+ let nome = cmd.toLowerCase();
779
+ if (RENOMEADOS[nome]) {
780
+ const novo = RENOMEADOS[nome];
781
+ console.log(c.muted(' Agora é ') + c.brand('/' + novo) + c.muted('.'));
782
+ nome = novo;
783
+ }
784
+
785
+ switch (nome) {
728
786
  case 'help': console.log(HELP); break;
729
787
 
730
788
  case 'project': {
@@ -751,8 +809,7 @@ async function handleSlash(text) {
751
809
  // Antes só existia /project, que SEMPRE cria em ~/primocode/<nome>. Se a
752
810
  // sua pasta já existe em outro lugar, não havia como apontar para ela.
753
811
  case 'dir':
754
- case 'cd':
755
- case 'pasta': {
812
+ case 'cd': {
756
813
  if (!rest) {
757
814
  const de = {
758
815
  padrao: 'a pasta padrão do PrimoCode',
@@ -820,7 +877,7 @@ async function handleSlash(text) {
820
877
  /* A chave de API da própria pessoa. Guardada em ~/.primocode/
821
878
  chaves.json com permissão 600 — nunca no config.json, que é o
822
879
  arquivo que se cola num chamado de suporte. */
823
- case 'chave': case 'chaves': {
880
+ case 'key': case 'keys': {
824
881
  const [qual, ...resto] = rest.split(/\s+/).filter(Boolean);
825
882
  const valor = resto.join(' ');
826
883
 
@@ -831,8 +888,8 @@ async function handleSlash(text) {
831
888
  + (p.tem ? c.ok(p.disfarce) : c.dim('não configurada'))
832
889
  + c.dim(` ${p.variavel}`));
833
890
  }
834
- console.log(c.dim('\n /chave claude sk-ant-… guarda a chave'));
835
- console.log(c.dim(' /chave codex remover apaga a que estiver lá'));
891
+ console.log(c.dim('\n /key claude sk-ant-… guarda a chave'));
892
+ console.log(c.dim(' /key gpt remover apaga a que estiver lá'));
836
893
  console.log(c.muted('\n Sem chave própria, cada motor usa a conta em que você já entrou nele.'));
837
894
  break;
838
895
  }
@@ -848,7 +905,7 @@ async function handleSlash(text) {
848
905
  }
849
906
  if (!valor) {
850
907
  const p = chaves.PROVEDORES[qual];
851
- console.log(c.muted(` Uso: `) + c.brand(`/chave ${qual} ${p.prefixo}…`));
908
+ console.log(c.muted(` Uso: `) + c.brand(`/key ${qual} ${p.prefixo}…`));
852
909
  console.log(c.dim(` Pegue a sua em ${p.onde}`));
853
910
  break;
854
911
  }
@@ -911,7 +968,7 @@ async function handleSlash(text) {
911
968
  // O /model virou /effort: você escolhe o ESFORÇO num slider (←/→,
912
969
  // Enter confirma, Esc cancela) e o modelo sai daí. Com argumento
913
970
  // ("/effort high") aplica direto, para quem já sabe o que quer.
914
- case 'effort': case 'esforco': case 'esforço': {
971
+ case 'effort': {
915
972
  const pedido = rest.toLowerCase().trim();
916
973
  if (pedido) {
917
974
  if (!effort.NOMES.includes(pedido)) {
@@ -964,9 +1021,8 @@ async function handleSlash(text) {
964
1021
  // A lista vem de lib/catalogo.js, e o test/catalogo.test.js cruza aquela
965
1022
  // lista com as tools que o tools.execute realmente atende. Ou seja: esta
966
1023
  // tela não consegue prometer coisa que o programa não faz.
967
- case 'tudo':
968
- case 'tools':
969
- case 'capacidades': {
1024
+ case 'all':
1025
+ case 'tools': {
970
1026
  const cols = Math.max(40, Math.min(process.stdout.columns || 90, 110));
971
1027
  console.log('');
972
1028
  for (const g of catalogo.GRUPOS) {
@@ -1055,6 +1111,29 @@ async function handleSlash(text) {
1055
1111
  // Limpar aqui não apaga o que já subiu: encerra ESTA conversa e a
1056
1112
  // próxima pergunta abre outra. Apagar do histórico é decisão da
1057
1113
  // pessoa, e ela toma no app — não de raspão num comando de limpar tela.
1114
+ /* Resume a conversa por ordem da pessoa. Diferente do /clear: aqui
1115
+ nada é esquecido — o que já passou vira resumo e o fim continua
1116
+ cru, então dá para seguir de onde parou. */
1117
+ case 'compact': {
1118
+ if (!state.history.length) {
1119
+ console.log(c.muted(' A conversa está vazia — não há o que resumir.'));
1120
+ break;
1121
+ }
1122
+ const antes = memoria.usoDoContexto(state.memoria, state.history);
1123
+ await comprimirConversa(true);
1124
+ const depois = memoria.usoDoContexto(state.memoria, state.history);
1125
+ if (depois.tokens >= antes.tokens) {
1126
+ // Pode não haver o que dobrar (conversa curta, ou o servidor
1127
+ // recusou). Dizer isso é melhor que fingir que encolheu.
1128
+ console.log(c.muted(' Não deu para encolher mais desta vez.'));
1129
+ } else {
1130
+ console.log(c.ok(' Conversa resumida.')
1131
+ + c.muted(` ctx ${antes.pct}% → ${depois.pct}%`));
1132
+ }
1133
+ refreshPrompt();
1134
+ break;
1135
+ }
1136
+
1058
1137
  case 'clear': state.history = []; state.memoria = memoria.nova();
1059
1138
  state.gravador.zerar(); console.log(c.ok('Conversa limpa.')); break;
1060
1139
  case 'cls': process.stdout.write('\x1Bc'); break;
@@ -1096,10 +1175,7 @@ async function handleSlash(text) {
1096
1175
  não existe" e acha que a atualização quebrou alguma coisa; o aviso
1097
1176
  custa uma linha e ensina o nome novo na hora em que ele é
1098
1177
  procurado, que é a única hora em que alguém lê. */
1099
- case 'rc': case 'app': {
1100
- if (cmd.toLowerCase() === 'app') {
1101
- console.log(c.muted(' Agora é ') + c.brand('/rc') + c.muted('. Abrindo…'));
1102
- }
1178
+ case 'rc': {
1103
1179
  // `/rc --instalar` põe o PrimoCode no Dock, no menu Iniciar ou no
1104
1180
  // menu de aplicativos, com o ícone do mascote. Uma vez só.
1105
1181
  if (/^--?(instalar|install)$/.test(rest)) {
@@ -1150,7 +1226,7 @@ async function handleSlash(text) {
1150
1226
  // /rc viraria ruído para quem já instalou de propósito.
1151
1227
  if (!require('../lib/instalar-app.js').instalado()) {
1152
1228
  console.log(c.dim(' Quer o ícone no ') + c.dim(os.platform() === 'darwin' ? 'Dock' : 'menu Iniciar')
1153
- + c.dim('? ') + c.brand('/app --instalar'));
1229
+ + c.dim('? ') + c.brand('/rc --instalar'));
1154
1230
  }
1155
1231
  } catch (e) {
1156
1232
  sp.stop();
@@ -1161,15 +1237,12 @@ async function handleSlash(text) {
1161
1237
 
1162
1238
  /* A conta do Conecta Primo. O login inteiro mora em lib/conta.js;
1163
1239
  aqui é a tela dele. */
1164
- case 'conectar': case 'entrar': case 'login': {
1165
- if (/^(entrar|login)$/.test(cmd.toLowerCase())) {
1166
- console.log(c.muted(' Agora é ') + c.brand('/conectar') + c.muted('.'));
1167
- }
1240
+ case 'connect': {
1168
1241
  const jaEsta = conta.atual();
1169
1242
  if (jaEsta && rest !== 'trocar') {
1170
1243
  console.log(c.muted(' Já conectado como ') + c.brand(jaEsta.uid.slice(0, 12) + '…')
1171
1244
  + c.muted(` · plano ${jaEsta.plano}`));
1172
- console.log(c.dim(' Para entrar com outra conta: ') + c.brand('/conectar trocar'));
1245
+ console.log(c.dim(' Para entrar com outra conta: ') + c.brand('/connect trocar'));
1173
1246
  break;
1174
1247
  }
1175
1248
  if (rest === 'trocar') conta.sair();
@@ -1185,11 +1258,20 @@ async function handleSlash(text) {
1185
1258
  console.log(' ' + c.brand(msg));
1186
1259
  primeiro = false;
1187
1260
  } else {
1188
- // O código aparece AQUI para a pessoa comparar com o
1189
- // da página. Sem essa comparação, um link mandado por
1190
- // outra pessoa conectaria o computador dela à conta
1191
- // de quem clicou.
1192
- console.log(c.muted(' Confira o código na página: ') + c.bold(c.warn(msg)));
1261
+ /* O código é para DIGITAR na página, e é ele que
1262
+ protege: como não vai no link, quem não está olhando
1263
+ este terminal não tem o que digitar — e um link
1264
+ mandado por outra pessoa não conecta o computador
1265
+ dela à sua conta.
1266
+
1267
+ Por isso ele aparece grande e sozinho, com o traço
1268
+ no meio: o que se lê de uma tela e se digita noutra
1269
+ erra menos partido em dois. */
1270
+ const meio = msg.length === 8 ? msg.slice(0, 4) + '-' + msg.slice(4) : msg;
1271
+ console.log('');
1272
+ console.log(c.muted(' Digite este código na página:'));
1273
+ console.log(' ' + c.bold(c.brand(meio.split('').join(' '))));
1274
+ console.log('');
1193
1275
  console.log(c.dim(' Esperando você aprovar…'));
1194
1276
  }
1195
1277
  },
@@ -1203,9 +1285,9 @@ async function handleSlash(text) {
1203
1285
 
1204
1286
  /* O histórico da conta — o MESMO que o app desktop e o celular
1205
1287
  mostram. Sem número, lista; com número, abre e continua dali. */
1206
- case 'conversas': case 'historico': {
1288
+ case 'chats': {
1207
1289
  if (!conta.atual()) {
1208
- console.log(c.muted(' Não conectado. ') + c.brand('/conectar')
1290
+ console.log(c.muted(' Não conectado. ') + c.brand('/connect')
1209
1291
  + c.muted(' para ver suas conversas aqui, no app e no celular.'));
1210
1292
  break;
1211
1293
  }
@@ -1226,7 +1308,7 @@ async function handleSlash(text) {
1226
1308
  + ' ' + c.white(x.titulo.slice(0, 52).padEnd(52))
1227
1309
  + c.dim(`${String(x.mensagens).padStart(3)} msg ${quando}`));
1228
1310
  });
1229
- console.log(c.dim('\n /conversas 1 abre a primeira e continua dela.'));
1311
+ console.log(c.dim('\n /chats 1 abre a primeira e continua dela.'));
1230
1312
  break;
1231
1313
  }
1232
1314
  const n = Number(rest);
@@ -1250,18 +1332,18 @@ async function handleSlash(text) {
1250
1332
  break;
1251
1333
  }
1252
1334
 
1253
- case 'sair': case 'logout': {
1335
+ case 'logout': {
1254
1336
  if (!conta.atual()) { console.log(c.muted(' Você não está conectado.')); break; }
1255
1337
  conta.sair();
1256
1338
  console.log(c.ok(' Desconectado deste computador.')
1257
- + c.dim(' A conta continua intacta; entre de novo com /conectar.'));
1339
+ + c.dim(' A conta continua intacta; entre de novo com /connect.'));
1258
1340
  break;
1259
1341
  }
1260
1342
 
1261
- case 'conta': {
1343
+ case 'account': {
1262
1344
  const eu = conta.atual();
1263
1345
  if (!eu) {
1264
- console.log(c.muted(' Não conectado. ') + c.brand('/conectar') + c.muted(' para entrar.'));
1346
+ console.log(c.muted(' Não conectado. ') + c.brand('/connect') + c.muted(' para entrar.'));
1265
1347
  break;
1266
1348
  }
1267
1349
  const sp = new Spinner('conferindo').start();
@@ -1582,7 +1664,7 @@ async function main() {
1582
1664
  cores: c,
1583
1665
  interativo: !!(process.stdin.isTTY && process.stdout.isTTY),
1584
1666
  conectado: () => conta.atual(),
1585
- conectar: () => handleSlash('/conectar').then(() => conta.atual()),
1667
+ conectar: () => handleSlash('/connect').then(() => conta.atual()),
1586
1668
  sessao: () => conta.sessao(),
1587
1669
  aplicar: (cmd) => handleSlash(cmd),
1588
1670
  }).then((r) => {
package/lib/act.js CHANGED
@@ -616,6 +616,24 @@ async function runAct(userPrompt, opts) {
616
616
  continue;
617
617
  }
618
618
 
619
+ /* O PIPELINE DE QUATRO PAPÉIS mora aqui pelo mesmo motivo que o
620
+ `spawn_agent`: ele PRECISA rodar agentes, e quem sabe rodar
621
+ agente é este arquivo. Pôr isto no `tools.execute` obrigaria
622
+ aquele módulo a conhecer o loop do agente — e um segundo lugar
623
+ que sabe abrir conversa com o modelo é um segundo lugar para
624
+ esquecer de passar o token, a pasta ou o motor. */
625
+ if (tc.name === 'construir_app') {
626
+ algoRodou = true;
627
+ console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
628
+ const r = await rodarPipeline(tc.arguments || {}, {
629
+ server, model, token, projectDir: pastaAtual, projectName, profundidade, motor,
630
+ });
631
+ logResult(tc.name, r, profundidade);
632
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
633
+ if (r && r.ok && catalogo.escreveArquivo(tc.name)) temPasta = true;
634
+ continue;
635
+ }
636
+
619
637
  // Repetiu a mesma chamada? Não executa de novo. Rodar outra vez
620
638
  // gastaria a cota para devolver exatamente o mesmo texto, e é
621
639
  // esse texto que convence o modelo a repetir mais uma vez.
@@ -798,5 +816,55 @@ async function runSubagent(args, ctx) {
798
816
  return resumo;
799
817
  }
800
818
 
819
+ /* ══ O PIPELINE DE QUATRO PAPÉIS ═══════════════════════════════════════════
820
+ *
821
+ * "Engenheiro de Prompt → Construtor 1 (protótipo) → Construtor 2 (MVP) →
822
+ * Revisador (testa ocultamente) → loop de correção → abre no navegador."
823
+ *
824
+ * As regras dos papéis, os prompts e o revisor que abre a página moram em
825
+ * `pipeline.js`, que não conhece o loop do agente. O que esta função faz é a
826
+ * costura: dar a ele uma forma de RODAR um agente, uma de escrever na tela e
827
+ * uma de abrir o navegador da pessoa.
828
+ *
829
+ * Cada papel roda com `history: []` — conversa própria, sem ver a do usuário
830
+ * nem a do papel anterior. É essa separação que faz o revisor valer alguma
831
+ * coisa: quem acabou de escrever o código é o pior revisor dele, porque lê o
832
+ * que quis escrever em vez do que escreveu.
833
+ */
834
+ async function rodarPipeline(args, ctx) {
835
+ const { server, model, token, projectDir, projectName, profundidade, motor } = ctx;
836
+ if (profundidade > 0) {
837
+ return { ok: false, error: 'Recusado: o pipeline não roda dentro de um subagente — '
838
+ + 'os papéis dele já são subagentes. Faça a parte que te cabe.' };
839
+ }
840
+
841
+ const pipeline = require('./pipeline.js');
842
+ const path = require('path');
843
+ const pedido = args.pedido || args.prompt || '';
844
+ const base = projectDir || cfg.ROOT_DIR;
845
+ const pasta = args.pasta
846
+ ? path.resolve(base, args.pasta)
847
+ : pipeline.pastaPara(pedido, base);
848
+
849
+ const correr = async (prompt, rotulo) => {
850
+ if (PARAR) return '';
851
+ console.log(ident(profundidade) + ` ${c.magenta('◆')} ${c.bold(c.magenta('Agente'))} ${c.muted(rotulo)}`);
852
+ const r = await runAct(prompt, {
853
+ server, model, token, projectDir: pasta, projectName,
854
+ history: [], profundidade: profundidade + 1, subagent: true, motor,
855
+ });
856
+ return (r && r.assistantMessage && r.assistantMessage.content) || '';
857
+ };
858
+
859
+ return pipeline.construir({
860
+ pedido,
861
+ pasta,
862
+ rodadas: args.rodadas,
863
+ correr,
864
+ mostrar: (linha) => console.log(ident(profundidade) + ` ${c.muted('⎿ ' + linha)}`),
865
+ abrir: tools.openInBrowser,
866
+ });
867
+ }
868
+
801
869
  module.exports = { runAct, interromper, compactar, motivoDaParada, ehTetoDeToken, esperaDoErro,
802
870
  anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };