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.
Files changed (47) hide show
  1. package/README.md +16 -2
  2. package/bin/primocode-rpc.js +100 -0
  3. package/lib/agentes-pessoais.js +120 -0
  4. package/lib/app-server/cli.js +64 -0
  5. package/lib/app-server/enderecos.js +43 -0
  6. package/lib/app-server/index.js +3 -0
  7. package/lib/app-server/pareamento.js +61 -0
  8. package/lib/app-server/protocolo.js +86 -0
  9. package/lib/app-server/servidor.js +805 -0
  10. package/lib/app-server/websocket.js +87 -0
  11. package/lib/avisos.js +65 -0
  12. package/lib/cli/sessao.js +202 -17
  13. package/lib/cli-nova.js +237 -0
  14. package/lib/config/config.js +22 -2
  15. package/lib/diff.js +12 -2
  16. package/lib/motor/aprovacao.js +114 -0
  17. package/lib/motor/eventos.js +77 -0
  18. package/lib/motor/index.js +8 -0
  19. package/lib/motor/tela-terminal.js +64 -0
  20. package/lib/motor/transcricao.js +57 -0
  21. package/lib/motor/turno.js +43 -0
  22. package/lib/motor/voz.js +65 -0
  23. package/lib/piloto.js +39 -0
  24. package/lib/plugin/entregaveis/nucleo.js +2 -2
  25. package/lib/plugin/estudio/criar.js +113 -0
  26. package/lib/plugin/estudio/gosto.js +3 -0
  27. package/lib/plugin/estudio/historico.js +60 -0
  28. package/lib/plugin/estudio/index.js +14 -1
  29. package/lib/plugin/estudio/kits/tela.html +3 -1
  30. package/lib/plugin/estudio/miniatura.js +56 -0
  31. package/lib/plugin/video/auto.js +1 -1
  32. package/lib/plugin/video/catalogo.js +20 -0
  33. package/lib/plugin/video/mutar.js +28 -0
  34. package/lib/plugin/video/ops-fx.js +6 -6
  35. package/lib/plugin/video/ops.js +2 -13
  36. package/lib/plugin/video/pedido.js +1 -1
  37. package/lib/plugin/video/zoom.js +1 -1
  38. package/lib/projetos-meta.js +107 -0
  39. package/lib/qr.js +209 -0
  40. package/lib/session/runner/act.js +127 -68
  41. package/lib/skill/catalogo.js +3 -1
  42. package/lib/tool/tools.js +7 -0
  43. package/lib/tui/entrada.js +143 -18
  44. package/lib/tui/tela.js +312 -49
  45. package/lib/tui/ui.js +78 -4
  46. package/package.json +10 -2
  47. package/voz/primo_voz/transcrever.py +12 -0
@@ -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,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 next = { server, model, effort, token, autoApply, currentProject, userName, mascot, reacoes, theme, mode, permissoes, team };
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
- if (o.tipo === '+') out.push(prefixo + c.dim(n) + ' ' + c.ok('+ ' + texto));
147
- else if (o.tipo === '-') out.push(prefixo + c.dim(n) + ' ' + c.err('- ' + texto));
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,8 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ ...require('./eventos'),
5
+ ...require('./turno'),
6
+ ...require('./aprovacao'),
7
+ telaTerminal: require('./tela-terminal'),
8
+ };
@@ -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 };