primocode 9.8.0-beta.1 → 9.8.0-beta.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.
- package/README.md +21 -6
- package/bin/primocode.js +338 -56
- package/comandos/explicar.md +14 -0
- package/comandos/publicar.md +10 -0
- package/comandos/revisar.md +13 -0
- package/kits/landing/index.html +162 -0
- package/kits/landing/script.js +199 -0
- package/kits/landing/style.css +219 -0
- package/lib/act.js +43 -10
- package/lib/api.js +5 -2
- package/lib/checkpoint.js +134 -0
- package/lib/claude-engine.js +630 -0
- package/lib/codex-engine.js +207 -0
- package/lib/comandos.js +104 -0
- package/lib/config.js +18 -4
- package/lib/kits.js +81 -0
- package/lib/local/motor-cli.js +90 -0
- package/lib/local/provedor.js +20 -2
- package/lib/mencoes.js +98 -0
- package/lib/perigo.js +256 -0
- package/package.json +4 -2
package/lib/mencoes.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// O que a pessoa TRAZ para o pedido, além do texto: as instruções do projeto
|
|
2
|
+
// (PRIMO.md, AGENTS.md, CLAUDE.md) e os arquivos citados com @.
|
|
3
|
+
//
|
|
4
|
+
// Ideia tirada do Claude Code (CLAUDE.md, @arquivo, /init) e do Codex
|
|
5
|
+
// (AGENTS.md). Quem já escreveu um AGENTS.md para outro agente não deveria ter
|
|
6
|
+
// de escrever de novo para o PrimoCode — então os três nomes valem.
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
/* ── INSTRUÇÕES DO PROJETO ───────────────────────────────────────────────
|
|
12
|
+
* Só o PRIMEIRO que existir, na ordem abaixo: um repo com AGENTS.md e
|
|
13
|
+
* CLAUDE.md quase sempre tem o mesmo texto nos dois (um aponta pro outro), e
|
|
14
|
+
* mandar os dois é pagar duas vezes pela mesma regra. O PRIMO.md vem antes
|
|
15
|
+
* porque é o que a pessoa escreveu PARA nós. */
|
|
16
|
+
const NOMES_INSTRUCOES = ['PRIMO.md', 'AGENTS.md', 'CLAUDE.md'];
|
|
17
|
+
const LIMITE_INSTRUCOES = 6000;
|
|
18
|
+
|
|
19
|
+
// A cabeça fica, o rabo sai: é no começo que se escreve o que mais importa.
|
|
20
|
+
function cortar(texto, limite) {
|
|
21
|
+
if (texto.length <= limite) return texto;
|
|
22
|
+
return texto.slice(0, limite).trimEnd() + '\n(cortado)';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function lerSeExiste(arquivo) {
|
|
26
|
+
try {
|
|
27
|
+
if (!fs.statSync(arquivo).isFile()) return '';
|
|
28
|
+
return fs.readFileSync(arquivo, 'utf-8');
|
|
29
|
+
} catch { return ''; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** O texto que entra no contexto, ou '' se não há instrução nenhuma. */
|
|
33
|
+
function instrucoesDoProjeto(projectDir, limite = LIMITE_INSTRUCOES) {
|
|
34
|
+
const partes = [];
|
|
35
|
+
// HOME lido na hora, não na carga do módulo: o teste troca a casa.
|
|
36
|
+
const global = lerSeExiste(path.join(os.homedir(), '.primocode', 'PRIMO.md'));
|
|
37
|
+
if (global.trim()) partes.push(`[~/.primocode/PRIMO.md — suas instruções para todo projeto]\n${cortar(global, limite)}`);
|
|
38
|
+
if (projectDir) {
|
|
39
|
+
for (const nome of NOMES_INSTRUCOES) {
|
|
40
|
+
const texto = lerSeExiste(path.join(projectDir, nome));
|
|
41
|
+
if (!texto.trim()) continue;
|
|
42
|
+
partes.push(`[${nome} — instruções deste projeto: siga]\n${cortar(texto, limite)}`);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return partes.join('\n\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ── @ARQUIVO ────────────────────────────────────────────────────────────
|
|
50
|
+
* O @ só conta no começo de uma palavra: "joel@x.com" e "@" solto passam
|
|
51
|
+
* direto. E só vira anexo o que existe DENTRO da pasta do projeto — "@../../
|
|
52
|
+
* .ssh/id_rsa" não pode mandar a chave de ninguém para o modelo. O que não
|
|
53
|
+
* resolve fica como está no texto: pode ser um @usuario de verdade. */
|
|
54
|
+
const LIMITE_ARQUIVO = 40 * 1024;
|
|
55
|
+
const LIMITE_TOTAL = 120 * 1024;
|
|
56
|
+
const RE_MENCAO = /(^|[\s(])@(?:"([^"]+)"|([^\s"]+))/g;
|
|
57
|
+
|
|
58
|
+
function pareceBinario(abs) {
|
|
59
|
+
let fd;
|
|
60
|
+
try {
|
|
61
|
+
fd = fs.openSync(abs, 'r');
|
|
62
|
+
const buf = Buffer.alloc(8000);
|
|
63
|
+
const lidos = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
64
|
+
return buf.subarray(0, lidos).includes(0);
|
|
65
|
+
} catch { return true; }
|
|
66
|
+
finally { if (fd !== undefined) try { fs.closeSync(fd); } catch { /* já fechado */ } }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function mencoes(texto, projectDir, limites = {}) {
|
|
70
|
+
const porArquivo = limites.porArquivo || LIMITE_ARQUIVO;
|
|
71
|
+
const total = limites.total || LIMITE_TOTAL;
|
|
72
|
+
const arquivos = [];
|
|
73
|
+
const ignorados = [];
|
|
74
|
+
if (!texto || !projectDir) return { arquivos, ignorados };
|
|
75
|
+
const raiz = path.resolve(projectDir);
|
|
76
|
+
const vistos = new Set();
|
|
77
|
+
let soma = 0;
|
|
78
|
+
for (const m of String(texto).matchAll(RE_MENCAO)) {
|
|
79
|
+
// Pontuação colada ("veja @index.html.") é da frase, não do nome.
|
|
80
|
+
const bruto = m[2] || m[3].replace(/[.,;:!?)\]]+$/, '');
|
|
81
|
+
if (!bruto) continue;
|
|
82
|
+
const abs = path.resolve(raiz, bruto);
|
|
83
|
+
const rel = path.relative(raiz, abs);
|
|
84
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) continue;
|
|
85
|
+
let st;
|
|
86
|
+
try { st = fs.statSync(abs); } catch { continue; }
|
|
87
|
+
if (!st.isFile() || vistos.has(abs)) continue;
|
|
88
|
+
vistos.add(abs);
|
|
89
|
+
if (st.size > porArquivo) { ignorados.push({ rel, motivo: `grande demais (limite ${Math.round(porArquivo / 1024)} KB)` }); continue; }
|
|
90
|
+
if (pareceBinario(abs)) { ignorados.push({ rel, motivo: 'arquivo binário' }); continue; }
|
|
91
|
+
if (soma + st.size > total) { ignorados.push({ rel, motivo: `passou do total de ${Math.round(total / 1024)} KB` }); continue; }
|
|
92
|
+
soma += st.size;
|
|
93
|
+
arquivos.push({ rel, abs, bytes: st.size });
|
|
94
|
+
}
|
|
95
|
+
return { arquivos, ignorados };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { mencoes, instrucoesDoProjeto, NOMES_INSTRUCOES, LIMITE_INSTRUCOES, LIMITE_ARQUIVO, LIMITE_TOTAL };
|
package/lib/perigo.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* perigo.js — o que não tem volta pede licença antes.
|
|
3
|
+
*
|
|
4
|
+
* Até aqui o PrimoCode rodava qualquer `run_shell` e escrevia qualquer arquivo
|
|
5
|
+
* sem perguntar. Para quase tudo isso é certo: quem não programa não sabe
|
|
6
|
+
* responder "autoriza `npm install`?", e uma pergunta que ninguém sabe
|
|
7
|
+
* responder vira um "sim" automático. O problema é o resto — o `rm -rf ~`, o
|
|
8
|
+
* `git push --force`, o `curl … | sh`. Esses não se desfazem.
|
|
9
|
+
*
|
|
10
|
+
* Por isso a régua é estreita DE PROPÓSITO: perigoso é o que apaga, reescreve
|
|
11
|
+
* história ou sai da pasta do projeto. `rm -rf node_modules` dentro do projeto
|
|
12
|
+
* NÃO é perigoso — se fosse, o Primo perguntaria dez vezes por dia, e na
|
|
13
|
+
* décima primeira a pessoa diria sim sem ler. Falso positivo aqui é tão ruim
|
|
14
|
+
* quanto falso negativo: ele treina a pessoa a não ler a pergunta.
|
|
15
|
+
*
|
|
16
|
+
* Os três modos (/permissoes):
|
|
17
|
+
* auto — o padrão. Roda tudo, mas SEMPRE pergunta antes do perigoso.
|
|
18
|
+
* perguntar — pergunta antes de todo comando e de toda escrita de arquivo.
|
|
19
|
+
* livre — nunca pergunta. Nem o perigoso.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
const MODOS = ['auto', 'perguntar', 'livre'];
|
|
28
|
+
const COMANDOS = new Set(['run_shell', 'run_in_new_terminal']);
|
|
29
|
+
const ESCRITAS = new Set(['write_file', 'edit_file', 'multi_edit']);
|
|
30
|
+
|
|
31
|
+
function normalizarModo(m) {
|
|
32
|
+
const s = String(m || '').trim().toLowerCase();
|
|
33
|
+
const apelido = { ask: 'perguntar', suggest: 'perguntar', full: 'livre', 'full-auto': 'livre', yolo: 'livre' };
|
|
34
|
+
const n = apelido[s] || s;
|
|
35
|
+
return MODOS.includes(n) ? n : 'auto';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Quebra o comando em trechos (separados por ; && || | e quebra de linha) e
|
|
39
|
+
cada trecho em palavras — respeitando aspas. Sem respeitar aspas,
|
|
40
|
+
`git commit -m "tira o rm -rf /"` viraria um trecho `rm -rf /` e o Primo
|
|
41
|
+
pararia para perguntar sobre uma mensagem de commit. */
|
|
42
|
+
function trechos(cmd) {
|
|
43
|
+
const saida = [];
|
|
44
|
+
let palavras = [], atual = '', aspa = null, temPalavra = false;
|
|
45
|
+
const fechaPalavra = () => { if (temPalavra) palavras.push(atual); atual = ''; temPalavra = false; };
|
|
46
|
+
const fechaTrecho = (sep) => { fechaPalavra(); if (palavras.length) saida.push({ palavras, sep }); palavras = []; };
|
|
47
|
+
const s = String(cmd || '');
|
|
48
|
+
for (let i = 0; i < s.length; i++) {
|
|
49
|
+
const ch = s[i];
|
|
50
|
+
if (aspa) {
|
|
51
|
+
if (ch === aspa) aspa = null;
|
|
52
|
+
else if (ch === '\\' && aspa === '"' && i + 1 < s.length) atual += s[++i];
|
|
53
|
+
else atual += ch;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (ch === '"' || ch === "'") { aspa = ch; temPalavra = true; continue; }
|
|
57
|
+
if (ch === '\\' && i + 1 < s.length) { atual += s[++i]; temPalavra = true; continue; }
|
|
58
|
+
if (ch === ';' || ch === '\n') { fechaTrecho(';'); continue; }
|
|
59
|
+
if (ch === '&' && s[i + 1] === '&') { fechaTrecho(';'); i++; continue; }
|
|
60
|
+
if (ch === '|' && s[i + 1] === '|') { fechaTrecho(';'); i++; continue; }
|
|
61
|
+
if (ch === '|') { fechaTrecho('|'); continue; }
|
|
62
|
+
if (ch === '&') { fechaTrecho(';'); continue; }
|
|
63
|
+
if (/\s/.test(ch)) { fechaPalavra(); continue; }
|
|
64
|
+
atual += ch; temPalavra = true;
|
|
65
|
+
}
|
|
66
|
+
fechaTrecho(';');
|
|
67
|
+
return saida;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const TEMPORARIAS = [...new Set(['/tmp', '/private/tmp', os.tmpdir()])];
|
|
71
|
+
|
|
72
|
+
function dentro(pai, filho) {
|
|
73
|
+
const r = path.relative(pai, filho);
|
|
74
|
+
return !!r && !r.startsWith('..') && !path.isAbsolute(r);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function expandir(alvo) {
|
|
78
|
+
const home = os.homedir();
|
|
79
|
+
return String(alvo).replace(/^~(?=\/|$)/, home).replace(/^\$\{?HOME\}?(?=\/|$)/, home);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** O alvo cai FORA do projeto (ou é o projeto inteiro, ou um curinga que o esvazia)? */
|
|
83
|
+
function alvoRuim(alvo, cwd, projeto, { recursivo, raizOk = false }) {
|
|
84
|
+
const a = String(alvo);
|
|
85
|
+
if (/^\.?\/?\*$|^\.\*$/.test(a)) return recursivo ? 'apaga tudo o que tem na pasta' : null;
|
|
86
|
+
const e = expandir(a);
|
|
87
|
+
// Variável que não é o HOME: não dá para saber aonde aponta, e "não sei"
|
|
88
|
+
// num rm é motivo de perguntar.
|
|
89
|
+
if (/\$/.test(e)) return 'apaga um caminho que depende de variável';
|
|
90
|
+
const alvoAbs = path.resolve(cwd, e.replace(/\/\*$/, ''));
|
|
91
|
+
const rel = path.relative(projeto, alvoAbs);
|
|
92
|
+
if (e === '/' || alvoAbs === path.parse(alvoAbs).root) return 'apaga a partir da raiz do computador';
|
|
93
|
+
if (alvoAbs === os.homedir()) return 'apaga a sua pasta pessoal inteira';
|
|
94
|
+
if (rel === '') return raizOk ? null : 'apaga a pasta do projeto inteira';
|
|
95
|
+
// A pasta temporária é descartável por definição: limpar lá não é perigo.
|
|
96
|
+
// (Mas nunca o que mora dentro da sua pasta pessoal.)
|
|
97
|
+
if (!dentro(os.homedir(), alvoAbs) && TEMPORARIAS.some((t) => dentro(t, alvoAbs))) return null;
|
|
98
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) return `apaga algo fora da pasta do projeto (${a})`;
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Palavras que vêm ANTES do comando de verdade e não mudam o que ele faz.
|
|
103
|
+
const PREFIXOS = new Set(['env', 'nohup', 'time', 'command', 'exec', 'nice', 'xargs']);
|
|
104
|
+
|
|
105
|
+
function analisarTrecho(palavras, estado) {
|
|
106
|
+
let p = palavras.slice();
|
|
107
|
+
// `VAR=x cmd`, `env -i cmd`, `xargs -0 rm`: pula até o comando.
|
|
108
|
+
while (p.length && (PREFIXOS.has(p[0]) || /^[A-Za-z_]\w*=/.test(p[0]) || (estado.pulandoFlags && /^-/.test(p[0])))) {
|
|
109
|
+
estado.pulandoFlags = PREFIXOS.has(p[0]) && p[0] !== 'command';
|
|
110
|
+
p.shift();
|
|
111
|
+
}
|
|
112
|
+
estado.pulandoFlags = false;
|
|
113
|
+
if (!p.length) return null;
|
|
114
|
+
const cmd = path.basename(p[0]);
|
|
115
|
+
const args = p.slice(1);
|
|
116
|
+
const flags = args.filter((x) => /^-/.test(x));
|
|
117
|
+
const alvos = args.filter((x) => !/^-/.test(x));
|
|
118
|
+
const tem = (re) => flags.some((f) => re.test(f));
|
|
119
|
+
|
|
120
|
+
if (cmd === 'sudo' || cmd === 'doas') return 'usa sudo: mexe como administrador no computador inteiro';
|
|
121
|
+
if (cmd === 'cd') { estado.cwd = path.resolve(estado.cwd, expandir(alvos[0] || os.homedir())); return null; }
|
|
122
|
+
if ((cmd === 'sh' || cmd === 'bash' || cmd === 'zsh') && args[0] === '-c' && args[1]) {
|
|
123
|
+
return analisarComando(args[1], estado.cwd, estado.projeto);
|
|
124
|
+
}
|
|
125
|
+
if (cmd === 'rm' || cmd === 'rmdir' || cmd === 'trash') {
|
|
126
|
+
const recursivo = cmd === 'rmdir' || tem(/^-[a-zA-Z]*[rR]|^--recursive$/);
|
|
127
|
+
for (const a of alvos) {
|
|
128
|
+
const r = alvoRuim(a, estado.cwd, estado.projeto, { recursivo });
|
|
129
|
+
if (r) return r;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (cmd === 'find' && args.includes('-delete')) {
|
|
134
|
+
for (const a of alvos.slice(0, 1)) {
|
|
135
|
+
const r = alvoRuim(a, estado.cwd, estado.projeto, { recursivo: true, raizOk: true });
|
|
136
|
+
if (r) return r.replace(/^apaga/, 'apaga com find');
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
if (cmd === 'git') {
|
|
141
|
+
const sub = alvos[0];
|
|
142
|
+
if (sub === 'push' && (tem(/^--force$|^-[a-zA-Z]*f/) || alvos.some((a) => /^\+/.test(a)))) {
|
|
143
|
+
return 'git push --force: reescreve o histórico no GitHub, e o que estava lá some';
|
|
144
|
+
}
|
|
145
|
+
if (sub === 'reset' && flags.includes('--hard')) return 'git reset --hard: joga fora o que não foi salvo em commit';
|
|
146
|
+
if (sub === 'clean' && tem(/^-[a-zA-Z]*f/) && !tem(/^-[a-zA-Z]*n|^--dry-run$/)) {
|
|
147
|
+
return 'git clean: apaga arquivos que o git não guardou';
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (cmd === 'dd') return 'dd: escreve direto no disco';
|
|
152
|
+
if (/^mkfs(\.|$)/.test(cmd) || cmd === 'newfs' || cmd === 'fdisk') return 'formata um disco';
|
|
153
|
+
if (cmd === 'diskutil' && /^(erase|zero|secureErase|partitionDisk|reformat)/i.test(alvos[0] || '')) return 'diskutil: apaga um disco';
|
|
154
|
+
if ((cmd === 'chmod' || cmd === 'chown') && tem(/^-[a-zA-Z]*R/)) {
|
|
155
|
+
for (const a of alvos) {
|
|
156
|
+
const e = path.resolve(estado.cwd, expandir(a));
|
|
157
|
+
if (e === '/' || e === os.homedir()) return `${cmd} -R em ${a}: muda a permissão de tudo`;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
if (cmd === 'kill' && args.includes('-1')) return 'kill -1: encerra todos os seus programas';
|
|
162
|
+
if (['shutdown', 'reboot', 'halt', 'poweroff'].includes(cmd)) return 'desliga ou reinicia o computador';
|
|
163
|
+
if (['npm', 'pnpm', 'yarn'].includes(cmd) && alvos[0] === 'publish' && !flags.includes('--dry-run')) return `${cmd} publish: publica o pacote para o mundo inteiro`;
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function analisarComando(comando, cwd, projeto) {
|
|
168
|
+
const s = String(comando || '');
|
|
169
|
+
// Baixar e rodar na hora: o que roda é o que o site mandar naquele segundo.
|
|
170
|
+
if (/\b(curl|wget)\b[^;&]*\|\s*(sudo\s+)?(env\s+)?(ba|z|da|k)?sh\b/.test(s)) {
|
|
171
|
+
return 'baixa um script da internet e roda sem ler';
|
|
172
|
+
}
|
|
173
|
+
const estado = { cwd, projeto, pulandoFlags: false };
|
|
174
|
+
for (const t of trechos(s)) {
|
|
175
|
+
const r = analisarTrecho(t.palavras, estado);
|
|
176
|
+
if (r) return r;
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Esta ação é perigosa?
|
|
183
|
+
* @returns {{perigoso: boolean, motivo: string}}
|
|
184
|
+
*/
|
|
185
|
+
function classificar(nome, args, { projectDir } = {}) {
|
|
186
|
+
const a = args || {};
|
|
187
|
+
const projeto = path.resolve(projectDir || process.cwd());
|
|
188
|
+
let motivo = null;
|
|
189
|
+
if (COMANDOS.has(nome)) {
|
|
190
|
+
const cwd = a.cwd ? path.resolve(projeto, expandir(a.cwd)) : projeto;
|
|
191
|
+
motivo = analisarComando(a.command, cwd, projeto);
|
|
192
|
+
} else if (ESCRITAS.has(nome) && a.path) {
|
|
193
|
+
const alvo = path.resolve(projeto, expandir(a.path));
|
|
194
|
+
const rel = path.relative(projeto, alvo);
|
|
195
|
+
if (rel.startsWith('..') || path.isAbsolute(rel) || /^[a-zA-Z]:[\\/]/.test(String(a.path))) {
|
|
196
|
+
motivo = `mexe num arquivo fora da pasta do projeto (${a.path})`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { perigoso: !!motivo, motivo: motivo || '' };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function novo() { return { liberados: new Set(), negados: new Set() }; }
|
|
203
|
+
|
|
204
|
+
/* Uma pergunta por vez. Subagentes rodam em paralelo (Promise.all) e dois
|
|
205
|
+
pedidos ao mesmo tempo no mesmo terminal se atropelariam. */
|
|
206
|
+
let fila = Promise.resolve();
|
|
207
|
+
function emFila(fn) { const p = fila.then(fn, fn); fila = p.catch(() => {}); return p; }
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* O portão. Devolve {ok:true} ou {ok:false, error, motivo}.
|
|
211
|
+
*
|
|
212
|
+
* `perguntar(frase, {tipo, motivo, primeira})` → 'agora' | 'sessao' | 'nao',
|
|
213
|
+
* o mesmo contrato do portão do desktop (lib/permissao.js).
|
|
214
|
+
*/
|
|
215
|
+
async function portao(estado, { modo, nome, args, projectDir, subagent = false, perguntar = null }) {
|
|
216
|
+
const m = normalizarModo(modo);
|
|
217
|
+
if (m === 'livre') return { ok: true };
|
|
218
|
+
const { perigoso, motivo: motivoPerigo } = classificar(nome, args, { projectDir });
|
|
219
|
+
const tipo = perigoso ? 'perigo' : (m === 'perguntar' && COMANDOS.has(nome)) ? 'comando'
|
|
220
|
+
: (m === 'perguntar' && ESCRITAS.has(nome)) ? 'arquivo' : null;
|
|
221
|
+
if (!tipo) return { ok: true };
|
|
222
|
+
|
|
223
|
+
const motivo = perigoso ? motivoPerigo
|
|
224
|
+
: tipo === 'comando' ? `rodar: ${String((args || {}).command || '').slice(0, 80)}`
|
|
225
|
+
: `escrever ${String((args || {}).path || 'um arquivo')}`;
|
|
226
|
+
const recusa = (texto) => ({ ok: false, motivo, error: texto });
|
|
227
|
+
|
|
228
|
+
// Escrever fora da pasta o tools.js já recusa sozinho: perguntar seria
|
|
229
|
+
// pedir um "sim" que não serve para nada.
|
|
230
|
+
if (perigoso && ESCRITAS.has(nome)) {
|
|
231
|
+
return recusa(`Recusado: ${motivo}. O PrimoCode só escreve dentro da pasta do projeto — use /dir se a pessoa quiser trabalhar em outra pasta.`);
|
|
232
|
+
}
|
|
233
|
+
if (subagent) {
|
|
234
|
+
return recusa(`Recusado: ${motivo}. Subagente não tem como pedir autorização — `
|
|
235
|
+
+ 'deixe esse passo para o agente principal e diga no seu resumo o que falta fazer.');
|
|
236
|
+
}
|
|
237
|
+
const chave = `${nome}:${JSON.stringify(args || {})}`;
|
|
238
|
+
const negado = `A pessoa não autorizou: ${motivo}. Siga por outro caminho ou pergunte.`;
|
|
239
|
+
if (estado.negados.has(chave)) return recusa(negado);
|
|
240
|
+
// "A sessão inteira" libera o TIPO de pergunta: no perigoso, aquele motivo
|
|
241
|
+
// (um push --force autorizado não autoriza um rm -rf ~); no modo
|
|
242
|
+
// perguntar, os comandos ou as escritas comuns.
|
|
243
|
+
const liberacao = perigoso ? `perigo:${motivo}` : tipo;
|
|
244
|
+
if (estado.liberados.has(liberacao)) return { ok: true };
|
|
245
|
+
if (typeof perguntar !== 'function') {
|
|
246
|
+
return recusa(`Recusado: ${motivo}. Isso precisa de autorização e não há ninguém para autorizar neste modo.`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const resposta = await emFila(() => perguntar(motivo, { tipo, motivo, primeira: false }));
|
|
250
|
+
if (resposta === 'sessao') { estado.liberados.add(liberacao); return { ok: true }; }
|
|
251
|
+
if (resposta === 'agora') return { ok: true };
|
|
252
|
+
estado.negados.add(chave);
|
|
253
|
+
return recusa(negado);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = { classificar, portao, novo, normalizarModo, trechos, MODOS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "9.8.0-beta.
|
|
3
|
+
"version": "9.8.0-beta.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": {
|
|
@@ -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/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/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/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/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"
|
|
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/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/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/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"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
|
@@ -30,8 +30,10 @@
|
|
|
30
30
|
"app/",
|
|
31
31
|
"studio/",
|
|
32
32
|
"skills/",
|
|
33
|
+
"kits/",
|
|
33
34
|
"voz/",
|
|
34
35
|
"README.md",
|
|
36
|
+
"comandos/",
|
|
35
37
|
"!**/__pycache__",
|
|
36
38
|
"!**/*.pyc",
|
|
37
39
|
"!**/*.pyo",
|