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/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 motor = require('./local/motor.js');
210
- return motor.ativo() ? motor : null;
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 };