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/act.js
CHANGED
|
@@ -21,7 +21,9 @@ const repeticao = require('./repeticao');
|
|
|
21
21
|
const diff = require('./diff');
|
|
22
22
|
const registro = require('./registro');
|
|
23
23
|
const permissao = require('./permissao');
|
|
24
|
+
const perigo = require('./perigo');
|
|
24
25
|
const skills = require('./skills');
|
|
26
|
+
const checkpoint = require('./checkpoint');
|
|
25
27
|
|
|
26
28
|
// 40, não 15: com o freio de repetição (repeticao.js) e o de rodada parada,
|
|
27
29
|
// volta a mais não é volta em círculo — é trabalho. Quinze acabavam no meio
|
|
@@ -475,6 +477,11 @@ function mapaDoProjeto(dir, limite = 80) {
|
|
|
475
477
|
|
|
476
478
|
function buildMemoryContext(projectDir, projectName) {
|
|
477
479
|
const parts = [];
|
|
480
|
+
// As instruções vêm primeiro: são regra de quem manda no projeto, e a
|
|
481
|
+
// memória é o que o agente anotou. Entram aqui (e não no bin) para valer
|
|
482
|
+
// igual no terminal, no modo voz e em cada passo de um plano.
|
|
483
|
+
const instrucoes = require('./mencoes.js').instrucoesDoProjeto(projectDir);
|
|
484
|
+
if (instrucoes) parts.push(instrucoes);
|
|
478
485
|
const geral = cfg.readGeneralMemory();
|
|
479
486
|
if (geral.trim()) parts.push(`[memory-geral.md]\n${geral}`);
|
|
480
487
|
const projeto = cfg.readProjectMemory(projectDir);
|
|
@@ -493,6 +500,10 @@ async function runAct(userPrompt, opts) {
|
|
|
493
500
|
quem sabe lembrar é a SESSÃO — um subagente não pode começar do
|
|
494
501
|
zero e pedir de novo o que já foi autorizado. */
|
|
495
502
|
autorizar = null, controle = null,
|
|
503
|
+
/* O modo do /permissoes (auto · perguntar · livre). Vem da config pela
|
|
504
|
+
CLI e desce para os subagentes pelo ctx: eles recusam o perigoso,
|
|
505
|
+
mas só sabem que é "livre" se alguém disser. */
|
|
506
|
+
permissoes = 'auto',
|
|
496
507
|
/* O cliente HTTP, injetável. NÃO é mais um comutador de motor — o
|
|
497
508
|
PrimoCode tem um motor só, o do servidor, e não há provedor para o
|
|
498
509
|
usuário escolher. Continua um parâmetro porque é por aqui que o
|
|
@@ -962,7 +973,7 @@ async function runAct(userPrompt, opts) {
|
|
|
962
973
|
fala.paralelo(juntos.length, profundidade);
|
|
963
974
|
const rs = await Promise.all(juntos.map((tc) =>
|
|
964
975
|
runSubagent(tc.arguments || {}, {
|
|
965
|
-
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor,
|
|
976
|
+
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
|
|
966
977
|
}).catch((e) => `Subagente falhou: ${e && e.message}`)));
|
|
967
978
|
juntos.forEach((tc, i) => resultadosSpawn.set(tc.id, rs[i]));
|
|
968
979
|
}
|
|
@@ -1005,7 +1016,7 @@ async function runAct(userPrompt, opts) {
|
|
|
1005
1016
|
const r = resultadosSpawn.has(tc.id)
|
|
1006
1017
|
? resultadosSpawn.get(tc.id)
|
|
1007
1018
|
: await runSubagent(tc.arguments || {}, {
|
|
1008
|
-
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor,
|
|
1019
|
+
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
|
|
1009
1020
|
});
|
|
1010
1021
|
repeticao.executou(jaChamado, tc.name, tc.arguments);
|
|
1011
1022
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: r });
|
|
@@ -1018,7 +1029,7 @@ async function runAct(userPrompt, opts) {
|
|
|
1018
1029
|
algoRodou = true;
|
|
1019
1030
|
fala.acao(tc.name, tc.arguments, profundidade);
|
|
1020
1031
|
const r = await rodarDiretor(tc.arguments || {}, {
|
|
1021
|
-
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor,
|
|
1032
|
+
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
|
|
1022
1033
|
});
|
|
1023
1034
|
logResult(tc.name, r, profundidade);
|
|
1024
1035
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
|
|
@@ -1036,7 +1047,7 @@ async function runAct(userPrompt, opts) {
|
|
|
1036
1047
|
garantirPasta();
|
|
1037
1048
|
fala.acao(tc.name, tc.arguments, profundidade);
|
|
1038
1049
|
const r = await rodarPipeline(tc.arguments || {}, {
|
|
1039
|
-
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, livro, autorizar, controle: portaoControle,
|
|
1050
|
+
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes, livro, autorizar, controle: portaoControle,
|
|
1040
1051
|
});
|
|
1041
1052
|
logResult(tc.name, r, profundidade);
|
|
1042
1053
|
repeticao.executou(jaChamado, tc.name, tc.arguments);
|
|
@@ -1087,9 +1098,30 @@ async function runAct(userPrompt, opts) {
|
|
|
1087
1098
|
}
|
|
1088
1099
|
}
|
|
1089
1100
|
|
|
1101
|
+
/* O PORTÃO do que não tem volta (lib/perigo.js). No mesmo lugar do
|
|
1102
|
+
portão do desktop e pelo mesmo motivo: antes, não depois. A
|
|
1103
|
+
resposta mora no mesmo objeto da sessão — quem lembra é ela. */
|
|
1104
|
+
if (!portaoControle.perigo) portaoControle.perigo = perigo.novo();
|
|
1105
|
+
const risco = await perigo.portao(portaoControle.perigo, {
|
|
1106
|
+
modo: permissoes, nome: tc.name, args: tc.arguments,
|
|
1107
|
+
projectDir: pastaAtual, subagent, perguntar: autorizar,
|
|
1108
|
+
});
|
|
1109
|
+
if (!risco.ok) {
|
|
1110
|
+
fala.acao(tc.name, tc.arguments, profundidade);
|
|
1111
|
+
console.log(fala.resultado(profundidade) + c.warn(`não autorizado — ${risco.motivo}`));
|
|
1112
|
+
registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: risco.error });
|
|
1113
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: risco.error }) });
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1090
1117
|
algoRodou = true;
|
|
1091
1118
|
rodouTool = true;
|
|
1092
1119
|
fala.acao(tc.name, tc.arguments, profundidade);
|
|
1120
|
+
/* Antes de mexer no arquivo, guarda o original — é o que o /desfazer
|
|
1121
|
+
devolve. Caminho inválido não é problema nosso: a tool recusa. */
|
|
1122
|
+
if (checkpoint.MUDAM_ARQUIVO.has(tc.name)) {
|
|
1123
|
+
try { checkpoint.antesDeMudar(tools.resolveInProject(tc.arguments && tc.arguments.path, pastaAtual), pastaAtual); } catch {}
|
|
1124
|
+
}
|
|
1093
1125
|
const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
|
|
1094
1126
|
logResult(tc.name, result, profundidade);
|
|
1095
1127
|
repeticao.executou(jaChamado, tc.name, tc.arguments);
|
|
@@ -1232,7 +1264,7 @@ async function runAct(userPrompt, opts) {
|
|
|
1232
1264
|
}
|
|
1233
1265
|
|
|
1234
1266
|
async function runSubagent(args, ctx) {
|
|
1235
|
-
const { server, model, token, projectDir, projectName, profundidade, motor } = ctx;
|
|
1267
|
+
const { server, model, token, projectDir, projectName, profundidade, motor, permissoes } = ctx;
|
|
1236
1268
|
const descricao = args.description || 'sub-tarefa';
|
|
1237
1269
|
|
|
1238
1270
|
if (profundidade + 1 >= MAX_PROFUNDIDADE) {
|
|
@@ -1251,6 +1283,7 @@ async function runSubagent(args, ctx) {
|
|
|
1251
1283
|
profundidade: profundidade + 1,
|
|
1252
1284
|
subagent: true,
|
|
1253
1285
|
motor,
|
|
1286
|
+
permissoes,
|
|
1254
1287
|
autorizar: ctx.autorizar,
|
|
1255
1288
|
controle: ctx.controle, // o mesmo objeto: a resposta vale para todos
|
|
1256
1289
|
});
|
|
@@ -1294,7 +1327,7 @@ async function runSubagent(args, ctx) {
|
|
|
1294
1327
|
* inventar — e inventar é onde ele erra.
|
|
1295
1328
|
*/
|
|
1296
1329
|
async function rodarDiretor(args, ctx) {
|
|
1297
|
-
const { server, model, token, projectDir, projectName, profundidade, motor } = ctx;
|
|
1330
|
+
const { server, model, token, projectDir, projectName, profundidade, motor, permissoes } = ctx;
|
|
1298
1331
|
if (profundidade > 0) {
|
|
1299
1332
|
return { ok: false, error: 'Recusado: o diretor não roda dentro de um subagente. '
|
|
1300
1333
|
+ 'Produza com o plano que você já recebeu.' };
|
|
@@ -1305,7 +1338,7 @@ async function rodarDiretor(args, ctx) {
|
|
|
1305
1338
|
fala.agente('Diretor', rotulo, profundidade);
|
|
1306
1339
|
const r = await runAct(prompt, {
|
|
1307
1340
|
server, model, token, projectDir, projectName,
|
|
1308
|
-
history: [], profundidade: profundidade + 1, subagent: true, motor,
|
|
1341
|
+
history: [], profundidade: profundidade + 1, subagent: true, motor, permissoes,
|
|
1309
1342
|
});
|
|
1310
1343
|
return (r && r.assistantMessage && r.assistantMessage.content) || '';
|
|
1311
1344
|
};
|
|
@@ -1338,7 +1371,7 @@ async function rodarDiretor(args, ctx) {
|
|
|
1338
1371
|
}
|
|
1339
1372
|
|
|
1340
1373
|
async function rodarPipeline(args, ctx) {
|
|
1341
|
-
const { server, model, token, projectDir, projectName, profundidade, motor } = ctx;
|
|
1374
|
+
const { server, model, token, projectDir, projectName, profundidade, motor, permissoes } = ctx;
|
|
1342
1375
|
if (profundidade > 0) {
|
|
1343
1376
|
return { ok: false, error: 'Recusado: o pipeline não roda dentro de um subagente — '
|
|
1344
1377
|
+ 'os papéis dele já são subagentes. Faça a parte que te cabe.' };
|
|
@@ -1361,7 +1394,7 @@ async function rodarPipeline(args, ctx) {
|
|
|
1361
1394
|
fala.agente('Agente', rotulo, profundidade);
|
|
1362
1395
|
const r = await runAct(prompt, {
|
|
1363
1396
|
server, model, token, projectDir: pasta, projectName,
|
|
1364
|
-
history: [], profundidade: profundidade + 1, subagent: true, motor,
|
|
1397
|
+
history: [], profundidade: profundidade + 1, subagent: true, motor, permissoes,
|
|
1365
1398
|
});
|
|
1366
1399
|
return (r && r.assistantMessage && r.assistantMessage.content) || '';
|
|
1367
1400
|
};
|
|
@@ -1376,5 +1409,5 @@ async function rodarPipeline(args, ctx) {
|
|
|
1376
1409
|
});
|
|
1377
1410
|
}
|
|
1378
1411
|
|
|
1379
|
-
module.exports = { runAct, interromper, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
|
|
1412
|
+
module.exports = { runAct, buildMemoryContext, interromper, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
|
|
1380
1413
|
anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
|
package/lib/api.js
CHANGED
|
@@ -206,8 +206,11 @@ function requestStream(serverUrl, endpoint, payload, onEvent, token, sinal) {
|
|
|
206
206
|
O resto (login, transcrição, conectores) continua no servidor. */
|
|
207
207
|
function local() {
|
|
208
208
|
try {
|
|
209
|
-
const
|
|
210
|
-
|
|
209
|
+
const atual = require('./local/provedor.js').atual();
|
|
210
|
+
if (!atual) return null;
|
|
211
|
+
// Claude Code e Codex: a ferramenta de fora é o cérebro.
|
|
212
|
+
if (atual.cli) return require('./local/motor-cli.js').criar(atual.id);
|
|
213
|
+
return require('./local/motor.js');
|
|
211
214
|
} catch { return null; }
|
|
212
215
|
}
|
|
213
216
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* checkpoint.js — toda mudança do agente num arquivo tem volta.
|
|
3
|
+
*
|
|
4
|
+
* Quem usa o PrimoCode quase nunca sabe git. Uma edição ruim ("ele apagou
|
|
5
|
+
* metade do meu site") não pode ser o fim da história: antes de o agente
|
|
6
|
+
* escrever, editar ou apagar um arquivo, guardamos o conteúdo ORIGINAL dele,
|
|
7
|
+
* uma vez por pedido. O `/desfazer` devolve tudo como estava.
|
|
8
|
+
*
|
|
9
|
+
* Por que o checkpoint é do PEDIDO e não do passo: um pedido vira vários
|
|
10
|
+
* runAct (passos do plano, continuações, subagentes). Para a pessoa, "desfaz"
|
|
11
|
+
* quer dizer "desfaz o que eu pedi", não "desfaz o terceiro passo interno".
|
|
12
|
+
* Por isso o CLI chama `iniciar(pedido)` antes do laço de passos, e o loop do
|
|
13
|
+
* agente só avisa `antesDeMudar(arquivo, pasta)` — o estado fica aqui, no
|
|
14
|
+
* módulo, e o subagente (mesmo processo) cai no mesmo checkpoint.
|
|
15
|
+
*
|
|
16
|
+
* Onde fica: ~/.primocode/checkpoints/<projeto>-<hash>/<id>/
|
|
17
|
+
* manifest.json o pedido, a hora e cada arquivo (existia ou não)
|
|
18
|
+
* 0, 1, 2… a cópia original de cada arquivo que existia
|
|
19
|
+
* Só os últimos MAXIMO por projeto: é rede de segurança, não arquivo morto.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const os = require('os');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const crypto = require('crypto');
|
|
28
|
+
|
|
29
|
+
const MAXIMO = 20;
|
|
30
|
+
/** As tools de arquivo que mudam o disco. run_shell fica de fora: não dá para
|
|
31
|
+
saber, antes de rodar, que arquivos um comando vai tocar. */
|
|
32
|
+
const MUDAM_ARQUIVO = new Set(['write_file', 'edit_file', 'multi_edit']);
|
|
33
|
+
|
|
34
|
+
let atual = null; // o pedido em andamento: { pedido, dir, manifest }
|
|
35
|
+
|
|
36
|
+
/* os.homedir() lido na hora, não no require: o teste troca o HOME. */
|
|
37
|
+
function raiz() { return path.join(os.homedir(), '.primocode', 'checkpoints'); }
|
|
38
|
+
|
|
39
|
+
/** Uma pasta por projeto: o nome para a pessoa reconhecer, o hash para dois
|
|
40
|
+
projetos "site" em lugares diferentes não se misturarem. */
|
|
41
|
+
function pastaDoProjeto(projectDir) {
|
|
42
|
+
const abs = path.resolve(projectDir);
|
|
43
|
+
const nome = path.basename(abs).replace(/[^\w.-]+/g, '-').slice(0, 40) || 'raiz';
|
|
44
|
+
const hash = crypto.createHash('sha1').update(abs).digest('hex').slice(0, 8);
|
|
45
|
+
return path.join(raiz(), `${nome}-${hash}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Começa o checkpoint de um pedido. Nada vai ao disco ainda: pedido que não
|
|
49
|
+
mexe em arquivo (uma pergunta, uma pesquisa) não deixa checkpoint vazio. */
|
|
50
|
+
function iniciar(pedido) {
|
|
51
|
+
atual = { pedido: String(pedido || '').replace(/\s+/g, ' ').trim(), dir: null, manifest: null };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function encerrar() { atual = null; }
|
|
55
|
+
|
|
56
|
+
function gravarManifest() {
|
|
57
|
+
fs.writeFileSync(path.join(atual.dir, 'manifest.json'), JSON.stringify(atual.manifest, null, 2));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Chamado pelo loop ANTES de uma tool de arquivo rodar. Guarda o original só
|
|
61
|
+
na primeira vez que o arquivo é tocado no pedido — a segunda edição do
|
|
62
|
+
mesmo arquivo não pode sobrescrever o original com a versão já editada. */
|
|
63
|
+
function antesDeMudar(caminhoAbs, projectDir) {
|
|
64
|
+
if (!atual || !caminhoAbs) return;
|
|
65
|
+
try {
|
|
66
|
+
if (!atual.dir) {
|
|
67
|
+
const base = pastaDoProjeto(projectDir);
|
|
68
|
+
fs.mkdirSync(base, { recursive: true });
|
|
69
|
+
// Sempre maior que o anterior: a ordem dos ids É a ordem dos pedidos.
|
|
70
|
+
const ultimo = fs.readdirSync(base).filter((n) => /^\d+$/.test(n)).reduce((m, n) => Math.max(m, Number(n)), 0);
|
|
71
|
+
const id = String(Math.max(Date.now(), ultimo + 1));
|
|
72
|
+
atual.dir = path.join(base, id);
|
|
73
|
+
fs.mkdirSync(atual.dir);
|
|
74
|
+
atual.manifest = { pedido: atual.pedido, quando: new Date().toISOString(),
|
|
75
|
+
projectDir: path.resolve(projectDir), arquivos: [] };
|
|
76
|
+
podar(base);
|
|
77
|
+
}
|
|
78
|
+
const abs = path.resolve(caminhoAbs);
|
|
79
|
+
if (atual.manifest.arquivos.some((a) => a.caminho === abs)) return;
|
|
80
|
+
const existia = fs.existsSync(abs) && fs.statSync(abs).isFile();
|
|
81
|
+
const item = { caminho: abs, existia };
|
|
82
|
+
if (existia) {
|
|
83
|
+
item.copia = String(atual.manifest.arquivos.length);
|
|
84
|
+
fs.copyFileSync(abs, path.join(atual.dir, item.copia));
|
|
85
|
+
}
|
|
86
|
+
atual.manifest.arquivos.push(item);
|
|
87
|
+
gravarManifest();
|
|
88
|
+
} catch { /* checkpoint nunca derruba o trabalho do agente */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function podar(base) {
|
|
92
|
+
const ids = fs.readdirSync(base).filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(b) - Number(a));
|
|
93
|
+
for (const velho of ids.slice(MAXIMO)) fs.rmSync(path.join(base, velho), { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Os checkpoints do projeto, o mais novo primeiro. */
|
|
97
|
+
function lista(projectDir) {
|
|
98
|
+
const base = pastaDoProjeto(projectDir);
|
|
99
|
+
let ids = [];
|
|
100
|
+
try { ids = fs.readdirSync(base).filter((n) => /^\d+$/.test(n)); } catch { return []; }
|
|
101
|
+
return ids.sort((a, b) => Number(b) - Number(a)).map((id) => {
|
|
102
|
+
try {
|
|
103
|
+
const m = JSON.parse(fs.readFileSync(path.join(base, id, 'manifest.json'), 'utf8'));
|
|
104
|
+
return { id, dir: path.join(base, id), ...m };
|
|
105
|
+
} catch { return null; }
|
|
106
|
+
}).filter(Boolean);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Desfaz os N pedidos mais recentes, do mais novo para o mais velho — assim,
|
|
110
|
+
se dois pedidos mexeram no mesmo arquivo, o que sobra é o original do mais
|
|
111
|
+
antigo. Cada checkpoint desfeito é apagado: o próximo /desfazer volta mais
|
|
112
|
+
um pedido, em vez de repetir o mesmo. */
|
|
113
|
+
function desfazer(projectDir, n = 1) {
|
|
114
|
+
const feitos = [];
|
|
115
|
+
for (const cp of lista(projectDir).slice(0, Math.max(1, n))) {
|
|
116
|
+
for (const a of cp.arquivos) {
|
|
117
|
+
try {
|
|
118
|
+
if (a.existia) {
|
|
119
|
+
fs.mkdirSync(path.dirname(a.caminho), { recursive: true });
|
|
120
|
+
fs.copyFileSync(path.join(cp.dir, a.copia), a.caminho);
|
|
121
|
+
feitos.push({ caminho: a.caminho, acao: 'restaurado' });
|
|
122
|
+
} else {
|
|
123
|
+
fs.rmSync(a.caminho, { force: true });
|
|
124
|
+
feitos.push({ caminho: a.caminho, acao: 'apagado' });
|
|
125
|
+
}
|
|
126
|
+
} catch (e) { feitos.push({ caminho: a.caminho, acao: 'falhou', erro: e.message }); }
|
|
127
|
+
}
|
|
128
|
+
fs.rmSync(cp.dir, { recursive: true, force: true });
|
|
129
|
+
if (atual && atual.dir === cp.dir) atual = null;
|
|
130
|
+
}
|
|
131
|
+
return feitos;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { MAXIMO, MUDAM_ARQUIVO, iniciar, encerrar, antesDeMudar, lista, desfazer, pastaDoProjeto };
|