primocode 7.0.3 → 7.1.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/README.md +78 -76
- package/bin/primocode.js +243 -225
- package/lib/act.js +120 -116
- package/lib/api.js +110 -17
- package/lib/browser.js +492 -492
- package/lib/config.js +105 -52
- package/lib/desktop.js +142 -142
- package/lib/tools.js +147 -104
- package/lib/ui.js +165 -161
- package/package.json +5 -3
package/lib/act.js
CHANGED
|
@@ -1,153 +1,157 @@
|
|
|
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
|
|
13
|
+
const cfg = require('./config');
|
|
11
14
|
|
|
12
15
|
const MAX_ITERATIONS = 15;
|
|
13
16
|
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (
|
|
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
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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(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(projectName);
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
62
|
+
const { server, model, token, history, projectDir, projectName } = opts || {};
|
|
63
|
+
|
|
64
|
+
// Conversa completa desta tarefa: histórico recente + memória + pedido.
|
|
65
|
+
const messages = [];
|
|
66
|
+
const memory = buildMemoryContext(projectName);
|
|
67
|
+
if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
|
|
68
|
+
for (const m of (history || []).slice(-20)) {
|
|
69
|
+
if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
|
|
70
|
+
}
|
|
71
|
+
messages.push({ role: 'user', content: userPrompt });
|
|
85
72
|
|
|
86
|
-
|
|
87
|
-
let iteration = 0;
|
|
88
|
-
let plan = '';
|
|
89
|
-
let assistantContent = '';
|
|
73
|
+
let finalText = '';
|
|
90
74
|
|
|
91
|
-
|
|
92
|
-
iteration++;
|
|
75
|
+
for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
|
|
93
76
|
const sp = new Spinner('Primo Code pensando').start();
|
|
94
77
|
|
|
78
|
+
const toolCalls = [];
|
|
79
|
+
let assistantText = '';
|
|
80
|
+
let streamError = null;
|
|
81
|
+
let streaming = false;
|
|
82
|
+
|
|
95
83
|
try {
|
|
96
|
-
await api.requestAct(server,
|
|
97
|
-
if (evt.
|
|
98
|
-
|
|
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
|
-
}
|
|
84
|
+
await api.requestAct(server, { messages, model, token, projectDir, projectName }, (evt) => {
|
|
85
|
+
if (evt.erro) { streamError = evt.erro; return; }
|
|
86
|
+
if (evt.tool_call) { toolCalls.push(evt.tool_call); return; }
|
|
116
87
|
if (evt.delta) {
|
|
117
|
-
sp.stop();
|
|
118
|
-
|
|
119
|
-
|
|
88
|
+
if (!streaming) { sp.stop(); streaming = true; }
|
|
89
|
+
assistantText += evt.delta;
|
|
90
|
+
process.stdout.write(c.white(evt.delta));
|
|
120
91
|
}
|
|
92
|
+
if (evt.assistant_message) assistantText = evt.assistant_message.content || assistantText;
|
|
121
93
|
});
|
|
94
|
+
} catch (e) {
|
|
122
95
|
sp.stop();
|
|
96
|
+
console.log(c.err(`\nErro: ${e.message || e}`));
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
sp.stop();
|
|
123
100
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
updateMemory(projectDir, projectName, `Tarefa: ${userPrompt}\nResultado: ${assistantContent.slice(0, 500)}`);
|
|
129
|
-
return { userMessage: { role: 'user', content: userPrompt }, assistantMessage: { role: 'assistant', content: assistantContent } };
|
|
130
|
-
}
|
|
101
|
+
if (streamError) {
|
|
102
|
+
console.log(c.err(`\nErro: ${streamError}`));
|
|
103
|
+
return { assistantMessage: { role: 'assistant', content: `Erro: ${streamError}` } };
|
|
104
|
+
}
|
|
131
105
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
106
|
+
// Sem tool_calls: o modelo respondeu e acabou.
|
|
107
|
+
if (!toolCalls.length) {
|
|
108
|
+
finalText = assistantText.trim();
|
|
109
|
+
if (finalText && !streaming) console.log('\n' + c.white(finalText));
|
|
110
|
+
else if (streaming) process.stdout.write('\n');
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (streaming) process.stdout.write('\n');
|
|
115
|
+
|
|
116
|
+
// Registra na conversa o turno do assistente COM os tool_calls.
|
|
117
|
+
// A API exige que cada mensagem role:'tool' venha depois deste turno.
|
|
118
|
+
messages.push({
|
|
119
|
+
role: 'assistant',
|
|
120
|
+
content: assistantText || null,
|
|
121
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
122
|
+
id: tc.id,
|
|
123
|
+
type: 'function',
|
|
124
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.arguments || {}) },
|
|
125
|
+
})),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
let finished = null;
|
|
129
|
+
for (const tc of toolCalls) {
|
|
130
|
+
if (tc.name === 'finish') {
|
|
131
|
+
finished = (tc.arguments && tc.arguments.summary) || 'Concluído.';
|
|
132
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: 'ok' });
|
|
137
133
|
continue;
|
|
138
134
|
}
|
|
139
135
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
throw e;
|
|
136
|
+
console.log(actionHeader(tc.name, tc.arguments));
|
|
137
|
+
const result = await tools.execute(tc.name, tc.arguments, { projectDir });
|
|
138
|
+
logResult(tc.name, result);
|
|
139
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: summarizeForModel(result) });
|
|
145
140
|
}
|
|
141
|
+
|
|
142
|
+
if (finished) { finalText = finished; console.log('\n' + c.white(finished)); break; }
|
|
146
143
|
}
|
|
147
144
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
if (!finalText) finalText = 'Cheguei ao limite de passos sem concluir. Refaça o pedido em partes menores.';
|
|
146
|
+
|
|
147
|
+
if (projectName) {
|
|
148
|
+
cfg.appendProjectMemory(projectName, `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
userMessage: { role: 'user', content: userPrompt },
|
|
153
|
+
assistantMessage: { role: 'assistant', content: finalText },
|
|
154
|
+
};
|
|
151
155
|
}
|
|
152
156
|
|
|
153
157
|
module.exports = { runAct };
|
package/lib/api.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
|
|
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
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
req.
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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) => {
|
|
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 };
|