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.
@@ -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`) + '\n');
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
  }
@@ -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 };
@@ -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 };