primocode 9.8.0-beta.0 → 9.8.0-beta.10
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/README.md +36 -8
- package/app/ansi.js +55 -0
- package/app/app.css +482 -281
- package/app/app.js +888 -315
- package/app/index.html +176 -101
- package/app/primo-marca.js +58 -1
- package/bin/primocode.js +770 -118
- package/comandos/explicar.md +14 -0
- package/comandos/publicar.md +10 -0
- package/comandos/revisar.md +13 -0
- package/lib/act.js +118 -18
- package/lib/api.js +21 -0
- package/lib/app.js +317 -74
- package/lib/browser.js +79 -3
- package/lib/checkpoint-git.js +218 -0
- package/lib/checkpoint.js +249 -0
- package/lib/claude-engine.js +630 -0
- package/lib/codex-engine.js +207 -0
- package/lib/comandos-extra.js +481 -0
- package/lib/comandos.js +104 -0
- package/lib/config.js +21 -4
- package/lib/fala.js +95 -1
- package/lib/local/cerebro.json +2116 -0
- package/lib/local/entrada.js +332 -0
- package/lib/local/motor-cli.js +90 -0
- package/lib/local/motor.js +264 -0
- package/lib/local/provedor.js +169 -0
- package/lib/mencoes.js +98 -0
- package/lib/offline.js +168 -0
- package/lib/painel.js +291 -0
- package/lib/perigo.js +256 -0
- package/lib/pre-leitura.js +118 -0
- package/lib/repeticao.js +4 -2
- package/lib/skills.js +6 -1
- package/lib/tempos.js +62 -0
- package/lib/ui.js +171 -17
- package/package.json +3 -2
- package/skills/apresentacao-pptx/SKILL.md +52 -0
- package/skills/cartaz-de-arte/LICENSE.txt +202 -0
- package/skills/cartaz-de-arte/SKILL.md +47 -0
- package/skills/criar-skill/LICENSE.txt +202 -0
- package/skills/criar-skill/SKILL.md +49 -0
- package/skills/depurar-de-verdade/LICENSE.txt +21 -0
- package/skills/depurar-de-verdade/SKILL.md +14 -5
- package/skills/design-de-verdade/LICENSE.txt +177 -0
- package/skills/design-de-verdade/SKILL.md +42 -0
- package/skills/documento-word/SKILL.md +57 -0
- package/skills/interface-revisada/SKILL.md +47 -0
- package/skills/me-ensina/LICENSE.txt +21 -0
- package/skills/me-ensina/SKILL.md +27 -0
- package/skills/me-entrevista/LICENSE.txt +21 -0
- package/skills/me-entrevista/SKILL.md +27 -0
- package/skills/motion-de-primeira/SKILL.md +1 -1
- package/skills/pdf-bonito/SKILL.md +48 -0
- package/skills/planilha-excel/SKILL.md +47 -0
- package/skills/react-sem-dor/SKILL.md +48 -0
- package/skills/revisao-antes-de-entregar/LICENSE.txt +21 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +6 -1
- package/skills/site-com-motion/SKILL.md +39 -0
- package/skills/temas-prontos/LICENSE.txt +202 -0
- package/skills/temas-prontos/SKILL.md +50 -0
- package/skills/testar-o-site/LICENSE.txt +202 -0
- package/skills/testar-o-site/SKILL.md +46 -0
- package/skills/video-no-remotion/SKILL.md +48 -0
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 provedorLocal = require('../lib/local/provedor');
|
|
24
25
|
const { runAct, interromper, ehTetoDeToken, ehTransitorio, esperaDoErro } = require('../lib/act');
|
|
25
26
|
const fala = require('../lib/fala');
|
|
26
27
|
const boasVindas = require('../lib/boas-vindas');
|
|
@@ -30,13 +31,29 @@ const effort = require('../lib/effort');
|
|
|
30
31
|
const memoria = require('../lib/memoria-conversa');
|
|
31
32
|
const registro = require('../lib/registro');
|
|
32
33
|
const permissao = require('../lib/permissao');
|
|
34
|
+
const perigo = require('../lib/perigo');
|
|
33
35
|
const skills = require('../lib/skills');
|
|
36
|
+
const checkpoint = require('../lib/checkpoint');
|
|
37
|
+
const comandosLib = require('../lib/comandos');
|
|
34
38
|
const marketplace = require('../lib/marketplace');
|
|
35
39
|
const vozModo = require('../lib/voz');
|
|
36
40
|
const appLib = require('../lib/app');
|
|
37
41
|
const conta = require('../lib/conta');
|
|
38
42
|
const nuvem = require('../lib/nuvem');
|
|
39
43
|
const compat = require('../lib/compatibilidade');
|
|
44
|
+
const mencoesLib = require('../lib/mencoes');
|
|
45
|
+
const extra = require('../lib/comandos-extra');
|
|
46
|
+
const offline = require('../lib/offline');
|
|
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
|
+
});
|
|
56
|
+
const tempos = require('../lib/tempos');
|
|
40
57
|
|
|
41
58
|
// Rede de segurança de último recurso: sem isto, uma exceção fora de um
|
|
42
59
|
// try/catch (ex.: dentro de um callback de socket, como o parser de SSE) não
|
|
@@ -91,27 +108,90 @@ const state = {
|
|
|
91
108
|
// existe para a frente em vez de subir outro servidor ao lado.
|
|
92
109
|
let janelaAberta = null;
|
|
93
110
|
|
|
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
|
+
function opcoesDaJanela(extra) {
|
|
116
|
+
return Object.assign({
|
|
117
|
+
versao: VERSION,
|
|
118
|
+
enviar: (t) => sendPrompt(t),
|
|
119
|
+
barra: (t) => handleSlash(t),
|
|
120
|
+
interromper,
|
|
121
|
+
estado: () => ({
|
|
122
|
+
pasta: state.workDir ? shortPath(state.workDir) : null,
|
|
123
|
+
projeto: state.currentProject,
|
|
124
|
+
modo: state.mode,
|
|
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),
|
|
131
|
+
}),
|
|
132
|
+
// Lida na hora, não guardada: o /dir e o /project trocam a pasta
|
|
133
|
+
// com a janela aberta, e o painel tem de acompanhar.
|
|
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),
|
|
157
|
+
}, extra || {});
|
|
158
|
+
}
|
|
159
|
+
|
|
94
160
|
const HELP = `
|
|
95
161
|
${c.bold('Comandos')}
|
|
96
162
|
${c.brand('/dir')} <caminho> trabalha numa pasta que já existe (onde você quiser)
|
|
97
163
|
${c.brand('/project')} lista projetos ou cria em ~/primocode: /project <nome>
|
|
164
|
+
${c.brand('/projetos')} [n] projetos do mais recente, com tamanho · /projetos 2 entra
|
|
165
|
+
${c.brand('/novo')} <descrição> começa um projeto NOVO do zero, em pasta própria
|
|
98
166
|
${c.brand('/mode')} /mode primocode|chat|auto
|
|
99
167
|
${c.brand('/effort')} esforço do agente — do rápido ao mais esperto
|
|
168
|
+
${c.brand('/permissoes')} quando ele pergunta antes de agir: auto|perguntar|livre
|
|
100
169
|
${c.brand('/compact')} resume a conversa e devolve o contexto
|
|
101
|
-
${c.brand('/
|
|
170
|
+
${c.brand('/init')} estuda o projeto e escreve o PRIMO.md
|
|
171
|
+
${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido · ou @arquivo no pedido
|
|
102
172
|
${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
|
|
103
173
|
${c.brand('/all')} lista tudo que ele sabe fazer
|
|
174
|
+
${c.brand('/desfazer')} [lista|N] desfaz o que o último pedido (ou os N últimos) mudou
|
|
104
175
|
${c.brand('/tree')} [n] árvore do projeto
|
|
105
176
|
${c.brand('/read')} <arquivo> mostra um arquivo
|
|
106
177
|
${c.brand('/run')} <comando> executa no shell do projeto
|
|
107
178
|
${c.brand('/open')} <arq|url> abre no navegador
|
|
179
|
+
${c.brand('/abrir')} abre o projeto: o servidor de pé, ou a index.html
|
|
180
|
+
${c.brand('/copiar')} copia a última resposta
|
|
181
|
+
${c.brand('/historico')} [termo] procura nos seus pedidos antigos
|
|
108
182
|
${c.brand('/rc')} abre a janela do PrimoCode (chat, sem terminal)
|
|
183
|
+
${c.brand('/ide')} abre a janela com a prévia do site e os arquivos ao lado
|
|
109
184
|
${c.brand('/login')} entra na sua conta · ${c.brand('/account')} mostra o plano
|
|
110
185
|
${c.brand('/chats')} [n] suas conversas — as mesmas do app e do celular
|
|
111
186
|
${c.brand('/logout')} desconecta este computador da conta
|
|
187
|
+
${c.brand('/comandos')} seus comandos (/revisar, /explicar…) · /comandos novo <nome>
|
|
112
188
|
${c.brand('/status')} checa o servidor
|
|
189
|
+
${c.brand('/offline')} roda o servidor no seu computador (temporário) · /offline off volta
|
|
190
|
+
${c.brand('/doctor')} confere tudo (servidor, /api, Chrome, disco…) e diz como consertar
|
|
191
|
+
${c.brand('/custo')} tokens e pedidos desta sessão, por provedor
|
|
192
|
+
${c.brand('/atalhos')} as teclas que ajudam
|
|
113
193
|
${c.brand('/name')} <nome> como o Primo te chama
|
|
114
|
-
${c.brand('/mascot')}
|
|
194
|
+
${c.brand('/mascot')} [nome|n] galeria dos mascotes · /mascot reacoes on|off
|
|
115
195
|
${c.brand('/config')} mostra suas preferências
|
|
116
196
|
${c.brand('/clear')} limpa a conversa ${c.brand('/cls')} limpa a tela
|
|
117
197
|
${c.brand('/uninstall')} remove o PrimoCode
|
|
@@ -137,19 +217,28 @@ ${c.bold('Onde ele trabalha')}
|
|
|
137
217
|
const SLASH_COMANDOS = [
|
|
138
218
|
['/help', 'todos os comandos'],
|
|
139
219
|
['/project', 'lista/cria projetos em ~/primocode'],
|
|
220
|
+
['/projetos', 'projetos do mais recente, com tamanho · /projetos 2 entra'],
|
|
221
|
+
['/novo', 'começa um projeto novo do zero: /novo site da padaria'],
|
|
140
222
|
['/dir', 'trabalhar numa pasta sua'],
|
|
141
223
|
['/mode', 'primocode | chat | auto'],
|
|
142
224
|
['/effort', 'esforço: low → ultracode (←/→ para ajustar)'],
|
|
225
|
+
['/permissoes', 'auto | perguntar | livre — quando ele pede licença'],
|
|
143
226
|
['/compact', 'resume a conversa e devolve o contexto'],
|
|
227
|
+
['/init', 'estuda o projeto e escreve o PRIMO.md (as instruções dele)'],
|
|
144
228
|
['/attach', 'anexa arquivo ao pedido'],
|
|
145
229
|
['/detach', 'remove os anexos'],
|
|
146
230
|
['/files', 'lista os anexos'],
|
|
147
231
|
['/all', 'tudo que ele sabe fazer'],
|
|
232
|
+
['/desfazer', 'desfaz o que o último pedido mudou nos arquivos (lista · N)'],
|
|
148
233
|
['/tree', 'árvore do projeto'],
|
|
149
234
|
['/read', 'mostra um arquivo'],
|
|
150
235
|
['/run', 'roda um comando'],
|
|
151
236
|
['/open', 'abre no navegador'],
|
|
237
|
+
['/abrir', 'abre o projeto atual: o servidor de pé, ou a index.html'],
|
|
238
|
+
['/copiar', 'copia a última resposta'],
|
|
239
|
+
['/historico', 'procura nos seus pedidos antigos: /historico login'],
|
|
152
240
|
['/rc', 'abre a janela do PrimoCode'],
|
|
241
|
+
['/ide', 'abre a janela com a prévia do site e os arquivos ao lado'],
|
|
153
242
|
['/login', 'entra na sua conta Conecta Primo'],
|
|
154
243
|
['/account', 'sua conta, plano e computadores conectados'],
|
|
155
244
|
['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
|
|
@@ -160,10 +249,16 @@ const SLASH_COMANDOS = [
|
|
|
160
249
|
['/skills', 'o que ele sabe fazer bem — ligue e desligue'],
|
|
161
250
|
['/marketplace', 'a tela das skills: navegue, ligue, crie'],
|
|
162
251
|
['/install-skill', 'instala uma skill de um arquivo'],
|
|
252
|
+
['/comandos', 'seus comandos: um .md vira /nome · /comandos novo <nome>'],
|
|
253
|
+
['/api', 'usa a SUA chave (Gemini, ChatGPT, Claude…) direto daqui, sem o limite do servidor'],
|
|
163
254
|
['/status', 'checa o servidor'],
|
|
255
|
+
['/offline', 'roda o servidor aqui no seu computador, com as chaves do Render'],
|
|
256
|
+
['/doctor', 'confere se está tudo certo e diz como consertar'],
|
|
257
|
+
['/custo', 'tokens e pedidos desta sessão, por provedor'],
|
|
258
|
+
['/atalhos', 'as teclas que ajudam'],
|
|
164
259
|
['/connectors', 'quem está atendendo os seus pedidos'],
|
|
165
260
|
['/name', 'como ele te chama'],
|
|
166
|
-
['/mascot', '
|
|
261
|
+
['/mascot', 'galeria dos mascotes · escolha por nome ou número'],
|
|
167
262
|
['/config', 'suas preferências'],
|
|
168
263
|
['/clear', 'limpa a conversa'],
|
|
169
264
|
['/cls', 'limpa a tela'],
|
|
@@ -172,6 +267,69 @@ const SLASH_COMANDOS = [
|
|
|
172
267
|
['/exit', 'sair'],
|
|
173
268
|
];
|
|
174
269
|
|
|
270
|
+
/* ── /api: a chave da própria pessoa ──────────────────────────────
|
|
271
|
+
* "Fica batendo no limite. Coloca um /provider onde você seleciona se é
|
|
272
|
+
* Gemini, ChatGPT etc., cola a key e ela roda local." (23/09/2026)
|
|
273
|
+
* Com provedor, o agente fala direto com a API da pessoa (lib/local/motor.js);
|
|
274
|
+
* sem, tudo segue pelo servidor como sempre. */
|
|
275
|
+
async function comandoApi(rest) {
|
|
276
|
+
const P = provedorLocal.PROVEDORES;
|
|
277
|
+
const ids = Object.keys(P);
|
|
278
|
+
const arg = String(rest || '').trim();
|
|
279
|
+
const atual = provedorLocal.atual();
|
|
280
|
+
|
|
281
|
+
if (/^(off|desliga[r]?|servidor|nenhum|sair)$/i.test(arg)) {
|
|
282
|
+
provedorLocal.desligar();
|
|
283
|
+
console.log(c.ok(' De volta ao servidor do PrimoCode.') + c.muted(' (a chave foi apagada desta máquina)'));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const trocaModelo = arg.match(/^(modelo|model)\s+(\S+)$/i);
|
|
287
|
+
if (trocaModelo) {
|
|
288
|
+
if (!atual) { console.log(c.warn(' Nenhum provedor configurado. Rode /api primeiro.')); return; }
|
|
289
|
+
provedorLocal.trocarModelo(trocaModelo[2]);
|
|
290
|
+
console.log(c.ok(` ${atual.nome} · modelo ${trocaModelo[2]}`));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const apelido = { claude: 'claude-code', gpt: 'chatgpt', openai: 'chatgpt' }[arg.toLowerCase()];
|
|
295
|
+
let id = apelido || ids.find((k) => k === arg.toLowerCase() || P[k].nome.toLowerCase().startsWith(arg.toLowerCase()) && arg);
|
|
296
|
+
if (!id) {
|
|
297
|
+
console.log('');
|
|
298
|
+
console.log(' ' + c.bold('Com qual IA você quer rodar?') + c.muted(' a sua chave ou a sua assinatura, sem o limite do servidor'));
|
|
299
|
+
if (atual) console.log(' ' + c.muted('agora: ') + c.white(atual.cli ? atual.nome : `${atual.nome} · ${atual.modelo} · chave ${provedorLocal.mascarar(atual.chave)}`));
|
|
300
|
+
ids.forEach((k, i) => console.log(` ${c.brand(String(i + 1))} ${c.white(P[k].nome.padEnd(24))}${c.dim(P[k].dica)}`));
|
|
301
|
+
console.log(` ${c.brand('0')} ${c.white('Servidor do PrimoCode'.padEnd(24))}${c.dim('sem chave, com o limite do plano')}`);
|
|
302
|
+
const escolha = String(await ask(` ${c.brand('?')} Número: `)).trim();
|
|
303
|
+
if (escolha === '0') return comandoApi('off');
|
|
304
|
+
id = ids[Number(escolha) - 1];
|
|
305
|
+
if (!id) { console.log(c.muted(' Nada mudou.')); return; }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const p = P[id];
|
|
309
|
+
// Claude Code e Codex não pedem chave: basta a ferramenta estar instalada
|
|
310
|
+
// e logada. Quem pensa é ela; quem executa é o PrimoCode.
|
|
311
|
+
if (p.cli) {
|
|
312
|
+
if (!require('../lib/local/motor-cli').instalado(id)) {
|
|
313
|
+
console.log(c.warn(` O ${p.nome} não está instalado nesta máquina.`) + c.muted(` Veja ${p.onde}`));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const salvo = provedorLocal.salvar({ provedor: id, modelo: p.modelos[0] });
|
|
317
|
+
console.log(c.ok(` Pronto: ${salvo.nome}`) + c.muted(' · usa a sua assinatura, sem chave'));
|
|
318
|
+
console.log(c.dim(` O ${p.nome} decide, o PrimoCode executa. /api off volta ao servidor`));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
console.log(c.muted(` Pegue a chave em `) + c.white(p.onde));
|
|
322
|
+
const chave = String(await askSecret(` ${c.brand('?')} Cole a chave do ${p.nome} (não aparece na tela): `)).trim();
|
|
323
|
+
if (!chave) { console.log(c.muted(' Nada mudou.')); return; }
|
|
324
|
+
const sp = new Spinner('conferindo a chave').start();
|
|
325
|
+
const r = await provedorLocal.conferir(id, chave);
|
|
326
|
+
sp.stop();
|
|
327
|
+
if (!r.ok) { console.log(c.warn(` ${r.error}`)); return; }
|
|
328
|
+
const salvo = provedorLocal.salvar({ provedor: id, chave, modelo: r.modelo });
|
|
329
|
+
console.log(c.ok(` Pronto: ${salvo.nome} · ${salvo.modelo}`) + c.muted(` · chave ${provedorLocal.mascarar(chave)}`));
|
|
330
|
+
console.log(c.dim(' Agora os pedidos vão direto daqui, com a sua cota. /api modelo <nome> troca o modelo · /api off volta ao servidor'));
|
|
331
|
+
}
|
|
332
|
+
|
|
175
333
|
// ── /effort: o slider de esforço (estilo Claude Code) ──────────────────
|
|
176
334
|
// Régua com os níveis, o marcador ▲ embaixo do escolhido, "Faster" à
|
|
177
335
|
// esquerda e "Smarter" à direita. ←/→ move, Enter confirma, Esc cancela.
|
|
@@ -247,9 +405,34 @@ function aplicarEffort(nome) {
|
|
|
247
405
|
// Enquanto o slider está no ar, o fantasma do autocompletar não desenha.
|
|
248
406
|
let sliderAtivo = false;
|
|
249
407
|
|
|
408
|
+
/* Os comandos da pessoa (lib/comandos.js), com o do projeto atual por cima.
|
|
409
|
+
A caixa de digitar pede a lista a cada tecla para desenhar o fantasma; um
|
|
410
|
+
cache de dois segundos poupa ler três pastas por letra digitada, e ainda
|
|
411
|
+
pega na hora um arquivo que a pessoa acabou de salvar. */
|
|
412
|
+
let cacheComandos = { em: 0, dir: null, lista: [] };
|
|
413
|
+
function comandosDaPessoa() {
|
|
414
|
+
const agora = Date.now();
|
|
415
|
+
if (agora - cacheComandos.em > 2000 || cacheComandos.dir !== state.workDir) {
|
|
416
|
+
let lista = [];
|
|
417
|
+
try { lista = comandosLib.carregar({ projectDir: state.workDir }); } catch { /* disco estranho não trava a caixa */ }
|
|
418
|
+
cacheComandos = { em: agora, dir: state.workDir, lista };
|
|
419
|
+
}
|
|
420
|
+
return cacheComandos.lista;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* O menu e o fantasma enxergam os dois tipos. O embutido vem primeiro e,
|
|
424
|
+
com o mesmo nome, o da pessoa nem aparece: quem ganha é sempre o embutido. */
|
|
425
|
+
function comandosDoMenu() {
|
|
426
|
+
const embutidos = new Set(SLASH_COMANDOS.map(([n]) => n));
|
|
427
|
+
const deles = comandosDaPessoa()
|
|
428
|
+
.filter((x) => !embutidos.has('/' + x.nome))
|
|
429
|
+
.map((x) => ['/' + x.nome, x.descricao || `seu comando (${x.origem})`]);
|
|
430
|
+
return SLASH_COMANDOS.concat(deles);
|
|
431
|
+
}
|
|
432
|
+
|
|
250
433
|
function mostrarMenuSlash() {
|
|
251
434
|
console.log('');
|
|
252
|
-
for (const [nome, desc] of
|
|
435
|
+
for (const [nome, desc] of comandosDoMenu()) {
|
|
253
436
|
console.log(` ${c.brand(ui.padRight(nome, 12))}${c.muted(desc)}`);
|
|
254
437
|
}
|
|
255
438
|
console.log(c.dim('\n Tab completa · metade do comando já serve: /eff vira /effort'));
|
|
@@ -489,14 +672,18 @@ const RE_PROJETO_NOVO = /\b(novo projeto|projeto novo|outro projeto|do zero|come
|
|
|
489
672
|
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/;
|
|
490
673
|
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/;
|
|
491
674
|
|
|
675
|
+
/* "crie uma nova seção", "cria um botão", "faz uma tela de login": é um
|
|
676
|
+
PEDAÇO do projeto em que se está, não outro projeto. */
|
|
677
|
+
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/;
|
|
678
|
+
|
|
492
679
|
function pedeProjetoNovo(texto) {
|
|
493
680
|
const t = semAcento(texto);
|
|
494
681
|
if (RE_PROJETO_NOVO.test(t)) return true;
|
|
495
|
-
if (!RE_CRIAR_UM.test(t) || RE_REFERE_ATUAL.test(t)) return false;
|
|
682
|
+
if (!RE_CRIAR_UM.test(t) || RE_REFERE_ATUAL.test(t) || RE_PEDACO.test(t)) return false;
|
|
496
683
|
// Mesmo assunto do projeto atual ("crie uma fase nova pro jogo de carro")
|
|
497
684
|
// é continuação, não projeto novo.
|
|
498
|
-
const atual = new Set(cfg.
|
|
499
|
-
const pedido = cfg.
|
|
685
|
+
const atual = new Set(cfg.tokensDoAssunto(state.currentProject || ''));
|
|
686
|
+
const pedido = cfg.tokensDoAssunto(deriveProjectName(texto));
|
|
500
687
|
return !pedido.some((x) => atual.has(x));
|
|
501
688
|
}
|
|
502
689
|
|
|
@@ -547,6 +734,8 @@ function ehSoEstudio(texto) {
|
|
|
547
734
|
}
|
|
548
735
|
|
|
549
736
|
function planWorkDir(userText) {
|
|
737
|
+
// O /novo já criou a pasta e entrou nela: nenhuma heurística decide outra.
|
|
738
|
+
if (state.projetoFixo) return { dir: state.workDir, pending: false, name: state.currentProject };
|
|
550
739
|
const escolhida = ['/dir', 'detectada', 'projeto'].includes(state.workDirOrigem);
|
|
551
740
|
// Pasta escolhida a dedo (/dir, ou dita no pedido) manda sempre. Projeto
|
|
552
741
|
// em andamento manda também — a menos que o pedido abra outra coisa.
|
|
@@ -568,16 +757,17 @@ function planWorkDir(userText) {
|
|
|
568
757
|
paralelo com o trabalho: a pasta só nasce na primeira escrita, e até lá a
|
|
569
758
|
resposta já chegou. Só pergunta quando o pedido é de CRIAR algo — pedir
|
|
570
759
|
para abrir o Chrome não gasta chamada com nome de pasta. */
|
|
760
|
+
async function perguntarNome(texto) {
|
|
761
|
+
let resposta = '';
|
|
762
|
+
await api.requestChat(state.config.server, {
|
|
763
|
+
prompt: texto, history: [], model: state.config.model, token: state.config.token,
|
|
764
|
+
}, (evt) => { if (evt.delta) resposta += evt.delta; });
|
|
765
|
+
return resposta;
|
|
766
|
+
}
|
|
767
|
+
|
|
571
768
|
function batizarDepois(plan, userText) {
|
|
572
769
|
if (!plan.pending || !(RE_CRIAR_UM.test(semAcento(userText)) || PEDIDO_DE_CODIGO.test(userText))) return;
|
|
573
|
-
|
|
574
|
-
let resposta = '';
|
|
575
|
-
await api.requestChat(state.config.server, {
|
|
576
|
-
prompt: texto, history: [], model: state.config.model, token: state.config.token,
|
|
577
|
-
}, (evt) => { if (evt.delta) resposta += evt.delta; });
|
|
578
|
-
return resposta;
|
|
579
|
-
};
|
|
580
|
-
nomeProjeto.escolher({ pedido: userText, nomes: cfg.listProjects(), perguntar })
|
|
770
|
+
plan.batizando = nomeProjeto.escolher({ pedido: userText, nomes: cfg.listProjects(), perguntar: perguntarNome })
|
|
581
771
|
.then((nome) => { if (nome) plan.nomeEscolhido = nome; });
|
|
582
772
|
}
|
|
583
773
|
|
|
@@ -661,8 +851,15 @@ function prepararPasta(userText) {
|
|
|
661
851
|
* Enter sozinho é NÃO. O padrão de quem apertou Enter sem ler não pode ser
|
|
662
852
|
* entregar o mouse e o teclado.
|
|
663
853
|
*/
|
|
664
|
-
async function autorizarControle(oQueVaiFazer, { primeira } = {}) {
|
|
665
|
-
|
|
854
|
+
async function autorizarControle(oQueVaiFazer, { primeira, tipo } = {}) {
|
|
855
|
+
/* O mesmo portão serve ao /permissoes (lib/perigo.js): a pergunta muda de
|
|
856
|
+
cabeçalho, as três respostas são as mesmas. */
|
|
857
|
+
if (tipo) {
|
|
858
|
+
console.log('');
|
|
859
|
+
console.log(' ' + c.warn('▸▸') + ' ' + c.bold(c.white(tipo === 'perigo'
|
|
860
|
+
? 'Isto não tem volta. O Primo quer:'
|
|
861
|
+
: 'O Primo quer (você escolheu /permissoes perguntar):')));
|
|
862
|
+
} else if (primeira) {
|
|
666
863
|
console.log('');
|
|
667
864
|
console.log(' ' + c.warn('▸▸') + ' ' + c.bold(c.white('O Primo quer usar o seu computador.')));
|
|
668
865
|
console.log(' ' + c.muted('Ele vai mexer no mouse e no teclado de verdade, no programa que'));
|
|
@@ -693,7 +890,15 @@ async function autorizarControle(oQueVaiFazer, { primeira } = {}) {
|
|
|
693
890
|
* simplesmente travava. Abrir o /voice e PEDIR em voz para ele mexer no
|
|
694
891
|
* computador já é a autorização — é o produto funcionando como o Jarvis. Fica
|
|
695
892
|
* dito no terminal, a cada ação, o que ele fez; e fechar o PrimoCode esquece. */
|
|
696
|
-
async function autorizarPorVoz(oQueVaiFazer, { primeira } = {}) {
|
|
893
|
+
async function autorizarPorVoz(oQueVaiFazer, { primeira, tipo } = {}) {
|
|
894
|
+
/* Pedir em voz é autorizar o que foi pedido — mas não o que não tem volta.
|
|
895
|
+
Ninguém está lendo o terminal para ver um `rm -rf` passar: o perigoso
|
|
896
|
+
é recusado, e o agente conta em voz o que ficou por fazer. */
|
|
897
|
+
if (tipo === 'perigo') {
|
|
898
|
+
console.log(' ' + c.warn('▸▸ recusado no modo voz: ') + c.muted(oQueVaiFazer));
|
|
899
|
+
return 'nao';
|
|
900
|
+
}
|
|
901
|
+
if (tipo) return 'agora';
|
|
697
902
|
if (primeira) {
|
|
698
903
|
console.log(' ' + c.warn('▸▸') + ' ' + c.muted('Modo voz: o Primo vai usar o mouse e o teclado. Você vê o cursor dele na tela.'));
|
|
699
904
|
}
|
|
@@ -870,9 +1075,23 @@ async function sendPrompt(userText) {
|
|
|
870
1075
|
return;
|
|
871
1076
|
}
|
|
872
1077
|
|
|
1078
|
+
// PRIMOCODE_TEMPOS=1: uma linha no fim com o custo de cada fase.
|
|
1079
|
+
tempos.comecar();
|
|
1080
|
+
try { await enviarPedido(userText); }
|
|
1081
|
+
finally {
|
|
1082
|
+
const linha = tempos.relatorio();
|
|
1083
|
+
if (linha) console.log(c.dim(` ⏱ ${linha}`));
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async function enviarPedido(userText) {
|
|
873
1088
|
let prompt = userText;
|
|
874
|
-
|
|
875
|
-
|
|
1089
|
+
// "@index.html" no pedido é um /attach só deste pedido: vai junto dos
|
|
1090
|
+
// anexos, mas não fica em state.attachedFiles para o próximo.
|
|
1091
|
+
const citados = mencoesDoPedido(userText);
|
|
1092
|
+
const anexos = [...state.attachedFiles, ...citados];
|
|
1093
|
+
if (anexos.length) {
|
|
1094
|
+
const parts = anexos.map((f) => `Arquivo: ${f.path}\n\`\`\`\n${f.content}\n\`\`\``);
|
|
876
1095
|
prompt = parts.join('\n\n') + '\n\n' + userText;
|
|
877
1096
|
}
|
|
878
1097
|
|
|
@@ -902,15 +1121,22 @@ async function sendPrompt(userText) {
|
|
|
902
1121
|
|
|
903
1122
|
// O resumo da rodada anterior é feito em segundo plano logo depois
|
|
904
1123
|
// dela; aqui só se espera ele terminar, para não sair com memória velha.
|
|
905
|
-
|
|
906
|
-
|
|
1124
|
+
//
|
|
907
1125
|
// O pedido passa por uma cabeça antes de virar trabalho: conserta o
|
|
908
1126
|
// que está torto, diz qual é o entregável e o que conta como pronto.
|
|
909
1127
|
// UMA chamada faz as duas coisas: arruma o pedido torto e, se fizer
|
|
910
1128
|
// sentido, divide em passos. Eram duas idas ao modelo por pedido, e
|
|
911
1129
|
// num plano limitado por REQUISIÇÕES (o grátis da Groq são 30 por
|
|
912
1130
|
// minuto) cada chamada extra custa tanto quanto token.
|
|
913
|
-
|
|
1131
|
+
//
|
|
1132
|
+
// As duas esperas correm JUNTAS. Uma não lê o que a outra escreve: o
|
|
1133
|
+
// plano só olha o texto do pedido, e o resumo só a conversa de antes.
|
|
1134
|
+
// Em fila, o pedido de duas fases pagava o resumo e DEPOIS o plano —
|
|
1135
|
+
// duas idas ao modelo em série antes da primeira rodada.
|
|
1136
|
+
const [, passos] = await Promise.all([
|
|
1137
|
+
tempos.medir('espera do resumo', () => esperarResumo()),
|
|
1138
|
+
tempos.medir('plano', () => dividirTarefa(prompt)),
|
|
1139
|
+
]);
|
|
914
1140
|
|
|
915
1141
|
let r = null;
|
|
916
1142
|
let jaFeito = '';
|
|
@@ -919,6 +1145,10 @@ async function sendPrompt(userText) {
|
|
|
919
1145
|
interessa é o do pedido inteiro: quem pediu um vídeo não quer três
|
|
920
1146
|
balanços, quer saber o que existe no disco quando ele terminar. */
|
|
921
1147
|
const livro = registro.novo();
|
|
1148
|
+
// Um checkpoint por PEDIDO (não por passo): é o que o /desfazer volta.
|
|
1149
|
+
// Com a pasta: a foto do projeto inteiro sai AGORA, antes de qualquer
|
|
1150
|
+
// tool — é ela que dá volta ao `rm`/`mv` que o run_shell fizer.
|
|
1151
|
+
checkpoint.iniciar(userText, projectDir());
|
|
922
1152
|
for (let i = 0; i < passos.length; i++) {
|
|
923
1153
|
if (i > 0) console.log(c.dim(` ⎿ ${passos[i].titulo}`));
|
|
924
1154
|
// Só fato de passo CONCLUÍDO entra aqui. Num pedido real o passo de
|
|
@@ -948,8 +1178,11 @@ async function sendPrompt(userText) {
|
|
|
948
1178
|
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
949
1179
|
// O portão do computador: quem pergunta é o terminal, quem
|
|
950
1180
|
// lembra da resposta é a sessão.
|
|
951
|
-
|
|
1181
|
+
// No pedido único (`primo "…"`) não há ninguém para responder:
|
|
1182
|
+
// sem `autorizar`, o que pede licença é recusado (lib/perigo.js).
|
|
1183
|
+
autorizar: state.semTerminal ? undefined : autorizarControle,
|
|
952
1184
|
controle: state.controle,
|
|
1185
|
+
permissoes: state.config.permissoes,
|
|
953
1186
|
// Pedido de mexer no computador leva a dica que libera as
|
|
954
1187
|
// ferramentas desktop_* no servidor (ver DICA_COMPUTADOR).
|
|
955
1188
|
instrucao: i === 0 && querComputador(userText) ? vozModo.DICA_COMPUTADOR : '',
|
|
@@ -993,12 +1226,19 @@ async function sendPrompt(userText) {
|
|
|
993
1226
|
: '';
|
|
994
1227
|
if (r.parado) break; // Esc Esc no meio do plano para o plano inteiro
|
|
995
1228
|
}
|
|
1229
|
+
checkpoint.encerrar();
|
|
996
1230
|
|
|
997
1231
|
/* O balanço, depois da resposta. Depois, e não antes, de propósito: o
|
|
998
1232
|
que a pessoa quer ler primeiro é o que ele TEM A DIZER; a lista de
|
|
999
1233
|
arquivos é a conferência, e conferência se lê por último. */
|
|
1000
1234
|
const balanco = registro.resumo(livro, { colunas: process.stdout.columns || 80 });
|
|
1001
1235
|
if (balanco) console.log(balanco);
|
|
1236
|
+
// O rostinho do mascote, por último e numa linha só. O humor sai do
|
|
1237
|
+
// que aconteceu, não de adivinhar pelo texto da resposta.
|
|
1238
|
+
if (state.config.reacoes !== false && r) {
|
|
1239
|
+
const humor = r.parado ? 'parado' : r.concluiu === false ? 'preocupado' : 'feliz';
|
|
1240
|
+
console.log(ui.reacao(state.config.mascot, humor));
|
|
1241
|
+
}
|
|
1002
1242
|
state.history.push({ role: 'user', content: userText });
|
|
1003
1243
|
state.history.push({ role: 'assistant', content: r.assistantMessage.content });
|
|
1004
1244
|
resumirDepois();
|
|
@@ -1009,8 +1249,8 @@ async function sendPrompt(userText) {
|
|
|
1009
1249
|
state.gravador.turno(userText, r.assistantMessage.content,
|
|
1010
1250
|
{ modo: 'primocode', projeto: state.currentProject || null });
|
|
1011
1251
|
} else {
|
|
1012
|
-
await esperarResumo();
|
|
1013
|
-
await runChat(prompt, userText);
|
|
1252
|
+
await tempos.medir('espera do resumo', () => esperarResumo());
|
|
1253
|
+
await tempos.medir('chat', () => runChat(prompt, userText));
|
|
1014
1254
|
}
|
|
1015
1255
|
|
|
1016
1256
|
state.attachedFiles = [];
|
|
@@ -1186,6 +1426,31 @@ async function esperarResumo(tetoMs = 15000) {
|
|
|
1186
1426
|
/* A linha que vai junto do resumo: em que projeto se está. É o que faz
|
|
1187
1427
|
"continua o jogo" e "muda a cor" caírem no mesmo projeto em vez de inventar
|
|
1188
1428
|
outro. */
|
|
1429
|
+
function mencoesDoPedido(userText) {
|
|
1430
|
+
const { arquivos, ignorados } = mencoesLib.mencoes(userText, projectDir());
|
|
1431
|
+
const lidos = [];
|
|
1432
|
+
for (const a of arquivos) {
|
|
1433
|
+
try {
|
|
1434
|
+
lidos.push({ path: a.rel, content: fs.readFileSync(a.abs, 'utf-8') });
|
|
1435
|
+
console.log(c.dim(` ⎿ anexado: ${a.rel} (${tools.fmtBytes(a.bytes)})`));
|
|
1436
|
+
} catch { console.log(c.dim(` ⎿ não consegui ler: ${a.rel}`)); }
|
|
1437
|
+
}
|
|
1438
|
+
for (const i of ignorados) console.log(c.dim(` ⎿ não anexei ${i.rel}: ${i.motivo}`));
|
|
1439
|
+
return lidos;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/* O pedido do /init. Passa pelo MESMO caminho de um pedido digitado
|
|
1443
|
+
(sendPrompt → runAct), então usa as ferramentas e o provedor de agora — um
|
|
1444
|
+
/init que falasse direto com o modelo não enxergaria os arquivos. */
|
|
1445
|
+
const PEDIDO_INIT = 'Estude este projeto (a pasta atual) e escreva um arquivo PRIMO.md na raiz dela. '
|
|
1446
|
+
+ 'Primeiro olhe de verdade: liste os arquivos, leia o README, o package.json (ou o equivalente) e os arquivos principais. '
|
|
1447
|
+
+ 'Depois escreva o PRIMO.md em português, curto (menos de 60 linhas), com estas seções: '
|
|
1448
|
+
+ 'o que é o projeto (2 ou 3 frases); como rodar (os comandos exatos que você viu, de instalar, rodar e testar); '
|
|
1449
|
+
+ 'a stack (linguagem, frameworks, banco); as convenções que o código segue (nomes, estilo, onde ficam os testes); '
|
|
1450
|
+
+ 'e onde está cada coisa (as pastas e arquivos que importam, uma linha cada). '
|
|
1451
|
+
+ 'Só escreva o que você conferiu nos arquivos — nada de inventar comando ou dependência. '
|
|
1452
|
+
+ 'Esse arquivo vai ser lido no começo de todo pedido neste projeto, então cada linha precisa valer o espaço.';
|
|
1453
|
+
|
|
1189
1454
|
function contextoDoProjeto() {
|
|
1190
1455
|
if (!state.currentProject) return '';
|
|
1191
1456
|
return `Projeto atual: ${state.currentProject} (pasta ${shortPath(projectDir())}). `
|
|
@@ -1193,6 +1458,14 @@ function contextoDoProjeto() {
|
|
|
1193
1458
|
+ 'trabalhe nos arquivos que já existem lá, não crie outro projeto.';
|
|
1194
1459
|
}
|
|
1195
1460
|
|
|
1461
|
+
/* O chat não passa pelo runAct, e é no runAct (buildMemoryContext) que as
|
|
1462
|
+
instruções do projeto entram. Sem isto o PRIMO.md valeria para o agente e
|
|
1463
|
+
seria ignorado quando o modo auto manda o pedido para a conversa. */
|
|
1464
|
+
function comInstrucoes(history) {
|
|
1465
|
+
const texto = mencoesLib.instrucoesDoProjeto(state.currentProject || state.workDir ? projectDir() : null);
|
|
1466
|
+
return texto ? [{ role: 'system', content: texto }, ...history] : history;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1196
1469
|
// O chat também precisa parar no Esc Esc. Sem isto o texto continuava saindo
|
|
1197
1470
|
// na tela depois de o usuário ter mandado parar.
|
|
1198
1471
|
let chatEmVoo = null;
|
|
@@ -1219,7 +1492,7 @@ async function runChat(promptForModel, userTextForMemory) {
|
|
|
1219
1492
|
try {
|
|
1220
1493
|
await api.requestChat(state.config.server, {
|
|
1221
1494
|
prompt: promptForModel,
|
|
1222
|
-
history: memoria.paraEnviar(state.memoria, state.history, contextoDoProjeto()),
|
|
1495
|
+
history: comInstrucoes(memoria.paraEnviar(state.memoria, state.history, contextoDoProjeto())),
|
|
1223
1496
|
model: state.config.model,
|
|
1224
1497
|
token: state.config.token,
|
|
1225
1498
|
sinal: chatEmVoo.signal,
|
|
@@ -1339,13 +1612,59 @@ const RENOMEADOS = {
|
|
|
1339
1612
|
conta: 'account',
|
|
1340
1613
|
sair: 'exit', // sair é SAIR; desconectar a conta é /logout
|
|
1341
1614
|
// o histórico
|
|
1342
|
-
|
|
1615
|
+
// /historico deixou de ser apelido do /chats: agora procura nos pedidos
|
|
1616
|
+
// guardados nesta máquina (lib/comandos-extra.js). /chats segue sendo a
|
|
1617
|
+
// conversa inteira, da conta.
|
|
1618
|
+
conversas: 'chats',
|
|
1343
1619
|
// o resto
|
|
1344
1620
|
pasta: 'dir',
|
|
1345
1621
|
esforco: 'effort', esforço: 'effort',
|
|
1346
1622
|
tudo: 'all', capacidades: 'all',
|
|
1347
1623
|
};
|
|
1348
1624
|
|
|
1625
|
+
/* Abrir uma conversa salva e seguir dela. Uma função só, para o /chats n e o
|
|
1626
|
+
`primo -c` carregarem a conversa do MESMO jeito: dois caminhos divergiriam
|
|
1627
|
+
no primeiro detalhe (a memória, o gravador) e um deles gravaria errado. */
|
|
1628
|
+
async function abrirConversa(alvo) {
|
|
1629
|
+
const sp = new Spinner('abrindo').start();
|
|
1630
|
+
const conv = await nuvem.lerConversa(alvo.id);
|
|
1631
|
+
sp.stop();
|
|
1632
|
+
if (!conv.ok) return conv;
|
|
1633
|
+
// A memória entra ZERADA: o resumo antigo não veio junto, e um
|
|
1634
|
+
// resumo de outra conversa colado aqui faria o modelo responder
|
|
1635
|
+
// sobre coisa que não foi dita nesta.
|
|
1636
|
+
state.history = conv.mensagens.map((m) => ({
|
|
1637
|
+
role: m.papel, content: m.texto,
|
|
1638
|
+
}));
|
|
1639
|
+
state.memoria = memoria.nova();
|
|
1640
|
+
state.gravador.retomar(alvo.id, alvo.titulo, conv.mensagens.length);
|
|
1641
|
+
return conv;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/* `primo --continue` / `primo -c`: volta na última conversa, sem passar pelo
|
|
1645
|
+
/chats. A lista do servidor já vem da mais recente para a mais antiga, então
|
|
1646
|
+
a última é a primeira. Sem conta ou sem conversa, começa do zero e diz. */
|
|
1647
|
+
async function continuarUltima() {
|
|
1648
|
+
if (!conta.atual()) {
|
|
1649
|
+
console.log(c.dim(' ⎿ sem conta conectada, não há conversa para continuar — começando do zero'));
|
|
1650
|
+
return false;
|
|
1651
|
+
}
|
|
1652
|
+
const l = await nuvem.listarConversas({ limite: 1 });
|
|
1653
|
+
const alvo = l.ok && l.conversas[0];
|
|
1654
|
+
if (!alvo) {
|
|
1655
|
+
console.log(c.dim(' ⎿ nenhuma conversa salva ainda — começando do zero'));
|
|
1656
|
+
return false;
|
|
1657
|
+
}
|
|
1658
|
+
const conv = await abrirConversa(alvo);
|
|
1659
|
+
if (!conv.ok) {
|
|
1660
|
+
console.log(c.dim(` ⎿ não consegui abrir a última conversa (${conv.error}) — começando do zero`));
|
|
1661
|
+
return false;
|
|
1662
|
+
}
|
|
1663
|
+
const quando = alvo.atualizada ? new Date(alvo.atualizada).toLocaleString('pt-BR') : 'data desconhecida';
|
|
1664
|
+
console.log(c.dim(` ⎿ continuando: ${alvo.titulo.slice(0, 60)} (${quando})`));
|
|
1665
|
+
return true;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1349
1668
|
async function handleSlash(text) {
|
|
1350
1669
|
const [cmd] = text.trim().slice(1).split(/\s+/);
|
|
1351
1670
|
const rest = text.trim().slice(1 + cmd.length).trim();
|
|
@@ -1360,16 +1679,33 @@ async function handleSlash(text) {
|
|
|
1360
1679
|
switch (nome) {
|
|
1361
1680
|
case 'help': console.log(HELP); break;
|
|
1362
1681
|
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1682
|
+
/* /projetos e /project sem nome são a MESMA lista: antes o /project só
|
|
1683
|
+
listava os nomes, em ordem alfabética, e quem procurava "o de ontem"
|
|
1684
|
+
tinha de abrir pasta por pasta. Agora vem do mais recente, com
|
|
1685
|
+
quando mexeu e o tamanho, e um número para entrar. */
|
|
1686
|
+
case 'projetos': case 'project': {
|
|
1687
|
+
const lista = extra.listarProjetos(cfg.ROOT_DIR);
|
|
1688
|
+
const numero = /^\d+$/.test(rest) ? Number(rest) : 0;
|
|
1689
|
+
if (!rest || (numero && nome === 'projetos') || (numero && numero <= lista.length)) {
|
|
1690
|
+
if (!lista.length) { console.log(c.muted(' Nenhum projeto ainda. Peça algo e eu crio o primeiro.')); break; }
|
|
1691
|
+
if (numero) {
|
|
1692
|
+
const alvo = lista[numero - 1];
|
|
1693
|
+
if (!alvo) { console.log(c.err(` Não há projeto ${numero}. São ${lista.length}.`)); break; }
|
|
1694
|
+
entrarNoProjeto(alvo.dir, 'projeto');
|
|
1695
|
+
console.log(c.ok(` Trabalhando em ${alvo.nome}`) + c.muted(` (${shortPath(alvo.dir)})`));
|
|
1696
|
+
refreshPrompt();
|
|
1697
|
+
break;
|
|
1371
1698
|
}
|
|
1372
|
-
console.log(
|
|
1699
|
+
console.log('');
|
|
1700
|
+
const largura = Math.min(34, Math.max(...lista.map((x) => x.nome.length)));
|
|
1701
|
+
lista.forEach((x, i) => {
|
|
1702
|
+
const aqui = x.nome === state.currentProject;
|
|
1703
|
+
const nomeCol = ui.padRight(ui.truncate(aqui ? c.brand(x.nome) : c.white(x.nome), largura), largura);
|
|
1704
|
+
console.log(` ${aqui ? c.brand('●') : ' '} ${c.dim(String(i + 1).padStart(2))} ${nomeCol} `
|
|
1705
|
+
+ c.muted(ui.padRight(extra.quandoFoi(x.modificado), 11))
|
|
1706
|
+
+ c.dim(`${extra.fmtTamanho(x.bytes)}${x.cortado ? '+' : ''} · ${x.arquivos} arq.`));
|
|
1707
|
+
});
|
|
1708
|
+
console.log(c.dim(`\n ${shortPath(cfg.ROOT_DIR)} · /projetos 1 entra no primeiro · /novo <descrição> começa outro`));
|
|
1373
1709
|
break;
|
|
1374
1710
|
}
|
|
1375
1711
|
const dir = cfg.createProjectDir(rest);
|
|
@@ -1431,6 +1767,38 @@ async function handleSlash(text) {
|
|
|
1431
1767
|
break;
|
|
1432
1768
|
}
|
|
1433
1769
|
|
|
1770
|
+
/* Quando ele pergunta antes de agir. Os três modos e o porquê de cada
|
|
1771
|
+
um estão em lib/perigo.js. */
|
|
1772
|
+
case 'permissoes':
|
|
1773
|
+
case 'permissions': {
|
|
1774
|
+
const DIZ = {
|
|
1775
|
+
auto: 'roda tudo, mas pergunta antes do que não tem volta (apagar fora do projeto, push --force, sudo…)',
|
|
1776
|
+
perguntar: 'pergunta antes de todo comando e de toda escrita de arquivo',
|
|
1777
|
+
livre: 'nunca pergunta — nem antes do que não tem volta',
|
|
1778
|
+
};
|
|
1779
|
+
const atual = perigo.normalizarModo(state.config.permissoes);
|
|
1780
|
+
if (!rest) {
|
|
1781
|
+
console.log(c.muted(' Agora: ') + c.brand(atual) + c.muted(' — ' + DIZ[atual]));
|
|
1782
|
+
for (const m of perigo.MODOS) console.log(` ${m === atual ? c.brand('● ' + m) : c.muted('○ ' + m)} ${c.dim(DIZ[m])}`);
|
|
1783
|
+
console.log(c.dim(' Use /permissoes auto|perguntar|livre'));
|
|
1784
|
+
break;
|
|
1785
|
+
}
|
|
1786
|
+
const pedido = rest.toLowerCase();
|
|
1787
|
+
if (!perigo.MODOS.includes(perigo.normalizarModo(pedido)) || (perigo.normalizarModo(pedido) === 'auto' && pedido !== 'auto')) {
|
|
1788
|
+
console.log(c.err('Uso: /permissoes auto|perguntar|livre'));
|
|
1789
|
+
break;
|
|
1790
|
+
}
|
|
1791
|
+
const m = perigo.normalizarModo(pedido);
|
|
1792
|
+
state.config.permissoes = m;
|
|
1793
|
+
cfg.saveConfig({ permissoes: m });
|
|
1794
|
+
console.log(c.ok(` Permissões: ${m}`) + c.muted(' — ' + DIZ[m]));
|
|
1795
|
+
if (m === 'livre') {
|
|
1796
|
+
console.log(' ' + c.warn('▸▸ Atenção: ele vai apagar, forçar push e usar sudo sem te perguntar.'));
|
|
1797
|
+
console.log(' ' + c.warn(' O que der errado pode não ter volta. /permissoes auto desfaz.'));
|
|
1798
|
+
}
|
|
1799
|
+
break;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1434
1802
|
case 'mode': {
|
|
1435
1803
|
const m = rest.toLowerCase();
|
|
1436
1804
|
if (['primocode', 'chat', 'auto'].includes(m)) {
|
|
@@ -1491,6 +1859,20 @@ async function handleSlash(text) {
|
|
|
1491
1859
|
break;
|
|
1492
1860
|
}
|
|
1493
1861
|
|
|
1862
|
+
case 'init': {
|
|
1863
|
+
const alvo = path.join(projectDir(), 'PRIMO.md');
|
|
1864
|
+
if (fs.existsSync(alvo) && !(await confirm(`Já existe um PRIMO.md em ${shortPath(projectDir())}. Escrever outro por cima?`))) {
|
|
1865
|
+
console.log(c.muted(' Deixei o PRIMO.md como estava.'));
|
|
1866
|
+
break;
|
|
1867
|
+
}
|
|
1868
|
+
// Escrever arquivo pede o agente: no modo chat (ou num auto que
|
|
1869
|
+
// achasse que é conversa) o /init só responderia um texto.
|
|
1870
|
+
const modoAntes = state.mode;
|
|
1871
|
+
state.mode = 'primocode';
|
|
1872
|
+
try { await sendPrompt(PEDIDO_INIT); } finally { state.mode = modoAntes; }
|
|
1873
|
+
break;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1494
1876
|
case 'detach': state.attachedFiles = []; console.log(c.ok('Anexos removidos.')); refreshPrompt(); break;
|
|
1495
1877
|
|
|
1496
1878
|
case 'files':
|
|
@@ -1516,6 +1898,41 @@ async function handleSlash(text) {
|
|
|
1516
1898
|
break;
|
|
1517
1899
|
}
|
|
1518
1900
|
|
|
1901
|
+
/* ── /desfazer ────────────────────────────────────────────────
|
|
1902
|
+
* Quem não sabe git não tem como voltar uma edição ruim. Aqui volta:
|
|
1903
|
+
* cada pedido tirou uma foto do projeto (com git) ou guardou o
|
|
1904
|
+
* original dos arquivos que mexeu (sem git). */
|
|
1905
|
+
case 'desfazer': case 'undo': case 'rewind': {
|
|
1906
|
+
const cps = checkpoint.lista(projectDir());
|
|
1907
|
+
if (!cps.length) { console.log(c.muted(' Nada para desfazer nesta pasta.')); break; }
|
|
1908
|
+
const quando = (cp) => new Date(cp.quando).toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' });
|
|
1909
|
+
if (/^(lista|list|ls)$/i.test(rest)) {
|
|
1910
|
+
cps.forEach((cp, i) => console.log(` ${c.brand(String(i + 1).padStart(2))} ${c.dim(quando(cp))} `
|
|
1911
|
+
+ `${c.white(cp.pedido.slice(0, 50) + (cp.pedido.length > 50 ? '…' : ''))} `
|
|
1912
|
+
+ c.muted(`${cp.mudou.length} ${cp.mudou.length === 1 ? 'arquivo mudou' : 'arquivos mudaram'}`)));
|
|
1913
|
+
console.log(c.muted('\n /desfazer N volta os N últimos pedidos.'));
|
|
1914
|
+
if (checkpoint.modo(projectDir()) === 'arquivo') {
|
|
1915
|
+
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).'));
|
|
1916
|
+
}
|
|
1917
|
+
break;
|
|
1918
|
+
}
|
|
1919
|
+
const n = rest ? parseInt(rest, 10) : 1;
|
|
1920
|
+
if (!(n >= 1)) { console.log(c.err('Uso: /desfazer · /desfazer lista · /desfazer N')); break; }
|
|
1921
|
+
const alvo = cps.slice(0, n);
|
|
1922
|
+
console.log(c.muted(`\n Volta ${alvo.length === 1 ? 'o último pedido' : `os ${alvo.length} últimos pedidos`}:`));
|
|
1923
|
+
for (const cp of alvo) console.log(` ${c.dim(quando(cp))} ${c.white(cp.pedido.slice(0, 60))}`);
|
|
1924
|
+
const previa = checkpoint.previa(projectDir(), n);
|
|
1925
|
+
if (!previa.length) console.log(c.muted(' (o disco já está como estava antes)'));
|
|
1926
|
+
for (const a of previa.slice(0, 40)) console.log(c.muted(` ${a.acao} `) + shortPath(a.caminho));
|
|
1927
|
+
if (previa.length > 40) console.log(c.muted(` … e mais ${previa.length - 40}`));
|
|
1928
|
+
if (!(await confirm('Desfazer?'))) { console.log(c.muted('Cancelado.')); break; }
|
|
1929
|
+
for (const f of checkpoint.desfazer(projectDir(), n)) {
|
|
1930
|
+
const rotulo = f.acao === 'falhou' ? c.err(` falhou: ${f.erro} `) : c.ok(` ${f.acao} `);
|
|
1931
|
+
console.log(rotulo + shortPath(f.caminho));
|
|
1932
|
+
}
|
|
1933
|
+
break;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1519
1936
|
case 'tree': console.log(tools.treeDir(projectDir(), parseInt(rest, 10) || 2)); break;
|
|
1520
1937
|
|
|
1521
1938
|
case 'read': {
|
|
@@ -1538,8 +1955,57 @@ async function handleSlash(text) {
|
|
|
1538
1955
|
break;
|
|
1539
1956
|
}
|
|
1540
1957
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1958
|
+
/* /novo: projeto novo por ORDEM da pessoa. Sem ele, "crie um site de
|
|
1959
|
+
receitas" pode cair dentro de "site-receitas-velho" porque o nome
|
|
1960
|
+
parece (findSimilarProject) — e quem quer começar do zero não tem
|
|
1961
|
+
como dizer isso. Aqui a pasta nasce antes, com nome que não colide,
|
|
1962
|
+
e o pedido roda preso nela. */
|
|
1963
|
+
case 'novo': case 'new': {
|
|
1964
|
+
if (!rest) { console.log(c.err('Uso: /novo <o que você quer criar>') + c.dim(' ex.: /novo site da padaria')); break; }
|
|
1965
|
+
const nomes = cfg.listProjects();
|
|
1966
|
+
const sp = new Spinner('escolhendo o nome da pasta').start();
|
|
1967
|
+
const sugerido = await nomeProjeto.escolher({ pedido: rest, nomes, perguntar: perguntarNome }).catch(() => null);
|
|
1968
|
+
sp.stop();
|
|
1969
|
+
const nomeNovo = extra.nomeDoNovo({
|
|
1970
|
+
descricao: rest, nomes, sugerido, derivar: deriveProjectName,
|
|
1971
|
+
slugify: cfg.slugify, semColisao: nomeProjeto.semColisao,
|
|
1972
|
+
});
|
|
1973
|
+
const dir = cfg.createProjectDir(nomeNovo);
|
|
1974
|
+
entrarNoProjeto(dir, 'projeto');
|
|
1975
|
+
// Conversa nova junto: o que se falou do projeto anterior puxaria o
|
|
1976
|
+
// agente de volta para ele.
|
|
1977
|
+
state.history = []; state.memoria = memoria.nova(); state.gravador.zerar();
|
|
1978
|
+
console.log(c.ok(` Projeto novo: ${state.currentProject}`) + c.muted(` (${shortPath(dir)})`));
|
|
1979
|
+
refreshPrompt();
|
|
1980
|
+
const modoAntes = state.mode;
|
|
1981
|
+
state.mode = 'primocode';
|
|
1982
|
+
state.projetoFixo = true;
|
|
1983
|
+
try { await sendPrompt(extra.pedidoDoNovo(rest, state.currentProject)); }
|
|
1984
|
+
finally { state.mode = modoAntes; state.projetoFixo = false; }
|
|
1985
|
+
break;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
/* /open sem nada (e /abrir) acha sozinho o que abrir: o servidor do
|
|
1989
|
+
projeto de pé, ou a index.html. Antes pedia o caminho, e quem não
|
|
1990
|
+
sabe que a página se chama index.html ficava parado aqui. */
|
|
1991
|
+
case 'abrir': case 'open': {
|
|
1992
|
+
if (!rest) {
|
|
1993
|
+
const pastaAtual = projectDir();
|
|
1994
|
+
if (path.resolve(pastaAtual) === path.resolve(cfg.ROOT_DIR)) {
|
|
1995
|
+
console.log(c.muted(' Nenhum projeto aberto.') + c.dim(' /projetos para escolher um'));
|
|
1996
|
+
break;
|
|
1997
|
+
}
|
|
1998
|
+
const alvo = await extra.acharAlvoAbrir(pastaAtual);
|
|
1999
|
+
if (!alvo) {
|
|
2000
|
+
console.log(c.muted(` Não achei página nem servidor em ${shortPath(pastaAtual)}.`));
|
|
2001
|
+
console.log(c.dim(' Peça "roda o projeto" ao Primo, ou /open <arquivo>.'));
|
|
2002
|
+
break;
|
|
2003
|
+
}
|
|
2004
|
+
const r = await tools.openInBrowser(alvo.url || alvo.caminho);
|
|
2005
|
+
if (r && r.ok === false) { console.log(c.err(` Não consegui abrir: ${r.error}`)); break; }
|
|
2006
|
+
console.log(c.ok(` Aberto: ${alvo.url || shortPath(alvo.caminho)}`) + c.dim(` (${alvo.porque})`));
|
|
2007
|
+
break;
|
|
2008
|
+
}
|
|
1543
2009
|
const target = /^https?:\/\//i.test(rest) ? rest : path.resolve(projectDir(), rest);
|
|
1544
2010
|
await tools.openInBrowser(target);
|
|
1545
2011
|
console.log(c.ok(`Aberto: ${rest}`));
|
|
@@ -1554,32 +2020,47 @@ async function handleSlash(text) {
|
|
|
1554
2020
|
break;
|
|
1555
2021
|
}
|
|
1556
2022
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
2023
|
+
/* A galeria: todos lado a lado, número e nome em cima. Escolhe por
|
|
2024
|
+
nome ou número — na hora, se há teclado; senão pelo /mascot <n>.
|
|
2025
|
+
Num pipe não pergunta: a "resposta" seria o próximo comando. */
|
|
2026
|
+
case 'mascot': case 'mascote': {
|
|
2027
|
+
const reac = rest.match(/^(reac(?:oes|ões)?|rea[cç][aã]o|reactions?)\s*(on|off|liga[r]?|desliga[r]?|sim|n[aã]o)?$/i);
|
|
2028
|
+
if (reac) {
|
|
2029
|
+
const quer = reac[2] ? /^(on|liga|ligar|sim)$/i.test(reac[2]) : state.config.reacoes === false;
|
|
2030
|
+
state.config.reacoes = quer;
|
|
2031
|
+
cfg.saveConfig({ reacoes: quer });
|
|
2032
|
+
console.log(c.ok(` Reações do mascote: ${quer ? 'ligadas' : 'desligadas'}`)
|
|
2033
|
+
+ (quer ? ' ' + ui.reacao(state.config.mascot, 'feliz').trim() : ''));
|
|
2034
|
+
break;
|
|
2035
|
+
}
|
|
2036
|
+
let pedido = rest;
|
|
2037
|
+
if (!pedido) {
|
|
1561
2038
|
console.log('');
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
2039
|
+
console.log(ui.galeriaMascotes(state.config.mascot || 'primo', process.stdout.columns || 80));
|
|
2040
|
+
if (!process.stdin.isTTY || state.semTerminal) {
|
|
2041
|
+
console.log(c.muted(' Escolha: /mascot <nome ou número>') + c.dim(' · /mascot reacoes off desliga o rostinho'));
|
|
2042
|
+
break;
|
|
1566
2043
|
}
|
|
1567
|
-
|
|
1568
|
-
break;
|
|
2044
|
+
pedido = String(await ask(` ${c.brand('?')} Nome ou número ${c.dim('(Enter mantém)')}: `)).trim();
|
|
2045
|
+
if (!pedido) { console.log(c.muted(' Nada mudou.')); break; }
|
|
1569
2046
|
}
|
|
1570
|
-
|
|
2047
|
+
const m = ui.resolverMascote(pedido);
|
|
2048
|
+
if (!m) { console.log(c.err(` Não conheço "${pedido}".`) + c.dim(` São: ${ui.MASCOTES.join(', ')}`)); break; }
|
|
1571
2049
|
state.config.mascot = m;
|
|
1572
2050
|
cfg.saveConfig({ mascot: m });
|
|
2051
|
+
const info = ui.mascoteInfo(m);
|
|
1573
2052
|
console.log('');
|
|
1574
|
-
ui.mascot(m)
|
|
1575
|
-
console.log(c.
|
|
2053
|
+
const arte = ui.mascot(m);
|
|
2054
|
+
arte.forEach((l, i) => console.log(' ' + l + (i === 0 ? ' ' + c.bold(c.white(info.nome))
|
|
2055
|
+
: i === 1 ? ' ' + c.dim(`"${info.frase}"`) : '')));
|
|
2056
|
+
console.log(c.ok(`\n Mascote: ${m}`));
|
|
1576
2057
|
break;
|
|
1577
2058
|
}
|
|
1578
2059
|
|
|
1579
2060
|
case 'config': {
|
|
1580
2061
|
console.log('');
|
|
1581
2062
|
console.log(` ${c.muted('nome')} ${c.white(state.config.userName || '(nenhum)')}`);
|
|
1582
|
-
console.log(` ${c.muted('mascote')} ${c.white(state.config.mascot || 'primo')}`);
|
|
2063
|
+
console.log(` ${c.muted('mascote')} ${c.white(state.config.mascot || 'primo')}${c.dim(state.config.reacoes === false ? ' · sem reações' : ' · com reações')}`);
|
|
1583
2064
|
console.log(` ${c.muted('esforço')} ${c.white(state.config.effort)}${c.dim(' · /effort muda')}`);
|
|
1584
2065
|
console.log(` ${c.muted('modelo')} ${c.white(state.config.model)}${c.dim(' (vem do esforço)')}`);
|
|
1585
2066
|
console.log(` ${c.muted('modo')} ${c.white(state.mode)}`);
|
|
@@ -1618,6 +2099,78 @@ async function handleSlash(text) {
|
|
|
1618
2099
|
state.gravador.zerar(); console.log(c.ok('Conversa limpa.')); break;
|
|
1619
2100
|
case 'cls': process.stdout.write('\x1Bc'); break;
|
|
1620
2101
|
|
|
2102
|
+
/* O /status olha só o servidor. O /doctor olha tudo o que costuma
|
|
2103
|
+
ser o motivo de "não funciona" — e cada ✗ diz como consertar. */
|
|
2104
|
+
case 'doctor': case 'medico': case 'médico': {
|
|
2105
|
+
const sp = new Spinner('conferindo').start();
|
|
2106
|
+
const linhas = await extra.diagnosticar({
|
|
2107
|
+
saude: () => api.getHealth(state.config.server, state.config.token),
|
|
2108
|
+
provedor: () => provedorLocal.atual(),
|
|
2109
|
+
conferirChave: (id, chave) => provedorLocal.conferir(id, chave),
|
|
2110
|
+
cliInstalado: (id) => require('../lib/local/motor-cli').instalado(id),
|
|
2111
|
+
acharChrome: () => require('../lib/browser').findChrome(),
|
|
2112
|
+
raiz: cfg.ROOT_DIR,
|
|
2113
|
+
arquivoConfig: cfg.CONFIG_FILE,
|
|
2114
|
+
});
|
|
2115
|
+
sp.stop();
|
|
2116
|
+
console.log('');
|
|
2117
|
+
for (const l of linhas) {
|
|
2118
|
+
const marca = l.ok === true ? c.ok('✓') : l.ok === false ? c.err('✗') : c.dim('·');
|
|
2119
|
+
console.log(` ${marca} ${ui.padRight(c.white(l.item), 18)}${ui.truncate(c.muted(l.detalhe || ''), Math.max(20, (process.stdout.columns || 80) - 23))}`);
|
|
2120
|
+
if (l.conserto) console.log(` ${c.dim('→ ' + l.conserto)}`);
|
|
2121
|
+
}
|
|
2122
|
+
const ruins = linhas.filter((l) => l.ok === false).length;
|
|
2123
|
+
console.log(ruins ? c.warn(`\n ${ruins} ${ruins === 1 ? 'coisa para arrumar' : 'coisas para arrumar'}.`) : c.ok('\n Tudo certo.'));
|
|
2124
|
+
break;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
case 'custo': case 'cost': case 'tokens': {
|
|
2128
|
+
const { linhas, total } = extra.resumoCusto(contadorDaSessao);
|
|
2129
|
+
if (!linhas.length) { console.log(c.muted(' Nenhum pedido ao modelo nesta sessão ainda.')); break; }
|
|
2130
|
+
console.log('');
|
|
2131
|
+
for (const l of linhas) {
|
|
2132
|
+
console.log(` ${c.white(ui.padRight(l.provedor, 22))}${c.muted(ui.padRight(l.pedidos + (l.pedidos === 1 ? ' pedido' : ' pedidos'), 12))}`
|
|
2133
|
+
+ `${c.dim('enviou ')}${c.white(extra.fmtNumero(l.enviados))}${c.dim(' · recebeu ')}${c.white(extra.fmtNumero(l.recebidos))}${c.dim(' tokens' + (l.exato ? '' : ' (estimado)'))}`);
|
|
2134
|
+
}
|
|
2135
|
+
if (linhas.length > 1) {
|
|
2136
|
+
console.log(` ${c.white(ui.padRight('Total', 22))}${c.muted(ui.padRight(total.pedidos + ' pedidos', 12))}`
|
|
2137
|
+
+ `${c.dim('enviou ')}${c.white(extra.fmtNumero(total.enviados))}${c.dim(' · recebeu ')}${c.white(extra.fmtNumero(total.recebidos))}`);
|
|
2138
|
+
}
|
|
2139
|
+
if (linhas.some((l) => !l.exato)) console.log(c.dim('\n Estimado = caracteres ÷ 4. Conta cada pedido inteiro, com a conversa que vai junto.'));
|
|
2140
|
+
break;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
case 'copiar': case 'copy': {
|
|
2144
|
+
const r = extra.copiar(state.lastResponse);
|
|
2145
|
+
if (r.ok) console.log(c.ok(' Copiado.') + c.dim(` ${state.lastResponse.length} caracteres · é só colar`));
|
|
2146
|
+
else console.log(c.muted(` Não copiei: ${r.error}.`));
|
|
2147
|
+
break;
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
case 'historico': case 'histórico': {
|
|
2151
|
+
const achados = extra.buscarHistorico(cfg.HISTORY_FILE, rest, 20);
|
|
2152
|
+
if (!achados.length) {
|
|
2153
|
+
console.log(c.muted(rest ? ` Nada com "${rest}" nos seus pedidos.` : ' Nenhum pedido guardado ainda.'));
|
|
2154
|
+
console.log(c.dim(' /chats mostra as conversas inteiras da sua conta.'));
|
|
2155
|
+
break;
|
|
2156
|
+
}
|
|
2157
|
+
console.log('');
|
|
2158
|
+
const largura = Math.max(30, (process.stdout.columns || 80) - 34);
|
|
2159
|
+
for (const e of achados) {
|
|
2160
|
+
const quando = new Date(e.ts).toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' });
|
|
2161
|
+
console.log(` ${c.dim(quando)} ${c.muted(ui.padRight(ui.truncate(e.project || e.mode || '', 14), 14))} `
|
|
2162
|
+
+ ui.truncate(c.white(String(e.prompt).replace(/\s+/g, ' ')), largura));
|
|
2163
|
+
}
|
|
2164
|
+
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`));
|
|
2165
|
+
break;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
case 'atalhos': case 'shortcuts': case 'teclas': {
|
|
2169
|
+
console.log('');
|
|
2170
|
+
for (const [tecla, faz] of extra.ATALHOS) console.log(` ${c.brand(ui.padRight(tecla, 20))}${c.muted(faz)}`);
|
|
2171
|
+
break;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
1621
2174
|
case 'status': {
|
|
1622
2175
|
const sp = new Spinner('verificando').start();
|
|
1623
2176
|
try {
|
|
@@ -1788,6 +2341,8 @@ async function handleSlash(text) {
|
|
|
1788
2341
|
return resposta;
|
|
1789
2342
|
}
|
|
1790
2343
|
const plan = prepararPasta(texto);
|
|
2344
|
+
// Pasta pendente (projeto que ainda vai nascer) é o ~/primocode: sem foto.
|
|
2345
|
+
checkpoint.iniciar(texto, plan.pending ? null : plan.dir);
|
|
1791
2346
|
const resultado = await runAct(texto, {
|
|
1792
2347
|
instrucao: vozModo.COMO_FALAR,
|
|
1793
2348
|
server: state.config.server,
|
|
@@ -1800,7 +2355,9 @@ async function handleSlash(text) {
|
|
|
1800
2355
|
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
1801
2356
|
autorizar: autorizarPorVoz,
|
|
1802
2357
|
controle: state.controle,
|
|
2358
|
+
permissoes: state.config.permissoes,
|
|
1803
2359
|
});
|
|
2360
|
+
checkpoint.encerrar();
|
|
1804
2361
|
const resposta = (resultado.assistantMessage && resultado.assistantMessage.content) || '';
|
|
1805
2362
|
// A conversa é a MESMA do terminal: sair do modo voz e
|
|
1806
2363
|
// continuar digitando não pode começar do zero.
|
|
@@ -1910,18 +2467,7 @@ async function handleSlash(text) {
|
|
|
1910
2467
|
}
|
|
1911
2468
|
const sp = new Spinner('abrindo a janela').start();
|
|
1912
2469
|
try {
|
|
1913
|
-
janelaAberta = await appLib.abrir(
|
|
1914
|
-
versao: VERSION,
|
|
1915
|
-
enviar: (t) => sendPrompt(t),
|
|
1916
|
-
barra: (t) => handleSlash(t),
|
|
1917
|
-
interromper,
|
|
1918
|
-
estado: () => ({
|
|
1919
|
-
pasta: state.workDir ? shortPath(state.workDir) : null,
|
|
1920
|
-
projeto: state.currentProject,
|
|
1921
|
-
modo: state.mode,
|
|
1922
|
-
esforco: state.config.effort,
|
|
1923
|
-
}),
|
|
1924
|
-
});
|
|
2470
|
+
janelaAberta = await appLib.abrir(opcoesDaJanela());
|
|
1925
2471
|
sp.stop();
|
|
1926
2472
|
const j = janelaAberta.janela || {};
|
|
1927
2473
|
console.log(c.ok(' Janela aberta.')
|
|
@@ -1945,6 +2491,37 @@ async function handleSlash(text) {
|
|
|
1945
2491
|
break;
|
|
1946
2492
|
}
|
|
1947
2493
|
|
|
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. */
|
|
2499
|
+
case 'ide': {
|
|
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é)'));
|
|
2503
|
+
break;
|
|
2504
|
+
}
|
|
2505
|
+
const sp = new Spinner('abrindo a janela').start();
|
|
2506
|
+
try {
|
|
2507
|
+
janelaAberta = await appLib.abrir(opcoesDaJanela({ abrirEm: 'ide' }));
|
|
2508
|
+
sp.stop();
|
|
2509
|
+
const j = janelaAberta.janela || {};
|
|
2510
|
+
console.log(c.ok(' Janela aberta, com a prévia do site ao lado.')
|
|
2511
|
+
+ c.muted(j.modo === 'janela' ? ` (${j.navegador})`
|
|
2512
|
+
: j.modo === 'aba' ? ' (aba do navegador padrão)' : ''));
|
|
2513
|
+
console.log(c.dim(` ${janelaAberta.urlPainel}`));
|
|
2514
|
+
if (j.modo === 'nenhum') {
|
|
2515
|
+
console.log(c.warn(' Não consegui abrir sozinho — copie o endereço acima no navegador.'));
|
|
2516
|
+
}
|
|
2517
|
+
console.log(c.muted(` Pasta: ${shortPath(projectDir())} · o terminal continua funcionando, os dois mostram a mesma coisa.`));
|
|
2518
|
+
} catch (e) {
|
|
2519
|
+
sp.stop();
|
|
2520
|
+
console.log(c.err(` Não subiu: ${e.message}`));
|
|
2521
|
+
}
|
|
2522
|
+
break;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
1948
2525
|
/* A conta do Conecta Primo, sem navegador: e-mail e senha aqui
|
|
1949
2526
|
mesmo. O login inteiro mora em lib/conta.js; aqui é a tela dele. */
|
|
1950
2527
|
case 'login': {
|
|
@@ -2002,18 +2579,8 @@ async function handleSlash(text) {
|
|
|
2002
2579
|
const n = Number(rest);
|
|
2003
2580
|
const alvo = l.conversas[n - 1];
|
|
2004
2581
|
if (!alvo) { console.log(c.err(` Não achei a conversa ${rest}.`)); break; }
|
|
2005
|
-
const
|
|
2006
|
-
const conv = await nuvem.lerConversa(alvo.id);
|
|
2007
|
-
sp2.stop();
|
|
2582
|
+
const conv = await abrirConversa(alvo);
|
|
2008
2583
|
if (!conv.ok) { console.log(c.err(` ${conv.error}`)); break; }
|
|
2009
|
-
// A memória entra ZERADA: o resumo antigo não veio junto, e um
|
|
2010
|
-
// resumo de outra conversa colado aqui faria o modelo responder
|
|
2011
|
-
// sobre coisa que não foi dita nesta.
|
|
2012
|
-
state.history = conv.mensagens.map((m) => ({
|
|
2013
|
-
role: m.papel, content: m.texto,
|
|
2014
|
-
}));
|
|
2015
|
-
state.memoria = memoria.nova();
|
|
2016
|
-
state.gravador.retomar(alvo.id, alvo.titulo, conv.mensagens.length);
|
|
2017
2584
|
console.log(c.ok(` ${alvo.titulo}`)
|
|
2018
2585
|
+ c.muted(` · ${conv.mensagens.length} mensagens carregadas`));
|
|
2019
2586
|
console.log(c.dim(' Pode continuar daqui — o que você disser volta pro app e pro celular.'));
|
|
@@ -2084,6 +2651,73 @@ async function handleSlash(text) {
|
|
|
2084
2651
|
* Eles não somem calados. Quem tem o dedo viciado em `/claude` e
|
|
2085
2652
|
* bate num "comando não existe" conclui que a atualização quebrou
|
|
2086
2653
|
* alguma coisa — e não vai procurar no /help, vai desinstalar. */
|
|
2654
|
+
case 'api': {
|
|
2655
|
+
await comandoApi(rest);
|
|
2656
|
+
break;
|
|
2657
|
+
}
|
|
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
|
+
|
|
2698
|
+
/* Os comandos da pessoa: lista, e explica como fazer o dela. */
|
|
2699
|
+
case 'comandos': {
|
|
2700
|
+
const novo = rest.match(/^(novo|new|criar)\s+(\S+)$/i);
|
|
2701
|
+
if (novo) {
|
|
2702
|
+
const r = comandosLib.criar(novo[2]);
|
|
2703
|
+
if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
|
|
2704
|
+
cacheComandos.em = 0;
|
|
2705
|
+
console.log(c.ok(` Criado: /${path.basename(r.arquivo, '.md')}`) + c.muted(' abra e escreva o pedido:'));
|
|
2706
|
+
console.log(' ' + c.white(r.arquivo));
|
|
2707
|
+
break;
|
|
2708
|
+
}
|
|
2709
|
+
const lista = comandosDaPessoa();
|
|
2710
|
+
console.log(c.white('\n Seus comandos'));
|
|
2711
|
+
for (const x of lista) {
|
|
2712
|
+
console.log(` ${c.brand(ui.padRight('/' + x.nome, 14))}${c.dim(ui.padRight(x.origem, 11))}${c.muted(x.descricao)}`);
|
|
2713
|
+
}
|
|
2714
|
+
console.log(c.dim('\n Um arquivo .md vira um comando: revisar.md é /revisar.'));
|
|
2715
|
+
console.log(c.dim(' ~/.primocode/comandos/ vale em todo projeto · <projeto>/.primocode/comandos/ só naquele.'));
|
|
2716
|
+
console.log(c.dim(' $ARGUMENTOS no texto recebe o que você digitar depois: /revisar o login.'));
|
|
2717
|
+
console.log(c.dim(' /comandos novo <nome> cria um para você editar.'));
|
|
2718
|
+
break;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2087
2721
|
case 'claude': case 'gpt': case 'codex': case 'key': case 'keys': {
|
|
2088
2722
|
console.log(c.muted(' Não existe mais provedor para configurar.'));
|
|
2089
2723
|
console.log(c.muted(' A API já vem com o PrimoCode: entrou na conta, pode usar.'));
|
|
@@ -2095,7 +2729,18 @@ async function handleSlash(text) {
|
|
|
2095
2729
|
// "/" sozinho abre o menu; um prefixo único autopreenche e executa
|
|
2096
2730
|
// ("/eff" → /effort); prefixo ambíguo mostra as opções.
|
|
2097
2731
|
if (!cmd) { mostrarMenuSlash(); break; }
|
|
2098
|
-
|
|
2732
|
+
// Chegou aqui, não é embutido: o switch já atendeu todos eles.
|
|
2733
|
+
// Por isso um /help.md da pessoa nunca troca o /help de verdade.
|
|
2734
|
+
const proprio = comandosDaPessoa().find((x) => x.nome === nome);
|
|
2735
|
+
if (proprio) {
|
|
2736
|
+
console.log(c.dim(` ⎿ /${proprio.nome} · ${proprio.origem}`));
|
|
2737
|
+
// O MESMO caminho de um pedido digitado: modo, pasta, plano,
|
|
2738
|
+
// tudo. Um atalho que pulasse o sendPrompt pularia junto o
|
|
2739
|
+
// portão do plano e a escolha da pasta.
|
|
2740
|
+
await sendPrompt(comandosLib.expandir(proprio, rest));
|
|
2741
|
+
break;
|
|
2742
|
+
}
|
|
2743
|
+
const casam = comandosDoMenu().map(([nome]) => nome.slice(1)).filter((n) => n.startsWith(cmd.toLowerCase()));
|
|
2099
2744
|
if (casam.length === 1) {
|
|
2100
2745
|
console.log(c.dim(` → /${casam[0]}`));
|
|
2101
2746
|
await handleSlash('/' + casam[0] + (rest ? ' ' + rest : ''));
|
|
@@ -2104,7 +2749,7 @@ async function handleSlash(text) {
|
|
|
2104
2749
|
if (casam.length > 1) {
|
|
2105
2750
|
console.log(c.muted(` /${cmd}… pode ser:`));
|
|
2106
2751
|
for (const n of casam) {
|
|
2107
|
-
const [, desc] =
|
|
2752
|
+
const [, desc] = comandosDoMenu().find(([nome]) => nome === '/' + n);
|
|
2108
2753
|
console.log(` ${c.brand(ui.padRight('/' + n, 12))}${c.dim(desc)}`);
|
|
2109
2754
|
}
|
|
2110
2755
|
break;
|
|
@@ -2141,6 +2786,9 @@ function conferirServidor() {
|
|
|
2141
2786
|
jaConferiu = true;
|
|
2142
2787
|
const minimo = (pkg.primocode && pkg.primocode.servidorMinimo) || '';
|
|
2143
2788
|
if (!minimo) return;
|
|
2789
|
+
// Com a chave própria (/api), quem pensa não é o servidor: a versão dele
|
|
2790
|
+
// não muda nada no que a pessoa pede.
|
|
2791
|
+
if (provedorLocal.atual()) return;
|
|
2144
2792
|
const servidor = state.config.server;
|
|
2145
2793
|
Promise.resolve()
|
|
2146
2794
|
.then(() => api.getHealth(servidor, state.config.token))
|
|
@@ -2174,7 +2822,11 @@ async function main() {
|
|
|
2174
2822
|
// mesmo que uma refatoração.
|
|
2175
2823
|
state.mode = ['primocode', 'chat', 'auto'].includes(state.config.mode) ? state.config.mode : 'auto';
|
|
2176
2824
|
|
|
2177
|
-
|
|
2825
|
+
// `-c` / `--continue` pode vir em qualquer posição e sai da lista: o
|
|
2826
|
+
// resto (one-shot, -f) segue sendo lido do jeito de sempre.
|
|
2827
|
+
const todos = process.argv.slice(2);
|
|
2828
|
+
const continuar = todos.some((a) => a === '-c' || a === '--continue');
|
|
2829
|
+
const args = todos.filter((a) => a !== '-c' && a !== '--continue');
|
|
2178
2830
|
|
|
2179
2831
|
// Flags padrão de CLI — funcionam em qualquer máquina, úteis em scripts/CI.
|
|
2180
2832
|
if (args[0] === '--version' || args[0] === '-v' || args[0] === '-V') {
|
|
@@ -2212,18 +2864,7 @@ async function main() {
|
|
|
2212
2864
|
process.exit(r.ok ? 0 : 1);
|
|
2213
2865
|
}
|
|
2214
2866
|
|
|
2215
|
-
const janela = await appLib.abrir(
|
|
2216
|
-
versao: VERSION,
|
|
2217
|
-
enviar: (t) => sendPrompt(t),
|
|
2218
|
-
barra: (t) => handleSlash(t),
|
|
2219
|
-
interromper,
|
|
2220
|
-
estado: () => ({
|
|
2221
|
-
pasta: state.workDir ? shortPath(state.workDir) : null,
|
|
2222
|
-
projeto: state.currentProject,
|
|
2223
|
-
modo: state.mode,
|
|
2224
|
-
esforco: state.config.effort,
|
|
2225
|
-
}),
|
|
2226
|
-
});
|
|
2867
|
+
const janela = await appLib.abrir(opcoesDaJanela());
|
|
2227
2868
|
janelaAberta = janela;
|
|
2228
2869
|
console.log(c.brand(` PrimoCode`) + c.muted(` ${janela.url.split('?')[0]}`));
|
|
2229
2870
|
// Sem readline não há nada segurando o laço de eventos além do
|
|
@@ -2234,33 +2875,31 @@ async function main() {
|
|
|
2234
2875
|
return;
|
|
2235
2876
|
}
|
|
2236
2877
|
|
|
2878
|
+
/* `primo ide`: o terminal de sempre, com a janela (e a prévia) já aberta.
|
|
2879
|
+
Só a palavra SOZINHA: `primo ide para gatos` continua sendo um pedido
|
|
2880
|
+
ao agente (modo um-tiro), como sempre foi. */
|
|
2881
|
+
const abrirIde = args.length === 1 && args[0].toLowerCase() === 'ide';
|
|
2882
|
+
|
|
2237
2883
|
// Modo one-shot: primocode "crie um jogo de carro"
|
|
2238
|
-
if (args.length && !args[0].startsWith('-')) {
|
|
2884
|
+
if (args.length && !args[0].startsWith('-') && !abrirIde) {
|
|
2239
2885
|
const fileFlag = args.indexOf('-f');
|
|
2240
2886
|
const words = fileFlag >= 0 ? args.slice(0, fileFlag) : args;
|
|
2241
2887
|
const promptText = words.join(' ');
|
|
2242
|
-
let fullPrompt = promptText;
|
|
2243
2888
|
|
|
2244
2889
|
if (fileFlag >= 0 && args[fileFlag + 1]) {
|
|
2245
2890
|
try {
|
|
2246
2891
|
const fc = fs.readFileSync(path.resolve(args[fileFlag + 1]), 'utf-8');
|
|
2247
|
-
|
|
2892
|
+
state.attachedFiles.push({ path: args[fileFlag + 1], content: fc });
|
|
2248
2893
|
} catch { console.log(c.err(`Não consegui ler: ${args[fileFlag + 1]}`)); process.exit(1); }
|
|
2249
2894
|
}
|
|
2250
2895
|
|
|
2251
|
-
//
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
token: state.config.token,
|
|
2259
|
-
history: [],
|
|
2260
|
-
projectDir: plan.dir,
|
|
2261
|
-
projectName: plan.name || state.currentProject,
|
|
2262
|
-
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
2263
|
-
});
|
|
2896
|
+
// Com -c, o pedido continua a última conversa em vez de chegar sozinho.
|
|
2897
|
+
if (continuar) await continuarUltima();
|
|
2898
|
+
/* O MESMO caminho do pedido digitado (sendPrompt). Este modo tinha um
|
|
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. */
|
|
2901
|
+
state.semTerminal = true;
|
|
2902
|
+
await sendPrompt(promptText);
|
|
2264
2903
|
process.exit(0);
|
|
2265
2904
|
}
|
|
2266
2905
|
|
|
@@ -2272,7 +2911,7 @@ async function main() {
|
|
|
2272
2911
|
}));
|
|
2273
2912
|
rl = entrada.criar({
|
|
2274
2913
|
status: barraDeStatus,
|
|
2275
|
-
comandos:
|
|
2914
|
+
comandos: comandosDoMenu,
|
|
2276
2915
|
vazio: 'peça alguma coisa, ou / para os comandos',
|
|
2277
2916
|
});
|
|
2278
2917
|
|
|
@@ -2354,7 +2993,13 @@ async function main() {
|
|
|
2354
2993
|
try {
|
|
2355
2994
|
if (ehBash(text)) await rodarBash(text);
|
|
2356
2995
|
else if (isSlash(text)) await handleSlash(text);
|
|
2357
|
-
else
|
|
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
|
+
}
|
|
2358
3003
|
} catch (e) {
|
|
2359
3004
|
fala.problema(e);
|
|
2360
3005
|
}
|
|
@@ -2411,15 +3056,22 @@ async function main() {
|
|
|
2411
3056
|
// o primocode-server, que tem a chave do provedor.
|
|
2412
3057
|
state.liberado = !r || r.liberado !== false;
|
|
2413
3058
|
state.motivoBloqueio = r && r.motivo;
|
|
2414
|
-
}).catch(() => { state.liberado = true; })
|
|
2415
|
-
//
|
|
2416
|
-
//
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
3059
|
+
}).catch(() => { state.liberado = true; })
|
|
3060
|
+
// Depois da abertura, porque é nela que a pessoa entra na conta — e
|
|
3061
|
+
// sem conta não há conversa para achar.
|
|
3062
|
+
.then(() => (continuar ? continuarUltima().catch(() => false) : null))
|
|
3063
|
+
.then(() => {
|
|
3064
|
+
// O que sobrou de adiantado não era resposta de nada: vira comando,
|
|
3065
|
+
// na ordem em que foi digitado. Nada se perde, só esperou a vez.
|
|
3066
|
+
segurando = false;
|
|
3067
|
+
// Depois da abertura (e do login dela): a janela sem conta abriria
|
|
3068
|
+
// mandando pedidos que o servidor recusa.
|
|
3069
|
+
if (abrirIde) queue.push('/ide');
|
|
3070
|
+
while (adiantadas.length) queue.push(adiantadas.shift());
|
|
3071
|
+
if (queue.length) drain();
|
|
3072
|
+
else if (!closed) rl.prompt();
|
|
3073
|
+
conferirServidor();
|
|
3074
|
+
});
|
|
2423
3075
|
}
|
|
2424
3076
|
|
|
2425
3077
|
main().catch((e) => { console.error(c.err(e.message || e)); process.exit(1); });
|