primocode 9.8.0-beta.7 → 9.8.0-beta.9

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,7 +21,6 @@ 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');
25
24
  const provedorLocal = require('../lib/local/provedor');
26
25
  const { runAct, interromper, ehTetoDeToken, ehTransitorio, esperaDoErro } = require('../lib/act');
27
26
  const fala = require('../lib/fala');
@@ -44,6 +43,7 @@ const nuvem = require('../lib/nuvem');
44
43
  const compat = require('../lib/compatibilidade');
45
44
  const mencoesLib = require('../lib/mencoes');
46
45
  const extra = require('../lib/comandos-extra');
46
+ const offline = require('../lib/offline');
47
47
 
48
48
  /* O /custo: toda conversa com o modelo passa pelo lib/api.js, e o contador
49
49
  embrulha as funções dele uma vez, aqui. Quem atende é o provedor do /api
@@ -108,10 +108,10 @@ const state = {
108
108
  // existe para a frente em vez de subir outro servidor ao lado.
109
109
  let janelaAberta = null;
110
110
 
111
- /* O que a janela do /rc, a IDE do /ide e o `primo --app` passam ao
112
- lib/app.js. Uma função só porque os três sobem o MESMO servidor: se cada um
113
- montasse o seu, o /ide aberto depois do /rc acharia um servidor sem a pasta
114
- do projeto — e a IDE nasceria sem árvore. */
111
+ /* O que o /rc, o /ide e o `primo --app` passam ao lib/app.js. Uma função
112
+ só porque os três sobem a MESMA janela: se cada um montasse a sua, o /ide
113
+ aberto depois do /rc acharia um servidor sem a pasta do projeto — e o
114
+ painel da prévia nasceria vazio. */
115
115
  function opcoesDaJanela(extra) {
116
116
  return Object.assign({
117
117
  versao: VERSION,
@@ -123,10 +123,37 @@ function opcoesDaJanela(extra) {
123
123
  projeto: state.currentProject,
124
124
  modo: state.mode,
125
125
  esforco: state.config.effort,
126
+ permissoes: perigo.normalizarModo(state.config.permissoes),
127
+ voz: !!vozEmEspera,
128
+ usuario: state.config.userName || null,
129
+ mascote: state.config.mascot || 'primo',
130
+ anexos: state.attachedFiles.map((f) => f.path),
126
131
  }),
127
132
  // Lida na hora, não guardada: o /dir e o /project trocam a pasta
128
- // com a IDE aberta, e ela tem de acompanhar.
133
+ // com a janela aberta, e o painel tem de acompanhar.
129
134
  pastaProjeto: () => projectDir(),
135
+ /* As ações da janela passam pelos MESMOS caminhos dos comandos — o
136
+ /projetos, o /clear, o /permissoes, o /effort, o /attach — para a
137
+ janela e o terminal nunca discordarem sobre onde se está. */
138
+ entrarProjeto: (dir) => {
139
+ entrarNoProjeto(dir, 'projeto');
140
+ console.log(c.ok(` Trabalhando em ${state.currentProject}`) + c.muted(` (${shortPath(dir)})`));
141
+ refreshPrompt();
142
+ },
143
+ novoChat: () => handleSlash('/clear'),
144
+ mudarPermissoes: (m) => handleSlash('/permissoes ' + m),
145
+ mudarEsforco: (n) => aplicarEffort(n),
146
+ anexar: (f) => {
147
+ state.attachedFiles.push(f);
148
+ console.log(c.ok(`Anexado: ${f.path} (${tools.fmtBytes(f.content.length)})`) + c.muted(' pela janela'));
149
+ refreshPrompt();
150
+ },
151
+ desanexar: (nome) => {
152
+ state.attachedFiles = nome ? state.attachedFiles.filter((f) => f.path !== nome) : [];
153
+ refreshPrompt();
154
+ },
155
+ voz: (ligar) => handleSlash(ligar ? '/voice' : '/voice parar'),
156
+ continuarConversa: (alvo) => abrirConversa(alvo),
130
157
  }, extra || {});
131
158
  }
132
159
 
@@ -153,12 +180,13 @@ ${c.bold('Comandos')}
153
180
  ${c.brand('/copiar')} copia a última resposta
154
181
  ${c.brand('/historico')} [termo] procura nos seus pedidos antigos
155
182
  ${c.brand('/rc')} abre a janela do PrimoCode (chat, sem terminal)
156
- ${c.brand('/ide')} abre a IDE: arquivos, editor, prévia do site e o chat
183
+ ${c.brand('/ide')} abre a janela com a prévia do site e os arquivos ao lado
157
184
  ${c.brand('/login')} entra na sua conta · ${c.brand('/account')} mostra o plano
158
185
  ${c.brand('/chats')} [n] suas conversas — as mesmas do app e do celular
159
186
  ${c.brand('/logout')} desconecta este computador da conta
160
187
  ${c.brand('/comandos')} seus comandos (/revisar, /explicar…) · /comandos novo <nome>
161
188
  ${c.brand('/status')} checa o servidor
189
+ ${c.brand('/offline')} roda o servidor no seu computador (temporário) · /offline off volta
162
190
  ${c.brand('/doctor')} confere tudo (servidor, /api, Chrome, disco…) e diz como consertar
163
191
  ${c.brand('/custo')} tokens e pedidos desta sessão, por provedor
164
192
  ${c.brand('/atalhos')} as teclas que ajudam
@@ -210,7 +238,7 @@ const SLASH_COMANDOS = [
210
238
  ['/copiar', 'copia a última resposta'],
211
239
  ['/historico', 'procura nos seus pedidos antigos: /historico login'],
212
240
  ['/rc', 'abre a janela do PrimoCode'],
213
- ['/ide', 'abre a IDE: arquivos, editor, prévia e o chat, lado a lado'],
241
+ ['/ide', 'abre a janela com a prévia do site e os arquivos ao lado'],
214
242
  ['/login', 'entra na sua conta Conecta Primo'],
215
243
  ['/account', 'sua conta, plano e computadores conectados'],
216
244
  ['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
@@ -224,6 +252,7 @@ const SLASH_COMANDOS = [
224
252
  ['/comandos', 'seus comandos: um .md vira /nome · /comandos novo <nome>'],
225
253
  ['/api', 'usa a SUA chave (Gemini, ChatGPT, Claude…) direto daqui, sem o limite do servidor'],
226
254
  ['/status', 'checa o servidor'],
255
+ ['/offline', 'roda o servidor aqui no seu computador, com as chaves do Render'],
227
256
  ['/doctor', 'confere se está tudo certo e diz como consertar'],
228
257
  ['/custo', 'tokens e pedidos desta sessão, por provedor'],
229
258
  ['/atalhos', 'as teclas que ajudam'],
@@ -742,16 +771,6 @@ function batizarDepois(plan, userText) {
742
771
  .then((nome) => { if (nome) plan.nomeEscolhido = nome; });
743
772
  }
744
773
 
745
- /* A inspiração visual (lib/kits.js): pedido de landing leva a ficha da
746
- direção que combina com o negócio. Nada é copiado para a pasta — o agente
747
- escreve a página dele, inspirado, e não troca texto num molde. */
748
- async function prepararKit(plan, userText) {
749
- const kit = kits.escolher(userText, { provedor: provedorLocal.atual() });
750
- if (!kit) return '';
751
- console.log(c.dim(` ⎿ inspiração visual: ${kit.titulo}`));
752
- return kit.instrucao;
753
- }
754
-
755
774
  function createPendingProject(plan) {
756
775
  const dir = cfg.createProjectDir(plan.nomeEscolhido || plan.name);
757
776
  entrarNoProjeto(dir, 'projeto');
@@ -1118,7 +1137,6 @@ async function enviarPedido(userText) {
1118
1137
  tempos.medir('espera do resumo', () => esperarResumo()),
1119
1138
  tempos.medir('plano', () => dividirTarefa(prompt)),
1120
1139
  ]);
1121
- const instrucaoKit = await prepararKit(plan, userText);
1122
1140
 
1123
1141
  let r = null;
1124
1142
  let jaFeito = '';
@@ -1167,8 +1185,7 @@ async function enviarPedido(userText) {
1167
1185
  permissoes: state.config.permissoes,
1168
1186
  // Pedido de mexer no computador leva a dica que libera as
1169
1187
  // ferramentas desktop_* no servidor (ver DICA_COMPUTADOR).
1170
- instrucao: [instrucaoKit, i === 0 && querComputador(userText) ? vozModo.DICA_COMPUTADOR : '']
1171
- .filter(Boolean).join('\n\n'),
1188
+ instrucao: i === 0 && querComputador(userText) ? vozModo.DICA_COMPUTADOR : '',
1172
1189
  });
1173
1190
 
1174
1191
  r = await rodarPasso();
@@ -2474,26 +2491,26 @@ async function handleSlash(text) {
2474
2491
  break;
2475
2492
  }
2476
2493
 
2477
- /* A IDE: "um app ao invés de CLI, aí fica uma IDE e uma CLI".
2478
- Mora no MESMO servidor do /rc (lib/app.js + lib/ide.js): com a
2479
- janela do chat já aberta, o /ide só abre outra tela nele — mesma
2480
- chave, mesmo agente, a mesma conversa nas duas. */
2494
+ /* O /ide virou a MESMA janela do /rc, abrindo com a prévia do site
2495
+ à direita. O editor de código saiu ("remove a IDE ou deixa
2496
+ idêntico ao Codex"): quem não programa precisa ver o site
2497
+ nascendo, não de um editor para quebrar o que o agente fez. O
2498
+ comando continua existindo para o dedo viciado não bater em nada. */
2481
2499
  case 'ide': {
2482
- if (janelaAberta && janelaAberta.urlIde) {
2483
- await appLib.abrirJanela(janelaAberta.urlIde);
2484
- console.log(c.ok(' IDE aberta.') + c.muted(' (no mesmo servidor da janela que já estava aberta)'));
2485
- console.log(c.dim(` ${janelaAberta.urlIde}`));
2500
+ if (janelaAberta) {
2501
+ await appLib.abrirJanela(janelaAberta.urlPainel || janelaAberta.url);
2502
+ console.log(c.ok(' Janela com a prévia aberta.') + c.muted(' (no mesmo servidor que já estava de pé)'));
2486
2503
  break;
2487
2504
  }
2488
- const sp = new Spinner('abrindo a IDE').start();
2505
+ const sp = new Spinner('abrindo a janela').start();
2489
2506
  try {
2490
2507
  janelaAberta = await appLib.abrir(opcoesDaJanela({ abrirEm: 'ide' }));
2491
2508
  sp.stop();
2492
2509
  const j = janelaAberta.janela || {};
2493
- console.log(c.ok(' IDE aberta.')
2510
+ console.log(c.ok(' Janela aberta, com a prévia do site ao lado.')
2494
2511
  + c.muted(j.modo === 'janela' ? ` (${j.navegador})`
2495
2512
  : j.modo === 'aba' ? ' (aba do navegador padrão)' : ''));
2496
- console.log(c.dim(` ${janelaAberta.urlIde}`));
2513
+ console.log(c.dim(` ${janelaAberta.urlPainel}`));
2497
2514
  if (j.modo === 'nenhum') {
2498
2515
  console.log(c.warn(' Não consegui abrir sozinho — copie o endereço acima no navegador.'));
2499
2516
  }
@@ -2639,6 +2656,45 @@ async function handleSlash(text) {
2639
2656
  break;
2640
2657
  }
2641
2658
 
2659
+ /* O servidor aqui, no computador da pessoa, até a sessão acabar
2660
+ (lib/offline.js). "Enquanto não tenho acesso ao Railway, eu testo
2661
+ com o projeto rodando localmente." (24/09/2026) */
2662
+ case 'offline': case 'local': {
2663
+ if (/^(off|sair|desligar|parar)$/i.test(rest)) {
2664
+ if (!state.servidorAntes) { console.log(c.muted(' O /offline não está ligado.')); break; }
2665
+ offline.desligar();
2666
+ state.config.server = state.servidorAntes;
2667
+ state.servidorAntes = null;
2668
+ console.log(c.ok(' Voltei ao servidor de antes: ') + c.dim(state.config.server));
2669
+ break;
2670
+ }
2671
+ if (state.servidorAntes) { console.log(c.muted(' Já está no servidor local: ') + c.dim(state.config.server) + c.muted(' · /offline off volta')); break; }
2672
+ const chaves = offline.prepararEnv();
2673
+ if (chaves.criado || !chaves.temChave) {
2674
+ console.log(c.warn(` ${chaves.criado ? 'Criei' : 'Falta chave em'} ${chaves.arquivo}`));
2675
+ console.log(c.muted(' Cole nele as chaves do Render (primocode-server → Environment), uma por linha:'));
2676
+ console.log(c.dim(' GROQ_API_KEY=… OPENROUTER_API_KEY=… GEMINI_API_KEY=…'));
2677
+ console.log(c.muted(' Depois rode ') + c.brand('/offline') + c.muted(' de novo.'));
2678
+ break;
2679
+ }
2680
+ const dir = offline.acharServidor({ pedido: rest });
2681
+ const sp = new Spinner('subindo o servidor aqui').start();
2682
+ const r = await offline.ligar({ dir, env: chaves.env });
2683
+ sp.stop();
2684
+ if (!r.ok) {
2685
+ console.log(c.err(' ' + r.error));
2686
+ if (r.comoResolver) console.log(c.dim(' → ' + r.comoResolver));
2687
+ break;
2688
+ }
2689
+ state.servidorAntes = state.config.server;
2690
+ state.config.server = r.url;
2691
+ console.log(c.ok(` Servidor ${r.versao || ''} ${r.reaproveitado ? 'que já estava de pé' : 'rodando aqui'}: `) + c.dim(r.url));
2692
+ console.log(c.dim(` ${dir || ''}${dir ? ' · ' : ''}chaves: ${chaves.nomes.join(', ')}${r.reaproveitado ? '' : ' · log em ' + offline.ARQUIVO_LOG}`));
2693
+ if (provedorLocal.atual()) console.log(c.warn(' Atenção: o /api está ligado e fala direto com a IA. Para usar o servidor local, /api off.'));
2694
+ console.log(c.muted(' Vale até fechar o PrimoCode. ') + c.brand('/offline off') + c.muted(' volta antes.'));
2695
+ break;
2696
+ }
2697
+
2642
2698
  /* Os comandos da pessoa: lista, e explica como fazer o dela. */
2643
2699
  case 'comandos': {
2644
2700
  const novo = rest.match(/^(novo|new|criar)\s+(\S+)$/i);
@@ -2819,7 +2875,7 @@ async function main() {
2819
2875
  return;
2820
2876
  }
2821
2877
 
2822
- /* `primo ide`: o terminal de sempre, com a IDE já aberta ao lado.
2878
+ /* `primo ide`: o terminal de sempre, com a janela (e a prévia) já aberta.
2823
2879
  Só a palavra SOZINHA: `primo ide para gatos` continua sendo um pedido
2824
2880
  ao agente (modo um-tiro), como sempre foi. */
2825
2881
  const abrirIde = args.length === 1 && args[0].toLowerCase() === 'ide';
@@ -2840,10 +2896,8 @@ async function main() {
2840
2896
  // Com -c, o pedido continua a última conversa em vez de chegar sozinho.
2841
2897
  if (continuar) await continuarUltima();
2842
2898
  /* O MESMO caminho do pedido digitado (sendPrompt). Este modo tinha um
2843
- runAct próprio, e por isso ficava sem tudo o que vive lá: o kit de
2844
- partida, o plano em passos, a retomada automática, o @arquivo, o
2845
- checkpoint do /desfazer. Medido em 24/09: `primo "crie uma landing…"`
2846
- não pegou o kit. */
2899
+ runAct próprio, e por isso ficava sem tudo o que vive lá: o plano em
2900
+ passos, a retomada automática, o @arquivo, o checkpoint do /desfazer. */
2847
2901
  state.semTerminal = true;
2848
2902
  await sendPrompt(promptText);
2849
2903
  process.exit(0);
@@ -2939,7 +2993,13 @@ async function main() {
2939
2993
  try {
2940
2994
  if (ehBash(text)) await rodarBash(text);
2941
2995
  else if (isSlash(text)) await handleSlash(text);
2942
- else await sendPrompt(text);
2996
+ else {
2997
+ // Com a janela aberta, o pedido digitado AQUI também vira
2998
+ // balão lá — senão a janela mostraria a resposta de uma
2999
+ // pergunta que ninguém fez.
3000
+ if (janelaAberta && janelaAberta.pedidoDoTerminal) janelaAberta.pedidoDoTerminal(text);
3001
+ await sendPrompt(text);
3002
+ }
2943
3003
  } catch (e) {
2944
3004
  fala.problema(e);
2945
3005
  }
@@ -3004,7 +3064,7 @@ async function main() {
3004
3064
  // O que sobrou de adiantado não era resposta de nada: vira comando,
3005
3065
  // na ordem em que foi digitado. Nada se perde, só esperou a vez.
3006
3066
  segurando = false;
3007
- // Depois da abertura (e do login dela): a IDE sem conta abriria
3067
+ // Depois da abertura (e do login dela): a janela sem conta abriria
3008
3068
  // mandando pedidos que o servidor recusa.
3009
3069
  if (abrirIde) queue.push('/ide');
3010
3070
  while (adiantadas.length) queue.push(adiantadas.shift());
package/lib/act.js CHANGED
@@ -138,6 +138,9 @@ function limparParaOModelo(result) {
138
138
 
139
139
  function logResult(kind, result, profundidade) {
140
140
  const p = fala.resultado(profundidade);
141
+ // A linha da janela (lib/app.js) fecha aqui: o mesmo resultado que o
142
+ // terminal desenha embaixo, resumido. Ver o canal em lib/fala.js.
143
+ fala.avisar('resultado', fala.resumoDoResultado(kind, result));
141
144
  if (!result) return;
142
145
  // Uma tool que falhou é informação para o AGENTE, que vai consertar na
143
146
  // volta seguinte — não é o erro do usuário. Âmbar, uma linha, sem o
@@ -1131,6 +1134,7 @@ async function runAct(userPrompt, opts) {
1131
1134
  const porta = await permissao.pedir(portaoControle, tc.name, tc.arguments, autorizar);
1132
1135
  if (!porta.ok) {
1133
1136
  fala.acao(tc.name, tc.arguments, profundidade);
1137
+ fala.avisar('resultado', { nome: tc.name, ok: false, resumo: 'não autorizado' });
1134
1138
  console.log(fala.resultado(profundidade) + c.warn('não autorizado'));
1135
1139
  registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: porta.error });
1136
1140
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: porta.error }) });
@@ -1148,6 +1152,7 @@ async function runAct(userPrompt, opts) {
1148
1152
  });
1149
1153
  if (!risco.ok) {
1150
1154
  fala.acao(tc.name, tc.arguments, profundidade);
1155
+ fala.avisar('resultado', { nome: tc.name, ok: false, resumo: `não autorizado — ${risco.motivo}` });
1151
1156
  console.log(fala.resultado(profundidade) + c.warn(`não autorizado — ${risco.motivo}`));
1152
1157
  registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: risco.error });
1153
1158
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: risco.error }) });