primocode 9.6.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/lib/act.js +102 -1
- package/lib/cursor.js +11 -2
- package/lib/tools.js +32 -6
- package/lib/voz.js +1 -0
- 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 +68 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v9.
|
|
1
|
+
# PrimoCode v9.7.0
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
4
|
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v9.
|
|
12
|
+
▐▛███▜▌ Primo Code v9.7.0
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/lib/act.js
CHANGED
|
@@ -429,12 +429,48 @@ function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
|
|
|
429
429
|
return 'Parei sem concluir e sem explicação do modelo. Tente pedir de novo, em partes menores.';
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
+
/* ── O MAPA DO PROJETO ───────────────────────────────────────────────────
|
|
433
|
+
* "Continua o jogo" num projeto de vinte arquivos: o agente começava às
|
|
434
|
+
* cegas, gastava uma volta listando a pasta — ou, pior, escrevia um index.html
|
|
435
|
+
* novo por cima do que existia. Agora ele recebe, junto da memória, a lista dos
|
|
436
|
+
* arquivos do projeto (até 80, sem node_modules nem build), e sabe de saída
|
|
437
|
+
* onde está cada coisa. Só em pasta de projeto: a raiz ~/primocode lista
|
|
438
|
+
* outros projetos, não este. */
|
|
439
|
+
const PULAR_NO_MAPA = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt', 'out',
|
|
440
|
+
'.cache', 'coverage', '__pycache__', '.venv', 'venv', '.turbo', '.vercel', '.idea', '.vscode']);
|
|
441
|
+
|
|
442
|
+
function mapaDoProjeto(dir, limite = 80) {
|
|
443
|
+
const path = require('path');
|
|
444
|
+
const fs = require('fs');
|
|
445
|
+
if (!dir || path.resolve(dir) === path.resolve(cfg.ROOT_DIR)) return '';
|
|
446
|
+
const achados = [];
|
|
447
|
+
const andar = (atual, fundo) => {
|
|
448
|
+
if (fundo > 4 || achados.length >= limite) return;
|
|
449
|
+
let itens = [];
|
|
450
|
+
try { itens = fs.readdirSync(atual, { withFileTypes: true }); } catch { return; }
|
|
451
|
+
itens.sort((a, b) => (a.isDirectory() === b.isDirectory() ? a.name.localeCompare(b.name) : a.isDirectory() ? 1 : -1));
|
|
452
|
+
for (const it of itens) {
|
|
453
|
+
if (achados.length >= limite) return;
|
|
454
|
+
if (PULAR_NO_MAPA.has(it.name) || (it.name.startsWith('.') && it.name !== '.env.example')) continue;
|
|
455
|
+
const cheio = path.join(atual, it.name);
|
|
456
|
+
if (it.isDirectory()) andar(cheio, fundo + 1);
|
|
457
|
+
else if (it.name !== 'memory.md') achados.push(path.relative(dir, cheio));
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
andar(dir, 0);
|
|
461
|
+
if (!achados.length) return '';
|
|
462
|
+
const mais = achados.length >= limite ? `\n… (mais arquivos — use glob para achar)` : '';
|
|
463
|
+
return `Arquivos que JÁ existem neste projeto (edite-os em vez de recriar):\n${achados.join('\n')}${mais}`;
|
|
464
|
+
}
|
|
465
|
+
|
|
432
466
|
function buildMemoryContext(projectDir, projectName) {
|
|
433
467
|
const parts = [];
|
|
434
468
|
const geral = cfg.readGeneralMemory();
|
|
435
469
|
if (geral.trim()) parts.push(`[memory-geral.md]\n${geral}`);
|
|
436
470
|
const projeto = cfg.readProjectMemory(projectDir);
|
|
437
471
|
if (projeto.trim()) parts.push(`[${projectName || 'projeto'}/memory.md]\n${projeto}`);
|
|
472
|
+
const mapa = mapaDoProjeto(projectDir);
|
|
473
|
+
if (mapa) parts.push(`[${projectName || 'projeto'}/arquivos]\n${mapa}`);
|
|
438
474
|
return parts.join('\n\n');
|
|
439
475
|
}
|
|
440
476
|
|
|
@@ -460,6 +496,9 @@ async function runAct(userPrompt, opts) {
|
|
|
460
496
|
Antes ela ia colada no texto, e a memória do projeto enchia de
|
|
461
497
|
"[MODO VOZ] A sua resposta vai ser lida em voz alta…". */
|
|
462
498
|
instrucao = '',
|
|
499
|
+
/* Quem abre a página para conferir antes do "pronto". Injetável pelo
|
|
500
|
+
mesmo motivo do `motor`: o teste mede a regra sem abrir um Chrome. */
|
|
501
|
+
medirPagina = (dir) => require('./pipeline.js').medirNoNavegador(dir),
|
|
463
502
|
} = opts || {};
|
|
464
503
|
|
|
465
504
|
// A pasta de projeto só nasce quando o agente for mesmo MEXER EM ARQUIVO.
|
|
@@ -481,6 +520,37 @@ async function runAct(userPrompt, opts) {
|
|
|
481
520
|
};
|
|
482
521
|
const falhasPendentes = [];
|
|
483
522
|
let tentativasDeEncerrar = 0;
|
|
523
|
+
/* ── CONFERIR ANTES DE DIZER "PRONTO" ───────────────────────────────
|
|
524
|
+
* Mexeu em página (html, css, js) nesta tarefa? Antes de aceitar o
|
|
525
|
+
* `finish`, o PrimoCode abre a página num navegador invisível e olha: tela
|
|
526
|
+
* em branco, erro de JavaScript, recurso que não carregou. Achou problema,
|
|
527
|
+
* a tarefa volta para o agente com o relato — uma vez só, para não virar
|
|
528
|
+
* laço. É a diferença entre "pronto!" e "pronto, e abre". */
|
|
529
|
+
let mexeuNaPagina = false; // mexeu desde a última conferência?
|
|
530
|
+
let conferencias = 0; // até duas: entregar, e conferir o conserto
|
|
531
|
+
let avisoDaPagina = ''; // o que ainda estava quebrado na última
|
|
532
|
+
/* Abre a página e devolve o relato quando é para VOLTAR (achou problema e
|
|
533
|
+
ainda cabe uma volta); null quando é para entregar. O que ficar
|
|
534
|
+
quebrado na última conferência fica em `avisoDaPagina`, e a entrega sai
|
|
535
|
+
com ele — nunca "pronto" seco sobre uma página com erro. Vale para os
|
|
536
|
+
dois jeitos de terminar: a ferramenta finish e a resposta em texto. */
|
|
537
|
+
const conferirPagina = async () => {
|
|
538
|
+
if (subagent || !mexeuNaPagina || conferencias >= 2 || PARAR) return null;
|
|
539
|
+
conferencias++;
|
|
540
|
+
mexeuNaPagina = false;
|
|
541
|
+
let medida = null;
|
|
542
|
+
try {
|
|
543
|
+
fala.pensamento('abrindo a página para conferir antes de entregar', profundidade);
|
|
544
|
+
medida = await medirPagina(pastaAtual);
|
|
545
|
+
} catch { medida = null; }
|
|
546
|
+
if (!medida || !medida.testou) return null;
|
|
547
|
+
avisoDaPagina = medida.limpo ? ''
|
|
548
|
+
: String(medida.relato || '').split('\n').filter((l) => /·|BRANCO/.test(l)).slice(0, 3).join(' ');
|
|
549
|
+
if (medida.limpo) { fala.pensamento('a página abriu sem erro', profundidade); return null; }
|
|
550
|
+
return conferencias >= 2 ? null : medida.relato;
|
|
551
|
+
};
|
|
552
|
+
const comAviso = (texto) => (avisoDaPagina
|
|
553
|
+
? `${texto}\n\nAtenção: abri a página e ela ainda tem problema — ${avisoDaPagina.trim()}` : texto);
|
|
484
554
|
// Quem já foi chamado, e quantas vezes. Ver lib/repeticao.js.
|
|
485
555
|
const jaChamado = repeticao.novo();
|
|
486
556
|
/* O livro deste pedido: arquivos, comandos, links, testes e falhas. Sai
|
|
@@ -822,6 +892,21 @@ async function runAct(userPrompt, opts) {
|
|
|
822
892
|
finalText = '';
|
|
823
893
|
continue;
|
|
824
894
|
}
|
|
895
|
+
// Terminou falando, sem chamar finish: confere a página do mesmo jeito.
|
|
896
|
+
if (finalText) {
|
|
897
|
+
const relato = await conferirPagina();
|
|
898
|
+
if (relato) {
|
|
899
|
+
messages.push({ role: 'assistant', content: finalText });
|
|
900
|
+
messages.push({
|
|
901
|
+
role: 'user',
|
|
902
|
+
content: `AINDA NÃO. Abri a página para conferir e ela tem problema:\n${relato}\n\n`
|
|
903
|
+
+ 'Conserte (leia o arquivo antes de editar) e finalize de novo.',
|
|
904
|
+
});
|
|
905
|
+
finalText = '';
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
finalText = comAviso(finalText);
|
|
909
|
+
}
|
|
825
910
|
if (!subagent) {
|
|
826
911
|
if (fluxo) fluxo.fechar();
|
|
827
912
|
else fala.resposta(finalText);
|
|
@@ -873,6 +958,18 @@ async function runAct(userPrompt, opts) {
|
|
|
873
958
|
|
|
874
959
|
for (const tc of toolCalls) {
|
|
875
960
|
if (PARAR) break;
|
|
961
|
+
if (tc.name === 'finish') {
|
|
962
|
+
const relato = await conferirPagina();
|
|
963
|
+
if (relato) {
|
|
964
|
+
messages.push({
|
|
965
|
+
role: 'tool',
|
|
966
|
+
tool_call_id: tc.id,
|
|
967
|
+
content: `AINDA NÃO. Abri a página para conferir e ela tem problema:\n${relato}\n\n`
|
|
968
|
+
+ 'Conserte (leia o arquivo antes de editar) e chame finish de novo.',
|
|
969
|
+
});
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
876
973
|
if (tc.name === 'finish') {
|
|
877
974
|
// Encerrar com falha pendente é como o agente dizia "pronto!"
|
|
878
975
|
// sem ter mudado nada. Devolvemos a tarefa para ele.
|
|
@@ -887,7 +984,7 @@ async function runAct(userPrompt, opts) {
|
|
|
887
984
|
continue;
|
|
888
985
|
}
|
|
889
986
|
algoRodou = true;
|
|
890
|
-
finished = (tc.arguments && tc.arguments.summary) || 'Concluído.';
|
|
987
|
+
finished = comAviso((tc.arguments && tc.arguments.summary) || 'Concluído.');
|
|
891
988
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: 'ok' });
|
|
892
989
|
continue;
|
|
893
990
|
}
|
|
@@ -987,6 +1084,10 @@ async function runAct(userPrompt, opts) {
|
|
|
987
1084
|
// nasceu, mudou, rodou e falhou. Narrar sozinho não basta — numa
|
|
988
1085
|
// tarefa de trinta ações, a primeira já saiu da tela.
|
|
989
1086
|
registro.anotar(livro, tc.name, tc.arguments, result);
|
|
1087
|
+
if (result && result.ok !== false && catalogo.criaProjeto(tc.name, tc.arguments)
|
|
1088
|
+
&& /\.(html?|css|m?jsx?|tsx?|vue|svelte)$/i.test(String((tc.arguments && tc.arguments.path) || ''))) {
|
|
1089
|
+
mexeuNaPagina = true;
|
|
1090
|
+
}
|
|
990
1091
|
|
|
991
1092
|
// Uma escrita bem-sucedida limpa a dívida — mas só a do MESMO
|
|
992
1093
|
// arquivo. Escrever algo sem relação (ou um run_shell qualquer)
|
package/lib/cursor.js
CHANGED
|
@@ -39,10 +39,16 @@ function pythonComQuartz() {
|
|
|
39
39
|
if (!py) return { py: null };
|
|
40
40
|
try {
|
|
41
41
|
execFileSync(py, ['-c', 'import Quartz, AppKit, PyObjCTools'], { stdio: 'ignore', timeout: 20000 });
|
|
42
|
-
return { py, tem: true };
|
|
43
42
|
} catch {
|
|
44
43
|
return { py, tem: false };
|
|
45
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
|
+
}
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
/** Sobe o processo do cursor, instalando o PyObjC se faltar. Uma vez. */
|
|
@@ -54,8 +60,11 @@ function subir() {
|
|
|
54
60
|
if (!py) { semCursor = 'sem Python 3'; return false; }
|
|
55
61
|
if (!tem) {
|
|
56
62
|
const voz = require('./voz.js');
|
|
57
|
-
const r = await voz.instalar(py, ['pyobjc-framework-Quartz', 'pyobjc-framework-Cocoa']);
|
|
63
|
+
const r = await voz.instalar(py, ['pyobjc-framework-Quartz', 'pyobjc-framework-Cocoa', 'pyobjc-framework-Vision']);
|
|
58
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(() => {});
|
|
59
68
|
}
|
|
60
69
|
return await new Promise((resolve) => {
|
|
61
70
|
proc = spawn(py, ['-m', 'primo_voz.cursor'], {
|
package/lib/tools.js
CHANGED
|
@@ -32,6 +32,14 @@ async function peloCursor(cmd, args, reserva) {
|
|
|
32
32
|
|
|
33
33
|
function numero(v) { const n = Number(v); return Number.isFinite(n) ? n : null; }
|
|
34
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
|
+
|
|
35
43
|
async function clicarPeloCursor(x, y, { botao = 'left', duplo = false, rotulo } = {}) {
|
|
36
44
|
const [nx, ny] = [numero(x), numero(y)];
|
|
37
45
|
if (nx == null || ny == null) return { ok: false, error: `Coordenada inválida: x=${x}, y=${y}` };
|
|
@@ -643,8 +651,16 @@ async function despachar(name, args, ctx) {
|
|
|
643
651
|
return desktop.listWindows();
|
|
644
652
|
case 'desktop_focus':
|
|
645
653
|
return desktop.focusWindow(args.window || args.title || '');
|
|
646
|
-
case 'desktop_elements':
|
|
647
|
-
|
|
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
|
+
}
|
|
648
664
|
case 'desktop_click_element': {
|
|
649
665
|
if (!cursor.disponivel()) {
|
|
650
666
|
return desktop.clickElement(args.name || args.text || '', {
|
|
@@ -656,11 +672,21 @@ async function despachar(name, args, ctx) {
|
|
|
656
672
|
const pedido = String(args.name || args.text || '').trim();
|
|
657
673
|
if (!pedido) return { ok: false, error: 'Diga o nome do elemento (o texto do botão, ícone ou campo).' };
|
|
658
674
|
const lidos = desktop.uiElements(args.window);
|
|
659
|
-
|
|
660
|
-
|
|
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
|
+
}
|
|
661
684
|
if (!alvo) {
|
|
662
|
-
return { ok: false,
|
|
663
|
-
|
|
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 } : {}),
|
|
664
690
|
comoCorrigir: 'Chame de novo com um nome de "elementosDisponiveis", copiado literalmente.' };
|
|
665
691
|
}
|
|
666
692
|
const r = await clicarPeloCursor(alvo.x, alvo.y, {
|
package/lib/voz.js
CHANGED
|
@@ -430,6 +430,7 @@ const OPCIONAIS = () => [
|
|
|
430
430
|
...(process.platform === 'darwin' ? [
|
|
431
431
|
['AppKit', 'pyobjc-framework-Cocoa', 'a orb transparente do macOS'],
|
|
432
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)'],
|
|
433
434
|
] : []),
|
|
434
435
|
];
|
|
435
436
|
|
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/projeto.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.
|
package/voz/primo_voz/cursor.py
CHANGED
|
@@ -171,6 +171,72 @@ def digitar(texto: str) -> None:
|
|
|
171
171
|
time.sleep(0.008)
|
|
172
172
|
|
|
173
173
|
|
|
174
|
+
# ── Ler a tela (OCR) ──────────────────────────────────────────────────────
|
|
175
|
+
# "Clicar no texto X" em QUALQUER app, e não só nos que expõem botões pela
|
|
176
|
+
# acessibilidade: fotografa a tela e reconhece o texto com o Vision do macOS
|
|
177
|
+
# (o mesmo do Jarvis, motor/screen.py). Cada trecho volta com o CENTRO em
|
|
178
|
+
# pontos da tela — a mesma unidade dos cliques.
|
|
179
|
+
|
|
180
|
+
def tela_liberada() -> bool:
|
|
181
|
+
try:
|
|
182
|
+
return bool(Quartz.CGPreflightScreenCaptureAccess())
|
|
183
|
+
except Exception:
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def ler_tela(largura: float, altura: float, arquivo: str = "") -> dict:
|
|
188
|
+
try:
|
|
189
|
+
import Vision
|
|
190
|
+
except Exception:
|
|
191
|
+
return {"ok": False, "error": "falta o pyobjc-framework-Vision para ler a tela"}
|
|
192
|
+
if arquivo:
|
|
193
|
+
# Só para o teste: lê uma imagem do disco em vez da tela.
|
|
194
|
+
from Foundation import NSURL
|
|
195
|
+
fonte = Quartz.CGImageSourceCreateWithURL(NSURL.fileURLWithPath_(arquivo), None)
|
|
196
|
+
img = Quartz.CGImageSourceCreateImageAtIndex(fonte, 0, None) if fonte else None
|
|
197
|
+
else:
|
|
198
|
+
if not tela_liberada():
|
|
199
|
+
try:
|
|
200
|
+
Quartz.CGRequestScreenCaptureAccess()
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
return {"ok": False, "permissaoTela": False,
|
|
204
|
+
"error": "o macOS não deixou ver a tela. Libere o Terminal (ou o app de onde você "
|
|
205
|
+
"abriu o PrimoCode) em Ajustes do Sistema > Privacidade e Segurança > "
|
|
206
|
+
"Gravação de Tela, e peça de novo."}
|
|
207
|
+
img = Quartz.CGWindowListCreateImage(Quartz.CGRectInfinite, Quartz.kCGWindowListOptionOnScreenOnly,
|
|
208
|
+
Quartz.kCGNullWindowID, Quartz.kCGWindowImageDefault)
|
|
209
|
+
if img is None:
|
|
210
|
+
return {"ok": False, "error": "não consegui fotografar a tela"}
|
|
211
|
+
|
|
212
|
+
pedido = Vision.VNRecognizeTextRequest.alloc().init()
|
|
213
|
+
pedido.setRecognitionLevel_(Vision.VNRequestTextRecognitionLevelAccurate)
|
|
214
|
+
pedido.setUsesLanguageCorrection_(True)
|
|
215
|
+
try:
|
|
216
|
+
pedido.setRecognitionLanguages_(["pt-BR", "en-US"])
|
|
217
|
+
except Exception:
|
|
218
|
+
pass
|
|
219
|
+
leitor = Vision.VNImageRequestHandler.alloc().initWithCGImage_options_(img, None)
|
|
220
|
+
ok, erro = leitor.performRequests_error_([pedido], None)
|
|
221
|
+
if not ok:
|
|
222
|
+
return {"ok": False, "error": f"o Vision não leu a tela: {erro}"}
|
|
223
|
+
textos = []
|
|
224
|
+
for obs in (pedido.results() or []):
|
|
225
|
+
candidatos = obs.topCandidates_(1)
|
|
226
|
+
if not candidatos:
|
|
227
|
+
continue
|
|
228
|
+
texto = str(candidatos[0].string()).strip()
|
|
229
|
+
if not texto:
|
|
230
|
+
continue
|
|
231
|
+
bb = obs.boundingBox() # normalizado, origem embaixo à esquerda
|
|
232
|
+
cx = (bb.origin.x + bb.size.width / 2) * largura
|
|
233
|
+
cy = (1 - (bb.origin.y + bb.size.height / 2)) * altura
|
|
234
|
+
textos.append({"texto": texto, "x": round(cx), "y": round(cy),
|
|
235
|
+
"confianca": round(float(candidatos[0].confidence()), 2)})
|
|
236
|
+
textos.sort(key=lambda t: (t["y"], t["x"]))
|
|
237
|
+
return {"ok": True, "textos": textos}
|
|
238
|
+
|
|
239
|
+
|
|
174
240
|
# ── A janela do cursor ──────────────────────────────────────────────────────
|
|
175
241
|
|
|
176
242
|
class VistaCursor(NSView):
|
|
@@ -431,6 +497,8 @@ def executar(cur: Cursor, msg: dict) -> dict:
|
|
|
431
497
|
return {"ok": ok}
|
|
432
498
|
if cmd == "tamanho":
|
|
433
499
|
return {"ok": True, "largura": int(cur.largura), "altura": int(cur.altura)}
|
|
500
|
+
if cmd == "ler_tela":
|
|
501
|
+
return ler_tela(cur.largura, cur.altura, str(msg.get("arquivo") or ""))
|
|
434
502
|
if cmd == "rotulo":
|
|
435
503
|
cur._ui(rotulo=str(msg.get("texto") or "")[:48], visivel=True, x=posicao()[0], y=posicao()[1])
|
|
436
504
|
return {"ok": True}
|