primocode 9.8.0-beta.8 → 9.8.0-beta.9
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 +3 -2
- package/bin/primocode.js +45 -18
- package/lib/offline.js +168 -0
- package/package.json +2 -3
- package/skills/design-de-verdade/SKILL.md +2 -0
- package/skills/motion-de-primeira/SKILL.md +1 -1
- package/skills/site-com-motion/SKILL.md +39 -0
- package/kits/acolhedor/index.html +0 -130
- package/kits/acolhedor/script.js +0 -25
- package/kits/acolhedor/style.css +0 -146
- package/kits/editorial/index.html +0 -115
- package/kits/editorial/script.js +0 -19
- package/kits/editorial/style.css +0 -112
- package/kits/landing/index.html +0 -162
- package/kits/landing/script.js +0 -199
- package/kits/landing/style.css +0 -219
- package/kits/premium/index.html +0 -83
- package/kits/premium/script.js +0 -11
- package/kits/premium/style.css +0 -87
- package/kits/vitrine/index.html +0 -108
- package/kits/vitrine/script.js +0 -20
- package/kits/vitrine/style.css +0 -117
- package/lib/kits.js +0 -206
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v9.8.0-beta.
|
|
1
|
+
# PrimoCode v9.8.0-beta.9
|
|
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.8.0-beta.
|
|
12
|
+
▐▛███▜▌ Primo Code v9.8.0-beta.9
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
|
@@ -311,6 +311,7 @@ primocode -c # continua a última conversa salva (--co
|
|
|
311
311
|
| `/copiar` | Copia a última resposta para a área de transferência (pbcopy, clip, wl-copy/xclip/xsel) |
|
|
312
312
|
| `/historico [termo]` | Procura nos seus pedidos guardados nesta máquina (`~/.primocode/history.jsonl`), do mais recente. As conversas inteiras da conta ficam no `/chats` |
|
|
313
313
|
| `/status` | Health check do servidor |
|
|
314
|
+
| `/offline` | Temporário: sobe o `primocode-server` no seu computador (a pasta ao lado do CLI, ou `/offline <caminho>`) com as chaves de `~/.primocode/servidor.env` e aponta a sessão pra ele. `/offline off` volta ao servidor de antes |
|
|
314
315
|
| `/doctor` | Confere tudo com ✓/✗: Node, servidor, `/api`, Chrome, Python+PyObjC (cursor, macOS), disco e config. Cada ✗ diz como consertar |
|
|
315
316
|
| `/custo` | Tokens enviados e recebidos e pedidos feitos nesta sessão, por provedor (estimado em caracteres ÷ 4 quando o provedor não manda o número) |
|
|
316
317
|
| `/atalhos` | As teclas: Esc Esc para o agente, `!` roda no shell, Alt+Enter quebra linha… |
|
package/bin/primocode.js
CHANGED
|
@@ -21,7 +21,6 @@ const api = require('../lib/api');
|
|
|
21
21
|
const tools = require('../lib/tools');
|
|
22
22
|
const cfg = require('../lib/config');
|
|
23
23
|
const nomeProjeto = require('../lib/nome-projeto');
|
|
24
|
-
const kits = require('../lib/kits');
|
|
25
24
|
const provedorLocal = require('../lib/local/provedor');
|
|
26
25
|
const { runAct, interromper, ehTetoDeToken, ehTransitorio, esperaDoErro } = require('../lib/act');
|
|
27
26
|
const fala = require('../lib/fala');
|
|
@@ -44,6 +43,7 @@ const nuvem = require('../lib/nuvem');
|
|
|
44
43
|
const compat = require('../lib/compatibilidade');
|
|
45
44
|
const mencoesLib = require('../lib/mencoes');
|
|
46
45
|
const extra = require('../lib/comandos-extra');
|
|
46
|
+
const offline = require('../lib/offline');
|
|
47
47
|
|
|
48
48
|
/* O /custo: toda conversa com o modelo passa pelo lib/api.js, e o contador
|
|
49
49
|
embrulha as funções dele uma vez, aqui. Quem atende é o provedor do /api
|
|
@@ -186,6 +186,7 @@ ${c.bold('Comandos')}
|
|
|
186
186
|
${c.brand('/logout')} desconecta este computador da conta
|
|
187
187
|
${c.brand('/comandos')} seus comandos (/revisar, /explicar…) · /comandos novo <nome>
|
|
188
188
|
${c.brand('/status')} checa o servidor
|
|
189
|
+
${c.brand('/offline')} roda o servidor no seu computador (temporário) · /offline off volta
|
|
189
190
|
${c.brand('/doctor')} confere tudo (servidor, /api, Chrome, disco…) e diz como consertar
|
|
190
191
|
${c.brand('/custo')} tokens e pedidos desta sessão, por provedor
|
|
191
192
|
${c.brand('/atalhos')} as teclas que ajudam
|
|
@@ -251,6 +252,7 @@ const SLASH_COMANDOS = [
|
|
|
251
252
|
['/comandos', 'seus comandos: um .md vira /nome · /comandos novo <nome>'],
|
|
252
253
|
['/api', 'usa a SUA chave (Gemini, ChatGPT, Claude…) direto daqui, sem o limite do servidor'],
|
|
253
254
|
['/status', 'checa o servidor'],
|
|
255
|
+
['/offline', 'roda o servidor aqui no seu computador, com as chaves do Render'],
|
|
254
256
|
['/doctor', 'confere se está tudo certo e diz como consertar'],
|
|
255
257
|
['/custo', 'tokens e pedidos desta sessão, por provedor'],
|
|
256
258
|
['/atalhos', 'as teclas que ajudam'],
|
|
@@ -769,16 +771,6 @@ function batizarDepois(plan, userText) {
|
|
|
769
771
|
.then((nome) => { if (nome) plan.nomeEscolhido = nome; });
|
|
770
772
|
}
|
|
771
773
|
|
|
772
|
-
/* A inspiração visual (lib/kits.js): pedido de landing leva a ficha da
|
|
773
|
-
direção que combina com o negócio. Nada é copiado para a pasta — o agente
|
|
774
|
-
escreve a página dele, inspirado, e não troca texto num molde. */
|
|
775
|
-
async function prepararKit(plan, userText) {
|
|
776
|
-
const kit = kits.escolher(userText, { provedor: provedorLocal.atual() });
|
|
777
|
-
if (!kit) return '';
|
|
778
|
-
console.log(c.dim(` ⎿ inspiração visual: ${kit.titulo}`));
|
|
779
|
-
return kit.instrucao;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
774
|
function createPendingProject(plan) {
|
|
783
775
|
const dir = cfg.createProjectDir(plan.nomeEscolhido || plan.name);
|
|
784
776
|
entrarNoProjeto(dir, 'projeto');
|
|
@@ -1145,7 +1137,6 @@ async function enviarPedido(userText) {
|
|
|
1145
1137
|
tempos.medir('espera do resumo', () => esperarResumo()),
|
|
1146
1138
|
tempos.medir('plano', () => dividirTarefa(prompt)),
|
|
1147
1139
|
]);
|
|
1148
|
-
const instrucaoKit = await prepararKit(plan, userText);
|
|
1149
1140
|
|
|
1150
1141
|
let r = null;
|
|
1151
1142
|
let jaFeito = '';
|
|
@@ -1194,8 +1185,7 @@ async function enviarPedido(userText) {
|
|
|
1194
1185
|
permissoes: state.config.permissoes,
|
|
1195
1186
|
// Pedido de mexer no computador leva a dica que libera as
|
|
1196
1187
|
// ferramentas desktop_* no servidor (ver DICA_COMPUTADOR).
|
|
1197
|
-
instrucao:
|
|
1198
|
-
.filter(Boolean).join('\n\n'),
|
|
1188
|
+
instrucao: i === 0 && querComputador(userText) ? vozModo.DICA_COMPUTADOR : '',
|
|
1199
1189
|
});
|
|
1200
1190
|
|
|
1201
1191
|
r = await rodarPasso();
|
|
@@ -2666,6 +2656,45 @@ async function handleSlash(text) {
|
|
|
2666
2656
|
break;
|
|
2667
2657
|
}
|
|
2668
2658
|
|
|
2659
|
+
/* O servidor aqui, no computador da pessoa, até a sessão acabar
|
|
2660
|
+
(lib/offline.js). "Enquanto não tenho acesso ao Railway, eu testo
|
|
2661
|
+
com o projeto rodando localmente." (24/09/2026) */
|
|
2662
|
+
case 'offline': case 'local': {
|
|
2663
|
+
if (/^(off|sair|desligar|parar)$/i.test(rest)) {
|
|
2664
|
+
if (!state.servidorAntes) { console.log(c.muted(' O /offline não está ligado.')); break; }
|
|
2665
|
+
offline.desligar();
|
|
2666
|
+
state.config.server = state.servidorAntes;
|
|
2667
|
+
state.servidorAntes = null;
|
|
2668
|
+
console.log(c.ok(' Voltei ao servidor de antes: ') + c.dim(state.config.server));
|
|
2669
|
+
break;
|
|
2670
|
+
}
|
|
2671
|
+
if (state.servidorAntes) { console.log(c.muted(' Já está no servidor local: ') + c.dim(state.config.server) + c.muted(' · /offline off volta')); break; }
|
|
2672
|
+
const chaves = offline.prepararEnv();
|
|
2673
|
+
if (chaves.criado || !chaves.temChave) {
|
|
2674
|
+
console.log(c.warn(` ${chaves.criado ? 'Criei' : 'Falta chave em'} ${chaves.arquivo}`));
|
|
2675
|
+
console.log(c.muted(' Cole nele as chaves do Render (primocode-server → Environment), uma por linha:'));
|
|
2676
|
+
console.log(c.dim(' GROQ_API_KEY=… OPENROUTER_API_KEY=… GEMINI_API_KEY=…'));
|
|
2677
|
+
console.log(c.muted(' Depois rode ') + c.brand('/offline') + c.muted(' de novo.'));
|
|
2678
|
+
break;
|
|
2679
|
+
}
|
|
2680
|
+
const dir = offline.acharServidor({ pedido: rest });
|
|
2681
|
+
const sp = new Spinner('subindo o servidor aqui').start();
|
|
2682
|
+
const r = await offline.ligar({ dir, env: chaves.env });
|
|
2683
|
+
sp.stop();
|
|
2684
|
+
if (!r.ok) {
|
|
2685
|
+
console.log(c.err(' ' + r.error));
|
|
2686
|
+
if (r.comoResolver) console.log(c.dim(' → ' + r.comoResolver));
|
|
2687
|
+
break;
|
|
2688
|
+
}
|
|
2689
|
+
state.servidorAntes = state.config.server;
|
|
2690
|
+
state.config.server = r.url;
|
|
2691
|
+
console.log(c.ok(` Servidor ${r.versao || ''} ${r.reaproveitado ? 'que já estava de pé' : 'rodando aqui'}: `) + c.dim(r.url));
|
|
2692
|
+
console.log(c.dim(` ${dir || ''}${dir ? ' · ' : ''}chaves: ${chaves.nomes.join(', ')}${r.reaproveitado ? '' : ' · log em ' + offline.ARQUIVO_LOG}`));
|
|
2693
|
+
if (provedorLocal.atual()) console.log(c.warn(' Atenção: o /api está ligado e fala direto com a IA. Para usar o servidor local, /api off.'));
|
|
2694
|
+
console.log(c.muted(' Vale até fechar o PrimoCode. ') + c.brand('/offline off') + c.muted(' volta antes.'));
|
|
2695
|
+
break;
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2669
2698
|
/* Os comandos da pessoa: lista, e explica como fazer o dela. */
|
|
2670
2699
|
case 'comandos': {
|
|
2671
2700
|
const novo = rest.match(/^(novo|new|criar)\s+(\S+)$/i);
|
|
@@ -2867,10 +2896,8 @@ async function main() {
|
|
|
2867
2896
|
// Com -c, o pedido continua a última conversa em vez de chegar sozinho.
|
|
2868
2897
|
if (continuar) await continuarUltima();
|
|
2869
2898
|
/* O MESMO caminho do pedido digitado (sendPrompt). Este modo tinha um
|
|
2870
|
-
runAct próprio, e por isso ficava sem tudo o que vive lá: o
|
|
2871
|
-
|
|
2872
|
-
checkpoint do /desfazer. Medido em 24/09: `primo "crie uma landing…"`
|
|
2873
|
-
não pegou o kit. */
|
|
2899
|
+
runAct próprio, e por isso ficava sem tudo o que vive lá: o plano em
|
|
2900
|
+
passos, a retomada automática, o @arquivo, o checkpoint do /desfazer. */
|
|
2874
2901
|
state.semTerminal = true;
|
|
2875
2902
|
await sendPrompt(promptText);
|
|
2876
2903
|
process.exit(0);
|
package/lib/offline.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* offline.js — o primocode-server rodando AQUI, no computador da pessoa.
|
|
3
|
+
*
|
|
4
|
+
* "Como eu não tenho acesso ao Railway, enquanto isso eu posso testar com o
|
|
5
|
+
* projeto rodando localmente, chamando as APIs pelo meu computador."
|
|
6
|
+
* (24/09/2026)
|
|
7
|
+
*
|
|
8
|
+
* O /offline sobe o servidor da pasta primocode-server com as MESMAS chaves
|
|
9
|
+
* que o Render usa (coladas em ~/.primocode/servidor.env) e aponta o CLI para
|
|
10
|
+
* ele até a sessão acabar. É temporário: serve para testar o servidor novo
|
|
11
|
+
* antes de ele ir para produção.
|
|
12
|
+
*
|
|
13
|
+
* "Offline" é o nome que o pedido deu, e ele mente um pouco: as IAs continuam
|
|
14
|
+
* na internet. O que sai do caminho é o servidor de produção.
|
|
15
|
+
*
|
|
16
|
+
* ── O QUE NUNCA APARECE ──────────────────────────────────────────────────
|
|
17
|
+
* O valor de uma chave. Mostram-se só os NOMES das variáveis que o arquivo
|
|
18
|
+
* tem, o bastante para ver o que falta.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const http = require('http');
|
|
27
|
+
const { spawn } = require('child_process');
|
|
28
|
+
|
|
29
|
+
const PORTA = Number(process.env.PRIMOCODE_OFFLINE_PORTA || 3190);
|
|
30
|
+
const CASA = path.join(os.homedir(), '.primocode');
|
|
31
|
+
const ARQUIVO_ENV = path.join(CASA, 'servidor.env');
|
|
32
|
+
const ARQUIVO_LOG = path.join(CASA, 'servidor-local.log');
|
|
33
|
+
|
|
34
|
+
// As variáveis que o primocode-server lê. As de chave vêm primeiro: sem pelo
|
|
35
|
+
// menos uma delas, o servidor sobe mas não tem com quem falar.
|
|
36
|
+
const CHAVES = ['GROQ_API_KEY', 'OPENROUTER_API_KEY', 'GEMINI_API_KEY', 'GOOGLE_API_KEY', 'COHERE_API_KEY', 'HF_TOKEN', 'HUGGINGFACE_API_KEY', 'PG_GATEWAY_API_KEY'];
|
|
37
|
+
const OUTRAS = ['PG_GATEWAY_BASE_URL', 'CONECTA_API', 'PRIMOCODE_TOKEN', 'REQUIRE_TOKEN', 'RATE_LIMIT_PER_HOUR'];
|
|
38
|
+
|
|
39
|
+
const MODELO_ENV = `# As chaves do primocode-server rodando no seu computador (/offline).
|
|
40
|
+
# Copie do Render: o serviço primocode-server → Environment. Uma por linha,
|
|
41
|
+
# NOME=valor, sem aspas. Este arquivo é só seu: não mande para ninguém.
|
|
42
|
+
#
|
|
43
|
+
${CHAVES.map((k) => `# ${k}=`).join('\n')}
|
|
44
|
+
#
|
|
45
|
+
${OUTRAS.map((k) => `# ${k}=`).join('\n')}
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
/** Lê um .env simples: NOME=valor, # comenta, aspas em volta saem. */
|
|
49
|
+
function lerEnv(texto) {
|
|
50
|
+
const env = {};
|
|
51
|
+
for (const linha of String(texto || '').split(/\r?\n/)) {
|
|
52
|
+
const m = linha.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
53
|
+
if (!m) continue;
|
|
54
|
+
let valor = m[2];
|
|
55
|
+
if (/^(['"]).*\1$/.test(valor)) valor = valor.slice(1, -1);
|
|
56
|
+
if (valor) env[m[1]] = valor;
|
|
57
|
+
}
|
|
58
|
+
return env;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** O arquivo de chaves: cria o modelo (vazio, 600) na primeira vez. */
|
|
62
|
+
function prepararEnv(arquivo = ARQUIVO_ENV) {
|
|
63
|
+
let criado = false;
|
|
64
|
+
if (!fs.existsSync(arquivo)) {
|
|
65
|
+
fs.mkdirSync(path.dirname(arquivo), { recursive: true });
|
|
66
|
+
fs.writeFileSync(arquivo, MODELO_ENV, { mode: 0o600 });
|
|
67
|
+
criado = true;
|
|
68
|
+
}
|
|
69
|
+
const env = lerEnv(fs.readFileSync(arquivo, 'utf8'));
|
|
70
|
+
return { arquivo, criado, env, nomes: Object.keys(env), temChave: CHAVES.some((k) => env[k]) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ehServidor(dir) {
|
|
74
|
+
try {
|
|
75
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
76
|
+
return pkg.name === 'primocode-server' && fs.existsSync(path.join(dir, 'server.js'));
|
|
77
|
+
} catch { return false; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Onde está a pasta do primocode-server. A primeira que existir ganha. */
|
|
81
|
+
function acharServidor({ pedido = '', raizCli = path.join(__dirname, '..') } = {}) {
|
|
82
|
+
const candidatos = [
|
|
83
|
+
pedido,
|
|
84
|
+
process.env.PRIMOCODE_SERVER_DIR,
|
|
85
|
+
path.join(raizCli, '..', 'primocode-server'),
|
|
86
|
+
path.join(os.homedir(), 'Primocode CLI', 'primocode-server'),
|
|
87
|
+
path.join(os.homedir(), 'primocode-server'),
|
|
88
|
+
].filter(Boolean).map((p) => path.resolve(p.replace(/^~(?=$|\/)/, os.homedir())));
|
|
89
|
+
return candidatos.find(ehServidor) || null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** GET /health; devolve o JSON ou null. */
|
|
93
|
+
function saude(url, espera = 1500) {
|
|
94
|
+
return new Promise((resolve) => {
|
|
95
|
+
const req = http.get(url + '/health', { timeout: espera }, (res) => {
|
|
96
|
+
let corpo = '';
|
|
97
|
+
res.on('data', (d) => { corpo += d; });
|
|
98
|
+
res.on('end', () => { try { resolve(JSON.parse(corpo)); } catch { resolve(null); } });
|
|
99
|
+
});
|
|
100
|
+
req.on('timeout', () => req.destroy());
|
|
101
|
+
req.on('error', () => resolve(null));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let filho = null;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Sobe o servidor (ou reaproveita um que já responde na porta).
|
|
109
|
+
* Devolve { ok, url, versao, reaproveitado } ou { ok: false, error, comoResolver }.
|
|
110
|
+
*/
|
|
111
|
+
async function ligar({ dir, env = {}, porta = PORTA, log = ARQUIVO_LOG, prazo = 20000 } = {}) {
|
|
112
|
+
const url = `http://localhost:${porta}`;
|
|
113
|
+
const ja = await saude(url);
|
|
114
|
+
if (ja && ja.status === 'ok') return { ok: true, url, versao: ja.version, reaproveitado: true };
|
|
115
|
+
if (!dir) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
error: 'Não achei a pasta do primocode-server.',
|
|
119
|
+
comoResolver: 'clone o repo ao lado do primocode, ou diga onde ele está: /offline ~/caminho/primocode-server',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (!fs.existsSync(path.join(dir, 'node_modules'))) {
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
error: `O servidor em ${dir} ainda não tem as dependências.`,
|
|
126
|
+
comoResolver: `rode uma vez: cd "${dir}" && npm install`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
fs.mkdirSync(path.dirname(log), { recursive: true });
|
|
130
|
+
const saida = fs.openSync(log, 'a');
|
|
131
|
+
filho = spawn(process.execPath, ['server.js'], {
|
|
132
|
+
cwd: dir,
|
|
133
|
+
env: { ...process.env, ...env, PORT: String(porta) },
|
|
134
|
+
stdio: ['ignore', saida, saida],
|
|
135
|
+
});
|
|
136
|
+
let morreu = null;
|
|
137
|
+
filho.on('exit', (codigo) => { morreu = codigo; filho = null; });
|
|
138
|
+
|
|
139
|
+
const fim = Date.now() + prazo;
|
|
140
|
+
while (Date.now() < fim) {
|
|
141
|
+
if (morreu !== null) {
|
|
142
|
+
return { ok: false, error: `O servidor fechou sozinho (código ${morreu}).`, comoResolver: `veja o fim de ${log}` };
|
|
143
|
+
}
|
|
144
|
+
const h = await saude(url);
|
|
145
|
+
if (h && h.status === 'ok') return { ok: true, url, versao: h.version, reaproveitado: false };
|
|
146
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
147
|
+
}
|
|
148
|
+
desligar();
|
|
149
|
+
return { ok: false, error: `O servidor não respondeu em ${Math.round(prazo / 1000)}s.`, comoResolver: `veja o fim de ${log}` };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Desliga o servidor que ESTE processo subiu. Um reaproveitado fica de pé. */
|
|
153
|
+
function desligar() {
|
|
154
|
+
if (!filho) return false;
|
|
155
|
+
try { filho.kill(); } catch {}
|
|
156
|
+
filho = null;
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function ligado() { return Boolean(filho); }
|
|
161
|
+
|
|
162
|
+
// O servidor não pode sobrar rodando quando o CLI fecha.
|
|
163
|
+
process.on('exit', desligar);
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
PORTA, ARQUIVO_ENV, ARQUIVO_LOG, CHAVES,
|
|
167
|
+
lerEnv, prepararEnv, acharServidor, ehServidor, saude, ligar, desligar, ligado,
|
|
168
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "9.8.0-beta.
|
|
3
|
+
"version": "9.8.0-beta.9",
|
|
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/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.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/
|
|
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/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.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/claude-engine.test.js && node test/codex.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/desfazer.test.js && node test/desfazer-git.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/app-janela.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 && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
"app/",
|
|
31
31
|
"studio/",
|
|
32
32
|
"skills/",
|
|
33
|
-
"kits/",
|
|
34
33
|
"voz/",
|
|
35
34
|
"README.md",
|
|
36
35
|
"comandos/",
|
|
@@ -7,6 +7,8 @@ autor: adaptado de "frontend-design" (Anthropic, anthropics/skills, Apache-2.0
|
|
|
7
7
|
Trabalhe como o diretor de arte de um estúdio conhecido por dar a cada cliente uma identidade que
|
|
8
8
|
ninguém confunde. O cliente já recusou propostas com cara de modelo pronto.
|
|
9
9
|
|
|
10
|
+
**Não existe modelo.** Toda página começa do zero, e nenhuma sai com a estrutura da anterior.
|
|
11
|
+
|
|
10
12
|
**Comece pelo assunto.** O ramo, os materiais e o vocabulário do negócio são de onde vem a escolha
|
|
11
13
|
visual. Um brinquedo para meninas de 8 anos e um painel de analista financeiro não se parecem em nada.
|
|
12
14
|
Use o conteúdo real do pedido em tudo, e não texto genérico.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
nome: Motion de primeira
|
|
3
3
|
descricao: como um vídeo criado do zero deixa de parecer feito por máquina
|
|
4
|
-
quando: vídeo, video, motion,
|
|
4
|
+
quando: vídeo, video, motion graphics, vídeo animado, video animado, anúncio, anuncio, reels, teaser, vinheta, comercial, propaganda, estúdio, estudio
|
|
5
5
|
autor: PrimoCode
|
|
6
6
|
---
|
|
7
7
|
Não existe template. Cada propriedade deste vídeo é uma decisão sua, e a
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
nome: Site com motion
|
|
3
|
+
descricao: movimento em site e landing que parece de estúdio, e não efeito jogado em tudo
|
|
4
|
+
quando: motion, site animado, landing animada, página animada, pagina animada, animação no site, animações no site, animacao no site, com animação, com animacao, com animações, scroll, parallax, transição, transicao, microinteração, microinteracao, efeito, efeitos, gsap, hover, landing, landing page, página, pagina, site, one page
|
|
5
|
+
autor: PrimoCode
|
|
6
|
+
---
|
|
7
|
+
Movimento em site serve para três coisas: **mostrar a ordem** (o que ler primeiro), **responder à
|
|
8
|
+
pessoa** (clicou, passou o mouse, rolou) e **dar um momento memorável**. Fora disso, é ruído.
|
|
9
|
+
|
|
10
|
+
**Planeje o movimento antes do código**, em três linhas:
|
|
11
|
+
- o momento marcante: UMA coisa (a abertura se montando, o título que se desenha, o produto que gira
|
|
12
|
+
com o scroll);
|
|
13
|
+
- como as seções entram: cada uma do jeito que o conteúdo pede, e não o mesmo fade em todas;
|
|
14
|
+
- a resposta ao toque: botão, card, link, campo.
|
|
15
|
+
|
|
16
|
+
**O que denuncia animação feita por IA:** fade subindo em toda seção; hover que aumenta todo card;
|
|
17
|
+
tudo com a mesma duração; `transition: all`; texto piscando; nada que responda ao scroll.
|
|
18
|
+
|
|
19
|
+
**Como se faz bem:**
|
|
20
|
+
- **Tempo:** resposta ao toque em 120 a 200 ms. Entrada de elemento em 400 a 700 ms. O momento
|
|
21
|
+
marcante pode levar 1 a 1,5 s, uma vez só.
|
|
22
|
+
- **Curva:** nada linear. Para entrar, `cubic-bezier(.2,.8,.2,1)`. Para sair, a curva ao contrário
|
|
23
|
+
e mais curta que a entrada.
|
|
24
|
+
- **Cascata:** itens de uma lista entram com 40 a 80 ms de diferença, e não todos juntos.
|
|
25
|
+
- **Scroll:** `IntersectionObserver` põe uma classe quando a seção aparece, e o CSS anima. Pra algo
|
|
26
|
+
que acompanha o scroll (parallax, barra de progresso, imagem que cresce), use
|
|
27
|
+
`animation-timeline: view()` e deixe a página parada onde isso não existe (`@supports`).
|
|
28
|
+
- **Só `transform` e `opacity`.** Animar `top`, `width`, `height` ou `box-shadow` trava no celular.
|
|
29
|
+
- **Troca de página ou de estado:** a View Transitions API (`document.startViewTransition`), com o
|
|
30
|
+
caminho sem animação quando o navegador não tiver.
|
|
31
|
+
- **Biblioteca só se precisar.** CSS e um pouco de JS resolvem quase tudo. Timeline longa ou texto
|
|
32
|
+
que se quebra letra a letra: GSAP pelo CDN.
|
|
33
|
+
|
|
34
|
+
**Obrigatório:** `@media (prefers-reduced-motion: reduce)` desliga o movimento grande e deixa só
|
|
35
|
+
troca de cor e opacidade. O conteúdo aparece mesmo sem JS: esconda o elemento só depois que o script
|
|
36
|
+
carregou (uma classe `js` no `<html>`).
|
|
37
|
+
|
|
38
|
+
**Confira:** abra a página, role do topo ao fim, e pergunte se cada movimento tem um dos três
|
|
39
|
+
motivos. O que não tiver, tire.
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang='pt-BR'>
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset='UTF-8'>
|
|
5
|
-
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
|
6
|
-
<!-- TROQUE: título e descrição com o nome do negócio e o bairro -->
|
|
7
|
-
<title>Sua Marca, o que você faz no seu bairro</title>
|
|
8
|
-
<meta name='description' content='Uma frase que diz o que o negócio faz, onde fica e para quem.'>
|
|
9
|
-
<link rel='preconnect' href='https://fonts.googleapis.com'>
|
|
10
|
-
<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin>
|
|
11
|
-
<!-- TROQUE (se quiser): as fontes. Se trocar aqui, troque --fonte-titulo e --fonte-texto no topo do style.css -->
|
|
12
|
-
<link href='https://fonts.googleapis.com/css2?family=Fredoka:wght@500;600&family=Nunito:wght@400;600;700&display=swap' rel='stylesheet'>
|
|
13
|
-
<link rel='stylesheet' href='style.css'>
|
|
14
|
-
<script src='script.js' defer></script>
|
|
15
|
-
</head>
|
|
16
|
-
<body>
|
|
17
|
-
<a class='pular' href='#conteudo'>Pular para o conteúdo</a>
|
|
18
|
-
|
|
19
|
-
<!-- TROQUE: a inicial, o nome, os links e o número do WhatsApp (só dígitos, com 55 e DDD) -->
|
|
20
|
-
<header class='topo'>
|
|
21
|
-
<a href='#' class='marca'><span class='marca-selo' aria-hidden='true'>S</span>Sua Marca</a>
|
|
22
|
-
<nav class='menu' aria-label='Principal'>
|
|
23
|
-
<a href='#servicos'>Serviços</a>
|
|
24
|
-
<a href='#horarios'>Horários</a>
|
|
25
|
-
<a href='https://wa.me/5500000000000' class='botao botao-pequeno'>Chamar no WhatsApp</a>
|
|
26
|
-
</nav>
|
|
27
|
-
</header>
|
|
28
|
-
|
|
29
|
-
<main id='conteudo'>
|
|
30
|
-
<!-- TROQUE: o convite (fale como quem atende no balcão), o texto e o bairro. A ilustração é CSS: pode mudar a letra da placa. -->
|
|
31
|
-
<section class='abertura'>
|
|
32
|
-
<div class='abertura-texto'>
|
|
33
|
-
<p class='aviso'><span class='bolinha' aria-hidden='true'></span><span id='statusHoje'>Veja o horário de hoje</span></p>
|
|
34
|
-
<h1>Um convite simples, do jeito que você falaria no balcão.</h1>
|
|
35
|
-
<p class='abertura-lead'>Uma ou duas frases sobre o que vocês fazem, para quem e o cuidado que faz a pessoa voltar. No Seu Bairro, em Sua Cidade.</p>
|
|
36
|
-
<div class='acoes'>
|
|
37
|
-
<a href='https://wa.me/5500000000000' class='botao'>Chamar no WhatsApp</a>
|
|
38
|
-
<a href='#servicos' class='botao botao-claro'>Ver os serviços</a>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
<div class='vitrine' aria-hidden='true'>
|
|
42
|
-
<div class='sol'></div>
|
|
43
|
-
<div class='loja'>
|
|
44
|
-
<div class='toldo'></div>
|
|
45
|
-
<div class='porta'><span class='placa'>S</span></div>
|
|
46
|
-
</div>
|
|
47
|
-
<div class='planta'></div>
|
|
48
|
-
<div class='nuvem'></div>
|
|
49
|
-
</div>
|
|
50
|
-
</section>
|
|
51
|
-
|
|
52
|
-
<!-- TROQUE: os serviços e os preços (se não souber o preço, apague o <span class='preco'> daquela linha) -->
|
|
53
|
-
<section id='servicos' class='secao'>
|
|
54
|
-
<div class='quadro'>
|
|
55
|
-
<h2>O que a gente faz</h2>
|
|
56
|
-
<p class='quadro-lead'>Uma frase curta sobre os serviços, e que dá para combinar pelo WhatsApp.</p>
|
|
57
|
-
<ul class='precos'>
|
|
58
|
-
<li><span class='item'>Primeiro serviço</span><span class='pontos' aria-hidden='true'></span><span class='preco'>R$ 00</span></li>
|
|
59
|
-
<li><span class='item'>Segundo serviço</span><span class='pontos' aria-hidden='true'></span><span class='preco'>R$ 00</span></li>
|
|
60
|
-
<li><span class='item'>Terceiro serviço</span><span class='pontos' aria-hidden='true'></span><span class='preco'>R$ 00</span></li>
|
|
61
|
-
<li><span class='item'>Quarto serviço</span><span class='pontos' aria-hidden='true'></span><span class='preco'>R$ 00</span></li>
|
|
62
|
-
<li><span class='item'>Quinto serviço</span><span class='pontos' aria-hidden='true'></span><span class='preco'>R$ 00</span></li>
|
|
63
|
-
<li><span class='item'>Sexto serviço</span><span class='pontos' aria-hidden='true'></span><span class='preco'>R$ 00</span></li>
|
|
64
|
-
</ul>
|
|
65
|
-
</div>
|
|
66
|
-
</section>
|
|
67
|
-
|
|
68
|
-
<!-- TROQUE: três motivos para escolher vocês, com palavras do dia a dia -->
|
|
69
|
-
<section class='secao motivos'>
|
|
70
|
-
<h2>Por que o bairro volta</h2>
|
|
71
|
-
<ul class='motivos-lista'>
|
|
72
|
-
<li><span class='forma forma-a' aria-hidden='true'></span><h3>O primeiro motivo</h3><p>Uma frase concreta sobre o cuidado, o produto ou o atendimento.</p></li>
|
|
73
|
-
<li><span class='forma forma-b' aria-hidden='true'></span><h3>O segundo motivo</h3><p>Uma frase concreta sobre o cuidado, o produto ou o atendimento.</p></li>
|
|
74
|
-
<li><span class='forma forma-c' aria-hidden='true'></span><h3>O terceiro motivo</h3><p>Uma frase concreta sobre o cuidado, o produto ou o atendimento.</p></li>
|
|
75
|
-
</ul>
|
|
76
|
-
</section>
|
|
77
|
-
|
|
78
|
-
<!-- TROQUE: duas falas de clientes (reais; se não tiver, apague a seção inteira) -->
|
|
79
|
-
<section class='secao falas' aria-label='O que os clientes dizem'>
|
|
80
|
-
<figure class='fala fala-1'>
|
|
81
|
-
<blockquote>“O que um cliente contou sobre vocês, com as palavras dele.”</blockquote>
|
|
82
|
-
<figcaption>Nome, do bairro</figcaption>
|
|
83
|
-
</figure>
|
|
84
|
-
<figure class='fala fala-2'>
|
|
85
|
-
<blockquote>“Outra fala curta, de outra pessoa, sobre outro motivo.”</blockquote>
|
|
86
|
-
<figcaption>Nome, cliente desde 2020</figcaption>
|
|
87
|
-
</figure>
|
|
88
|
-
</section>
|
|
89
|
-
|
|
90
|
-
<!-- TROQUE: os horários (o texto E os data-abre/data-fecha, no formato 08:00; dia fechado fica sem data-abre) e o endereço -->
|
|
91
|
-
<section id='horarios' class='secao onde'>
|
|
92
|
-
<div class='onde-horas'>
|
|
93
|
-
<h2>Quando estamos abertos</h2>
|
|
94
|
-
<table id='tabelaHorarios' class='horas'>
|
|
95
|
-
<tbody>
|
|
96
|
-
<tr data-dia='1' data-abre='08:00' data-fecha='18:00'><th scope='row'>Segunda</th><td>8h às 18h</td></tr>
|
|
97
|
-
<tr data-dia='2' data-abre='08:00' data-fecha='18:00'><th scope='row'>Terça</th><td>8h às 18h</td></tr>
|
|
98
|
-
<tr data-dia='3' data-abre='08:00' data-fecha='18:00'><th scope='row'>Quarta</th><td>8h às 18h</td></tr>
|
|
99
|
-
<tr data-dia='4' data-abre='08:00' data-fecha='18:00'><th scope='row'>Quinta</th><td>8h às 18h</td></tr>
|
|
100
|
-
<tr data-dia='5' data-abre='08:00' data-fecha='18:00'><th scope='row'>Sexta</th><td>8h às 18h</td></tr>
|
|
101
|
-
<tr data-dia='6' data-abre='08:00' data-fecha='13:00'><th scope='row'>Sábado</th><td>8h às 13h</td></tr>
|
|
102
|
-
<tr data-dia='0'><th scope='row'>Domingo</th><td>Fechado</td></tr>
|
|
103
|
-
</tbody>
|
|
104
|
-
</table>
|
|
105
|
-
</div>
|
|
106
|
-
<div class='onde-endereco'>
|
|
107
|
-
<h2>Como chegar</h2>
|
|
108
|
-
<p class='endereco'>Rua Exemplo, 100<br>Seu Bairro, Sua Cidade</p>
|
|
109
|
-
<p>Um ponto de referência que ajuda a achar: ao lado de algo que todo mundo conhece.</p>
|
|
110
|
-
<a class='botao botao-claro' href='https://www.google.com/maps/search/?api=1&query=Rua+Exemplo+100+Sua+Cidade'>Abrir no mapa</a>
|
|
111
|
-
</div>
|
|
112
|
-
</section>
|
|
113
|
-
|
|
114
|
-
<!-- TROQUE: as dúvidas que o balcão mais ouve (3 a 5) -->
|
|
115
|
-
<section class='secao duvidas'>
|
|
116
|
-
<h2>Perguntas de sempre</h2>
|
|
117
|
-
<details><summary>Uma pergunta que todo cliente faz?</summary><p>A resposta curta e direta.</p></details>
|
|
118
|
-
<details><summary>Outra pergunta comum?</summary><p>A resposta curta e direta.</p></details>
|
|
119
|
-
<details><summary>Mais uma pergunta?</summary><p>A resposta curta e direta.</p></details>
|
|
120
|
-
</section>
|
|
121
|
-
</main>
|
|
122
|
-
|
|
123
|
-
<!-- TROQUE: o convite final, o nome, o ano e o Instagram -->
|
|
124
|
-
<footer class='rodape'>
|
|
125
|
-
<p class='rodape-convite'>Passa aqui, ou manda uma mensagem.</p>
|
|
126
|
-
<a href='https://wa.me/5500000000000' class='botao'>Chamar no WhatsApp</a>
|
|
127
|
-
<p class='rodape-fim'>© 2026 Sua Marca, Seu Bairro. <a href='https://instagram.com/'>@suamarca</a></p>
|
|
128
|
-
</footer>
|
|
129
|
-
</body>
|
|
130
|
-
</html>
|
package/kits/acolhedor/script.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// Acolhedor: o que o cliente de bairro mais pergunta é "está aberto agora?".
|
|
2
|
-
// O script lê a tabela de horários (data-abre / data-fecha) e responde
|
|
3
|
-
// no topo da página, e pinta a linha de hoje. Sem animação decorativa.
|
|
4
|
-
(function(){
|
|
5
|
-
var tabela=document.getElementById('tabelaHorarios');
|
|
6
|
-
var status=document.getElementById('statusHoje');
|
|
7
|
-
if(!tabela||!status) return;
|
|
8
|
-
var agora=new Date(), dia=agora.getDay();
|
|
9
|
-
var minutos=agora.getHours()*60+agora.getMinutes();
|
|
10
|
-
function emMinutos(hhmm){ var p=String(hhmm||'').split(':'); return (+p[0])*60+(+p[1]||0); }
|
|
11
|
-
function falar(hhmm){ var p=String(hhmm).split(':'); return (+p[0])+'h'+(p[1]&&p[1]!=='00'?p[1]:''); }
|
|
12
|
-
var linha=tabela.querySelector('tr[data-dia="'+dia+'"]');
|
|
13
|
-
if(!linha) return;
|
|
14
|
-
linha.classList.add('hoje');
|
|
15
|
-
var abre=linha.getAttribute('data-abre'), fecha=linha.getAttribute('data-fecha');
|
|
16
|
-
if(!abre||!fecha){ status.textContent='Hoje estamos fechados'; return; }
|
|
17
|
-
if(minutos>=emMinutos(abre)&&minutos<emMinutos(fecha)){
|
|
18
|
-
status.textContent='Aberto agora, até as '+falar(fecha);
|
|
19
|
-
status.parentNode.classList.add('aberto');
|
|
20
|
-
} else if(minutos<emMinutos(abre)){
|
|
21
|
-
status.textContent='Hoje abrimos às '+falar(abre);
|
|
22
|
-
} else {
|
|
23
|
-
status.textContent='Já fechamos por hoje';
|
|
24
|
-
}
|
|
25
|
-
})();
|