primocode 9.7.0 → 9.8.0-beta.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/README.md +5 -4
- package/bin/primocode.js +167 -11
- package/lib/act.js +30 -6
- package/lib/api.js +18 -0
- package/lib/browser.js +80 -6
- package/lib/local/cerebro.json +2116 -0
- package/lib/local/entrada.js +332 -0
- package/lib/local/motor.js +264 -0
- package/lib/local/provedor.js +151 -0
- package/lib/nome-projeto.js +97 -0
- package/lib/repeticao.js +65 -5
- package/lib/tools.js +15 -1
- package/lib/voz.js +7 -1
- package/package.json +2 -2
- package/voz/primo_voz/__main__.py +4 -2
- package/voz/primo_voz/transcrever.py +37 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provedor.js — a chave da própria pessoa, e com qual IA ela fala.
|
|
3
|
+
*
|
|
4
|
+
* "Fica batendo no limite. Coloca um comando /provider onde você seleciona
|
|
5
|
+
* se é Gemini, ChatGPT etc., cola a key e ela roda local com a key
|
|
6
|
+
* configurada." (23/09/2026)
|
|
7
|
+
*
|
|
8
|
+
* Com um provedor configurado (/api), o agente fala DIRETO com a API dele, daqui da
|
|
9
|
+
* máquina (lib/local/motor.js). O servidor e a cota compartilhada saem do
|
|
10
|
+
* caminho: o limite passa a ser o da conta da pessoa.
|
|
11
|
+
*
|
|
12
|
+
* ── ONDE A CHAVE MORA ────────────────────────────────────────────────────
|
|
13
|
+
* Em `~/.primocode/provedor.json`, permissão 600. Não no config.json: esse é
|
|
14
|
+
* o arquivo que alguém abre para conferir um ajuste ou cola num chamado.
|
|
15
|
+
*
|
|
16
|
+
* ── O QUE NUNCA APARECE ──────────────────────────────────────────────────
|
|
17
|
+
* A chave inteira — nem ao listar, nem num erro. Mostra-se o começo e o fim,
|
|
18
|
+
* o bastante para reconhecer qual é.
|
|
19
|
+
*
|
|
20
|
+
* Todos falam o formato de chat da OpenAI (/chat/completions, tools, stream):
|
|
21
|
+
* é o que Gemini, Claude, Groq, OpenRouter e DeepSeek aceitam também. Um
|
|
22
|
+
* cliente só, e o provedor é só o endereço, o modelo e a chave.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
const ARQUIVO = () => path.join(os.homedir(), '.primocode', 'provedor.json');
|
|
32
|
+
|
|
33
|
+
/* A ordem é a do menu. `modelos` é a preferência: ao colar a chave, fica o
|
|
34
|
+
primeiro que a conta da pessoa de fato tem (a lista vem da própria API). */
|
|
35
|
+
const PROVEDORES = {
|
|
36
|
+
gemini: {
|
|
37
|
+
nome: 'Gemini (Google)',
|
|
38
|
+
base: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
39
|
+
modelos: ['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.0-flash'],
|
|
40
|
+
onde: 'https://aistudio.google.com/apikey',
|
|
41
|
+
dica: 'tem plano grátis',
|
|
42
|
+
},
|
|
43
|
+
chatgpt: {
|
|
44
|
+
nome: 'ChatGPT (OpenAI)',
|
|
45
|
+
base: 'https://api.openai.com/v1',
|
|
46
|
+
modelos: ['gpt-5-mini', 'gpt-5', 'gpt-4.1-mini', 'gpt-4o-mini'],
|
|
47
|
+
onde: 'https://platform.openai.com/api-keys',
|
|
48
|
+
dica: 'pago por uso',
|
|
49
|
+
},
|
|
50
|
+
claude: {
|
|
51
|
+
nome: 'Claude (Anthropic)',
|
|
52
|
+
base: 'https://api.anthropic.com/v1',
|
|
53
|
+
modelos: ['claude-sonnet-5', 'claude-haiku-4-5-20251001', 'claude-opus-5-5'],
|
|
54
|
+
onde: 'https://console.anthropic.com/settings/keys',
|
|
55
|
+
dica: 'pago por uso',
|
|
56
|
+
},
|
|
57
|
+
groq: {
|
|
58
|
+
nome: 'Groq',
|
|
59
|
+
base: 'https://api.groq.com/openai/v1',
|
|
60
|
+
modelos: ['openai/gpt-oss-120b', 'llama-3.3-70b-versatile', 'openai/gpt-oss-20b'],
|
|
61
|
+
onde: 'https://console.groq.com/keys',
|
|
62
|
+
dica: 'tem plano grátis, muito rápido',
|
|
63
|
+
},
|
|
64
|
+
openrouter: {
|
|
65
|
+
nome: 'OpenRouter',
|
|
66
|
+
base: 'https://openrouter.ai/api/v1',
|
|
67
|
+
modelos: ['openai/gpt-oss-120b', 'google/gemini-2.5-flash', 'anthropic/claude-sonnet-5'],
|
|
68
|
+
onde: 'https://openrouter.ai/keys',
|
|
69
|
+
dica: 'vários modelos numa chave só',
|
|
70
|
+
},
|
|
71
|
+
deepseek: {
|
|
72
|
+
nome: 'DeepSeek',
|
|
73
|
+
base: 'https://api.deepseek.com/v1',
|
|
74
|
+
modelos: ['deepseek-chat'],
|
|
75
|
+
onde: 'https://platform.deepseek.com/api_keys',
|
|
76
|
+
dica: 'barato',
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** A chave, irreconhecível para quem olha por cima do ombro. */
|
|
81
|
+
function mascarar(chave) {
|
|
82
|
+
const k = String(chave || '');
|
|
83
|
+
if (k.length <= 10) return '••••';
|
|
84
|
+
return `${k.slice(0, 4)}…${k.slice(-4)}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let cache = null;
|
|
88
|
+
let cacheMtime = 0;
|
|
89
|
+
|
|
90
|
+
/** O provedor configurado, ou null (= segue pelo servidor, como sempre). */
|
|
91
|
+
function atual() {
|
|
92
|
+
if (process.env.PRIMOCODE_SEM_PROVEDOR === '1') return null;
|
|
93
|
+
let st;
|
|
94
|
+
try { st = fs.statSync(ARQUIVO()); } catch { cache = null; return null; }
|
|
95
|
+
if (cache && st.mtimeMs === cacheMtime) return cache;
|
|
96
|
+
try {
|
|
97
|
+
const j = JSON.parse(fs.readFileSync(ARQUIVO(), 'utf8'));
|
|
98
|
+
const p = PROVEDORES[j.provedor];
|
|
99
|
+
cache = p && j.chave ? { id: j.provedor, nome: p.nome, base: j.base || p.base, modelo: j.modelo || p.modelos[0], chave: j.chave } : null;
|
|
100
|
+
} catch { cache = null; }
|
|
101
|
+
cacheMtime = st.mtimeMs;
|
|
102
|
+
return cache;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function salvar({ provedor, chave, modelo }) {
|
|
106
|
+
if (!PROVEDORES[provedor]) throw new Error(`provedor desconhecido: ${provedor}`);
|
|
107
|
+
const arq = ARQUIVO();
|
|
108
|
+
fs.mkdirSync(path.dirname(arq), { recursive: true });
|
|
109
|
+
fs.writeFileSync(arq, JSON.stringify({ provedor, chave: String(chave).trim(), modelo }, null, 2), { mode: 0o600 });
|
|
110
|
+
try { fs.chmodSync(arq, 0o600); } catch { /* Windows */ }
|
|
111
|
+
cache = null;
|
|
112
|
+
return atual();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function trocarModelo(modelo) {
|
|
116
|
+
const a = atual();
|
|
117
|
+
if (!a) return null;
|
|
118
|
+
return salvar({ provedor: a.id, chave: a.chave, modelo });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function desligar() {
|
|
122
|
+
try { fs.unlinkSync(ARQUIVO()); } catch { /* já não havia */ }
|
|
123
|
+
cache = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A chave colada serve? Pergunta a lista de modelos à própria API. */
|
|
127
|
+
async function conferir(provedor, chave, { buscar = fetch } = {}) {
|
|
128
|
+
const p = PROVEDORES[provedor];
|
|
129
|
+
let r;
|
|
130
|
+
try {
|
|
131
|
+
r = await buscar(`${p.base}/models`, {
|
|
132
|
+
headers: { Authorization: `Bearer ${String(chave).trim()}` },
|
|
133
|
+
signal: AbortSignal.timeout(15000),
|
|
134
|
+
});
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return { ok: false, error: `não consegui falar com ${p.nome}: ${e.message}` };
|
|
137
|
+
}
|
|
138
|
+
if (r.status === 401 || r.status === 403) return { ok: false, error: `${p.nome} recusou a chave (${r.status}). Confira se copiou inteira.` };
|
|
139
|
+
if (!r.ok) return { ok: false, error: `${p.nome} respondeu ${r.status} ao conferir a chave.` };
|
|
140
|
+
let lista = [];
|
|
141
|
+
try {
|
|
142
|
+
const j = await r.json();
|
|
143
|
+
lista = (j.data || j.models || []).map((m) => String(m.id || m.name || '').replace(/^models\//, '')).filter(Boolean);
|
|
144
|
+
} catch { /* sem lista: fica a preferência */ }
|
|
145
|
+
// Nenhum da preferência na lista (ou lista vazia): fica o primeiro dela, e
|
|
146
|
+
// `/api modelo <nome>` troca.
|
|
147
|
+
const modelo = p.modelos.find((m) => lista.includes(m)) || p.modelos[0];
|
|
148
|
+
return { ok: true, modelo, disponiveis: lista };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { PROVEDORES, ARQUIVO, mascarar, atual, salvar, trocarModelo, desligar, conferir };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nome-projeto.js — quem dá nome à pasta é quem entendeu o pedido.
|
|
3
|
+
*
|
|
4
|
+
* "Quero que ele dê nome às pastas que criar. Ele escolhe o nome, ele
|
|
5
|
+
* entendeu o contexto, ele analisa os outros projetos." (23/09/2026)
|
|
6
|
+
*
|
|
7
|
+
* O nome saía de `deriveProjectName`: cortar as palavras de abertura da frase
|
|
8
|
+
* e ficar com até quatro. Funciona para "crie um jogo de carro", e erra tudo
|
|
9
|
+
* que não começa assim — "aquela ideia do cardápio que falei, bora fazer" vira
|
|
10
|
+
* `aquela-ideia-do-cardapio`. Quem sabe o que está sendo feito é o modelo.
|
|
11
|
+
*
|
|
12
|
+
* ── COMO ─────────────────────────────────────────────────────────────────
|
|
13
|
+
* Uma pergunta curta ao modelo, com o pedido e os nomes das pastas que já
|
|
14
|
+
* existem, para ele seguir o mesmo jeito (idioma, tamanho, prefixo). Ela
|
|
15
|
+
* roda em PARALELO com o trabalho: a pasta só nasce na primeira escrita, e
|
|
16
|
+
* até lá a resposta já chegou. Se não chegou, ou veio lixo, vale o nome de
|
|
17
|
+
* antes — nome pior é melhor que pasta nenhuma.
|
|
18
|
+
*
|
|
19
|
+
* Puro, menos `escolher`, que recebe quem pergunta de fora (teste sem rede).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const { slugify } = require('./config.js');
|
|
25
|
+
|
|
26
|
+
// Nome que não diz nada: o modelo devolveu isso, é como não ter respondido.
|
|
27
|
+
const GENERICOS = new Set(['projeto', 'project', 'app', 'site', 'novo', 'teste', 'test', 'meu-projeto', 'my-project']);
|
|
28
|
+
const TEMPO_MAXIMO_MS = 8000;
|
|
29
|
+
|
|
30
|
+
/** O jeito das pastas que a pessoa já tem, para o modelo seguir. */
|
|
31
|
+
function convencao(nomes) {
|
|
32
|
+
const lista = (nomes || []).filter((n) => n && !n.startsWith('.'));
|
|
33
|
+
if (!lista.length) return null;
|
|
34
|
+
const hifen = lista.filter((n) => n.includes('-')).length;
|
|
35
|
+
const sublinhado = lista.filter((n) => n.includes('_')).length;
|
|
36
|
+
const separador = sublinhado > hifen ? '_' : '-';
|
|
37
|
+
const palavras = lista.map((n) => n.split(/[-_]/).length);
|
|
38
|
+
const media = Math.round(palavras.reduce((a, b) => a + b, 0) / palavras.length);
|
|
39
|
+
return { separador, palavras: Math.min(4, Math.max(1, media)), exemplos: lista.slice(0, 12) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A pergunta. Curta: vai numa chamada à parte, e cada token conta. */
|
|
43
|
+
function pergunta(pedido, nomes) {
|
|
44
|
+
const c = convencao(nomes);
|
|
45
|
+
const jeito = c
|
|
46
|
+
? `Pastas que a pessoa já tem: ${c.exemplos.join(', ')}. Siga o mesmo jeito (idioma, tamanho, separador "${c.separador}"), `
|
|
47
|
+
+ 'e NÃO repita um nome que já existe.'
|
|
48
|
+
: 'Use kebab-case em português, de 1 a 3 palavras.';
|
|
49
|
+
return 'Dê um nome curto para a pasta do projeto deste pedido. O nome diz O QUE é '
|
|
50
|
+
+ '(ex.: site-padaria, jogo-de-carro, loja-roupas), nunca o que a pessoa pediu para fazer. '
|
|
51
|
+
+ `${jeito} Responda SÓ com o nome, sem aspas e sem explicação.\n\nPEDIDO: ${String(pedido || '').slice(0, 600)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Limpa a resposta do modelo. Devolve null quando não dá para confiar nela. */
|
|
55
|
+
function limpar(resposta, nomes) {
|
|
56
|
+
const primeira = String(resposta || '').trim().split('\n')[0]
|
|
57
|
+
.replace(/^[`"'*\s]+|[`"'*.\s]+$/g, '')
|
|
58
|
+
.replace(/^(nome|pasta)\s*:\s*/i, '');
|
|
59
|
+
if (!primeira || primeira.length > 60 || primeira.split(/\s+/).length > 5) return null;
|
|
60
|
+
const c = convencao(nomes);
|
|
61
|
+
let nome = slugify(primeira).split('-').filter(Boolean).slice(0, 4).join('-');
|
|
62
|
+
if (!nome || GENERICOS.has(nome)) return null;
|
|
63
|
+
if (c && c.separador === '_') nome = nome.replace(/-/g, '_');
|
|
64
|
+
return semColisao(nome, nomes);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Nome que já existe ganha -2, -3… — escrever na pasta de outro projeto é pior que um sufixo. */
|
|
68
|
+
function semColisao(nome, nomes) {
|
|
69
|
+
const tomados = new Set((nomes || []).map((n) => String(n).toLowerCase()));
|
|
70
|
+
if (!tomados.has(nome)) return nome;
|
|
71
|
+
for (let i = 2; i < 100; i++) {
|
|
72
|
+
const outro = `${nome}-${i}`;
|
|
73
|
+
if (!tomados.has(outro)) return outro;
|
|
74
|
+
}
|
|
75
|
+
return `${nome}-${Date.now()}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Pergunta o nome ao modelo. Nunca rejeita: sem resposta boa a tempo,
|
|
80
|
+
* devolve null e quem chamou fica com o nome de antes.
|
|
81
|
+
*
|
|
82
|
+
* `perguntar(texto)` → Promise<string> é quem fala com o servidor.
|
|
83
|
+
*/
|
|
84
|
+
async function escolher({ pedido, nomes, perguntar, tempo = TEMPO_MAXIMO_MS }) {
|
|
85
|
+
let relogio;
|
|
86
|
+
const estourou = new Promise((r) => { relogio = setTimeout(() => r(null), tempo); });
|
|
87
|
+
try {
|
|
88
|
+
const resposta = await Promise.race([perguntar(pergunta(pedido, nomes)), estourou]);
|
|
89
|
+
return resposta == null ? null : limpar(resposta, nomes);
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
} finally {
|
|
93
|
+
clearTimeout(relogio);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { convencao, pergunta, limpar, semColisao, escolher, GENERICOS };
|
package/lib/repeticao.js
CHANGED
|
@@ -83,8 +83,61 @@ function consultaDe(nome, args) {
|
|
|
83
83
|
return String(a.consulta || a.query || '');
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/* ── A ÉPOCA: repetir depois de mudar algo não é repetir ────────────────
|
|
87
|
+
* Medido: "edita o arquivo → roda npm test → edita → roda npm test". O
|
|
88
|
+
* segundo `npm test` tinha os mesmos argumentos do primeiro e era barrado —
|
|
89
|
+
* o agente nunca via se o conserto funcionou, ficava três voltas sem rodar
|
|
90
|
+
* nada e morria com "peça em partes". O mesmo com reler um arquivo depois de
|
|
91
|
+
* editá-lo, ou tirar outra foto da tela depois de clicar.
|
|
92
|
+
*
|
|
93
|
+
* Então olhar o mundo (ler, listar, rodar teste, fotografar a tela) vale por
|
|
94
|
+
* ÉPOCA: cada mudança de verdade (escrever, editar, clicar, rodar um comando
|
|
95
|
+
* que não é só de leitura) abre uma época nova, e a mesma leitura volta a
|
|
96
|
+
* valer. O laço de verdade — a mesma chamada sem nada mudar no meio — segue
|
|
97
|
+
* barrado. Pesquisa na web e documento de referência não dependem do disco:
|
|
98
|
+
* ficam fora da época. */
|
|
99
|
+
const OBSERVAM = new Set(['read_file', 'list_dir', 'glob', 'grep', 'browser_read', 'browser_screenshot',
|
|
100
|
+
'desktop_screenshot', 'desktop_size', 'desktop_windows', 'desktop_elements']);
|
|
101
|
+
const NAO_MUDAM = new Set(['write_todos', 'remember_general', 'pesquisar_referencia', ...REFERENCIAS]);
|
|
102
|
+
const SHELL_SO_LEITURA = /^\s*(ls|cat|head|tail|pwd|echo|grep|rg|find|wc|tree|stat|file|which|type|du|df|env|printenv|date|git\s+(status|log|diff|show|branch|remote))\b[^;&|>]*$/;
|
|
103
|
+
|
|
104
|
+
function comandoDe(args) {
|
|
105
|
+
return String((args && (args.command || args.comando)) || '');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Só olha: ler de novo depois de uma mudança é trabalho, não laço. */
|
|
109
|
+
function observa(nome, args) {
|
|
110
|
+
if (OBSERVAM.has(nome)) return true;
|
|
111
|
+
return nome === 'run_shell' && SHELL_SO_LEITURA.test(comandoDe(args));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Muda o mundo: abre uma época nova. */
|
|
115
|
+
function muda(nome, args) {
|
|
116
|
+
return !observa(nome, args) && !NAO_MUDAM.has(nome);
|
|
117
|
+
}
|
|
118
|
+
|
|
86
119
|
function novo() {
|
|
87
|
-
return { vistos: new Map(), buscas: [] };
|
|
120
|
+
return { vistos: new Map(), buscas: [], epoca: 0, epocaDaMudanca: 0, ultimaMudanca: null, olhouDesde: false };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Avisa que a chamada RODOU. Quem chama faz isto depois de executar (e não
|
|
125
|
+
* de barrar): é o que abre a época nova.
|
|
126
|
+
*
|
|
127
|
+
* A mesma mudança repetida sem olhar nada no meio (escrever o mesmo conteúdo
|
|
128
|
+
* no mesmo arquivo, clicar no mesmo ponto às cegas) não abre época e é
|
|
129
|
+
* barrada já na segunda vez — senão o laço de escrita passaria livre.
|
|
130
|
+
*/
|
|
131
|
+
function executou(registro, nome, args) {
|
|
132
|
+
if (observa(nome, args)) { registro.olhouDesde = true; return; }
|
|
133
|
+
if (!muda(nome, args)) return;
|
|
134
|
+
const chave = assinatura(nome, args);
|
|
135
|
+
if (chave !== registro.ultimaMudanca || registro.olhouDesde) {
|
|
136
|
+
registro.epocaDaMudanca = registro.epoca;
|
|
137
|
+
registro.epoca++;
|
|
138
|
+
}
|
|
139
|
+
registro.ultimaMudanca = chave;
|
|
140
|
+
registro.olhouDesde = false;
|
|
88
141
|
}
|
|
89
142
|
|
|
90
143
|
/**
|
|
@@ -94,8 +147,14 @@ function novo() {
|
|
|
94
147
|
* NÃO deve executar a tool: manda o `aviso` de volta como resultado.
|
|
95
148
|
*/
|
|
96
149
|
function ver(registro, nome, args) {
|
|
97
|
-
const chave = assinatura(nome, args);
|
|
98
150
|
const busca = consultaDe(nome, args);
|
|
151
|
+
const foraDaEpoca = busca !== null || REFERENCIAS.has(nome);
|
|
152
|
+
const base = assinatura(nome, args);
|
|
153
|
+
// A mesma mudança logo em seguida, sem olhar nada no meio, conta na época
|
|
154
|
+
// em que a primeira rodou — senão ela mesma abriria a porta da segunda.
|
|
155
|
+
const epoca = (base === registro.ultimaMudanca && !registro.olhouDesde)
|
|
156
|
+
? registro.epocaDaMudanca : (registro.epoca || 0);
|
|
157
|
+
const chave = base + (foraDaEpoca ? '' : `@${epoca}`);
|
|
99
158
|
|
|
100
159
|
let vezes = (registro.vistos.get(chave) || 0) + 1;
|
|
101
160
|
let parecidaCom = null;
|
|
@@ -123,14 +182,15 @@ function ver(registro, nome, args) {
|
|
|
123
182
|
function aviso(nome, vezes, parecidaCom) {
|
|
124
183
|
const oQue = parecidaCom
|
|
125
184
|
? `Você já pesquisou isto: "${parecidaCom}". A busca de agora é a mesma coisa com outras palavras, e traria as mesmas fontes.`
|
|
126
|
-
: `Você já chamou ${nome} com estes mesmos argumentos.`;
|
|
185
|
+
: `Você já chamou ${nome} com estes mesmos argumentos, e nada mudou desde então.`;
|
|
127
186
|
|
|
128
187
|
if (vezes === 2) {
|
|
129
188
|
return `${oQue} O resultado está na conversa acima — role para cima e use o que já tem. `
|
|
130
189
|
+ 'Não chame esta tool de novo com os mesmos argumentos.';
|
|
131
190
|
}
|
|
132
191
|
return `${oQue} Esta é a ${vezes}ª vez, e a tool NÃO foi executada. PARE de repetir: `
|
|
133
|
-
+ 'com o que você já tem, produza AGORA o resultado que o usuário pediu (crie o arquivo, o documento, o vídeo)
|
|
192
|
+
+ 'com o que você já tem, produza AGORA o resultado que o usuário pediu (crie o arquivo, o documento, o vídeo), '
|
|
193
|
+
+ 'ou tente OUTRO caminho (outra ferramenta, outro arquivo, outro comando). '
|
|
134
194
|
+ 'Se algo realmente impede, chame finish e diga ao usuário o que faltou.';
|
|
135
195
|
}
|
|
136
196
|
|
|
@@ -187,5 +247,5 @@ function referenciasNaConversa(messages) {
|
|
|
187
247
|
module.exports = {
|
|
188
248
|
REFERENCIAS, LIMITE_SEMELHANCA,
|
|
189
249
|
normalizar, palavras, semelhanca, assinatura, consultaDe,
|
|
190
|
-
novo, ver, chamadasPorId, referenciasNaConversa,
|
|
250
|
+
novo, ver, executou, observa, muda, chamadasPorId, referenciasNaConversa,
|
|
191
251
|
};
|
package/lib/tools.js
CHANGED
|
@@ -22,10 +22,24 @@ const cursor = require('./cursor');
|
|
|
22
22
|
* ação acontece de verdade, e a pessoa VÊ a seta dele indo até lá, clicando,
|
|
23
23
|
* digitando. Sem o cursor (outro sistema, ou o Python não subiu), vale o
|
|
24
24
|
* caminho antigo do desktop.js. */
|
|
25
|
+
/* Sem o cursor, a ação vai pelo caminho antigo, que NÃO aparece na tela. Isso
|
|
26
|
+
era calado: a pessoa via o terminal dizer "ok" e nada se mexer. Agora sai
|
|
27
|
+
uma linha, uma vez por sessão, dizendo por quê — e o modelo recebe o
|
|
28
|
+
mesmo aviso junto do resultado, para contar em vez de fingir. */
|
|
29
|
+
let avisouSemCursor = false;
|
|
30
|
+
function semCursorVisivel(resultado, motivo) {
|
|
31
|
+
const aviso = `o cursor do Primo não abriu (${motivo || 'motivo desconhecido'}): a ação roda, mas não aparece na tela`;
|
|
32
|
+
if (!avisouSemCursor) {
|
|
33
|
+
avisouSemCursor = true;
|
|
34
|
+
try { console.log(require('./ui.js').c.warn(` ⎿ ${aviso}`)); } catch { /* sem tela */ }
|
|
35
|
+
}
|
|
36
|
+
return resultado && typeof resultado === 'object' ? { ...resultado, avisoCursor: aviso } : resultado;
|
|
37
|
+
}
|
|
38
|
+
|
|
25
39
|
async function peloCursor(cmd, args, reserva) {
|
|
26
40
|
if (!cursor.disponivel()) return reserva();
|
|
27
41
|
const r = await cursor.mandar(cmd, args);
|
|
28
|
-
if (r.semCursor) return reserva();
|
|
42
|
+
if (r.semCursor) return semCursorVisivel(await reserva(), r.error);
|
|
29
43
|
if (!r.ok) return { ok: false, error: r.error || 'o cursor não conseguiu', ...(r.permissao === false ? { permissao: false } : {}) };
|
|
30
44
|
return { ok: true };
|
|
31
45
|
}
|
package/lib/voz.js
CHANGED
|
@@ -62,6 +62,12 @@ ${DICA_COMPUTADOR}
|
|
|
62
62
|
Faça o trabalho inteiro normalmente, com as ferramentas de sempre. O que muda
|
|
63
63
|
é só o jeito de contar no fim.`;
|
|
64
64
|
|
|
65
|
+
/* A forma da resposta de CONVERSA por voz ("oi", "tudo bem?", "o que você
|
|
66
|
+
faz?"). Sem ferramenta e sem memória de tarefa: um "oi" ia para o agente
|
|
67
|
+
inteiro, com a lista das tarefas antigas e a ordem de mexer no cursor, e
|
|
68
|
+
ele respondia falando delas — "eu mandei oi e ele respondeu nada a ver". */
|
|
69
|
+
const COMO_CONVERSAR = `[MODO VOZ] A sua resposta vai ser LIDA EM VOZ ALTA. Responda ao que a pessoa acabou de dizer, e só a isso, em uma ou duas frases curtas, em português falado, sem markdown. Não puxe assunto de tarefa antiga. Se for só um cumprimento, cumprimente de volta e pergunte o que ela quer fazer.`;
|
|
70
|
+
|
|
65
71
|
/** Onde está o Python 3. Mesmo problema do Estúdio, mesma cura. */
|
|
66
72
|
function python() {
|
|
67
73
|
const nomes = process.platform === 'win32'
|
|
@@ -575,4 +581,4 @@ async function preparar(aoFalar = () => {}) {
|
|
|
575
581
|
return { ok: true, python: py, esperando, avisos };
|
|
576
582
|
}
|
|
577
583
|
|
|
578
|
-
module.exports = { DICA_COMPUTADOR, conferir, instalar, preparar, porqueOPipFalhou, abrir, conferirEspera, baixarEspera, python, COMO_FALAR, PASTA };
|
|
584
|
+
module.exports = { DICA_COMPUTADOR, COMO_CONVERSAR, 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.8.0-beta.1",
|
|
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/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"
|
|
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/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.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/autonomia.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"
|
|
@@ -243,8 +243,10 @@ class Voz:
|
|
|
243
243
|
continue
|
|
244
244
|
|
|
245
245
|
self.orb.mudar(estado="pensando")
|
|
246
|
-
|
|
247
|
-
|
|
246
|
+
# O servidor também transcreve com Whisper: a mesma limpeza vale.
|
|
247
|
+
texto = transcrever.limpar(self._transcrever(wav))
|
|
248
|
+
if not texto:
|
|
249
|
+
self.orb.mudar(estado="esperando")
|
|
248
250
|
continue
|
|
249
251
|
|
|
250
252
|
falar_para_o_node({"tipo": "pedido", "texto": texto})
|
|
@@ -74,13 +74,49 @@ def aquecer() -> None:
|
|
|
74
74
|
threading.Thread(target=_ir, daemon=True).start()
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
# ── O QUE O WHISPER INVENTA ─────────────────────────────────────────────
|
|
78
|
+
# Treinado em legenda de vídeo, ele "ouve" no ruído o fim de um vídeo do
|
|
79
|
+
# YouTube. Essas frases chegavam ao agente como pedido, e ele respondia a
|
|
80
|
+
# elas: "eu mandei oi e ele respondeu nada a ver" (23/09/2026).
|
|
81
|
+
ALUCINACOES = (
|
|
82
|
+
"obrigado por assistir", "obrigada por assistir", "obrigado pela atenção",
|
|
83
|
+
"legendas pela comunidade", "amara.org", "inscreva-se", "se inscreva",
|
|
84
|
+
"deixe seu like", "ative o sininho", "até o próximo vídeo", "até a próxima",
|
|
85
|
+
"legendado por", "legenda por", "tradução e legendas", "transcrição por",
|
|
86
|
+
"www.", ".com.br",
|
|
87
|
+
)
|
|
88
|
+
# Acima disto o próprio Whisper acha que não havia fala no trecho.
|
|
89
|
+
SEM_FALA = 0.6
|
|
90
|
+
# Abaixo disto a confiança média é baixa demais para virar pedido.
|
|
91
|
+
CONFIANCA_MINIMA = -1.0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def limpar(texto: str) -> str:
|
|
95
|
+
"""Tira a frase inventada. Devolve "" quando não sobra pedido."""
|
|
96
|
+
t = " ".join(str(texto or "").split())
|
|
97
|
+
baixo = t.lower()
|
|
98
|
+
if any(frase in baixo for frase in ALUCINACOES):
|
|
99
|
+
return ""
|
|
100
|
+
# Só pontuação ou uma letra solta não é pedido.
|
|
101
|
+
if len("".join(ch for ch in t if ch.isalnum())) < 2:
|
|
102
|
+
return ""
|
|
103
|
+
return t
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def aproveitavel(segmento) -> bool:
|
|
107
|
+
"""O segmento tem fala de verdade, pelo que o próprio Whisper mediu?"""
|
|
108
|
+
sem_fala = getattr(segmento, "no_speech_prob", 0.0) or 0.0
|
|
109
|
+
confianca = getattr(segmento, "avg_logprob", 0.0) or 0.0
|
|
110
|
+
return not (sem_fala > SEM_FALA and confianca < CONFIANCA_MINIMA)
|
|
111
|
+
|
|
112
|
+
|
|
77
113
|
def transcrever(wav: Path, idioma: str = "pt") -> str:
|
|
78
114
|
modelo = carregar()
|
|
79
115
|
# vad_filter corta o silêncio das pontas: sem ele, o Whisper às vezes
|
|
80
116
|
# "ouve" uma frase inteira inventada no silêncio ("Obrigado por assistir").
|
|
81
117
|
segmentos, _ = modelo.transcribe(str(wav), language=idioma, beam_size=5,
|
|
82
118
|
vad_filter=True, condition_on_previous_text=False)
|
|
83
|
-
return " ".join(s.text.strip() for s in segmentos
|
|
119
|
+
return limpar(" ".join(s.text.strip() for s in segmentos if aproveitavel(s)))
|
|
84
120
|
|
|
85
121
|
|
|
86
122
|
def main() -> int:
|