primocode 7.0.3 → 7.2.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.
package/lib/act.js CHANGED
@@ -1,153 +1,163 @@
1
- // Loop do agente autônomo — streaming via /api/act/stream
1
+ // Loop do agente autônomo — streaming via /api/act/stream.
2
+ //
3
+ // O contrato com o servidor é uma conversa completa (`messages`, estilo OpenAI).
4
+ // Cada volta do loop: o servidor devolve tool_calls -> executamos AQUI, com await,
5
+ // e devolvemos o resultado na próxima volta. Sem isso o modelo fica cego e repete
6
+ // a mesma tool até estourar o limite.
2
7
 
3
8
  'use strict';
4
9
 
5
- const fs = require('fs');
6
- const path = require('path');
7
10
  const { c, actionHeader, Spinner } = require('./ui');
8
11
  const tools = require('./tools');
9
12
  const api = require('./api');
10
- const { ROOT_DIR } = require('./config');
13
+ const cfg = require('./config');
11
14
 
12
15
  const MAX_ITERATIONS = 15;
13
16
 
14
- function makeLogger() {
15
- return {
16
- event(kind, arg, result) {
17
- if (kind === 'run_shell') return;
18
- if (kind === 'run_shell_done') {
19
- if (result) { const s = result.ok ? c.ok(' exit 0') : c.err(` exit ${result.code}${result.killed?' timeout':''}`); process.stdout.write('\n'+s+'\n'); }
20
- return;
21
- }
22
- const prefix = c.muted(' ⎿ ');
23
- if (kind === 'write_file' && result && result.ok) { console.log(prefix + c.muted(`${result.created?'created':'updated'} · ${tools.fmtBytes(result.bytes)}`)); return; }
24
- if (kind === 'read_file' && result && result.ok) { console.log(prefix + c.muted(`${tools.fmtBytes(result.bytes||0)}${result.truncated?' (truncated)':''}`)); return; }
25
- if (kind === 'list_dir' && result && result.ok) { const lines = result.tree.split('\n').slice(0,8); for (const l of lines) console.log(prefix + c.muted(l)); const total = result.tree.split('\n').length; if (total>8) console.log(prefix + c.muted(`… +${total-8}`)); return; }
26
- if (kind === 'open_in_browser' && result && result.ok) { console.log(prefix + c.muted(`${result.opener||'browser'}`)); return; }
27
- if (kind === 'run_in_new_terminal' && result && result.ok) { console.log(prefix + c.muted(`${result.opener}`)); return; }
28
- if (kind === 'finish') { console.log(prefix + c.muted(arg || 'ok')); return; }
29
- if (result && result.ok === false) console.log(prefix + c.err(result.error || 'failed'));
30
- },
31
- };
32
- }
33
-
34
- function readMemory(projectDir, projectName) {
35
- let context = '';
36
-
37
- // Ler you.md (memória geral)
38
- const youMdPath = path.join(ROOT_DIR, 'you.md');
39
- if (fs.existsSync(youMdPath)) {
40
- try {
41
- const youContent = fs.readFileSync(youMdPath, 'utf-8');
42
- context += `[Memória Geral (you.md)]\n${youContent.slice(0, 2000)}\n\n`;
43
- } catch {}
44
- }
45
-
46
- // Ler memory.md do projeto
47
- if (projectDir && projectName) {
48
- const memoryPath = path.join(projectDir, 'memory.md');
49
- if (fs.existsSync(memoryPath)) {
50
- try {
51
- const memContent = fs.readFileSync(memoryPath, 'utf-8');
52
- context += `[Memória do Projeto: ${projectName}]\n${memContent.slice(0, 2000)}\n\n`;
53
- } catch {}
17
+ function logResult(kind, result) {
18
+ const prefix = c.muted(' ⎿ ');
19
+ if (!result) return;
20
+ if (result.ok === false) { console.log(prefix + c.err(result.error || `exit ${result.code ?? 'erro'}`)); return; }
21
+
22
+ switch (kind) {
23
+ case 'write_file':
24
+ console.log(prefix + c.muted(`${result.created ? 'created' : 'updated'} · ${tools.fmtBytes(result.bytes)}`));
25
+ return;
26
+ case 'read_file':
27
+ console.log(prefix + c.muted(`${tools.fmtBytes(result.bytes || 0)}${result.truncated ? ' (truncated)' : ''}`));
28
+ return;
29
+ case 'list_dir': {
30
+ const lines = (result.tree || '').split('\n').filter(Boolean);
31
+ for (const l of lines.slice(0, 8)) console.log(prefix + c.muted(l));
32
+ if (lines.length > 8) console.log(prefix + c.muted(`… +${lines.length - 8}`));
33
+ return;
54
34
  }
35
+ case 'run_shell':
36
+ console.log(prefix + c.ok('exit 0'));
37
+ return;
38
+ case 'open_in_browser':
39
+ case 'run_in_new_terminal':
40
+ console.log(prefix + c.muted(result.opener || 'ok'));
41
+ return;
55
42
  }
43
+ }
56
44
 
57
- return context;
45
+ // O que mandamos de volta ao modelo. Cortamos saídas gigantes para não
46
+ // estourar a janela de contexto na iteração seguinte.
47
+ function summarizeForModel(result) {
48
+ const json = JSON.stringify(result);
49
+ return json.length > 4000 ? json.slice(0, 4000) + '…(truncado)' : json;
58
50
  }
59
51
 
60
- function updateMemory(projectDir, projectName, entry) {
61
- if (!projectDir || !projectName) return;
62
- const memoryPath = path.join(projectDir, 'memory.md');
63
- try {
64
- let content = '';
65
- if (fs.existsSync(memoryPath)) {
66
- content = fs.readFileSync(memoryPath, 'utf-8');
67
- }
68
- const timestamp = new Date().toISOString().split('T')[0];
69
- content += `\n## ${timestamp}\n${entry}\n`;
70
- fs.writeFileSync(memoryPath, content, 'utf-8');
71
- } catch {}
52
+ function buildMemoryContext(projectDir, projectName) {
53
+ const parts = [];
54
+ const geral = cfg.readGeneralMemory();
55
+ if (geral.trim()) parts.push(`[memory-geral.md]\n${geral}`);
56
+ const projeto = cfg.readProjectMemory(projectDir);
57
+ if (projeto.trim()) parts.push(`[${projectName}/memory.md]\n${projeto}`);
58
+ return parts.join('\n\n');
72
59
  }
73
60
 
74
61
  async function runAct(userPrompt, opts) {
75
- const { server, model, history, projectDir, projectName } = opts || {};
76
- const logger = makeLogger();
77
-
78
- // Ler memórias
79
- const memoryContext = readMemory(projectDir, projectName);
80
-
81
- // Montar prompt com contexto de memória
82
- const fullPrompt = memoryContext
83
- ? `[Contexto de memória]\n${memoryContext}\n[Tarefa]\n${userPrompt}`
84
- : userPrompt;
62
+ const { server, model, token, history, projectDir, projectName, ensureWorkDir } = opts || {};
63
+ // A pasta só nasce quando o agente for mesmo mexer em arquivo. Um "oi"
64
+ // não deve deixar diretório vazio espalhado pelo disco.
65
+ let workDirReady = !ensureWorkDir;
66
+
67
+ // Conversa completa desta tarefa: histórico recente + memória + pedido.
68
+ const messages = [];
69
+ const memory = buildMemoryContext(projectDir, projectName);
70
+ if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
71
+ for (const m of (history || []).slice(-20)) {
72
+ if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
73
+ }
74
+ messages.push({ role: 'user', content: userPrompt });
85
75
 
86
- const toolResults = [];
87
- let iteration = 0;
88
- let plan = '';
89
- let assistantContent = '';
76
+ let finalText = '';
90
77
 
91
- while (iteration < MAX_ITERATIONS) {
92
- iteration++;
78
+ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
93
79
  const sp = new Spinner('Primo Code pensando').start();
94
80
 
81
+ const toolCalls = [];
82
+ let assistantText = '';
83
+ let streamError = null;
84
+ let streaming = false;
85
+
95
86
  try {
96
- await api.requestAct(server, fullPrompt, history, model, { projectDir, projectName, plan_cache: plan, tool_results: toolResults }, (evt) => {
97
- if (evt.plan) {
98
- plan = evt.plan;
99
- }
100
- if (evt.tool_call) {
101
- sp.stop();
102
- const tc = evt.tool_call;
103
- console.log(actionHeader(tc.name, tc.arguments));
104
- logger.event(tc.name, tc.arguments);
105
-
106
- // Executar tool localmente
107
- tools.execute(tc.name, tc.arguments).then(result => {
108
- logger.event(tc.name + '_done', null, result);
109
- toolResults.push({ tool_call_id: tc.id || 'tc_' + iteration, result });
110
- });
111
- sp.start();
112
- }
113
- if (evt.assistant_message) {
114
- assistantContent = evt.assistant_message.content || '';
115
- }
87
+ await api.requestAct(server, { messages, model, token, projectDir, projectName }, (evt) => {
88
+ if (evt.erro) { streamError = evt.erro; return; }
89
+ if (evt.tool_call) { toolCalls.push(evt.tool_call); return; }
116
90
  if (evt.delta) {
117
- sp.stop();
118
- process.stdout.write(evt.delta);
119
- sp.start();
91
+ if (!streaming) { sp.stop(); streaming = true; }
92
+ assistantText += evt.delta;
93
+ process.stdout.write(c.white(evt.delta));
120
94
  }
95
+ if (evt.assistant_message) assistantText = evt.assistant_message.content || assistantText;
121
96
  });
97
+ } catch (e) {
122
98
  sp.stop();
99
+ console.log(c.err(`\nErro: ${e.message || e}`));
100
+ throw e;
101
+ }
102
+ sp.stop();
123
103
 
124
- // Se não teve tool_calls, mostrar conteúdo
125
- if (toolResults.length === 0 && assistantContent) {
126
- console.log('\n' + c.white(assistantContent));
127
- // Atualizar memória
128
- updateMemory(projectDir, projectName, `Tarefa: ${userPrompt}\nResultado: ${assistantContent.slice(0, 500)}`);
129
- return { userMessage: { role: 'user', content: userPrompt }, assistantMessage: { role: 'assistant', content: assistantContent } };
130
- }
104
+ if (streamError) {
105
+ console.log(c.err(`\nErro: ${streamError}`));
106
+ return { assistantMessage: { role: 'assistant', content: `Erro: ${streamError}` } };
107
+ }
131
108
 
132
- // Se teve tool_results, enviar de volta para próximo turno
133
- if (toolResults.length > 0) {
134
- // Limpar para próximo turno
135
- toolResults.length = 0;
136
- assistantContent = '';
109
+ // Sem tool_calls: o modelo respondeu e acabou.
110
+ if (!toolCalls.length) {
111
+ finalText = assistantText.trim();
112
+ if (finalText && !streaming) console.log('\n' + c.white(finalText));
113
+ else if (streaming) process.stdout.write('\n');
114
+ break;
115
+ }
116
+
117
+ if (streaming) process.stdout.write('\n');
118
+
119
+ // Registra na conversa o turno do assistente COM os tool_calls.
120
+ // A API exige que cada mensagem role:'tool' venha depois deste turno.
121
+ messages.push({
122
+ role: 'assistant',
123
+ content: assistantText || null,
124
+ tool_calls: toolCalls.map((tc) => ({
125
+ id: tc.id,
126
+ type: 'function',
127
+ function: { name: tc.name, arguments: JSON.stringify(tc.arguments || {}) },
128
+ })),
129
+ });
130
+
131
+ let finished = null;
132
+ for (const tc of toolCalls) {
133
+ if (tc.name === 'finish') {
134
+ finished = (tc.arguments && tc.arguments.summary) || 'Concluído.';
135
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: 'ok' });
137
136
  continue;
138
137
  }
139
138
 
140
- break;
141
- } catch (e) {
142
- sp.stop();
143
- console.log(c.err(`\nErro: ${e.message||e}`));
144
- throw e;
139
+ if (!workDirReady) { ensureWorkDir(); workDirReady = true; }
140
+
141
+ console.log(actionHeader(tc.name, tc.arguments));
142
+ const result = await tools.execute(tc.name, tc.arguments, { projectDir });
143
+ logResult(tc.name, result);
144
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: summarizeForModel(result) });
145
145
  }
146
+
147
+ if (finished) { finalText = finished; console.log('\n' + c.white(finished)); break; }
146
148
  }
147
149
 
148
- // Atualizar memória
149
- updateMemory(projectDir, projectName, `Tarefa: ${userPrompt}\nResultado: Concluído`);
150
- return { userMessage: { role: 'user', content: userPrompt }, assistantMessage: { role: 'assistant', content: assistantContent || 'Concluído.' } };
150
+ if (!finalText) finalText = 'Cheguei ao limite de passos sem concluir. Refaça o pedido em partes menores.';
151
+
152
+ // Nada de arquivo mexido = nada para lembrar.
153
+ if (projectName && workDirReady) {
154
+ cfg.appendProjectMemory(projectDir, `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
155
+ }
156
+
157
+ return {
158
+ userMessage: { role: 'user', content: userPrompt },
159
+ assistantMessage: { role: 'assistant', content: finalText },
160
+ };
151
161
  }
152
162
 
153
163
  module.exports = { runAct };
package/lib/api.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * API client — comunicação com primocode-server
2
+ * Cliente HTTP do primocode-server.
3
3
  */
4
4
 
5
5
  'use strict';
@@ -8,35 +8,128 @@ const https = require('https');
8
8
  const http = require('http');
9
9
  const { URL } = require('url');
10
10
 
11
- function request(serverUrl, endpoint, payload) {
11
+ // Erro cru do servidor não diz o que fazer. Estes dois dizem.
12
+ function explainHttpError(status, msg) {
13
+ if (status === 401) {
14
+ return `${msg}\nEste servidor pede um token. Configure com:\n export PRIMOCODE_TOKEN="seu-token"\nou adicione "token" em ~/.primocode/config.json`;
15
+ }
16
+ if (status === 429) {
17
+ return `${msg}\nSe você é o dono do servidor, configure PRIMOCODE_TOKEN para não ter limite.`;
18
+ }
19
+ return msg;
20
+ }
21
+
22
+ function buildOptions(url, method, extraHeaders, bodyLength, token) {
23
+ const headers = { ...extraHeaders };
24
+ if (bodyLength != null) {
25
+ headers['Content-Type'] = 'application/json';
26
+ headers['Content-Length'] = bodyLength;
27
+ }
28
+ if (token) headers['x-primocode-token'] = token;
29
+ return {
30
+ hostname: url.hostname,
31
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
32
+ path: url.pathname + url.search,
33
+ method,
34
+ headers,
35
+ };
36
+ }
37
+
38
+ function request(serverUrl, endpoint, payload, token) {
12
39
  return new Promise((resolve, reject) => {
13
40
  const url = new URL(endpoint, serverUrl);
14
41
  const data = JSON.stringify(payload);
15
- const options = { hostname: url.hostname, port: url.port || (url.protocol==='https:'?443:80), path: url.pathname+url.search, method: 'POST', headers: { 'Content-Type':'application/json', 'Content-Length':Buffer.byteLength(data) } };
16
- const client = url.protocol==='https:' ? https : http;
17
- const req = client.request(options, (res) => { let body=''; res.on('data', c=>body+=c); res.on('end', () => { try { const json=JSON.parse(body); if (res.statusCode>=400) reject(new Error(json.erro||json.error||`HTTP ${res.statusCode}`)); else resolve(json); } catch { reject(new Error(`Resposta inválida: ${body.slice(0,200)}`)); } }); });
18
- req.on('error', reject); req.write(data); req.end();
42
+ const client = url.protocol === 'https:' ? https : http;
43
+ const req = client.request(buildOptions(url, 'POST', {}, Buffer.byteLength(data), token), (res) => {
44
+ let body = '';
45
+ res.on('data', (chunk) => { body += chunk; });
46
+ res.on('end', () => {
47
+ let json;
48
+ try { json = JSON.parse(body); }
49
+ catch { return reject(new Error(`Resposta inválida (HTTP ${res.statusCode}): ${body.slice(0, 200)}`)); }
50
+ if (res.statusCode >= 400) reject(new Error(json.erro || json.error || `HTTP ${res.statusCode}`));
51
+ else resolve(json);
52
+ });
53
+ });
54
+ req.on('error', reject);
55
+ req.write(data);
56
+ req.end();
19
57
  });
20
58
  }
21
59
 
22
- function requestStream(serverUrl, endpoint, payload, onEvent) {
60
+ function requestStream(serverUrl, endpoint, payload, onEvent, token) {
23
61
  return new Promise((resolve, reject) => {
24
62
  const url = new URL(endpoint, serverUrl);
25
63
  const data = JSON.stringify(payload);
26
- const options = { hostname: url.hostname, port: url.port || (url.protocol==='https:'?443:80), path: url.pathname+url.search, method: 'POST', headers: { 'Content-Type':'application/json', 'Content-Length':Buffer.byteLength(data), 'Accept':'text/event-stream' } };
27
- const client = url.protocol==='https:' ? https : http;
28
- const req = client.request(options, (res) => { let buffer=''; res.on('data', chunk => { buffer+=chunk; const lines=buffer.split('\n'); buffer=lines.pop(); for (const line of lines) if (line.startsWith('data: ')) { try { const evt=JSON.parse(line.slice(6)); onEvent(evt); if (evt.done) resolve(); } catch {} } }); res.on('end', () => resolve()); });
29
- req.on('error', reject); req.write(data); req.end();
64
+ const client = url.protocol === 'https:' ? https : http;
65
+ const options = buildOptions(url, 'POST', { Accept: 'text/event-stream' }, Buffer.byteLength(data), token);
66
+
67
+ const req = client.request(options, (res) => {
68
+ // Erro HTTP não vem como SSE — o corpo é um JSON de erro.
69
+ if (res.statusCode >= 400) {
70
+ let body = '';
71
+ res.on('data', (chunk) => { body += chunk; });
72
+ res.on('end', () => {
73
+ let msg = `HTTP ${res.statusCode}`;
74
+ try { const j = JSON.parse(body); msg = j.erro || j.error || msg; } catch {}
75
+ reject(new Error(explainHttpError(res.statusCode, msg)));
76
+ });
77
+ return;
78
+ }
79
+
80
+ let settled = false;
81
+ const finish = () => { if (!settled) { settled = true; resolve(); } };
82
+
83
+ let buffer = '';
84
+ res.setEncoding('utf8');
85
+ res.on('data', (chunk) => {
86
+ buffer += chunk;
87
+ const lines = buffer.split('\n');
88
+ buffer = lines.pop();
89
+ for (const line of lines) {
90
+ if (!line.startsWith('data: ')) continue;
91
+ let evt;
92
+ try { evt = JSON.parse(line.slice(6)); } catch { continue; }
93
+ onEvent(evt);
94
+ if (evt.done) finish();
95
+ }
96
+ });
97
+ res.on('end', finish);
98
+ res.on('error', reject);
99
+ });
100
+
101
+ req.on('error', reject);
102
+ req.write(data);
103
+ req.end();
30
104
  });
31
105
  }
32
106
 
33
- function requestAct(serverUrl, prompt, history, model, extras, onEvent) {
34
- // Envia prompt (string) e history (array) separados para /api/act/stream
35
- return requestStream(serverUrl, '/api/act/stream', { prompt, history, model, ...(extras || {}) }, onEvent);
107
+ // Agente autônomo. `payload` carrega a conversa completa (messages).
108
+ function requestAct(serverUrl, payload, onEvent) {
109
+ const { token, ...body } = payload;
110
+ return requestStream(serverUrl, '/api/act/stream', body, onEvent, token);
111
+ }
112
+
113
+ // Chat puro.
114
+ function requestChat(serverUrl, payload, onEvent) {
115
+ const { token, ...body } = payload;
116
+ return requestStream(serverUrl, '/api/chat/stream', body, onEvent, token);
36
117
  }
37
118
 
38
- function getHealth(serverUrl) {
39
- return new Promise((resolve, reject) => { const url=new URL('/health',serverUrl); const client=url.protocol==='https:'?https:http; const req=client.get(url, (res)=>{ let body=''; res.on('data',c=>body+=c); res.on('end',()=>{ try { resolve(JSON.parse(body)); } catch { reject(new Error('Resposta inválida do health check')); } }); }); req.on('error',reject); });
119
+ function getHealth(serverUrl, token) {
120
+ return new Promise((resolve, reject) => {
121
+ const url = new URL('/health', serverUrl);
122
+ const client = url.protocol === 'https:' ? https : http;
123
+ const req = client.get(buildOptions(url, 'GET', {}, null, token), (res) => {
124
+ let body = '';
125
+ res.on('data', (chunk) => { body += chunk; });
126
+ res.on('end', () => {
127
+ try { resolve(JSON.parse(body)); }
128
+ catch { reject(new Error(`Resposta inválida do health check (HTTP ${res.statusCode})`)); }
129
+ });
130
+ });
131
+ req.on('error', reject);
132
+ });
40
133
  }
41
134
 
42
- module.exports = { request, requestStream, requestAct, getHealth };
135
+ module.exports = { request, requestStream, requestAct, requestChat, getHealth };