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
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,17 +340,37 @@ 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
|
+
// Preferências da tela Configurações (legenda, silêncios, efeitos, voz, formato, idioma, testes).
|
|
347
|
+
preferencias: preferenciasValidas(file.preferencias),
|
|
348
|
+
// A tela de primeira execução do app (docs/estrutura.md, 16.7): some depois de feita ou pulada.
|
|
349
|
+
inicioFeito: file.inicioFeito === true,
|
|
343
350
|
rootDir: ROOT_DIR,
|
|
344
351
|
_file: file,
|
|
345
352
|
};
|
|
346
353
|
}
|
|
347
354
|
|
|
355
|
+
/* Preferências da tela Configurações: só estas chaves, cada uma com o seu tipo. O resto é descartado. */
|
|
356
|
+
const PREFERENCIAS = { legenda: 'b', silencio: 'b', sfx: 'b', testes: 'b', voz: 's', formato: 's', idioma: 's' };
|
|
357
|
+
function preferenciasValidas(o) {
|
|
358
|
+
const out = {};
|
|
359
|
+
if (!o || typeof o !== 'object') return out;
|
|
360
|
+
for (const [k, t] of Object.entries(PREFERENCIAS)) {
|
|
361
|
+
if (t === 'b' && typeof o[k] === 'boolean') out[k] = o[k];
|
|
362
|
+
else if (t === 's' && typeof o[k] === 'string' && o[k].length <= 40) out[k] = o[k];
|
|
363
|
+
}
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
|
|
348
367
|
function saveConfig(partial) {
|
|
349
368
|
ensureDir(CONFIG_DIR);
|
|
350
369
|
const cur = (() => { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; } })();
|
|
351
370
|
// 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
|
|
371
|
+
const { server, model, effort, token, autoApply, currentProject, userName, mascot, reacoes, theme, mode, permissoes, team, piloto, grupo, inicioFeito } = { ...cur, ...partial };
|
|
372
|
+
const preferencias = preferenciasValidas({ ...(cur.preferencias || {}), ...(partial.preferencias || {}) });
|
|
373
|
+
const next = { server, model, effort, token, autoApply, currentProject, userName, mascot, reacoes, theme, mode, permissoes, team, piloto, grupo, inicioFeito, preferencias };
|
|
354
374
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8');
|
|
355
375
|
return next;
|
|
356
376
|
}
|
package/lib/diff.js
CHANGED
|
@@ -134,6 +134,8 @@ function desenhar(antes, depois, { prefixo = '', max = MAX_MOSTRADAS } = {}) {
|
|
|
134
134
|
const out = [];
|
|
135
135
|
let desenhadas = 0;
|
|
136
136
|
let cortou = false;
|
|
137
|
+
let larguraFaixa = 20;
|
|
138
|
+
for (const bloco of blocos) for (const o of bloco) larguraFaixa = Math.max(larguraFaixa, 4 + 1 + 2 + Math.min(o.texto.length, 120) + 1);
|
|
137
139
|
|
|
138
140
|
for (const bloco of blocos) {
|
|
139
141
|
if (desenhadas >= max) { cortou = true; break; }
|
|
@@ -143,8 +145,16 @@ function desenhar(antes, depois, { prefixo = '', max = MAX_MOSTRADAS } = {}) {
|
|
|
143
145
|
// removidas, o do antes. Sem número, "+ const x = 1" não diz onde.
|
|
144
146
|
const n = String(o.b || o.a || '').padStart(4);
|
|
145
147
|
const texto = o.texto.length > 120 ? o.texto.slice(0, 119) + '…' : o.texto;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
/* As linhas que entraram e saíram levam um FUNDO verde e vermelho até o fim
|
|
149
|
+
(o diff do OpenCode); as que ficaram, só o texto apagado. O fundo vale
|
|
150
|
+
a largura da linha mais comprida do bloco, para a faixa ficar reta. */
|
|
151
|
+
const faixa = (cor, fundo, sinal) => {
|
|
152
|
+
const corpo = `${n} ${sinal} ${texto}`;
|
|
153
|
+
return prefixo + fundo + c.dim(n) + fundo + ' ' + cor + sinal + ' ' + texto
|
|
154
|
+
+ ' '.repeat(Math.max(0, larguraFaixa - corpo.length)) + '\x1b[0m';
|
|
155
|
+
};
|
|
156
|
+
if (o.tipo === '+') out.push(faixa('\x1b[38;5;78m', '\x1b[48;5;22m', '+'));
|
|
157
|
+
else if (o.tipo === '-') out.push(faixa('\x1b[38;5;204m', '\x1b[48;5;52m', '-'));
|
|
148
158
|
else out.push(prefixo + c.dim(n) + ' ' + c.dim(' ' + texto));
|
|
149
159
|
desenhadas++;
|
|
150
160
|
}
|
|
@@ -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 };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Eventos do motor (docs/estrutura.md, seção 3). O motor só EMITE; quem
|
|
2
|
+
// desenha (terminal, app desktop, -p) assina. Node puro, sem dependência.
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const EVENTOS = Object.freeze({
|
|
6
|
+
TURNO_COMECOU: 'turn/started',
|
|
7
|
+
TURNO_TERMINOU: 'turn/completed',
|
|
8
|
+
TURNO_INTERROMPIDO: 'turn/interrupted',
|
|
9
|
+
ITEM_COMECOU: 'item/started',
|
|
10
|
+
ITEM_DELTA: 'item/delta',
|
|
11
|
+
ITEM_TERMINOU: 'item/completed',
|
|
12
|
+
STATUS: 'status/changed',
|
|
13
|
+
USO: 'usage/updated',
|
|
14
|
+
APROVACAO: 'approval/requested',
|
|
15
|
+
PERGUNTA: 'question/requested',
|
|
16
|
+
AVISO: 'notice/created',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const TIPOS = Object.freeze(Object.values(EVENTOS));
|
|
20
|
+
|
|
21
|
+
/* Cada "vista" é uma coisa que a tela do terminal sabe desenhar. O evento
|
|
22
|
+
carrega os campos do contrato (`evento`, `id`, `tipo`, `estado`) e os dados
|
|
23
|
+
que a tela precisa. Estados: rodando · ok · erro · interrompido. */
|
|
24
|
+
const VISTAS = Object.freeze({
|
|
25
|
+
'spinner-inicia': { evento: EVENTOS.STATUS, tipo: 'status', estado: 'rodando' },
|
|
26
|
+
'spinner-detalhe': { evento: EVENTOS.STATUS, tipo: 'status', estado: 'rodando' },
|
|
27
|
+
'spinner-para': { evento: EVENTOS.STATUS, tipo: 'status', estado: 'parado' },
|
|
28
|
+
'pensamento': { evento: EVENTOS.ITEM_TERMINOU, tipo: 'raciocinio', estado: 'ok' },
|
|
29
|
+
'resposta': { evento: EVENTOS.ITEM_TERMINOU, tipo: 'mensagem', estado: 'ok' },
|
|
30
|
+
'fluxo-inicia': { evento: EVENTOS.ITEM_COMECOU, tipo: 'mensagem', estado: 'rodando' },
|
|
31
|
+
'fluxo-escreve': { evento: EVENTOS.ITEM_DELTA, tipo: 'mensagem', estado: 'rodando' },
|
|
32
|
+
'fluxo-fecha': { evento: EVENTOS.ITEM_TERMINOU, tipo: 'mensagem', estado: 'ok' },
|
|
33
|
+
'acao': { evento: EVENTOS.ITEM_COMECOU, tipo: 'chamada', estado: 'rodando' },
|
|
34
|
+
'resultado': { evento: EVENTOS.ITEM_TERMINOU, tipo: 'chamada', estado: 'ok' },
|
|
35
|
+
'resultado-linha': { evento: EVENTOS.ITEM_TERMINOU, tipo: 'chamada', estado: 'ok' },
|
|
36
|
+
'paralelo': { evento: EVENTOS.ITEM_COMECOU, tipo: 'agente', estado: 'rodando' },
|
|
37
|
+
'agente': { evento: EVENTOS.ITEM_COMECOU, tipo: 'agente', estado: 'rodando' },
|
|
38
|
+
'agente-voltou': { evento: EVENTOS.ITEM_TERMINOU, tipo: 'agente', estado: 'ok' },
|
|
39
|
+
'estudio-progresso': { evento: EVENTOS.ITEM_DELTA, tipo: 'estudio', estado: 'rodando' },
|
|
40
|
+
'aviso': { evento: EVENTOS.AVISO, tipo: 'aviso', estado: 'ok' },
|
|
41
|
+
'problema': { evento: EVENTOS.AVISO, tipo: 'aviso', estado: 'erro' },
|
|
42
|
+
'linha': { evento: EVENTOS.AVISO, tipo: 'aviso', estado: 'ok' },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let SEQ = 0;
|
|
46
|
+
const novoId = () => `i${++SEQ}`;
|
|
47
|
+
|
|
48
|
+
/* Monta um evento a partir de uma vista. `dados.id` junta eventos do mesmo
|
|
49
|
+
item (spinner, fluxo); sem ele nasce um id novo. */
|
|
50
|
+
function montar(vista, dados) {
|
|
51
|
+
const base = VISTAS[vista];
|
|
52
|
+
if (!base) throw new Error(`vista desconhecida: ${vista}`);
|
|
53
|
+
return Object.assign({ vista, id: novoId() }, base, dados || {});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Emissor mínimo. on('*', fn) escuta tudo. Um assinante que quebra não
|
|
57
|
+
derruba o turno. emitir(evento) aceita o objeto montado por `montar`. */
|
|
58
|
+
function criarEmissor() {
|
|
59
|
+
const ouvintes = new Map();
|
|
60
|
+
const on = (tipo, fn) => {
|
|
61
|
+
if (!ouvintes.has(tipo)) ouvintes.set(tipo, new Set());
|
|
62
|
+
ouvintes.get(tipo).add(fn);
|
|
63
|
+
return () => off(tipo, fn);
|
|
64
|
+
};
|
|
65
|
+
const off = (tipo, fn) => { const s = ouvintes.get(tipo); if (s) s.delete(fn); };
|
|
66
|
+
const emitir = (evento) => {
|
|
67
|
+
if (!evento || !evento.evento) return;
|
|
68
|
+
for (const chave of [evento.evento, '*']) {
|
|
69
|
+
for (const fn of [...(ouvintes.get(chave) || [])]) {
|
|
70
|
+
try { fn(evento); } catch (e) { if (process.env.PRIMOCODE_DEBUG) console.error('[motor] ouvinte falhou:', e && e.message); }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
return { on, off, emitir };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { EVENTOS, TIPOS, VISTAS, montar, novoId, criarEmissor };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// A tela de terminal: assina os eventos do motor e chama exatamente o que
|
|
2
|
+
// `runAct` chamava antes de a tela sair do turno (fala.*, Spinner, logResult).
|
|
3
|
+
// Mesma saída, mesma ordem. O motor não sabe que isto existe.
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const { criarEmissor } = require('./eventos');
|
|
7
|
+
|
|
8
|
+
/* Uma tela por turno: guarda os spinners e os fluxos abertos, por id. */
|
|
9
|
+
function conectar(emissor) {
|
|
10
|
+
const em = emissor || criarEmissor();
|
|
11
|
+
const spinners = new Map();
|
|
12
|
+
const fluxos = new Map();
|
|
13
|
+
|
|
14
|
+
em.on('*', (e) => {
|
|
15
|
+
// Carregados na hora de usar: act.js carrega este arquivo, e o contrário seria um ciclo.
|
|
16
|
+
const fala = require('../fala');
|
|
17
|
+
const { c, Spinner } = require('../tui/ui');
|
|
18
|
+
const act = require('../session/runner/act');
|
|
19
|
+
const p = e.profundidade;
|
|
20
|
+
switch (e.vista) {
|
|
21
|
+
case 'spinner-inicia':
|
|
22
|
+
spinners.set(e.id, new Spinner(e.rotulo).start());
|
|
23
|
+
break;
|
|
24
|
+
case 'spinner-detalhe': {
|
|
25
|
+
const sp = spinners.get(e.id);
|
|
26
|
+
if (sp) sp.detalhe = e.detalhe;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case 'spinner-para': {
|
|
30
|
+
const sp = spinners.get(e.id);
|
|
31
|
+
if (sp) sp.stop();
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case 'pensamento': fala.pensamento(e.texto, p); break;
|
|
35
|
+
case 'resposta': fala.resposta(e.texto); break;
|
|
36
|
+
case 'fluxo-inicia': {
|
|
37
|
+
const f = fala.fluxo();
|
|
38
|
+
fluxos.set(e.id, f);
|
|
39
|
+
if (e.texto) f.escrever(e.texto);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case 'fluxo-escreve': { const f = fluxos.get(e.id); if (f) f.escrever(e.texto); break; }
|
|
43
|
+
case 'fluxo-fecha': { const f = fluxos.get(e.id); if (f) { f.fechar(); fluxos.delete(e.id); } break; }
|
|
44
|
+
case 'acao': require('../cli-nova').contar(); fala.acao(e.nome, e.args, p); break;
|
|
45
|
+
case 'resultado': act.logResult(e.nome, e.resultado, p); break;
|
|
46
|
+
case 'resultado-linha':
|
|
47
|
+
console.log(fala.resultado(p) + (e.nivel === 'aviso' ? c.warn(e.texto) : c.muted(e.texto)));
|
|
48
|
+
break;
|
|
49
|
+
case 'paralelo': fala.paralelo(e.quantos, p); break;
|
|
50
|
+
case 'agente': fala.agente(e.rotulo, e.descricao, p); break;
|
|
51
|
+
case 'agente-voltou': fala.agenteVoltou(e.texto, p); break;
|
|
52
|
+
case 'aviso': fala.aviso(e.texto); break;
|
|
53
|
+
case 'problema': fala.problema(e.erro); break;
|
|
54
|
+
case 'linha': console.log(e.nivel === 'aviso' ? c.warn(e.texto) : e.nivel === 'ok' ? c.ok(e.texto) : e.nivel === 'dim' ? c.dim(e.texto) : e.texto); break;
|
|
55
|
+
default: break;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return em;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* A função `emitir` pronta para o runAct: uma tela de terminal nova. */
|
|
62
|
+
function criar() { return conectar().emitir; }
|
|
63
|
+
|
|
64
|
+
module.exports = { conectar, criar };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Transcrição da fala do app (bug 3, 02/11): o renderer grava com MediaRecorder e manda o WAV; aqui ele vira texto
|
|
2
|
+
// com o Whisper local que o produto já tem (voz/primo_voz/transcrever.py, no voz-venv). Sem provedor pago, sem rede.
|
|
3
|
+
// Nunca toca som: só lê um arquivo.
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { spawn, execFileSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
const PASTA_VOZ = path.join(__dirname, '..', '..', 'voz');
|
|
12
|
+
const TETO_BYTES = 11 * 1024 * 1024; // ~5 min de WAV 16 kHz mono (em base64 cabe no teto de 16 MB do WebSocket)
|
|
13
|
+
const TEMPO_MS = 120000;
|
|
14
|
+
|
|
15
|
+
/** O que falta para transcrever aqui, ou null se está tudo pronto. `py` vem de lib/plugin/voz.js (o venv do Primo ganha). */
|
|
16
|
+
function faltaAlgo(py) {
|
|
17
|
+
if (!py) return { error: 'o modo voz precisa do Python 3, e não achei nenhum nesta máquina.', comoResolver: 'instale o Python 3 e rode `primocode doutor`' };
|
|
18
|
+
try { execFileSync(py, ['-c', 'import faster_whisper'], { stdio: ['ignore', 'pipe', 'pipe'], timeout: 20000 }); return null; } catch {
|
|
19
|
+
return { error: 'falta o Whisper local (faster-whisper), que transforma a sua fala em texto.', comoResolver: 'rode `primocode doutor` (ou abra o /voice no terminal uma vez, que ele instala)' };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** `transcrever(wav, { python })` → { ok, texto } | { ok:false, error, comoResolver? }. Nunca rejeita. */
|
|
24
|
+
function transcrever(wav, { python: py, idioma = 'pt', tempoMs = TEMPO_MS } = {}) {
|
|
25
|
+
py = py || require('../plugin/voz').python();
|
|
26
|
+
const falta = faltaAlgo(py);
|
|
27
|
+
if (falta) return Promise.resolve({ ok: false, ...falta });
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
const p = spawn(py, ['-m', 'primo_voz.transcrever', 'arquivo', wav, idioma], { cwd: PASTA_VOZ, env: { ...process.env, PYTHONPATH: PASTA_VOZ } });
|
|
30
|
+
let fora = '', erro = '';
|
|
31
|
+
const t = setTimeout(() => { try { p.kill('SIGKILL'); } catch { /* já saiu */ } resolve({ ok: false, error: 'a transcrição demorou demais' }); }, tempoMs);
|
|
32
|
+
p.stdout.on('data', (d) => { fora += d; });
|
|
33
|
+
p.stderr.on('data', (d) => { erro += d; });
|
|
34
|
+
p.on('error', (e) => { clearTimeout(t); resolve({ ok: false, error: e.message }); });
|
|
35
|
+
p.on('close', () => {
|
|
36
|
+
clearTimeout(t);
|
|
37
|
+
try { const r = JSON.parse(fora.trim().split('\n').pop()); resolve(r.ok ? { ok: true, texto: String(r.texto || '') } : { ok: false, error: r.error || 'não consegui transcrever' }); }
|
|
38
|
+
catch { resolve({ ok: false, error: erro.trim().split('\n').pop() || 'o transcritor não respondeu' }); }
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Grava o áudio (base64, WAV) num arquivo temporário, transcreve e apaga. `transcritor` troca o Whisper (teste). */
|
|
44
|
+
async function transcreverBase64(audio, { pasta, transcritor = transcrever } = {}) {
|
|
45
|
+
if (typeof audio !== 'string' || !audio) return { ok: false, error: 'falta o áudio' };
|
|
46
|
+
const bytes = Buffer.from(audio, 'base64');
|
|
47
|
+
if (!bytes.length) return { ok: false, error: 'áudio vazio' };
|
|
48
|
+
if (bytes.length > TETO_BYTES) return { ok: false, error: 'áudio grande demais (máximo ~5 minutos)' };
|
|
49
|
+
if (bytes.subarray(0, 4).toString('latin1') !== 'RIFF') return { ok: false, error: 'o áudio precisa ser WAV' };
|
|
50
|
+
const dir = pasta || path.join(os.tmpdir(), 'primocode-voz');
|
|
51
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
52
|
+
const arq = path.join(dir, `fala-${Date.now()}-${process.pid}.wav`);
|
|
53
|
+
fs.writeFileSync(arq, bytes, { mode: 0o600 });
|
|
54
|
+
try { return await transcritor(arq); } finally { try { fs.rmSync(arq, { force: true }); } catch { /* sem arquivo */ } }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { transcrever, transcreverBase64, faltaAlgo, TETO_BYTES };
|