primocode 8.42.2 → 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 +598 -360
- 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,56 +252,99 @@ 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
|
-
conversa: um "ctx 0%" na abertura é ruído, e ruído que aparece sempre é
|
|
331
|
-
o que a pessoa aprende a não enxergar — inclusive quando marcar 90%.
|
|
332
|
-
Fica âmbar a partir de 70% e vermelho a partir de 100%, que é quando
|
|
333
|
-
`/compact` deixa de ser conselho e vira o que fazer. */
|
|
334
|
-
const uso = memoria.usoDoContexto(state.memoria, state.history);
|
|
335
|
-
const ctx = uso.pct < 5 ? '' : c.dim(' · ')
|
|
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('❯')} `;
|
|
344
|
-
}
|
|
345
|
-
function refreshPrompt() { if (rl) rl.setPrompt(makePrompt()); }
|
|
336
|
+
const primeira = partes.join(c.dim(' | '));
|
|
346
337
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
* "conecta mas depois de uns 10 segundos desconecta": era exatamente isto.
|
|
353
|
-
* A pessoa via "Conectado. Plano premium" — real, acabou de vir do
|
|
354
|
-
* servidor — e no pedido seguinte caía em "O PrimoCode pede o plano
|
|
355
|
-
* Premium ou Super", porque `state.liberado` tinha ficado `false` de uma
|
|
356
|
-
* tentativa anterior (sem plano, ou login que não deu) e nada o atualizava.
|
|
357
|
-
*
|
|
358
|
-
* Toda vez que a conexão muda, chame isto com o que se sabe AGORA — o
|
|
359
|
-
* `{ pro }` que já veio de `conta.entrar()` ou de `conta.atual()`, sem ir à
|
|
360
|
-
* rede de novo: essas chamadas já confirmaram o servidor há segundos.
|
|
361
|
-
*/
|
|
362
|
-
function sincronizarLiberacao({ pro, motivo } = {}) {
|
|
363
|
-
state.liberado = !!pro;
|
|
364
|
-
state.motivoBloqueio = pro ? null : (motivo || 'sem-plano');
|
|
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];
|
|
365
343
|
}
|
|
366
344
|
|
|
345
|
+
// A barra se redesenha sozinha a cada quadro; isto só pede um quadro novo.
|
|
346
|
+
function refreshPrompt() { if (rl) rl.prompt(); }
|
|
347
|
+
|
|
367
348
|
// Enquanto uma pergunta está no ar, a próxima linha digitada é a RESPOSTA dela
|
|
368
349
|
// — não um novo comando. Sem isso o "s" de confirmação virava prompt para a IA.
|
|
369
350
|
let pendingAsk = null;
|
|
@@ -545,6 +526,195 @@ function prepararPasta(userText) {
|
|
|
545
526
|
return plan;
|
|
546
527
|
}
|
|
547
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
|
+
|
|
548
718
|
async function sendPrompt(userText) {
|
|
549
719
|
/* O portão do plano. Sem ele o bloqueio da abertura seria só um texto na
|
|
550
720
|
tela: a pessoa leria "você não tem acesso" e mandaria a primeira coisa
|
|
@@ -607,6 +777,11 @@ async function sendPrompt(userText) {
|
|
|
607
777
|
|
|
608
778
|
let r = null;
|
|
609
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();
|
|
610
785
|
for (let i = 0; i < passos.length; i++) {
|
|
611
786
|
if (i > 0) console.log(c.dim(` ⎿ ${passos[i].titulo}`));
|
|
612
787
|
// Só fato de passo CONCLUÍDO entra aqui. Num pedido real o passo de
|
|
@@ -634,7 +809,10 @@ async function sendPrompt(userText) {
|
|
|
634
809
|
// Só chamado se o agente for mesmo MEXER EM ARQUIVO — abrir o Chrome
|
|
635
810
|
// não é motivo para criar pasta de projeto.
|
|
636
811
|
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
637
|
-
|
|
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,
|
|
638
816
|
});
|
|
639
817
|
|
|
640
818
|
r = await rodarPasso();
|
|
@@ -646,11 +824,18 @@ async function sendPrompt(userText) {
|
|
|
646
824
|
console.log(c.dim(' ⎿ o passo não concluiu — tentando de novo'));
|
|
647
825
|
r = await rodarPasso();
|
|
648
826
|
}
|
|
827
|
+
registro.juntar(livro, r.registro);
|
|
649
828
|
jaFeito = (r.concluiu !== false)
|
|
650
829
|
? String((r.assistantMessage && r.assistantMessage.content) || '').slice(0, 1200)
|
|
651
830
|
: '';
|
|
652
831
|
if (r.parado) break; // Esc Esc no meio do plano para o plano inteiro
|
|
653
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);
|
|
654
839
|
state.history.push({ role: 'user', content: userText });
|
|
655
840
|
state.history.push({ role: 'assistant', content: r.assistantMessage.content });
|
|
656
841
|
state.lastResponse = r.assistantMessage.content;
|
|
@@ -695,30 +880,39 @@ async function sendPrompt(userText) {
|
|
|
695
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;
|
|
696
881
|
const RE_CRIAR_DEPOIS = /\b(cri[ae]|faz|faça|mont[ae]|ger[ae]|escrev[ae]|produz|desenvolv)\w*/i;
|
|
697
882
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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;
|
|
704
896
|
|
|
705
897
|
async function baixarCriacaoDireto(texto) {
|
|
706
898
|
if (!(RE_BAIXAR.test(texto) && RE_PECA_STUDIO.test(texto))) return false;
|
|
707
899
|
try {
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if (!
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
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í.`));
|
|
719
913
|
return true;
|
|
720
914
|
} catch {
|
|
721
|
-
return false; //
|
|
915
|
+
return false; // sem projeto renderizado: segue o caminho normal
|
|
722
916
|
}
|
|
723
917
|
}
|
|
724
918
|
|
|
@@ -827,17 +1021,6 @@ async function runChat(promptForModel, userTextForMemory) {
|
|
|
827
1021
|
|
|
828
1022
|
// ── Slash commands ─────────────────────────────────────────────────────
|
|
829
1023
|
|
|
830
|
-
/* Qual motor executa este turno.
|
|
831
|
-
*
|
|
832
|
-
* Um `if` a mais aqui e um `if` a menos em qualquer outro lugar seria o
|
|
833
|
-
* caminho para o /codex funcionar no modo agente e não no chat, ou ao
|
|
834
|
-
* contrário. Um lugar só decide, e os dois modos chamam este. */
|
|
835
|
-
function motorDaVez() {
|
|
836
|
-
if (state.engine === 'claude-code') return claudeEngine;
|
|
837
|
-
if (state.engine === 'codex') return codexEngine;
|
|
838
|
-
return api;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
1024
|
function isSlash(text) { return text.trim().startsWith('/'); }
|
|
842
1025
|
|
|
843
1026
|
/* ── OS NOMES ANTIGOS ────────────────────────────────────────────────────
|
|
@@ -861,8 +1044,6 @@ const RENOMEADOS = {
|
|
|
861
1044
|
entrar: 'connect', login: 'connect', conectar: 'connect',
|
|
862
1045
|
conta: 'account',
|
|
863
1046
|
sair: 'exit', // sair é SAIR; desconectar a conta é /logout
|
|
864
|
-
// as chaves de API
|
|
865
|
-
chave: 'key', chaves: 'key',
|
|
866
1047
|
// o histórico
|
|
867
1048
|
conversas: 'chats', historico: 'chats', histórico: 'chats',
|
|
868
1049
|
// o resto
|
|
@@ -974,97 +1155,6 @@ async function handleSlash(text) {
|
|
|
974
1155
|
break;
|
|
975
1156
|
}
|
|
976
1157
|
|
|
977
|
-
/* A chave de API da própria pessoa. Guardada em ~/.primocode/
|
|
978
|
-
chaves.json com permissão 600 — nunca no config.json, que é o
|
|
979
|
-
arquivo que se cola num chamado de suporte. */
|
|
980
|
-
case 'key': case 'keys': {
|
|
981
|
-
const [qual, ...resto] = rest.split(/\s+/).filter(Boolean);
|
|
982
|
-
const valor = resto.join(' ');
|
|
983
|
-
|
|
984
|
-
if (!qual) {
|
|
985
|
-
console.log(c.white('\n Suas chaves de API'));
|
|
986
|
-
for (const p of chaves.listar()) {
|
|
987
|
-
console.log(' ' + c.white(p.nome.padEnd(8))
|
|
988
|
-
+ (p.tem ? c.ok(p.disfarce) : c.dim('não configurada'))
|
|
989
|
-
+ c.dim(` ${p.variavel}`));
|
|
990
|
-
}
|
|
991
|
-
console.log(c.dim('\n /key claude sk-ant-… guarda a chave'));
|
|
992
|
-
console.log(c.dim(' /key gpt remover apaga a que estiver lá'));
|
|
993
|
-
console.log(c.muted('\n Sem chave própria, cada motor usa a conta em que você já entrou nele.'));
|
|
994
|
-
break;
|
|
995
|
-
}
|
|
996
|
-
if (!chaves.PROVEDORES[qual]) {
|
|
997
|
-
console.log(c.err(` Não conheço "${qual}". Use: ${Object.keys(chaves.PROVEDORES).join(', ')}`));
|
|
998
|
-
break;
|
|
999
|
-
}
|
|
1000
|
-
if (valor === 'remover' || valor === 'apagar') {
|
|
1001
|
-
const r = chaves.esquecer(qual);
|
|
1002
|
-
console.log(r.ok ? c.ok(` Chave do ${chaves.PROVEDORES[qual].nome} apagada.`)
|
|
1003
|
-
: c.muted(` ${r.error}`));
|
|
1004
|
-
break;
|
|
1005
|
-
}
|
|
1006
|
-
if (!valor) {
|
|
1007
|
-
const p = chaves.PROVEDORES[qual];
|
|
1008
|
-
console.log(c.muted(` Uso: `) + c.brand(`/key ${qual} ${p.prefixo}…`));
|
|
1009
|
-
console.log(c.dim(` Pegue a sua em ${p.onde}`));
|
|
1010
|
-
break;
|
|
1011
|
-
}
|
|
1012
|
-
const r = chaves.guardar(qual, valor);
|
|
1013
|
-
if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
|
|
1014
|
-
// O que se mostra de volta é o disfarce, nunca a chave: a linha
|
|
1015
|
-
// fica no scrollback do terminal e no print que a pessoa manda.
|
|
1016
|
-
console.log(c.ok(` Chave do ${r.nome} guardada.`) + c.muted(` ${r.disfarce}`));
|
|
1017
|
-
console.log(c.dim(` Ela vai como ${chaves.PROVEDORES[qual].variavel} só quando o motor rodar.`));
|
|
1018
|
-
break;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
/* O Codex da conta da pessoa, com o modelo barato.
|
|
1022
|
-
O protocolo é o MESMO do /claude — ver lib/codex-engine.js. */
|
|
1023
|
-
/* `/gpt` e `/codex` são a mesma coisa e existem os dois de propósito:
|
|
1024
|
-
o programa se chama Codex, mas quem usa fala "GPT" — e o comando que
|
|
1025
|
-
a pessoa tenta é o nome que ela tem na cabeça, não o do fabricante. */
|
|
1026
|
-
case 'gpt': case 'codex': {
|
|
1027
|
-
if (rest.toLowerCase() === 'off') {
|
|
1028
|
-
if (state.engine !== 'codex') { 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 o Codex…'));
|
|
1034
|
-
const r = codexEngine.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 = 'codex';
|
|
1041
|
-
console.log(c.ok(` Motor: Codex (${r.modelo})`) + c.muted(' — na sua conta, não na minha'));
|
|
1042
|
-
console.log(c.muted(' /codex off volta pro motor padrão.'));
|
|
1043
|
-
break;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
case 'claude': {
|
|
1047
|
-
if (rest.toLowerCase() === 'off') {
|
|
1048
|
-
if (state.engine !== 'claude-code') { console.log(c.muted(' Já estava no motor padrão.')); break; }
|
|
1049
|
-
state.engine = 'server';
|
|
1050
|
-
console.log(c.ok(' Motor: OpenRouter/Groq (padrão)'));
|
|
1051
|
-
break;
|
|
1052
|
-
}
|
|
1053
|
-
console.log(c.dim(' Checando Claude Code e ORCA…'));
|
|
1054
|
-
const r = await claudeEngine.garantirEngine();
|
|
1055
|
-
if (!r.ok) {
|
|
1056
|
-
console.log(c.err(` ${r.error}`));
|
|
1057
|
-
if (r.comoResolver) console.log(c.muted(` ${r.comoResolver}`));
|
|
1058
|
-
break;
|
|
1059
|
-
}
|
|
1060
|
-
state.engine = 'claude-code';
|
|
1061
|
-
const onde = r.caminho === 'A' ? 'sessão contínua via ORCA' : 'chamada avulsa (claude -p, sem ORCA)';
|
|
1062
|
-
console.log(c.ok(` Motor: Claude Code (${onde})`)
|
|
1063
|
-
+ c.muted(` — ${claudeEngine.MODELO_CLAUDE || 'o padrão da sua conta'}, na sua conta`));
|
|
1064
|
-
console.log(c.muted(' /claude off volta pro motor padrão.'));
|
|
1065
|
-
break;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
1158
|
// O /model virou /effort: você escolhe o ESFORÇO num slider (←/→,
|
|
1069
1159
|
// Enter confirma, Esc cancela) e o modelo sai daí. Com argumento
|
|
1070
1160
|
// ("/effort high") aplica direto, para quem já sabe o que quer.
|
|
@@ -1264,6 +1354,191 @@ async function handleSlash(text) {
|
|
|
1264
1354
|
break;
|
|
1265
1355
|
}
|
|
1266
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
|
+
|
|
1267
1542
|
/* A janela. "Se mandar /rc pra ele, ele abre o aplicativo — para o
|
|
1268
1543
|
Mac e o Windows, já instalado junto com o npm, com as cores do
|
|
1269
1544
|
PrimoCode. Um chat estilo terminal para quem não quer o terminal."
|
|
@@ -1343,11 +1618,6 @@ async function handleSlash(text) {
|
|
|
1343
1618
|
console.log(c.muted(' Já conectado como ') + c.brand(jaEsta.uid.slice(0, 12) + '…')
|
|
1344
1619
|
+ c.muted(` · plano ${jaEsta.plano}`));
|
|
1345
1620
|
console.log(c.dim(' Para entrar com outra conta: ') + c.brand('/connect trocar'));
|
|
1346
|
-
// Sem isto, quem tinha sido barrado na abertura (plano velho,
|
|
1347
|
-
// login que não deu a tempo) e depois só confere com um
|
|
1348
|
-
// `/connect` continuava barrado: este comando não passava por
|
|
1349
|
-
// `entrar()`, e nada reabria o portão.
|
|
1350
|
-
sincronizarLiberacao(jaEsta);
|
|
1351
1621
|
break;
|
|
1352
1622
|
}
|
|
1353
1623
|
if (rest === 'trocar') conta.sair();
|
|
@@ -1357,29 +1627,31 @@ async function handleSlash(text) {
|
|
|
1357
1627
|
abrir: (u) => tools.openInBrowser(u),
|
|
1358
1628
|
contar: (info) => {
|
|
1359
1629
|
sp.stop();
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
* "Pode tirar essa parte no login onde aparece o código
|
|
1363
|
-
* — o terminal mandou o código, pode tirar."
|
|
1364
|
-
*
|
|
1365
|
-
* A página do token já resolve o pareamento sozinha: ela
|
|
1366
|
-
* pergunta o código ao 127.0.0.1 desta máquina, e onde o
|
|
1367
|
-
* navegador bloqueia isso (Safari) ela oferece "Colar" —
|
|
1368
|
-
* o código continua indo para a área de transferência,
|
|
1369
|
-
* que é o caminho sem leitura. Oito letras impressas aqui
|
|
1370
|
-
* eram uma tarefa para a pessoa que o produto já não
|
|
1371
|
-
* precisa, e a tela ficava dizendo que talvez precisasse.
|
|
1372
|
-
*
|
|
1373
|
-
* O código continua existindo no protocolo (lib/conta.js)
|
|
1374
|
-
* — 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;
|
|
1375
1632
|
console.log(c.muted(' Abrindo no navegador:'));
|
|
1376
1633
|
console.log(' ' + c.brand(info.url.replace(/[?&]porta=\d+/, '')));
|
|
1377
1634
|
console.log('');
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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.')));
|
|
1383
1655
|
}
|
|
1384
1656
|
console.log(c.dim(' Esperando o navegador…'));
|
|
1385
1657
|
},
|
|
@@ -1388,11 +1660,6 @@ async function handleSlash(text) {
|
|
|
1388
1660
|
if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
|
|
1389
1661
|
console.log(c.ok(' Conectado.')
|
|
1390
1662
|
+ c.muted(` Plano ${r.plano}${r.pro ? '' : ' — assine Premium no app para liberar tudo'}`));
|
|
1391
|
-
// O QUE FALTAVA: sem isto, "Conectado. Plano premium" na tela e o
|
|
1392
|
-
// PRÓXIMO pedido caindo em "O PrimoCode pede Premium ou Super" —
|
|
1393
|
-
// porque o portão (`state.liberado`) só era decidido na abertura,
|
|
1394
|
-
// e um /connect no meio da sessão nunca o reabria.
|
|
1395
|
-
sincronizarLiberacao(r);
|
|
1396
1663
|
break;
|
|
1397
1664
|
}
|
|
1398
1665
|
|
|
@@ -1450,10 +1717,6 @@ async function handleSlash(text) {
|
|
|
1450
1717
|
conta.sair();
|
|
1451
1718
|
console.log(c.ok(' Desconectado deste computador.')
|
|
1452
1719
|
+ c.dim(' A conta continua intacta; entre de novo com /connect.'));
|
|
1453
|
-
// O mesmo portão do connect: sem login não há como confirmar
|
|
1454
|
-
// Premium/Super, e deixar `state.liberado` de pé seria a mesma
|
|
1455
|
-
// dessincronia ao contrário — trabalhando sem estar conectado.
|
|
1456
|
-
sincronizarLiberacao({ pro: false, motivo: 'sem-login' });
|
|
1457
1720
|
break;
|
|
1458
1721
|
}
|
|
1459
1722
|
|
|
@@ -1501,6 +1764,25 @@ async function handleSlash(text) {
|
|
|
1501
1764
|
// rl.close() em vez de process.exit(): sair na marra descarta o que
|
|
1502
1765
|
// ainda estava no buffer de saída.
|
|
1503
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
|
+
|
|
1504
1786
|
default: {
|
|
1505
1787
|
// "/" sozinho abre o menu; um prefixo único autopreenche e executa
|
|
1506
1788
|
// ("/eff" → /effort); prefixo ambíguo mostra as opções.
|
|
@@ -1645,7 +1927,11 @@ async function main() {
|
|
|
1645
1927
|
userName: state.config.userName,
|
|
1646
1928
|
pastaPadrao: state.workDirOrigem === 'padrao',
|
|
1647
1929
|
}));
|
|
1648
|
-
rl =
|
|
1930
|
+
rl = entrada.criar({
|
|
1931
|
+
status: barraDeStatus,
|
|
1932
|
+
comandos: () => SLASH_COMANDOS,
|
|
1933
|
+
vazio: 'peça alguma coisa, ou / para os comandos',
|
|
1934
|
+
});
|
|
1649
1935
|
|
|
1650
1936
|
rl.prompt();
|
|
1651
1937
|
|
|
@@ -1656,57 +1942,31 @@ async function main() {
|
|
|
1656
1942
|
let closed = false;
|
|
1657
1943
|
let saidBye = false;
|
|
1658
1944
|
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
let ultimoEsc = 0;
|
|
1685
|
-
process.stdin.on('keypress', (ch, key) => {
|
|
1686
|
-
if (sliderAtivo) return; // o slider é dono do teclado enquanto está no ar
|
|
1687
|
-
if (key && (key.name === 'escape' || key.sequence === '\x1b\x1b')) {
|
|
1688
|
-
if (!busy) { ultimoEsc = 0; return; } // sem agente rodando, Esc é só Esc
|
|
1689
|
-
const agora = Date.now();
|
|
1690
|
-
if (key.sequence === '\x1b\x1b' || agora - ultimoEsc < 2000) {
|
|
1691
|
-
ultimoEsc = 0;
|
|
1692
|
-
interromper();
|
|
1693
|
-
if (chatEmVoo) { try { chatEmVoo.abort(); } catch {} }
|
|
1694
|
-
process.stdout.write('\n' + c.warn(' ⏹ parando o agente…') + '\n');
|
|
1695
|
-
} else {
|
|
1696
|
-
ultimoEsc = agora;
|
|
1697
|
-
process.stdout.write('\n' + c.dim(' Esc de novo para parar o agente') + '\n');
|
|
1698
|
-
}
|
|
1699
|
-
return;
|
|
1700
|
-
}
|
|
1701
|
-
// Seta → no fim da linha aceita a sugestão inteira.
|
|
1702
|
-
if (key && key.name === 'right' && fantasma && rl && rl.cursor === rl.line.length) {
|
|
1703
|
-
const resto = fantasma;
|
|
1704
|
-
fantasma = '';
|
|
1705
|
-
rl.write(resto);
|
|
1706
|
-
}
|
|
1707
|
-
setImmediate(desenharFantasma);
|
|
1708
|
-
});
|
|
1709
|
-
}
|
|
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());
|
|
1710
1970
|
|
|
1711
1971
|
function sayBye() {
|
|
1712
1972
|
if (saidBye) return;
|
|
@@ -1729,6 +1989,10 @@ async function main() {
|
|
|
1729
1989
|
async function drain() {
|
|
1730
1990
|
if (busy) return;
|
|
1731
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);
|
|
1732
1996
|
while (queue.length) {
|
|
1733
1997
|
const text = queue.shift().trim();
|
|
1734
1998
|
if (!text) continue;
|
|
@@ -1740,6 +2004,8 @@ async function main() {
|
|
|
1740
2004
|
}
|
|
1741
2005
|
}
|
|
1742
2006
|
busy = false;
|
|
2007
|
+
state.trabalhando = false;
|
|
2008
|
+
if (rl.trabalhando) rl.trabalhando(false);
|
|
1743
2009
|
// Se o stdin já fechou (pipe, Ctrl+D), a despedida espera a fila terminar
|
|
1744
2010
|
// — senão ela aparece antes do resultado do último pedido.
|
|
1745
2011
|
if (closed) sayBye();
|
|
@@ -1747,10 +2013,6 @@ async function main() {
|
|
|
1747
2013
|
}
|
|
1748
2014
|
|
|
1749
2015
|
rl.on('line', (line) => {
|
|
1750
|
-
// A lista de setinhas (e o slider do /effort) são donos do teclado
|
|
1751
|
-
// enquanto estão no ar: o Enter que confirma a escolha não é uma
|
|
1752
|
-
// linha para a fila, e sem isto ele vira um pedido em branco.
|
|
1753
|
-
if (sliderAtivo) return;
|
|
1754
2016
|
if (pendingAsk) { const resolve = pendingAsk; pendingAsk = null; resolve(line); return; }
|
|
1755
2017
|
// Abertura correndo: guarda em vez de mandar ao agente.
|
|
1756
2018
|
if (segurando) { adiantadas.push(line); return; }
|
|
@@ -1786,31 +2048,7 @@ async function main() {
|
|
|
1786
2048
|
interativo: !!(process.stdin.isTTY && process.stdout.isTTY),
|
|
1787
2049
|
conectado: () => conta.atual(),
|
|
1788
2050
|
conectar: () => handleSlash('/connect').then(() => conta.atual()),
|
|
1789
|
-
/* A ESCOLHA, COM AS SETINHAS — E O QUE FOI DIGITADO ANTES DELA.
|
|
1790
|
-
*
|
|
1791
|
-
* A lista só aparece depois de o plano voltar do servidor, e quem
|
|
1792
|
-
* colou "3⏎1⏎/version" já respondeu antes de ver. Esse texto está em
|
|
1793
|
-
* `adiantadas`; se a lista ignorasse, a resposta se perderia e o
|
|
1794
|
-
* menu ficaria esperando um teclado que já falou. Então: primeiro o
|
|
1795
|
-
* que foi adiantado (se casar com algum item), depois as setinhas. O
|
|
1796
|
-
* que não casa fica na fila — é comando, não resposta. */
|
|
1797
|
-
selecionar: ({ titulo, ajuda, lista }) => {
|
|
1798
|
-
while (adiantadas.length) {
|
|
1799
|
-
const dito = String(adiantadas[0] || '').trim();
|
|
1800
|
-
if (!dito) { adiantadas.shift(); continue; }
|
|
1801
|
-
const qual = boasVindas.escolher(dito, lista);
|
|
1802
|
-
if (!qual) break; // isso é comando, não escolha
|
|
1803
|
-
adiantadas.shift();
|
|
1804
|
-
const item = lista.find((e) => e.id === qual);
|
|
1805
|
-
console.log('');
|
|
1806
|
-
console.log(c.bold(c.brand(' ' + titulo)));
|
|
1807
|
-
console.log(c.brand(' ❯ ') + c.white(item ? item.titulo : qual));
|
|
1808
|
-
return Promise.resolve(qual);
|
|
1809
|
-
}
|
|
1810
|
-
return menuSetas({ titulo, ajuda, lista });
|
|
1811
|
-
},
|
|
1812
2051
|
sessao: () => conta.sessao(),
|
|
1813
|
-
aplicar: (cmd) => handleSlash(cmd),
|
|
1814
2052
|
}).then((r) => {
|
|
1815
2053
|
// O bloqueio de quem não é Premium/Super. É de CORTESIA: este arquivo
|
|
1816
2054
|
// vai publicado no npm com o código à vista. Quem segura de verdade é
|