primocode 9.8.0-beta.2 → 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 +20 -5
- package/bin/primocode.js +322 -53
- 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/checkpoint.js +134 -0
- package/lib/comandos.js +104 -0
- package/lib/config.js +18 -4
- package/lib/kits.js +81 -0
- package/lib/mencoes.js +98 -0
- package/lib/perigo.js +256 -0
- package/package.json +4 -2
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/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/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",
|