primocode 9.8.0-beta.7 → 9.8.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/fala.js CHANGED
@@ -37,9 +37,95 @@
37
37
 
38
38
  'use strict';
39
39
 
40
+ const { EventEmitter } = require('events');
40
41
  const { c, actionHeader, friendlyError } = require('./ui');
41
42
  const markdown = require('./markdown.js');
42
43
 
44
+ /* ── O CANAL ESTRUTURADO ─────────────────────────────────────────────────
45
+ * A janela do app (lib/app.js) mostra a conversa como CHAT: a fala do Primo
46
+ * em texto e cada ação dele numa linha cinza ("Leu index.html", "Rodou npm
47
+ * test ✓"). Adivinhar isso relendo a saída colorida do terminal seria frágil
48
+ * — um rótulo que muda aqui quebraria a janela calado. Então a MESMA função
49
+ * que desenha no terminal também avisa, por este canal, o que desenhou.
50
+ *
51
+ * Sem ouvinte (terminal puro), `avisar` não faz nada: nenhum custo para quem
52
+ * não abriu a janela. */
53
+ const eventos = new EventEmitter();
54
+ eventos.setMaxListeners(20);
55
+ function avisar(tipo, dados) {
56
+ if (!eventos.listenerCount('evento')) return;
57
+ try { eventos.emit('evento', Object.assign({ tipo }, dados || {})); } catch { /* a janela nunca derruba o agente */ }
58
+ }
59
+
60
+ /* O que a linha de ação da janela diz, por ferramenta. Verbo no passado
61
+ porque a linha fica na conversa depois que a ação acabou — "Lendo" viraria
62
+ mentira um segundo depois. O ícone é uma família, não um desenho por tool:
63
+ a página tem dez ícones, não setenta. */
64
+ const VERBOS = {
65
+ read_file: ['Leu', 'ler'], write_file: ['Escreveu', 'escrever'], edit_file: ['Editou', 'editar'],
66
+ multi_edit: ['Editou', 'editar'], list_dir: ['Listou', 'buscar'], glob: ['Procurou', 'buscar'],
67
+ grep: ['Buscou', 'buscar'], run_shell: ['Rodou', 'rodar'], run_in_new_terminal: ['Abriu um terminal com', 'rodar'],
68
+ open_in_browser: ['Abriu', 'web'], spawn_agent: ['Chamou um agente', 'agente'], write_todos: ['Planejou', 'plano'],
69
+ finish: ['Concluiu', 'plano'], pesquisar_referencia: ['Pesquisou', 'web'], remember_general: ['Guardou na memória', 'plano'],
70
+ planejar_producao: ['Planejou a produção', 'agente'], construir_app: ['Construiu', 'agente'],
71
+ };
72
+
73
+ /** Verbo, alvo e ícone de uma ação — só o que cabe numa linha, nunca o
74
+ * conteúdo inteiro de um write_file. */
75
+ function resumoDaAcao(name, args) {
76
+ const a = args || {};
77
+ let [verbo, icone] = VERBOS[name] || [];
78
+ if (!verbo) {
79
+ if (/^browser_/.test(name)) { verbo = 'Navegador · ' + name.slice(8); icone = 'web'; }
80
+ else if (/^desktop_/.test(name)) { verbo = 'Computador · ' + name.slice(8).replace(/_/g, ' '); icone = 'computador'; }
81
+ else if (/^estudio_/.test(name)) { verbo = 'Estúdio · ' + name.slice(8); icone = 'agente'; }
82
+ else { verbo = name; icone = 'outro'; }
83
+ }
84
+ let alvo = a.path || a.command || a.pattern || a.consulta || a.url || a.target || a.app || a.selector
85
+ || a.titulo || a.description || a.arquivo || (a.todos ? `${a.todos.length} tarefas` : '') || '';
86
+ if (typeof alvo !== 'string') alvo = '';
87
+ alvo = alvo.replace(/\s+/g, ' ').trim();
88
+ if (alvo.length > 120) alvo = alvo.slice(0, 119) + '…';
89
+ return { nome: name, verbo, alvo, icone };
90
+ }
91
+
92
+ /** O resultado de uma ação, resumido para a linha da janela: ok ou não, o
93
+ * "+12 −3" das edições e até 40 linhas de detalhe para quem expandir. */
94
+ function resumoDoResultado(name, r) {
95
+ if (!r || typeof r !== 'object') return { nome: name, ok: true };
96
+ if (r.ok === false) {
97
+ return { nome: name, ok: false, resumo: String(r.error || `exit ${r.code ?? 'erro'}`).replace(/\s+/g, ' ').trim().slice(0, 300) };
98
+ }
99
+ const fora = { nome: name, ok: true };
100
+ if (typeof r._antes === 'string' && typeof r._depois === 'string') {
101
+ try {
102
+ const diff = require('./diff.js');
103
+ const n = r._antes.split('\n').length + r._depois.split('\n').length;
104
+ if (n <= diff.MAX_LINHAS_ARQUIVO * 2) {
105
+ const ops = diff.operacoes(r._antes, r._depois);
106
+ Object.assign(fora, diff.contar(ops));
107
+ fora.detalhe = ops.filter((o) => o.tipo !== ' ').slice(0, 40).map((o) => o.tipo + ' ' + o.texto).join('\n');
108
+ }
109
+ } catch { /* sem diff: a linha sai sem os números */ }
110
+ }
111
+ if (name === 'write_file') fora.criado = !!r.created;
112
+ if (name === 'run_shell' || name === 'run_in_new_terminal') {
113
+ const saida = String(r.stdout || '').trim() + (r.stderr ? '\n' + String(r.stderr).trim() : '');
114
+ fora.detalhe = saida.split('\n').slice(-40).join('\n').trim();
115
+ fora.codigo = r.code ?? 0;
116
+ }
117
+ const plural = (n, um, varios) => `${n} ${n === 1 ? um : varios}`;
118
+ if (name === 'read_file') fora.resumo = plural(r.linhas || 0, 'linha', 'linhas');
119
+ if (name === 'grep') fora.resumo = plural(r.total || 0, 'ocorrência', 'ocorrências');
120
+ if (name === 'glob') fora.resumo = plural(r.total || 0, 'arquivo', 'arquivos');
121
+ if (name === 'list_dir' && r.tree) fora.detalhe = String(r.tree).split('\n').slice(0, 40).join('\n');
122
+ if (name === 'write_todos' && Array.isArray(r.todos)) {
123
+ fora.detalhe = r.todos.map((t) => (t.status === 'completed' ? '✓ ' : '○ ') + t.content).join('\n');
124
+ fora.resumo = `${r.feitos}/${r.total}`;
125
+ }
126
+ return fora;
127
+ }
128
+
43
129
  /* Até este tamanho, o texto que o modelo escreve antes de chamar uma tool é
44
130
  guardado e sai de uma vez, como pensamento. Passou disso sem tool à vista,
45
131
  é resposta — e volta a correr na tela conforme chega. Uma frase de
@@ -58,6 +144,7 @@ function pensamento(texto, profundidade) {
58
144
  if (!ls.length || !ls[0]) return '';
59
145
  const p = ident(profundidade);
60
146
  const saida = ls.map((l, i) => `${p} ${i === 0 ? c.dim('✦') : ' '} ${c.muted(l)}`).join('\n');
147
+ avisar('pensamento', { texto: ls.join('\n'), prof: profundidade || 0 });
61
148
  console.log(saida);
62
149
  return saida;
63
150
  }
@@ -65,6 +152,7 @@ function pensamento(texto, profundidade) {
65
152
  /** A ação (tool) que ele decidiu fazer. */
66
153
  function acao(name, args, profundidade) {
67
154
  const saida = ident(profundidade) + actionHeader(name, args);
155
+ avisar('acao', Object.assign(resumoDaAcao(name, args), { prof: profundidade || 0 }));
68
156
  console.log(saida);
69
157
  return saida;
70
158
  }
@@ -102,6 +190,7 @@ function resposta(texto) {
102
190
  const largura = Math.max(40, (process.stdout.columns || 80) - 4);
103
191
  const linhasMd = markdown.renderizar(texto, largura);
104
192
  const saida = '\n' + linhasMd.map((l, i) => (i === 0 ? `${c.brand('●')} ${l}` : ` ${l}`)).join('\n');
193
+ avisar('resposta', { texto: String(texto) });
105
194
  console.log(saida);
106
195
  return saida;
107
196
  }
@@ -111,6 +200,7 @@ function resposta(texto) {
111
200
  function aviso(texto) {
112
201
  if (!String(texto || '').trim()) return '';
113
202
  const saida = '\n' + formatar(texto, c.warn('●'));
203
+ avisar('aviso', { texto: String(texto) });
114
204
  console.log(saida);
115
205
  return saida;
116
206
  }
@@ -154,9 +244,11 @@ function fluxo() {
154
244
  escrever(delta) {
155
245
  const t = String(delta || '');
156
246
  if (!t) return;
247
+ avisar('pedaco', { texto: t });
157
248
  sair(desenho.pedaco(t));
158
249
  },
159
250
  fechar() {
251
+ avisar('fim', {});
160
252
  sair(desenho.fim());
161
253
  if (escreveu) process.stdout.write('\n');
162
254
  },
@@ -174,6 +266,7 @@ function paralelo(quantos, profundidade) {
174
266
  /** Um agente delegado começando: "◆ Agente·frontend a folha de estilo". */
175
267
  function agente(rotulo, descricao, profundidade) {
176
268
  const saida = `${ident(profundidade)} ${c.brand('◆')} ${c.bold(rotulo || 'Agente')}${descricao ? ' ' + c.muted(descricao) : ''}`;
269
+ avisar('acao', { nome: 'agente', verbo: rotulo || 'Agente', alvo: String(descricao || '').slice(0, 120), icone: 'agente', prof: profundidade || 0 });
177
270
  console.log(saida);
178
271
  return saida;
179
272
  }
@@ -182,8 +275,9 @@ function agente(rotulo, descricao, profundidade) {
182
275
  function agenteVoltou(resumo, profundidade) {
183
276
  const texto = String(resumo || 'sem resultado').replace(/\s+/g, ' ').trim();
184
277
  const saida = resultado(profundidade) + c.muted(texto.length > 140 ? texto.slice(0, 139) + '…' : texto);
278
+ avisar('resultado', { nome: 'agente', ok: true, detalhe: texto.slice(0, 2000) });
185
279
  console.log(saida);
186
280
  return saida;
187
281
  }
188
282
 
189
- module.exports = { LIMIAR_PENSAMENTO, pensamento, acao, resultado, recuo, resposta, aviso, problema, frase, fluxo, paralelo, agente, agenteVoltou };
283
+ module.exports = { eventos, avisar, resumoDaAcao, resumoDoResultado, LIMIAR_PENSAMENTO, pensamento, acao, resultado, recuo, resposta, aviso, problema, frase, fluxo, paralelo, agente, agenteVoltou };
package/lib/offline.js ADDED
@@ -0,0 +1,168 @@
1
+ /**
2
+ * offline.js — o primocode-server rodando AQUI, no computador da pessoa.
3
+ *
4
+ * "Como eu não tenho acesso ao Railway, enquanto isso eu posso testar com o
5
+ * projeto rodando localmente, chamando as APIs pelo meu computador."
6
+ * (24/09/2026)
7
+ *
8
+ * O /offline sobe o servidor da pasta primocode-server com as MESMAS chaves
9
+ * que o Render usa (coladas em ~/.primocode/servidor.env) e aponta o CLI para
10
+ * ele até a sessão acabar. É temporário: serve para testar o servidor novo
11
+ * antes de ele ir para produção.
12
+ *
13
+ * "Offline" é o nome que o pedido deu, e ele mente um pouco: as IAs continuam
14
+ * na internet. O que sai do caminho é o servidor de produção.
15
+ *
16
+ * ── O QUE NUNCA APARECE ──────────────────────────────────────────────────
17
+ * O valor de uma chave. Mostram-se só os NOMES das variáveis que o arquivo
18
+ * tem, o bastante para ver o que falta.
19
+ */
20
+
21
+ 'use strict';
22
+
23
+ const fs = require('fs');
24
+ const os = require('os');
25
+ const path = require('path');
26
+ const http = require('http');
27
+ const { spawn } = require('child_process');
28
+
29
+ const PORTA = Number(process.env.PRIMOCODE_OFFLINE_PORTA || 3190);
30
+ const CASA = path.join(os.homedir(), '.primocode');
31
+ const ARQUIVO_ENV = path.join(CASA, 'servidor.env');
32
+ const ARQUIVO_LOG = path.join(CASA, 'servidor-local.log');
33
+
34
+ // As variáveis que o primocode-server lê. As de chave vêm primeiro: sem pelo
35
+ // menos uma delas, o servidor sobe mas não tem com quem falar.
36
+ const CHAVES = ['GROQ_API_KEY', 'OPENROUTER_API_KEY', 'GEMINI_API_KEY', 'GOOGLE_API_KEY', 'COHERE_API_KEY', 'HF_TOKEN', 'HUGGINGFACE_API_KEY', 'PG_GATEWAY_API_KEY'];
37
+ const OUTRAS = ['PG_GATEWAY_BASE_URL', 'CONECTA_API', 'PRIMOCODE_TOKEN', 'REQUIRE_TOKEN', 'RATE_LIMIT_PER_HOUR'];
38
+
39
+ const MODELO_ENV = `# As chaves do primocode-server rodando no seu computador (/offline).
40
+ # Copie do Render: o serviço primocode-server → Environment. Uma por linha,
41
+ # NOME=valor, sem aspas. Este arquivo é só seu: não mande para ninguém.
42
+ #
43
+ ${CHAVES.map((k) => `# ${k}=`).join('\n')}
44
+ #
45
+ ${OUTRAS.map((k) => `# ${k}=`).join('\n')}
46
+ `;
47
+
48
+ /** Lê um .env simples: NOME=valor, # comenta, aspas em volta saem. */
49
+ function lerEnv(texto) {
50
+ const env = {};
51
+ for (const linha of String(texto || '').split(/\r?\n/)) {
52
+ const m = linha.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
53
+ if (!m) continue;
54
+ let valor = m[2];
55
+ if (/^(['"]).*\1$/.test(valor)) valor = valor.slice(1, -1);
56
+ if (valor) env[m[1]] = valor;
57
+ }
58
+ return env;
59
+ }
60
+
61
+ /** O arquivo de chaves: cria o modelo (vazio, 600) na primeira vez. */
62
+ function prepararEnv(arquivo = ARQUIVO_ENV) {
63
+ let criado = false;
64
+ if (!fs.existsSync(arquivo)) {
65
+ fs.mkdirSync(path.dirname(arquivo), { recursive: true });
66
+ fs.writeFileSync(arquivo, MODELO_ENV, { mode: 0o600 });
67
+ criado = true;
68
+ }
69
+ const env = lerEnv(fs.readFileSync(arquivo, 'utf8'));
70
+ return { arquivo, criado, env, nomes: Object.keys(env), temChave: CHAVES.some((k) => env[k]) };
71
+ }
72
+
73
+ function ehServidor(dir) {
74
+ try {
75
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
76
+ return pkg.name === 'primocode-server' && fs.existsSync(path.join(dir, 'server.js'));
77
+ } catch { return false; }
78
+ }
79
+
80
+ /** Onde está a pasta do primocode-server. A primeira que existir ganha. */
81
+ function acharServidor({ pedido = '', raizCli = path.join(__dirname, '..') } = {}) {
82
+ const candidatos = [
83
+ pedido,
84
+ process.env.PRIMOCODE_SERVER_DIR,
85
+ path.join(raizCli, '..', 'primocode-server'),
86
+ path.join(os.homedir(), 'Primocode CLI', 'primocode-server'),
87
+ path.join(os.homedir(), 'primocode-server'),
88
+ ].filter(Boolean).map((p) => path.resolve(p.replace(/^~(?=$|\/)/, os.homedir())));
89
+ return candidatos.find(ehServidor) || null;
90
+ }
91
+
92
+ /** GET /health; devolve o JSON ou null. */
93
+ function saude(url, espera = 1500) {
94
+ return new Promise((resolve) => {
95
+ const req = http.get(url + '/health', { timeout: espera }, (res) => {
96
+ let corpo = '';
97
+ res.on('data', (d) => { corpo += d; });
98
+ res.on('end', () => { try { resolve(JSON.parse(corpo)); } catch { resolve(null); } });
99
+ });
100
+ req.on('timeout', () => req.destroy());
101
+ req.on('error', () => resolve(null));
102
+ });
103
+ }
104
+
105
+ let filho = null;
106
+
107
+ /**
108
+ * Sobe o servidor (ou reaproveita um que já responde na porta).
109
+ * Devolve { ok, url, versao, reaproveitado } ou { ok: false, error, comoResolver }.
110
+ */
111
+ async function ligar({ dir, env = {}, porta = PORTA, log = ARQUIVO_LOG, prazo = 20000 } = {}) {
112
+ const url = `http://localhost:${porta}`;
113
+ const ja = await saude(url);
114
+ if (ja && ja.status === 'ok') return { ok: true, url, versao: ja.version, reaproveitado: true };
115
+ if (!dir) {
116
+ return {
117
+ ok: false,
118
+ error: 'Não achei a pasta do primocode-server.',
119
+ comoResolver: 'clone o repo ao lado do primocode, ou diga onde ele está: /offline ~/caminho/primocode-server',
120
+ };
121
+ }
122
+ if (!fs.existsSync(path.join(dir, 'node_modules'))) {
123
+ return {
124
+ ok: false,
125
+ error: `O servidor em ${dir} ainda não tem as dependências.`,
126
+ comoResolver: `rode uma vez: cd "${dir}" && npm install`,
127
+ };
128
+ }
129
+ fs.mkdirSync(path.dirname(log), { recursive: true });
130
+ const saida = fs.openSync(log, 'a');
131
+ filho = spawn(process.execPath, ['server.js'], {
132
+ cwd: dir,
133
+ env: { ...process.env, ...env, PORT: String(porta) },
134
+ stdio: ['ignore', saida, saida],
135
+ });
136
+ let morreu = null;
137
+ filho.on('exit', (codigo) => { morreu = codigo; filho = null; });
138
+
139
+ const fim = Date.now() + prazo;
140
+ while (Date.now() < fim) {
141
+ if (morreu !== null) {
142
+ return { ok: false, error: `O servidor fechou sozinho (código ${morreu}).`, comoResolver: `veja o fim de ${log}` };
143
+ }
144
+ const h = await saude(url);
145
+ if (h && h.status === 'ok') return { ok: true, url, versao: h.version, reaproveitado: false };
146
+ await new Promise((r) => setTimeout(r, 300));
147
+ }
148
+ desligar();
149
+ return { ok: false, error: `O servidor não respondeu em ${Math.round(prazo / 1000)}s.`, comoResolver: `veja o fim de ${log}` };
150
+ }
151
+
152
+ /** Desliga o servidor que ESTE processo subiu. Um reaproveitado fica de pé. */
153
+ function desligar() {
154
+ if (!filho) return false;
155
+ try { filho.kill(); } catch {}
156
+ filho = null;
157
+ return true;
158
+ }
159
+
160
+ function ligado() { return Boolean(filho); }
161
+
162
+ // O servidor não pode sobrar rodando quando o CLI fecha.
163
+ process.on('exit', desligar);
164
+
165
+ module.exports = {
166
+ PORTA, ARQUIVO_ENV, ARQUIVO_LOG, CHAVES,
167
+ lerEnv, prepararEnv, acharServidor, ehServidor, saude, ligar, desligar, ligado,
168
+ };
@@ -1,27 +1,30 @@
1
1
  /**
2
- * ide.js — a PrimoCode IDE: árvore, editor, prévia e chat, na mesma janela.
2
+ * painel.js — o painel da direita da janela: a prévia do site e os arquivos.
3
3
  *
4
- * "Um app ao invés de CLI, aí fica uma IDE e uma CLI."
4
+ * "Remove a IDE ou deixa idêntico ao Codex."
5
5
  *
6
- * ── POR QUE MORA DENTRO DO SERVIDOR DO /rc ───────────────────────────────
7
- * A janela do /rc já resolveu o difícil: servidor em 127.0.0.1 com chave
8
- * sorteada, fluxo de eventos (SSE) ao vivo e o espelho da saída do terminal.
9
- * Uma IDE com servidor próprio seria uma segunda chave, uma segunda porta e um
10
- * segundo fluxo — e as duas janelas mostrariam agentes que parecem diferentes
11
- * sendo o mesmo. Aqui só entram as rotas que a IDE precisa a mais (árvore,
12
- * ler, salvar, prévia) e o vigia da pasta, que empurra eventos pelo MESMO
13
- * fluxo do /rc.
6
+ * Antes isto era a PrimoCode IDE: árvore, EDITOR de código, prévia e chat. O
7
+ * editor saiu. Quem usa o PrimoCode não programa — um editor de código na
8
+ * cara dessa pessoa é uma porta para quebrar o que o agente fez, sem saber
9
+ * como voltar. O que ficou é o que ela usa de verdade: ver o site que está
10
+ * nascendo (Prévia) e olhar o que tem na pasta (Arquivos), só leitura.
11
+ *
12
+ * ── POR QUE MORA DENTRO DO SERVIDOR DA JANELA ────────────────────────────
13
+ * A janela (lib/app.js) já resolveu o difícil: 127.0.0.1, chave sorteada e o
14
+ * fluxo de eventos (SSE). Aqui só entram as rotas do painel (árvore, ler,
15
+ * prévia) e o vigia da pasta, que empurra "arquivo mudou" pelo MESMO fluxo —
16
+ * é o que faz a prévia recarregar enquanto o agente escreve.
14
17
  *
15
18
  * ── SEGURANÇA ────────────────────────────────────────────────────────────
16
- * Estas rotas leem e ESCREVEM no disco do usuário. Três travas:
17
19
  * · a chave do app.js vale aqui também (tudo mora em /api/);
18
20
  * · todo caminho é resolvido DENTRO da pasta do projeto — "../", caminho
19
21
  * absoluto e link simbólico que aponta para fora são recusados;
22
+ * · nada aqui escreve no disco: sem editor, não há rota de salvar;
20
23
  * · a prévia roda num iframe sandbox SEM allow-same-origin, e o servidor
21
24
  * manda o mesmo sandbox no cabeçalho. O código da prévia foi escrito pelo
22
- * agente (ou pela pessoa); se ele herdasse a origem da IDE, poderia chamar
23
- * /api/enviar e dar ordens ao agente. Com origem opaca, o fetch dele sai
24
- * com `Origin: null` e o app.js recusa.
25
+ * agente; se ele herdasse a origem da janela, poderia chamar /api/enviar
26
+ * e dar ordens ao agente. Com origem opaca, o fetch dele sai com
27
+ * `Origin: null` e o app.js recusa.
25
28
  */
26
29
 
27
30
  'use strict';
@@ -36,9 +39,9 @@ const IGNORADOS = new Set(['node_modules', '.git', '.DS_Store', '.next', '.cache
36
39
  '__pycache__', '.venv', 'venv', '.turbo', '.parcel-cache']);
37
40
  const MAX_ITENS = 4000;
38
41
  const MAX_PROFUNDIDADE = 12;
39
- // 1 MB: acima disso um <textarea> engasga a cada tecla, e arquivo desse
40
- // tamanho num projeto de iniciante é quase sempre gerado (bundle, dado).
41
- const MAX_EDITOR = 1024 * 1024;
42
+ // 1 MB: acima disso o visualizador engasga, e arquivo desse tamanho num
43
+ // projeto de iniciante é quase sempre gerado (bundle, dado).
44
+ const MAX_LEITURA = 1024 * 1024;
42
45
 
43
46
  // ── caminhos ─────────────────────────────────────────────────────────────
44
47
 
@@ -46,12 +49,12 @@ const MAX_EDITOR = 1024 * 1024;
46
49
  *
47
50
  * `path.relative` pega o "../" e o caminho absoluto; o `realpath` pega o link
48
51
  * simbólico, que passa pelo primeiro teste com cara de arquivo de dentro e
49
- * aponta para ~/.ssh. Arquivo que ainda não existe (salvar um novo) é checado
52
+ * aponta para ~/.ssh. Caminho que ainda não existe é checado
50
53
  * pela pasta onde vai nascer. */
51
54
  function dentro(raiz, relativo) {
52
55
  if (typeof relativo !== 'string' || relativo.includes('\0')) return null;
53
56
  // Absoluto é recusado de cara, e não "reinterpretado" como relativo:
54
- // quem manda /etc/hosts quer o /etc/hosts, e salvar em <projeto>/etc/hosts
57
+ // quem manda /etc/hosts quer o /etc/hosts, e ler <projeto>/etc/hosts
55
58
  // no lugar seria uma surpresa. `C:\` e `\\servidor` também contam.
56
59
  if (path.isAbsolute(relativo) || path.win32.isAbsolute(relativo)) return null;
57
60
  const base = path.resolve(raiz);
@@ -113,11 +116,10 @@ function arvore(raiz) {
113
116
  return { itens, cortada };
114
117
  }
115
118
 
116
- // ── ler e salvar ─────────────────────────────────────────────────────────
119
+ // ── ler ──────────────────────────────────────────────────────────────────
117
120
 
118
121
  /* Binário é o que tem byte zero nos primeiros 8 KB — o mesmo teste do git.
119
- Mostrar um PNG como texto no editor e deixar a pessoa salvar por cima
120
- destruiria a imagem sem aviso. */
122
+ Mostrar um PNG como texto é uma tela de lixo que parece arquivo quebrado. */
121
123
  function ehBinario(buf) {
122
124
  const n = Math.min(buf.length, 8000);
123
125
  for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
@@ -131,35 +133,12 @@ function ler(raiz, rel) {
131
133
  try { st = fs.statSync(abs); } catch { return { status: 404, corpo: { ok: false, error: 'não existe' } }; }
132
134
  if (!st.isFile()) return { status: 400, corpo: { ok: false, error: 'não é arquivo' } };
133
135
  const base = { ok: true, caminho: relativoDe(raiz, abs), tamanho: st.size, mtime: st.mtimeMs };
134
- if (st.size > MAX_EDITOR) return { status: 200, corpo: Object.assign(base, { grande: true }) };
136
+ if (st.size > MAX_LEITURA) return { status: 200, corpo: Object.assign(base, { grande: true }) };
135
137
  const buf = fs.readFileSync(abs);
136
138
  if (ehBinario(buf)) return { status: 200, corpo: Object.assign(base, { binario: true }) };
137
139
  return { status: 200, corpo: Object.assign(base, { conteudo: buf.toString('utf8') }) };
138
140
  }
139
141
 
140
- /* `mtimeBase` é o mtime que a página viu ao abrir o arquivo. Se o disco está
141
- mais novo, alguém (quase sempre o agente) mexeu no meio — e salvar por cima
142
- apagaria o trabalho dele sem ninguém perceber. A página decide: recarregar
143
- ou mandar de novo com `forcar`. */
144
- function salvar(raiz, dados) {
145
- const rel = dados && dados.caminho;
146
- const conteudo = dados && dados.conteudo;
147
- if (typeof conteudo !== 'string') return { status: 400, corpo: { ok: false, error: 'conteúdo faltando' } };
148
- if (Buffer.byteLength(conteudo) > MAX_EDITOR) return { status: 413, corpo: { ok: false, error: 'maior que 1 MB' } };
149
- const abs = dentro(raiz, rel);
150
- if (!abs || abs === path.resolve(raiz)) return { status: 403, corpo: { ok: false, error: 'fora da pasta do projeto' } };
151
- if (ignorado(relativoDe(raiz, abs))) return { status: 403, corpo: { ok: false, error: 'pasta protegida' } };
152
- let st = null;
153
- try { st = fs.statSync(abs); } catch { /* arquivo novo */ }
154
- if (st && !st.isFile()) return { status: 400, corpo: { ok: false, error: 'não é arquivo' } };
155
- if (st && dados.mtimeBase && !dados.forcar && st.mtimeMs > Number(dados.mtimeBase) + 1) {
156
- return { status: 409, corpo: { ok: false, conflito: true, error: 'mudou no disco depois que você abriu', mtime: st.mtimeMs } };
157
- }
158
- fs.mkdirSync(path.dirname(abs), { recursive: true });
159
- fs.writeFileSync(abs, conteudo);
160
- return { status: 200, corpo: { ok: true, caminho: relativoDe(raiz, abs), mtime: fs.statSync(abs).mtimeMs } };
161
- }
162
-
163
142
  // ── prévia ───────────────────────────────────────────────────────────────
164
143
 
165
144
  const TIPOS = {
@@ -193,7 +172,7 @@ function servirPrevia(raiz, rel, res) {
193
172
  'Content-Type': TIPOS[path.extname(alvo).toLowerCase()] || 'application/octet-stream',
194
173
  'Cache-Control': 'no-store',
195
174
  // O mesmo sandbox do iframe, dito pelo servidor: vale até se alguém
196
- // abrir o endereço da prévia fora da IDE.
175
+ // abrir o endereço da prévia fora da janela.
197
176
  'Content-Security-Policy': 'sandbox allow-scripts allow-forms allow-modals allow-popups allow-pointer-lock',
198
177
  });
199
178
  fs.createReadStream(alvo).pipe(res);
@@ -237,7 +216,7 @@ function vigiar(raiz, aoMudar, espera = 150) {
237
216
  // ── as rotas ─────────────────────────────────────────────────────────────
238
217
 
239
218
  /**
240
- * Monta a IDE num servidor do app.js.
219
+ * Monta o painel num servidor do app.js.
241
220
  *
242
221
  * @param {object} op
243
222
  * @param {()=>string} op.pasta a pasta do projeto AGORA (o /dir muda ela)
@@ -247,9 +226,6 @@ function vigiar(raiz, aoMudar, espera = 150) {
247
226
  function montar(op) {
248
227
  let pastaVigiada = null;
249
228
  let pararVigia = () => {};
250
- // O que a própria IDE acabou de salvar, para a página não pintar como
251
- // "o agente mexeu" a mudança que a pessoa fez.
252
- const salvosAgora = new Map();
253
229
 
254
230
  function conferirPasta() {
255
231
  const atual = op.pasta && op.pasta();
@@ -257,19 +233,14 @@ function montar(op) {
257
233
  pararVigia();
258
234
  pastaVigiada = atual;
259
235
  if (!fs.existsSync(atual)) { pararVigia = () => {}; return atual; }
260
- pararVigia = vigiar(atual, (mudados) => {
261
- const agora = Date.now();
262
- const deVoce = mudados.filter((m) => (salvosAgora.get(m) || 0) > agora - 1500);
263
- op.emitir('arquivos', { pasta: atual, mudados, deVoce });
264
- });
236
+ pararVigia = vigiar(atual, (mudados) => op.emitir('arquivos', { pasta: atual, mudados }));
265
237
  op.emitir('arquivos', { pasta: atual, mudados: [], trocou: true });
266
238
  return atual;
267
239
  }
268
- // O /dir troca a pasta pelo terminal, sem passar por aqui: sem esta volta
269
- // a IDE continuaria vigiando o projeto anterior.
270
- /* E só depois que a IDE foi aberta de verdade: quem abriu só o chat do
271
- /rc não precisa de ninguém vigiando o ~/primocode inteiro — no Linux
272
- o fs.watch recursivo custa um aviso do sistema por pasta. */
240
+ /* O /dir e os projetos da barra lateral trocam a pasta sem passar por
241
+ aqui: sem esta ronda o painel vigiaria o projeto anterior. E só depois
242
+ que o painel foi aberto de verdade — no Linux o fs.watch recursivo custa
243
+ um aviso do sistema por pasta, e quem só conversa não precisa disso. */
273
244
  let usada = false;
274
245
  const ronda = setInterval(() => { if (usada) conferirPasta(); }, 2000);
275
246
  if (ronda.unref) ronda.unref();
@@ -279,48 +250,31 @@ function montar(op) {
279
250
  res.end(JSON.stringify(corpo));
280
251
  }
281
252
 
282
- function lerCorpo(req) {
283
- return new Promise((ok) => {
284
- let corpo = '';
285
- let grande = false;
286
- req.on('data', (c) => { corpo += c; if (corpo.length > MAX_EDITOR * 2) { grande = true; req.destroy(); } });
287
- req.on('end', () => { try { ok(grande ? null : JSON.parse(corpo)); } catch { ok(null); } });
288
- });
289
- }
290
-
291
- /* Devolve true se a rota era da IDE. A chave já foi conferida pelo
292
- app.js antes de chegar aqui — só as rotas com /api/ passam por ela, e
293
- por isso a prévia também mora debaixo de /api/. */
253
+ /* Devolve true se a rota era do painel. A chave já foi conferida pelo
254
+ app.js antes de chegar aqui — por isso a prévia também mora em /api/. */
294
255
  async function atender(req, res, url) {
295
256
  const rota = url.pathname;
296
- if (!rota.startsWith('/api/ide/')) return false;
257
+ const ehArquivos = rota.startsWith('/api/arquivos/');
258
+ const m = rota.match(/^\/api\/previa\/[0-9a-f]+\/?(.*)$/);
259
+ if (!ehArquivos && !m) return false;
297
260
  usada = true;
298
261
  const raiz = conferirPasta();
299
262
  if (!raiz || !fs.existsSync(raiz)) { json(res, 404, { ok: false, error: 'sem pasta de projeto' }); return true; }
300
263
 
301
- if (rota === '/api/ide/arvore') {
264
+ if (rota === '/api/arquivos/arvore') {
302
265
  const a = arvore(raiz);
303
266
  json(res, 200, { ok: true, pasta: raiz, nome: path.basename(raiz), itens: a.itens,
304
267
  cortada: a.cortada, entrada: entradaDaPrevia(raiz) });
305
268
  return true;
306
269
  }
307
- if (rota === '/api/ide/arquivo') {
270
+ if (rota === '/api/arquivos/ler') {
308
271
  const r = ler(raiz, url.searchParams.get('p') || '');
309
272
  json(res, r.status, r.corpo);
310
273
  return true;
311
274
  }
312
- if (rota === '/api/ide/salvar' && req.method === 'POST') {
313
- const dados = await lerCorpo(req);
314
- if (!dados) { json(res, 400, { ok: false, error: 'pedido ilegível ou maior que 1 MB' }); return true; }
315
- const r = salvar(raiz, dados);
316
- if (r.corpo.ok) salvosAgora.set(r.corpo.caminho, Date.now());
317
- json(res, r.status, r.corpo);
318
- return true;
319
- }
320
- /* /api/ide/previa/<chave>/<caminho>: a chave vai no CAMINHO, não na
275
+ /* /api/previa/<chave>/<caminho>: a chave vai no CAMINHO, não na
321
276
  query, porque a página da prévia pede style.css e script.js por
322
277
  endereço relativo — e o relativo herda o caminho, não a query. */
323
- const m = rota.match(/^\/api\/ide\/previa\/[0-9a-f]+\/?(.*)$/);
324
278
  if (m) { servirPrevia(raiz, m[1], res); return true; }
325
279
 
326
280
  json(res, 404, { ok: false, error: 'rota desconhecida' });
@@ -334,4 +288,4 @@ function montar(op) {
334
288
  };
335
289
  }
336
290
 
337
- module.exports = { montar, dentro, arvore, ler, salvar, vigiar, ehBinario, entradaDaPrevia, MAX_EDITOR };
291
+ module.exports = { montar, dentro, arvore, ler, vigiar, ehBinario, entradaDaPrevia, MAX_LEITURA };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "9.8.0-beta.7",
3
+ "version": "9.8.0-beta.9",
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": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/primocode.js",
11
- "test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/kits.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/ide.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/app-janela.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"
@@ -30,7 +30,6 @@
30
30
  "app/",
31
31
  "studio/",
32
32
  "skills/",
33
- "kits/",
34
33
  "voz/",
35
34
  "README.md",
36
35
  "comandos/",
@@ -7,6 +7,8 @@ autor: adaptado de "frontend-design" (Anthropic, anthropics/skills, Apache-2.0
7
7
  Trabalhe como o diretor de arte de um estúdio conhecido por dar a cada cliente uma identidade que
8
8
  ninguém confunde. O cliente já recusou propostas com cara de modelo pronto.
9
9
 
10
+ **Não existe modelo.** Toda página começa do zero, e nenhuma sai com a estrutura da anterior.
11
+
10
12
  **Comece pelo assunto.** O ramo, os materiais e o vocabulário do negócio são de onde vem a escolha
11
13
  visual. Um brinquedo para meninas de 8 anos e um painel de analista financeiro não se parecem em nada.
12
14
  Use o conteúdo real do pedido em tudo, e não texto genérico.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  nome: Motion de primeira
3
3
  descricao: como um vídeo criado do zero deixa de parecer feito por máquina
4
- quando: vídeo, video, motion, animação, animacao, anúncio, anuncio, reels, teaser, abertura, comercial, propaganda, estúdio, estudio
4
+ quando: vídeo, video, motion graphics, vídeo animado, video animado, anúncio, anuncio, reels, teaser, vinheta, comercial, propaganda, estúdio, estudio
5
5
  autor: PrimoCode
6
6
  ---
7
7
  Não existe template. Cada propriedade deste vídeo é uma decisão sua, e a