primocode 8.0.0 → 8.0.1
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 +13 -3
- package/lib/desktop.js +3 -1
- package/lib/referencia.js +8 -4
- package/lib/tools.js +9 -22
- package/package.json +1 -1
- package/studio/server.py +91 -21
- package/studio/__pycache__/server.cpython-311.pyc +0 -0
package/lib/act.js
CHANGED
|
@@ -299,9 +299,19 @@ async function runAct(userPrompt, opts) {
|
|
|
299
299
|
const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
|
|
300
300
|
logResult(tc.name, result, profundidade);
|
|
301
301
|
|
|
302
|
-
// Uma escrita bem-sucedida limpa a dívida
|
|
303
|
-
|
|
304
|
-
|
|
302
|
+
// Uma escrita bem-sucedida limpa a dívida — mas só a do MESMO
|
|
303
|
+
// arquivo. Escrever algo sem relação (ou um run_shell qualquer)
|
|
304
|
+
// não corrige um teste quebrado noutro lugar; antes isto zerava
|
|
305
|
+
// a lista inteira e o agente conseguia dizer "concluído" com uma
|
|
306
|
+
// falha real ainda pendente.
|
|
307
|
+
if (result && result.ok === false) {
|
|
308
|
+
falhasPendentes.push(`${tc.name}(${tc.arguments?.path || ''}): ${result.error}`);
|
|
309
|
+
} else if (['write_file', 'edit_file'].includes(tc.name) && tc.arguments?.path) {
|
|
310
|
+
const alvo = tc.arguments.path;
|
|
311
|
+
const mantidas = falhasPendentes.filter((f) => !f.includes(`(${alvo})`));
|
|
312
|
+
falhasPendentes.length = 0;
|
|
313
|
+
falhasPendentes.push(...mantidas);
|
|
314
|
+
}
|
|
305
315
|
|
|
306
316
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: summarizeForModel(result) });
|
|
307
317
|
}
|
package/lib/desktop.js
CHANGED
|
@@ -19,7 +19,9 @@ const os = require('os');
|
|
|
19
19
|
const PLATFORM = os.platform();
|
|
20
20
|
|
|
21
21
|
function hasCmd(cmd) {
|
|
22
|
-
|
|
22
|
+
// cmd pode vir de texto do usuário (desktop_open_app com nome sem alias
|
|
23
|
+
// conhecido) — sem aspas aqui, um ";"/"&&" no meio vira comando extra.
|
|
24
|
+
try { execSync(`which ${sh1(cmd)} || where ${sh1(cmd)}`, { stdio: 'ignore' }); return true; } catch { return false; }
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
// Detectar ferramenta disponível
|
package/lib/referencia.js
CHANGED
|
@@ -64,11 +64,15 @@ function viaProxy(alvo, { timeout }) {
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function pegar(url, { timeout = TIMEOUT_MS } = {}) {
|
|
67
|
+
function pegar(url, { timeout = TIMEOUT_MS, saltos = 0 } = {}) {
|
|
68
68
|
return new Promise((resolve) => {
|
|
69
69
|
let req;
|
|
70
70
|
const alvo = (() => { try { return new URL(url); } catch { return null; } })();
|
|
71
71
|
if (!alvo) return resolve(null);
|
|
72
|
+
// 5 saltos cobre qualquer doc real; sem teto, um redirect em loop
|
|
73
|
+
// (A→B→A→...) trava essa fonte — e a pesquisa inteira — para sempre.
|
|
74
|
+
if (saltos > 5) return resolve(null);
|
|
75
|
+
const cliente = alvo.protocol === 'http:' ? http : https;
|
|
72
76
|
|
|
73
77
|
const opcoes = {
|
|
74
78
|
headers: {
|
|
@@ -88,12 +92,12 @@ function pegar(url, { timeout = TIMEOUT_MS } = {}) {
|
|
|
88
92
|
abrir.then((cfg) => {
|
|
89
93
|
if (!cfg) return resolve(null);
|
|
90
94
|
try {
|
|
91
|
-
req =
|
|
92
|
-
// Redirecionamento: segue
|
|
95
|
+
req = cliente.get(url, cfg, (res) => {
|
|
96
|
+
// Redirecionamento: segue até o teto de saltos acima.
|
|
93
97
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
94
98
|
res.resume();
|
|
95
99
|
const destino = new URL(res.headers.location, url).href;
|
|
96
|
-
return pegar(destino, { timeout }).then(resolve);
|
|
100
|
+
return pegar(destino, { timeout, saltos: saltos + 1 }).then(resolve);
|
|
97
101
|
}
|
|
98
102
|
if (res.statusCode !== 200) { res.resume(); return resolve(null); }
|
|
99
103
|
let corpo = '';
|
package/lib/tools.js
CHANGED
|
@@ -289,7 +289,7 @@ function multiEdit({ path: rawPath, edits }, projectDir) {
|
|
|
289
289
|
if (ocorrencias > 1 && !replace_all) {
|
|
290
290
|
return { ok: false, error: `edit ${i + 1}: trecho aparece ${ocorrencias}x — use replace_all ou inclua mais contexto para torná-lo único` };
|
|
291
291
|
}
|
|
292
|
-
conteudo = replace_all ? conteudo.split(old_string).join(new_string) : conteudo.replace(old_string, new_string);
|
|
292
|
+
conteudo = replace_all ? conteudo.split(old_string).join(new_string ?? '') : conteudo.replace(old_string, new_string ?? '');
|
|
293
293
|
substituicoes++;
|
|
294
294
|
}
|
|
295
295
|
|
|
@@ -422,7 +422,10 @@ async function despachar(name, args, ctx) {
|
|
|
422
422
|
return texto == null ? { ok: false, error: 'nada na página (ou seletor não encontrado)' } : { ok: true, text: texto };
|
|
423
423
|
}
|
|
424
424
|
case 'browser_screenshot':
|
|
425
|
-
|
|
425
|
+
// Sem path, cai num nome-padrão — mas SEMPRE dentro do projeto: sem
|
|
426
|
+
// isso o arquivo ia para o cwd real do processo, não para a pasta
|
|
427
|
+
// virtual que o CLI mostra ao usuário (o processo nunca faz chdir).
|
|
428
|
+
return browser.screenshot(resolveInProject(args.path || `screenshot-${Date.now()}.png`, projectDir));
|
|
426
429
|
case 'browser_scroll':
|
|
427
430
|
return browser.scroll(args.dy != null ? args.dy : 600);
|
|
428
431
|
case 'browser_wait':
|
|
@@ -432,7 +435,9 @@ async function despachar(name, args, ctx) {
|
|
|
432
435
|
|
|
433
436
|
// ── Desktop físico — mouse, teclado e tela do usuário ───────────
|
|
434
437
|
case 'desktop_screenshot':
|
|
435
|
-
|
|
438
|
+
// Mesmo motivo do browser_screenshot: nome-padrão também precisa
|
|
439
|
+
// resolver dentro do projeto, não do cwd real do processo.
|
|
440
|
+
return desktop.screenshot(resolveInProject(args.path || `desktop-${Date.now()}.png`, projectDir));
|
|
436
441
|
case 'desktop_click':
|
|
437
442
|
return desktop.click(args.x, args.y, args.button || 'left');
|
|
438
443
|
case 'desktop_move':
|
|
@@ -484,24 +489,6 @@ async function despachar(name, args, ctx) {
|
|
|
484
489
|
}
|
|
485
490
|
}
|
|
486
491
|
|
|
487
|
-
function applyCodeBlocks(response) {
|
|
488
|
-
const regex = /```[\w]*\n([\s\S]*?)```/g;
|
|
489
|
-
const applied = [];
|
|
490
|
-
let match;
|
|
491
|
-
while ((match = regex.exec(response || '')) !== null) {
|
|
492
|
-
const block = match[1];
|
|
493
|
-
const nome = block.match(/\/\/\s*filename:\s*(.+)/);
|
|
494
|
-
if (!nome) continue;
|
|
495
|
-
const filename = nome[1].trim();
|
|
496
|
-
const content = block.replace(/\/\/\s*filename:.+\n?/, '');
|
|
497
|
-
const created = !fs.existsSync(filename);
|
|
498
|
-
fs.mkdirSync(path.dirname(filename), { recursive: true });
|
|
499
|
-
fs.writeFileSync(filename, content, 'utf-8');
|
|
500
|
-
applied.push({ path: filename, created });
|
|
501
|
-
}
|
|
502
|
-
return applied;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
492
|
async function uninstall() {
|
|
506
493
|
const configDir = path.join(os.homedir(), '.primocode');
|
|
507
494
|
console.log('Removendo PrimoCode…');
|
|
@@ -519,7 +506,7 @@ async function uninstall() {
|
|
|
519
506
|
}
|
|
520
507
|
|
|
521
508
|
module.exports = {
|
|
522
|
-
fmtBytes, treeDir,
|
|
509
|
+
fmtBytes, treeDir, runShell, openInBrowser, runInNewTerminal,
|
|
523
510
|
execute, uninstall, resolveInProject, globToRegex, globFiles, grepFiles, editFile,
|
|
524
511
|
multiEdit, writeTodos, getTodos,
|
|
525
512
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.0.1",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
package/studio/server.py
CHANGED
|
@@ -9,6 +9,7 @@ Só biblioteca padrão do Python. Nada para instalar.
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import base64
|
|
12
|
+
import functools
|
|
12
13
|
import json
|
|
13
14
|
import mimetypes
|
|
14
15
|
import os
|
|
@@ -98,10 +99,17 @@ POR_SLUG = {f["slug"]: f for f in FERRAMENTAS_INFO}
|
|
|
98
99
|
|
|
99
100
|
# -------------------------------------------------------------- utilidades --
|
|
100
101
|
|
|
101
|
-
def
|
|
102
|
+
def _normaliza(texto, tamanho):
|
|
103
|
+
"""NFKD → ASCII → não-alfanumérico vira hífen → minúsculo → corta.
|
|
104
|
+
Compartilhada por slugify() e nome_seguro(), que faziam a mesma conta
|
|
105
|
+
com um corte de tamanho diferente cada uma."""
|
|
102
106
|
texto = unicodedata.normalize("NFKD", texto or "").encode("ascii", "ignore").decode()
|
|
103
107
|
texto = re.sub(r"[^a-zA-Z0-9]+", "-", texto).strip("-").lower()
|
|
104
|
-
return (texto or "arquivo")[:
|
|
108
|
+
return (texto or "arquivo")[:tamanho]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def slugify(texto):
|
|
112
|
+
return _normaliza(texto, 40)
|
|
105
113
|
|
|
106
114
|
|
|
107
115
|
def caminho(arq_id):
|
|
@@ -163,10 +171,7 @@ mimetypes.add_type("audio/mp4", ".m4a")
|
|
|
163
171
|
|
|
164
172
|
def nome_seguro(nome):
|
|
165
173
|
base, ext = os.path.splitext(nome or "arquivo")
|
|
166
|
-
|
|
167
|
-
base = unicodedata.normalize("NFKD", base).encode("ascii", "ignore").decode()
|
|
168
|
-
base = re.sub(r"[^a-zA-Z0-9]+", "-", base).strip("-").lower()[:48] or "arquivo"
|
|
169
|
-
return base, ext
|
|
174
|
+
return _normaliza(base, 48), ext.lower()
|
|
170
175
|
|
|
171
176
|
|
|
172
177
|
def listar_midia():
|
|
@@ -218,16 +223,31 @@ def guardar_midia(dados):
|
|
|
218
223
|
|
|
219
224
|
|
|
220
225
|
def criar(dados):
|
|
221
|
-
|
|
226
|
+
# O cliente é um agente de IA falando JSON cru — um tipo errado aqui
|
|
227
|
+
# (titulo como lista, por exemplo) não pode virar AttributeError sem
|
|
228
|
+
# resposta HTTP nenhuma; melhor um 400 com o motivo.
|
|
229
|
+
ferramenta_bruta = dados.get("ferramenta")
|
|
230
|
+
if ferramenta_bruta is not None and not isinstance(ferramenta_bruta, str):
|
|
231
|
+
raise ValueError("ferramenta precisa ser texto")
|
|
232
|
+
ferramenta = (ferramenta_bruta or "prisma").lower()
|
|
222
233
|
if ferramenta not in POR_SLUG:
|
|
223
234
|
return None
|
|
224
|
-
|
|
235
|
+
|
|
236
|
+
titulo_bruto = dados.get("titulo")
|
|
237
|
+
if titulo_bruto is not None and not isinstance(titulo_bruto, str):
|
|
238
|
+
raise ValueError("titulo precisa ser texto")
|
|
239
|
+
titulo = (titulo_bruto or "Sem título").strip()
|
|
240
|
+
|
|
241
|
+
doc = dados.get("doc")
|
|
242
|
+
if doc is not None and not isinstance(doc, dict):
|
|
243
|
+
raise ValueError("doc precisa ser um objeto")
|
|
244
|
+
|
|
225
245
|
arq_id = ferramenta + "-" + slugify(titulo) + "-" + uuid.uuid4().hex[:5]
|
|
226
246
|
return gravar({
|
|
227
247
|
"id": arq_id,
|
|
228
248
|
"ferramenta": ferramenta,
|
|
229
249
|
"titulo": titulo,
|
|
230
|
-
"doc":
|
|
250
|
+
"doc": doc or exemplo(ferramenta),
|
|
231
251
|
})
|
|
232
252
|
|
|
233
253
|
|
|
@@ -311,6 +331,23 @@ def manual():
|
|
|
311
331
|
|
|
312
332
|
# ----------------------------------------------------------------- handler --
|
|
313
333
|
|
|
334
|
+
def seguro(metodo):
|
|
335
|
+
"""Qualquer exceção não prevista na rota vira 500 em JSON, não conexão
|
|
336
|
+
fechada sem resposta nenhuma — era o comportamento padrão do
|
|
337
|
+
BaseHTTPRequestHandler para exceção não tratada num do_*, e o cliente (o
|
|
338
|
+
agente de IA) não tem como interpretar uma conexão que só morre."""
|
|
339
|
+
@functools.wraps(metodo)
|
|
340
|
+
def envolto(self, *a, **kw):
|
|
341
|
+
try:
|
|
342
|
+
return metodo(self, *a, **kw)
|
|
343
|
+
except Exception as e:
|
|
344
|
+
try:
|
|
345
|
+
self.js({"erro": "erro interno: %s" % e}, 500)
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
return envolto
|
|
349
|
+
|
|
350
|
+
|
|
314
351
|
class Handler(SimpleHTTPRequestHandler):
|
|
315
352
|
server_version = "PrimoCode/1.0"
|
|
316
353
|
|
|
@@ -360,6 +397,7 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
360
397
|
|
|
361
398
|
# -------------------------------------------------------------- rotas --
|
|
362
399
|
|
|
400
|
+
@seguro
|
|
363
401
|
def do_GET(self):
|
|
364
402
|
p = urlparse(self.path).path
|
|
365
403
|
|
|
@@ -426,10 +464,17 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
426
464
|
if faixa:
|
|
427
465
|
m = re.match(r"bytes=(\d*)-(\d*)", faixa)
|
|
428
466
|
if m:
|
|
429
|
-
if m.group(1):
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
fim
|
|
467
|
+
if not m.group(1) and m.group(2):
|
|
468
|
+
# "bytes=-500" é sufixo HTTP válido: os ÚLTIMOS 500 bytes,
|
|
469
|
+
# não os primeiros 500 — sem este caso à parte, group(2)
|
|
470
|
+
# virava fim absoluto e o pedido servia o pedaço errado.
|
|
471
|
+
inicio = max(0, tamanho - int(m.group(2)))
|
|
472
|
+
fim = tamanho - 1
|
|
473
|
+
else:
|
|
474
|
+
if m.group(1):
|
|
475
|
+
inicio = int(m.group(1))
|
|
476
|
+
if m.group(2):
|
|
477
|
+
fim = min(int(m.group(2)), tamanho - 1)
|
|
433
478
|
if inicio > fim or inicio >= tamanho:
|
|
434
479
|
self.send_response(416)
|
|
435
480
|
self.send_header("Content-Range", "bytes */%d" % tamanho)
|
|
@@ -455,13 +500,18 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
455
500
|
self.wfile.write(pedaco)
|
|
456
501
|
restante -= len(pedaco)
|
|
457
502
|
|
|
503
|
+
@seguro
|
|
458
504
|
def do_POST(self):
|
|
459
|
-
|
|
505
|
+
rota = urlparse(self.path).path
|
|
506
|
+
if rota == "/api/midia":
|
|
460
507
|
corpo, status = guardar_midia(self.corpo())
|
|
461
508
|
return self.js(corpo, status)
|
|
462
509
|
|
|
463
|
-
if
|
|
464
|
-
|
|
510
|
+
if rota == "/api/arquivos":
|
|
511
|
+
try:
|
|
512
|
+
a = criar(self.corpo())
|
|
513
|
+
except ValueError as e:
|
|
514
|
+
return self.js({"erro": str(e)}, 400)
|
|
465
515
|
if not a:
|
|
466
516
|
return self.js({"erro": "ferramenta inválida",
|
|
467
517
|
"validas": list(POR_SLUG)}, 400)
|
|
@@ -475,6 +525,7 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
475
525
|
}, 201)
|
|
476
526
|
return self.js({"erro": "não encontrado"}, 404)
|
|
477
527
|
|
|
528
|
+
@seguro
|
|
478
529
|
def do_PUT(self):
|
|
479
530
|
m = re.fullmatch(r"/api/arquivos/([^/]+)", urlparse(self.path).path)
|
|
480
531
|
if not m:
|
|
@@ -483,13 +534,21 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
483
534
|
if not a:
|
|
484
535
|
return self.js({"erro": "não encontrado"}, 404)
|
|
485
536
|
dados = self.corpo()
|
|
537
|
+
# Mesma validação de criar(): sem isso, um PUT com titulo/doc do tipo
|
|
538
|
+
# errado grava sujeira que só quebra depois, no consumo (export etc).
|
|
539
|
+
if "titulo" in dados and dados["titulo"] is not None and not isinstance(dados["titulo"], str):
|
|
540
|
+
return self.js({"erro": "titulo precisa ser texto"}, 400)
|
|
541
|
+
if "doc" in dados and dados["doc"] is not None and not isinstance(dados["doc"], dict):
|
|
542
|
+
return self.js({"erro": "doc precisa ser um objeto"}, 400)
|
|
486
543
|
for chave in ("titulo", "doc"):
|
|
487
544
|
if chave in dados:
|
|
488
545
|
a[chave] = dados[chave]
|
|
489
546
|
return self.js(gravar(a))
|
|
490
547
|
|
|
548
|
+
@seguro
|
|
491
549
|
def do_DELETE(self):
|
|
492
|
-
|
|
550
|
+
rota = urlparse(self.path).path
|
|
551
|
+
m = re.fullmatch(r"/api/midia/([^/]+)", rota)
|
|
493
552
|
if m:
|
|
494
553
|
nome = unquote(m.group(1))
|
|
495
554
|
alvo = os.path.join(MIDIA, nome)
|
|
@@ -498,7 +557,7 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
498
557
|
return self.js({"ok": True})
|
|
499
558
|
return self.js({"erro": "não encontrado"}, 404)
|
|
500
559
|
|
|
501
|
-
m = re.fullmatch(r"/api/arquivos/([^/]+)",
|
|
560
|
+
m = re.fullmatch(r"/api/arquivos/([^/]+)", rota)
|
|
502
561
|
c = caminho(m.group(1)) if m else None
|
|
503
562
|
if c and os.path.exists(c):
|
|
504
563
|
os.remove(c)
|
|
@@ -535,7 +594,7 @@ def csv_celula(valor):
|
|
|
535
594
|
if valor == int(valor):
|
|
536
595
|
valor = int(valor)
|
|
537
596
|
s = "" if valor is None else str(valor)
|
|
538
|
-
return '"%s"' % s.replace('"', '""') if re.search(r'[",\n]', s) else s
|
|
597
|
+
return '"%s"' % s.replace('"', '""') if re.search(r'[",\n\r]', s) else s
|
|
539
598
|
|
|
540
599
|
|
|
541
600
|
# ------------------------------------------------- fórmulas (espelha o JS) --
|
|
@@ -602,7 +661,11 @@ def calcular_aba(aba):
|
|
|
602
661
|
linhas = [list(l) for l in aba.get("linhas", [])]
|
|
603
662
|
|
|
604
663
|
def celula(c, l, prof):
|
|
605
|
-
|
|
664
|
+
# l/c negativos (referência tipo "A0", que vira índice -1) não podem
|
|
665
|
+
# passar: índice negativo do Python "funciona" contando do fim da
|
|
666
|
+
# lista, e a última linha da planilha aparecia como resposta de uma
|
|
667
|
+
# referência inválida em vez de virar 0.
|
|
668
|
+
if prof > 24 or l < 0 or c < 0 or l >= len(linhas) or c >= len(linhas[l]):
|
|
606
669
|
return 0
|
|
607
670
|
bruto = linhas[l][c]
|
|
608
671
|
if isinstance(bruto, str) and bruto.startswith("="):
|
|
@@ -642,7 +705,14 @@ def calcular_aba(aba):
|
|
|
642
705
|
elif parte:
|
|
643
706
|
# qualquer outro argumento é uma expressão: resolve antes
|
|
644
707
|
vals.append(_num(avaliar(parte, prof)))
|
|
645
|
-
|
|
708
|
+
try:
|
|
709
|
+
# MAIOR/MENOR estouram IndexError com k maior que o intervalo;
|
|
710
|
+
# POT estoura OverflowError com base/expoente grandes demais.
|
|
711
|
+
# Uma fórmula ruim vira "#ERRO" NESSA célula, não derruba a
|
|
712
|
+
# exportação inteira (mesma regra do eval() logo abaixo).
|
|
713
|
+
return repr(f(vals))
|
|
714
|
+
except Exception:
|
|
715
|
+
return "#ERRO"
|
|
646
716
|
|
|
647
717
|
# repete até esgotar: resolve de dentro para fora em fórmula aninhada
|
|
648
718
|
e, voltas = expr, 0
|
|
Binary file
|