primocode 11.3.1 → 11.3.3

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Vanilagy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v11.3.1
1
+ # PrimoCode v11.3.2
2
2
 
3
3
  Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
4
4
  Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
9
9
  está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
10
10
 
11
11
  ```
12
- ▐▛███▜▌ Primo Code v11.3.1
12
+ ▐▛███▜▌ Primo Code v11.3.2
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
package/lib/cli/sessao.js CHANGED
@@ -143,6 +143,27 @@ function linhasDosGrupos() {
143
143
  return saida;
144
144
  }
145
145
 
146
+ /* O `--help` de fora: só o essencial. Os grupos completos moram no /help (e no "/") dentro da sessão. */
147
+ const HELP_ENXUTO = ['new', 'projects', 'undo', 'check', 'open', 'studio', 'help', 'exit'];
148
+ /* PRIMO_DEBUG=1: o que os catch silenciosos engolem vai pro stderr. Sem a variável, nada muda. */
149
+ function depurar(onde, e) {
150
+ if (process.env.PRIMO_DEBUG !== '1') return;
151
+ try { process.stderr.write(`[PRIMO_DEBUG] ${onde}: ${e && e.stack ? e.stack : e}\n`); } catch { /* stderr fechado */ }
152
+ }
153
+
154
+ function montarHelpEnxuto() {
155
+ const desc = new Map(comandosDoMenu().map(([n, d]) => [n.slice(1), d]));
156
+ const linhas = HELP_ENXUTO.map((n) => { const d = desc.get(n); return d ? ` ${c.brand(ui.padRight('/' + n, 16))}${c.muted(d)}` : ` ${c.brand('/' + n)}`; });
157
+ return `
158
+ ${c.bold('Comandos')}
159
+
160
+ ${linhas.join('\n')}
161
+ ${c.brand('-p')} "pedido" modo script: sem tela, aceita stdin e --output-format json
162
+
163
+ ${c.dim('digite / pra ver todos')}
164
+ `;
165
+ }
166
+
146
167
  function montarHelp() {
147
168
  return `
148
169
  ${c.bold('Comandos')} ${c.dim('· os nomes em português continuam valendo (/desfazer = /undo)')}
@@ -218,10 +239,6 @@ const SLASH_COMANDOS = [
218
239
  ['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
219
240
  ['/logout', 'desconecta este computador da conta'],
220
241
  ['!comando', 'roda direto no shell, sem passar pelo agente (o campo fica âmbar)'],
221
- ['/voice', 'liga o Primo: clique nele e fale; ele faz e responde em voz alta, com a tela dele (robô, agenda, clima, música)'],
222
- ['/voice painel', 'só o painel do Primo (ESC esconde)'],
223
- ['/voice nome <Nome>', 'dá outro nome ao assistente (só o nome; o modo voz abre no clique)'],
224
- ['/voice parar', 'desliga o modo voz e fecha o microfone'],
225
242
  ['/skills', 'o que ele sabe fazer bem · /skills store: a loja · /skills add <arquivo>: instala'],
226
243
  ['/commands', 'seus comandos: um .md vira /nome · /commands novo <nome>'],
227
244
  ['/styles', 'os estilos de motion (kits): a lista e a galeria · /styles <estilo>: o preview dele'],
@@ -234,6 +251,7 @@ const SLASH_COMANDOS = [
234
251
  ['/keys', 'as teclas que ajudam'],
235
252
  ['/connectors', 'quem está atendendo os seus pedidos'],
236
253
  ['/name', 'como ele te chama · /name voz <Nome>: o nome do assistente de voz'],
254
+ ['/studio', 'escolhe o que criar: slides, vídeo, motion, anúncio, abertura'],
237
255
  ['/brand', 'kit de marca: cores, fontes e logo'],
238
256
  ['/mascot', 'galeria dos mascotes · escolha por nome ou número'],
239
257
  ['/config', 'suas preferências'],
@@ -243,7 +261,7 @@ const SLASH_COMANDOS = [
243
261
  ['/version', 'versão'],
244
262
  ['/exit', 'sair'],
245
263
  ];
246
- // A CLI nova (lib/cli-nova.js): /estudio /agentes /piloto /avisos /buscar entram no menu e no autopreenchimento.
264
+ // A CLI nova (lib/cli-nova.js): /studio /agentes /piloto /avisos /buscar entram no menu e no autopreenchimento.
247
265
  SLASH_COMANDOS.splice(Math.max(0, SLASH_COMANDOS.findIndex(([n]) => n === '/marca')), 0, ...require('../cli-nova.js').SLASH_NOVOS);
248
266
 
249
267
  /* /api: a chave da própria pessoa — lib/cli/comando-api.js. */
@@ -540,7 +558,7 @@ function barraLateral() {
540
558
  * O desenho é do OpenCode: uma lista com busca no meio da tela. O conteúdo é
541
559
  * o que já existe aqui: os comandos de barra, os modos, os agentes da pessoa,
542
560
  * as conversas salvas e os temas. Escolher um item roda o comando de sempre. */
543
- const ATALHOS_DO_LIDER = { '/clear': 'ctrl+x n', '/projects': 'ctrl+x p', '/estudio': 'ctrl+x s' };
561
+ const ATALHOS_DO_LIDER = { '/clear': 'ctrl+x n', '/projects': 'ctrl+x p', '/studio': 'ctrl+x s' };
544
562
 
545
563
  function paletaDeComandos() {
546
564
  const itens = [
@@ -552,7 +570,7 @@ function paletaDeComandos() {
552
570
  { secao: 'Sugeridos', nome: 'Mostrar ou esconder a barra da direita', tecla: 'ctrl+x b', acao: (r) => { r.lateralOculta = !r.lateralOculta; r._quadroAnterior = null; r._pintar(); } },
553
571
  ];
554
572
  for (const [nome, desc] of comandosDoMenu()) {
555
- if (/^(\/uninstall|!comando|\/voice )/.test(nome) || nome.includes(' ') || nome === '/clear') continue;
573
+ if (/^(\/uninstall|!comando)/.test(nome) || nome.includes(' ') || nome === '/clear') continue;
556
574
  itens.push({ secao: 'Comandos', nome, desc, cmd: nome, tecla: ATALHOS_DO_LIDER[nome] || '', preencher: /\s<|<.*>/.test(desc) && /^\/(dir|worktree|rule|lock|attach|read|run|open|history|howto)$/.test(nome) });
557
575
  }
558
576
  return itens;
@@ -1747,7 +1765,8 @@ async function baixarCriacaoDireto(texto) {
1747
1765
  console.log(c.muted(' ') + c.white(ultimo.arquivo));
1748
1766
  console.log(c.dim(` ${(ultimo.bytes / 1048576).toFixed(1)} MB · o arquivo já estava pronto, é só copiar daí.`));
1749
1767
  return true;
1750
- } catch {
1768
+ } catch (e) {
1769
+ depurar('projeto renderizado', e);
1751
1770
  return false; // sem projeto renderizado: segue o caminho normal
1752
1771
  }
1753
1772
  }
@@ -1790,7 +1809,8 @@ async function dividirTarefa(pedido) {
1790
1809
  const passos = (r && Array.isArray(r.passos) && r.passos.length) ? r.passos : inteiro;
1791
1810
  if (passos.length > 1) console.log(c.dim(` ⎿ ${passos[0].titulo}`));
1792
1811
  return passos;
1793
- } catch {
1812
+ } catch (e) {
1813
+ depurar('preparar o pedido', e);
1794
1814
  sp.stop(); // preparar o pedido é melhoria, não requisito
1795
1815
  return inteiro;
1796
1816
  }
@@ -1832,7 +1852,8 @@ async function comprimirConversa(forcado = false, { silencioso = false } = {}) {
1832
1852
  gravarMemoriaDaPasta(dir);
1833
1853
  const e = memoria.economia(state.memoria, state.history);
1834
1854
  if (!silencioso && e.poupado > 0) console.log(c.dim(` ⎿ conversa resumida · ${e.cheio} → ${e.enxuto} tokens por pedido`));
1835
- } catch {
1855
+ } catch (e) {
1856
+ depurar('resumo da conversa', e);
1836
1857
  sp.stop(); // resumo é economia, não requisito: falhou, segue a vida
1837
1858
  }
1838
1859
  }
@@ -2269,8 +2290,8 @@ async function handleSlash(text) {
2269
2290
  listava os nomes, em ordem alfabética, e quem procurava "o de ontem"
2270
2291
  tinha de abrir pasta por pasta. Agora vem do mais recente, com
2271
2292
  quando mexeu e o tamanho, e um número para entrar. */
2272
- case 'estudio': case 'agentes': case 'piloto': case 'avisos': case 'buscar': {
2273
- const r = require('../cli-nova.js').COMANDOS[nome](rest, { c, dir: process.env.PRIMOCODE_AGENTES });
2293
+ case 'studio': case 'estudio': case 'agentes': case 'piloto': case 'avisos': case 'buscar': {
2294
+ const r = require('../cli-nova.js').COMANDOS[nome === 'studio' ? 'estudio' : nome](rest, { c, dir: process.env.PRIMOCODE_AGENTES });
2274
2295
  for (const l of r.linhas) console.log(l);
2275
2296
  if (r.pedido) await sendPrompt(`${r.instrucao}\n\nPEDIDO: ${r.pedido}`);
2276
2297
  break;
@@ -3063,7 +3084,7 @@ async function handleSlash(text) {
3063
3084
  const nomeVoz = rest.match(/^(?:voz|voice|assistente)\s+(.{2,20})$/i);
3064
3085
  if (nomeVoz) {
3065
3086
  const salvo = vozModo.definirNome(nomeVoz[1]);
3066
- console.log(salvo ? c.ok(` Fechado: o assistente agora se chama ${salvo}. Vale na próxima vez que ligar o /voice.`) : c.err(' Nome inválido (1 ou 2 palavras, até 20 letras).'));
3087
+ console.log(salvo ? c.ok(` Fechado: o assistente agora se chama ${salvo}. Vale na próxima vez que o modo voz abrir.`) : c.err(' Nome inválido (1 ou 2 palavras, até 20 letras).'));
3067
3088
  break;
3068
3089
  }
3069
3090
  if (!rest) { console.log(c.muted(` Nome atual: ${state.config.userName || '(nenhum)'}`)); break; }
@@ -3344,86 +3365,6 @@ async function handleSlash(text) {
3344
3365
  }
3345
3366
 
3346
3367
 
3347
- /* ── /voice ──────────────────────────────────────────────────────
3348
- * O ouvido e a boca são Python (voz/primo_voz); o cérebro é este
3349
- * terminal. Ver entradaDeVoz no main() e lib/plugin/voz.js.
3350
- */
3351
- case 'voice': {
3352
- /* ── O MODO VOZ É UMA FORMA DE DIGITAR ─────────────────────────
3353
- Você fala; o que foi dito entra aqui como se tivesse sido
3354
- digitado — a mesma fila, o mesmo recado no meio da tarefa, o
3355
- mesmo roteador de conversa — e a resposta final sai em voz alta.
3356
- O terminal continua seu: dá para falar e digitar ao mesmo tempo.
3357
-
3358
- Desligar é o primeiro caso de propósito: quem quer fechar o
3359
- microfone quer AGORA, sem esbarrar em checagem de dependência. */
3360
- const argVoz = (rest || '').trim();
3361
- // Só o painel (HUD em tela cheia no Mac; quadro no terminal fora dele).
3362
- if (/^painel$/i.test(argVoz)) {
3363
- const py = vozModo.python();
3364
- if (!py) { console.log(c.err(' O painel precisa do Python 3.')); break; }
3365
- if (vozPainel) { try { vozPainel.processo.kill(); } catch {} vozPainel = null; }
3366
- vozPainel = vozModo.abrirPainel({ python: py });
3367
- console.log(c.muted(vozPainel.tela ? ' Tela do Primo aberta — ESC esconde.' : ' Sem o painel gráfico aqui: rode `primocode voz painel` num terminal separado.'));
3368
- break;
3369
- }
3370
- // /voice nome <Nome>: a pessoa dá o nome ao assistente (o nome não ativa nada: o modo voz só abre no clique).
3371
- const mNome = argVoz.match(/^nome\s+(.{2,20})$/i);
3372
- if (mNome) {
3373
- const salvo = vozModo.definirNome(mNome[1]);
3374
- console.log(salvo ? c.ok(` Fechado: o assistente agora se chama ${salvo}. Vale na próxima vez que ligar o /voice.`) : c.err(' Nome inválido (1 ou 2 palavras, até 20 letras).'));
3375
- break;
3376
- }
3377
- if (/^(parar|stop|off|sair)$/i.test(argVoz)) {
3378
- if (vozPainel) { try { vozPainel.processo.kill(); } catch {} vozPainel = null; }
3379
- if (!vozEmEspera) { console.log(c.muted(vozPainel === null ? ' Modo voz e painel fechados.' : ' O modo voz não está ligado.')); break; }
3380
- try { vozEmEspera.processo.kill(); } catch {}
3381
- vozEmEspera = null;
3382
- console.log(c.muted(' Modo voz desligado. O microfone fechou.'));
3383
- break;
3384
- }
3385
- if (vozEmEspera) {
3386
- console.log(c.muted(' O modo voz já está ouvindo — é só falar.') + c.dim(' /voice parar desliga.'));
3387
- break;
3388
- }
3389
-
3390
- // "Instale tudo que precisa, rode tudo que precisa." Da segunda vez
3391
- // em diante tudo já está lá, e isto leva um segundo.
3392
- const sp = new Spinner('preparando o modo voz').start();
3393
- const prep = await vozModo.preparar((msg) => { sp.text = msg; });
3394
- sp.stop();
3395
- if (!prep.ok) {
3396
- console.log(c.err(` ${prep.error}`));
3397
- if (prep.comoResolver) console.log(c.muted(` ${prep.comoResolver}`));
3398
- for (const a of prep.avisos) console.log(c.dim(` ${a}`));
3399
- break;
3400
- }
3401
- for (const a of prep.avisos) console.log(c.dim(` ${a}`));
3402
- if (process.platform === 'darwin') { try { vozModo.instalarApp({ python: prep.python }); } catch { /* o app é bônus */ } }
3403
-
3404
- const sessao = vozModo.abrir({
3405
- python: prep.python,
3406
- servidor: state.config.server,
3407
- token: state.config.token,
3408
- voz: state.config.voz || 'antonio',
3409
- aoPronto: () => {
3410
- console.log(' ' + c.bold(c.brand('🎙 Modo voz ligado')) + c.muted(' — é só falar. A resposta sai em voz alta.'));
3411
- console.log(' ' + c.dim('Falar por cima corta a voz · digitar continua valendo · /voice parar desliga'));
3412
- },
3413
- aoParar: () => { try { require('../session/runner/act.js').interromper(); } catch { /* nada rodando */ } },
3414
- aoOuvir: (texto) => { if (entradaDeVoz) entradaDeVoz(texto); },
3415
- mostrar: (texto) => console.log(' ' + c.dim(texto)),
3416
- });
3417
- vozEmEspera = sessao;
3418
- require('../plugin/voz-ponte').escutar((nome, args) => sessao.ferramenta(nome, args));
3419
- sessao.pronto.then((r) => {
3420
- if (vozEmEspera === sessao) vozEmEspera = null;
3421
- if (r && !r.ok) console.log(c.err(` o modo voz fechou: ${r.error}`));
3422
- });
3423
- break;
3424
- }
3425
-
3426
-
3427
3368
  /* ── /connectors ─────────────────────────────────────────────────
3428
3369
  * Só mostra. Não há o que configurar — os conectores vivem no
3429
3370
  * servidor, com as chaves dele, e é assim de propósito: a promessa do
@@ -3923,12 +3864,12 @@ async function main() {
3923
3864
  }
3924
3865
  if (sub === 'instalar-app') {
3925
3866
  const py = vz.python();
3926
- if (!py) { console.log('O app precisa do Python 3 (rode /voice uma vez para preparar o ambiente).'); process.exit(1); }
3867
+ if (!py) { console.log('O app precisa do Python 3 (instale o Python 3).'); process.exit(1); }
3927
3868
  const r = vz.instalarApp({ python: py, dock: resto.includes('--dock') });
3928
3869
  console.log(r.ok ? `Primo.app instalado em ${r.app}. Abra pelo Launchpad, Spotlight ou Finder.` : `Não deu: ${r.erro || 'veja o log'}`);
3929
3870
  process.exit(r.ok ? 0 : 1);
3930
3871
  }
3931
- console.log('Uso: primocode voz nome <Nome> | primocode voz painel | primocode voz instalar-app [--dock] (o resto é /voice dentro da sessão)');
3872
+ console.log('Uso: primocode voz nome <Nome> | primocode voz painel | primocode voz instalar-app [--dock]');
3932
3873
  process.exit(0);
3933
3874
  }
3934
3875
  if (['doutor', 'doctor', 'preparar'].includes(process.argv[2])) {
@@ -4003,8 +3944,8 @@ async function main() {
4003
3944
  process.exit(0);
4004
3945
  }
4005
3946
  if (args[0] === '--help' || args[0] === '-h') {
4006
- console.log(welcomeBox(VERSION, null, { mascot: state.config.mascot, userName: state.config.userName, modo: state.mode }));
4007
- console.log(montarHelp());
3947
+ if (process.stdout.isTTY && !(process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '')) console.log(welcomeBox(VERSION, null, { mascot: state.config.mascot, userName: state.config.userName, modo: state.mode }));
3948
+ console.log(montarHelpEnxuto());
4008
3949
  process.exit(0);
4009
3950
  }
4010
3951
 
@@ -4150,7 +4091,7 @@ async function main() {
4150
4091
  if (agora - ultimoEsc < 2000) {
4151
4092
  ultimoEsc = 0;
4152
4093
  interromper();
4153
- if (chatEmVoo) { try { chatEmVoo.abort(); } catch {} }
4094
+ if (chatEmVoo) { try { chatEmVoo.abort(); } catch (e) { depurar('abortar o chat', e); } }
4154
4095
  console.log(c.warn(' ⏹ parando o agente…'));
4155
4096
  } else {
4156
4097
  ultimoEsc = agora;
package/lib/cli-nova.js CHANGED
@@ -98,13 +98,13 @@ function estudio(rest, { c } = {}) {
98
98
  if (!t0) {
99
99
  linhas.push(cor(c, 'white')('O que vamos criar?'));
100
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]'));
101
+ linhas.push(cor(c, 'dim')(' /studio <tipo> [o que criar]'));
102
102
  return { linhas };
103
103
  }
104
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
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
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 };
107
+ if (!briefing) return { linhas: [cor(c, 'white')(`O que vamos criar em ${tipo}?`), cor(c, 'dim')(` /studio ${tipo} <descrição do que criar>`)], tipo };
108
108
  return { linhas: [], tipo, pedido: briefing, instrucao: instrucaoDoTipo(tipo) };
109
109
  }
110
110
 
@@ -205,7 +205,6 @@ function buscar(rest, { c, dir } = {}) {
205
205
  }
206
206
 
207
207
  const SLASH_NOVOS = [
208
- ['/estudio', 'escolhe o que criar: slides, vídeo, motion, anúncio, abertura'],
209
208
  ['/agentes', 'seus agentes: lista · ligar · desligar · criar · apagar'],
210
209
  ['/piloto', 'piloto automático: on | off [grupo]'],
211
210
  ['/avisos', 'fila de aprovação e avisos · a <n> aprova · r <n> recusa'],
package/lib/nomes.js CHANGED
@@ -91,7 +91,7 @@ const COMANDOS = {
91
91
  taste: { grupo: 'video', antigos: ['gosto', 'referencias'] },
92
92
  brand: { grupo: 'video', antigos: ['marca'] },
93
93
  mascot: { grupo: 'video', antigos: ['mascote'] },
94
- voice: { grupo: 'video', antigos: ['voz'] },
94
+ studio: { grupo: 'video', antigos: ['estudio', 'estúdio'] },
95
95
  styles: { grupo: 'video', antigos: ['estilos', 'kits'] },
96
96
  // 9. Conta e sistema
97
97
  login: { grupo: 'conta', antigos: ['entrar', 'conectar'] },
@@ -167,7 +167,7 @@ const QUANDO = {
167
167
  taste: 'quer ver, refazer ou ensinar o gosto de motion (`/taste referencias` com vídeos que curtiu)',
168
168
  brand: 'quer cores, fontes e logo da marca em todo vídeo',
169
169
  mascot: 'quer escolher ou ver os mascotes',
170
- voice: 'quer falar com o Primo por voz',
170
+ studio: 'quer criar um vídeo, slides, motion, anúncio ou abertura (o Estúdio)',
171
171
  styles: 'quer ver os estilos de motion prontos (galeria com capa e preview; `/styles <estilo>`: um)',
172
172
  login: 'quer entrar na conta',
173
173
  logout: 'quer sair da conta neste computador',
@@ -103,7 +103,7 @@ function acharNavegador(env = process.env, existe = require('fs').existsSync) {
103
103
  function htmlParaPdf(htmlPath, pdfPath, { navegador = acharNavegador() } = {}) {
104
104
  if (!navegador) return { ok: false, erro: 'Chrome/Chromium não encontrado (defina PRIMO_CHROME)' };
105
105
  const { spawnSync } = require('child_process');
106
- const r = spawnSync(navegador, ['--headless=new', '--use-mock-keychain', '--password-store=basic', '--disable-gpu', '--no-pdf-header-footer',
106
+ const r = spawnSync(navegador, ['--headless=new', '--use-mock-keychain', '--password-store=basic', '--use-mock-keychain', '--password-store=basic', '--use-mock-keychain', '--password-store=basic', '--use-mock-keychain', '--password-store=basic', '--use-mock-keychain', '--password-store=basic', '--disable-gpu', '--no-pdf-header-footer',
107
107
  `--print-to-pdf=${pdfPath}`, 'file://' + htmlPath], { encoding: 'utf8', timeout: 90000 });
108
108
  return require('fs').existsSync(pdfPath) ? { ok: true } : { ok: false, erro: (r.stderr || '').slice(-300) || 'PDF não saiu' };
109
109
  }
@@ -180,7 +180,7 @@ function htmlParaPngs(htmlPath, baseSemExt, n, w, h, { escala = 1, navegador = a
180
180
  const tmp = `${baseSemExt}.tmp-${i}.html`;
181
181
  const alvo = `${baseSemExt}-${String(i).padStart(2, '0')}.png`;
182
182
  fs.writeFileSync(tmp, html.replace('</head>', `<style>section:not(:nth-of-type(${i})){display:none!important}html,body{height:${h}px;overflow:hidden}</style></head>`));
183
- spawnSync(navegador, ['--headless=new', '--use-mock-keychain', '--password-store=basic', '--disable-gpu', '--hide-scrollbars', `--force-device-scale-factor=${escala}`,
183
+ spawnSync(navegador, ['--headless=new', '--use-mock-keychain', '--password-store=basic', '--use-mock-keychain', '--password-store=basic', '--use-mock-keychain', '--password-store=basic', '--use-mock-keychain', '--password-store=basic', '--use-mock-keychain', '--password-store=basic', '--disable-gpu', '--hide-scrollbars', `--force-device-scale-factor=${escala}`,
184
184
  `--window-size=${w},${h}`, `--screenshot=${alvo}`, 'file://' + tmp], { encoding: 'utf8', timeout: 90000 });
185
185
  try { fs.unlinkSync(tmp); } catch {}
186
186
  if (fs.existsSync(alvo)) arquivos.push(alvo); else erro = `PNG ${i} não saiu`;
@@ -7,13 +7,13 @@
7
7
 
8
8
  const path = require('path');
9
9
 
10
- /* tipo → receita. `via`: 'roteiro' (plano pelo roteirista + render) ou 'motion' (a máquina de motion). */
10
+ /* tipo → receita. `via`: 'roteiro' (plano pelo roteirista + render) ou 'kit' (motion pelos estilos: os 50 kits ou um estilo próprio, lib/plugin/kits.js). */
11
11
  const TIPOS = Object.freeze({
12
12
  slides: { rotulo: 'Slides', via: 'roteiro', formato: '16:9', projeto: 'slides' },
13
13
  video: { rotulo: 'Vídeo curto', via: 'roteiro', formato: '9:16', projeto: 'video', duracao: [15, 60, 30] },
14
- motion: { rotulo: 'Motion', via: 'motion', formato: '9:16', duracao: [6, 60, 15] },
15
- anuncio: { rotulo: 'Anúncio', via: 'motion', formato: '9:16', duracao: [10, 30, 15], prefixo: 'Anúncio para chamar pra comprar: ' },
16
- abertura: { rotulo: 'Abertura de canal', via: 'motion', formato: '16:9', duracao: [5, 5, 5], prefixo: 'Abertura de canal com a marca: ' },
14
+ motion: { rotulo: 'Motion', via: 'kit', formato: '9:16', duracao: [6, 60, 15] },
15
+ anuncio: { rotulo: 'Anúncio', via: 'kit', formato: '9:16', duracao: [10, 30, 15], prefixo: 'Anúncio para chamar pra comprar: ' },
16
+ abertura: { rotulo: 'Abertura de canal', via: 'kit', formato: '16:9', duracao: [5, 5, 5], prefixo: 'Abertura de canal com a marca: ' },
17
17
  });
18
18
  const FORMATOS = ['9:16', '16:9', '1:1'];
19
19
 
@@ -28,7 +28,10 @@ function normalizar(args = {}) {
28
28
  if (briefing.length < 3) return { ok: false, error: 'falta o briefing: diga em uma frase o que criar.' };
29
29
  const formato = FORMATOS.includes(String(args.formato)) ? String(args.formato) : receita.formato;
30
30
  const duracao = receita.duracao ? limitar(args.duracao, receita.duracao) : undefined;
31
- return { ok: true, tipo, receita, briefing: briefing.slice(0, 600), formato, duracao };
31
+ const estilo = String(args.estilo || '').trim().slice(0, 60) || undefined;
32
+ const en = args.estilo_novo && typeof args.estilo_novo === 'object' ? args.estilo_novo : null;
33
+ const estiloNovo = en && String(en.nome || '').trim() ? { nome: String(en.nome).trim().slice(0, 40), descricao: String(en.descricao || '').slice(0, 300), base: en.base ? String(en.base).slice(0, 60) : undefined } : undefined;
34
+ return { ok: true, tipo, receita, briefing: briefing.slice(0, 600), formato, duracao, estilo, estiloNovo };
32
35
  }
33
36
 
34
37
  /** Os arquivos entregues, com o tipo que a tela sabe abrir. */
@@ -52,12 +55,12 @@ function tipoDoArquivo(c) {
52
55
 
53
56
  /**
54
57
  * @param {object} args { tipo, briefing, duracao?, formato? }
55
- * @param {object} deps { projeto, plano(args), motion(args), render(args), progresso(pct, etapa) }
58
+ * @param {object} deps { projeto, kit(args), plano(args), render(args), progresso(pct, etapa) }
56
59
  */
57
60
  async function criar(args, deps) {
58
61
  const n = normalizar(args);
59
62
  if (!n.ok) return n;
60
- const { tipo, receita, briefing, formato, duracao } = n;
63
+ const { tipo, receita, briefing, formato, duracao, estilo, estiloNovo } = n;
61
64
  const progresso = deps.progresso || (() => {});
62
65
  const falha = (error, extra) => ({ ok: false, error, ...extra });
63
66
 
@@ -69,12 +72,16 @@ async function criar(args, deps) {
69
72
  const id = proj.id;
70
73
  const base = { tipo, id, pasta: proj.pasta, formato, duracao, titulo };
71
74
 
72
- if (receita.via === 'motion') {
73
- progresso(15, 'Montando as cenas de motion');
74
- const m = await deps.motion({ id, assunto: pedido, formato, duracao });
75
- if (!m || m.ok === false) return falha((m && m.error) || 'o motion não saiu.', base);
76
- progresso(100, 'Pronto');
77
- return { ok: true, ...base, duracao: m.duracao || duracao, arquivo: m.video, arquivos: arquivosDe(m), resumo: `${receita.rotulo} de ${m.duracao || duracao} s.` };
75
+ if (receita.via === 'kit') {
76
+ /* Motion SEMPRE pelos kits (11.1): escolhe o estilo (um dos 50, um criado, ou o da tela de estilos) e monta a pasta
77
+ do vídeo com as peças desmontadas dele. A cena a IA compõe com as peças e renderiza com `primocode kit pecas`.
78
+ Nunca cai no gerador antigo (estudio_motion): quem quiser esse caminho chama a ferramenta dele. */
79
+ progresso(15, 'Escolhendo o estilo e montando as pastas');
80
+ const k = await (deps.kit || ((a) => require('./kit-projeto.js').montar(a)))({ pasta: proj.pasta, pedido, formato, duracao, estilo, estiloNovo });
81
+ if (!k || k.ok === false) return falha((k && k.error) || 'não consegui montar o projeto do estilo.', { ...base, ...(k && k.precisa ? { precisa: k.precisa } : {}), ...(k && k.menu ? { menu: k.menu } : {}) });
82
+ progresso(40, 'Pastas do estilo prontas');
83
+ return { ok: true, ...base, kit: k.slug, estilo: k.nome, pastaKit: k.pasta, pecas: k.pecas, categorias: k.categorias, etapa: 'cena', proximo: k.proximo,
84
+ resumo: `${receita.rotulo} no estilo ${k.nome}: pastas montadas, falta compor a cena e renderizar.` };
78
85
  }
79
86
 
80
87
  progresso(15, 'Escrevendo o roteiro');
@@ -99,9 +106,9 @@ module.exports = { criar, normalizar, TIPOS, FORMATOS, tipoDoArquivo, arquivosDe
99
106
  const QUADRADOS = Object.freeze({
100
107
  slides: 'Tipo escolhido na tela: Apresentação de slides. Use estudio_criar com tipo "slides" e o briefing do pedido (a skill slides-que-prendem ajuda no conteúdo).',
101
108
  video: 'Tipo escolhido na tela: Vídeo curto vertical. Use estudio_criar com tipo "video" e o briefing do pedido.',
102
- motion: 'Tipo escolhido na tela: Motion. Use estudio_criar com tipo "motion" e o briefing do pedido.',
103
- anuncio: 'Tipo escolhido na tela: Anúncio de 15 s. Use estudio_criar com tipo "anuncio" e o briefing do pedido.',
104
- abertura: 'Tipo escolhido na tela: Abertura de canal de 5 s. Use estudio_criar com tipo "abertura" e o briefing do pedido.',
109
+ motion: 'Tipo escolhido na tela: Motion (pelos estilos/kits). Use estudio_criar com tipo "motion", o briefing e o estilo (um dos kits do menu, ou estilo_novo; sem estilo vale o da tela de estilos). Depois siga o caminho "primocode kit pecas <estilo>" que a ferramenta devolve.',
110
+ anuncio: 'Tipo escolhido na tela: Anúncio de 15 s em motion (pelos estilos/kits). Use estudio_criar com tipo "anuncio", o briefing e o estilo (um dos kits do menu, ou estilo_novo). Depois siga o caminho "primocode kit pecas <estilo>" que a ferramenta devolve.',
111
+ abertura: 'Tipo escolhido na tela: Abertura de canal de 5 s em motion (pelos estilos/kits). Use estudio_criar com tipo "abertura", o briefing e o estilo (um dos kits do menu, ou estilo_novo). Depois siga o caminho "primocode kit pecas <estilo>" que a ferramenta devolve.',
105
112
  pagina: 'Tipo escolhido na tela: Página ou app. Construa com write_file na pasta do projeto, confira abrindo no navegador e entregue o arquivo principal. Não use o Estúdio.',
106
113
  bug: 'Tipo escolhido na tela: Corrigir um bug. Ache a causa lendo o código do projeto (read_file, grep), corrija com edit_file e crie um teste que pegue o caso; rode os testes e conte o que mudou. Não use o Estúdio.',
107
114
  pr: 'Tipo escolhido na tela: Revisar um PR. Leia o diff do PR (o número ou o link que a pessoa der; gh ou git diff), aponte os riscos do mais grave ao menos grave e sugira a correção. Não use o Estúdio.',
@@ -7,7 +7,7 @@ const path = require('path');
7
7
  const { tipoDoArquivo } = require('./criar');
8
8
  const Miniatura = require('./miniatura');
9
9
 
10
- const PULAR = new Set(['node_modules', 'remotion', 'midia', 'entrada', 'versoes', '.git']);
10
+ const PULAR = new Set(['node_modules', 'remotion', 'midia', 'entrada', 'versoes', '.git', 'pecas']);
11
11
  const SAIDAS = new Set(['video', 'pdf', 'imagem']);
12
12
 
13
13
  /* Procura as saídas em até 2 níveis; imagem solta (quadro de conferência) serve de miniatura, não de entrega. */
@@ -750,12 +750,12 @@ async function renderMotion(args, p, receita) {
750
750
 
751
751
  // ── estudio_criar ────────────────────────────────────────────────────────
752
752
 
753
- /** A porta simples (criar.js): escolhe a receita e chama as ferramentas de cima. `ctx.progresso(pct, etapa)` alimenta o item da tela. */
753
+ /** A porta simples (criar.js): escolhe a receita e chama as ferramentas de cima (motion vai pelos kits: kit-projeto.js). `ctx.progresso(pct, etapa)` alimenta o item da tela. */
754
754
  async function ferramentaCriar(args, ctx = {}) {
755
755
  return require('./criar.js').criar(args, {
756
756
  projeto,
757
757
  plano: (a) => ferramentaPlano({ ...a, __pasta: ctx.projectDir }),
758
- motion: (a) => ferramentaMotion(a),
758
+ kit: (a) => require('./kit-projeto.js').montar({ ...a, projectDir: ctx.projectDir }, { aoFalar: ctx.aoFalar }),
759
759
  render: (a) => ferramentaRender({ ...a, __pasta: ctx.projectDir }, ctx),
760
760
  progresso: ctx.progresso,
761
761
  });
@@ -0,0 +1,69 @@
1
+ // O motion pelo sistema de estilos (kits): o `estudio_criar` dos tipos motion, anuncio e abertura passa por aqui.
2
+ // Resolve o estilo (um dos 50, um que a pessoa criou, ou a escolha dela na tela de estilos) e monta a pasta do vídeo
3
+ // com as peças desmontadas do estilo (formas, texturas, transições, tipografia, câmera, sons, trilha, cores, fontes,
4
+ // regras). A cena em si a IA compõe do zero com essas peças e renderiza com `primocode kit pecas` (lib/plugin/kits.js).
5
+ // Nada de gerador antigo aqui: é a 11.1 de kits, só que com a porta simples na frente. Node puro.
6
+ 'use strict';
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const semReferencia = (src) => !/[\\/]referencia([\\/]|$)/.test(src);
12
+
13
+ /**
14
+ * @param {object} a { pasta (do projeto), pedido, formato, duracao, estilo?, estiloNovo?: {nome, descricao, base, ref?}, home?, projectDir? }
15
+ * @param {object} d { K (lib/plugin/kits.js), MAT (materiais.js), T (kits-tela.js), aoFalar }
16
+ */
17
+ async function montar(a, d = {}) {
18
+ const K = d.K || require('../kits.js');
19
+ const MAT = d.MAT || require('./materiais.js');
20
+ const op = { home: a.home, projectDir: a.projectDir || a.pasta, env: d.env, aoFalar: d.aoFalar };
21
+ let slug = null, nome = null, origem = null;
22
+
23
+ // 1) estilo novo, pedido pela IA: copia as peças de um estilo base e devolve o slug
24
+ if (a.estiloNovo && a.estiloNovo.nome) {
25
+ const e = a.estiloNovo;
26
+ const argv = ['criar', '--nome', String(e.nome), '--descricao', String(e.descricao || e.nome)];
27
+ if (e.base) argv.push('--base', String(e.base));
28
+ if (e.ref) argv.push('--ref', String(e.ref));
29
+ const r = await K.main(argv, op);
30
+ if (!r.ok || !r.slug) return { ok: false, error: r.texto || 'não consegui criar o estilo.' };
31
+ slug = r.slug; origem = 'criado';
32
+ }
33
+ // 2) estilo nomeado (slug ou nome) entre os 50 e os da pessoa
34
+ if (!slug && a.estilo) {
35
+ const k = K.achar(a.estilo, op);
36
+ if (!k) return { ok: false, error: `o estilo "${a.estilo}" não existe. Escolha um do menu ou crie um novo (estilo_novo).`, menu: K.menuParaIA(op) };
37
+ slug = k.slug; nome = k.nome; origem = 'escolhido';
38
+ }
39
+ // 3) a escolha dela na tela de estilos (no primeiro motion a tela abre e a escolha volta)
40
+ if (!slug) {
41
+ const m = await K.main(['menu'], op);
42
+ if (m && m.escolha) { slug = m.escolha; origem = 'tela'; }
43
+ else return { ok: false, precisa: 'estilo', error: 'falta escolher o estilo: chame de novo com estilo=<slug> (um do menu) ou estilo_novo={nome, descricao, base}.', menu: m && m.texto ? m.texto : K.menuParaIA(op) };
44
+ }
45
+ if (!MAT.migrado(slug)) return { ok: false, error: `o estilo ${slug} não tem as peças soltas (materiais/${slug}).` };
46
+ if (!nome) { const k = K.achar(slug, op); nome = k ? k.nome : slug; }
47
+
48
+ // a pasta do vídeo: motion/pecas/<categorias> + kit.json + LEIA.md
49
+ const raiz = path.join(a.pasta, 'motion');
50
+ const pecas = path.join(raiz, 'pecas');
51
+ fs.rmSync(pecas, { recursive: true, force: true });
52
+ fs.mkdirSync(raiz, { recursive: true });
53
+ fs.cpSync(MAT.pastaDe(slug), pecas, { recursive: true, filter: semReferencia });
54
+ const pastas = fs.readdirSync(pecas, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
55
+ const arquivos = fs.readdirSync(pecas, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name).sort();
56
+ const kit = { slug, nome, origem, formato: a.formato, duracao: a.duracao || null, pedido: a.pedido, pecas: path.relative(a.pasta, pecas), criado: new Date().toISOString() };
57
+ fs.writeFileSync(path.join(raiz, 'kit.json'), JSON.stringify(kit, null, 2) + '\n');
58
+ const proximo = [
59
+ `Estilo ${nome} (${slug}): as peças estão em ${pecas} (${pastas.join(', ')}).`,
60
+ `1) primocode kit pecas ${slug} regras (identidade e densidade mínima)`,
61
+ `2) primocode kit pecas ${slug} ver <peça> (código e params das peças que vai usar)`,
62
+ `3) escreva a cena nova em TSX com o conteúdo do pedido e grave: primocode kit pecas ${slug} escrever <nome> --arquivo cena.tsx --pasta ${a.pasta}`,
63
+ `4) renderize: primocode kit pecas ${slug} renderizar <nome> --pasta ${a.pasta} (o vídeo sai em ${path.join(a.pasta, 'saida')})`,
64
+ ].join('\n');
65
+ fs.writeFileSync(path.join(raiz, 'LEIA.md'), `# ${nome}\n\nPedido: ${a.pedido}\n\n${proximo}\n`);
66
+ return { ok: true, slug, nome, origem, pasta: raiz, pecas, categorias: pastas, arquivos, proximo };
67
+ }
68
+
69
+ module.exports = { montar };
@@ -460,8 +460,9 @@ async function cmdPecas(a, op) {
460
460
  const [estilo, sub, alvo] = a._;
461
461
  if (!estilo) return { ok: true, texto: `Estilos com peças soltas: ${MATERIAIS.estilos().join(', ') || 'nenhum'}. Uso: primocode kit pecas <estilo> [ver <peça> | regras | escrever <nome> --arquivo <cena.tsx> | conferir <cena.tsx> | renderizar <nome>]` };
462
462
  if (!MATERIAIS.migrado(estilo)) return { ok: false, texto: `O estilo "${estilo}" ainda não tem peças soltas (migrados: ${MATERIAIS.estilos().join(', ') || 'nenhum'}).` };
463
- const pasta = op.projectDir || process.cwd();
464
- const lerArq = (f) => fs.readFileSync(path.resolve(pasta, String(f)), 'utf8');
463
+ // --pasta <projeto>: a pasta do vídeo que o estudio_criar montou (motion/ e saida/ ficam nela)
464
+ const pasta = a.pasta ? path.resolve(String(a.pasta)) : (op.projectDir || process.cwd());
465
+ const lerArq = (f) => { const aqui = path.resolve(op.projectDir || process.cwd(), String(f)); return fs.readFileSync(fs.existsSync(aqui) ? aqui : path.resolve(pasta, String(f)), 'utf8'); };
465
466
  try {
466
467
  switch (sub) {
467
468
  case undefined: case 'listar': return MATERIAIS.listar(estilo);
@@ -477,7 +478,7 @@ async function cmdPecas(a, op) {
477
478
  const dir = path.join(pasta, 'motion', `.render-${alvo}`);
478
479
  MATERIAIS.prepararRender(estilo, dir, arq, { duracao: a.quadros ? Number(a.quadros) : undefined });
479
480
  ligarModulos(dir, est.modulos);
480
- const saida = path.resolve(pasta, String(a.saida || path.join('motion', `${alvo}.mp4`)));
481
+ const saida = path.resolve(pasta, String(a.saida || (a.pasta ? path.join('saida', `${alvo}.mp4`) : path.join('motion', `${alvo}.mp4`))));
481
482
  const CQ = require('./estudio/conferir-quadros.js');
482
483
  const cenas = CQ.cenasDe(fs.readFileSync(arq, 'utf8'));
483
484
  const r = await rodarRender(dir, saida, { escala: Number(a.escala) || 1, quadros: cenas.map((c) => c.meio), aoFalar: op.aoFalar || (() => {}) });
@@ -506,7 +507,7 @@ function opcoes(args) {
506
507
  const AJUDA = `primocode kit — os estilos de motion (peças soltas)
507
508
  menu o menu dos estilos (no primeiro motion abre a tela de estilos)
508
509
  ver <slug> as peças e as regras do estilo (igual a pecas <slug>)
509
- pecas <estilo> [ver <peça>|regras|escrever <nome> --arquivo <tsx>|conferir <tsx>|renderizar <nome>]
510
+ pecas <estilo> [ver <peça>|regras|escrever <nome> --arquivo <tsx>|conferir <tsx>|renderizar <nome>] [--pasta <projeto>]
510
511
  a IA compõe a cena do zero com as peças; o conferir cobra a densidade
511
512
  referencia abre a tela de referências e devolve o que mediu
512
513
  criar --nome "<nome>" --descricao "<como é>" [--base <slug>] [--ref <vídeo>] [--quando "a; b; c"] [--familia "<família>"]
@@ -1200,7 +1200,8 @@ async function runActInterno(userPrompt, opts) {
1200
1200
  try {
1201
1201
  const kits = require('../../plugin/kits.js');
1202
1202
  kits.novoTurno();
1203
- const cabeca = kits.cabecaDoPedido(userPrompt, { projectDir });
1203
+ // a instrução do quadrado (Motion, Anúncio, Abertura) também conta: o pedido sozinho ("bolo de pote") não parece motion
1204
+ const cabeca = kits.cabecaDoPedido(instrucao ? `${userPrompt}\n${instrucao}` : userPrompt, { projectDir });
1204
1205
  // o vídeo esperando o sim vai colado na resposta da pessoa (é dela que a IA decide); o menu, na frente
1205
1206
  if (cabeca && /^\[VÍDEO (ESPERANDO|SENDO MONTADO)/.test(cabeca) && pedido.endsWith(userPrompt)) pedido = `${pedido.slice(0, pedido.length - userPrompt.length)}${cabeca}\n\nRESPOSTA DA PESSOA: ${userPrompt}`;
1206
1207
  else if (cabeca) pedido = `${cabeca}\n\n${pedido}`;
@@ -1488,7 +1489,7 @@ async function runActInterno(userPrompt, opts) {
1488
1489
  emVoo = null;
1489
1490
  if (!subagent && !avisouReserva && modeloDaVolta && modeloDaVolta.reserva) {
1490
1491
  avisouReserva = true;
1491
- ev('linha', { texto: ` ⎿ ${linhaDaReserva(modeloDaVolta.modelo)}`, nivel: 'dim' });
1492
+ ev('linha', { texto: ` ⎿ ${linhaDaReserva(modeloDaVolta.modelo)}`, nivel: 'aviso' });
1492
1493
  }
1493
1494
  traco(`volta ${voltas} voltou em ${Math.round((Date.now() - tRodada) / 1000)}s · ${toolCalls.map((t) => t.name).join(', ') || 'texto'}${streamError ? ' · erro: ' + String(streamError).slice(0, 160) : ''}`);
1494
1495
  tempos.somar(subagent ? 'rodada do subagente' : 'rodada do modelo', Date.now() - tRodada);
@@ -82,8 +82,8 @@ const GRUPOS = [
82
82
  nome: 'Estúdio',
83
83
  resumo: 'vídeo, motion, slides e arte — a IA faz, e no fim abre o resultado pronto',
84
84
  itens: [
85
- { tool: 'estudio_criar', escreve: true, faz: 'a porta simples: cria slides, vídeo, motion, anúncio ou abertura do pedido ao arquivo, e abre o resultado',
86
- args: 'tipo (slides|video|motion|anuncio|abertura). briefing (uma frase). duracao (s, opcional). formato (9:16|16:9|1:1, opcional). Para controle fino use as outras estudio_*.' },
85
+ { tool: 'estudio_criar', escreve: true, faz: 'a porta simples: slides e vídeo vão do pedido ao arquivo; motion, anúncio e abertura escolhem um dos 50 estilos (kits) e montam as pastas do vídeo',
86
+ args: 'tipo (slides|video|motion|anuncio|abertura). briefing. duracao (s). formato (9:16|16:9|1:1). Motion: estilo (slug do kit) ou estilo_novo {nome, descricao, base}; depois, primocode kit pecas.' },
87
87
  { tool: 'estudio_projeto', escreve: true, faz: 'abre, acha e lista os projetos do estúdio — cada peça é uma pasta que não some',
88
88
  args: 'acao: criar | abrir | procurar | listar | anotar | editar | assinatura. '
89
89
  + 'assinatura: quando o usuário diz o que achou de um vídeo ("adorei a virada", "texto demais"), grave com texto (e o id) — o próximo vídeo parte disso; sem texto, lê as faixas e as notas. '
package/lib/tui/ui.js CHANGED
@@ -36,9 +36,11 @@ const PALETTE = {
36
36
  reset: '\x1b[0m',
37
37
  };
38
38
 
39
- /* NO_COLOR (https://no-color.org): sem nenhuma cor. Negrito e reset ficam. */
40
- if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') {
41
- for (const k of Object.keys(PALETTE)) if (k !== 'bold' && k !== 'reset') PALETTE[k] = '';
39
+ /* Sem cor: NO_COLOR (https://no-color.org) ou a saída não é um terminal (pipe, arquivo, CI), salvo FORCE_COLOR.
40
+ Nenhum código ANSI sai, nem negrito. */
41
+ const SEM_COR = (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') || (!process.stdout.isTTY && !process.env.FORCE_COLOR);
42
+ if (SEM_COR) {
43
+ for (const k of Object.keys(PALETTE)) PALETTE[k] = '';
42
44
  }
43
45
 
44
46
  /* O que a barra da direita da tela cheia mostra e que vem do trabalho do agente:
@@ -418,6 +420,7 @@ let temaAtual = 'marca';
418
420
  function definirTema(id) {
419
421
  const t = TEMAS[id];
420
422
  if (!t) return false;
423
+ if (SEM_COR) { temaAtual = id; return true; }
421
424
  PALETTE.brand = `\x1b[38;5;${t.brand}m`; PALETTE.brandDim = `\x1b[38;5;${t.brandDim}m`;
422
425
  PALETTE.ok = PALETTE.brand; PALETTE.cyan = PALETTE.brand;
423
426
  temaAtual = id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "11.3.1",
3
+ "version": "11.3.3",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  "postinstall": "node lib/preparar.js || true",
12
12
  "test:server": "cd server && npm test",
13
13
  "test": "node scripts/testes.js",
14
- "test:app": "node test/app-fluxo.test.mjs && node test/app-primeira.test.mjs && node test/app-entrar.test.mjs && node test/app-tela-estudio.test.mjs && node test/app-celular-telas.test.mjs && node test/app-compositor-voz.test.mjs",
14
+ "test:app": "node test/app-fluxo.test.mjs && node test/app-primeira.test.mjs && node test/app-entrar.test.mjs && node test/app-tela-estudio.test.mjs && node test/app-celular-telas.test.mjs && node test/app-compositor-voz.test.mjs && node test/app-render-celular.test.mjs && node test/app-nuvem-estudio.test.mjs",
15
15
  "app:build": "cd app && npm run dist",
16
16
  "app:dev": "cd app && npm start",
17
17
  "mobile:build": "cd mobile && npm run build"
@@ -117,11 +117,10 @@ a{color:#67e8f9}a:hover{color:#22d3ee}
117
117
  .enviar:hover{transform:scale(1.08);background:#4a7de0}
118
118
  .enviar:active{transform:scale(.92)}
119
119
  .enviar svg{position:absolute;transition:opacity .22s,transform .45s cubic-bezier(.3,1.7,.5,1)}
120
- .enviar .i-voz,.enviar .i-seta,.enviar .i-stop{opacity:0;transform:scale(.3) rotate(-90deg)}
121
- .enviar.vazio .i-voz{opacity:1;transform:none}
120
+ .enviar .i-seta,.enviar .i-stop{opacity:0;transform:scale(.3) rotate(-90deg)}
122
121
  .enviar.cheio .i-seta{opacity:1;transform:none}
123
122
  .enviar.parar .i-stop{opacity:1;transform:none}
124
- .enviar.semvoz{visibility:hidden;pointer-events:none}
123
+ .enviar.vazio{visibility:hidden;pointer-events:none}
125
124
  /* mascote e título */
126
125
  .mascote{transition:transform .35s cubic-bezier(.3,1.5,.5,1);filter:drop-shadow(0 0 26px rgba(34,211,238,.35))}
127
126
  .pula-entra{animation:cai .9s cubic-bezier(.3,1.6,.5,1) both}
@@ -354,8 +353,7 @@ a{color:#67e8f9}a:hover{color:#22d3ee}
354
353
  <button class="chip" id="chipModo" data-a="menuModo"></button>
355
354
  <button class="chip" id="chipEf" data-a="menuEf" hidden></button>
356
355
  </div>
357
- <button class="enviar vazio" id="enviar" data-a="enviar" aria-label="Abrir a voz do Primo">
358
- <svg class="i-voz" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M5 10v4M9.5 7v10M14 4.5v15M18.5 8v8"></path></svg>
356
+ <button class="enviar vazio" id="enviar" data-a="enviar" aria-label="Enviar">
359
357
  <svg class="i-seta" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"></path></svg>
360
358
  <svg class="i-stop" width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2.5"></rect></svg>
361
359
  </button>
@@ -413,7 +411,6 @@ var ICO={
413
411
  var ABAS=[
414
412
  {id:'geral',t:'Geral',cor:'#6b6b66',ico:'<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M4.9 19.1 7 17M17 7l2.1-2.1"></path>'},
415
413
  {id:'modelos',t:'Modelos',cor:'#7c5cff',ico:'<path d="M12 3l2.2 5.8L20 11l-5.8 2.2L12 19l-2.2-5.8L4 11l5.8-2.2Z"></path>'},
416
- {id:'voz',t:'Voz',cor:'#f0597a',ico:'<path d="M5 10v4M9.5 7v10M14 4.5v15M18.5 8v8"></path>'},
417
414
  {id:'conta',t:'Conta',cor:'#0ea5b7',ico:'<path d="M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8ZM4 21a8 8 0 0 1 16 0"></path>'}
418
415
  ];
419
416
  var MODOS=[
@@ -667,7 +664,7 @@ var estado={
667
664
  anexos:[],menu:false,aviso:null,
668
665
  skills:null,ajustes:null,busca:'',
669
666
  comandos:[],pal:{aberta:false,itens:[],sel:0,q:null},abertas:{},confirma:null,provSel:'',chaveDraft:'',chaveSalva:false,
670
- semVoz:false,arqBase:'' /* o app desktop manda semVoz (sem motor de voz) e arqBase (o protocolo que serve os arquivos); no Mac ficam vazios */
667
+ arqBase:'' /* o app desktop manda arqBase (o protocolo que serve os arquivos); no Mac fica vazio */
671
668
  };
672
669
  var animar=false;
673
670
  var mx=0,my=0;
@@ -841,11 +838,10 @@ function htmlSkills(anim){
841
838
  /* ajustes */
842
839
  function tg(a,on,rot,extra){return '<button class="tg'+(on?' on':'')+'" data-a="'+a+'" aria-label="'+esc(rot)+'"'+(extra||'')+'></button>';}
843
840
  function htmlConfig(anim){
844
- var abas=ABAS.filter(function(a){return !(estado.semVoz&&a.id==='voz');}).map(function(a){
841
+ var abas=ABAS.map(function(a){
845
842
  return '<button class="lado-cfg'+(estado.aba===a.id?' on':'')+'" data-a="aba" data-id="'+a.id+'"><span class="tile" style="background:'+a.cor+'">'+ic(a.ico,14,2.2)+'</span>'+a.t+'</button>';
846
843
  }).join('');
847
- if(estado.semVoz&&estado.aba==='voz')estado.aba='modelos';
848
- var corpo={geral:cfgGeral,modelos:cfgModelos,voz:cfgVoz,conta:cfgConta}[estado.aba]();
844
+ var corpo={geral:cfgGeral,modelos:cfgModelos,conta:cfgConta}[estado.aba]();
849
845
  return '<div class="'+(anim?'tela-entra':'')+'" style="width:100%;max-width:860px;align-self:center;margin-bottom:auto;display:flex;flex-wrap:wrap;gap:32px">'+
850
846
  '<nav style="flex:1 1 170px;max-width:210px;display:flex;flex-direction:column;gap:2px"><h2 style="margin:0 0 14px 8px;font-family:\'Instrument Serif\',serif;font-weight:400;font-size:34px">Ajustes</h2>'+abas+'</nav>'+
851
847
  '<div style="flex:999 1 440px;min-width:0">'+corpo+'</div></div>';
@@ -882,12 +878,6 @@ function cfgModelos(){
882
878
  }
883
879
  return h;
884
880
  }
885
- function cfgVoz(){
886
- var A=estado.ajustes;
887
- if(!A||!A.voz)return h3('Voz')+'<p style="color:#8a8983;font-size:13px;margin:16px 4px">Carregando os ajustes.</p>';
888
- return h3('Voz')+'<div class="grupo"><div class="lin"><label for="vozNome" style="min-width:0">Nome do assistente<small>é a palavra que chama o Primo na voz</small></label><input id="vozNome" class="campo campo-s" data-in="voz_nome" autocomplete="off" value="'+esc(A.voz.nome||'')+'"></div>'+
889
- '<div class="lin"><span>Sons</span>'+tg('sons',!!A.voz.sons,'Sons')+'</div></div>';
890
- }
891
881
  function cfgConta(){
892
882
  if(!estado.logado)return h3('Conta')+'<div class="grupo"><div class="lin"><span>Entre pra salvar as conversas e usar o Primo.</span><button class="entrar" data-a="entrar">Entrar</button></div></div>';
893
883
  var ini=(String(estado.email||'?').trim().charAt(0)||'?').toUpperCase();
@@ -1161,14 +1151,13 @@ function renderAvisos(){
1161
1151
  var a=estado.aviso,el=$('avisos');
1162
1152
  if(!a){el.innerHTML='';return;}
1163
1153
  if(a.tipo==='login')el.innerHTML='<div class="cartao-av"><span>Entre na sua conta do PrimoCode pra conversar.</span><span style="display:flex;align-items:center;gap:6px"><button class="entrar" data-a="entrar">Entrar</button><button class="x" data-a="fechaAviso" aria-label="Fechar">'+ic(ICO.x,12,2.4)+'</button></span></div>';
1164
- else if(a.tipo==='voz')el.innerHTML='<div class="cartao-av"><span style="min-width:0;overflow-wrap:anywhere">'+esc(a.texto)+'</span><span style="display:flex;align-items:center;gap:6px">'+(a.instalar?'<button class="entrar" data-a="vozInstalar">Instalar a voz</button>':'')+(a.instalando?'':'<button class="x" data-a="fechaAviso" aria-label="Fechar">'+ic(ICO.x,12,2.4)+'</button>')+'</span></div>';
1165
1154
  else if(a.tipo==='sair')el.innerHTML='<div class="cartao-av"><span>Sair da conta?</span><span style="display:flex;align-items:center;gap:6px"><button class="mc-b" data-a="fechaAviso">Cancelar</button><button class="entrar" style="background:#b8473d" data-a="sairConfirma">Sair</button></span></div>';
1166
1155
  else el.innerHTML='<div class="cartao-av erro"><span style="min-width:0;overflow-wrap:anywhere">'+esc(a.texto)+'</span><button class="x" data-a="fechaAviso" aria-label="Fechar">'+ic(ICO.x,12,2.4)+'</button></div>';
1167
1156
  }
1168
1157
  function atualizaBotao(){
1169
1158
  var b=$('enviar'),vazio=!ta().value.trim()&&!estado.anexos.length;
1170
1159
  if(estado.esperando){b.className='enviar parar';b.setAttribute('aria-label','Parar');}
1171
- else if(vazio){b.className='enviar vazio'+(estado.semVoz?' semvoz':'');b.setAttribute('aria-label','Abrir a voz do Primo');}
1160
+ else if(vazio){b.className='enviar vazio';b.setAttribute('aria-label','Enviar');}
1172
1161
  else{b.className='enviar cheio';b.setAttribute('aria-label','Enviar');}
1173
1162
  }
1174
1163
  function cresce(){var t=ta();t.style.height='auto';t.style.height=Math.min(t.scrollHeight,160)+'px';}
@@ -1307,13 +1296,6 @@ function appCmd(c,args){
1307
1296
  var u=ultimaResposta();
1308
1297
  if(u){copiarTexto(u,{textContent:'Copiar'});toast('Copiado');}else toast('Nada pra copiar');
1309
1298
  break;
1310
- case 'voz':if(!estado.semVoz)post({acao:'voz'});break;
1311
- case 'voz_nome':
1312
- estado.aba='voz';post({acao:'ajustes'});
1313
- if(args){post({acao:'ajuste',chave:'voz_nome',valor:String(args)});if(estado.ajustes&&estado.ajustes.voz)estado.ajustes.voz.nome=String(args);}
1314
- navegar('config');
1315
- setTimeout(function(){var e=$('vozNome');if(e)e.focus();},80);
1316
- break;
1317
1299
  case 'effort':
1318
1300
  var efc=estado.ajustes&&estado.ajustes.effort,nvs=efc?efNiveis().map(function(x){return x.nome;}):['low','medium','high','xhigh','max','ultracode'];
1319
1301
  if(al){
@@ -1351,7 +1333,7 @@ function enviar(pular){
1351
1333
  }
1352
1334
  function apertar(){
1353
1335
  if(estado.esperando){post({acao:'parar'});estado.esperando=false;estado.passos=[];renderEspera();atualizaBotao();return;}
1354
- if(!ta().value.trim()&&!estado.anexos.length){if(!estado.semVoz)post({acao:'voz'});return;}
1336
+ if(!ta().value.trim()&&!estado.anexos.length)return;
1355
1337
  enviar();
1356
1338
  }
1357
1339
  function setModo(v){
@@ -1453,7 +1435,6 @@ document.addEventListener('click',function(e){
1453
1435
  case 'entrar':post({acao:'entrar'});break;
1454
1436
  case 'sair':post({acao:'sair_conta'});break;
1455
1437
  case 'fechaAviso':estado.aviso=null;renderAvisos();break;
1456
- case 'vozInstalar':estado.aviso={tipo:'voz',texto:'Preparando a voz…',instalando:true};renderAvisos();post({acao:'voz_instalar'});break;
1457
1438
  case 'link':e.preventDefault();post({acao:'abrir_link',url:d.url});break;
1458
1439
  case 'skill':
1459
1440
  var k=estado.skills&&estado.skills[+d.i];
@@ -1480,13 +1461,6 @@ document.addEventListener('click',function(e){
1480
1461
  estado.chaveDraft='';estado.chaveSalva=true;renderPrincipal();
1481
1462
  break;
1482
1463
  case 'pasta':post({acao:'escolher_pasta'});break;
1483
- case 'sons':
1484
- if(estado.ajustes&&estado.ajustes.voz){
1485
- estado.ajustes.voz.sons=!estado.ajustes.voz.sons;
1486
- post({acao:'ajuste',chave:'voz_sons',valor:!!estado.ajustes.voz.sons});
1487
- renderPrincipal();
1488
- }
1489
- break;
1490
1464
  }
1491
1465
  });
1492
1466
  $('lb').addEventListener('click',function(){$('lb').hidden=true;});
@@ -1526,13 +1500,6 @@ document.addEventListener('input',function(e){
1526
1500
  document.addEventListener('change',function(e){
1527
1501
  var t=e.target;
1528
1502
  if(t.id==='provSel'){estado.provSel=t.value;estado.chaveSalva=false;renderPrincipal();}
1529
- else if(t.getAttribute&&t.getAttribute('data-in')==='voz_nome'){
1530
- var v=t.value.trim();
1531
- if(estado.ajustes&&estado.ajustes.voz&&v!==estado.ajustes.voz.nome){
1532
- estado.ajustes.voz.nome=v;
1533
- post({acao:'ajuste',chave:'voz_nome',valor:v});
1534
- }
1535
- }
1536
1503
  });
1537
1504
  document.addEventListener('keydown',function(e){
1538
1505
  if(e.key==='Escape'&&!$('lb').hidden){$('lb').hidden=true;return;}
@@ -1553,7 +1520,6 @@ document.addEventListener('keydown',function(e){
1553
1520
  if(e.key==='Escape'&&estado.menu){estado.menu=false;renderMenu();}
1554
1521
  if(e.target.id==='pergunta'&&e.key==='Enter'&&!e.shiftKey&&!e.isComposing){e.preventDefault();enviar();}
1555
1522
  if(e.target.id==='chaveApi'&&e.key==='Enter'){e.preventDefault();var b=$('chaveBtn');if(b)b.click();}
1556
- if(e.target.id==='vozNome'&&e.key==='Enter'){e.preventDefault();e.target.blur();}
1557
1523
  });
1558
1524
 
1559
1525
  /* arrastar e soltar */
@@ -1604,7 +1570,7 @@ var tratar={
1604
1570
  sincConta(v);
1605
1571
  if(v.pasta!=null)estado.pasta=v.pasta;
1606
1572
  if(v.modo==='chat'||v.modo==='agent')estado.modo=v.modo;
1607
- estado.semVoz=!!v.semVoz;estado.arqBase=String(v.arqBase||'');atualizaBotao();
1573
+ estado.arqBase=String(v.arqBase||'');atualizaBotao();
1608
1574
  renderConta();renderMenu();renderAvisos();
1609
1575
  if(estado.tela==='config')renderPrincipal();
1610
1576
  if(estado.logado)pedirConversas();
@@ -1662,10 +1628,6 @@ var tratar={
1662
1628
  else{estado.aviso={tipo:'erro',texto:txt};renderAvisos();}
1663
1629
  }else{estado.aviso={tipo:'erro',texto:txt};renderAvisos();}
1664
1630
  },
1665
- vozAviso:function(v){ /* o app desktop: a voz não está instalada, está instalando ou terminou */
1666
- if(!v)return;
1667
- estado.aviso={tipo:'voz',texto:String(v.texto||''),instalar:!!v.instalar,instalando:!!v.instalando};renderAvisos();
1668
- },
1669
1631
  anexo:function(v){
1670
1632
  if(!v||!v.caminho)return;
1671
1633
  if(estado.anexos.some(function(a){return a.caminho===v.caminho;}))return;
@@ -1730,7 +1692,7 @@ if(location.search.indexOf('demo')>=0&&!(window.webkit&&window.webkit.messageHan
1730
1692
  d1:{id:'d1',titulo:'Botão do WhatsApp no topo',mensagens:[{papel:'voce',texto:'Põe um botão do WhatsApp fixo no topo'},{papel:'primo',texto:'Pronto: o botão fica **fixo no topo**.\n\n- abre a conversa\n- mensagem já escrita\n\nVeja `index.html`:\n```html\n<a class="zap">Fale comigo</a>\n```\nMais em [o site](https://example.com).'}]},
1731
1693
  d2:{id:'d2',titulo:'Cardápio com fotos',mensagens:[{papel:'voce',texto:'Monta o cardápio'},{papel:'primo',texto:'Montei o cardápio em 3 seções.'}]}
1732
1694
  };
1733
- var aj={effort:{atual:'medium',niveis:[{nome:'low',resumo:'rápido — tarefa simples, resposta curta'},{nome:'medium',resumo:'equilibrado — o padrão'},{nome:'high',resumo:'pensa mais antes de agir'},{nome:'xhigh',resumo:'bem fundo — tarefas difíceis'},{nome:'max',resumo:'o máximo de raciocínio'},{nome:'ultracode',resumo:'esforço total em código, sem pressa'}]},permissoes:{atual:'auto',opcoes:[{nome:'auto',resumo:'o Primo decide quando pedir licença'},{nome:'livre',resumo:'faz tudo sem perguntar'},{nome:'plano',resumo:'só planeja, não mexe em nada'}]},modo:'agent',provedor:{id:'primo',nome:'Primo',chave_mascarada:''},provedores_chave:[{id:'anthropic',nome:'Anthropic'},{id:'openai',nome:'OpenAI'},{id:'openrouter',nome:'OpenRouter'}],pasta:'~/Primo',voz:{nome:'Primo',sons:true},conta:{logado:!fora,email:'joel@exemplo.com',plano:'Pro',planoAte:'2026-12-31'}};
1695
+ var aj={effort:{atual:'medium',niveis:[{nome:'low',resumo:'rápido — tarefa simples, resposta curta'},{nome:'medium',resumo:'equilibrado — o padrão'},{nome:'high',resumo:'pensa mais antes de agir'},{nome:'xhigh',resumo:'bem fundo — tarefas difíceis'},{nome:'max',resumo:'o máximo de raciocínio'},{nome:'ultracode',resumo:'esforço total em código, sem pressa'}]},permissoes:{atual:'auto',opcoes:[{nome:'auto',resumo:'o Primo decide quando pedir licença'},{nome:'livre',resumo:'faz tudo sem perguntar'},{nome:'plano',resumo:'só planeja, não mexe em nada'}]},modo:'agent',provedor:{id:'primo',nome:'Primo',chave_mascarada:''},provedores_chave:[{id:'anthropic',nome:'Anthropic'},{id:'openai',nome:'OpenAI'},{id:'openrouter',nome:'OpenRouter'}],pasta:'~/Primo',conta:{logado:!fora,email:'joel@exemplo.com',plano:'Pro',planoAte:'2026-12-31'}};
1734
1696
  function r(m,t){setTimeout(function(){window.chat(m);},t||120);}
1735
1697
  window.__demo=function(o){
1736
1698
  console.log('post',o);
@@ -1739,7 +1701,7 @@ if(location.search.indexOf('demo')>=0&&!(window.webkit&&window.webkit.messageHan
1739
1701
  r({passo:'Lendo a pasta do projeto'},400);
1740
1702
  r({saida:{comando:o.texto.split(' ')[0],texto:'projeto/\n├── src/\n│ ├── app.js 12 KB\n│ └── util.js 3 KB\n├── public/\n│ └── capa.png 80 KB\n└── package.json\n\n✓ 3 pastas, 4 arquivos em /demo/projeto\n✗ falta o README.md\n'+Array.apply(null,Array(24)).map(function(_,i){return 'linha extra '+(i+1);}).join('\n'),conversa:null,midias:[{achado:'/demo/projeto',caminho:'/demo/projeto',nome:'projeto',tipo:'pasta',url:'',tamanho:0}]}},1200);
1741
1703
  break;
1742
- case 'pronto':r({comandos:[{nome:'/tree',descricao:'árvore do projeto',tipo:'terminal'},{nome:'/read',descricao:'lê um arquivo',tipo:'terminal'},{nome:'/voice nome',descricao:'muda o nome do assistente',tipo:'app',acao:'voz_nome'},{nome:'/nova',descricao:'começa uma conversa nova',tipo:'app',acao:'nova'},{nome:'/modo',descricao:'chat ou agent',tipo:'app',acao:'modo'},{nome:'/copiar',descricao:'copia a última resposta',tipo:'app',acao:'copiar'},{nome:'/sair',descricao:'sai da conta',tipo:'app',acao:'sair_conta'},{nome:'/ajuda',descricao:'mostra todos os comandos',tipo:'app',acao:'ajuda'},{nome:'/effort',descricao:'nível de esforço do Primo',tipo:'app',acao:'effort'},{nome:'/permissoes',descricao:'o que o Primo pode fazer sozinho',tipo:'app',acao:'permissoes'},{nome:'/revisar',descricao:'revisa o código do projeto',tipo:'pedido'},{nome:'/explicar',descricao:'explica o que o projeto faz',tipo:'pedido'}]},300);
1704
+ case 'pronto':r({comandos:[{nome:'/tree',descricao:'árvore do projeto',tipo:'terminal'},{nome:'/read',descricao:'lê um arquivo',tipo:'terminal'},{nome:'/nova',descricao:'começa uma conversa nova',tipo:'app',acao:'nova'},{nome:'/modo',descricao:'chat ou agent',tipo:'app',acao:'modo'},{nome:'/copiar',descricao:'copia a última resposta',tipo:'app',acao:'copiar'},{nome:'/sair',descricao:'sai da conta',tipo:'app',acao:'sair_conta'},{nome:'/ajuda',descricao:'mostra todos os comandos',tipo:'app',acao:'ajuda'},{nome:'/effort',descricao:'nível de esforço do Primo',tipo:'app',acao:'effort'},{nome:'/permissoes',descricao:'o que o Primo pode fazer sozinho',tipo:'app',acao:'permissoes'},{nome:'/revisar',descricao:'revisa o código do projeto',tipo:'pedido'},{nome:'/explicar',descricao:'explica o que o projeto faz',tipo:'pedido'}]},300);
1743
1705
  r({inicio:{logado:!fora,email:fora?'':'joel@exemplo.com',plano:'Pro',planoAte:'2026-12-31',pasta:'~/Primo',modo:'agent'}});break;
1744
1706
  case 'conversas':r({conversas:[{id:'d2',titulo:'Cardápio com fotos',atualizada:'2026-10-18T10:00:00Z'},{id:'d1',titulo:'Botão do WhatsApp no topo',atualizada:'2026-10-19T09:00:00Z'}]});break;
1745
1707
  case 'abrir':if(convs[o.id])r({conversa:convs[o.id]});break;