primocode 9.5.0 → 9.7.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 +2 -2
- package/bin/primocode.js +168 -40
- package/lib/act.js +128 -9
- package/lib/catalogo.js +20 -1
- package/lib/config.js +29 -0
- package/lib/cursor.js +129 -0
- package/lib/memoria-conversa.js +20 -11
- package/lib/tools.js +117 -20
- package/lib/ui.js +3 -0
- package/lib/voz.js +22 -3
- package/package.json +2 -2
- package/skills/projeto-no-github/SKILL.md +32 -0
- package/skills/publicar-na-internet/SKILL.md +36 -0
- package/voz/primo_voz/cursor.py +593 -0
- package/voz/primo_voz/orbe.py +4 -1
package/lib/catalogo.js
CHANGED
|
@@ -206,6 +206,25 @@ function escreveArquivo(tool) {
|
|
|
206
206
|
return ESCREVEM.has(tool);
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
/* ── O QUE FAZ NASCER UMA PASTA DE PROJETO ──────────────────────────────
|
|
210
|
+
* "Ela deve saber quando criar pasta do projeto e quando não criar — só vai
|
|
211
|
+
* criar quando fizer código." `escreve` era largo demais para isso: um print
|
|
212
|
+
* de tela, uma narração do estúdio e um vídeo também escrevem arquivo, e cada
|
|
213
|
+
* um deles criava um projeto com o nome da frase. Aqui só o que é CÓDIGO:
|
|
214
|
+
* escrever ou editar arquivo, montar um app, delegar a um especialista, e o
|
|
215
|
+
* comando de terminal que cria arquivo (o agente que faz `npm create` ou
|
|
216
|
+
* `mkdir` estava pondo o projeto solto na raiz de ~/primocode). */
|
|
217
|
+
const CRIAM_PROJETO = new Set(['write_file', 'edit_file', 'multi_edit', 'construir_app', 'spawn_agent']);
|
|
218
|
+
const SHELL_QUE_CRIA = /\b(mkdir|touch|git\s+clone|npm\s+(init|create)|npx\s+(create-|degit|nuxi\s+init)|yarn\s+create|pnpm\s+(create|dlx\s+create-)|bun\s+create|cargo\s+(new|init)|python3?\s+-m\s+venv|django-admin\s+startproject|rails\s+new|flutter\s+create|dotnet\s+new|composer\s+create-project|expo\s+init)\b|(^|[^<>&|0-9])>>?\s*[\w./~-]/;
|
|
219
|
+
|
|
220
|
+
function criaProjeto(tool, args) {
|
|
221
|
+
if (CRIAM_PROJETO.has(tool)) return true;
|
|
222
|
+
if (tool === 'run_shell' || tool === 'run_in_new_terminal') {
|
|
223
|
+
return SHELL_QUE_CRIA.test(String((args && (args.command || args.comando)) || ''));
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
209
228
|
|
|
210
229
|
/**
|
|
211
230
|
* As ferramentas de nome mais parecido com o que foi chamado.
|
|
@@ -244,5 +263,5 @@ function parecidasCom(nome) {
|
|
|
244
263
|
return pontos.slice(0, 6).map((x) => x.t);
|
|
245
264
|
}
|
|
246
265
|
|
|
247
|
-
module.exports = {
|
|
266
|
+
module.exports = { criaProjeto,
|
|
248
267
|
parecidasCom, GRUPOS, TODAS, ESCREVEM, escreveArquivo };
|
package/lib/config.js
CHANGED
|
@@ -21,6 +21,7 @@ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
|
21
21
|
const HISTORY_FILE = path.join(CONFIG_DIR, 'history.jsonl');
|
|
22
22
|
const CONTA_FILE = path.join(CONFIG_DIR, 'conta.json');
|
|
23
23
|
const GENERAL_MEMORY = path.join(ROOT_DIR, 'memory-geral.md');
|
|
24
|
+
const PROJETO_ATIVO_FILE = path.join(CONFIG_DIR, 'projeto-ativo.json');
|
|
24
25
|
|
|
25
26
|
/* O token da conta, lido DIRETO do arquivo.
|
|
26
27
|
*
|
|
@@ -309,7 +310,35 @@ function appendHistory(entry) {
|
|
|
309
310
|
} catch {}
|
|
310
311
|
}
|
|
311
312
|
|
|
313
|
+
/* ── O PROJETO EM QUE SE ESTAVA ─────────────────────────────────────────
|
|
314
|
+
* "Eu peço o jogo de carro, depois peço pra editar ou continuar, e ele cria
|
|
315
|
+
* outro projeto." Nada sobrevivia entre uma sessão e outra: cada abertura
|
|
316
|
+
* começava em ~/primocode, e o "continua" virava projeto novo. Agora o projeto
|
|
317
|
+
* ativo fica guardado aqui, num arquivo só dele (a config tem lista fechada de
|
|
318
|
+
* campos), e a próxima sessão continua nele. Vale por uma semana: depois
|
|
319
|
+
* disso, abrir o PrimoCode e cair num projeto esquecido seria a surpresa. */
|
|
320
|
+
function lembrarProjeto(dir, origem = 'projeto') {
|
|
321
|
+
try {
|
|
322
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
323
|
+
fs.writeFileSync(PROJETO_ATIVO_FILE, JSON.stringify({ dir, origem, quando: Date.now() }, null, 2), 'utf-8');
|
|
324
|
+
} catch { /* lembrar é conveniência: falhou, a sessão segue igual */ }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function projetoLembrado(maxDias = 7) {
|
|
328
|
+
try {
|
|
329
|
+
const j = JSON.parse(fs.readFileSync(PROJETO_ATIVO_FILE, 'utf-8'));
|
|
330
|
+
if (!j || !j.dir || !fs.existsSync(j.dir) || !fs.statSync(j.dir).isDirectory()) return null;
|
|
331
|
+
if (Date.now() - Number(j.quando || 0) > maxDias * 86400000) return null;
|
|
332
|
+
return { dir: j.dir, origem: j.origem || 'projeto' };
|
|
333
|
+
} catch { return null; }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function esquecerProjeto() {
|
|
337
|
+
try { fs.rmSync(PROJETO_ATIVO_FILE, { force: true }); } catch { /* nada a esquecer */ }
|
|
338
|
+
}
|
|
339
|
+
|
|
312
340
|
module.exports = {
|
|
341
|
+
lembrarProjeto, projetoLembrado, esquecerProjeto, PROJETO_ATIVO_FILE,
|
|
313
342
|
loadConfig, saveConfig, appendHistory,
|
|
314
343
|
CONFIG_DIR, CONFIG_FILE, HISTORY_FILE, ROOT_DIR, GENERAL_MEMORY,
|
|
315
344
|
ensureStructure, slugify, stripAccents,
|
package/lib/cursor.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cursor.js — o lado Node do cursor do Primo (só macOS).
|
|
3
|
+
*
|
|
4
|
+
* "Quando ele tá no modo, eu consigo ver o cursor dele, o mouse, ver ele
|
|
5
|
+
* digitando, abrindo as coisas, ver ele fazer as coisas em tempo real."
|
|
6
|
+
*
|
|
7
|
+
* O cursor mora em voz/primo_voz/cursor.py: um processo Python que mexe no
|
|
8
|
+
* mouse e no teclado de verdade (Quartz) e desenha a seta dele por cima. Aqui
|
|
9
|
+
* é a ponta de cá: subir o processo na PRIMEIRA ação no computador (não antes
|
|
10
|
+
* — quem só escreve código nunca paga por ele), mandar os comandos e esperar
|
|
11
|
+
* cada um terminar.
|
|
12
|
+
*
|
|
13
|
+
* ── POR QUE ELE, E NÃO O cliclick ───────────────────────────────────────
|
|
14
|
+
* O cliclick precisava de Homebrew, e sem ele TODO clique do agente falhava —
|
|
15
|
+
* foi o que fez "mostra o teu mouse" virar uma página HTML com coordenadas. O
|
|
16
|
+
* Quartz vem com o sistema; o que falta instalar é o PyObjC, pelo pip, e isso
|
|
17
|
+
* este módulo faz sozinho.
|
|
18
|
+
*
|
|
19
|
+
* Fora do macOS, ou se o Python/PyObjC não estiverem lá, `disponivel()` diz
|
|
20
|
+
* que não e o desktop.js segue com o caminho de sempre.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const { spawn, execFileSync } = require('child_process');
|
|
27
|
+
|
|
28
|
+
const PASTA = path.join(__dirname, '..', 'voz');
|
|
29
|
+
|
|
30
|
+
let proc = null; // o processo do cursor, quando de pé
|
|
31
|
+
let pronto = null; // Promise<boolean> da subida
|
|
32
|
+
let fila = new Map(); // id → { resolve, timer }
|
|
33
|
+
let proximo = 1;
|
|
34
|
+
let semCursor = null; // motivo, quando não dá para ter cursor nesta máquina
|
|
35
|
+
|
|
36
|
+
function pythonComQuartz() {
|
|
37
|
+
let py = null;
|
|
38
|
+
try { py = require('./voz.js').python(); } catch { /* sem voz.js: sem cursor */ }
|
|
39
|
+
if (!py) return { py: null };
|
|
40
|
+
try {
|
|
41
|
+
execFileSync(py, ['-c', 'import Quartz, AppKit, PyObjCTools'], { stdio: 'ignore', timeout: 20000 });
|
|
42
|
+
} catch {
|
|
43
|
+
return { py, tem: false };
|
|
44
|
+
}
|
|
45
|
+
// O Vision (ler a tela) é um extra: sem ele o mouse funciona igual.
|
|
46
|
+
try {
|
|
47
|
+
execFileSync(py, ['-c', 'import Vision'], { stdio: 'ignore', timeout: 20000 });
|
|
48
|
+
return { py, tem: true, vision: true };
|
|
49
|
+
} catch {
|
|
50
|
+
return { py, tem: true, vision: false };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Sobe o processo do cursor, instalando o PyObjC se faltar. Uma vez. */
|
|
55
|
+
function subir() {
|
|
56
|
+
if (pronto) return pronto;
|
|
57
|
+
pronto = (async () => {
|
|
58
|
+
if (process.platform !== 'darwin') { semCursor = 'só no macOS'; return false; }
|
|
59
|
+
let { py, tem } = pythonComQuartz();
|
|
60
|
+
if (!py) { semCursor = 'sem Python 3'; return false; }
|
|
61
|
+
if (!tem) {
|
|
62
|
+
const voz = require('./voz.js');
|
|
63
|
+
const r = await voz.instalar(py, ['pyobjc-framework-Quartz', 'pyobjc-framework-Cocoa', 'pyobjc-framework-Vision']);
|
|
64
|
+
if (!r.ok || !pythonComQuartz().tem) { semCursor = `não instalou o PyObjC: ${r.error || ''}`; return false; }
|
|
65
|
+
} else if (!pythonComQuartz().vision) {
|
|
66
|
+
// Falhar aqui não impede o cursor; só "clicar pelo texto" fica sem OCR.
|
|
67
|
+
await require('./voz.js').instalar(py, ['pyobjc-framework-Vision']).catch(() => {});
|
|
68
|
+
}
|
|
69
|
+
return await new Promise((resolve) => {
|
|
70
|
+
proc = spawn(py, ['-m', 'primo_voz.cursor'], {
|
|
71
|
+
cwd: PASTA,
|
|
72
|
+
env: { ...process.env, PYTHONPATH: PASTA, PYTHONUNBUFFERED: '1' },
|
|
73
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
74
|
+
});
|
|
75
|
+
let buffer = '';
|
|
76
|
+
let subiu = false;
|
|
77
|
+
proc.stdout.on('data', (d) => {
|
|
78
|
+
buffer += d.toString();
|
|
79
|
+
const linhas = buffer.split('\n');
|
|
80
|
+
buffer = linhas.pop();
|
|
81
|
+
for (const linha of linhas) {
|
|
82
|
+
let msg;
|
|
83
|
+
try { msg = JSON.parse(linha); } catch { continue; }
|
|
84
|
+
if (msg.pronto && !subiu) { subiu = true; resolve(true); continue; }
|
|
85
|
+
const espera = fila.get(msg.id);
|
|
86
|
+
if (espera) { clearTimeout(espera.timer); fila.delete(msg.id); espera.resolve(msg); }
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
const morreu = () => {
|
|
90
|
+
proc = null;
|
|
91
|
+
pronto = null; // a próxima ação tenta subir de novo
|
|
92
|
+
for (const [, e] of fila) { clearTimeout(e.timer); e.resolve({ ok: false, error: 'o cursor fechou' }); }
|
|
93
|
+
fila = new Map();
|
|
94
|
+
if (!subiu) { semCursor = 'o processo do cursor não subiu'; resolve(false); }
|
|
95
|
+
};
|
|
96
|
+
proc.on('exit', morreu);
|
|
97
|
+
proc.on('error', morreu);
|
|
98
|
+
setTimeout(() => { if (!subiu) { semCursor = 'o cursor demorou demais para abrir'; resolve(false); } }, 20000);
|
|
99
|
+
// O cursor não pode segurar o PrimoCode aberto na saída.
|
|
100
|
+
proc.unref();
|
|
101
|
+
proc.stdout.unref && proc.stdout.unref();
|
|
102
|
+
});
|
|
103
|
+
})();
|
|
104
|
+
return pronto;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Manda um comando e espera a resposta (a animação inteira, no caso do mouse). */
|
|
108
|
+
async function mandar(cmd, args = {}, tetoMs = 60000) {
|
|
109
|
+
const ok = await subir();
|
|
110
|
+
if (!ok || !proc) return { ok: false, semCursor: true, error: semCursor || 'cursor indisponível' };
|
|
111
|
+
const id = proximo++;
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
const timer = setTimeout(() => { fila.delete(id); resolve({ ok: false, error: 'o cursor não respondeu a tempo' }); }, tetoMs);
|
|
114
|
+
fila.set(id, { resolve, timer });
|
|
115
|
+
try { proc.stdin.write(JSON.stringify({ id, cmd, ...args }) + '\n'); }
|
|
116
|
+
catch (e) { clearTimeout(timer); fila.delete(id); resolve({ ok: false, error: e.message }); }
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Esta máquina pode ter o cursor do Primo? (sem subir nada) */
|
|
121
|
+
function disponivel() {
|
|
122
|
+
return process.platform === 'darwin' && process.env.PRIMOCODE_SEM_CURSOR !== '1';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function fechar() {
|
|
126
|
+
if (proc) { try { proc.stdin.end(); } catch { /* já fechou */ } }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { disponivel, mandar, subir, fechar };
|
package/lib/memoria-conversa.js
CHANGED
|
@@ -17,10 +17,17 @@
|
|
|
17
17
|
* O custo fica limitado por construção: a entrada do resumo é sempre
|
|
18
18
|
* "resumo de antes + o que aconteceu desde então", nunca a conversa inteira.
|
|
19
19
|
*
|
|
20
|
-
* ──
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* ── RESUMO A CADA MENSAGEM (23/09/2026) ──────────────────────────────────
|
|
21
|
+
* "Pra não demorar tanto as respostas, quero que ela gere um resumo a cada
|
|
22
|
+
* mensagem, pra daí na próxima mensagem ela ler só esse resumo, não precisa
|
|
23
|
+
* ler a conversa inteira."
|
|
24
|
+
*
|
|
25
|
+
* Antes, o resumo só nascia quando a conversa passava de ~700 tokens, e as
|
|
26
|
+
* últimas quatro mensagens iam sempre cruas. Agora TODA resposta é dobrada no
|
|
27
|
+
* resumo logo depois de terminar (em segundo plano, sem segurar o prompt), e o
|
|
28
|
+
* pedido seguinte leva só o resumo — mais o que, por falha ou pressa, ainda
|
|
29
|
+
* não entrou nele. O resumo é que carrega o "de onde parei": ele é instruído a
|
|
30
|
+
* guardar o último pedido, o que foi feito e o projeto em que se estava.
|
|
24
31
|
*
|
|
25
32
|
* Este arquivo é puro — quem fala com o servidor é quem chama `atualizar`.
|
|
26
33
|
* Assim o teste mede a política inteira sem rede e sem chave de API.
|
|
@@ -34,10 +41,10 @@ function tokensDe(x) {
|
|
|
34
41
|
return Math.ceil(texto.length / 4);
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
//
|
|
38
|
-
const CRUAS =
|
|
39
|
-
//
|
|
40
|
-
const LIMITE_TOKENS =
|
|
44
|
+
// Nenhuma mensagem fica de fora do resumo: ele é atualizado a cada rodada.
|
|
45
|
+
const CRUAS = 0;
|
|
46
|
+
// Qualquer coisa nova já vale o resumo — é o que deixa o pedido seguinte leve.
|
|
47
|
+
const LIMITE_TOKENS = 1;
|
|
41
48
|
|
|
42
49
|
/** Estado inicial — o que o CLI guarda entre um pedido e outro. */
|
|
43
50
|
function nova() {
|
|
@@ -103,13 +110,15 @@ function inicioSeguro(todas, alvo) {
|
|
|
103
110
|
* O resumo entra como mensagem de sistema, não como fala do usuário — o modelo
|
|
104
111
|
* precisa saber que aquilo é memória, e não algo que alguém acabou de dizer.
|
|
105
112
|
*/
|
|
106
|
-
function paraEnviar(memoria, history) {
|
|
113
|
+
function paraEnviar(memoria, history, contexto = '') {
|
|
107
114
|
const todas = history || [];
|
|
108
115
|
if (!memoria || !memoria.resumo) return todas.slice(inicioSeguro(todas, todas.length - 20));
|
|
109
116
|
|
|
110
|
-
|
|
117
|
+
// Só o que ainda NÃO entrou no resumo vai cru — normalmente, nada.
|
|
118
|
+
const cruas = todas.slice(inicioSeguro(todas, Math.min(memoria.resumidasAte, todas.length)));
|
|
119
|
+
const extra = contexto ? `\n\n${contexto}` : '';
|
|
111
120
|
return [
|
|
112
|
-
{ role: 'system', content: `Resumo da conversa até aqui (memória, não é fala de ninguém):\n${memoria.resumo}` },
|
|
121
|
+
{ role: 'system', content: `Resumo da conversa até aqui (memória, não é fala de ninguém):\n${memoria.resumo}${extra}` },
|
|
113
122
|
...cruas,
|
|
114
123
|
];
|
|
115
124
|
}
|
package/lib/tools.js
CHANGED
|
@@ -15,6 +15,37 @@ const os = require('os');
|
|
|
15
15
|
const { exec, execFile } = require('child_process');
|
|
16
16
|
const browser = require('./browser');
|
|
17
17
|
const desktop = require('./desktop');
|
|
18
|
+
const cursor = require('./cursor');
|
|
19
|
+
|
|
20
|
+
/* ── O CURSOR DO PRIMO NO MAC ────────────────────────────────────────────
|
|
21
|
+
* No macOS, mouse e teclado passam pelo cursor dele (lib/cursor.js): a mesma
|
|
22
|
+
* ação acontece de verdade, e a pessoa VÊ a seta dele indo até lá, clicando,
|
|
23
|
+
* digitando. Sem o cursor (outro sistema, ou o Python não subiu), vale o
|
|
24
|
+
* caminho antigo do desktop.js. */
|
|
25
|
+
async function peloCursor(cmd, args, reserva) {
|
|
26
|
+
if (!cursor.disponivel()) return reserva();
|
|
27
|
+
const r = await cursor.mandar(cmd, args);
|
|
28
|
+
if (r.semCursor) return reserva();
|
|
29
|
+
if (!r.ok) return { ok: false, error: r.error || 'o cursor não conseguiu', ...(r.permissao === false ? { permissao: false } : {}) };
|
|
30
|
+
return { ok: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function numero(v) { const n = Number(v); return Number.isFinite(n) ? n : null; }
|
|
34
|
+
|
|
35
|
+
/** Os textos visíveis na tela, pelo OCR do cursor, no formato dos elementos. */
|
|
36
|
+
async function textosDaTela() {
|
|
37
|
+
if (!cursor.disponivel()) return { ok: false, error: 'ler a tela é só no macOS' };
|
|
38
|
+
const r = await cursor.mandar('ler_tela', {}, 30000);
|
|
39
|
+
if (!r.ok) return { ok: false, error: r.error || 'não consegui ler a tela' };
|
|
40
|
+
return { ok: true, elementos: (r.textos || []).map((t) => ({ nome: t.texto, tipo: 'texto na tela', x: t.x, y: t.y })) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function clicarPeloCursor(x, y, { botao = 'left', duplo = false, rotulo } = {}) {
|
|
44
|
+
const [nx, ny] = [numero(x), numero(y)];
|
|
45
|
+
if (nx == null || ny == null) return { ok: false, error: `Coordenada inválida: x=${x}, y=${y}` };
|
|
46
|
+
return peloCursor('clicar', { x: nx, y: ny, botao: botao === 'right' ? 'direito' : 'esquerdo', duplo, rotulo },
|
|
47
|
+
() => (duplo ? desktop.doubleClick(nx, ny) : desktop.click(nx, ny, botao)));
|
|
48
|
+
}
|
|
18
49
|
const referencia = require('./referencia');
|
|
19
50
|
const estudio = require('./estudio');
|
|
20
51
|
const cfg = require('./config');
|
|
@@ -538,19 +569,46 @@ async function despachar(name, args, ctx) {
|
|
|
538
569
|
// resolver dentro do projeto, não do cwd real do processo.
|
|
539
570
|
return desktop.screenshot(resolveInProject(args.path || `desktop-${Date.now()}.png`, projectDir));
|
|
540
571
|
case 'desktop_click':
|
|
541
|
-
return
|
|
542
|
-
case 'desktop_move':
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
return
|
|
572
|
+
return clicarPeloCursor(args.x, args.y, { botao: args.button || 'left', rotulo: 'clicando' });
|
|
573
|
+
case 'desktop_move': {
|
|
574
|
+
const [x, y] = [numero(args.x), numero(args.y)];
|
|
575
|
+
if (x == null || y == null) return { ok: false, error: `Coordenada inválida: x=${args.x}, y=${args.y}` };
|
|
576
|
+
return peloCursor('mover', { x, y }, () => desktop.move(x, y));
|
|
577
|
+
}
|
|
578
|
+
case 'desktop_drag': {
|
|
579
|
+
const c4 = [args.from_x, args.from_y, args.to_x, args.to_y].map(numero);
|
|
580
|
+
if (c4.some((v) => v == null)) return { ok: false, error: 'Coordenada inválida no drag' };
|
|
581
|
+
return peloCursor('arrastar', { x1: c4[0], y1: c4[1], x2: c4[2], y2: c4[3], rotulo: 'arrastando' },
|
|
582
|
+
() => desktop.drag(...c4));
|
|
583
|
+
}
|
|
546
584
|
case 'desktop_type':
|
|
547
|
-
return
|
|
548
|
-
|
|
549
|
-
|
|
585
|
+
return peloCursor('digitar', { texto: args.text || '', rotulo: 'digitando' },
|
|
586
|
+
() => desktop.type(args.text || ''));
|
|
587
|
+
case 'desktop_key': {
|
|
588
|
+
const r = await peloCursor('tecla', { combo: args.key || '', rotulo: String(args.key || '') },
|
|
589
|
+
() => desktop.key(args.key));
|
|
590
|
+
// Tecla SOLTA que o cursor não conhece: o desktop.js sabe mais
|
|
591
|
+
// nomes. Combinação com tecla desconhecida ("cmd+xyz") NÃO cai
|
|
592
|
+
// lá: o osascript digitaria x, y e z com Command apertado —
|
|
593
|
+
// recortar e desfazer no app da frente.
|
|
594
|
+
if (!r.ok && /tecla desconhecida/.test(r.error || '') && !String(args.key || '').includes('+')) {
|
|
595
|
+
return desktop.key(args.key);
|
|
596
|
+
}
|
|
597
|
+
return r;
|
|
598
|
+
}
|
|
550
599
|
case 'desktop_scroll':
|
|
551
|
-
return
|
|
552
|
-
|
|
600
|
+
return peloCursor('rolar', { dy: args.dy != null ? Number(args.dy) : 300, rotulo: 'rolando' },
|
|
601
|
+
() => desktop.scroll(args.dy != null ? args.dy : 300));
|
|
602
|
+
case 'desktop_size': {
|
|
603
|
+
// No Mac, em PONTOS — a mesma unidade dos cliques e das posições
|
|
604
|
+
// dos botões. O system_profiler dava pixels físicos (2880 num
|
|
605
|
+
// Retina de 1440), e o clique caía no lugar errado.
|
|
606
|
+
if (cursor.disponivel()) {
|
|
607
|
+
const t = await cursor.mandar('tamanho');
|
|
608
|
+
if (t.ok) return { ok: true, width: t.largura, height: t.altura, unidade: 'pontos' };
|
|
609
|
+
}
|
|
553
610
|
return desktop.getScreenSize();
|
|
611
|
+
}
|
|
554
612
|
/* ── O ESTÚDIO ─────────────────────────────────────────────────
|
|
555
613
|
* Sete ferramentas, e nenhuma delas fala com servidor nenhum: o
|
|
556
614
|
* Studio antigo subia um Python em localhost:7777 porque tinha
|
|
@@ -583,21 +641,60 @@ async function despachar(name, args, ctx) {
|
|
|
583
641
|
return estudio.ferramentaSite(args);
|
|
584
642
|
|
|
585
643
|
case 'desktop_double_click':
|
|
586
|
-
return
|
|
587
|
-
case 'desktop_open_app':
|
|
588
|
-
|
|
644
|
+
return clicarPeloCursor(args.x, args.y, { duplo: true, rotulo: 'abrindo' });
|
|
645
|
+
case 'desktop_open_app': {
|
|
646
|
+
const app = args.app || args.name || '';
|
|
647
|
+
if (cursor.disponivel()) await cursor.mandar('rotulo', { texto: `abrindo ${app}` });
|
|
648
|
+
return desktop.openApp(app);
|
|
649
|
+
}
|
|
589
650
|
case 'desktop_windows':
|
|
590
651
|
return desktop.listWindows();
|
|
591
652
|
case 'desktop_focus':
|
|
592
653
|
return desktop.focusWindow(args.window || args.title || '');
|
|
593
|
-
case 'desktop_elements':
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
654
|
+
case 'desktop_elements': {
|
|
655
|
+
const lidos = desktop.uiElements(args.window);
|
|
656
|
+
if (!cursor.disponivel()) return lidos;
|
|
657
|
+
// No Mac, junto dos botões da acessibilidade vão os TEXTOS
|
|
658
|
+
// que aparecem na tela (OCR), com a posição de cada um.
|
|
659
|
+
const naTela = await textosDaTela();
|
|
660
|
+
if (!naTela.ok) return lidos.ok ? { ...lidos, telaIndisponivel: naTela.error } : lidos;
|
|
661
|
+
return { ...(lidos.ok ? lidos : { ok: true, elementos: [] }),
|
|
662
|
+
textosNaTela: naTela.elementos.slice(0, 120).map((e) => ({ texto: e.nome, x: e.x, y: e.y })) };
|
|
663
|
+
}
|
|
664
|
+
case 'desktop_click_element': {
|
|
665
|
+
if (!cursor.disponivel()) {
|
|
666
|
+
return desktop.clickElement(args.name || args.text || '', {
|
|
667
|
+
window: args.window, double: !!args.double, button: args.button,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
// Mesma escolha do desktop.clickElement, mas o clique vai pelo
|
|
671
|
+
// cursor: a pessoa vê a seta ir até o botão pelo nome.
|
|
672
|
+
const pedido = String(args.name || args.text || '').trim();
|
|
673
|
+
if (!pedido) return { ok: false, error: 'Diga o nome do elemento (o texto do botão, ícone ou campo).' };
|
|
674
|
+
const lidos = desktop.uiElements(args.window);
|
|
675
|
+
let alvo = lidos.ok ? desktop.escolherElemento(lidos.elementos, pedido) : null;
|
|
676
|
+
/* A acessibilidade não achou (ou o app não expõe botões): procura
|
|
677
|
+
o TEXTO na tela, pelo OCR do cursor. É o que faz "clica em
|
|
678
|
+
Salvar" funcionar em qualquer app, como no Jarvis. */
|
|
679
|
+
let naTela = null;
|
|
680
|
+
if (!alvo) {
|
|
681
|
+
naTela = await textosDaTela();
|
|
682
|
+
if (naTela.ok) alvo = desktop.escolherElemento(naTela.elementos, pedido);
|
|
683
|
+
}
|
|
684
|
+
if (!alvo) {
|
|
685
|
+
return { ok: false,
|
|
686
|
+
error: `Nenhum elemento parecido com "${pedido}"${lidos.ok ? ` na janela "${lidos.janela}"` : ''}, nem como texto na tela.`,
|
|
687
|
+
elementosDisponiveis: [...(lidos.ok ? lidos.elementos : []), ...((naTela && naTela.ok) ? naTela.elementos : [])]
|
|
688
|
+
.slice(0, 50).map((e) => e.nome),
|
|
689
|
+
...(naTela && !naTela.ok ? { telaIndisponivel: naTela.error } : {}),
|
|
690
|
+
comoCorrigir: 'Chame de novo com um nome de "elementosDisponiveis", copiado literalmente.' };
|
|
691
|
+
}
|
|
692
|
+
const r = await clicarPeloCursor(alvo.x, alvo.y, {
|
|
693
|
+
botao: args.button || 'left', duplo: !!args.double, rotulo: `clicando em ${alvo.nome}`.slice(0, 44),
|
|
600
694
|
});
|
|
695
|
+
if (!r.ok) return r;
|
|
696
|
+
return { ok: true, clicado: alvo.nome, tipo: alvo.tipo, x: alvo.x, y: alvo.y, duplo: !!args.double };
|
|
697
|
+
}
|
|
601
698
|
|
|
602
699
|
default:
|
|
603
700
|
/* O MODELO PRECISA CONSEGUIR SE CORRIGIR AQUI.
|
package/lib/ui.js
CHANGED
|
@@ -319,6 +319,9 @@ const ROTULOS = {
|
|
|
319
319
|
function actionHeader(name, args) {
|
|
320
320
|
const label = ROTULOS[name] || name;
|
|
321
321
|
let argStr = args ? (args.consulta || args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.app || args.window || args.name || args.ferramenta || args.titulo || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || args.arquivo || args.id || JSON.stringify(args).slice(0,60)) : '';
|
|
322
|
+
// Campo que veio como objeto (o modelo às vezes manda {"window": {}})
|
|
323
|
+
// virava "[object Object]" na tela.
|
|
324
|
+
if (argStr && typeof argStr === 'object') argStr = JSON.stringify(argStr).slice(0, 60);
|
|
322
325
|
argStr = String(argStr || '').replace(/\s+/g, ' ').trim();
|
|
323
326
|
// Uma tool sem argumento nenhum caía no JSON.stringify e desenhava "{}"
|
|
324
327
|
// ao lado do nome — ruído que não diz nada.
|
package/lib/voz.js
CHANGED
|
@@ -32,10 +32,23 @@ const { spawn, execFileSync } = require('child_process');
|
|
|
32
32
|
|
|
33
33
|
const PASTA = path.join(__dirname, '..', 'voz');
|
|
34
34
|
|
|
35
|
+
/* ── A DICA DO COMPUTADOR ────────────────────────────────────────────────
|
|
36
|
+
* Duas razões para existir, e as duas foram medidas:
|
|
37
|
+
*
|
|
38
|
+
* 1. O servidor só oferece as ferramentas desktop_* ao modelo quando o pedido
|
|
39
|
+
* cita palavras como "mouse", "teclado", "ícone", "atalho", "janela" (o
|
|
40
|
+
* grupo "computador" em lib/orcamento-entrada.js do servidor). "Abre o
|
|
41
|
+
* Safari e pesquisa X" não cita nenhuma — e o modelo ficava sem as mãos.
|
|
42
|
+
* Esta dica as cita de propósito.
|
|
43
|
+
* 2. "Mostra o teu mouse" virou uma página HTML com as coordenadas do mouse
|
|
44
|
+
* DA PESSOA. O certo é mexer o cursor dele, que ela vê na tela.
|
|
45
|
+
*/
|
|
46
|
+
const DICA_COMPUTADOR = `[COMPUTADOR] Você pode usar o mouse, o teclado, as janelas e os ícones do computador da pessoa com as ferramentas desktop_* (desktop_open_app, desktop_click_element, desktop_click, desktop_type, desktop_key para atalho, desktop_scroll). Cada movimento aparece na tela como o SEU cursor. Para abrir um app, use desktop_open_app; para clicar em algo, prefira desktop_click_element pelo nome (leia os nomes com desktop_elements); coordenadas são em pontos da tela (desktop_size). Se pedirem para "mostrar o seu mouse/cursor", MEXA o cursor com desktop_move — nunca crie uma página ou arquivo para isso.`;
|
|
47
|
+
|
|
35
48
|
/* A instrução de FORMA. Ela não muda o que o agente faz — muda como ele
|
|
36
49
|
conta. O que está proibido aqui é o que não existe em voz: marcador,
|
|
37
50
|
caminho, código, e parágrafo que ninguém segura de ouvido. */
|
|
38
|
-
const COMO_FALAR = `[MODO VOZ] A sua resposta vai ser LIDA EM VOZ ALTA
|
|
51
|
+
const COMO_FALAR = `[MODO VOZ] A sua resposta vai ser LIDA EM VOZ ALTA. A pessoa está olhando a tela: ela vê o SEU cursor (o mouse e o teclado do computador dela, que você controla) se mexendo em tempo real.
|
|
39
52
|
|
|
40
53
|
Responda em no máximo 3 frases curtas, em português falado. Nada de lista com
|
|
41
54
|
marcador, nada de caminho de arquivo, nada de bloco de código, nada de
|
|
@@ -44,6 +57,8 @@ markdown — nada disso existe no ouvido de alguém.
|
|
|
44
57
|
Diga o que você FEZ e o que mudou, não como. Se algo deu errado, diga em uma
|
|
45
58
|
frase o que foi e o que você precisa.
|
|
46
59
|
|
|
60
|
+
${DICA_COMPUTADOR}
|
|
61
|
+
|
|
47
62
|
Faça o trabalho inteiro normalmente, com as ferramentas de sempre. O que muda
|
|
48
63
|
é só o jeito de contar no fim.`;
|
|
49
64
|
|
|
@@ -412,7 +427,11 @@ print(json.dumps(desperta.baixar_modelo()))
|
|
|
412
427
|
const OPCIONAIS = () => [
|
|
413
428
|
['faster_whisper', 'faster-whisper', 'a transcrição local (Whisper)'],
|
|
414
429
|
['vosk', 'vosk', 'o reconhecedor local do "ei, primo"'],
|
|
415
|
-
...(process.platform === 'darwin' ? [
|
|
430
|
+
...(process.platform === 'darwin' ? [
|
|
431
|
+
['AppKit', 'pyobjc-framework-Cocoa', 'a orb transparente do macOS'],
|
|
432
|
+
['Quartz', 'pyobjc-framework-Quartz', 'o cursor do Primo na tela'],
|
|
433
|
+
['Vision', 'pyobjc-framework-Vision', 'ler o texto da tela (clicar pelo nome em qualquer app)'],
|
|
434
|
+
] : []),
|
|
416
435
|
];
|
|
417
436
|
|
|
418
437
|
function temModulo(py, modulo) {
|
|
@@ -556,4 +575,4 @@ async function preparar(aoFalar = () => {}) {
|
|
|
556
575
|
return { ok: true, python: py, esperando, avisos };
|
|
557
576
|
}
|
|
558
577
|
|
|
559
|
-
module.exports = { conferir, instalar, preparar, porqueOPipFalhou, abrir, conferirEspera, baixarEspera, python, COMO_FALAR, PASTA };
|
|
578
|
+
module.exports = { DICA_COMPUTADOR, conferir, instalar, preparar, porqueOPipFalhou, abrir, conferirEspera, baixarEspera, python, COMO_FALAR, PASTA };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.7.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). 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": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
nome: Projeto no GitHub
|
|
3
|
+
descricao: criar o repositório, subir o projeto e abrir pull request pelo gh
|
|
4
|
+
quando: github, repositório, repositorio, repo, subir pro github, sobe pro github, pull request, pr, gh, versionar, push, clonar
|
|
5
|
+
autor: PrimoCode
|
|
6
|
+
---
|
|
7
|
+
Tudo pelo `gh` (GitHub CLI) e pelo `git`, rodando no terminal do projeto.
|
|
8
|
+
|
|
9
|
+
**Antes de tudo:** `gh auth status`. Sem login, diga à pessoa para rodar
|
|
10
|
+
`gh auth login` e pare — NUNCA peça token ou senha no chat.
|
|
11
|
+
|
|
12
|
+
**Projeto novo no GitHub:**
|
|
13
|
+
1. Se não houver `.git`: `git init -b main`.
|
|
14
|
+
2. `.gitignore` antes do primeiro commit (node_modules, dist, build, .env,
|
|
15
|
+
.DS_Store, __pycache__, .venv). Confira com `git status` que nenhum segredo
|
|
16
|
+
ou pasta pesada vai junto.
|
|
17
|
+
3. README curto: o que é, como rodar.
|
|
18
|
+
4. `git add -A` e `git commit -m "<o que o projeto é>"`.
|
|
19
|
+
5. `gh repo create <nome-do-projeto> --private --source . --push` (privado
|
|
20
|
+
por padrão; público só se a pessoa pedir). Diga o link do repositório.
|
|
21
|
+
|
|
22
|
+
**Mudança num repositório que já existe (pull request):**
|
|
23
|
+
1. `git status` e `git pull` primeiro. Branch nova com nome que diz a
|
|
24
|
+
mudança: `git switch -c feat/<assunto>`.
|
|
25
|
+
2. Rode o que o projeto roda (teste, lint, build) antes de commitar.
|
|
26
|
+
3. Commit com a mensagem dizendo o que MUDA; `git push -u origin <branch>`.
|
|
27
|
+
4. `gh pr create --fill` (ou `--title` e `--body` dizendo o que e por quê).
|
|
28
|
+
Diga o link do PR.
|
|
29
|
+
|
|
30
|
+
**Nunca** faça `push --force` em `main`, nunca apague branch dos outros, nunca
|
|
31
|
+
commite `.env`. Conflito: resolva lendo os dois lados, não escolhendo um às
|
|
32
|
+
cegas.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
nome: Publicar na internet
|
|
3
|
+
descricao: colocar o projeto no ar com um link público, conferido depois de subir
|
|
4
|
+
quando: publicar, publica, deploy, colocar no ar, coloca no ar, no ar, subir o site, sobe o site, hospedar, hospedagem, link público, link publico, vercel, netlify, github pages, railway, dominio, domínio
|
|
5
|
+
autor: PrimoCode
|
|
6
|
+
---
|
|
7
|
+
Publicar é terminar com um LINK que abre. Nada de "pronto, publicado" sem ter
|
|
8
|
+
aberto o link.
|
|
9
|
+
|
|
10
|
+
**1. Descubra o que é o projeto** (leia a pasta): página estática (index.html
|
|
11
|
+
solto), app com build (package.json com `build`: Vite, React, Next) ou servidor
|
|
12
|
+
(Express, Python). Rode o build antes (`npm install` e `npm run build`) e
|
|
13
|
+
conserte o que quebrar — publicar build quebrado é publicar tela branca.
|
|
14
|
+
|
|
15
|
+
**2. Escolha onde, nesta ordem, conferindo o login ANTES:**
|
|
16
|
+
- **Vercel** (sites e Next): `npx --yes vercel whoami`. Logado →
|
|
17
|
+
`npx --yes vercel --prod --yes` na pasta do projeto. A URL de produção sai no
|
|
18
|
+
fim da saída.
|
|
19
|
+
- **Netlify** (sites estáticos): `npx --yes netlify-cli status`. Logado →
|
|
20
|
+
`npx --yes netlify-cli deploy --prod --dir <pasta do build ou .>`.
|
|
21
|
+
- **GitHub Pages** (só estático, sem build de servidor): `gh auth status`.
|
|
22
|
+
Logado → publique o repositório (ver a skill "Projeto no GitHub") e ligue o
|
|
23
|
+
Pages com `gh api -X POST repos/<dono>/<repo>/pages -f "source[branch]=main" -f "source[path]=/"`.
|
|
24
|
+
- **Servidor** (API, backend): Railway (`npx --yes @railway/cli whoami`, depois
|
|
25
|
+
`railway up`) ou Render. Precisa de conta; não invente.
|
|
26
|
+
|
|
27
|
+
**Sem login em nenhum:** NÃO peça senha nem token no chat. Diga à pessoa, em
|
|
28
|
+
uma frase, o comando para ela entrar (ex.: `npx vercel login`) e pare ali. É
|
|
29
|
+
ela quem entra na conta dela.
|
|
30
|
+
|
|
31
|
+
**3. Confira:** abra a URL publicada com browser_open e leia a página. Só diga
|
|
32
|
+
que está no ar se ela abriu com o conteúdo certo. Termine dizendo o LINK.
|
|
33
|
+
|
|
34
|
+
**Nunca** publique `.env`, chave ou senha: confira se `.gitignore` e o
|
|
35
|
+
`.vercelignore`/`.netlifyignore` os deixam de fora. Variáveis secretas vão no
|
|
36
|
+
painel do serviço (ou `vercel env add`), não no código.
|