primocode 8.42.1 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -37
- package/bin/primocode.js +599 -327
- package/lib/acervo.js +8 -1
- package/lib/act.js +205 -114
- package/lib/api.js +35 -4
- package/lib/boas-vindas.js +31 -192
- package/lib/catalogo.js +60 -60
- package/lib/config.js +1 -1
- package/lib/conta.js +3 -24
- package/lib/diff.js +161 -0
- package/lib/diretor.js +37 -47
- package/lib/entrada.js +545 -0
- package/lib/especialistas.js +2 -2
- package/lib/estudio/assets.js +343 -0
- package/lib/estudio/identidade.js +560 -0
- package/lib/estudio/index.js +565 -0
- package/lib/estudio/plano.js +327 -0
- package/lib/estudio/projeto.js +246 -0
- package/lib/estudio/remotion.js +606 -0
- package/lib/estudio/slides.js +190 -0
- package/lib/estudio/video.js +296 -0
- package/lib/fala.js +13 -1
- package/lib/marca.js +4 -1
- package/lib/marketplace.js +189 -0
- package/lib/permissao.js +137 -0
- package/lib/registro.js +229 -0
- package/lib/skills.js +329 -0
- package/lib/tools.js +51 -181
- package/lib/ui.js +7 -3
- package/lib/voz.js +239 -0
- package/package.json +12 -3
- package/skills/acessibilidade/SKILL.md +28 -0
- package/skills/api-que-nao-quebra/SKILL.md +27 -0
- package/skills/depurar-de-verdade/SKILL.md +29 -0
- package/skills/editar-meu-video/SKILL.md +59 -0
- package/skills/escrever-em-portugues/SKILL.md +28 -0
- package/skills/git-limpo/SKILL.md +28 -0
- package/skills/motion-de-primeira/SKILL.md +62 -0
- package/skills/pagina-que-converte/SKILL.md +28 -0
- package/skills/performance-web/SKILL.md +28 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
- package/skills/slides-que-prendem/SKILL.md +40 -0
- package/studio/edge.py +16 -66
- package/studio/narrar.py +274 -0
- package/studio/pessoa.py +167 -0
- package/voz/primo_voz/__init__.py +0 -0
- package/voz/primo_voz/__main__.py +325 -0
- package/voz/primo_voz/escuta.py +235 -0
- package/voz/primo_voz/orbe.py +216 -0
- package/lib/audio-livre.js +0 -177
- package/lib/chaves.js +0 -149
- package/lib/claude-engine.js +0 -631
- package/lib/codex-engine.js +0 -208
- package/lib/remotion.js +0 -663
- package/lib/studio.js +0 -960
- package/studio/PRIMOCODE.md +0 -58
- package/studio/README.md +0 -154
- package/studio/__pycache__/edge.cpython-311.pyc +0 -0
- package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
- package/studio/ferramentas/00-protocolo.md +0 -125
- package/studio/ferramentas/01-modelos.md +0 -270
- package/studio/ferramentas/README.md +0 -32
- package/studio/ferramentas/corte.md +0 -919
- package/studio/ferramentas/grade.md +0 -92
- package/studio/ferramentas/prisma.md +0 -107
- package/studio/ferramentas/prosa.md +0 -86
- package/studio/ferramentas/tela.md +0 -93
- package/studio/ferramentas/traco.md +0 -79
- package/studio/server.py +0 -2254
- package/studio/studio.log +0 -57
- package/studio/voz.py +0 -388
- package/studio/web/analise.html +0 -52
- package/studio/web/core.css +0 -1167
- package/studio/web/fontes/bebas.woff2 +0 -0
- package/studio/web/fontes/grotesk-700.woff2 +0 -0
- package/studio/web/fontes/inter-400.woff2 +0 -0
- package/studio/web/fontes/inter-700.woff2 +0 -0
- package/studio/web/fontes/jetbrains-400.woff2 +0 -0
- package/studio/web/fontes/playfair-700.woff2 +0 -0
- package/studio/web/fontes.css +0 -23
- package/studio/web/home.html +0 -217
- package/studio/web/js/analise.js +0 -510
- package/studio/web/js/audio.js +0 -607
- package/studio/web/js/editor.js +0 -1802
- package/studio/web/js/exportar.js +0 -222
- package/studio/web/js/ferramentas.js +0 -108
- package/studio/web/js/icones.js +0 -120
- package/studio/web/js/midia.js +0 -157
- package/studio/web/js/narracao.js +0 -325
- package/studio/web/js/player.js +0 -903
- package/studio/web/js/render-grade.js +0 -243
- package/studio/web/js/render-prosa.js +0 -104
- package/studio/web/js/render.js +0 -1852
- package/studio/web/js/sfx.js +0 -233
- package/studio/web/js/video.js +0 -464
- package/studio/web/js/voz-neural.js +0 -152
- package/studio/web/render.css +0 -1352
- package/studio/web/tf.html +0 -15
- package/studio/web/tool.html +0 -78
- package/studio/web/view.html +0 -27
package/bin/primocode.js
CHANGED
|
@@ -14,6 +14,7 @@ const os = require('os');
|
|
|
14
14
|
|
|
15
15
|
const pkg = require('../package.json');
|
|
16
16
|
const ui = require('../lib/ui');
|
|
17
|
+
const entrada = require('../lib/entrada');
|
|
17
18
|
const { c, welcomeBox, Spinner } = ui;
|
|
18
19
|
const { escolherModo } = require('../lib/modo.js');
|
|
19
20
|
const api = require('../lib/api');
|
|
@@ -21,14 +22,16 @@ const tools = require('../lib/tools');
|
|
|
21
22
|
const cfg = require('../lib/config');
|
|
22
23
|
const { runAct, interromper, ehTetoDeToken, ehTransitorio, esperaDoErro } = require('../lib/act');
|
|
23
24
|
const fala = require('../lib/fala');
|
|
24
|
-
const claudeEngine = require('../lib/claude-engine');
|
|
25
|
-
const codexEngine = require('../lib/codex-engine');
|
|
26
|
-
const chaves = require('../lib/chaves');
|
|
27
25
|
const boasVindas = require('../lib/boas-vindas');
|
|
28
26
|
const pasta = require('../lib/pasta');
|
|
29
27
|
const catalogo = require('../lib/catalogo');
|
|
30
28
|
const effort = require('../lib/effort');
|
|
31
29
|
const memoria = require('../lib/memoria-conversa');
|
|
30
|
+
const registro = require('../lib/registro');
|
|
31
|
+
const permissao = require('../lib/permissao');
|
|
32
|
+
const skills = require('../lib/skills');
|
|
33
|
+
const marketplace = require('../lib/marketplace');
|
|
34
|
+
const vozModo = require('../lib/voz');
|
|
32
35
|
const appLib = require('../lib/app');
|
|
33
36
|
const conta = require('../lib/conta');
|
|
34
37
|
const nuvem = require('../lib/nuvem');
|
|
@@ -71,10 +74,14 @@ const state = {
|
|
|
71
74
|
liberado: true,
|
|
72
75
|
motivoBloqueio: null,
|
|
73
76
|
lastResponse: '',
|
|
77
|
+
trabalhando: false, // o agente está no meio de um pedido (a barra muda)
|
|
78
|
+
/* A autorização para mexer no computador. Vale pela SESSÃO e morre com
|
|
79
|
+
ela: não há arquivo de "sempre permitir" de propósito — autorização
|
|
80
|
+
permanente é exatamente a que ninguém lembra de ter dado. */
|
|
81
|
+
controle: permissao.novo(),
|
|
74
82
|
currentProject: null,
|
|
75
83
|
workDir: null, // pasta onde o agente trabalha nesta sessão
|
|
76
84
|
workDirOrigem: null, // 'padrao' | '/dir' | 'detectada' | 'projeto' — para o /dir explicar
|
|
77
|
-
engine: 'server', // 'server' (OpenRouter/Groq) | 'claude-code' (/claude) | 'codex' (/codex)
|
|
78
85
|
};
|
|
79
86
|
|
|
80
87
|
// A janela do /rc, quando aberta. Uma por sessão: o segundo /rc traz a que
|
|
@@ -96,9 +103,6 @@ ${c.bold('Comandos')}
|
|
|
96
103
|
${c.brand('/run')} <comando> executa no shell do projeto
|
|
97
104
|
${c.brand('/open')} <arq|url> abre no navegador
|
|
98
105
|
${c.brand('/rc')} abre a janela do PrimoCode (chat, sem terminal)
|
|
99
|
-
${c.brand('/claude')} usa o Claude Code da SUA conta como motor (Opus)
|
|
100
|
-
${c.brand('/gpt')} usa o GPT (Codex) da SUA conta como motor
|
|
101
|
-
${c.brand('/key')} <provedor> sua própria chave de API (Claude, GPT)
|
|
102
106
|
${c.brand('/connect')} entra na sua conta · ${c.brand('/account')} mostra o plano
|
|
103
107
|
${c.brand('/chats')} [n] suas conversas — as mesmas do app e do celular
|
|
104
108
|
${c.brand('/logout')} desconecta este computador da conta
|
|
@@ -143,14 +147,16 @@ const SLASH_COMANDOS = [
|
|
|
143
147
|
['/run', 'roda um comando'],
|
|
144
148
|
['/open', 'abre no navegador'],
|
|
145
149
|
['/rc', 'abre a janela do PrimoCode'],
|
|
146
|
-
['/claude', 'usa o Claude Code da sua conta como motor'],
|
|
147
|
-
['/gpt', 'usa o GPT (Codex) da sua conta como motor'],
|
|
148
|
-
['/key', 'sua própria chave de API (Claude, Codex)'],
|
|
149
150
|
['/connect', 'entra na sua conta Conecta Primo'],
|
|
150
151
|
['/account', 'sua conta, plano e computadores conectados'],
|
|
151
152
|
['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
|
|
152
153
|
['/logout', 'desconecta este computador da conta'],
|
|
154
|
+
['/voice', 'o Primo Code em modo voz: você fala, ele faz'],
|
|
155
|
+
['/skills', 'o que ele sabe fazer bem — ligue e desligue'],
|
|
156
|
+
['/marketplace', 'a tela das skills: navegue, ligue, crie'],
|
|
157
|
+
['/install-skill', 'instala uma skill de um arquivo'],
|
|
153
158
|
['/status', 'checa o servidor'],
|
|
159
|
+
['/connectors', 'quem está atendendo os seus pedidos'],
|
|
154
160
|
['/name', 'como ele te chama'],
|
|
155
161
|
['/mascot', 'primo | fox | cat'],
|
|
156
162
|
['/config', 'suas preferências'],
|
|
@@ -161,15 +167,6 @@ const SLASH_COMANDOS = [
|
|
|
161
167
|
['/exit', 'sair'],
|
|
162
168
|
];
|
|
163
169
|
|
|
164
|
-
// O completer do readline: Tab completa o que der. Só age na palavra do
|
|
165
|
-
// comando — depois do primeiro espaço o argumento é seu, não dele.
|
|
166
|
-
function completarSlash(line) {
|
|
167
|
-
if (!line.startsWith('/') || /\s/.test(line)) return [[], line];
|
|
168
|
-
const baixa = line.toLowerCase();
|
|
169
|
-
const acertos = SLASH_COMANDOS.map(([nome]) => nome).filter((nome) => nome.startsWith(baixa));
|
|
170
|
-
return [acertos, line];
|
|
171
|
-
}
|
|
172
|
-
|
|
173
170
|
// ── /effort: o slider de esforço (estilo Claude Code) ──────────────────
|
|
174
171
|
// Régua com os níveis, o marcador ▲ embaixo do escolhido, "Faster" à
|
|
175
172
|
// esquerda e "Smarter" à direita. ←/→ move, Enter confirma, Esc cancela.
|
|
@@ -221,72 +218,13 @@ function sliderEffort(atual) {
|
|
|
221
218
|
if (anima) { clearInterval(anima); anima = null; }
|
|
222
219
|
process.stdin.removeListener('keypress', aoTeclar);
|
|
223
220
|
sliderAtivo = false;
|
|
221
|
+
if (rl && rl.retomar) rl.retomar();
|
|
224
222
|
resolve(valor);
|
|
225
223
|
};
|
|
226
224
|
sliderAtivo = true;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
/* ── A LISTA COM AS SETINHAS ─────────────────────────────────────────────
|
|
232
|
-
*
|
|
233
|
-
* "O usuário seleciona usando as setinhas, igual você."
|
|
234
|
-
*
|
|
235
|
-
* ↑↓ anda, Enter confirma, Esc fica com o primeiro item. O número também
|
|
236
|
-
* funciona — quem já sabe qual quer não precisa navegar até lá.
|
|
237
|
-
*
|
|
238
|
-
* Devolve `undefined` quando não há terminal de verdade: aí quem chamou cai
|
|
239
|
-
* na lista numerada, que funciona em pipe e em CI. `null` é outra coisa —
|
|
240
|
-
* é Esc, que É uma resposta ("tanto faz, a primeira").
|
|
241
|
-
*/
|
|
242
|
-
function menuSetas({ titulo, ajuda, lista }) {
|
|
243
|
-
return new Promise((resolve) => {
|
|
244
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) return resolve(undefined);
|
|
245
|
-
let idx = 0;
|
|
246
|
-
const linhas = () => {
|
|
247
|
-
const fora = [''];
|
|
248
|
-
fora.push(c.bold(c.brand(' ' + titulo)));
|
|
249
|
-
if (ajuda) fora.push(c.muted(' ' + ajuda));
|
|
250
|
-
fora.push('');
|
|
251
|
-
lista.forEach((e, i) => {
|
|
252
|
-
const aqui = i === idx;
|
|
253
|
-
fora.push((aqui ? c.brand(' ❯ ') : ' ') + (aqui ? c.bold(c.white(e.titulo)) : c.muted(e.titulo)));
|
|
254
|
-
fora.push(' ' + c.dim(e.detalhe || ''));
|
|
255
|
-
});
|
|
256
|
-
fora.push('');
|
|
257
|
-
return fora;
|
|
258
|
-
};
|
|
259
|
-
const ALTURA = linhas().length;
|
|
260
|
-
|
|
261
|
-
const pintar = (primeira) => {
|
|
262
|
-
if (!primeira) process.stdout.write(`\x1b[${ALTURA}A\r`);
|
|
263
|
-
process.stdout.write(linhas().map((l) => '\x1b[2K' + l).join('\n') + '\n');
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
const encerrar = (valor) => {
|
|
267
|
-
process.stdin.removeListener('keypress', aoTeclar);
|
|
268
|
-
sliderAtivo = false;
|
|
269
|
-
try { process.stdout.write('\x1b[?25h'); } catch {}
|
|
270
|
-
resolve(valor);
|
|
271
|
-
};
|
|
272
|
-
const aoTeclar = (ch, key) => {
|
|
273
|
-
if (!key) return;
|
|
274
|
-
if (key.name === 'down' || key.name === 'j' || (key.name === 'tab' && !key.shift)) {
|
|
275
|
-
idx = (idx + 1) % lista.length; return pintar(false);
|
|
276
|
-
}
|
|
277
|
-
if (key.name === 'up' || key.name === 'k' || (key.name === 'tab' && key.shift)) {
|
|
278
|
-
idx = (idx - 1 + lista.length) % lista.length; return pintar(false);
|
|
279
|
-
}
|
|
280
|
-
if (/^[1-9]$/.test(ch || '') && Number(ch) <= lista.length) {
|
|
281
|
-
idx = Number(ch) - 1; return pintar(false);
|
|
282
|
-
}
|
|
283
|
-
if (key.name === 'return' || key.name === 'enter') return encerrar(lista[idx].id);
|
|
284
|
-
if (key.name === 'escape' || (key.ctrl && key.name === 'c')) return encerrar(null);
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
sliderAtivo = true; // enquanto está no ar, o resto do teclado se cala
|
|
288
|
-
try { process.stdout.write('\x1b[?25l'); } catch {}
|
|
289
|
-
pintar(true);
|
|
225
|
+
// A caixa sai da frente e larga o teclado: dois desenhos no mesmo
|
|
226
|
+
// lugar, e as setas moveriam os dois ao mesmo tempo.
|
|
227
|
+
if (rl && rl.suspender) rl.suspender();
|
|
290
228
|
process.stdin.on('keypress', aoTeclar);
|
|
291
229
|
});
|
|
292
230
|
}
|
|
@@ -314,35 +252,98 @@ function mostrarMenuSlash() {
|
|
|
314
252
|
|
|
315
253
|
let rl = null;
|
|
316
254
|
|
|
317
|
-
|
|
255
|
+
/* ── A BARRA DE STATUS ───────────────────────────────────────────────────
|
|
256
|
+
* As duas linhas embaixo da caixa de digitar. Elas existem para responder,
|
|
257
|
+
* sem a pessoa perguntar, as coisas que ela erra quando não sabe:
|
|
258
|
+
*
|
|
259
|
+
* · EM QUE PASTA ele está mexendo — de longe o que mais confunde, porque
|
|
260
|
+
* o PrimoCode NÃO trabalha na pasta de onde você o abriu;
|
|
261
|
+
* · em que ramo do git, quando a pasta é um repositório: um agente que
|
|
262
|
+
* escreve arquivo no `main` sem ninguém perceber é o pior tipo de
|
|
263
|
+
* surpresa;
|
|
264
|
+
* · quanto da conversa já foi gasto, para o /compact chegar antes do teto;
|
|
265
|
+
* · qual o modo e qual o esforço, que mudam o que ele pode fazer.
|
|
266
|
+
*
|
|
267
|
+
* A segunda linha é a dos atalhos. Ela repete o que existe porque atalho que
|
|
268
|
+
* a pessoa não vê é atalho que não existe.
|
|
269
|
+
*/
|
|
270
|
+
|
|
271
|
+
// `git rev-parse` a cada quadro seria um processo por tecla digitada. O ramo
|
|
272
|
+
// muda devagar; cinco segundos de atraso não confundem ninguém.
|
|
273
|
+
const ramoCache = new Map();
|
|
274
|
+
function ramoDoGit(dir) {
|
|
275
|
+
if (!dir) return '';
|
|
276
|
+
const guardado = ramoCache.get(dir);
|
|
277
|
+
if (guardado && Date.now() - guardado.quando < 5000) return guardado.ramo;
|
|
278
|
+
let ramo = '';
|
|
279
|
+
try {
|
|
280
|
+
ramo = require('child_process').execSync('git rev-parse --abbrev-ref HEAD', {
|
|
281
|
+
cwd: dir, stdio: ['ignore', 'pipe', 'ignore'], timeout: 1500,
|
|
282
|
+
}).toString().trim();
|
|
283
|
+
} catch { ramo = ''; }
|
|
284
|
+
ramoCache.set(dir, { quando: Date.now(), ramo });
|
|
285
|
+
return ramo;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function barraDeStatus() {
|
|
289
|
+
const partes = [];
|
|
290
|
+
|
|
291
|
+
/* O CONTEXTO. Só aparece depois que há conversa: um "ctx 0%" na abertura
|
|
292
|
+
é ruído, e ruído que aparece sempre é o que a pessoa aprende a não
|
|
293
|
+
enxergar — inclusive quando marcar 90%. Âmbar a partir de 70% e
|
|
294
|
+
vermelho a partir de 100%, que é quando `/compact` deixa de ser
|
|
295
|
+
conselho e vira o que fazer. */
|
|
296
|
+
const uso = memoria.usoDoContexto(state.memoria, state.history);
|
|
297
|
+
if (uso.pct >= 5) {
|
|
298
|
+
partes.push(uso.pct >= 100 ? c.err(`${uso.pct}% ctx`)
|
|
299
|
+
: uso.pct >= 70 ? c.warn(`${uso.pct}% ctx`)
|
|
300
|
+
: c.dim(`${uso.pct}% ctx`));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// O esforço, que é o que escolhe o modelo. Não há provedor para mostrar:
|
|
304
|
+
// a API vem com o produto.
|
|
305
|
+
partes.push(c.muted(effort.normalizar(state.config.effort)));
|
|
306
|
+
|
|
307
|
+
// A PASTA. O nome dela, e o caminho curto quando não é um projeto — quem
|
|
308
|
+
// apontou com /dir para fora do ~/primocode precisa ver onde está.
|
|
309
|
+
const dir = state.workDir || process.cwd();
|
|
310
|
+
partes.push(c.brandDim(shortPath(dir)));
|
|
311
|
+
|
|
312
|
+
const ramo = ramoDoGit(dir);
|
|
313
|
+
if (ramo) partes.push(c.dim('⎇ ' + ramo));
|
|
314
|
+
|
|
315
|
+
if (state.attachedFiles.length) {
|
|
316
|
+
partes.push(c.muted(`+${state.attachedFiles.length} anexo${state.attachedFiles.length > 1 ? 's' : ''}`));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* Enquanto o controle do computador está liberado, isso fica escrito na
|
|
320
|
+
barra. Uma autorização que some da vista é uma autorização que a pessoa
|
|
321
|
+
esquece que deu — e ela vale até o PrimoCode fechar. */
|
|
322
|
+
if (state.controle && state.controle.liberado) partes.push(c.warn('controle do pc'));
|
|
323
|
+
|
|
318
324
|
// No auto o rótulo mostra "Auto" e, depois do primeiro pedido, qual modo
|
|
319
325
|
// ele acabou usando — senão você nunca sabe quem respondeu.
|
|
320
|
-
let
|
|
326
|
+
let modo;
|
|
321
327
|
if (state.mode === 'auto') {
|
|
322
|
-
const ultimo = state.modoEfetivo === 'primocode' ? c.warn('primo')
|
|
323
|
-
|
|
328
|
+
const ultimo = state.modoEfetivo === 'primocode' ? c.warn('primo')
|
|
329
|
+
: state.modoEfetivo === 'chat' ? c.brandDim('chat') : '';
|
|
330
|
+
modo = c.brand('auto') + (ultimo ? c.dim('·') + ultimo : '');
|
|
324
331
|
} else {
|
|
325
|
-
|
|
332
|
+
modo = state.mode === 'primocode' ? c.warn('primo code') : c.brandDim('chat');
|
|
326
333
|
}
|
|
327
|
-
|
|
334
|
+
partes.push(modo);
|
|
328
335
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
+ (uso.pct >= 100 ? c.err(`ctx ${uso.pct}%`)
|
|
337
|
-
: uso.pct >= 70 ? c.warn(`ctx ${uso.pct}%`)
|
|
338
|
-
: c.dim(`ctx ${uso.pct}%`));
|
|
339
|
-
// O modelo ativo: qual motor está pensando. No padrão é o do servidor.
|
|
340
|
-
const motor = state.engine === 'claude-code' ? c.dim(' · ') + c.dim('claude')
|
|
341
|
-
: state.engine === 'codex' ? c.dim(' · ') + c.dim('gpt') : '';
|
|
342
|
-
const att = state.attachedFiles.length ? c.muted(` +${state.attachedFiles.length}`) : '';
|
|
343
|
-
return `\n${c.muted(modeTag)}${motor}${ctx}${proj}${att} ${c.brand('❯')} `;
|
|
336
|
+
const primeira = partes.join(c.dim(' | '));
|
|
337
|
+
|
|
338
|
+
const atalhos = state.trabalhando
|
|
339
|
+
? c.warn('▸▸ trabalhando') + c.dim(' (Esc Esc interrompe)')
|
|
340
|
+
: c.dim('/ comandos · ↑ histórico · \\ ou alt+enter quebra linha · Esc Esc interrompe');
|
|
341
|
+
|
|
342
|
+
return [primeira, atalhos];
|
|
344
343
|
}
|
|
345
|
-
|
|
344
|
+
|
|
345
|
+
// A barra se redesenha sozinha a cada quadro; isto só pede um quadro novo.
|
|
346
|
+
function refreshPrompt() { if (rl) rl.prompt(); }
|
|
346
347
|
|
|
347
348
|
// Enquanto uma pergunta está no ar, a próxima linha digitada é a RESPOSTA dela
|
|
348
349
|
// — não um novo comando. Sem isso o "s" de confirmação virava prompt para a IA.
|
|
@@ -525,6 +526,195 @@ function prepararPasta(userText) {
|
|
|
525
526
|
return plan;
|
|
526
527
|
}
|
|
527
528
|
|
|
529
|
+
/* ── O PEDIDO PARA MEXER NO COMPUTADOR ───────────────────────────────────
|
|
530
|
+
* Três respostas: agora, a sessão inteira, não.
|
|
531
|
+
*
|
|
532
|
+
* A do meio existe porque uma tarefa de desktop são dez, vinte ações. Pedir a
|
|
533
|
+
* cada clique transforma a permissão num botão que se aperta sem ler — o
|
|
534
|
+
* mesmo que não pedir, com uma etapa a mais. Uma vez, com o que ele vai fazer
|
|
535
|
+
* escrito, é a pergunta que ainda é lida.
|
|
536
|
+
*
|
|
537
|
+
* Enter sozinho é NÃO. O padrão de quem apertou Enter sem ler não pode ser
|
|
538
|
+
* entregar o mouse e o teclado.
|
|
539
|
+
*/
|
|
540
|
+
async function autorizarControle(oQueVaiFazer, { primeira } = {}) {
|
|
541
|
+
if (primeira) {
|
|
542
|
+
console.log('');
|
|
543
|
+
console.log(' ' + c.warn('▸▸') + ' ' + c.bold(c.white('O Primo quer usar o seu computador.')));
|
|
544
|
+
console.log(' ' + c.muted('Ele vai mexer no mouse e no teclado de verdade, no programa que'));
|
|
545
|
+
console.log(' ' + c.muted('estiver na frente. Você vê cada passo aqui enquanto acontece.'));
|
|
546
|
+
}
|
|
547
|
+
console.log('');
|
|
548
|
+
console.log(' ' + c.brand('●') + ' ' + c.white(oQueVaiFazer));
|
|
549
|
+
console.log(' ' + c.dim('1') + c.muted(' agora ')
|
|
550
|
+
+ c.dim('2') + c.muted(' esta sessão inteira ')
|
|
551
|
+
+ c.dim('Enter') + c.muted(' não'));
|
|
552
|
+
|
|
553
|
+
const dito = String(await ask(' ' + c.brand('❯ ') + c.muted('1, 2 ou Enter '))).trim().toLowerCase();
|
|
554
|
+
if (dito === '1' || /^(s|sim|ok|pode|agora|y|yes)$/.test(dito)) {
|
|
555
|
+
console.log(' ' + c.ok('✔ autorizado') + c.dim(' — só esta ação'));
|
|
556
|
+
return 'agora';
|
|
557
|
+
}
|
|
558
|
+
if (dito === '2' || /sess|sempre|tudo|todas/.test(dito)) {
|
|
559
|
+
console.log(' ' + c.ok('✔ autorizado') + c.dim(' — esta sessão inteira. Fechar o PrimoCode esquece.'));
|
|
560
|
+
return 'sessao';
|
|
561
|
+
}
|
|
562
|
+
console.log(' ' + c.muted('Não autorizado.') + c.dim(' Ele vai resolver por outro caminho.'));
|
|
563
|
+
return 'nao';
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/* ── A TELA DO MARKETPLACE ───────────────────────────────────────────────
|
|
567
|
+
* O desenho e as teclas vivem em lib/marketplace.js, que é função pura. O que
|
|
568
|
+
* mora aqui é o que precisa do terminal: tirar a caixa de digitar da frente,
|
|
569
|
+
* escutar as setas, e fazer o que a tela pediu (ligar uma skill, abrir o
|
|
570
|
+
* diálogo de criar).
|
|
571
|
+
*
|
|
572
|
+
* A divisão é a mesma do /effort, e pelo mesmo motivo: dois desenhos no mesmo
|
|
573
|
+
* lugar brigam pelo teclado e pelo cursor.
|
|
574
|
+
*/
|
|
575
|
+
function abrirMarketplace() {
|
|
576
|
+
return new Promise((resolve) => {
|
|
577
|
+
if (!process.stdin.isTTY) {
|
|
578
|
+
// Sem terminal não há seta para apertar. Cair no /skills é melhor
|
|
579
|
+
// que desenhar uma tela que ninguém pode usar.
|
|
580
|
+
return handleSlash('/skills').then(resolve);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
let estado = marketplace.novo(skills.listar());
|
|
584
|
+
const altura = () => Math.max(6, Math.min(16, (process.stdout.rows || 24) - 10));
|
|
585
|
+
let alturaDesenhada = 0;
|
|
586
|
+
|
|
587
|
+
const pintar = (primeira) => {
|
|
588
|
+
const texto = marketplace.montar(estado, {
|
|
589
|
+
colunas: process.stdout.columns || 80, altura: altura(),
|
|
590
|
+
});
|
|
591
|
+
const linhas = texto.split('\n');
|
|
592
|
+
// Redesenha POR CIMA do quadro anterior em vez de rolar a tela:
|
|
593
|
+
// sem isso, cada seta empurraria a tela inteira para cima e o
|
|
594
|
+
// terminal viraria um rolo de cópias do marketplace.
|
|
595
|
+
if (!primeira && alturaDesenhada) process.stdout.write(`\x1b[${alturaDesenhada}A\r`);
|
|
596
|
+
process.stdout.write(linhas.map((l) => '\x1b[2K' + l).join('\n') + '\n');
|
|
597
|
+
// O quadro novo pode ser mais curto que o anterior (a descrição
|
|
598
|
+
// que some ao mudar de item): o resto é apagado, senão sobra
|
|
599
|
+
// lixo da linha antiga embaixo.
|
|
600
|
+
if (alturaDesenhada > linhas.length) {
|
|
601
|
+
const sobra = alturaDesenhada - linhas.length;
|
|
602
|
+
process.stdout.write('\x1b[2K\n'.repeat(sobra) + `\x1b[${sobra}A`);
|
|
603
|
+
}
|
|
604
|
+
alturaDesenhada = Math.max(linhas.length, alturaDesenhada);
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
const aoTeclar = (ch, key) => {
|
|
608
|
+
if (!key) return;
|
|
609
|
+
const { estado: novoEstado, acao } = marketplace.andar(estado, key, { altura: altura() });
|
|
610
|
+
estado = novoEstado;
|
|
611
|
+
|
|
612
|
+
if (!acao) { pintar(false); return; }
|
|
613
|
+
|
|
614
|
+
if (acao.tipo === 'fechar') { encerrar(); resolve(); return; }
|
|
615
|
+
|
|
616
|
+
if (acao.tipo === 'alternar') {
|
|
617
|
+
skills.ligar(acao.skill.nome, !acao.skill.ligada);
|
|
618
|
+
// A lista é relida do disco: a skill muda de seção, e a tela
|
|
619
|
+
// tem de mostrar isso na hora — senão parece que nada
|
|
620
|
+
// aconteceu e a pessoa aperta Enter de novo.
|
|
621
|
+
estado = { ...estado, skills: skills.listar() };
|
|
622
|
+
pintar(false);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (acao.tipo === 'nova' || acao.tipo === 'instalar') {
|
|
627
|
+
encerrar();
|
|
628
|
+
(acao.tipo === 'nova' ? criarSkill() : instalarSkill())
|
|
629
|
+
.then(() => abrirMarketplace())
|
|
630
|
+
.then(resolve);
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
/* Largar o teclado e devolver a caixa. Chamado por TODAS as saídas —
|
|
635
|
+
o Esc, o "New Skill" e o "Instalar" —, porque o diálogo que vem
|
|
636
|
+
depois usa o `ask`, e o `ask` precisa da caixa de volta. */
|
|
637
|
+
const encerrar = () => {
|
|
638
|
+
process.stdin.removeListener('keypress', aoTeclar);
|
|
639
|
+
process.stdin.removeListener('end', aoAcabar);
|
|
640
|
+
process.stdin.removeListener('close', aoAcabar);
|
|
641
|
+
sliderAtivo = false;
|
|
642
|
+
if (rl && rl.retomar) rl.retomar();
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
/* A entrada pode acabar com a tela aberta (pipe que terminou, Ctrl+D,
|
|
646
|
+
terminal fechado). Sem isto o programa ficaria esperando para
|
|
647
|
+
sempre uma tecla que não vem mais — pendurado e calado, que é o
|
|
648
|
+
pior jeito de travar. */
|
|
649
|
+
const aoAcabar = () => { encerrar(); resolve(); };
|
|
650
|
+
process.stdin.once('end', aoAcabar);
|
|
651
|
+
process.stdin.once('close', aoAcabar);
|
|
652
|
+
|
|
653
|
+
sliderAtivo = true;
|
|
654
|
+
if (rl && rl.suspender) rl.suspender();
|
|
655
|
+
pintar(true);
|
|
656
|
+
process.stdin.on('keypress', aoTeclar);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/* ── New Skill: o nome, e depois o prompt ────────────────────────────────
|
|
661
|
+
* Em sequência, como foi pedido. O conteúdo é multilinha e termina numa linha
|
|
662
|
+
* com apenas `.` — não em Enter: o prompt de uma skill tem parágrafos, e
|
|
663
|
+
* terminar no primeiro Enter tornaria impossível escrever uma skill de
|
|
664
|
+
* verdade sem colar tudo numa linha só.
|
|
665
|
+
*/
|
|
666
|
+
async function criarSkill() {
|
|
667
|
+
console.log('');
|
|
668
|
+
console.log(' ' + c.bold(c.brand('New Skill')));
|
|
669
|
+
console.log(' ' + c.muted('Uma skill é conhecimento em texto: o que o Primo deve saber'));
|
|
670
|
+
console.log(' ' + c.muted('e seguir quando o assunto aparecer.'));
|
|
671
|
+
console.log('');
|
|
672
|
+
|
|
673
|
+
const nome = String(await ask(' ' + c.brand('❯ ') + c.white('Nome da skill: '))).trim();
|
|
674
|
+
if (!nome) { console.log(c.muted(' Sem nome, sem skill.')); return; }
|
|
675
|
+
|
|
676
|
+
console.log('');
|
|
677
|
+
console.log(' ' + c.muted('Agora o conteúdo — o prompt inteiro dela.'));
|
|
678
|
+
console.log(' ' + c.dim('Escreva quantas linhas quiser. Termine com um ponto sozinho numa linha.'));
|
|
679
|
+
console.log('');
|
|
680
|
+
|
|
681
|
+
const linhas = [];
|
|
682
|
+
for (;;) {
|
|
683
|
+
const l = await ask(' ' + c.dim('│ '));
|
|
684
|
+
if (String(l).trim() === '.') break;
|
|
685
|
+
linhas.push(l);
|
|
686
|
+
// Sem teto, um Enter preso na tecla viraria um arquivo de milhões de
|
|
687
|
+
// linhas. 400 é muito mais do que qualquer skill precisa.
|
|
688
|
+
if (linhas.length >= 400) { console.log(c.muted(' (chegou ao limite de linhas)')); break; }
|
|
689
|
+
}
|
|
690
|
+
const corpo = linhas.join('\n').trim();
|
|
691
|
+
if (!corpo) { console.log(c.muted(' Sem conteúdo, sem skill.')); return; }
|
|
692
|
+
|
|
693
|
+
console.log('');
|
|
694
|
+
const quando = String(await ask(' ' + c.brand('❯ ') + c.white('Quando usar? ')
|
|
695
|
+
+ c.dim('(palavras separadas por vírgula) '))).trim();
|
|
696
|
+
const descricao = String(await ask(' ' + c.brand('❯ ') + c.white('Uma linha do que ela faz: '))).trim();
|
|
697
|
+
|
|
698
|
+
const r = skills.criar(nome, corpo, { quando, descricao });
|
|
699
|
+
if (!r.ok) { console.log(c.err(` ${r.error}`)); return; }
|
|
700
|
+
console.log('');
|
|
701
|
+
console.log(c.ok(` Skill cadastrada: ${r.nome}`) + c.muted(` (${shortPath(r.caminho)})`));
|
|
702
|
+
if (!quando) {
|
|
703
|
+
// Sem gatilho ela nunca entra sozinha num pedido. Dizer isso agora
|
|
704
|
+
// evita a conclusão de que "a skill não funciona".
|
|
705
|
+
console.log(c.warn(' Sem palavras em "quando usar", ela não vai entrar sozinha nos pedidos.'));
|
|
706
|
+
console.log(c.dim(' Edite o arquivo e preencha o campo `quando:` quando quiser.'));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function instalarSkill() {
|
|
711
|
+
console.log('');
|
|
712
|
+
const caminho = String(await ask(' ' + c.brand('❯ ') + c.white('Caminho do SKILL.md: '))).trim();
|
|
713
|
+
if (!caminho) return;
|
|
714
|
+
const r = skills.instalarDe(caminho);
|
|
715
|
+
console.log(r.ok ? c.ok(` Skill instalada: ${r.nome}`) : c.err(` ${r.error}`));
|
|
716
|
+
}
|
|
717
|
+
|
|
528
718
|
async function sendPrompt(userText) {
|
|
529
719
|
/* O portão do plano. Sem ele o bloqueio da abertura seria só um texto na
|
|
530
720
|
tela: a pessoa leria "você não tem acesso" e mandaria a primeira coisa
|
|
@@ -587,6 +777,11 @@ async function sendPrompt(userText) {
|
|
|
587
777
|
|
|
588
778
|
let r = null;
|
|
589
779
|
let jaFeito = '';
|
|
780
|
+
/* O livro do PEDIDO, não do passo. Um plano de três passos são três
|
|
781
|
+
chamadas de runAct, cada uma com o seu registro — e o balanço que
|
|
782
|
+
interessa é o do pedido inteiro: quem pediu um vídeo não quer três
|
|
783
|
+
balanços, quer saber o que existe no disco quando ele terminar. */
|
|
784
|
+
const livro = registro.novo();
|
|
590
785
|
for (let i = 0; i < passos.length; i++) {
|
|
591
786
|
if (i > 0) console.log(c.dim(` ⎿ ${passos[i].titulo}`));
|
|
592
787
|
// Só fato de passo CONCLUÍDO entra aqui. Num pedido real o passo de
|
|
@@ -614,7 +809,10 @@ async function sendPrompt(userText) {
|
|
|
614
809
|
// Só chamado se o agente for mesmo MEXER EM ARQUIVO — abrir o Chrome
|
|
615
810
|
// não é motivo para criar pasta de projeto.
|
|
616
811
|
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
617
|
-
|
|
812
|
+
// O portão do computador: quem pergunta é o terminal, quem
|
|
813
|
+
// lembra da resposta é a sessão.
|
|
814
|
+
autorizar: autorizarControle,
|
|
815
|
+
controle: state.controle,
|
|
618
816
|
});
|
|
619
817
|
|
|
620
818
|
r = await rodarPasso();
|
|
@@ -626,11 +824,18 @@ async function sendPrompt(userText) {
|
|
|
626
824
|
console.log(c.dim(' ⎿ o passo não concluiu — tentando de novo'));
|
|
627
825
|
r = await rodarPasso();
|
|
628
826
|
}
|
|
827
|
+
registro.juntar(livro, r.registro);
|
|
629
828
|
jaFeito = (r.concluiu !== false)
|
|
630
829
|
? String((r.assistantMessage && r.assistantMessage.content) || '').slice(0, 1200)
|
|
631
830
|
: '';
|
|
632
831
|
if (r.parado) break; // Esc Esc no meio do plano para o plano inteiro
|
|
633
832
|
}
|
|
833
|
+
|
|
834
|
+
/* O balanço, depois da resposta. Depois, e não antes, de propósito: o
|
|
835
|
+
que a pessoa quer ler primeiro é o que ele TEM A DIZER; a lista de
|
|
836
|
+
arquivos é a conferência, e conferência se lê por último. */
|
|
837
|
+
const balanco = registro.resumo(livro, { colunas: process.stdout.columns || 80 });
|
|
838
|
+
if (balanco) console.log(balanco);
|
|
634
839
|
state.history.push({ role: 'user', content: userText });
|
|
635
840
|
state.history.push({ role: 'assistant', content: r.assistantMessage.content });
|
|
636
841
|
state.lastResponse = r.assistantMessage.content;
|
|
@@ -675,30 +880,39 @@ async function sendPrompt(userText) {
|
|
|
675
880
|
const RE_DESCOBRIR = /\b(pesquis|busca|procur|descubr|investig|analis|leia|estud|olh[ae] (o site|a p[áa]gina)|ver o que)\w*/i;
|
|
676
881
|
const RE_CRIAR_DEPOIS = /\b(cri[ae]|faz|faça|mont[ae]|ger[ae]|escrev[ae]|produz|desenvolv)\w*/i;
|
|
677
882
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
883
|
+
/* "me manda o vídeo" resolvido na hora, sem gastar modelo.
|
|
884
|
+
*
|
|
885
|
+
* Antes isso abria a última criação do Studio no navegador com `#gravar`, e a
|
|
886
|
+
* pessoa precisava aceitar uma permissão de tela para o navegador GRAVAR a
|
|
887
|
+
* animação passando — era assim que o mp4 nascia.
|
|
888
|
+
*
|
|
889
|
+
* Isso acabou junto com o renderizador de navegador: o Remotion escreve o
|
|
890
|
+
* arquivo direto, e todo render já deixa um .mp4 em `saida/`. Então "baixa o
|
|
891
|
+
* vídeo" não é mais uma tarefa — é dizer onde ele está, que o CLI sabe sem
|
|
892
|
+
* perguntar a modelo nenhum.
|
|
893
|
+
*/
|
|
894
|
+
const RE_BAIXAR = /\b(baixa\w*|exporta\w*|download|salva\w*|me manda|manda(r)? (o|a|esse|essa)|gera(r)? o arquivo|em mp4|pra mandar|arquivo do|onde (est[áa]|ficou))\b/i;
|
|
895
|
+
const RE_PECA_STUDIO = /\b(v[ií]deo|mp4|apresenta[çc][ãa]o|slides?|reels?|anima[çc][ãa]o|filme)\b/i;
|
|
684
896
|
|
|
685
897
|
async function baixarCriacaoDireto(texto) {
|
|
686
898
|
if (!(RE_BAIXAR.test(texto) && RE_PECA_STUDIO.test(texto))) return false;
|
|
687
899
|
try {
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
if (!
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
console.log(c.
|
|
900
|
+
const { projeto } = require('../lib/estudio');
|
|
901
|
+
// O mais recente que já foi renderizado: quem diz "me manda o vídeo"
|
|
902
|
+
// fala do último, e um projeto sem render não tem o que mandar.
|
|
903
|
+
const comArquivo = projeto.listar({ limite: 30 })
|
|
904
|
+
.find((p) => (p.renders || []).some((r) => fs.existsSync(r.arquivo)));
|
|
905
|
+
if (!comArquivo) return false;
|
|
906
|
+
const ultimo = [...comArquivo.renders].reverse().find((r) => fs.existsSync(r.arquivo));
|
|
907
|
+
|
|
908
|
+
await tools.execute('open_in_browser', { target: ultimo.arquivo },
|
|
909
|
+
{ projectDir: state.workDir || os.homedir() });
|
|
910
|
+
console.log(c.ok(` ⎿ abrindo "${comArquivo.titulo}"`));
|
|
911
|
+
console.log(c.muted(' ') + c.white(ultimo.arquivo));
|
|
912
|
+
console.log(c.dim(` ${(ultimo.bytes / 1048576).toFixed(1)} MB · o arquivo já estava pronto, é só copiar daí.`));
|
|
699
913
|
return true;
|
|
700
914
|
} catch {
|
|
701
|
-
return false; //
|
|
915
|
+
return false; // sem projeto renderizado: segue o caminho normal
|
|
702
916
|
}
|
|
703
917
|
}
|
|
704
918
|
|
|
@@ -807,17 +1021,6 @@ async function runChat(promptForModel, userTextForMemory) {
|
|
|
807
1021
|
|
|
808
1022
|
// ── Slash commands ─────────────────────────────────────────────────────
|
|
809
1023
|
|
|
810
|
-
/* Qual motor executa este turno.
|
|
811
|
-
*
|
|
812
|
-
* Um `if` a mais aqui e um `if` a menos em qualquer outro lugar seria o
|
|
813
|
-
* caminho para o /codex funcionar no modo agente e não no chat, ou ao
|
|
814
|
-
* contrário. Um lugar só decide, e os dois modos chamam este. */
|
|
815
|
-
function motorDaVez() {
|
|
816
|
-
if (state.engine === 'claude-code') return claudeEngine;
|
|
817
|
-
if (state.engine === 'codex') return codexEngine;
|
|
818
|
-
return api;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
1024
|
function isSlash(text) { return text.trim().startsWith('/'); }
|
|
822
1025
|
|
|
823
1026
|
/* ── OS NOMES ANTIGOS ────────────────────────────────────────────────────
|
|
@@ -841,8 +1044,6 @@ const RENOMEADOS = {
|
|
|
841
1044
|
entrar: 'connect', login: 'connect', conectar: 'connect',
|
|
842
1045
|
conta: 'account',
|
|
843
1046
|
sair: 'exit', // sair é SAIR; desconectar a conta é /logout
|
|
844
|
-
// as chaves de API
|
|
845
|
-
chave: 'key', chaves: 'key',
|
|
846
1047
|
// o histórico
|
|
847
1048
|
conversas: 'chats', historico: 'chats', histórico: 'chats',
|
|
848
1049
|
// o resto
|
|
@@ -954,97 +1155,6 @@ async function handleSlash(text) {
|
|
|
954
1155
|
break;
|
|
955
1156
|
}
|
|
956
1157
|
|
|
957
|
-
/* A chave de API da própria pessoa. Guardada em ~/.primocode/
|
|
958
|
-
chaves.json com permissão 600 — nunca no config.json, que é o
|
|
959
|
-
arquivo que se cola num chamado de suporte. */
|
|
960
|
-
case 'key': case 'keys': {
|
|
961
|
-
const [qual, ...resto] = rest.split(/\s+/).filter(Boolean);
|
|
962
|
-
const valor = resto.join(' ');
|
|
963
|
-
|
|
964
|
-
if (!qual) {
|
|
965
|
-
console.log(c.white('\n Suas chaves de API'));
|
|
966
|
-
for (const p of chaves.listar()) {
|
|
967
|
-
console.log(' ' + c.white(p.nome.padEnd(8))
|
|
968
|
-
+ (p.tem ? c.ok(p.disfarce) : c.dim('não configurada'))
|
|
969
|
-
+ c.dim(` ${p.variavel}`));
|
|
970
|
-
}
|
|
971
|
-
console.log(c.dim('\n /key claude sk-ant-… guarda a chave'));
|
|
972
|
-
console.log(c.dim(' /key gpt remover apaga a que estiver lá'));
|
|
973
|
-
console.log(c.muted('\n Sem chave própria, cada motor usa a conta em que você já entrou nele.'));
|
|
974
|
-
break;
|
|
975
|
-
}
|
|
976
|
-
if (!chaves.PROVEDORES[qual]) {
|
|
977
|
-
console.log(c.err(` Não conheço "${qual}". Use: ${Object.keys(chaves.PROVEDORES).join(', ')}`));
|
|
978
|
-
break;
|
|
979
|
-
}
|
|
980
|
-
if (valor === 'remover' || valor === 'apagar') {
|
|
981
|
-
const r = chaves.esquecer(qual);
|
|
982
|
-
console.log(r.ok ? c.ok(` Chave do ${chaves.PROVEDORES[qual].nome} apagada.`)
|
|
983
|
-
: c.muted(` ${r.error}`));
|
|
984
|
-
break;
|
|
985
|
-
}
|
|
986
|
-
if (!valor) {
|
|
987
|
-
const p = chaves.PROVEDORES[qual];
|
|
988
|
-
console.log(c.muted(` Uso: `) + c.brand(`/key ${qual} ${p.prefixo}…`));
|
|
989
|
-
console.log(c.dim(` Pegue a sua em ${p.onde}`));
|
|
990
|
-
break;
|
|
991
|
-
}
|
|
992
|
-
const r = chaves.guardar(qual, valor);
|
|
993
|
-
if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
|
|
994
|
-
// O que se mostra de volta é o disfarce, nunca a chave: a linha
|
|
995
|
-
// fica no scrollback do terminal e no print que a pessoa manda.
|
|
996
|
-
console.log(c.ok(` Chave do ${r.nome} guardada.`) + c.muted(` ${r.disfarce}`));
|
|
997
|
-
console.log(c.dim(` Ela vai como ${chaves.PROVEDORES[qual].variavel} só quando o motor rodar.`));
|
|
998
|
-
break;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
/* O Codex da conta da pessoa, com o modelo barato.
|
|
1002
|
-
O protocolo é o MESMO do /claude — ver lib/codex-engine.js. */
|
|
1003
|
-
/* `/gpt` e `/codex` são a mesma coisa e existem os dois de propósito:
|
|
1004
|
-
o programa se chama Codex, mas quem usa fala "GPT" — e o comando que
|
|
1005
|
-
a pessoa tenta é o nome que ela tem na cabeça, não o do fabricante. */
|
|
1006
|
-
case 'gpt': case 'codex': {
|
|
1007
|
-
if (rest.toLowerCase() === 'off') {
|
|
1008
|
-
if (state.engine !== 'codex') { console.log(c.muted(' Já estava no motor padrão.')); break; }
|
|
1009
|
-
state.engine = 'server';
|
|
1010
|
-
console.log(c.ok(' Motor: OpenRouter/Groq (padrão)'));
|
|
1011
|
-
break;
|
|
1012
|
-
}
|
|
1013
|
-
console.log(c.dim(' Checando o Codex…'));
|
|
1014
|
-
const r = codexEngine.garantirEngine();
|
|
1015
|
-
if (!r.ok) {
|
|
1016
|
-
console.log(c.err(` ${r.error}`));
|
|
1017
|
-
if (r.comoResolver) console.log(c.muted(` ${r.comoResolver}`));
|
|
1018
|
-
break;
|
|
1019
|
-
}
|
|
1020
|
-
state.engine = 'codex';
|
|
1021
|
-
console.log(c.ok(` Motor: Codex (${r.modelo})`) + c.muted(' — na sua conta, não na minha'));
|
|
1022
|
-
console.log(c.muted(' /codex off volta pro motor padrão.'));
|
|
1023
|
-
break;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
case 'claude': {
|
|
1027
|
-
if (rest.toLowerCase() === 'off') {
|
|
1028
|
-
if (state.engine !== 'claude-code') { console.log(c.muted(' Já estava no motor padrão.')); break; }
|
|
1029
|
-
state.engine = 'server';
|
|
1030
|
-
console.log(c.ok(' Motor: OpenRouter/Groq (padrão)'));
|
|
1031
|
-
break;
|
|
1032
|
-
}
|
|
1033
|
-
console.log(c.dim(' Checando Claude Code e ORCA…'));
|
|
1034
|
-
const r = await claudeEngine.garantirEngine();
|
|
1035
|
-
if (!r.ok) {
|
|
1036
|
-
console.log(c.err(` ${r.error}`));
|
|
1037
|
-
if (r.comoResolver) console.log(c.muted(` ${r.comoResolver}`));
|
|
1038
|
-
break;
|
|
1039
|
-
}
|
|
1040
|
-
state.engine = 'claude-code';
|
|
1041
|
-
const onde = r.caminho === 'A' ? 'sessão contínua via ORCA' : 'chamada avulsa (claude -p, sem ORCA)';
|
|
1042
|
-
console.log(c.ok(` Motor: Claude Code (${onde})`)
|
|
1043
|
-
+ c.muted(` — ${claudeEngine.MODELO_CLAUDE || 'o padrão da sua conta'}, na sua conta`));
|
|
1044
|
-
console.log(c.muted(' /claude off volta pro motor padrão.'));
|
|
1045
|
-
break;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
1158
|
// O /model virou /effort: você escolhe o ESFORÇO num slider (←/→,
|
|
1049
1159
|
// Enter confirma, Esc cancela) e o modelo sai daí. Com argumento
|
|
1050
1160
|
// ("/effort high") aplica direto, para quem já sabe o que quer.
|
|
@@ -1244,6 +1354,191 @@ async function handleSlash(text) {
|
|
|
1244
1354
|
break;
|
|
1245
1355
|
}
|
|
1246
1356
|
|
|
1357
|
+
/* ── AS SKILLS ───────────────────────────────────────────────────
|
|
1358
|
+
* `/skills` lista, `/install-skill` traz de um arquivo, e
|
|
1359
|
+
* `/marketplace` é a tela onde se navega e liga.
|
|
1360
|
+
*
|
|
1361
|
+
* Nenhum dos três pede confirmação para ligar ou desligar: skill é
|
|
1362
|
+
* TEXTO, e o pior que um texto ruim faz é dar um conselho ruim ao
|
|
1363
|
+
* modelo — que o usuário vê acontecendo, ação por ação, na tela.
|
|
1364
|
+
*/
|
|
1365
|
+
case 'skills': {
|
|
1366
|
+
const lista = skills.listar();
|
|
1367
|
+
if (!lista.length) { console.log(c.muted(' Nenhuma skill instalada.')); break; }
|
|
1368
|
+
const ligadas = lista.filter((s) => s.ligada);
|
|
1369
|
+
console.log('');
|
|
1370
|
+
console.log(c.bold(c.white(' Skills')) + c.dim(` ${ligadas.length} de ${lista.length} ligadas`));
|
|
1371
|
+
console.log('');
|
|
1372
|
+
for (const s of lista) {
|
|
1373
|
+
const marca = s.ligada ? c.ok('●') : c.dim('○');
|
|
1374
|
+
console.log(` ${marca} ${c.white(ui.padRight(s.titulo, 28))}`
|
|
1375
|
+
+ c.dim(ui.truncate(s.descricao, (process.stdout.columns || 80) - 36)));
|
|
1376
|
+
}
|
|
1377
|
+
console.log('');
|
|
1378
|
+
console.log(c.dim(' /marketplace liga e desliga · o corpo da skill só entra no pedido'));
|
|
1379
|
+
console.log(c.dim(' quando o assunto bate, para não gastar o teto de tokens à toa.'));
|
|
1380
|
+
break;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
case 'install-skill': case 'installskill': {
|
|
1384
|
+
if (!rest) {
|
|
1385
|
+
console.log(c.muted(' Uso: ') + c.brand('/install-skill <caminho do SKILL.md>'));
|
|
1386
|
+
console.log(c.dim(' Ou abra o ') + c.brand('/marketplace') + c.dim(' e escolha "New Skill" para escrever uma.'));
|
|
1387
|
+
break;
|
|
1388
|
+
}
|
|
1389
|
+
const r = skills.instalarDe(rest);
|
|
1390
|
+
if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
|
|
1391
|
+
console.log(c.ok(` Skill instalada: ${r.nome}`) + c.muted(` (${shortPath(r.caminho)})`));
|
|
1392
|
+
console.log(c.dim(' Já está ligada. /skills mostra todas.'));
|
|
1393
|
+
break;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/* ── /voice ──────────────────────────────────────────────────────
|
|
1397
|
+
* O modo voz abre uma janelinha com uma orb — não é uma janela de
|
|
1398
|
+
* terminal. O aplicativo é Python (ver voz/primo_voz); o que acontece
|
|
1399
|
+
* aqui é subir ele e responder ao que ele pedir.
|
|
1400
|
+
*
|
|
1401
|
+
* O cérebro NÃO vai junto: cada frase transcrita volta para o mesmo
|
|
1402
|
+
* `runAct` do terminal, com as mesmas ferramentas, as mesmas skills e
|
|
1403
|
+
* a mesma permissão de computador. Portar o laço do agente para o
|
|
1404
|
+
* Python seria manter dois laços, e dois laços do mesmo agente é a
|
|
1405
|
+
* garantia de que um deles está errado.
|
|
1406
|
+
*/
|
|
1407
|
+
case 'voice': case 'voz': {
|
|
1408
|
+
const pronto = vozModo.conferir();
|
|
1409
|
+
if (!pronto.ok && !pronto.instalavel) {
|
|
1410
|
+
console.log(c.err(` ${pronto.error}`));
|
|
1411
|
+
if (pronto.comoResolver) console.log(c.muted(` ${pronto.comoResolver}`));
|
|
1412
|
+
break;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
if (!pronto.ok) {
|
|
1416
|
+
console.log(c.muted(` ${pronto.error}`));
|
|
1417
|
+
if (!(await confirm('Instalar agora?'))) {
|
|
1418
|
+
console.log(c.muted(' Sem eles o modo voz não abre.'));
|
|
1419
|
+
if (pronto.comoResolver) console.log(c.dim(` ${pronto.comoResolver}`));
|
|
1420
|
+
break;
|
|
1421
|
+
}
|
|
1422
|
+
const sp = new Spinner('instalando').start();
|
|
1423
|
+
const r = await vozModo.instalar(pronto.python, pronto.instalavel, () => {});
|
|
1424
|
+
sp.stop();
|
|
1425
|
+
if (!r.ok) {
|
|
1426
|
+
console.log(c.err(` não deu: ${r.error}`));
|
|
1427
|
+
console.log(c.muted(` ${pronto.comoResolver}`));
|
|
1428
|
+
break;
|
|
1429
|
+
}
|
|
1430
|
+
const conferido = vozModo.conferir();
|
|
1431
|
+
if (!conferido.ok) {
|
|
1432
|
+
console.log(c.err(` ainda falta: ${conferido.error}`));
|
|
1433
|
+
if (conferido.comoResolver) console.log(c.muted(` ${conferido.comoResolver}`));
|
|
1434
|
+
break;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
const py = (pronto.ok ? pronto : vozModo.conferir()).python;
|
|
1439
|
+
console.log('');
|
|
1440
|
+
console.log(' ' + c.bold(c.brand('Primo Code Voice')));
|
|
1441
|
+
console.log(' ' + c.muted('É só falar. A orb responde: ')
|
|
1442
|
+
+ c.dim('parada espera · pulsa ouvindo · respira pensando · brilha falando'));
|
|
1443
|
+
console.log(' ' + c.dim('Falar por cima corta a resposta · Esc ou fechar a janela encerra'));
|
|
1444
|
+
console.log('');
|
|
1445
|
+
|
|
1446
|
+
// A caixa de digitar sai da frente: quem está no modo voz não
|
|
1447
|
+
// está digitando, e uma caixa piscando embaixo é ruído.
|
|
1448
|
+
if (rl && rl.suspender) rl.suspender();
|
|
1449
|
+
sliderAtivo = true;
|
|
1450
|
+
|
|
1451
|
+
const r = await vozModo.abrir({
|
|
1452
|
+
python: py,
|
|
1453
|
+
servidor: state.config.server,
|
|
1454
|
+
token: state.config.token,
|
|
1455
|
+
voz: state.config.voz || 'antonio',
|
|
1456
|
+
mostrar: (texto, deErro) => {
|
|
1457
|
+
if (deErro) console.log(' ' + c.dim(texto));
|
|
1458
|
+
else console.log(' ' + c.brand('❯ ') + c.white(texto));
|
|
1459
|
+
},
|
|
1460
|
+
/* O agente de verdade. A instrução de FORMA vai na frente
|
|
1461
|
+
porque resposta boa de ler é ruim de ouvir — o trabalho é o
|
|
1462
|
+
mesmo, o jeito de contar é que muda. */
|
|
1463
|
+
pensar: async (texto) => {
|
|
1464
|
+
const plan = prepararPasta(texto);
|
|
1465
|
+
const resultado = await runAct(`${vozModo.COMO_FALAR}\n\nPEDIDO: ${texto}`, {
|
|
1466
|
+
server: state.config.server,
|
|
1467
|
+
model: state.config.model,
|
|
1468
|
+
effort: state.config.effort,
|
|
1469
|
+
token: state.config.token,
|
|
1470
|
+
history: memoria.paraEnviar(state.memoria, state.history),
|
|
1471
|
+
projectDir: plan.dir,
|
|
1472
|
+
projectName: plan.name || state.currentProject,
|
|
1473
|
+
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
1474
|
+
autorizar: autorizarControle,
|
|
1475
|
+
controle: state.controle,
|
|
1476
|
+
});
|
|
1477
|
+
const resposta = (resultado.assistantMessage && resultado.assistantMessage.content) || '';
|
|
1478
|
+
// A conversa é a MESMA do terminal: sair do modo voz e
|
|
1479
|
+
// continuar digitando não pode começar do zero.
|
|
1480
|
+
state.history.push({ role: 'user', content: texto });
|
|
1481
|
+
state.history.push({ role: 'assistant', content: resposta });
|
|
1482
|
+
const balanco = registro.resumo(resultado.registro, { colunas: process.stdout.columns || 80 });
|
|
1483
|
+
if (balanco) console.log(balanco);
|
|
1484
|
+
return resposta;
|
|
1485
|
+
},
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
sliderAtivo = false;
|
|
1489
|
+
if (rl && rl.retomar) rl.retomar();
|
|
1490
|
+
if (!r.ok) console.log(c.err(` o modo voz fechou: ${r.error}`));
|
|
1491
|
+
else console.log(c.muted(' Modo voz encerrado.'));
|
|
1492
|
+
break;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
case 'marketplace': case 'market': {
|
|
1496
|
+
await abrirMarketplace();
|
|
1497
|
+
break;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/* ── /connectors ─────────────────────────────────────────────────
|
|
1501
|
+
* Só mostra. Não há o que configurar — os conectores vivem no
|
|
1502
|
+
* servidor, com as chaves dele, e é assim de propósito: a promessa do
|
|
1503
|
+
* produto é que a API já vem pronta.
|
|
1504
|
+
*
|
|
1505
|
+
* Então por que existe? Porque "a IA está lenta hoje" e "ele errou o
|
|
1506
|
+
* código" têm causas diferentes e invisíveis: qual conector atendeu.
|
|
1507
|
+
* Sem esta tela, a única resposta possível é dar de ombros. Com ela,
|
|
1508
|
+
* dá para ver que o preferido está fora e outro assumiu — e essa é a
|
|
1509
|
+
* primeira pergunta de todo suporte.
|
|
1510
|
+
*/
|
|
1511
|
+
case 'connectors': case 'conectores': {
|
|
1512
|
+
const sp = new Spinner('lendo os conectores').start();
|
|
1513
|
+
let dados = null;
|
|
1514
|
+
try {
|
|
1515
|
+
dados = await api.getConectores(state.config.server, state.config.token);
|
|
1516
|
+
} catch (e) { sp.stop(); console.log(c.err(` Não deu para ler: ${ui.friendlyError(e)}`)); break; }
|
|
1517
|
+
sp.stop();
|
|
1518
|
+
|
|
1519
|
+
const lista = (dados && dados.conectores) || [];
|
|
1520
|
+
if (!lista.length) { console.log(c.muted(' O servidor não listou conector nenhum.')); break; }
|
|
1521
|
+
|
|
1522
|
+
console.log('');
|
|
1523
|
+
console.log(c.bold(c.white(' Conectores')) + c.dim(' — quem atende os seus pedidos'));
|
|
1524
|
+
console.log('');
|
|
1525
|
+
for (const cx of lista) {
|
|
1526
|
+
const marca = cx.ligado ? c.ok('●') : c.dim('○');
|
|
1527
|
+
const selos = [
|
|
1528
|
+
cx.gratis ? c.dim('grátis') : c.warn('pago'),
|
|
1529
|
+
cx.faz && cx.faz.ferramentas ? c.dim('ferramentas') : c.dim('só conversa'),
|
|
1530
|
+
].join(c.dim(' · '));
|
|
1531
|
+
console.log(` ${marca} ${c.white(ui.padRight(cx.nome, 24))}${selos}`);
|
|
1532
|
+
console.log(` ${c.dim(ui.truncate(cx.porque || '', (process.stdout.columns || 80) - 6))}`);
|
|
1533
|
+
}
|
|
1534
|
+
const dePe = lista.filter((x) => x.ligado).length;
|
|
1535
|
+
console.log('');
|
|
1536
|
+
console.log(c.muted(` ${dePe} de ${lista.length} de pé.`)
|
|
1537
|
+
+ c.dim(' Quem está fora não some da fila: entra assim que o servidor tiver a chave.'));
|
|
1538
|
+
console.log(c.dim(' Não há nada para você configurar — a API vem com o PrimoCode.'));
|
|
1539
|
+
break;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1247
1542
|
/* A janela. "Se mandar /rc pra ele, ele abre o aplicativo — para o
|
|
1248
1543
|
Mac e o Windows, já instalado junto com o npm, com as cores do
|
|
1249
1544
|
PrimoCode. Um chat estilo terminal para quem não quer o terminal."
|
|
@@ -1332,29 +1627,31 @@ async function handleSlash(text) {
|
|
|
1332
1627
|
abrir: (u) => tools.openInBrowser(u),
|
|
1333
1628
|
contar: (info) => {
|
|
1334
1629
|
sp.stop();
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
* "Pode tirar essa parte no login onde aparece o código
|
|
1338
|
-
* — o terminal mandou o código, pode tirar."
|
|
1339
|
-
*
|
|
1340
|
-
* A página do token já resolve o pareamento sozinha: ela
|
|
1341
|
-
* pergunta o código ao 127.0.0.1 desta máquina, e onde o
|
|
1342
|
-
* navegador bloqueia isso (Safari) ela oferece "Colar" —
|
|
1343
|
-
* o código continua indo para a área de transferência,
|
|
1344
|
-
* que é o caminho sem leitura. Oito letras impressas aqui
|
|
1345
|
-
* eram uma tarefa para a pessoa que o produto já não
|
|
1346
|
-
* precisa, e a tela ficava dizendo que talvez precisasse.
|
|
1347
|
-
*
|
|
1348
|
-
* O código continua existindo no protocolo (lib/conta.js)
|
|
1349
|
-
* — some da TELA, não do pareamento. */
|
|
1630
|
+
const meio = info.codigo.length === 8
|
|
1631
|
+
? info.codigo.slice(0, 4) + '-' + info.codigo.slice(4) : info.codigo;
|
|
1350
1632
|
console.log(c.muted(' Abrindo no navegador:'));
|
|
1351
1633
|
console.log(' ' + c.brand(info.url.replace(/[?&]porta=\d+/, '')));
|
|
1352
1634
|
console.log('');
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1635
|
+
if (info.porta) {
|
|
1636
|
+
/* COM O SERVIDOR LOCAL DE PÉ, NÃO HÁ CÓDIGO PARA LER. A
|
|
1637
|
+
página pergunta ao 127.0.0.1 desta máquina, recebe o
|
|
1638
|
+
código e aprova sozinha assim que a pessoa entra com a
|
|
1639
|
+
conta — "não precisa colocar código, não precisa pra
|
|
1640
|
+
nada, porque ele já libera". Mostrar as oito letras
|
|
1641
|
+
grandes aqui era pedir para ler o que ninguém precisa
|
|
1642
|
+
mais; elas ficam numa linha apagada, só para o caso
|
|
1643
|
+
de o navegador bloquear o 127.0.0.1 (Safari) e a
|
|
1644
|
+
página voltar a pedir. */
|
|
1645
|
+
console.log(' ' + c.white('Entre com a sua conta na página que abriu — o resto é automático.'));
|
|
1646
|
+
console.log(c.dim(` Se a página pedir um código: ${meio}${info.copiado ? ' (já copiado)' : ''}.`));
|
|
1647
|
+
} else {
|
|
1648
|
+
/* Sem porta local (ocupadas, ou bloqueadas), o código
|
|
1649
|
+
volta a ser o caminho — grande, partido em dois,
|
|
1650
|
+
porque é o que se lê de uma tela e se confere noutra. */
|
|
1651
|
+
console.log(' ' + c.bold(c.brand(meio.split('').join(' '))));
|
|
1652
|
+
console.log('');
|
|
1653
|
+
console.log(c.dim(' ' + (info.copiado ? 'O código já está copiado — cole na página.'
|
|
1654
|
+
: 'Digite o código na página.')));
|
|
1358
1655
|
}
|
|
1359
1656
|
console.log(c.dim(' Esperando o navegador…'));
|
|
1360
1657
|
},
|
|
@@ -1467,6 +1764,25 @@ async function handleSlash(text) {
|
|
|
1467
1764
|
// rl.close() em vez de process.exit(): sair na marra descarta o que
|
|
1468
1765
|
// ainda estava no buffer de saída.
|
|
1469
1766
|
case 'exit': case 'quit': if (rl) rl.close(); break;
|
|
1767
|
+
/* ── OS COMANDOS APOSENTADOS ──────────────────────────────────────
|
|
1768
|
+
* `/claude`, `/gpt`, `/codex` e `/key` existiam quando o PrimoCode
|
|
1769
|
+
* rodava em cima da assinatura da própria pessoa: ela instalava o
|
|
1770
|
+
* Claude Code ou o Codex, colava uma chave de API, e só então o
|
|
1771
|
+
* agente pensava.
|
|
1772
|
+
*
|
|
1773
|
+
* Isso acabou. A API vem configurada no produto: quem entra na conta
|
|
1774
|
+
* já tem motor, e não há provedor nenhum para escolher.
|
|
1775
|
+
*
|
|
1776
|
+
* Eles não somem calados. Quem tem o dedo viciado em `/claude` e
|
|
1777
|
+
* bate num "comando não existe" conclui que a atualização quebrou
|
|
1778
|
+
* alguma coisa — e não vai procurar no /help, vai desinstalar. */
|
|
1779
|
+
case 'claude': case 'gpt': case 'codex': case 'key': case 'keys': {
|
|
1780
|
+
console.log(c.muted(' Não existe mais provedor para configurar.'));
|
|
1781
|
+
console.log(c.muted(' A API já vem com o PrimoCode: entrou na conta, pode usar.'));
|
|
1782
|
+
console.log(c.dim(' /effort escolhe o esforço · /account mostra o seu plano'));
|
|
1783
|
+
break;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1470
1786
|
default: {
|
|
1471
1787
|
// "/" sozinho abre o menu; um prefixo único autopreenche e executa
|
|
1472
1788
|
// ("/eff" → /effort); prefixo ambíguo mostra as opções.
|
|
@@ -1611,7 +1927,11 @@ async function main() {
|
|
|
1611
1927
|
userName: state.config.userName,
|
|
1612
1928
|
pastaPadrao: state.workDirOrigem === 'padrao',
|
|
1613
1929
|
}));
|
|
1614
|
-
rl =
|
|
1930
|
+
rl = entrada.criar({
|
|
1931
|
+
status: barraDeStatus,
|
|
1932
|
+
comandos: () => SLASH_COMANDOS,
|
|
1933
|
+
vazio: 'peça alguma coisa, ou / para os comandos',
|
|
1934
|
+
});
|
|
1615
1935
|
|
|
1616
1936
|
rl.prompt();
|
|
1617
1937
|
|
|
@@ -1622,57 +1942,31 @@ async function main() {
|
|
|
1622
1942
|
let closed = false;
|
|
1623
1943
|
let saidBye = false;
|
|
1624
1944
|
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
let ultimoEsc = 0;
|
|
1651
|
-
process.stdin.on('keypress', (ch, key) => {
|
|
1652
|
-
if (sliderAtivo) return; // o slider é dono do teclado enquanto está no ar
|
|
1653
|
-
if (key && (key.name === 'escape' || key.sequence === '\x1b\x1b')) {
|
|
1654
|
-
if (!busy) { ultimoEsc = 0; return; } // sem agente rodando, Esc é só Esc
|
|
1655
|
-
const agora = Date.now();
|
|
1656
|
-
if (key.sequence === '\x1b\x1b' || agora - ultimoEsc < 2000) {
|
|
1657
|
-
ultimoEsc = 0;
|
|
1658
|
-
interromper();
|
|
1659
|
-
if (chatEmVoo) { try { chatEmVoo.abort(); } catch {} }
|
|
1660
|
-
process.stdout.write('\n' + c.warn(' ⏹ parando o agente…') + '\n');
|
|
1661
|
-
} else {
|
|
1662
|
-
ultimoEsc = agora;
|
|
1663
|
-
process.stdout.write('\n' + c.dim(' Esc de novo para parar o agente') + '\n');
|
|
1664
|
-
}
|
|
1665
|
-
return;
|
|
1666
|
-
}
|
|
1667
|
-
// Seta → no fim da linha aceita a sugestão inteira.
|
|
1668
|
-
if (key && key.name === 'right' && fantasma && rl && rl.cursor === rl.line.length) {
|
|
1669
|
-
const resto = fantasma;
|
|
1670
|
-
fantasma = '';
|
|
1671
|
-
rl.write(resto);
|
|
1672
|
-
}
|
|
1673
|
-
setImmediate(desenharFantasma);
|
|
1674
|
-
});
|
|
1675
|
-
}
|
|
1945
|
+
/* ── Esc Esc: parar o agente no meio do trabalho ──
|
|
1946
|
+
* Um Esc avisa; o segundo (em até 2s) manda parar. A caixa não sabe o
|
|
1947
|
+
* que é "parar o agente" — ela só avisa que o Esc aconteceu, e quem
|
|
1948
|
+
* decide o que fazer é aqui.
|
|
1949
|
+
*
|
|
1950
|
+
* A sugestão fantasma, que morava aqui, foi junto com o readline: hoje
|
|
1951
|
+
* ela é desenhada pela própria caixa (ver lib/entrada.js). */
|
|
1952
|
+
let ultimoEsc = 0;
|
|
1953
|
+
rl.on('escape', () => {
|
|
1954
|
+
if (!busy) { ultimoEsc = 0; return; }
|
|
1955
|
+
const agora = Date.now();
|
|
1956
|
+
if (agora - ultimoEsc < 2000) {
|
|
1957
|
+
ultimoEsc = 0;
|
|
1958
|
+
interromper();
|
|
1959
|
+
if (chatEmVoo) { try { chatEmVoo.abort(); } catch {} }
|
|
1960
|
+
console.log(c.warn(' ⏹ parando o agente…'));
|
|
1961
|
+
} else {
|
|
1962
|
+
ultimoEsc = agora;
|
|
1963
|
+
console.log(c.dim(' Esc de novo para parar o agente'));
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
|
|
1967
|
+
// Ctrl+C com a caixa vazia: sair é sair, mas sem deixar o último turno
|
|
1968
|
+
// pela metade — o sayBye espera a gravação subir.
|
|
1969
|
+
rl.on('sigint', () => rl.close());
|
|
1676
1970
|
|
|
1677
1971
|
function sayBye() {
|
|
1678
1972
|
if (saidBye) return;
|
|
@@ -1695,6 +1989,10 @@ async function main() {
|
|
|
1695
1989
|
async function drain() {
|
|
1696
1990
|
if (busy) return;
|
|
1697
1991
|
busy = true;
|
|
1992
|
+
// A caixa esmaece enquanto ele trabalha, e a segunda linha da barra
|
|
1993
|
+
// troca os atalhos pelo que importa naquele momento: como parar.
|
|
1994
|
+
state.trabalhando = true;
|
|
1995
|
+
if (rl.trabalhando) rl.trabalhando(true);
|
|
1698
1996
|
while (queue.length) {
|
|
1699
1997
|
const text = queue.shift().trim();
|
|
1700
1998
|
if (!text) continue;
|
|
@@ -1706,6 +2004,8 @@ async function main() {
|
|
|
1706
2004
|
}
|
|
1707
2005
|
}
|
|
1708
2006
|
busy = false;
|
|
2007
|
+
state.trabalhando = false;
|
|
2008
|
+
if (rl.trabalhando) rl.trabalhando(false);
|
|
1709
2009
|
// Se o stdin já fechou (pipe, Ctrl+D), a despedida espera a fila terminar
|
|
1710
2010
|
// — senão ela aparece antes do resultado do último pedido.
|
|
1711
2011
|
if (closed) sayBye();
|
|
@@ -1713,10 +2013,6 @@ async function main() {
|
|
|
1713
2013
|
}
|
|
1714
2014
|
|
|
1715
2015
|
rl.on('line', (line) => {
|
|
1716
|
-
// A lista de setinhas (e o slider do /effort) são donos do teclado
|
|
1717
|
-
// enquanto estão no ar: o Enter que confirma a escolha não é uma
|
|
1718
|
-
// linha para a fila, e sem isto ele vira um pedido em branco.
|
|
1719
|
-
if (sliderAtivo) return;
|
|
1720
2016
|
if (pendingAsk) { const resolve = pendingAsk; pendingAsk = null; resolve(line); return; }
|
|
1721
2017
|
// Abertura correndo: guarda em vez de mandar ao agente.
|
|
1722
2018
|
if (segurando) { adiantadas.push(line); return; }
|
|
@@ -1752,31 +2048,7 @@ async function main() {
|
|
|
1752
2048
|
interativo: !!(process.stdin.isTTY && process.stdout.isTTY),
|
|
1753
2049
|
conectado: () => conta.atual(),
|
|
1754
2050
|
conectar: () => handleSlash('/connect').then(() => conta.atual()),
|
|
1755
|
-
/* A ESCOLHA, COM AS SETINHAS — E O QUE FOI DIGITADO ANTES DELA.
|
|
1756
|
-
*
|
|
1757
|
-
* A lista só aparece depois de o plano voltar do servidor, e quem
|
|
1758
|
-
* colou "3⏎1⏎/version" já respondeu antes de ver. Esse texto está em
|
|
1759
|
-
* `adiantadas`; se a lista ignorasse, a resposta se perderia e o
|
|
1760
|
-
* menu ficaria esperando um teclado que já falou. Então: primeiro o
|
|
1761
|
-
* que foi adiantado (se casar com algum item), depois as setinhas. O
|
|
1762
|
-
* que não casa fica na fila — é comando, não resposta. */
|
|
1763
|
-
selecionar: ({ titulo, ajuda, lista }) => {
|
|
1764
|
-
while (adiantadas.length) {
|
|
1765
|
-
const dito = String(adiantadas[0] || '').trim();
|
|
1766
|
-
if (!dito) { adiantadas.shift(); continue; }
|
|
1767
|
-
const qual = boasVindas.escolher(dito, lista);
|
|
1768
|
-
if (!qual) break; // isso é comando, não escolha
|
|
1769
|
-
adiantadas.shift();
|
|
1770
|
-
const item = lista.find((e) => e.id === qual);
|
|
1771
|
-
console.log('');
|
|
1772
|
-
console.log(c.bold(c.brand(' ' + titulo)));
|
|
1773
|
-
console.log(c.brand(' ❯ ') + c.white(item ? item.titulo : qual));
|
|
1774
|
-
return Promise.resolve(qual);
|
|
1775
|
-
}
|
|
1776
|
-
return menuSetas({ titulo, ajuda, lista });
|
|
1777
|
-
},
|
|
1778
2051
|
sessao: () => conta.sessao(),
|
|
1779
|
-
aplicar: (cmd) => handleSlash(cmd),
|
|
1780
2052
|
}).then((r) => {
|
|
1781
2053
|
// O bloqueio de quem não é Premium/Super. É de CORTESIA: este arquivo
|
|
1782
2054
|
// vai publicado no npm com o código à vista. Quem segura de verdade é
|