primocode 1.0.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 ADDED
@@ -0,0 +1,495 @@
1
+ // Modo Conecta Primo AI — faz tudo sozinho: código, arquivos, terminal, navegador
2
+
3
+ 'use strict';
4
+
5
+ const path = require('path');
6
+ const { c, actionHeader, toolLine, Spinner } = require('./ui');
7
+ const tools = require('./tools');
8
+ const api = require('./api');
9
+
10
+ const MAX_ITERATIONS = 15;
11
+
12
+ function fmtArgs(name, args) {
13
+ if (!args) return '';
14
+ switch (name) {
15
+ case 'read_file': return args.path || '';
16
+ case 'write_file': return `${args.path || ''} · ${(args.content || '').length} chars`;
17
+ case 'list_dir': return args.path || '.';
18
+ case 'run_shell': return args.command || '';
19
+ case 'run_in_new_terminal':return `${args.command || ''}${args.cwd ? ' @ ' + args.cwd : ''}`;
20
+ case 'open_in_browser': return args.target || '';
21
+ case 'browser_open': return args.url || '';
22
+ case 'browser_navigate': return args.url || '';
23
+ case 'browser_click': return args.selector || args.text || (args.x != null ? `${args.x},${args.y}` : '');
24
+ case 'browser_type': return `${args.selector ? args.selector + ' · ' : ''}${(args.text || '').length} chars`;
25
+ case 'browser_key': return args.key || '';
26
+ case 'browser_wait_for': return args.selector || '';
27
+ case 'browser_wait': return `${args.ms || 1000}ms`;
28
+ case 'browser_screenshot': return args.path || '(auto)';
29
+ case 'browser_eval': return (args.js || '').slice(0, 60);
30
+ case 'browser_scroll': return `dy=${args.dy || 400}`;
31
+ case 'browser_url': return '';
32
+ case 'finish': return args.summary || '';
33
+ default: return JSON.stringify(args).slice(0, 80);
34
+ }
35
+ }
36
+
37
+ // Nomes simples das ações
38
+ const NICE = {
39
+ read_file: 'Read', write_file: 'Write', list_dir: 'List',
40
+ run_shell: 'Bash', run_in_new_terminal: 'Terminal', open_in_browser: 'Open',
41
+ browser_open: 'Browser', browser_navigate: 'Navigate', browser_click: 'Click',
42
+ browser_clickText: 'ClickText', browser_type: 'Type', browser_key: 'Key',
43
+ browser_wait_for: 'Wait', browser_wait: 'Wait', browser_screenshot: 'Screenshot',
44
+ browser_eval: 'Eval', browser_scroll: 'Scroll', browser_url: 'URL', finish: 'Finish',
45
+ };
46
+
47
+ function makeLogger() {
48
+ return {
49
+ event(kind, arg, result) {
50
+ if (kind === 'run_shell') return;
51
+ if (kind === 'run_shell_done') {
52
+ if (result) {
53
+ const s = result.ok ? c.ok(' exit 0') : c.err(` exit ${result.code}${result.killed ? ' timeout' : ''}`);
54
+ process.stdout.write('\n' + s + '\n');
55
+ }
56
+ return;
57
+ }
58
+ const prefix = c.muted(' ⎿ ');
59
+ if (kind === 'write_file' && result && result.ok) {
60
+ console.log(prefix + c.muted(`${result.created ? 'created' : 'updated'} · ${tools.fmtBytes(result.bytes)}`));
61
+ return;
62
+ }
63
+ if (kind === 'read_file' && result && result.ok) {
64
+ console.log(prefix + c.muted(`${tools.fmtBytes(result.bytes || 0)}${result.truncated ? ' (truncated)' : ''}`));
65
+ return;
66
+ }
67
+ if (kind === 'list_dir' && result && result.ok) {
68
+ const lines = result.tree.split('\n').slice(0, 8);
69
+ for (const l of lines) console.log(prefix + c.muted(l));
70
+ const total = result.tree.split('\n').length;
71
+ if (total > 8) console.log(prefix + c.muted(`… +${total - 8}`));
72
+ return;
73
+ }
74
+ if (kind === 'open_in_browser' && result && result.ok) {
75
+ console.log(prefix + c.muted(`${result.opener || 'browser'}`));
76
+ return;
77
+ }
78
+ if (kind === 'run_in_new_terminal' && result && result.ok) {
79
+ console.log(prefix + c.muted(`${result.opener}`));
80
+ return;
81
+ }
82
+ if (kind === 'finish') {
83
+ console.log(prefix + c.muted(arg || 'ok'));
84
+ return;
85
+ }
86
+ if (result && result.ok === false) {
87
+ console.log(prefix + c.err(result.error || 'failed'));
88
+ }
89
+ },
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Executa uma tarefa em modo Act (agente autônomo com tools).
95
+ * @param {string} userPrompt - texto do usuário
96
+ * @param {object} opts - { server, model, history: [{role,content}, ...] }
97
+ * @returns {Promise<{userMessage, assistantMessage}>} para o caller atualizar seu histórico
98
+ */
99
+ async function runAct(userPrompt, opts) {
100
+ const { server, model, history: chatHistory = [] } = opts;
101
+
102
+ // Injeta contexto do CWD e inclui histórico curto de conversa
103
+ const cwd = process.cwd();
104
+ const cwdBase = path.basename(cwd);
105
+ const enriched = `[contexto: diretório atual = ${cwd} (basename: '${cwdBase}')]\n\n${userPrompt}`;
106
+
107
+ // Estado do loop
108
+ // Começamos com as mensagens do chat (para memória) + o novo user prompt.
109
+ let messages = [];
110
+ if (Array.isArray(chatHistory) && chatHistory.length) {
111
+ // Só passamos user/assistant sem tool_calls (o histórico de tool calls foi episódico).
112
+ for (const m of chatHistory) {
113
+ if (m && (m.role === 'user' || m.role === 'assistant') && m.content) {
114
+ messages.push({ role: m.role, content: m.content });
115
+ }
116
+ }
117
+ }
118
+
119
+ let promptForCall = enriched;
120
+ let toolResults = null;
121
+ let finished = false;
122
+ const log = makeLogger();
123
+ const recentCalls = [];
124
+ let finalAssistantText = '';
125
+ let sp = null;
126
+ let planCache = ''; // cache do plano do Planner entre turnos
127
+ let planShown = false; // evita mostrar plano múltiplas vezes
128
+ const filesWritten = new Set(); // arquivos criados/modificados pelo Construtor 1 (para refino)
129
+ const deferredOpens = []; // open_in_browser adiados até depois do refino
130
+
131
+ for (let iter = 1; iter <= MAX_ITERATIONS && !finished; iter++) {
132
+ let assistantContent = '';
133
+ let assistantMessage = null;
134
+ let toolCalls = [];
135
+ let serverErr = null;
136
+ let hasToolCalls = false;
137
+ let streamingStarted = false;
138
+ let printedUpTo = 0; // quantos chars de assistantContent já foram impressos
139
+ let suppressRest = false; // uma vez detectado marker, para de imprimir
140
+ let retryNeeded = false; // server pedidu retry (ex: finish rejeitado)
141
+ let rejectedNote = null; // motivo da rejeição, injetado como tool_result no próximo turno
142
+ let subagentsPayload = null; // resultado de spawn_parallel (injetado como tool_result)
143
+ let subagentsToolCallId = null;
144
+ let subagentsTotal = 0;
145
+ let subagentsDone = 0;
146
+
147
+ // Spinner enquanto aguarda primeiro byte
148
+ sp = new Spinner('Pensando').start();
149
+
150
+ try {
151
+ await api.requestStream(server, '/api/act/stream', {
152
+ prompt: promptForCall,
153
+ history: messages,
154
+ tool_results: toolResults,
155
+ model,
156
+ plan_cache: planCache,
157
+ }, (evt) => {
158
+ if (evt.phase === 'inspiring') {
159
+ if (sp) sp.stop();
160
+ sp = new Spinner('Buscando referências').start();
161
+ }
162
+ if (evt.inspiration_urls && Array.isArray(evt.inspiration_urls) && evt.inspiration_urls.length > 0) {
163
+ if (sp) { sp.stop(); sp = null; }
164
+ console.log('\n' + c.muted('◆ Inspirando-se em:'));
165
+ for (const u of evt.inspiration_urls) console.log(c.muted(' · ') + c.accent(u));
166
+ }
167
+ if (evt.phase === 'planning' && !planShown) {
168
+ if (sp) sp.stop();
169
+ sp = new Spinner('Pensando').start();
170
+ }
171
+ if (evt.plan) {
172
+ planCache = evt.plan;
173
+ if (sp) { sp.stop(); sp = null; }
174
+ planShown = true;
175
+ sp = new Spinner('Executando').start();
176
+ }
177
+ if (evt.subagents_spawning) {
178
+ subagentsTotal = evt.subagents_spawning;
179
+ if (sp) sp.stop();
180
+ console.log('\n' + c.muted(`◆ Spawn de ${subagentsTotal} subagentes em paralelo`));
181
+ sp = new Spinner(`Subagentes 0/${subagentsTotal}`).start();
182
+ }
183
+ if (evt.subagent_done) {
184
+ subagentsDone++;
185
+ const d = evt.subagent_done;
186
+ if (sp) { sp.stop(); sp = null; }
187
+ const status = d.ok ? c.ok('✓') : c.err('✗');
188
+ console.log(` ${status} ${c.muted(d.title)} ${c.muted('· ' + (d.bytes || 0) + ' bytes')}`);
189
+ if (subagentsDone < subagentsTotal) sp = new Spinner(`Subagentes ${subagentsDone}/${subagentsTotal}`).start();
190
+ }
191
+ if (evt.subagents_result) {
192
+ subagentsPayload = evt.subagents_result;
193
+ subagentsToolCallId = evt.tool_call_id;
194
+ if (sp) { sp.stop(); sp = null; }
195
+ }
196
+ if (evt.erro) { serverErr = evt.erro; return; }
197
+ if (evt.delta) {
198
+ if (!streamingStarted) {
199
+ sp.stop();
200
+ process.stdout.write('\n');
201
+ streamingStarted = true;
202
+ }
203
+ assistantContent += evt.delta;
204
+
205
+ if (suppressRest) return;
206
+
207
+ // Se aparece começo de marker suspeito, marca e para de imprimir daqui em diante.
208
+ // Também "rebobina" cortando o que já foi acumulado até antes do marker
209
+ // (não conseguimos apagar do terminal, mas o texto final ficará limpo).
210
+ const leakRe = /<\||【|\n\s*\{\s*"(name|type|summary|arguments|command|path)"|\bfinish\s*\(|\bwrite_file\s*\(|\brun_shell\s*\(|\bopen_in_browser\s*\(|\bbrowser_\w+\s*\(/;
211
+ const m = assistantContent.match(leakRe);
212
+ if (m && m.index !== undefined) {
213
+ const safeEnd = m.index;
214
+ if (safeEnd > printedUpTo) {
215
+ process.stdout.write(c.white(assistantContent.slice(printedUpTo, safeEnd)));
216
+ printedUpTo = safeEnd;
217
+ }
218
+ suppressRest = true;
219
+ return;
220
+ }
221
+
222
+ // Imprime até len-2 para dar margem se um marker começar a formar
223
+ const target = Math.max(printedUpTo, assistantContent.length - 2);
224
+ if (target > printedUpTo) {
225
+ process.stdout.write(c.white(assistantContent.slice(printedUpTo, target)));
226
+ printedUpTo = target;
227
+ }
228
+ }
229
+ if (evt.tool_call) toolCalls.push(evt.tool_call);
230
+ if (evt.assistant_message) assistantMessage = evt.assistant_message;
231
+ if (evt.tool_rejected) rejectedNote = evt.tool_rejected;
232
+ if (evt.retry_needed) retryNeeded = true;
233
+ if (evt.done) hasToolCalls = !!evt.has_tool_calls;
234
+ if (evt.instability) { serverErr = 'servidor com rate-limit; tente novamente em alguns minutos'; return; }
235
+ });
236
+ } catch (e) {
237
+ if (sp) sp.stop();
238
+ console.log(c.err(`\nerro: ${e.message}`));
239
+ return { userMessage: { role: 'user', content: userPrompt }, assistantMessage: { role: 'assistant', content: `[erro: ${e.message}]` } };
240
+ }
241
+
242
+ if (sp && !streamingStarted) sp.stop();
243
+
244
+ // Flush do buffer de 2 chars retido para evitar corte de markers
245
+ if (streamingStarted && !suppressRest && printedUpTo < assistantContent.length) {
246
+ process.stdout.write(c.white(assistantContent.slice(printedUpTo)));
247
+ printedUpTo = assistantContent.length;
248
+ }
249
+
250
+ if (serverErr) {
251
+ console.log(c.err(`\nerro: ${serverErr}`));
252
+ return { userMessage: { role: 'user', content: userPrompt }, assistantMessage: { role: 'assistant', content: `[erro: ${serverErr}]` } };
253
+ }
254
+
255
+ if (streamingStarted) process.stdout.write('\n');
256
+
257
+ // Atualiza messages internas do loop (com tool_calls, para o modelo saber o que pediu)
258
+ if (assistantMessage) messages.push(assistantMessage);
259
+ else messages.push({ role: 'assistant', content: assistantContent });
260
+
261
+ // No primeiro turno, inserimos o user antes do assistant (foi só passado como prompt)
262
+ if (iter === 1) {
263
+ const last = messages.pop();
264
+ messages.push({ role: 'user', content: enriched });
265
+ messages.push(last);
266
+ }
267
+
268
+ promptForCall = null;
269
+
270
+ if (assistantContent) {
271
+ let cleaned = assistantContent
272
+ .replace(/<\|[\s\S]*$/g, '')
273
+ .replace(/【[\s\S]*$/g, '')
274
+ .replace(/\n\s*\{\s*"(name|type|summary|arguments|command|path)"[\s\S]*$/, '')
275
+ .replace(/^\s*\{\s*"(name|type|summary|arguments)"[\s\S]*$/, '')
276
+ // Corta chamadas de função vazadas em texto
277
+ .replace(/\b(finish|write_file|read_file|list_dir|run_shell|run_in_new_terminal|open_in_browser|browser_\w+)\s*\([\s\S]*$/g, '')
278
+ .trim();
279
+ if (cleaned) finalAssistantText = cleaned;
280
+ }
281
+
282
+ // Subagentes rodaram — injeta resultado como tool_result e segue loop
283
+ if (subagentsPayload && subagentsToolCallId) {
284
+ toolResults = [{ tool_call_id: subagentsToolCallId, result: JSON.stringify(subagentsPayload) }];
285
+ promptForCall = null;
286
+ continue;
287
+ }
288
+
289
+ // Server pediu retry (ex: finish sem trabalho no primeiro turno)
290
+ if (retryNeeded && rejectedNote) {
291
+ // Injeta pseudo tool_result educativo pro próximo turno
292
+ const rejectedId = assistantMessage?.tool_calls?.[0]?.id;
293
+ if (rejectedId) {
294
+ toolResults = [{ tool_call_id: rejectedId, result: JSON.stringify({ ok: false, error: rejectedNote.reason }) }];
295
+ }
296
+ // Novo prompt reforçando a ordem
297
+ promptForCall = `[sistema] Seu último ${rejectedNote.name}() foi rejeitado: ${rejectedNote.reason}\n\nExecute o pedido original agora, usando as ferramentas corretas antes de qualquer finish.`;
298
+ continue;
299
+ }
300
+
301
+ if (!hasToolCalls || toolCalls.length === 0) {
302
+ finished = true;
303
+ break;
304
+ }
305
+
306
+ // Executa cada tool
307
+ const results = [];
308
+ for (const tc of toolCalls) {
309
+ const name = tc.name;
310
+ const args = tc.arguments || {};
311
+ const nice = NICE[name] || name;
312
+ const detail = fmtArgs(name, args);
313
+ console.log('\n' + toolLine(nice, detail));
314
+
315
+ let res;
316
+ // Interceptação: se o Construtor 1 quer abrir arquivo E há refino planejado,
317
+ // adia a abertura pra DEPOIS do refino (não mostra ao usuário versão bugada).
318
+ // Só adiamos se já houver pelo menos 1 arquivo escrito (senão não faz sentido refinar).
319
+ const shouldDeferOpen = (name === 'open_in_browser' && args.target && filesWritten.size > 0);
320
+ if (shouldDeferOpen) {
321
+ deferredOpens.push(args.target);
322
+ res = { ok: true, text_for_model: JSON.stringify({ ok: true, deferred: true, note: 'abertura adiada para depois do refino' }) };
323
+ console.log(c.muted(' ⎿ adiado até depois do refino'));
324
+ } else {
325
+ try {
326
+ res = await tools.executeTool(name, args, log);
327
+ } catch (e) {
328
+ res = { ok: false, text_for_model: JSON.stringify({ ok: false, error: e.message }) };
329
+ console.log(c.err(` exception: ${e.message}`));
330
+ }
331
+ }
332
+ results.push({ tool_call_id: tc.id, result: res.text_for_model });
333
+
334
+ // Rastreia arquivos escritos para depois passar ao Construtor 2 (refino)
335
+ if (name === 'write_file' && args.path) {
336
+ try { filesWritten.add(String(args.path)); } catch { /* noop */ }
337
+ }
338
+
339
+ if (res.finish) finished = true;
340
+
341
+ const sig = name + '|' + JSON.stringify(args);
342
+ recentCalls.push(sig);
343
+ if (recentCalls.length > 4) recentCalls.shift();
344
+ if (recentCalls.filter(s => s === sig).length >= 3) {
345
+ console.log('\n' + c.warn('loop detectado — encerrando.'));
346
+ finished = true;
347
+ break;
348
+ }
349
+ }
350
+
351
+ toolResults = results;
352
+ if (finished) break;
353
+ }
354
+
355
+ if (!finished) console.log('\n' + c.warn(`limite de ${MAX_ITERATIONS} iterações atingido.`));
356
+
357
+ // ═══ FASE C — Refino (Construtor 2) ═══
358
+ // Só refina se o Construtor 1 realmente criou/modificou arquivos.
359
+ // Não refina para saudações/perguntas conceituais.
360
+ if (filesWritten.size > 0) {
361
+ const refineFiles = Array.from(filesWritten).slice(0, 8); // cap defensivo
362
+ console.log('\n' + c.muted('◆ Refinando (Construtor 2)'));
363
+ let refineSp = new Spinner('Melhorando').start();
364
+ let refineIter = 0;
365
+ let refineDone = false;
366
+ let refineToolResults = null;
367
+ const REFINE_MAX = 6;
368
+ // Histórico ISOLADO do refino — nunca inclui o histórico do executor
369
+ // (isso quebra o Groq porque as tool_calls antigas não têm tool_results correspondentes).
370
+ const refineMessages = [];
371
+
372
+ while (!refineDone && refineIter < REFINE_MAX) {
373
+ refineIter++;
374
+ let assistantMessage = null;
375
+ let toolCalls = [];
376
+ let serverErr = null;
377
+ let hasToolCalls = false;
378
+ let streamingStarted = false;
379
+ let printedUpTo = 0;
380
+ let suppressRest = false;
381
+ let assistantContent = '';
382
+
383
+ try {
384
+ await api.requestStream(server, '/api/act/stream', {
385
+ refine: true,
386
+ refine_files: refineFiles,
387
+ refine_original_prompt: userPrompt,
388
+ refine_history: refineMessages,
389
+ tool_results: refineToolResults,
390
+ model,
391
+ }, (evt) => {
392
+ if (evt.erro) { serverErr = evt.erro; return; }
393
+ if (evt.delta) {
394
+ if (!streamingStarted) {
395
+ if (refineSp) { refineSp.stop(); refineSp = null; }
396
+ process.stdout.write('\n');
397
+ streamingStarted = true;
398
+ }
399
+ assistantContent += evt.delta;
400
+ if (suppressRest) return;
401
+ const leakRe = /<\||【|\n\s*\{\s*"(name|type|summary|arguments|command|path)"|\bfinish\s*\(|\bwrite_file\s*\(|\brun_shell\s*\(|\bopen_in_browser\s*\(|\bbrowser_\w+\s*\(/;
402
+ const mm = assistantContent.match(leakRe);
403
+ if (mm && mm.index !== undefined) {
404
+ const safeEnd = mm.index;
405
+ if (safeEnd > printedUpTo) {
406
+ process.stdout.write(c.white(assistantContent.slice(printedUpTo, safeEnd)));
407
+ printedUpTo = safeEnd;
408
+ }
409
+ suppressRest = true;
410
+ return;
411
+ }
412
+ const target = Math.max(printedUpTo, assistantContent.length - 2);
413
+ if (target > printedUpTo) {
414
+ process.stdout.write(c.white(assistantContent.slice(printedUpTo, target)));
415
+ printedUpTo = target;
416
+ }
417
+ }
418
+ if (evt.tool_call) toolCalls.push(evt.tool_call);
419
+ if (evt.assistant_message) assistantMessage = evt.assistant_message;
420
+ if (evt.done) hasToolCalls = !!evt.has_tool_calls;
421
+ if (evt.instability) { serverErr = 'refino indisponível (rate-limit)'; return; }
422
+ if (evt.refine_skipped) { refineDone = true; return; }
423
+ });
424
+ } catch (e) {
425
+ if (refineSp) refineSp.stop();
426
+ console.log(c.warn(`\n(refino pulado: ${e.message})`));
427
+ break;
428
+ }
429
+
430
+ if (refineSp && !streamingStarted) refineSp.stop();
431
+ if (streamingStarted && !suppressRest && printedUpTo < assistantContent.length) {
432
+ process.stdout.write(c.white(assistantContent.slice(printedUpTo)));
433
+ }
434
+ if (streamingStarted) process.stdout.write('\n');
435
+
436
+ if (serverErr) { console.log(c.warn(`\n(refino falhou: ${serverErr})`)); break; }
437
+
438
+ // Guarda o assistant_message NO HISTÓRICO ISOLADO do refino (não no messages global)
439
+ if (assistantMessage) refineMessages.push(assistantMessage);
440
+ if (!hasToolCalls || toolCalls.length === 0) { refineDone = true; break; }
441
+
442
+ const results = [];
443
+ for (const tc of toolCalls) {
444
+ const name = tc.name;
445
+ const args = tc.arguments || {};
446
+ const nice = NICE[name] || name;
447
+ const detail = fmtArgs(name, args);
448
+ console.log('\n' + toolLine(nice, detail));
449
+ let res;
450
+ try { res = await tools.executeTool(name, args, log); }
451
+ catch (e) { res = { ok: false, text_for_model: JSON.stringify({ ok: false, error: e.message }) }; console.log(c.err(` exception: ${e.message}`)); }
452
+ results.push({ tool_call_id: tc.id, result: res.text_for_model });
453
+ // O refino também precisa adicionar tool result no histórico isolado
454
+ refineMessages.push({ role: 'tool', tool_call_id: tc.id, content: res.text_for_model });
455
+ if (res.finish) refineDone = true;
456
+ }
457
+ refineToolResults = null; // já injetamos no refineMessages, não duplicar
458
+
459
+ // Prepara spinner do próximo turno de refino se ainda não terminou
460
+ if (!refineDone) refineSp = new Spinner('Melhorando').start();
461
+ }
462
+ if (refineSp) refineSp.stop();
463
+
464
+ // Executa aberturas adiadas agora que o refino terminou
465
+ if (deferredOpens.length > 0) {
466
+ for (const target of deferredOpens) {
467
+ console.log('\n' + toolLine('Open', target));
468
+ try {
469
+ const res = await tools.executeTool('open_in_browser', { target }, log);
470
+ if (!res.ok && res.text_for_model) {
471
+ console.log(c.muted(' ⎿ ' + (res.error || 'ok')));
472
+ }
473
+ } catch (e) {
474
+ console.log(c.err(` ⎿ falha: ${e.message}`));
475
+ }
476
+ }
477
+ }
478
+ } else if (deferredOpens.length > 0) {
479
+ // Não houve refino mas há aberturas adiadas — executa direto (fallback)
480
+ for (const target of deferredOpens) {
481
+ console.log('\n' + toolLine('Open', target));
482
+ try { await tools.executeTool('open_in_browser', { target }, log); }
483
+ catch (e) { console.log(c.err(` ⎿ falha: ${e.message}`)); }
484
+ }
485
+ }
486
+
487
+ // Retorna a mensagem final para o caller anexar ao histórico do chat
488
+ // Consolidamos toda a atividade + texto final para o usuário poder referenciar depois
489
+ return {
490
+ userMessage: { role: 'user', content: userPrompt },
491
+ assistantMessage: { role: 'assistant', content: finalAssistantText || '[ação concluída]' },
492
+ };
493
+ }
494
+
495
+ module.exports = { runAct };
package/lib/api.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * API client — comunicação com primocode-server
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const https = require('https');
8
+ const http = require('http');
9
+ const { URL } = require('url');
10
+
11
+ function request(serverUrl, endpoint, payload) {
12
+ return new Promise((resolve, reject) => {
13
+ const url = new URL(endpoint, serverUrl);
14
+ 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();
19
+ });
20
+ }
21
+
22
+ function requestStream(serverUrl, endpoint, payload, onEvent) {
23
+ return new Promise((resolve, reject) => {
24
+ const url = new URL(endpoint, serverUrl);
25
+ 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();
30
+ });
31
+ }
32
+
33
+ function requestAct(serverUrl, messages, model) { return request(serverUrl, '/api/act', { messages, model }); }
34
+
35
+ function getHealth(serverUrl) {
36
+ 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); });
37
+ }
38
+
39
+ module.exports = { request, requestStream, requestAct, getHealth };