primocode 11.1.0 → 11.2.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 +117 -0
- package/lib/app-server/cli.js +61 -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 +685 -0
- package/lib/app-server/websocket.js +87 -0
- package/lib/avisos.js +64 -0
- package/lib/cli/sessao.js +28 -1
- package/lib/cli-nova.js +237 -0
- package/lib/config/config.js +7 -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/turno.js +43 -0
- package/lib/motor/voz.js +65 -0
- package/lib/piloto.js +39 -0
- package/lib/plugin/estudio/criar.js +113 -0
- package/lib/plugin/estudio/historico.js +60 -0
- package/lib/plugin/estudio/index.js +14 -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 +90 -0
- package/lib/qr.js +209 -0
- package/lib/session/runner/act.js +104 -64
- package/lib/skill/catalogo.js +3 -1
- package/lib/tool/tools.js +6 -0
- package/lib/tui/ui.js +1 -1
- package/package.json +6 -2
|
@@ -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,64 @@
|
|
|
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
|
+
agente: dados.agente || null, thread: dados.thread || null, aprovacao: dados.aprovacao || null,
|
|
31
|
+
criado: Date.now(),
|
|
32
|
+
};
|
|
33
|
+
gravar([av, ...ler({ arquivo: a })], a);
|
|
34
|
+
return av;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const contagem = (l) => ({
|
|
38
|
+
naoLidos: l.filter((x) => x.estado === 'novo').length,
|
|
39
|
+
praAprovar: l.filter((x) => x.estado === 'pendente').length,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function listar({ arquivo: a } = {}) { const l = ler({ arquivo: a }); return { avisos: l, ...contagem(l) }; }
|
|
43
|
+
|
|
44
|
+
/* Marca como lido um aviso (id) ou todos. Aprovação pendente não some ao ler: só ao responder. */
|
|
45
|
+
function marcarLido({ id, todos } = {}, { arquivo: a } = {}) {
|
|
46
|
+
const l = ler({ arquivo: a });
|
|
47
|
+
let achou = false;
|
|
48
|
+
for (const x of l) if ((todos || x.id === id) && x.estado === 'novo') { x.estado = 'lido'; achou = true; }
|
|
49
|
+
if (!todos && !l.some((x) => x.id === id)) throw new Error(`não achei o aviso ${id}`);
|
|
50
|
+
if (achou) gravar(l, a);
|
|
51
|
+
return { avisos: l, ...contagem(l) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* A aprovação foi respondida (pelo cartão ou pelo aviso). */
|
|
55
|
+
function resolverAprovacao(aprovacaoId, resposta, { arquivo: a } = {}) {
|
|
56
|
+
const l = ler({ arquivo: a });
|
|
57
|
+
const x = l.find((n) => n.tipo === 'aprovacao' && n.aprovacao === aprovacaoId && n.estado === 'pendente');
|
|
58
|
+
if (!x) return null;
|
|
59
|
+
x.estado = 'resolvido'; x.resposta = resposta || null;
|
|
60
|
+
gravar(l, a);
|
|
61
|
+
return x;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { ler, criar, listar, marcarLido, resolverAprovacao, contagem };
|
package/lib/cli/sessao.js
CHANGED
|
@@ -242,6 +242,8 @@ const SLASH_COMANDOS = [
|
|
|
242
242
|
['/version', 'versão'],
|
|
243
243
|
['/exit', 'sair'],
|
|
244
244
|
];
|
|
245
|
+
// A CLI nova (lib/cli-nova.js): /estudio /agentes /piloto /avisos /buscar entram no menu e no autopreenchimento.
|
|
246
|
+
SLASH_COMANDOS.splice(Math.max(0, SLASH_COMANDOS.findIndex(([n]) => n === '/marca')), 0, ...require('../cli-nova.js').SLASH_NOVOS);
|
|
245
247
|
|
|
246
248
|
/* /api: a chave da própria pessoa — lib/cli/comando-api.js. */
|
|
247
249
|
const comandoApi = require('./comando-api.js').criar({ rl: () => rl, ask: (...a) => ask(...a), askSecret: (...a) => askSecret(...a) });
|
|
@@ -440,6 +442,8 @@ function barraDeStatus() {
|
|
|
440
442
|
// Modelo que respondeu · contexto · tokens de hoje (os mesmos do /usage) — lib/cli/statusline.js.
|
|
441
443
|
const SL = require('./statusline.js');
|
|
442
444
|
direita.push(...SL.partes({ modelo: SL.modelo(), ctxPct: uso.pct, tokensHoje: SL.tokensDeHoje(), c }));
|
|
445
|
+
const aprovar = require('../cli-nova.js').praAprovar();
|
|
446
|
+
if (aprovar) direita.push(c.warn(`${aprovar} pra aprovar`));
|
|
443
447
|
direita.push(state.trabalhando && rl && rl.ocupado
|
|
444
448
|
? c.warn('trabalhando') + c.dim(' · ') + c.white('esc esc') + c.dim(' para')
|
|
445
449
|
: c.white('tab') + c.dim(' modos ') + c.white('ctrl+p') + c.dim(' comandos'));
|
|
@@ -1190,7 +1194,10 @@ async function sendPrompt(userText, { digitado = false } = {}) {
|
|
|
1190
1194
|
const m = state.mode === 'auto' ? (state.modoEfetivo || 'auto') : state.mode;
|
|
1191
1195
|
if (!state.semTerminal) {
|
|
1192
1196
|
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`)
|
|
1197
|
+
+ c.dim(` · ${effort.normalizar(state.config.effort)} · ${((Date.now() - inicio) / 1000).toFixed(1)}s`));
|
|
1198
|
+
const nAcoes = require('../cli-nova.js').zerar();
|
|
1199
|
+
if (nAcoes) console.log(require('../cli-nova.js').fecho({ segundos: (Date.now() - inicio) / 1000, acoes: nAcoes }, { c }));
|
|
1200
|
+
console.log('');
|
|
1194
1201
|
// O que o agente deixou de pé (run_background), com o endereço pra abrir.
|
|
1195
1202
|
const rodape = require('../fundo.js').linhasRodape(require('../fundo.js').listar(), c);
|
|
1196
1203
|
if (rodape.length) console.log(rodape.join('\n') + '\n');
|
|
@@ -1354,6 +1361,7 @@ async function enviarPedido(userText) {
|
|
|
1354
1361
|
i === 0 ? require('../plugin/estudio/referencias.js').instrucao() : '',
|
|
1355
1362
|
// As imagens do pedido (arrastadas ou coladas): guardadas para o Estúdio importar, e ditas ao agente.
|
|
1356
1363
|
i === 0 ? (() => { const R = require('../plugin/estudio/referencias.js'); return R.instrucaoDasImagens(R.guardarImagens(R.imagensNoTexto(userText))); })() : '',
|
|
1364
|
+
i === 0 ? require('../cli-nova.js').instrucaoDeAgentes(userText).texto : '',
|
|
1357
1365
|
i === 0 ? plan.notaOutraPasta || '' : ''].filter(Boolean).join('\n\n'),
|
|
1358
1366
|
});
|
|
1359
1367
|
|
|
@@ -2113,7 +2121,18 @@ async function handleSlash(text) {
|
|
|
2113
2121
|
listava os nomes, em ordem alfabética, e quem procurava "o de ontem"
|
|
2114
2122
|
tinha de abrir pasta por pasta. Agora vem do mais recente, com
|
|
2115
2123
|
quando mexeu e o tamanho, e um número para entrar. */
|
|
2124
|
+
case 'estudio': case 'agentes': case 'piloto': case 'avisos': case 'buscar': {
|
|
2125
|
+
const r = require('../cli-nova.js').COMANDOS[nome](rest, { c, dir: process.env.PRIMOCODE_AGENTES });
|
|
2126
|
+
for (const l of r.linhas) console.log(l);
|
|
2127
|
+
if (r.pedido) await sendPrompt(`${r.instrucao}\n\nPEDIDO: ${r.pedido}`);
|
|
2128
|
+
break;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2116
2131
|
case 'projects': {
|
|
2132
|
+
if (/^(criar|novo|editar|arquivar)\b/.test(rest)) {
|
|
2133
|
+
for (const l of require('../cli-nova.js').projetos(rest, { c }).linhas) console.log(l);
|
|
2134
|
+
break;
|
|
2135
|
+
}
|
|
2117
2136
|
const lista = extra.listarProjetos(cfg.ROOT_DIR);
|
|
2118
2137
|
const numero = /^\d+$/.test(rest) ? Number(rest) : 0;
|
|
2119
2138
|
if (!rest || (numero && cmd.toLowerCase() === 'projetos') || (numero && numero <= lista.length)) {
|
|
@@ -3774,6 +3793,14 @@ async function main() {
|
|
|
3774
3793
|
if (process.argv[2] === 'marca') {
|
|
3775
3794
|
process.exit(await require('../plugin/marca-kit.js').main(process.argv.slice(3)));
|
|
3776
3795
|
}
|
|
3796
|
+
if (process.argv[2] === 'app') {
|
|
3797
|
+
const r = require('../cli-nova.js').app();
|
|
3798
|
+
console.log(r.texto); process.exit(r.ok ? 0 : 1);
|
|
3799
|
+
}
|
|
3800
|
+
/* `primocode server`: o app-server local (lib/app-server/). stdout é do JSON-RPC. */
|
|
3801
|
+
if (process.argv[2] === 'server') {
|
|
3802
|
+
process.exit(await require('../app-server/cli.js').main(process.argv.slice(3)));
|
|
3803
|
+
}
|
|
3777
3804
|
if (process.argv[2] === 'mcp') {
|
|
3778
3805
|
process.exit(await require('../mcp/mcp.js').main(process.argv.slice(3)));
|
|
3779
3806
|
}
|
package/lib/cli-nova.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// A CLI nova (docs/estrutura.md, seção 5): comandos de barra /estudio /projetos /agentes /piloto /avisos /buscar,
|
|
2
|
+
// `primocode app`, @agente no prompt, células com os verbos do PrimoCode e a status line.
|
|
3
|
+
// Só COMPÕE os módulos que o app já usa (projetos-meta, agentes, avisos, estudio/criar, estudio/historico,
|
|
4
|
+
// config): nenhuma regra é duplicada aqui. Cada comando devolve linhas de texto; quem imprime é a CLI. Node puro.
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const cfg = () => require('./config/config');
|
|
12
|
+
const sem = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
|
|
13
|
+
const sim = (v) => /^(on|liga|ligar|ligado|sim|s|1|true)$/i.test(String(v || ''));
|
|
14
|
+
const nao = (v) => /^(off|desliga|desligar|desligado|nao|não|n|0|false)$/i.test(String(v || ''));
|
|
15
|
+
const cor = (c, nome) => (c && c[nome]) || ((t) => t);
|
|
16
|
+
|
|
17
|
+
/* ── Células: o molde do Codex CLI com os verbos do PrimoCode ───────────────── */
|
|
18
|
+
const VERBOS = [
|
|
19
|
+
[/^(read_file|list_dir|list_files|glob|grep|search_files|tree|ler_arquivo|listar)/, 'Explorou'],
|
|
20
|
+
[/^(web_search|search_web|pesquisar|buscar_web)/, 'Pesquisou na web por'],
|
|
21
|
+
[/^(run_command|bash|shell|run|exec)/, 'Rodou'],
|
|
22
|
+
[/^(edit_file|apply_patch|str_replace|replace_in_file|editar)/, 'Editou'],
|
|
23
|
+
[/^(write_file|create_file|estudio_criar|criar_arquivo)/, 'Criou'],
|
|
24
|
+
[/^(spawn_agent|mcp_|call_agent|chamar)/, 'Chamou'],
|
|
25
|
+
];
|
|
26
|
+
const verboDe = (nome) => { const v = VERBOS.find(([re]) => re.test(String(nome || ''))); return v ? v[1] : 'Chamou'; };
|
|
27
|
+
|
|
28
|
+
/** `• Verbo alvo`; Editou ganha (+N −M). Minimizada = só a primeira linha; aberta traz o detalhe recuado. */
|
|
29
|
+
function celula(item, { c, aberta = false } = {}) {
|
|
30
|
+
const dim = cor(c, 'dim'); const brand = cor(c, 'brand');
|
|
31
|
+
const verbo = item.verbo || verboDe(item.nome);
|
|
32
|
+
let l = `${brand('•')} ${verbo}${item.alvo ? ' ' + item.alvo : ''}`;
|
|
33
|
+
if (verbo === 'Editou' && (item.mais != null || item.menos != null)) l += dim(` (+${item.mais || 0} −${item.menos || 0})`);
|
|
34
|
+
const linhas = [l];
|
|
35
|
+
if (aberta && item.detalhe) for (const d of String(item.detalhe).split('\n').slice(0, 12)) linhas.push(dim(' ' + d));
|
|
36
|
+
return linhas;
|
|
37
|
+
}
|
|
38
|
+
function plano(passos, { c } = {}) {
|
|
39
|
+
const ok = cor(c, 'ok'); const dim = cor(c, 'dim');
|
|
40
|
+
return passos.map((p) => (p.feito ? ` ${ok('✔')} ${dim(p.texto)}` : ` □ ${p.texto}`));
|
|
41
|
+
}
|
|
42
|
+
const fecho = ({ segundos = 0, acoes = 0 } = {}, { c } = {}) =>
|
|
43
|
+
cor(c, 'dim')(` Trabalhou por ${Math.max(0, Math.round(segundos))}s • ${acoes} ${acoes === 1 ? 'ação' : 'ações'}`);
|
|
44
|
+
const interrompida = ({ c } = {}) => cor(c, 'warn')(' ■ Conversa interrompida');
|
|
45
|
+
|
|
46
|
+
/* Conta as ações do turno (a tela do terminal chama contar a cada item `acao`). */
|
|
47
|
+
const turno = { acoes: 0 };
|
|
48
|
+
const contar = () => { turno.acoes++; };
|
|
49
|
+
const zerar = () => { const n = turno.acoes; turno.acoes = 0; return n; };
|
|
50
|
+
|
|
51
|
+
/* ── Status line ────────────────────────────────────────────────────────────── */
|
|
52
|
+
const NOMES_ESFORCO = { low: 'Leve', medium: 'Médio', high: 'Alto', ultra: 'Ultra', ultracode: 'Ultra' };
|
|
53
|
+
const tok = (n) => (n >= 1000 ? `${Math.round(n / 1000)}k` : String(n || 0));
|
|
54
|
+
|
|
55
|
+
/** `~/primocode/<projeto> · Alto · 34% ctx · 28k tok hoje · ● trabalhando · N pra aprovar`. ctx laranja ≥70%, vermelho ≥100%. */
|
|
56
|
+
function statusLinha(e = {}, { c } = {}) {
|
|
57
|
+
const dim = cor(c, 'dim'); const sep = dim(' · ');
|
|
58
|
+
const p = [];
|
|
59
|
+
p.push(cor(c, 'brandDim')(e.pasta || '~/primocode'));
|
|
60
|
+
p.push(cor(c, 'white')(NOMES_ESFORCO[String(e.esforco || '').toLowerCase()] || e.esforco || 'Alto'));
|
|
61
|
+
if (e.ctxPct != null) p.push(e.ctxPct >= 100 ? cor(c, 'err')(`${e.ctxPct}% ctx`) : e.ctxPct >= 70 ? cor(c, 'warn')(`${e.ctxPct}% ctx`) : dim(`${e.ctxPct}% ctx`));
|
|
62
|
+
if (e.tokensHoje) p.push(dim(`${tok(e.tokensHoje)} tok hoje`));
|
|
63
|
+
if (e.trabalhando) p.push(cor(c, 'warn')('● trabalhando'));
|
|
64
|
+
if (e.praAprovar) p.push(cor(c, 'warn')(`${e.praAprovar} pra aprovar`));
|
|
65
|
+
return p.join(sep);
|
|
66
|
+
}
|
|
67
|
+
const praAprovar = () => { try { return require('./avisos').contagem(require('./avisos').ler()).praAprovar; } catch { return 0; } };
|
|
68
|
+
|
|
69
|
+
/* ── @agente no prompt ──────────────────────────────────────────────────────── */
|
|
70
|
+
/** O texto que entra como instrução do turno: o agente citado (mesmo desligado) vira chamada, os ligados vão de cardápio. */
|
|
71
|
+
function instrucaoDeAgentes(texto, { dir } = {}) {
|
|
72
|
+
try { return require('./agentes-pessoais').instrucao(texto, [], { dir }); } catch { return { texto: '', chamados: [] }; }
|
|
73
|
+
}
|
|
74
|
+
/** Nomes do `@` na hora de digitar: agentes primeiro (com o ofício), arquivos depois. */
|
|
75
|
+
function completarArroba(prefixo, { projeto, dir } = {}) {
|
|
76
|
+
const q = sem(String(prefixo || '').replace(/^@/, ''));
|
|
77
|
+
const ags = require('./agentes-pessoais').listar({ dir }).filter((a) => sem(a.nome).startsWith(q))
|
|
78
|
+
.map((a) => ({ tipo: 'agente', valor: '@' + a.nome, descricao: a.descricao || a.oficio.slice(0, 60) }));
|
|
79
|
+
let arqs = [];
|
|
80
|
+
if (projeto) {
|
|
81
|
+
try {
|
|
82
|
+
arqs = fs.readdirSync(projeto, { withFileTypes: true }).filter((d) => !d.name.startsWith('.') && sem(d.name).startsWith(q))
|
|
83
|
+
.map((d) => ({ tipo: d.isDirectory() ? 'pasta' : 'arquivo', valor: '@' + d.name + (d.isDirectory() ? '/' : ''), descricao: '' }));
|
|
84
|
+
} catch { /* sem projeto */ }
|
|
85
|
+
}
|
|
86
|
+
return ags.concat(arqs.slice(0, 20));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ── Comandos de barra ──────────────────────────────────────────────────────── */
|
|
90
|
+
const TIPOS_ESTUDIO = [['slides', 'apresentação em PDF'], ['video', 'vídeo curto vertical'], ['motion', 'animação de motion'], ['carrossel', 'carrossel (sem receita no Estúdio ainda)'], ['anuncio', 'anúncio de 15 s'], ['abertura', 'abertura de canal de 5 s']];
|
|
91
|
+
|
|
92
|
+
/** `/estudio [tipo] [briefing]` → { linhas, pedido?, tipo? }. Com tipo e briefing, devolve o `pedido` para a sessão mandar ao motor. */
|
|
93
|
+
function estudio(rest, { c } = {}) {
|
|
94
|
+
const { QUADRADOS, instrucaoDoTipo } = require('./plugin/estudio/criar');
|
|
95
|
+
const [t0, ...r] = String(rest || '').trim().split(/\s+/).filter(Boolean);
|
|
96
|
+
const tipo = sem(t0).replace(/^v[ií]deo$/, 'video');
|
|
97
|
+
const linhas = [];
|
|
98
|
+
if (!t0) {
|
|
99
|
+
linhas.push(cor(c, 'white')('O que vamos criar?'));
|
|
100
|
+
for (const [n, d] of TIPOS_ESTUDIO) linhas.push(` ${cor(c, 'brand')(n.padEnd(10))}${cor(c, 'muted')(d)}`);
|
|
101
|
+
linhas.push(cor(c, 'dim')(' /estudio <tipo> [o que criar]'));
|
|
102
|
+
return { linhas };
|
|
103
|
+
}
|
|
104
|
+
if (!TIPOS_ESTUDIO.some(([n]) => n === tipo) && !QUADRADOS[tipo]) return { linhas: [cor(c, 'err')(`Tipo "${t0}" não existe. Use: ${TIPOS_ESTUDIO.map(([n]) => n).join(', ')}.`)], erro: true };
|
|
105
|
+
if (tipo === 'carrossel') return { linhas: [cor(c, 'warn')('Carrossel ainda não tem receita no Estúdio. Descreva como slides ou peça direto na conversa.')], erro: true };
|
|
106
|
+
const briefing = r.join(' ');
|
|
107
|
+
if (!briefing) return { linhas: [cor(c, 'white')(`O que vamos criar em ${tipo}?`), cor(c, 'dim')(` /estudio ${tipo} <descrição do que criar>`)], tipo };
|
|
108
|
+
return { linhas: [], tipo, pedido: briefing, instrucao: instrucaoDoTipo(tipo) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** `/projetos`: lista · `criar <nome>` · `editar <id> nome=… icone=… cor=… descricao=…` · `arquivar <id>`. Mesmo módulo do app. */
|
|
112
|
+
function projetos(rest, { c } = {}) {
|
|
113
|
+
const P = require('./projetos-meta');
|
|
114
|
+
const [op, ...r] = String(rest || '').trim().split(/\s+/).filter(Boolean);
|
|
115
|
+
try {
|
|
116
|
+
if (op === 'criar' || op === 'novo') { const p = P.criar({ nome: r.join(' ') }); return { linhas: [cor(c, 'ok')(`Projeto criado: ${p.nome}`) + cor(c, 'muted')(` (${p.caminho})`)], projeto: p }; }
|
|
117
|
+
if (op === 'editar') {
|
|
118
|
+
const [id, ...kv] = r; const m = {};
|
|
119
|
+
for (const x of kv) { const i = x.indexOf('='); if (i > 0) m[x.slice(0, i)] = x.slice(i + 1); }
|
|
120
|
+
const p = P.atualizar(id, m);
|
|
121
|
+
return { linhas: [cor(c, 'ok')(`Projeto atualizado: ${p.nome} · ${p.icone} · ${p.cor}`)], projeto: p };
|
|
122
|
+
}
|
|
123
|
+
if (op === 'arquivar') { const x = P.arquivar(r[0]); return { linhas: [cor(c, 'ok')(`Arquivado ${x.id} (os arquivos ficam).`)] }; }
|
|
124
|
+
} catch (e) { return { linhas: [cor(c, 'err')(e.message)], erro: true }; }
|
|
125
|
+
const l = P.listar();
|
|
126
|
+
if (!l.length) return { linhas: [cor(c, 'muted')('Nenhum projeto ainda.')] };
|
|
127
|
+
return { linhas: l.map((p, i) => ` ${String(i + 1).padStart(2)} ${cor(c, 'white')(p.nome.padEnd(24))}${cor(c, 'dim')(`${p.icone} ${p.cor}${p.descricao ? ' · ' + p.descricao : ''}`)}`), lista: l };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** `/agentes`: lista · ligar|desligar <nome> · criar <nome> | <descrição> | <ofício> · apagar <nome>. */
|
|
131
|
+
function agentes(rest, { c, dir } = {}) {
|
|
132
|
+
const A = require('./agentes-pessoais');
|
|
133
|
+
const [op, ...r] = String(rest || '').trim().split(/\s+/).filter(Boolean);
|
|
134
|
+
try {
|
|
135
|
+
if (op === 'ligar' || op === 'desligar') { const a = A.alternar(r[0], op === 'ligar', { dir }); return { linhas: [cor(c, 'ok')(`@${a.nome} ${a.ligado ? 'ligado' : 'desligado'}`)] }; }
|
|
136
|
+
if (op === 'apagar') { const x = A.apagar(r[0], { dir }); return { linhas: [cor(c, 'ok')(`@${x.nome} apagado`)] }; }
|
|
137
|
+
if (op === 'criar') {
|
|
138
|
+
const [nome, descricao, oficio] = r.join(' ').split('|').map((s) => s.trim());
|
|
139
|
+
const a = A.criar({ nome, descricao: descricao || '', oficio: oficio || descricao || '' }, { dir });
|
|
140
|
+
return { linhas: [cor(c, 'ok')(`@${a.nome} criado`) + cor(c, 'muted')(' (desligado: /agentes ligar ' + a.nome + ')')] };
|
|
141
|
+
}
|
|
142
|
+
} catch (e) { return { linhas: [cor(c, 'err')(e.message)], erro: true }; }
|
|
143
|
+
const l = A.listar({ dir });
|
|
144
|
+
if (!l.length) return { linhas: [cor(c, 'muted')('Nenhum agente. Crie: /agentes criar <nome> | <o que faz> | <ofício>')] };
|
|
145
|
+
return { linhas: l.map((a) => ` ${a.ligado ? cor(c, 'ok')('●') : cor(c, 'dim')('○')} ${cor(c, 'white')('@' + a.nome.padEnd(16))}${cor(c, 'dim')(a.descricao || a.oficio.slice(0, 60))}`), lista: l };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** `/piloto [on|off] [grupo]`: grava `piloto` e `grupo` na config; quem roda o ciclo é o app-server (lib/piloto.js). */
|
|
149
|
+
function piloto(rest, { c } = {}) {
|
|
150
|
+
const ps = String(rest || '').trim().split(/\s+/).filter(Boolean);
|
|
151
|
+
const mudar = {};
|
|
152
|
+
for (const p of ps) { if (sim(p)) mudar.piloto = true; else if (nao(p)) mudar.piloto = false; else if (/^grupo$/i.test(p)) mudar.grupo = true; else if (/^solo$/i.test(p)) mudar.grupo = false; }
|
|
153
|
+
if (Object.keys(mudar).length) cfg().saveConfig(mudar);
|
|
154
|
+
const e = cfg().loadConfig();
|
|
155
|
+
const ligados = require('./agentes-pessoais').listar().filter((a) => a.ligado).length;
|
|
156
|
+
return { linhas: [
|
|
157
|
+
`${e.piloto === true ? cor(c, 'ok')('● Piloto ligado') : cor(c, 'dim')('○ Piloto desligado')}${e.grupo === true ? cor(c, 'muted')(' · em grupo') : ''} ${cor(c, 'muted')(`${ligados} agente${ligados === 1 ? '' : 's'} ligado${ligados === 1 ? '' : 's'}`)}`,
|
|
158
|
+
cor(c, 'dim')(' O ciclo roda no app-server (primocode app/server); aqui só liga e desliga.'),
|
|
159
|
+
], ligado: e.piloto === true, grupo: e.grupo === true };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** `/avisos`: a fila. `a <n>` aprova, `r <n>` recusa, `ler [n|todos]` marca lido. */
|
|
163
|
+
function avisos(rest, { c, arquivo } = {}) {
|
|
164
|
+
const V = require('./avisos');
|
|
165
|
+
const [op, n] = String(rest || '').trim().split(/\s+/).filter(Boolean);
|
|
166
|
+
const arq = arquivo ? { arquivo } : {};
|
|
167
|
+
const lista = () => V.ler(arq).filter((x) => x.estado === 'pendente' || x.estado === 'novo');
|
|
168
|
+
try {
|
|
169
|
+
if (op === 'a' || op === 'r') {
|
|
170
|
+
const x = lista()[Number(n) - 1];
|
|
171
|
+
if (!x || x.tipo !== 'aprovacao') return { linhas: [cor(c, 'err')('Escolha o número de uma aprovação pendente: /avisos a 1')], erro: true };
|
|
172
|
+
V.resolverAprovacao(x.aprovacao, op === 'r' ? 'nao' : 'agora', arq);
|
|
173
|
+
return { linhas: [cor(c, 'ok')(`${op === 'r' ? 'Recusado' : 'Aprovado'}: ${x.texto}`)] };
|
|
174
|
+
}
|
|
175
|
+
if (op === 'ler') {
|
|
176
|
+
const x = n && n !== 'todos' ? lista()[Number(n) - 1] : null;
|
|
177
|
+
V.marcarLido(n === 'todos' || !n ? { todos: true } : { id: x && x.id }, arq);
|
|
178
|
+
return { linhas: [cor(c, 'ok')('Marcado como lido.')] };
|
|
179
|
+
}
|
|
180
|
+
} catch (e) { return { linhas: [cor(c, 'err')(e.message)], erro: true }; }
|
|
181
|
+
const l = lista();
|
|
182
|
+
const k = V.contagem(V.ler(arq));
|
|
183
|
+
if (!l.length) return { linhas: [cor(c, 'muted')('Nada pra ver. Fila vazia.')] };
|
|
184
|
+
return { linhas: [
|
|
185
|
+
...l.map((x, i) => ` ${String(i + 1).padStart(2)} ${x.tipo === 'aprovacao' ? cor(c, 'warn')('aprovar') : cor(c, x.nivel === 'erro' ? 'err' : 'ok')('aviso ')} ${x.texto}${x.agente ? cor(c, 'dim')(' · @' + x.agente) : ''}`),
|
|
186
|
+
cor(c, 'dim')(` ${k.praAprovar} pra aprovar · ${k.naoLidos} não lidos · a <n> aprova · r <n> recusa · ler [n|todos]`),
|
|
187
|
+
], lista: l };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** `/buscar <texto>`: projetos, agentes, vídeos/slides do Estúdio e conversas salvas. */
|
|
191
|
+
function buscar(rest, { c, dir } = {}) {
|
|
192
|
+
const q = sem(rest).trim();
|
|
193
|
+
if (!q) return { linhas: [cor(c, 'dim')('Uso: /buscar <texto> (projetos, agentes, vídeos, conversas)')], erro: true };
|
|
194
|
+
const achei = (txt) => sem(txt).includes(q);
|
|
195
|
+
const out = [];
|
|
196
|
+
const sec = (t, itens) => { if (itens.length) { out.push(cor(c, 'white')(t)); out.push(...itens.slice(0, 8)); } };
|
|
197
|
+
const tenta = (f, d) => { try { return f(); } catch { return d; } };
|
|
198
|
+
sec('Projetos', tenta(() => require('./projetos-meta').listar(), []).filter((p) => achei(`${p.nome} ${p.descricao}`)).map((p) => ` ${p.nome}${cor(c, 'dim')(' ' + p.caminho)}`));
|
|
199
|
+
sec('Agentes', tenta(() => require('./agentes-pessoais').listar({ dir }), []).filter((a) => achei(`${a.nome} ${a.descricao} ${a.oficio}`)).map((a) => ` @${a.nome}${cor(c, 'dim')(' ' + (a.descricao || ''))}`));
|
|
200
|
+
sec('Vídeos e slides', tenta(() => require('./plugin/estudio/historico').historico({ busca: rest, limite: 8 }).itens, []).map((i) => ` ${i.titulo}${cor(c, 'dim')(' ' + (i.principal || i.pasta))}`));
|
|
201
|
+
const pt = path.join(os.homedir(), '.primocode', 'threads');
|
|
202
|
+
sec('Conversas', tenta(() => fs.readdirSync(pt).filter((n) => n.endsWith('.json')).map((n) => JSON.parse(fs.readFileSync(path.join(pt, n), 'utf8'))), [])
|
|
203
|
+
.filter((t) => !t.arquivada && achei(t.titulo)).map((t) => ` ${t.titulo}${cor(c, 'dim')(' ' + t.id)}`));
|
|
204
|
+
return { linhas: out.length ? out : [cor(c, 'muted')(`Nada achado para "${rest}".`)] };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const SLASH_NOVOS = [
|
|
208
|
+
['/estudio', 'escolhe o que criar: slides, vídeo, motion, anúncio, abertura'],
|
|
209
|
+
['/agentes', 'seus agentes: lista · ligar · desligar · criar · apagar'],
|
|
210
|
+
['/piloto', 'piloto automático: on | off [grupo]'],
|
|
211
|
+
['/avisos', 'fila de aprovação e avisos · a <n> aprova · r <n> recusa'],
|
|
212
|
+
['/buscar', 'procura em projetos, agentes, vídeos e conversas'],
|
|
213
|
+
];
|
|
214
|
+
const COMANDOS = { estudio, projetos, agentes, piloto, avisos, buscar };
|
|
215
|
+
|
|
216
|
+
/* ── primocode app ──────────────────────────────────────────────────────────── */
|
|
217
|
+
function appInstalado({ env = process.env, plataforma = process.platform, existe = fs.existsSync } = {}) {
|
|
218
|
+
const alvos = env.PRIMOCODE_APP ? [env.PRIMOCODE_APP] : plataforma === 'darwin'
|
|
219
|
+
? ['/Applications/PrimoCode.app', path.join(os.homedir(), 'Applications', 'PrimoCode.app')]
|
|
220
|
+
: plataforma === 'win32' ? [path.join(env.LOCALAPPDATA || '', 'Programs', 'PrimoCode', 'PrimoCode.exe')]
|
|
221
|
+
: ['/opt/PrimoCode/primocode', path.join(os.homedir(), '.local', 'bin', 'primocode-app')];
|
|
222
|
+
return alvos.find((a) => a && existe(a)) || null;
|
|
223
|
+
}
|
|
224
|
+
/** `primocode app`: abre o desktop se instalado; senão diz como. `abrir(caminho)` é injetável (teste não abre nada). */
|
|
225
|
+
function app({ abrir, ...o } = {}) {
|
|
226
|
+
const a = appInstalado(o);
|
|
227
|
+
if (!a) return { ok: false, texto: 'O app desktop não está instalado. Baixe em primocode.app (ou rode `primocode server --ws` para usar outra tela). Dentro do repo: `cd app && npm install && npm start`.' };
|
|
228
|
+
const faz = abrir || ((p) => {
|
|
229
|
+
const { spawn } = require('child_process');
|
|
230
|
+
const [bin, args] = process.platform === 'darwin' ? ['open', [p]] : process.platform === 'win32' ? ['cmd', ['/c', 'start', '', p]] : [p, []];
|
|
231
|
+
spawn(bin, args, { detached: true, stdio: 'ignore' }).unref();
|
|
232
|
+
});
|
|
233
|
+
faz(a);
|
|
234
|
+
return { ok: true, texto: `Abrindo o PrimoCode (${a}).`, caminho: a };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { celula, plano, fecho, interrompida, verboDe, contar, zerar, statusLinha, praAprovar, instrucaoDeAgentes, completarArroba, SLASH_NOVOS, COMANDOS, estudio, projetos, agentes, piloto, avisos, buscar, app, appInstalado };
|
package/lib/config/config.js
CHANGED
|
@@ -340,6 +340,11 @@ function loadConfig() {
|
|
|
340
340
|
permissoes: file.permissoes || 'auto',
|
|
341
341
|
// O parceiro do /team (lib/team.js): quem revisa o que o PrimoCode faz. null = sozinho.
|
|
342
342
|
team: file.team || null,
|
|
343
|
+
// Piloto automático e agentes em grupo (docs/estrutura.md, seção 9). Desligados por padrão.
|
|
344
|
+
piloto: file.piloto === true,
|
|
345
|
+
grupo: file.grupo === true,
|
|
346
|
+
// A tela de primeira execução do app (docs/estrutura.md, 16.7): some depois de feita ou pulada.
|
|
347
|
+
inicioFeito: file.inicioFeito === true,
|
|
343
348
|
rootDir: ROOT_DIR,
|
|
344
349
|
_file: file,
|
|
345
350
|
};
|
|
@@ -349,8 +354,8 @@ function saveConfig(partial) {
|
|
|
349
354
|
ensureDir(CONFIG_DIR);
|
|
350
355
|
const cur = (() => { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; } })();
|
|
351
356
|
// Só persiste o que faz sentido guardar — nada de campos derivados.
|
|
352
|
-
const { server, model, effort, token, autoApply, currentProject, userName, mascot, reacoes, theme, mode, permissoes, team } = { ...cur, ...partial };
|
|
353
|
-
const next = { server, model, effort, token, autoApply, currentProject, userName, mascot, reacoes, theme, mode, permissoes, team };
|
|
357
|
+
const { server, model, effort, token, autoApply, currentProject, userName, mascot, reacoes, theme, mode, permissoes, team, piloto, grupo, inicioFeito } = { ...cur, ...partial };
|
|
358
|
+
const next = { server, model, effort, token, autoApply, currentProject, userName, mascot, reacoes, theme, mode, permissoes, team, piloto, grupo, inicioFeito };
|
|
354
359
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8');
|
|
355
360
|
return next;
|
|
356
361
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Aprovações e perguntas por evento (docs/estrutura.md, seções 3 e 6.7).
|
|
2
|
+
// O motor PEDE emitindo `approval/requested` ou `question/requested` e espera;
|
|
3
|
+
// quem desenha (a tela do app) responde pelo app-server, e o pedido destrava.
|
|
4
|
+
// Node puro, sem dependência. Sem ninguém respondendo, o turno espera: a tela
|
|
5
|
+
// (ou `cancelar()`, no interromper) decide. O padrão de quem cancela é NÃO.
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const { EVENTOS, novoId } = require('./eventos');
|
|
9
|
+
|
|
10
|
+
const MAX_PERGUNTAS = 4;
|
|
11
|
+
const MAX_OPCOES = 4;
|
|
12
|
+
const NENHUMA = 'Nenhuma dessas';
|
|
13
|
+
|
|
14
|
+
/* Uma pergunta limpa: texto, até 4 opções com rótulo e descrição curta. */
|
|
15
|
+
function limparPergunta(p) {
|
|
16
|
+
const opcoes = (Array.isArray(p && p.opcoes) ? p.opcoes : []).slice(0, MAX_OPCOES).map((o) => (
|
|
17
|
+
typeof o === 'string' ? { rotulo: o } : { rotulo: String((o && o.rotulo) || ''), descricao: o && o.descricao ? String(o.descricao) : undefined }
|
|
18
|
+
)).filter((o) => o.rotulo);
|
|
19
|
+
return { pergunta: String((p && p.pergunta) || '').trim(), opcoes };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function criarPortao({ emitir } = {}) {
|
|
23
|
+
const pend = new Map(); // id → { tipo, resolver, perguntas? }
|
|
24
|
+
const saida = typeof emitir === 'function' ? emitir : () => {};
|
|
25
|
+
|
|
26
|
+
const status = (estado, rotulo) => saida({ evento: EVENTOS.STATUS, id: novoId(), tipo: 'status', estado, rotulo });
|
|
27
|
+
const esperando = () => status('esperando', 'esperando você');
|
|
28
|
+
const voltar = () => { if (!pend.size) status('rodando', 'trabalhando'); };
|
|
29
|
+
|
|
30
|
+
function fechar(id, estado, extra) {
|
|
31
|
+
saida({ evento: EVENTOS.ITEM_TERMINOU, id, tipo: pend.get(id).tipo === 'aprovacao' ? 'aprovacao' : 'pergunta', estado, ...(extra || {}) });
|
|
32
|
+
pend.delete(id);
|
|
33
|
+
voltar();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Mesma assinatura do `autorizar` do runAct: (frase, info) → 'agora' | 'sessao' | 'nao'. */
|
|
37
|
+
function autorizar(frase, info = {}) {
|
|
38
|
+
const id = novoId();
|
|
39
|
+
return new Promise((resolver) => {
|
|
40
|
+
pend.set(id, { tipo: 'aprovacao', resolver });
|
|
41
|
+
saida({
|
|
42
|
+
evento: EVENTOS.APROVACAO, id, tipo: 'aprovacao', estado: 'esperando',
|
|
43
|
+
texto: String(frase || ''), categoria: info.tipo || 'computador',
|
|
44
|
+
motivo: info.motivo || undefined, primeira: !!info.primeira,
|
|
45
|
+
});
|
|
46
|
+
esperando();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* `perguntas`: até 4, cada uma com até 4 opções (a tela soma "Nenhuma dessas").
|
|
51
|
+
Resolve com [{ indice, rotulo, nota, nenhuma }] na mesma ordem; nunca rejeita. */
|
|
52
|
+
function perguntar({ titulo, perguntas } = {}) {
|
|
53
|
+
const lista = (Array.isArray(perguntas) ? perguntas : []).slice(0, MAX_PERGUNTAS).map(limparPergunta).filter((p) => p.pergunta);
|
|
54
|
+
if (!lista.length) return Promise.resolve([]);
|
|
55
|
+
const id = novoId();
|
|
56
|
+
return new Promise((resolver) => {
|
|
57
|
+
pend.set(id, { tipo: 'pergunta', resolver, perguntas: lista });
|
|
58
|
+
saida({
|
|
59
|
+
evento: EVENTOS.PERGUNTA, id, tipo: 'pergunta', estado: 'esperando',
|
|
60
|
+
titulo: titulo || undefined, perguntas: lista, nenhuma: NENHUMA,
|
|
61
|
+
});
|
|
62
|
+
esperando();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* A tela respondeu a uma aprovação: { aceitar, recusar, sempre }. */
|
|
67
|
+
function responderAprovacao(id, r = {}) {
|
|
68
|
+
const p = pend.get(id);
|
|
69
|
+
if (!p || p.tipo !== 'aprovacao') return false;
|
|
70
|
+
const resposta = r.recusar ? 'nao' : r.sempre ? 'sessao' : r.aceitar ? 'agora' : 'nao';
|
|
71
|
+
fechar(id, resposta === 'nao' ? 'erro' : 'ok', { resposta });
|
|
72
|
+
p.resolver(resposta);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* A tela respondeu a uma pergunta: respostas = [{ indice | null, nota }]. */
|
|
77
|
+
function responderPergunta(id, respostas) {
|
|
78
|
+
const p = pend.get(id);
|
|
79
|
+
if (!p || p.tipo !== 'pergunta') return false;
|
|
80
|
+
const lista = Array.isArray(respostas) ? respostas : [];
|
|
81
|
+
const saidaR = p.perguntas.map((q, i) => {
|
|
82
|
+
const r = lista[i] || {};
|
|
83
|
+
const n = Number.isInteger(r.indice) && r.indice >= 0 && r.indice < q.opcoes.length ? r.indice : null;
|
|
84
|
+
return { indice: n, rotulo: n === null ? NENHUMA : q.opcoes[n].rotulo, nota: r.nota ? String(r.nota).slice(0, 2000) : undefined, nenhuma: n === null };
|
|
85
|
+
});
|
|
86
|
+
fechar(id, 'ok', { respostas: saidaR });
|
|
87
|
+
p.resolver(saidaR);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Turno interrompido ou terminado: tudo o que esperava resolve como NÃO / nenhuma. */
|
|
92
|
+
function cancelar() {
|
|
93
|
+
for (const [id, p] of [...pend]) {
|
|
94
|
+
fechar(id, 'interrompido');
|
|
95
|
+
p.resolver(p.tipo === 'aprovacao' ? 'nao' : p.perguntas.map(() => ({ indice: null, rotulo: NENHUMA, nenhuma: true })));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* mudar_pasta do agente: uma pergunta de sim/não, com "Nenhuma dessas" valendo não. */
|
|
100
|
+
async function mudarPasta({ caminho, motivo }) {
|
|
101
|
+
const r = await perguntar({
|
|
102
|
+
titulo: 'O Primo quer trabalhar em outra pasta',
|
|
103
|
+
perguntas: [{ pergunta: `Trabalhar em ${caminho} a partir de agora?${motivo ? ` Motivo: ${motivo}` : ''}`,
|
|
104
|
+
opcoes: [{ rotulo: 'Sim, trocar', descricao: caminho }, { rotulo: 'Não, ficar onde está' }] }],
|
|
105
|
+
});
|
|
106
|
+
if (r[0] && r[0].indice === 0) return { ok: true, caminho };
|
|
107
|
+
try { require('../tool/tools').liberarLeitura(caminho); } catch { /* sem leitura extra */ }
|
|
108
|
+
return { ok: false };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { autorizar, perguntar, mudarPasta, responderAprovacao, responderPergunta, cancelar, pendentes: () => [...pend.keys()] };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { criarPortao, NENHUMA, MAX_PERGUNTAS, MAX_OPCOES };
|