primocode 10.15.1 → 10.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,164 @@
1
+ /**
2
+ * memoria-usuario.js — o PrimoCode lembra de quem a pessoa é.
3
+ *
4
+ * A memória que vale entre sessões, do jeito do Claude Code: um fato por
5
+ * arquivo .md em ~/.primocode/memoria/, e um índice MEMORIA.md com uma linha
6
+ * por fato. O índice entra no contexto de TODA sessão; o corpo de cada fato
7
+ * o modelo lê quando precisa (read_file no caminho).
8
+ *
9
+ * ~/.primocode/memoria/MEMORIA.md - [Título](arquivo.md) — gancho
10
+ * ~/.primocode/memoria/<nome-em-kebab>.md frontmatter (nome, descricao, tipo) + corpo
11
+ *
12
+ * Difere do memory-geral.md (uma lista corrida de frases) e do AGENTS.md da
13
+ * pasta (o resumo da conversa): aqui cada fato tem nome, tipo e pode ser
14
+ * atualizado ou apagado sem duplicar.
15
+ *
16
+ * HOME é injetável, como em lib/estudio/gosto.js: o teste usa uma casa temporária.
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const os = require('os');
23
+ const path = require('path');
24
+
25
+ const TIPOS = ['usuario', 'feedback', 'projeto', 'referencia'];
26
+ const LINHAS_DO_INDICE = 200;
27
+ const LIMITE_TEXTO = 8000;
28
+ const INDICE = 'MEMORIA.md';
29
+
30
+ // Cara de segredo: o fato fica em disco e volta em todo prompt, então chave não entra.
31
+ const SEGREDOS = [
32
+ /\bsk-[A-Za-z0-9_-]{8,}/,
33
+ /\bAIza[0-9A-Za-z_-]{10,}/,
34
+ /\bgh[pousr]_[A-Za-z0-9]{10,}/,
35
+ /-----BEGIN [A-Z ]*(?:PRIVATE KEY|CERTIFICATE)?-----/,
36
+ /\bxox[abprs]-[A-Za-z0-9-]{8,}/,
37
+ /\bAKIA[0-9A-Z]{12,}/,
38
+ /\b(?:senha|password|passwd|token|api[_-]?key|secret)\s*[:=]\s*\S+/i,
39
+ ];
40
+
41
+ function pasta(home = os.homedir()) { return path.join(home, '.primocode', 'memoria'); }
42
+ function arquivoIndice(home) { return path.join(pasta(home), INDICE); }
43
+
44
+ /** "Prefere Respostas Curtas!" → "prefere-respostas-curtas". '' se não sobrar nada. */
45
+ function kebab(s) {
46
+ return String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()
47
+ .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60).replace(/-+$/, '');
48
+ }
49
+
50
+ function umaLinha(s) { return String(s || '').replace(/\s+/g, ' ').trim(); }
51
+
52
+ /** O texto parece ter segredo? Devolve true/false. Puro. */
53
+ function temSegredo(...textos) {
54
+ return textos.some((t) => SEGREDOS.some((re) => re.test(String(t || ''))));
55
+ }
56
+
57
+ /** "usuario-prefere-curto" → "Usuario prefere curto": o título que aparece no índice. */
58
+ function titulo(nome) {
59
+ const t = String(nome || '').replace(/-/g, ' ');
60
+ return t.charAt(0).toUpperCase() + t.slice(1);
61
+ }
62
+
63
+ function montar({ nome, descricao, tipo, texto }) {
64
+ return `---\nnome: ${nome}\ndescricao: ${descricao}\ntipo: ${tipo}\n---\n\n${String(texto).trim()}\n`;
65
+ }
66
+
67
+ /** Lê o frontmatter simples (chave: valor) e o corpo. null se não tem. */
68
+ function analisar(conteudo) {
69
+ const m = String(conteudo || '').match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
70
+ if (!m) return null;
71
+ const meta = {};
72
+ for (const l of m[1].split('\n')) {
73
+ const i = l.indexOf(':');
74
+ if (i > 0) meta[l.slice(0, i).trim()] = l.slice(i + 1).trim();
75
+ }
76
+ return { ...meta, corpo: m[2].trim() };
77
+ }
78
+
79
+ /** Todas as memórias, em ordem alfabética do arquivo. */
80
+ function listar(home) {
81
+ let nomes = [];
82
+ try { nomes = fs.readdirSync(pasta(home)); } catch { return []; }
83
+ const saida = [];
84
+ for (const f of nomes.sort()) {
85
+ if (!f.endsWith('.md') || f === INDICE) continue;
86
+ let a = null;
87
+ try { a = analisar(fs.readFileSync(path.join(pasta(home), f), 'utf-8')); } catch { /* ilegível: pula */ }
88
+ if (!a) continue;
89
+ saida.push({ nome: a.nome || f.slice(0, -3), descricao: a.descricao || '', tipo: a.tipo || '', arquivo: f, corpo: a.corpo });
90
+ }
91
+ return saida;
92
+ }
93
+
94
+ /** Refaz o MEMORIA.md a partir dos arquivos: o índice nunca desanda do que existe. */
95
+ function reindexar(home) {
96
+ const linhas = listar(home).map((m) => `- [${titulo(m.nome)}](${m.arquivo}) — ${m.descricao}`);
97
+ fs.mkdirSync(pasta(home), { recursive: true });
98
+ fs.writeFileSync(arquivoIndice(home), linhas.length ? linhas.join('\n') + '\n' : '', 'utf-8');
99
+ }
100
+
101
+ /**
102
+ * Salva ou atualiza (mesmo nome = mesmo arquivo, sem duplicar).
103
+ * Devolve { ok, nome, arquivo, criado } ou { ok:false, error }.
104
+ */
105
+ function salvar({ nome, descricao, tipo, texto } = {}, home) {
106
+ const slug = kebab(nome);
107
+ if (!slug) return { ok: false, error: 'Dê um nome curto pra memória (ex.: "prefere-respostas-curtas").' };
108
+ const desc = umaLinha(descricao);
109
+ if (!desc) return { ok: false, error: 'Falta a descrição: uma linha dizendo do que se trata.' };
110
+ const t = String(tipo || '').toLowerCase();
111
+ if (!TIPOS.includes(t)) return { ok: false, error: `Tipo inválido. Use um destes: ${TIPOS.join(', ')}.` };
112
+ const corpo = String(texto || '').trim();
113
+ if (!corpo) return { ok: false, error: 'Falta o texto da memória.' };
114
+ if (corpo.length > LIMITE_TEXTO) return { ok: false, error: `Texto grande demais (máx. ${LIMITE_TEXTO} letras). Resuma.` };
115
+ if (temSegredo(nome, desc, corpo)) {
116
+ return { ok: false, error: 'Isso tem cara de segredo (chave, token ou senha). Não guardo isso na memória. Tire o segredo e tente de novo.' };
117
+ }
118
+ const arquivo = `${slug}.md`;
119
+ const caminho = path.join(pasta(home), arquivo);
120
+ const criado = !fs.existsSync(caminho);
121
+ try {
122
+ fs.mkdirSync(pasta(home), { recursive: true });
123
+ fs.writeFileSync(caminho, montar({ nome: slug, descricao: desc, tipo: t, texto: corpo }), 'utf-8');
124
+ reindexar(home);
125
+ } catch (e) { return { ok: false, error: `Não consegui gravar: ${e.message}` }; }
126
+ return { ok: true, nome: slug, arquivo, criado };
127
+ }
128
+
129
+ /** Apaga pelo nome. Devolve { ok } ou { ok:false, error }. */
130
+ function apagar(nome, home) {
131
+ const slug = kebab(nome);
132
+ const caminho = path.join(pasta(home), `${slug}.md`);
133
+ if (!slug || !fs.existsSync(caminho)) return { ok: false, error: `Não achei a memória "${nome}".` };
134
+ try { fs.unlinkSync(caminho); reindexar(home); } catch (e) { return { ok: false, error: `Não consegui apagar: ${e.message}` }; }
135
+ return { ok: true, nome: slug };
136
+ }
137
+
138
+ /** O índice, cortado em `max` linhas (a cabeça fica). '' se não há memória. */
139
+ function carregarIndice(home, max = LINHAS_DO_INDICE) {
140
+ let t = '';
141
+ try { t = fs.readFileSync(arquivoIndice(home), 'utf-8'); } catch { return ''; }
142
+ const linhas = t.split('\n').filter((l) => l.trim());
143
+ if (linhas.length <= max) return linhas.join('\n');
144
+ return linhas.slice(0, max).join('\n') + `\n(… cortado: mais ${linhas.length - max} memórias, use /memoria)`;
145
+ }
146
+
147
+ /** As regras de quando salvar. Entram no contexto junto com o índice. */
148
+ const INSTRUCAO = '[memória]\n'
149
+ + 'Você tem memória que dura entre sessões, em ~/.primocode/memoria/ (ferramenta `lembrar`: ação salvar ou apagar). '
150
+ + 'O índice está abaixo; para ler uma memória, read_file no arquivo dela.\n'
151
+ + '- SALVE quando a pessoa corrige um jeito de trabalhar, diz uma preferência, conta quem é, ou dá um fato de projeto que não está no código. '
152
+ + 'tipo: usuario (quem é, como trabalha), feedback (correção ou preferência), projeto (fato do projeto), referencia (onde achar algo).\n'
153
+ + '- NÃO salve o que o código ou o git já mostram, nem o que só vale para esta conversa. Nunca salve chave, token ou senha.\n'
154
+ + '- ANTES de salvar, confira o índice: se já existe uma que cobre, salve com o MESMO nome para atualizar, em vez de duplicar.';
155
+
156
+ /** O bloco que entra no contexto da sessão: instrução + índice. */
157
+ /* No servidor (plano grátis) o contexto fixo é curto: o índice entra só com as primeiras `linhas`, e o resto
158
+ vira o aviso de corte. Com provedor próprio (/api) entra até LINHAS_DO_INDICE. */
159
+ function blocoDoContexto(home, { linhas = LINHAS_DO_INDICE } = {}) {
160
+ const indice = carregarIndice(home, linhas);
161
+ return INSTRUCAO + (indice ? `\n\n[MEMORIA.md — o que você já sabe sobre esta pessoa]\n${indice}` : '\n\n(Ainda não há memórias salvas.)');
162
+ }
163
+
164
+ module.exports = { TIPOS, INDICE, LINHAS_DO_INDICE, pasta, arquivoIndice, kebab, temSegredo, listar, salvar, apagar, reindexar, carregarIndice, blocoDoContexto, INSTRUCAO };
package/lib/nuvem.js CHANGED
@@ -300,6 +300,27 @@ async function lerConversa(id, { limite = 200 } = {}) {
300
300
  } catch (e) { return { ok: false, error: e.message }; }
301
301
  }
302
302
 
303
+ /**
304
+ * Apaga uma conversa. O Firestore não apaga a subcoleção junto com o
305
+ * documento, então as mensagens vão antes, em lotes, e a conversa por último.
306
+ */
307
+ async function apagarConversa(id) {
308
+ const s = await abrir();
309
+ if (!s) return { ok: false, error: 'não conectado' };
310
+ const base = `${BASE(s.projeto)}/users/${s.uid}/${COLECAO}/${id}`;
311
+ const auth = { Authorization: `Bearer ${s.idToken}` };
312
+ try {
313
+ for (let volta = 0; volta < 50; volta++) {
314
+ const r = await httpsJson(`${base}/messages?pageSize=300`, 'GET', null, auth);
315
+ const docs = r.documents || [];
316
+ if (!docs.length) break;
317
+ for (const d of docs) await httpsJson(`${FIRESTORE}/v1/${d.name}`, 'DELETE', null, auth);
318
+ }
319
+ await httpsJson(base, 'DELETE', null, auth);
320
+ return { ok: true };
321
+ } catch (e) { return { ok: false, error: e.message }; }
322
+ }
323
+
303
324
  // ── o gravador da sessão ─────────────────────────────────────────────────
304
325
 
305
326
  /** Título a partir da primeira frase: uma linha, curta, sem cortar no meio de
@@ -388,7 +409,7 @@ module.exports = {
388
409
  cifrar, decifrar, cifrarJson, decifrarJson,
389
410
  gravador, tituloDe,
390
411
  abrir, esquecer,
391
- salvarConversa, salvarMensagem, listarConversas, lerConversa,
412
+ salvarConversa, salvarMensagem, listarConversas, lerConversa, apagarConversa,
392
413
  COLECAO, VERSAO_CIFRA, ITERACOES, TAM_IV,
393
414
  // A tradução para o formato do Firestore sai daqui porque é ela que decide
394
415
  // o que vira `stringValue` em claro e o que vira `mapValue` cifrado.
package/lib/repeticao.js CHANGED
@@ -100,7 +100,7 @@ function consultaDe(nome, args) {
100
100
  * ficam fora da época. */
101
101
  const OBSERVAM = new Set(['read_file', 'list_dir', 'glob', 'grep', 'browser_read', 'browser_screenshot',
102
102
  'desktop_screenshot', 'desktop_size', 'desktop_windows', 'desktop_elements']);
103
- const NAO_MUDAM = new Set(['write_todos', 'remember_general', 'pesquisar_referencia', ...REFERENCIAS]);
103
+ const NAO_MUDAM = new Set(['write_todos', 'remember_general', 'lembrar', 'pesquisar_referencia', ...REFERENCIAS]);
104
104
  const SHELL_SO_LEITURA = /^\s*(ls|cat|head|tail|pwd|echo|grep|rg|find|wc|tree|stat|file|which|type|du|df|env|printenv|date|git\s+(status|log|diff|show|branch|remote))\b[^;&|>]*$/;
105
105
 
106
106
  function comandoDe(args) {
package/lib/sandbox.js CHANGED
@@ -8,13 +8,18 @@
8
8
  * hard_requirement (recusa)
9
9
  *
10
10
  * O que o comando pode: escrever na pasta do projeto, em /tmp e em
11
- * ~/.primocode/cache; ler quase tudo, MENOS ~/.ssh, ~/.aws, ~/.config/gcloud,
12
- * ~/.npmrc e .env*. Rede ligada (o `npm install` precisa), com opção de cortar.
11
+ * ~/.primocode/cache; ler quase tudo, MENOS os arquivos de credencial da
12
+ * LEITURA_NEGADA_HOME (chaves, tokens, a conta do PrimoCode, o Keychain) e
13
+ * .env*. Rede ligada (o `npm install` precisa), com opção de cortar.
14
+ * No macOS ainda fica de fora: osascript, o comando `security` (Keychain) e
15
+ * escrever hook em .git/hooks (rodaria FORA da cerca no próximo commit).
16
+ * O ssh-agent da pessoa fica fora por padrão (review do Arthur, #162): sem
17
+ * ele, "não lê ~/.ssh" vale de verdade, e não só para o arquivo.
13
18
  * Quando bloqueia, a mensagem diz POR QUE, para o modelo aprender.
14
19
  *
15
20
  * Config em ~/.primocode/config.json:
16
21
  * "sandbox": { "ligado": true, "modo": "best_effort"|"hard_requirement",
17
- * "rede": true, "env_permitidas": ["MINHA_KEY"],
22
+ * "rede": true, "ssh_agent": false, "env_permitidas": ["MINHA_KEY"],
18
23
  * "escrita_extra": ["~/.npm"] }
19
24
  */
20
25
  'use strict';
@@ -25,10 +30,14 @@ const path = require('path');
25
30
  const { execFile, spawnSync } = require('child_process');
26
31
 
27
32
  const MODOS = ['best_effort', 'hard_requirement'];
28
- const LEITURA_NEGADA_HOME = ['.ssh', '.aws', '.config/gcloud', '.npmrc'];
33
+ const LEITURA_NEGADA_HOME = ['.ssh', '.aws', '.config/gcloud', '.npmrc',
34
+ '.primocode/conta.json', '.netrc', '.git-credentials', '.config/gh', '.docker', '.kube', 'Library/Keychains'];
35
+ // Socket do ssh-agent do macOS (launchd). O caminho exato vem do SSH_AUTH_SOCK; a regex pega os outros.
36
+ const SOCKET_SSH_MAC = '^/private/(tmp|var/run)/com\\.apple\\.launchd\\.[^/]+/Listeners$';
29
37
  const SECRETA = /(_KEY|_TOKEN|SECRET)/i;
30
38
 
31
39
  let _avisou = false;
40
+ let _avisouDesligado = false;
32
41
  let _forcaMecanismo; // teste: undefined = detectar; null = ausente; 'seatbelt'|'bwrap'
33
42
 
34
43
  function arquivoConfig(home) { return path.join(home || os.homedir(), '.primocode', 'config.json'); }
@@ -43,6 +52,7 @@ function config(home) {
43
52
  ligado: typeof s.ligado === 'boolean' ? s.ligado : process.platform === 'darwin',
44
53
  modo: MODOS.includes(s.modo) ? s.modo : 'best_effort',
45
54
  rede: s.rede !== false,
55
+ ssh_agent: s.ssh_agent === true,
46
56
  env_permitidas: Array.isArray(s.env_permitidas) ? s.env_permitidas.map(String) : [],
47
57
  escrita_extra: Array.isArray(s.escrita_extra) ? s.escrita_extra.map(String) : [],
48
58
  };
@@ -68,6 +78,17 @@ function mecanismo() {
68
78
  return null;
69
79
  }
70
80
 
81
+ /** Linha para a PESSOA ver no terminal (o stderr do comando só o modelo lê). Vai pro
82
+ * stderr do processo para não sujar a saída do `-p`. */
83
+ function avisarPessoa(msg) {
84
+ try { console.error(require('./ui.js').c.warn(` ⎿ ${msg}`)); } catch { /* sem tela */ }
85
+ }
86
+
87
+ function socketSsh() {
88
+ const s = process.env.SSH_AUTH_SOCK;
89
+ try { return s && fs.statSync(s).isSocket() ? real(s) : null; } catch { return null; }
90
+ }
91
+
71
92
  function real(p) { try { return fs.realpathSync(p); } catch { return path.resolve(p); } }
72
93
  function expandir(p, home) { return p.startsWith('~') ? path.join(home, p.slice(1)) : p; }
73
94
  const sbq = (s) => '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
@@ -86,8 +107,20 @@ function perfilSeatbelt(projectDir, cfg = config(), home = os.homedir()) {
86
107
  const l = ['(version 1)', '(allow default)', '(deny file-write*)'];
87
108
  for (const p of escrita) l.push(`(allow file-write* (subpath ${sbq(p)}))`);
88
109
  l.push('(allow file-write* (literal "/dev/null") (literal "/dev/tty") (literal "/dev/dtracehelper") (regex #"^/dev/fd/"))');
110
+ // Hook em .git/hooks roda fora da cerca no próximo `git commit` da pessoa. O .sample do `git init` passa.
111
+ l.push('(deny file-write* (regex #"/\\.git/hooks/[^/]+$"))');
112
+ l.push('(allow file-write* (regex #"/\\.git/hooks/[^/]+\\.sample$"))');
89
113
  for (const p of negadosLeitura) l.push(`(deny file-read* (subpath ${sbq(real(p))}) (subpath ${sbq(p)}))`);
90
114
  l.push('(deny file-read* (regex #"/\\.env($|\\.[^/]*$)"))');
115
+ // osascript manda o Terminal/Finder fazer o que a cerca não deixa; `security` lê o Keychain.
116
+ l.push('(deny process-exec (literal "/usr/bin/osascript") (literal "/usr/bin/security"))');
117
+ l.push('(deny appleevent-send)');
118
+ l.push('(deny mach-lookup (global-name "com.apple.SecurityServer") (global-name "com.apple.securityd"))');
119
+ if (!cfg.ssh_agent) {
120
+ const sock = socketSsh();
121
+ l.push(`(deny network-outbound (remote unix-socket (path-regex #"${SOCKET_SSH_MAC}")))`);
122
+ if (sock) l.push(`(deny network-outbound (remote unix-socket (path-literal ${sbq(sock)})))`);
123
+ }
91
124
  if (!cfg.rede) l.push('(deny network*)');
92
125
  return l.join('\n');
93
126
  }
@@ -106,6 +139,11 @@ function argsBwrap(projectDir, cfg = config(), home = os.homedir()) {
106
139
  if (/^\.env($|\.)/.test(f)) a.push('--ro-bind', '/dev/null', path.join(real(projectDir), f));
107
140
  }
108
141
  } catch { /* pasta sumiu */ }
142
+ // Montado DEPOIS do --bind do projeto, para valer por cima dele. [VERIFICAR] no Linux.
143
+ const hooks = path.join(real(projectDir), '.git', 'hooks');
144
+ if (fs.existsSync(hooks)) a.push('--ro-bind', hooks, hooks);
145
+ const sock = cfg.ssh_agent ? null : socketSsh();
146
+ if (sock) a.push('--ro-bind', '/dev/null', sock);
109
147
  if (!cfg.rede) a.push('--unshare-net');
110
148
  a.push('--die-with-parent', '--chdir', real(projectDir));
111
149
  return a;
@@ -115,7 +153,16 @@ function argsBwrap(projectDir, cfg = config(), home = os.homedir()) {
115
153
  function envolver(command, projectDir, opts = {}) {
116
154
  const home = opts.home || os.homedir();
117
155
  const cfg = opts.cfg || config(home);
118
- if (!cfg.ligado) return { semCerca: true, motivo: 'desligado' };
156
+ if (!cfg.ligado) {
157
+ let aviso = null;
158
+ if (!_avisouDesligado) {
159
+ _avisouDesligado = true;
160
+ aviso = process.platform === 'darwin'
161
+ ? 'aviso: o sandbox está desligado; os comandos do agente rodam sem cerca. /sandbox on liga.'
162
+ : 'aviso: o sandbox vem desligado fora do macOS; os comandos do agente rodam sem cerca. /sandbox on liga (no Linux precisa do bwrap).';
163
+ }
164
+ return { semCerca: true, motivo: 'desligado', aviso };
165
+ }
119
166
  const mec = mecanismo();
120
167
  if (!mec) {
121
168
  if (cfg.modo === 'hard_requirement') {
@@ -132,14 +179,15 @@ function envolver(command, projectDir, opts = {}) {
132
179
  /** Se o comando falhou por causa da cerca, diz por quê (e qual caminho). */
133
180
  function explicar(stderr, projectDir, cfg, home) {
134
181
  const txt = String(stderr || '');
135
- const m = /(?:^|\n)(?:[^\n:]*:\s*)?(\/[^\n:]+?|\.{1,2}\/[^\n:]+?|[^\s:'"]+?):?\s*(?:Operation not permitted|Permission denied|Read-only file system)/.exec(txt)
182
+ const m = /(?:^|\n)(?:[^\n:]*:\s*)?(\/[^\n:]+?|\.{1,2}\/[^\n:]+?|[^\s:'"]+?):?\s*(?:(?:open|read|stat|lstat|opendir|write):\s*)?(?:Operation not permitted|Permission denied|Read-only file system)/.exec(txt)
136
183
  || /(?:Operation not permitted|Permission denied|Read-only file system)[^\n]*?['"]([^'"\n]+)['"]/.exec(txt);
137
184
  if (!m) return null;
138
185
  const alvo = path.resolve(projectDir, m[1].trim());
139
186
  const { negadosLeitura } = caminhos(projectDir, cfg, home);
140
187
  const secreto = negadosLeitura.some((p) => alvo === p || alvo.startsWith(p + path.sep) || alvo.startsWith(real(p) + path.sep))
141
188
  || /(^|\/)\.env($|\.)/.test(alvo);
142
- if (secreto) return `o sandbox bloqueou leitura de segredo: ${alvo} (~/.ssh, ~/.aws, ~/.config/gcloud, ~/.npmrc e .env* ficam fora do alcance). Não tente ler isso; peça ao usuário o que precisa.`;
189
+ if (secreto) return `o sandbox bloqueou leitura de segredo: ${alvo} (${LEITURA_NEGADA_HOME.map((p) => '~/' + p).join(', ')} e .env* ficam fora do alcance). Não tente ler isso; peça ao usuário o que precisa.`;
190
+ if (/\/\.git\/hooks\//.test(alvo)) return `o sandbox bloqueou escrita em hook do git: ${alvo}. Hook roda fora da cerca; se o usuário quiser um, ele instala.`;
143
191
  if (/Read-only|Operation not permitted|Permission denied/.test(txt)) {
144
192
  return `o sandbox bloqueou escrita fora do projeto: ${alvo}. Escreva só dentro de ${projectDir}, em /tmp ou em ~/.primocode/cache.`;
145
193
  }
@@ -168,7 +216,7 @@ function executar(command, { projectDir, timeout = 60000, env = process.env, hom
168
216
  const termina = (error, stdout, stderr) => {
169
217
  let err = stderr || '';
170
218
  const info = { mecanismo: e.mec || null };
171
- if (e.aviso) err = e.aviso + '\n' + err;
219
+ if (e.aviso) { err = e.aviso + '\n' + err; avisarPessoa(e.aviso); }
172
220
  if (error && e.mec) {
173
221
  const why = explicar(err, cwd, cfg, e.home);
174
222
  if (why) { err = err + '\n[sandbox] ' + why; info.bloqueou = why; }
@@ -191,7 +239,7 @@ function estado(home) {
191
239
 
192
240
  function _teste({ mecanismo: m, avisou } = {}) {
193
241
  if (m !== undefined) _forcaMecanismo = m === 'auto' ? undefined : m;
194
- if (avisou !== undefined) _avisou = avisou;
242
+ if (avisou !== undefined) { _avisou = avisou; _avisouDesligado = avisou; }
195
243
  }
196
244
 
197
245
  module.exports = { config, salvar, estado, mecanismo, envolver, executar, explicar, filtrarEnv, perfilSeatbelt, argsBwrap, MODOS, _teste };
package/lib/tools.js CHANGED
@@ -274,10 +274,13 @@ function grepFiles({ pattern, glob, ignoreCase, contexto = 0, maxResultados = 10
274
274
  USER, NODE_*, nvm etc. continuam — sem eles `npm`, `node` e `git` quebram.
275
275
  Na dúvida o nome sai: um token a mais escondido custa pouco, um a menos
276
276
  vaza. Só o caminho do agente (execute → run_shell) é filtrado; os usos
277
- internos (abrir terminal, desinstalar) seguem com o ambiente inteiro. */
277
+ internos (abrir terminal, desinstalar) seguem com o ambiente inteiro.
278
+ O SSH_AUTH_SOCK também sai (review do Arthur, #162): com ele o agente usa o
279
+ ssh-agent da pessoa (`git push`, `ssh host`) sem ler ~/.ssh. Volta só com
280
+ `/sandbox ssh on`. */
278
281
  const NOME_SECRETO = /(TOKEN|SECRET|KEY|PASSWORD|PASSWD|CREDENTIAL|AUTH|COOKIE|SESSION)/i;
279
282
  const PREFIXO_SECRETO = /^(AWS_|OPENAI_|ANTHROPIC_|GEMINI_|GOOGLE_API|GROQ_|OPENROUTER_|AZURE_|HF_|HUGGING)/i;
280
- const SEMPRE_PASSA = new Set(['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'TERM', 'TMPDIR', 'PWD', 'SSH_AUTH_SOCK']);
283
+ const SEMPRE_PASSA = new Set(['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'TERM', 'TMPDIR', 'PWD']);
281
284
 
282
285
  function ambienteLimpo(env = process.env, permitidas = []) {
283
286
  const limpo = {};
@@ -289,6 +292,10 @@ function ambienteLimpo(env = process.env, permitidas = []) {
289
292
  return limpo;
290
293
  }
291
294
 
295
+ function permitidasNoShell(cfg = sandbox.config()) {
296
+ return cfg.ssh_agent ? [...cfg.env_permitidas, 'SSH_AUTH_SOCK'] : cfg.env_permitidas;
297
+ }
298
+
292
299
  async function runShell(command, timeout = 60000, cwd, env) {
293
300
  return new Promise((resolve) => {
294
301
  exec(command, { timeout, cwd: cwd || process.cwd(), maxBuffer: 10 * 1024 * 1024, env: env || process.env }, (error, stdout, stderr) => {
@@ -668,7 +675,7 @@ async function despachar(name, args, ctx) {
668
675
  case 'run_shell':
669
676
  // Dentro da cerca (lib/sandbox.js); desligada ou sem mecanismo cai no exec de sempre.
670
677
  return sandbox.executar(args.command, { projectDir, timeout: args.timeout_ms || 60000,
671
- env: ambienteLimpo(process.env, sandbox.config().env_permitidas) });
678
+ env: ambienteLimpo(process.env, permitidasNoShell()) });
672
679
  case 'run_in_new_terminal':
673
680
  return runInNewTerminal(args.command, args.cwd || projectDir);
674
681
  case 'open_in_browser': {
@@ -712,6 +719,15 @@ async function despachar(name, args, ctx) {
712
719
  return { ok: true, gravado: entry };
713
720
  }
714
721
 
722
+ // A memória que dura entre sessões (lib/memoria-usuario.js).
723
+ case 'lembrar': {
724
+ const mem = require('./memoria-usuario.js');
725
+ const acao = String(args.acao || 'salvar').toLowerCase();
726
+ if (acao === 'apagar') return mem.apagar(args.nome);
727
+ if (acao === 'salvar') return mem.salvar({ nome: args.nome, descricao: args.descricao, tipo: args.tipo, texto: args.texto });
728
+ return { ok: false, error: 'Ação inválida. Use "salvar" ou "apagar".' };
729
+ }
730
+
715
731
  // ── MCP: o serviço pela API dele, antes do mouse (lib/mcp.js) ──
716
732
  case 'mcp_procurar':
717
733
  return require('./mcp.js').procurar(args.busca || args.query || args.servico || '');
@@ -978,7 +994,7 @@ async function uninstall() {
978
994
  }
979
995
 
980
996
  module.exports = {
981
- fmtBytes, treeDir, runShell, ambienteLimpo, openInBrowser, runInNewTerminal,
997
+ fmtBytes, treeDir, runShell, ambienteLimpo, permitidasNoShell, openInBrowser, runInNewTerminal,
982
998
  execute, uninstall, resolveInProject, globToRegex, globFiles, grepFiles, editFile,
983
999
  multiEdit, writeTodos, getTodos,
984
1000
  liberarLeitura, pastasDeConsulta, esquecerConsultas, resolverParaLer,
package/lib/ui.js CHANGED
@@ -557,7 +557,7 @@ const ROTULOS = {
557
557
  desktop_windows:'Desktop·janelas', desktop_focus:'Desktop·foco',
558
558
  desktop_elements:'Desktop·elementos', desktop_click_element:'Desktop·click',
559
559
  estudio_midia:'Estúdio·mídia', estudio_ae:'Estúdio·After Effects', estudio_motion:'Estúdio·Motion', estudio_editar:'Estúdio·Editar', construir_app:'Construir',
560
- remember_general:'Lembrar',
560
+ remember_general:'Lembrar', lembrar:'Memória',
561
561
  mcp_procurar:'MCP·procurar', mcp_conectar:'MCP·conectar', mcp_usar:'MCP',
562
562
  };
563
563
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "10.15.1",
3
+ "version": "10.16.0",
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": {
@@ -10,7 +10,7 @@
10
10
  "start": "node bin/primocode.js",
11
11
  "postinstall": "node lib/preparar.js || true",
12
12
  "test:server": "cd server && npm test",
13
- "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/primo-voz.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/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.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/conta.test.mjs && node test/planos-tela.test.js && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.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 && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/conferencia.test.js && node test/cor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/mcp-http.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/rolar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js && node test/estilos.test.js && node test/proporcao.test.js && node test/cena.test.js && node test/sem-narracao.test.js && node test/camadas.test.js && node test/gerativo-referencias.test.js && node test/voicestudio.test.js && node test/effectcraft.test.js && node test/projetos.test.js && node test/maquina.test.js && node test/maquina-som.test.js && node test/maquina-voz.test.js && node test/prova-fresca.test.js && node test/cota-video.test.js && node test/hooks.test.js && node test/modo-plano.test.js && node test/headless.test.js && node test/retomar.test.js && node test/sandbox.test.js && node test/exportar.test.js && node test/editor-video.test.js && node test/marca-kit.test.js && node test/preparar.test.js && node test/landing-planos.test.js && node test/limites.test.js"
13
+ "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/primo-voz.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/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.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/conta.test.mjs && node test/planos-tela.test.js && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.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 && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/conferencia.test.js && node test/cor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/mcp-http.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/rolar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js && node test/estilos.test.js && node test/proporcao.test.js && node test/cena.test.js && node test/sem-narracao.test.js && node test/camadas.test.js && node test/gerativo-referencias.test.js && node test/voicestudio.test.js && node test/effectcraft.test.js && node test/projetos.test.js && node test/maquina.test.js && node test/maquina-som.test.js && node test/maquina-voz.test.js && node test/prova-fresca.test.js && node test/cota-video.test.js && node test/hooks.test.js && node test/modo-plano.test.js && node test/headless.test.js && node test/retomar.test.js && node test/sandbox.test.js && node test/exportar.test.js && node test/editor-video.test.js && node test/marca-kit.test.js && node test/preparar.test.js && node test/landing-planos.test.js && node test/limites.test.js && node test/app-chat-cli.test.js && node test/app-chat-ponte.test.js && node test/gosto-motion.test.js && node test/desktop-vivo.test.js && node test/memoria-usuario.test.js && node test/landing-instalar.test.js"
14
14
  },
15
15
  "engines": {
16
16
  "node": ">=18.17.0"