primocode 11.1.0 → 11.3.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 +16 -2
- package/bin/primocode-rpc.js +100 -0
- package/lib/agentes-pessoais.js +120 -0
- package/lib/app-server/cli.js +64 -0
- package/lib/app-server/enderecos.js +43 -0
- package/lib/app-server/index.js +3 -0
- package/lib/app-server/pareamento.js +61 -0
- package/lib/app-server/protocolo.js +86 -0
- package/lib/app-server/servidor.js +805 -0
- package/lib/app-server/websocket.js +87 -0
- package/lib/avisos.js +65 -0
- package/lib/cli/sessao.js +202 -17
- package/lib/cli-nova.js +237 -0
- package/lib/config/config.js +22 -2
- package/lib/diff.js +12 -2
- package/lib/motor/aprovacao.js +114 -0
- package/lib/motor/eventos.js +77 -0
- package/lib/motor/index.js +8 -0
- package/lib/motor/tela-terminal.js +64 -0
- package/lib/motor/transcricao.js +57 -0
- package/lib/motor/turno.js +43 -0
- package/lib/motor/voz.js +65 -0
- package/lib/piloto.js +39 -0
- package/lib/plugin/entregaveis/nucleo.js +2 -2
- package/lib/plugin/estudio/criar.js +113 -0
- package/lib/plugin/estudio/gosto.js +3 -0
- package/lib/plugin/estudio/historico.js +60 -0
- package/lib/plugin/estudio/index.js +14 -1
- package/lib/plugin/estudio/kits/tela.html +3 -1
- package/lib/plugin/estudio/miniatura.js +56 -0
- package/lib/plugin/video/auto.js +1 -1
- package/lib/plugin/video/catalogo.js +20 -0
- package/lib/plugin/video/mutar.js +28 -0
- package/lib/plugin/video/ops-fx.js +6 -6
- package/lib/plugin/video/ops.js +2 -13
- package/lib/plugin/video/pedido.js +1 -1
- package/lib/plugin/video/zoom.js +1 -1
- package/lib/projetos-meta.js +107 -0
- package/lib/qr.js +209 -0
- package/lib/session/runner/act.js +127 -68
- package/lib/skill/catalogo.js +3 -1
- package/lib/tool/tools.js +7 -0
- package/lib/tui/entrada.js +143 -18
- package/lib/tui/tela.js +312 -49
- package/lib/tui/ui.js +78 -4
- package/package.json +10 -2
- package/voz/primo_voz/transcrever.py +12 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// WebSocket mínimo (RFC 6455) feito na mão: handshake, frames de texto,
|
|
2
|
+
// ping/pong e close. Sem pacote. Só o que o app-server precisa.
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
8
|
+
const MAX_PAYLOAD = 16 * 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
const aceite = (chave) => crypto.createHash('sha1').update(chave + GUID).digest('base64');
|
|
11
|
+
|
|
12
|
+
/* Frame do servidor (nunca mascarado). */
|
|
13
|
+
function frame(opcode, payload) {
|
|
14
|
+
const corpo = Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload || ''), 'utf8');
|
|
15
|
+
const n = corpo.length;
|
|
16
|
+
let cab;
|
|
17
|
+
if (n < 126) cab = Buffer.from([0x80 | opcode, n]);
|
|
18
|
+
else if (n < 65536) { cab = Buffer.alloc(4); cab[0] = 0x80 | opcode; cab[1] = 126; cab.writeUInt16BE(n, 2); }
|
|
19
|
+
else { cab = Buffer.alloc(10); cab[0] = 0x80 | opcode; cab[1] = 127; cab.writeBigUInt64BE(BigInt(n), 2); }
|
|
20
|
+
return Buffer.concat([cab, corpo]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Responde ao upgrade. Devolve a conexão { enviar, fechar }; o resto vem por callbacks. */
|
|
24
|
+
function aceitar(req, socket, { aoTexto, aoFechar }) {
|
|
25
|
+
const chave = req.headers['sec-websocket-key'];
|
|
26
|
+
socket.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n'
|
|
27
|
+
+ `Sec-WebSocket-Accept: ${aceite(chave)}\r\n\r\n`);
|
|
28
|
+
socket.setNoDelay(true);
|
|
29
|
+
|
|
30
|
+
let buf = Buffer.alloc(0);
|
|
31
|
+
let fragmentos = [];
|
|
32
|
+
let fechado = false;
|
|
33
|
+
|
|
34
|
+
const fechar = (codigo = 1000) => {
|
|
35
|
+
if (fechado) return;
|
|
36
|
+
fechado = true;
|
|
37
|
+
const c = Buffer.alloc(2); c.writeUInt16BE(codigo, 0);
|
|
38
|
+
try { socket.write(frame(0x8, c)); } catch { /* socket já morreu */ }
|
|
39
|
+
try { socket.end(); } catch { /* idem */ }
|
|
40
|
+
if (aoFechar) aoFechar();
|
|
41
|
+
};
|
|
42
|
+
const enviar = (texto) => { if (!fechado) { try { socket.write(frame(0x1, texto)); } catch { /* idem */ } } };
|
|
43
|
+
|
|
44
|
+
const processar = () => {
|
|
45
|
+
for (;;) {
|
|
46
|
+
if (buf.length < 2) return;
|
|
47
|
+
const fin = (buf[0] & 0x80) !== 0;
|
|
48
|
+
const opcode = buf[0] & 0x0f;
|
|
49
|
+
const mascarado = (buf[1] & 0x80) !== 0;
|
|
50
|
+
let n = buf[1] & 0x7f;
|
|
51
|
+
let pos = 2;
|
|
52
|
+
if (n === 126) { if (buf.length < 4) return; n = buf.readUInt16BE(2); pos = 4; }
|
|
53
|
+
else if (n === 127) {
|
|
54
|
+
if (buf.length < 10) return;
|
|
55
|
+
const grande = buf.readBigUInt64BE(2);
|
|
56
|
+
if (grande > BigInt(MAX_PAYLOAD)) return fechar(1009);
|
|
57
|
+
n = Number(grande); pos = 10;
|
|
58
|
+
}
|
|
59
|
+
if (n > MAX_PAYLOAD) return fechar(1009);
|
|
60
|
+
// Cliente sempre mascara (RFC): sem máscara é protocolo quebrado.
|
|
61
|
+
if (!mascarado) return fechar(1002);
|
|
62
|
+
if (buf.length < pos + 4 + n) return;
|
|
63
|
+
const mascara = buf.subarray(pos, pos + 4);
|
|
64
|
+
const dados = Buffer.from(buf.subarray(pos + 4, pos + 4 + n));
|
|
65
|
+
for (let i = 0; i < n; i++) dados[i] ^= mascara[i % 4];
|
|
66
|
+
buf = buf.subarray(pos + 4 + n);
|
|
67
|
+
|
|
68
|
+
if (opcode === 0x8) return fechar(1000);
|
|
69
|
+
if (opcode === 0x9) { try { socket.write(frame(0xA, dados)); } catch { /* idem */ } continue; }
|
|
70
|
+
if (opcode === 0xA) continue;
|
|
71
|
+
if (opcode === 0x1 || opcode === 0x0) {
|
|
72
|
+
if (opcode === 0x1) fragmentos = [];
|
|
73
|
+
fragmentos.push(dados);
|
|
74
|
+
if (fin) { const t = Buffer.concat(fragmentos).toString('utf8'); fragmentos = []; aoTexto(t); }
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
return fechar(1003); // binário e o resto: não suportado
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
socket.on('data', (d) => { buf = Buffer.concat([buf, d]); processar(); });
|
|
82
|
+
socket.on('close', () => { if (!fechado) { fechado = true; if (aoFechar) aoFechar(); } });
|
|
83
|
+
socket.on('error', () => { /* o close vem em seguida */ });
|
|
84
|
+
return { enviar, fechar };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { aceitar, frame, aceite, GUID };
|
package/lib/avisos.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Avisos e fila de aprovação: ~/.primocode/avisos.json (docs/estrutura.md, seções 7 e 9). Node puro.
|
|
2
|
+
// tipo: "aviso" (o agente terminou algo, ou deu erro) | "aprovacao" (espera sim ou não).
|
|
3
|
+
// estado: "novo" → "lido"; aprovação: "pendente" → "resolvido".
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const MAX = 200;
|
|
11
|
+
const arquivo = (a) => a || process.env.PRIMOCODE_AVISOS || path.join(os.homedir(), '.primocode', 'avisos.json');
|
|
12
|
+
|
|
13
|
+
function ler({ arquivo: a } = {}) {
|
|
14
|
+
try { const l = JSON.parse(fs.readFileSync(arquivo(a), 'utf8')); return Array.isArray(l) ? l : []; } catch { return []; }
|
|
15
|
+
}
|
|
16
|
+
function gravar(lista, a) {
|
|
17
|
+
const f = arquivo(a);
|
|
18
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
19
|
+
fs.writeFileSync(f, JSON.stringify(lista.slice(0, MAX), null, 1), { mode: 0o600 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function criar(dados, { arquivo: a } = {}) {
|
|
23
|
+
const aprov = dados.tipo === 'aprovacao';
|
|
24
|
+
const av = {
|
|
25
|
+
id: `n${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
26
|
+
tipo: aprov ? 'aprovacao' : 'aviso',
|
|
27
|
+
estado: aprov ? 'pendente' : 'novo',
|
|
28
|
+
nivel: dados.nivel === 'erro' ? 'erro' : 'ok',
|
|
29
|
+
texto: String(dados.texto || '').slice(0, 600),
|
|
30
|
+
detalhe: dados.detalhe ? String(dados.detalhe).slice(0, 300) : null,
|
|
31
|
+
agente: dados.agente || null, thread: dados.thread || null, aprovacao: dados.aprovacao || null,
|
|
32
|
+
criado: Date.now(),
|
|
33
|
+
};
|
|
34
|
+
gravar([av, ...ler({ arquivo: a })], a);
|
|
35
|
+
return av;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const contagem = (l) => ({
|
|
39
|
+
naoLidos: l.filter((x) => x.estado === 'novo').length,
|
|
40
|
+
praAprovar: l.filter((x) => x.estado === 'pendente').length,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function listar({ arquivo: a } = {}) { const l = ler({ arquivo: a }); return { avisos: l, ...contagem(l) }; }
|
|
44
|
+
|
|
45
|
+
/* Marca como lido um aviso (id) ou todos. Aprovação pendente não some ao ler: só ao responder. */
|
|
46
|
+
function marcarLido({ id, todos } = {}, { arquivo: a } = {}) {
|
|
47
|
+
const l = ler({ arquivo: a });
|
|
48
|
+
let achou = false;
|
|
49
|
+
for (const x of l) if ((todos || x.id === id) && x.estado === 'novo') { x.estado = 'lido'; achou = true; }
|
|
50
|
+
if (!todos && !l.some((x) => x.id === id)) throw new Error(`não achei o aviso ${id}`);
|
|
51
|
+
if (achou) gravar(l, a);
|
|
52
|
+
return { avisos: l, ...contagem(l) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* A aprovação foi respondida (pelo cartão ou pelo aviso). */
|
|
56
|
+
function resolverAprovacao(aprovacaoId, resposta, { arquivo: a } = {}) {
|
|
57
|
+
const l = ler({ arquivo: a });
|
|
58
|
+
const x = l.find((n) => n.tipo === 'aprovacao' && n.aprovacao === aprovacaoId && n.estado === 'pendente');
|
|
59
|
+
if (!x) return null;
|
|
60
|
+
x.estado = 'resolvido'; x.resposta = resposta || null;
|
|
61
|
+
gravar(l, a);
|
|
62
|
+
return x;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { ler, criar, listar, marcarLido, resolverAprovacao, contagem };
|
package/lib/cli/sessao.js
CHANGED
|
@@ -13,6 +13,7 @@ const os = require('os');
|
|
|
13
13
|
|
|
14
14
|
const pkg = require('../../package.json');
|
|
15
15
|
const ui = require('../tui/ui');
|
|
16
|
+
const tela = require('../tui/tela.js');
|
|
16
17
|
const entrada = require('../tui/entrada');
|
|
17
18
|
const { c, welcomeBox, Spinner } = ui;
|
|
18
19
|
const { escolherModo } = require('../modo.js');
|
|
@@ -242,6 +243,8 @@ const SLASH_COMANDOS = [
|
|
|
242
243
|
['/version', 'versão'],
|
|
243
244
|
['/exit', 'sair'],
|
|
244
245
|
];
|
|
246
|
+
// A CLI nova (lib/cli-nova.js): /estudio /agentes /piloto /avisos /buscar entram no menu e no autopreenchimento.
|
|
247
|
+
SLASH_COMANDOS.splice(Math.max(0, SLASH_COMANDOS.findIndex(([n]) => n === '/marca')), 0, ...require('../cli-nova.js').SLASH_NOVOS);
|
|
245
248
|
|
|
246
249
|
/* /api: a chave da própria pessoa — lib/cli/comando-api.js. */
|
|
247
250
|
const comandoApi = require('./comando-api.js').criar({ rl: () => rl, ask: (...a) => ask(...a), askSecret: (...a) => askSecret(...a) });
|
|
@@ -440,9 +443,12 @@ function barraDeStatus() {
|
|
|
440
443
|
// Modelo que respondeu · contexto · tokens de hoje (os mesmos do /usage) — lib/cli/statusline.js.
|
|
441
444
|
const SL = require('./statusline.js');
|
|
442
445
|
direita.push(...SL.partes({ modelo: SL.modelo(), ctxPct: uso.pct, tokensHoje: SL.tokensDeHoje(), c }));
|
|
443
|
-
|
|
446
|
+
const aprovar = require('../cli-nova.js').praAprovar();
|
|
447
|
+
if (aprovar) direita.push(c.warn(`${aprovar} pra aprovar`));
|
|
448
|
+
const atalhos = state.trabalhando && rl && rl.ocupado
|
|
444
449
|
? c.warn('trabalhando') + c.dim(' · ') + c.white('esc esc') + c.dim(' para')
|
|
445
|
-
: c.white('tab') + c.dim(' modos ') + c.white('ctrl+p') + c.dim(' comandos')
|
|
450
|
+
: c.white('tab') + c.dim(' modos ') + c.white('ctrl+p') + c.dim(' comandos');
|
|
451
|
+
direita.push(atalhos);
|
|
446
452
|
|
|
447
453
|
/* O modo DENTRO do painel. No auto, depois do primeiro pedido, ele diz
|
|
448
454
|
qual modo acabou respondendo — senão você nunca sabe quem respondeu. */
|
|
@@ -451,9 +457,12 @@ function barraDeStatus() {
|
|
|
451
457
|
if (state.mode === 'auto' && state.modoEfetivo) {
|
|
452
458
|
modo += c.dim(' → ') + c.muted(NOME_MODO[state.modoEfetivo] || state.modoEfetivo);
|
|
453
459
|
}
|
|
454
|
-
|
|
460
|
+
/* "Build · Primo Max · esforço alto": o modo, o plano da conta ("Primo Max") e o esforço. */
|
|
461
|
+
let planoNome = '';
|
|
462
|
+
try { const eu = conta.atual(); if (eu && eu.plano && eu.plano !== 'free') planoNome = ' ' + String(eu.plano).replace(/^./, (x) => x.toUpperCase()); } catch { /* sem conta */ }
|
|
463
|
+
modo += c.dim(' · ') + c.white('Primo' + planoNome) + c.dim(' · esforço ') + c.white(effort.normalizar(state.config.effort));
|
|
455
464
|
|
|
456
|
-
return { modo, cor, esquerda: partes.join(c.dim(' · ')), direita: direita.join(c.dim(' ')) };
|
|
465
|
+
return { modo, cor, esquerda: partes.join(c.dim(' · ')), direita: direita.join(c.dim(' ')), atalhos };
|
|
457
466
|
}
|
|
458
467
|
|
|
459
468
|
/* A barra da direita da tela cheia, como a do OpenCode: o contexto (quantos
|
|
@@ -464,20 +473,61 @@ function barraLateral() {
|
|
|
464
473
|
const dir = state.workDir || process.cwd();
|
|
465
474
|
const ramo = ramoDoGit(dir);
|
|
466
475
|
const m = state.mode === 'auto' && state.modoEfetivo ? state.modoEfetivo : state.mode;
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
{
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
+
const larg = 22;
|
|
477
|
+
const secoes = [];
|
|
478
|
+
|
|
479
|
+
// Contexto: quanto da conversa já foi gasto, com a barra.
|
|
480
|
+
secoes.push({ titulo: 'Contexto', linhas: [
|
|
481
|
+
c.muted(`${uso.tokens.toLocaleString('pt-BR')} tokens · ${uso.pct}%`),
|
|
482
|
+
tela.barraDe(uso.pct, larg, uso.pct >= 100 ? c.err : uso.pct >= 70 ? c.warn : c.brand),
|
|
483
|
+
] });
|
|
484
|
+
|
|
485
|
+
/* Plano: o plano da CONTA (o que o login confirmou) e o gasto de hoje contra o limite que a
|
|
486
|
+
pessoa ensinou (/usage limite). O servidor não informa o limite da semana, então ele não é inventado. */
|
|
487
|
+
try {
|
|
488
|
+
const eu = conta.atual();
|
|
489
|
+
const L = require('../config/limite.js');
|
|
490
|
+
const hoje = L.usoDeHoje().tokens;
|
|
491
|
+
const lim = L.limiteConhecido();
|
|
492
|
+
const linhas = [];
|
|
493
|
+
if (eu && eu.plano) linhas.push(c.muted(`Plano ${String(eu.plano).replace(/^./, (x) => x.toUpperCase())}`));
|
|
494
|
+
if (lim) {
|
|
495
|
+
const pct = Math.round((hoje / lim) * 100);
|
|
496
|
+
linhas.push(c.muted(`${pct}% do limite de hoje`), tela.barraDe(pct, larg, pct >= 100 ? c.err : pct >= 70 ? c.warn : c.brand));
|
|
497
|
+
} else linhas.push(c.dim(`${hoje.toLocaleString('pt-BR')} tokens hoje`));
|
|
498
|
+
secoes.push({ titulo: 'Plano', linhas });
|
|
499
|
+
} catch { /* o plano nunca derruba a tela */ }
|
|
500
|
+
|
|
501
|
+
secoes.push({ titulo: 'Modo', linhas: [
|
|
502
|
+
`${COR_MODO[m] || COR_MODO.auto}${NOME_MODO[m] || m}${c.reset}` + c.muted(` · esforço ${effort.normalizar(state.config.effort)}`),
|
|
503
|
+
] });
|
|
504
|
+
|
|
505
|
+
// Tarefas: o write_todos do agente.
|
|
506
|
+
const todos = ui.estadoSessao.todos || [];
|
|
507
|
+
if (todos.length) {
|
|
508
|
+
const marca = { completed: c.ok('✓'), in_progress: c.brand('●'), pending: c.dim('○') };
|
|
509
|
+
secoes.push({ titulo: 'Tarefas', linhas: todos.slice(0, 8).map((t) => `${marca[t.status] || c.dim('○')} ` + (t.status === 'completed' ? c.dim(t.content) : t.status === 'in_progress' ? c.white(t.content) : c.muted(t.content))) });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Arquivos alterados, com +N −M.
|
|
513
|
+
if (ui.estadoSessao.arquivos.size) {
|
|
514
|
+
const arqs = [...ui.estadoSessao.arquivos.entries()].slice(-8);
|
|
515
|
+
secoes.push({ titulo: 'Arquivos alterados', linhas: arqs.map(([f, k]) =>
|
|
516
|
+
c.muted(path.basename(f)) + ' ' + (k.mais ? c.ok('+' + k.mais) : '') + (k.menos ? ' ' + c.err('−' + k.menos) : '')) });
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// MCP: os servidores configurados (o padrão, o do usuário, o do projeto).
|
|
520
|
+
try {
|
|
521
|
+
const nomes = Object.keys(require('../mcp/mcp.js').servidores(dir));
|
|
522
|
+
if (nomes.length) secoes.push({ titulo: 'MCP', linhas: nomes.slice(0, 5).map((n) => c.ok('●') + ' ' + c.muted(n)) });
|
|
523
|
+
} catch { /* sem MCP, sem seção */ }
|
|
524
|
+
|
|
476
525
|
if (state.attachedFiles.length) {
|
|
477
526
|
secoes.push({ titulo: 'Anexos', linhas: state.attachedFiles.map((f) => c.muted(path.basename(f.path))) });
|
|
478
527
|
}
|
|
479
528
|
if (state.controle && state.controle.liberado) secoes.push({ titulo: 'Controle do PC', linhas: [c.warn('liberado até fechar')] });
|
|
480
529
|
return {
|
|
530
|
+
subtitulo: state.currentProject ? state.currentProject : undefined,
|
|
481
531
|
secoes,
|
|
482
532
|
rodape: [
|
|
483
533
|
c.muted(shortPath(dir)) + (ramo ? c.white(':' + ramo) : ''),
|
|
@@ -486,6 +536,83 @@ function barraLateral() {
|
|
|
486
536
|
};
|
|
487
537
|
}
|
|
488
538
|
|
|
539
|
+
/* ── A PALETA (ctrl+p) E OS DIÁLOGOS ─────────────────────────────────────
|
|
540
|
+
* O desenho é do OpenCode: uma lista com busca no meio da tela. O conteúdo é
|
|
541
|
+
* o que já existe aqui: os comandos de barra, os modos, os agentes da pessoa,
|
|
542
|
+
* as conversas salvas e os temas. Escolher um item roda o comando de sempre. */
|
|
543
|
+
const ATALHOS_DO_LIDER = { '/clear': 'ctrl+x n', '/projects': 'ctrl+x p', '/estudio': 'ctrl+x s' };
|
|
544
|
+
|
|
545
|
+
function paletaDeComandos() {
|
|
546
|
+
const itens = [
|
|
547
|
+
{ secao: 'Sugeridos', nome: 'Nova conversa', cmd: '/clear', tecla: 'ctrl+x n' },
|
|
548
|
+
{ secao: 'Sugeridos', nome: 'Trocar o modo e o esforço', desc: 'auto · primo code · chat', tecla: 'ctrl+x m', acao: (r) => r.abrirDialogo(dialogoModelo()) },
|
|
549
|
+
{ secao: 'Sugeridos', nome: 'Agentes', desc: 'ligar e desligar', tecla: 'ctrl+x a', acao: (r) => r.abrirDialogo(dialogoAgentes()) },
|
|
550
|
+
{ secao: 'Sugeridos', nome: 'Conversas salvas', tecla: 'ctrl+x l', acao: (r) => dialogoSessoes().then((d) => d && r.abrirDialogo(d)) },
|
|
551
|
+
{ secao: 'Sugeridos', nome: 'Tema', desc: 'cor de destaque', tecla: 'ctrl+x t', acao: (r) => r.abrirDialogo(dialogoTema()) },
|
|
552
|
+
{ secao: 'Sugeridos', nome: 'Mostrar ou esconder a barra da direita', tecla: 'ctrl+x b', acao: (r) => { r.lateralOculta = !r.lateralOculta; r._quadroAnterior = null; r._pintar(); } },
|
|
553
|
+
];
|
|
554
|
+
for (const [nome, desc] of comandosDoMenu()) {
|
|
555
|
+
if (/^(\/uninstall|!comando|\/voice )/.test(nome) || nome.includes(' ') || nome === '/clear') continue;
|
|
556
|
+
itens.push({ secao: 'Comandos', nome, desc, cmd: nome, tecla: ATALHOS_DO_LIDER[nome] || '', preencher: /\s<|<.*>/.test(desc) && /^\/(dir|worktree|rule|lock|attach|read|run|open|history|howto)$/.test(nome) });
|
|
557
|
+
}
|
|
558
|
+
return itens;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function dialogoModelo() {
|
|
562
|
+
const atual = state.mode;
|
|
563
|
+
const esforco = effort.normalizar(state.config.effort);
|
|
564
|
+
const itens = [
|
|
565
|
+
['auto', 'Auto', 'o PrimoCode escolhe entre fazer e conversar'],
|
|
566
|
+
['primocode', 'Primo Code', 'o agente que cria e edita arquivos'],
|
|
567
|
+
['chat', 'Chat', 'só conversa, sem mexer em nada'],
|
|
568
|
+
].map(([id, nome, desc]) => ({ secao: 'Modo', nome: (atual === id ? '● ' : ' ') + nome, desc, id }));
|
|
569
|
+
for (const e of ['low', 'medium', 'high', 'ultracode']) itens.push({ secao: 'Esforço', nome: (esforco === effort.normalizar(e) ? '● ' : ' ') + effort.normalizar(e), desc: e === 'ultracode' ? 'o máximo' : '', esforco: e });
|
|
570
|
+
return {
|
|
571
|
+
titulo: 'Modo e esforço', itens,
|
|
572
|
+
aoEscolher: (it, r) => {
|
|
573
|
+
if (it.id) { state.mode = it.id; state.modoEfetivo = null; cfg.saveConfig({ mode: it.id }); r.toast(`Modo: ${NOME_MODO[it.id]}`, 'ok'); }
|
|
574
|
+
else { cfg.saveConfig({ effort: it.esforco }); state.config.effort = it.esforco; r.toast(`Esforço: ${effort.normalizar(it.esforco)}`, 'ok'); }
|
|
575
|
+
refreshPrompt();
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function dialogoAgentes() {
|
|
581
|
+
let lista = [];
|
|
582
|
+
try { lista = require('../agentes-pessoais').listar({ dir: state.workDir }); } catch { lista = []; }
|
|
583
|
+
return {
|
|
584
|
+
titulo: 'Agentes',
|
|
585
|
+
vazio: 'Nenhum agente. /agentes criar <nome> | <o que faz> | <ofício>',
|
|
586
|
+
itens: lista.map((a) => ({ nome: (a.ligado ? '● ' : '○ ') + '@' + a.nome, desc: a.descricao || a.oficio || '', agente: a })),
|
|
587
|
+
aoEscolher: (it, r) => {
|
|
588
|
+
try {
|
|
589
|
+
const a = require('../agentes-pessoais').alternar(it.agente.nome, !it.agente.ligado, { dir: state.workDir });
|
|
590
|
+
r.toast(`@${a.nome} ${a.ligado ? 'ligado' : 'desligado'}`, 'ok');
|
|
591
|
+
} catch (e) { r.toast(e.message, 'erro'); }
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function dialogoSessoes() {
|
|
597
|
+
if (!conta.atual()) { if (rl) rl.toast('Entre na conta (/login) para ver as conversas salvas', 'aviso'); return null; }
|
|
598
|
+
const l = await nuvem.listarConversas({ limite: 30 });
|
|
599
|
+
if (!l.ok || !l.conversas.length) { if (rl) rl.toast(l.ok ? 'Nenhuma conversa salva ainda' : l.error, 'aviso'); return null; }
|
|
600
|
+
return {
|
|
601
|
+
titulo: 'Conversas',
|
|
602
|
+
itens: l.conversas.map((x, i) => ({ nome: x.titulo.slice(0, 44), desc: `${x.mensagens} msg`, tecla: x.atualizada ? new Date(x.atualizada).toLocaleDateString('pt-BR') : '', n: i + 1 })),
|
|
603
|
+
aoEscolher: (it, r) => { r.line = '/resume ' + it.n; r.cursor = r.line.length; r._enviar(); },
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function dialogoTema() {
|
|
608
|
+
const atual = ui.temaDoMomento();
|
|
609
|
+
return {
|
|
610
|
+
titulo: 'Tema',
|
|
611
|
+
itens: Object.entries(ui.TEMAS).map(([id, t]) => ({ nome: (atual === id ? '● ' : ' ') + t.nome, id })),
|
|
612
|
+
aoEscolher: (it, r) => { ui.definirTema(it.id); cfg.saveConfig({ tema: it.id }); r._quadroAnterior = null; r._pintar(); r.toast(`Tema: ${ui.TEMAS[it.id].nome}`, 'ok'); },
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
489
616
|
/* O Tab com a caixa vazia gira o modo, como o "tab agents" do OpenCode:
|
|
490
617
|
auto → primo code → chat → auto. A escolha fica salva, como a do /mode. */
|
|
491
618
|
function trocarModo() {
|
|
@@ -503,6 +630,7 @@ function refreshPrompt() { if (rl) rl.prompt(); }
|
|
|
503
630
|
// Enquanto uma pergunta está no ar, a próxima linha digitada é a RESPOSTA dela
|
|
504
631
|
// — não um novo comando. Sem isso o "s" de confirmação virava prompt para a IA.
|
|
505
632
|
let pendingAsk = null;
|
|
633
|
+
let permissaoPendente = null; // resolve a pergunta de permissão quando a pessoa aperta Esc
|
|
506
634
|
|
|
507
635
|
/* ── O QUE A PESSOA ADIANTA ──────────────────────────────────────────────
|
|
508
636
|
* Enquanto a abertura corre — e ela demora, porque confere o plano no
|
|
@@ -918,6 +1046,30 @@ async function prepararPasta(userText) {
|
|
|
918
1046
|
* entregar o mouse e o teclado.
|
|
919
1047
|
*/
|
|
920
1048
|
async function autorizarControle(oQueVaiFazer, { primeira, tipo } = {}) {
|
|
1049
|
+
/* NA TELA CHEIA a pergunta é um PAINEL AMARELO no meio da conversa (o de permissão do OpenCode):
|
|
1050
|
+
"enter sim · a sempre · esc não". Só o /perms perguntar tem Enter = sim, porque a pessoa pediu para
|
|
1051
|
+
ser perguntada naquele projeto. O primeiro pedido de controle do computador e o que "não tem volta"
|
|
1052
|
+
mantêm o Enter = NÃO (o padrão de quem apertou Enter sem ler não entrega o mouse). */
|
|
1053
|
+
if (rl && rl.cheia) {
|
|
1054
|
+
const enterSim = !!tipo && tipo !== 'perigo';
|
|
1055
|
+
const titulo = tipo === 'perigo' ? 'Isto não tem volta' : primeira ? 'O Primo quer usar o seu computador' : 'Permissão';
|
|
1056
|
+
const w = Math.max(30, (process.stdout.columns || 80) - 2);
|
|
1057
|
+
const teclas = enterSim
|
|
1058
|
+
? c.white('enter') + c.dim(' sim ') + c.white('a') + c.dim(' sempre ') + c.white('esc') + c.dim(' não')
|
|
1059
|
+
: c.white('1') + c.dim(' sim, agora ') + c.white('a') + c.dim(' sempre ') + c.white('enter') + c.dim('/') + c.white('esc') + c.dim(' não');
|
|
1060
|
+
console.log('');
|
|
1061
|
+
console.log(tela.painelCor([c.warn(titulo) + ' ' + c.white(oQueVaiFazer), teclas], 'warn', w).join('\n'));
|
|
1062
|
+
const dito = String(await Promise.race([
|
|
1063
|
+
ask(''),
|
|
1064
|
+
new Promise((res) => { permissaoPendente = () => res('esc'); }),
|
|
1065
|
+
])).trim().toLowerCase().replace(/[.!?,;:…]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
1066
|
+
permissaoPendente = null;
|
|
1067
|
+
if (dito === 'esc') { pendingAsk = null; console.log(' ' + c.muted('Não autorizado.') + c.dim(' Ele vai resolver por outro caminho.')); return 'nao'; }
|
|
1068
|
+
if (dito === 'a' || /sess|sempre|tudo|todas|^2$/.test(dito)) { console.log(' ' + c.ok('✓ autorizado') + c.dim(' — esta sessão inteira. Fechar o PrimoCode esquece.')); return 'sessao'; }
|
|
1069
|
+
if (dito === '1' || /^(s|sim|ok|pode|agora|y|yes|claro|vai)\b/.test(dito) || (dito === '' && enterSim)) { console.log(' ' + c.ok('✓ autorizado') + c.dim(' — só esta ação')); return 'agora'; }
|
|
1070
|
+
console.log(' ' + c.muted('Não autorizado.') + c.dim(' Ele vai resolver por outro caminho.'));
|
|
1071
|
+
return 'nao';
|
|
1072
|
+
}
|
|
921
1073
|
/* O mesmo portão serve ao /permissoes (lib/permission/perigo.js): a pergunta muda de
|
|
922
1074
|
cabeçalho, as três respostas são as mesmas. */
|
|
923
1075
|
if (tipo) {
|
|
@@ -1190,7 +1342,10 @@ async function sendPrompt(userText, { digitado = false } = {}) {
|
|
|
1190
1342
|
const m = state.mode === 'auto' ? (state.modoEfetivo || 'auto') : state.mode;
|
|
1191
1343
|
if (!state.semTerminal) {
|
|
1192
1344
|
console.log('\n ' + `${COR_MODO[m] || COR_MODO.auto}▣${c.reset} ` + c.white(NOME_MODO[m] || m)
|
|
1193
|
-
+ c.dim(` · ${effort.normalizar(state.config.effort)} · ${((Date.now() - inicio) / 1000).toFixed(1)}s`)
|
|
1345
|
+
+ c.dim(` · ${effort.normalizar(state.config.effort)} · ${((Date.now() - inicio) / 1000).toFixed(1)}s`));
|
|
1346
|
+
const nAcoes = require('../cli-nova.js').zerar();
|
|
1347
|
+
if (nAcoes) console.log(require('../cli-nova.js').fecho({ segundos: (Date.now() - inicio) / 1000, acoes: nAcoes }, { c }));
|
|
1348
|
+
console.log('');
|
|
1194
1349
|
// O que o agente deixou de pé (run_background), com o endereço pra abrir.
|
|
1195
1350
|
const rodape = require('../fundo.js').linhasRodape(require('../fundo.js').listar(), c);
|
|
1196
1351
|
if (rodape.length) console.log(rodape.join('\n') + '\n');
|
|
@@ -1354,6 +1509,7 @@ async function enviarPedido(userText) {
|
|
|
1354
1509
|
i === 0 ? require('../plugin/estudio/referencias.js').instrucao() : '',
|
|
1355
1510
|
// As imagens do pedido (arrastadas ou coladas): guardadas para o Estúdio importar, e ditas ao agente.
|
|
1356
1511
|
i === 0 ? (() => { const R = require('../plugin/estudio/referencias.js'); return R.instrucaoDasImagens(R.guardarImagens(R.imagensNoTexto(userText))); })() : '',
|
|
1512
|
+
i === 0 ? require('../cli-nova.js').instrucaoDeAgentes(userText).texto : '',
|
|
1357
1513
|
i === 0 ? plan.notaOutraPasta || '' : ''].filter(Boolean).join('\n\n'),
|
|
1358
1514
|
});
|
|
1359
1515
|
|
|
@@ -2113,7 +2269,18 @@ async function handleSlash(text) {
|
|
|
2113
2269
|
listava os nomes, em ordem alfabética, e quem procurava "o de ontem"
|
|
2114
2270
|
tinha de abrir pasta por pasta. Agora vem do mais recente, com
|
|
2115
2271
|
quando mexeu e o tamanho, e um número para entrar. */
|
|
2272
|
+
case 'estudio': case 'agentes': case 'piloto': case 'avisos': case 'buscar': {
|
|
2273
|
+
const r = require('../cli-nova.js').COMANDOS[nome](rest, { c, dir: process.env.PRIMOCODE_AGENTES });
|
|
2274
|
+
for (const l of r.linhas) console.log(l);
|
|
2275
|
+
if (r.pedido) await sendPrompt(`${r.instrucao}\n\nPEDIDO: ${r.pedido}`);
|
|
2276
|
+
break;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2116
2279
|
case 'projects': {
|
|
2280
|
+
if (/^(criar|novo|editar|arquivar)\b/.test(rest)) {
|
|
2281
|
+
for (const l of require('../cli-nova.js').projetos(rest, { c }).linhas) console.log(l);
|
|
2282
|
+
break;
|
|
2283
|
+
}
|
|
2117
2284
|
const lista = extra.listarProjetos(cfg.ROOT_DIR);
|
|
2118
2285
|
const numero = /^\d+$/.test(rest) ? Number(rest) : 0;
|
|
2119
2286
|
if (!rest || (numero && cmd.toLowerCase() === 'projetos') || (numero && numero <= lista.length)) {
|
|
@@ -2990,7 +3157,10 @@ async function handleSlash(text) {
|
|
|
2990
3157
|
}
|
|
2991
3158
|
|
|
2992
3159
|
case 'clear': state.history = []; state.memoria = memoria.nova();
|
|
2993
|
-
state.gravador.zerar();
|
|
3160
|
+
state.gravador.zerar();
|
|
3161
|
+
ui.estadoSessao.todos = []; ui.estadoSessao.arquivos.clear();
|
|
3162
|
+
if (rl && rl.cheia) { rl.novaConversa(); rl.toast('Conversa nova', 'ok'); } else console.log(c.ok('Conversa limpa.'));
|
|
3163
|
+
break;
|
|
2994
3164
|
case 'cls': process.stdout.write('\x1Bc'); break;
|
|
2995
3165
|
|
|
2996
3166
|
/* O /status olha só o servidor. O /doctor olha tudo o que costuma
|
|
@@ -3774,6 +3944,14 @@ async function main() {
|
|
|
3774
3944
|
if (process.argv[2] === 'marca') {
|
|
3775
3945
|
process.exit(await require('../plugin/marca-kit.js').main(process.argv.slice(3)));
|
|
3776
3946
|
}
|
|
3947
|
+
if (process.argv[2] === 'app') {
|
|
3948
|
+
const r = require('../cli-nova.js').app();
|
|
3949
|
+
console.log(r.texto); process.exit(r.ok ? 0 : 1);
|
|
3950
|
+
}
|
|
3951
|
+
/* `primocode server`: o app-server local (lib/app-server/). stdout é do JSON-RPC. */
|
|
3952
|
+
if (process.argv[2] === 'server') {
|
|
3953
|
+
process.exit(await require('../app-server/cli.js').main(process.argv.slice(3)));
|
|
3954
|
+
}
|
|
3777
3955
|
if (process.argv[2] === 'mcp') {
|
|
3778
3956
|
process.exit(await require('../mcp/mcp.js').main(process.argv.slice(3)));
|
|
3779
3957
|
}
|
|
@@ -3898,6 +4076,7 @@ async function main() {
|
|
|
3898
4076
|
contexto, e sem conversa a home põe o logo e a caixa no meio da tela.
|
|
3899
4077
|
PRIMOCODE_TELA=linha (ou "tela": "linha" no config) volta ao jeito
|
|
3900
4078
|
antigo, que escreve em linha pelo terminal. */
|
|
4079
|
+
if (state.config.tema) ui.definirTema(state.config.tema);
|
|
3901
4080
|
const telaCheia = process.stdout.isTTY && process.stdout.rows >= 16
|
|
3902
4081
|
&& (process.env.PRIMOCODE_TELA || state.config.tela || 'cheia') !== 'linha';
|
|
3903
4082
|
if (!telaCheia) {
|
|
@@ -3919,7 +4098,11 @@ async function main() {
|
|
|
3919
4098
|
logoAnimado: (quadro, trabalhando) => ui.logoAnimado(state.config.mascot, quadro, trabalhando),
|
|
3920
4099
|
agora: () => (ui.agora.texto && Date.now() - ui.agora.desde < 10 * 60 * 1000 ? ui.agora.texto : ''),
|
|
3921
4100
|
versao: VERSION,
|
|
3922
|
-
logo: ui.
|
|
4101
|
+
logo: [...ui.mascot(state.config.mascot), '', ...ui.logoBloco()],
|
|
4102
|
+
logoSimples: ui.logoBloco(),
|
|
4103
|
+
plano: () => { try { const eu = conta.atual(); return eu && eu.plano ? 'Plano ' + String(eu.plano).replace(/^./, (x) => x.toUpperCase()) : ''; } catch { return ''; } },
|
|
4104
|
+
paleta: paletaDeComandos,
|
|
4105
|
+
dialogos: { modelo: dialogoModelo, agente: dialogoAgentes, sessoes: dialogoSessoes, tema: dialogoTema },
|
|
3923
4106
|
pasta: () => {
|
|
3924
4107
|
const dir = state.workDir || process.cwd();
|
|
3925
4108
|
const ramo = ramoDoGit(dir);
|
|
@@ -3927,7 +4110,8 @@ async function main() {
|
|
|
3927
4110
|
},
|
|
3928
4111
|
});
|
|
3929
4112
|
if (telaCheia && state.workDirOrigem === 'padrao') {
|
|
3930
|
-
|
|
4113
|
+
// Um aviso no canto, não uma linha na conversa: a home continua limpa.
|
|
4114
|
+
rl.toast(`pasta padrão: ${shortPath(state.workDir)} · /dir <pasta> para trabalhar numa sua`, 'aviso', 7000);
|
|
3931
4115
|
}
|
|
3932
4116
|
/* A última conversa desta pasta, em uma linha (lib/passagem.js). Uma vez por abertura, só aqui (o -p já saiu
|
|
3933
4117
|
antes) e só se a pessoa não abriu já retomando (-c / --resume). */
|
|
@@ -3960,6 +4144,7 @@ async function main() {
|
|
|
3960
4144
|
* ela é desenhada pela própria caixa (ver lib/tui/entrada.js). */
|
|
3961
4145
|
let ultimoEsc = 0;
|
|
3962
4146
|
rl.on('escape', () => {
|
|
4147
|
+
if (permissaoPendente) { permissaoPendente(); return; } // Esc na pergunta de permissão = não
|
|
3963
4148
|
if (!busy) { ultimoEsc = 0; return; }
|
|
3964
4149
|
const agora = Date.now();
|
|
3965
4150
|
if (agora - ultimoEsc < 2000) {
|