primocode 9.8.0-beta.4 → 9.8.0-beta.6
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 +15 -4
- package/app/ansi.js +55 -0
- package/app/ide.css +345 -0
- package/app/ide.html +183 -0
- package/app/ide.js +639 -0
- package/bin/primocode.js +315 -83
- package/kits/acolhedor/index.html +130 -0
- package/kits/acolhedor/script.js +25 -0
- package/kits/acolhedor/style.css +146 -0
- package/kits/editorial/index.html +115 -0
- package/kits/editorial/script.js +19 -0
- package/kits/editorial/style.css +112 -0
- package/kits/premium/index.html +83 -0
- package/kits/premium/script.js +11 -0
- package/kits/premium/style.css +87 -0
- package/kits/vitrine/index.html +108 -0
- package/kits/vitrine/script.js +20 -0
- package/kits/vitrine/style.css +117 -0
- package/lib/app.js +41 -6
- package/lib/browser.js +14 -1
- package/lib/checkpoint-git.js +218 -0
- package/lib/checkpoint.js +159 -44
- package/lib/comandos-extra.js +481 -0
- package/lib/config.js +5 -2
- package/lib/ide.js +337 -0
- package/lib/kits.js +165 -54
- package/lib/skills.js +6 -1
- package/lib/ui.js +171 -17
- package/package.json +2 -2
- package/skills/apresentacao-pptx/SKILL.md +52 -0
- package/skills/cartaz-de-arte/LICENSE.txt +202 -0
- package/skills/cartaz-de-arte/SKILL.md +47 -0
- package/skills/criar-skill/LICENSE.txt +202 -0
- package/skills/criar-skill/SKILL.md +49 -0
- package/skills/depurar-de-verdade/LICENSE.txt +21 -0
- package/skills/depurar-de-verdade/SKILL.md +14 -5
- package/skills/documento-word/SKILL.md +57 -0
- package/skills/interface-revisada/SKILL.md +47 -0
- package/skills/me-ensina/LICENSE.txt +21 -0
- package/skills/me-ensina/SKILL.md +27 -0
- package/skills/me-entrevista/LICENSE.txt +21 -0
- package/skills/me-entrevista/SKILL.md +27 -0
- package/skills/pdf-bonito/SKILL.md +48 -0
- package/skills/planilha-excel/SKILL.md +47 -0
- package/skills/react-sem-dor/SKILL.md +48 -0
- package/skills/revisao-antes-de-entregar/LICENSE.txt +21 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +6 -1
- package/skills/temas-prontos/LICENSE.txt +202 -0
- package/skills/temas-prontos/SKILL.md +50 -0
- package/skills/testar-o-site/LICENSE.txt +202 -0
- package/skills/testar-o-site/SKILL.md +46 -0
- package/skills/video-no-remotion/SKILL.md +48 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* comandos-extra.js — a lógica dos comandos pequenos do terminal.
|
|
3
|
+
*
|
|
4
|
+
* "Mais mascotes, mais comandos." (dono, 24/09/2026)
|
|
5
|
+
*
|
|
6
|
+
* /novo, /projetos, /abrir, /doctor, /custo, /copiar, /historico e /atalhos.
|
|
7
|
+
* Cada um é pequeno e serve a quem nunca programou: saber onde estão os
|
|
8
|
+
* projetos, abrir o que foi feito, descobrir por que algo não funciona, copiar
|
|
9
|
+
* a resposta sem selecionar com o mouse.
|
|
10
|
+
*
|
|
11
|
+
* O desenho na tela mora no bin/primocode.js; aqui fica o que dá para medir
|
|
12
|
+
* sem terminal. Por isso quase tudo recebe as dependências de fora (quem
|
|
13
|
+
* roda comando, quem sonda porta, quem fala com o servidor): o teste troca
|
|
14
|
+
* por falsos e confere a regra sem abrir navegador nem tocar a rede.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const http = require('http');
|
|
22
|
+
const { spawnSync, execFileSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
// ── tamanho e tempo, do jeito que se fala ────────────────────────────────
|
|
25
|
+
|
|
26
|
+
function fmtTamanho(bytes) {
|
|
27
|
+
const b = Number(bytes) || 0;
|
|
28
|
+
if (b < 1024) return `${b} B`;
|
|
29
|
+
if (b < 1024 * 1024) return `${(b / 1024).toFixed(0)} KB`;
|
|
30
|
+
if (b < 1024 ** 3) return `${(b / 1024 / 1024).toFixed(1)} MB`;
|
|
31
|
+
return `${(b / 1024 ** 3).toFixed(1)} GB`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* "há 3 h" diz mais que "24/09/2026 14:02" para quem procura o projeto de
|
|
35
|
+
ontem. Passou de uma semana, a data volta: "há 23 dias" obriga a fazer
|
|
36
|
+
conta. */
|
|
37
|
+
function quandoFoi(ms, agora = Date.now()) {
|
|
38
|
+
const s = Math.max(0, Math.round((agora - ms) / 1000));
|
|
39
|
+
if (s < 60) return 'agora';
|
|
40
|
+
if (s < 3600) return `há ${Math.floor(s / 60)} min`;
|
|
41
|
+
if (s < 86400) return `há ${Math.floor(s / 3600)} h`;
|
|
42
|
+
const dias = Math.floor(s / 86400);
|
|
43
|
+
if (dias === 1) return 'ontem';
|
|
44
|
+
if (dias < 7) return `há ${dias} dias`;
|
|
45
|
+
return new Date(ms).toLocaleDateString('pt-BR');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── /projetos ────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/* O que não é do projeto: dependência baixada e histórico do git. Entrar em
|
|
51
|
+
node_modules para somar tamanho custa segundos numa pasta grande, e o
|
|
52
|
+
número que a pessoa quer é o do que ELA (ou o agente) escreveu. */
|
|
53
|
+
const PULAR = new Set(['node_modules', '.git', '.next', 'dist', 'build', '.cache', '__pycache__', '.venv', 'venv']);
|
|
54
|
+
const TETO_ENTRADAS = 4000;
|
|
55
|
+
|
|
56
|
+
/** Tamanho e última mudança de uma pasta, olhando os arquivos de dentro. */
|
|
57
|
+
function medirPasta(dir) {
|
|
58
|
+
let bytes = 0, arquivos = 0, modificado = 0, vistos = 0, cortado = false;
|
|
59
|
+
const fila = [dir];
|
|
60
|
+
try { modificado = fs.statSync(dir).mtimeMs; } catch { /* sumiu */ }
|
|
61
|
+
while (fila.length) {
|
|
62
|
+
const atual = fila.pop();
|
|
63
|
+
let itens;
|
|
64
|
+
try { itens = fs.readdirSync(atual, { withFileTypes: true }); } catch { continue; }
|
|
65
|
+
for (const it of itens) {
|
|
66
|
+
// Teto: uma pasta com cem mil arquivos não pode segurar o /projetos.
|
|
67
|
+
if (++vistos > TETO_ENTRADAS) { cortado = true; fila.length = 0; break; }
|
|
68
|
+
if (PULAR.has(it.name)) continue;
|
|
69
|
+
const p = path.join(atual, it.name);
|
|
70
|
+
if (it.isDirectory()) { fila.push(p); continue; }
|
|
71
|
+
if (!it.isFile()) continue;
|
|
72
|
+
try {
|
|
73
|
+
const st = fs.statSync(p);
|
|
74
|
+
bytes += st.size;
|
|
75
|
+
arquivos++;
|
|
76
|
+
if (st.mtimeMs > modificado) modificado = st.mtimeMs;
|
|
77
|
+
} catch { /* arquivo que sumiu no meio */ }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { bytes, arquivos, modificado, cortado };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Os projetos de ~/primocode, do mais recente para o mais antigo.
|
|
85
|
+
* "Mais recente" é o último ARQUIVO mexido lá dentro — a data da pasta só
|
|
86
|
+
* muda quando nasce ou some um item, e editar o index.html não conta.
|
|
87
|
+
*/
|
|
88
|
+
function listarProjetos(raiz) {
|
|
89
|
+
let nomes = [];
|
|
90
|
+
try {
|
|
91
|
+
nomes = fs.readdirSync(raiz, { withFileTypes: true })
|
|
92
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith('.'))
|
|
93
|
+
.map((d) => d.name);
|
|
94
|
+
} catch { return []; }
|
|
95
|
+
return nomes
|
|
96
|
+
.map((nome) => ({ nome, dir: path.join(raiz, nome), ...medirPasta(path.join(raiz, nome)) }))
|
|
97
|
+
.sort((a, b) => b.modificado - a.modificado);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── /novo ────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* O nome da pasta do /novo. O do modelo (lib/nome-projeto.js) quando veio; o
|
|
104
|
+
* `deriveProjectName` quando não veio. Nunca o de uma pasta que já existe —
|
|
105
|
+
* /novo é justamente "não continue aquele": cair dentro de outro projeto
|
|
106
|
+
* por coincidência de nome é o defeito que ele existe para evitar.
|
|
107
|
+
*/
|
|
108
|
+
function nomeDoNovo({ descricao, nomes, sugerido, derivar, slugify, semColisao }) {
|
|
109
|
+
const base = slugify(sugerido || (derivar ? derivar(descricao) : '') || 'projeto') || 'projeto';
|
|
110
|
+
return semColisao(base, nomes || []);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** O pedido que vai ao agente: a descrição, com a ordem de começar do zero. */
|
|
114
|
+
function pedidoDoNovo(descricao, nome) {
|
|
115
|
+
return `Projeto NOVO, do zero, na pasta ${nome} (acabou de ser criada e está vazia). `
|
|
116
|
+
+ `Não reaproveite nem abra outros projetos. O pedido: ${descricao}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── /abrir ───────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
/* As portas que o projeto diz usar. Script com --port/-p/PORT= manda; sem
|
|
122
|
+
isso, a porta padrão do framework que ele instala. */
|
|
123
|
+
const PORTA_PADRAO = [
|
|
124
|
+
['vite', 5173], ['next', 3000], ['react-scripts', 3000], ['astro', 4321],
|
|
125
|
+
['@angular/core', 4200], ['nuxt', 3000], ['@sveltejs/kit', 5173], ['express', 3000],
|
|
126
|
+
['parcel', 1234], ['gatsby', 8000], ['@remix-run/dev', 3000],
|
|
127
|
+
];
|
|
128
|
+
/* Sem framework conhecido, as portas de sempre — mas SÓ se o projeto tem cara
|
|
129
|
+
de servidor (script dev/start, app.py…). Sondar 8000 às cegas abria o
|
|
130
|
+
servidor de OUTRO programa que estivesse de pé na máquina: medido aqui,
|
|
131
|
+
um projeto só com memory.md abriu um localhost:8000 que não era dele. */
|
|
132
|
+
const PORTAS_NODE = [3000, 5173, 8080];
|
|
133
|
+
const PORTAS_PYTHON = [5000, 8000];
|
|
134
|
+
const SERVIDOR_PYTHON = ['app.py', 'main.py', 'server.py', 'manage.py'];
|
|
135
|
+
|
|
136
|
+
function portasDoProjeto(dir) {
|
|
137
|
+
let pkg;
|
|
138
|
+
try { pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')); } catch { pkg = null; }
|
|
139
|
+
if (!pkg) return SERVIDOR_PYTHON.some((f) => fs.existsSync(path.join(dir, f))) ? PORTAS_PYTHON.slice() : [];
|
|
140
|
+
const portas = [];
|
|
141
|
+
const scripts = Object.values(pkg.scripts || {}).join(' ');
|
|
142
|
+
for (const m of scripts.matchAll(/(?:--port[= ]|-p |PORT=)(\d{2,5})\b/g)) portas.push(Number(m[1]));
|
|
143
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
144
|
+
for (const [dep, porta] of PORTA_PADRAO) if (deps[dep]) portas.push(porta);
|
|
145
|
+
if (!portas.length && pkg.scripts && (pkg.scripts.dev || pkg.scripts.start)) portas.push(...PORTAS_NODE);
|
|
146
|
+
return [...new Set(portas)];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** A porta responde HTTP? Um GET curto em 127.0.0.1, sem esperar o corpo. */
|
|
150
|
+
function sondarPorta(porta, ms = 600) {
|
|
151
|
+
return new Promise((ok) => {
|
|
152
|
+
const req = http.get({ host: '127.0.0.1', port: porta, path: '/', timeout: ms }, (res) => {
|
|
153
|
+
res.resume();
|
|
154
|
+
ok(true);
|
|
155
|
+
});
|
|
156
|
+
req.on('timeout', () => { req.destroy(); ok(false); });
|
|
157
|
+
req.on('error', () => ok(false));
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Onde procurar a página pronta: a raiz, e as pastas em que o build costuma
|
|
162
|
+
deixar o resultado. */
|
|
163
|
+
const PAGINAS = ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html', 'src/index.html', 'docs/index.html'];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* O que o /abrir deve abrir.
|
|
167
|
+
* 1. um servidor do projeto que esteja de pé (a porta que ele diz usar);
|
|
168
|
+
* 2. a index.html, se houver.
|
|
169
|
+
* Nessa ordem porque a página servida é a de verdade (com rota, API, build);
|
|
170
|
+
* o arquivo aberto direto às vezes nem carrega o JavaScript.
|
|
171
|
+
*/
|
|
172
|
+
async function acharAlvoAbrir(dir, { sondar = sondarPorta } = {}) {
|
|
173
|
+
for (const porta of portasDoProjeto(dir)) {
|
|
174
|
+
if (await sondar(porta)) return { tipo: 'url', url: `http://localhost:${porta}`, porque: 'o servidor do projeto está de pé' };
|
|
175
|
+
}
|
|
176
|
+
for (const rel of PAGINAS) {
|
|
177
|
+
const p = path.join(dir, rel);
|
|
178
|
+
if (fs.existsSync(p)) return { tipo: 'arquivo', caminho: p, porque: rel };
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── /doctor ──────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
function versaoMinima(atual, minima) {
|
|
186
|
+
const a = String(atual).replace(/^v/, '').split('.').map(Number);
|
|
187
|
+
const m = String(minima).replace(/^[^\d]*/, '').split('.').map(Number);
|
|
188
|
+
for (let i = 0; i < 3; i++) {
|
|
189
|
+
if ((a[i] || 0) > (m[i] || 0)) return true;
|
|
190
|
+
if ((a[i] || 0) < (m[i] || 0)) return false;
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* O Python com Quartz/AppKit é o cursor do Primo (lib/cursor.js). A mesma
|
|
196
|
+
pergunta que ele faz, sem instalar nada: o doctor só olha. */
|
|
197
|
+
function pythonComQuartz() {
|
|
198
|
+
let py = null;
|
|
199
|
+
try { py = require('./voz.js').python(); } catch { /* sem voz.js */ }
|
|
200
|
+
if (!py) return { py: null, tem: false };
|
|
201
|
+
try {
|
|
202
|
+
execFileSync(py, ['-c', 'import Quartz, AppKit'], { stdio: 'ignore', timeout: 20000 });
|
|
203
|
+
return { py, tem: true };
|
|
204
|
+
} catch { return { py, tem: false }; }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function espacoLivre(dir) {
|
|
208
|
+
// statfs é do Node 18.15+; o engines pede 18.17, mas sem ele não quebra.
|
|
209
|
+
if (typeof fs.statfsSync !== 'function') return null;
|
|
210
|
+
try { const s = fs.statfsSync(dir); return s.bavail * s.bsize; } catch { return null; }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function comPrazo(promessa, ms) {
|
|
214
|
+
let t;
|
|
215
|
+
return Promise.race([
|
|
216
|
+
promessa,
|
|
217
|
+
new Promise((_, nao) => { t = setTimeout(() => nao(new Error(`não respondeu em ${Math.round(ms / 1000)}s`)), ms); }),
|
|
218
|
+
]).finally(() => clearTimeout(t));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const UM_GB = 1024 ** 3;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* A lista do /doctor. Cada linha: `ok` true (✓), false (✗) ou null (·, não se
|
|
225
|
+
* aplica aqui), o que foi olhado, o detalhe, e — quando ✗ — como consertar em
|
|
226
|
+
* uma linha. Consertar é a parte que importa: "✗ Chrome" sozinho manda a
|
|
227
|
+
* pessoa pesquisar; "instale em google.com/chrome" resolve.
|
|
228
|
+
*
|
|
229
|
+
* Tudo injetável (`dep`), para o teste passar os casos sem rede nem Chrome.
|
|
230
|
+
*/
|
|
231
|
+
async function diagnosticar(dep = {}) {
|
|
232
|
+
const d = {
|
|
233
|
+
versaoNode: process.version,
|
|
234
|
+
minimaNode: '18.17.0',
|
|
235
|
+
plataforma: process.platform,
|
|
236
|
+
saude: null, // async () => objeto do /health
|
|
237
|
+
prazoServidor: 15000,
|
|
238
|
+
provedor: () => null, // provedor do /api, ou null
|
|
239
|
+
conferirChave: null, // async (id, chave) => { ok, error }
|
|
240
|
+
cliInstalado: () => false, // (id) => bool
|
|
241
|
+
acharChrome: () => null,
|
|
242
|
+
pythonQuartz: pythonComQuartz,
|
|
243
|
+
raiz: null,
|
|
244
|
+
espaco: espacoLivre,
|
|
245
|
+
arquivoConfig: null,
|
|
246
|
+
...dep,
|
|
247
|
+
};
|
|
248
|
+
const linhas = [];
|
|
249
|
+
const poe = (ok, item, detalhe, conserto) => linhas.push({ ok, item, detalhe, conserto: ok === false ? conserto : undefined });
|
|
250
|
+
|
|
251
|
+
// Node
|
|
252
|
+
const nodeOk = versaoMinima(d.versaoNode, d.minimaNode);
|
|
253
|
+
poe(nodeOk, 'Node', d.versaoNode, `atualize para ${d.minimaNode} ou mais em nodejs.org`);
|
|
254
|
+
|
|
255
|
+
// Servidor
|
|
256
|
+
if (d.saude) {
|
|
257
|
+
try {
|
|
258
|
+
const h = await comPrazo(d.saude(), d.prazoServidor);
|
|
259
|
+
poe(true, 'Servidor', `${h && h.status || 'ok'}${h && h.version ? ' · v' + h.version : ''}`);
|
|
260
|
+
} catch (e) {
|
|
261
|
+
poe(false, 'Servidor', e.message, 'confira a internet; se acabou de ligar, ele acorda em até 1 min — rode /doctor de novo');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// /api
|
|
266
|
+
const p = d.provedor();
|
|
267
|
+
if (!p) {
|
|
268
|
+
poe(null, '/api', 'não configurado — os pedidos vão pelo servidor (normal)');
|
|
269
|
+
} else if (p.cli) {
|
|
270
|
+
const ok = !!d.cliInstalado(p.id);
|
|
271
|
+
poe(ok, '/api', p.nome, `o ${p.nome} não está instalado ou logado — instale e faça login, ou /api off`);
|
|
272
|
+
} else if (d.conferirChave) {
|
|
273
|
+
let r;
|
|
274
|
+
try { r = await comPrazo(d.conferirChave(p.id, p.chave), d.prazoServidor); } catch (e) { r = { ok: false, error: e.message }; }
|
|
275
|
+
poe(!!r.ok, '/api', `${p.nome} · ${p.modelo}${r.ok ? '' : ' · ' + r.error}`, `cole a chave de novo com /api, ou /api off para voltar ao servidor`);
|
|
276
|
+
} else {
|
|
277
|
+
poe(true, '/api', `${p.nome} · ${p.modelo}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Chrome
|
|
281
|
+
let chrome = null;
|
|
282
|
+
try { chrome = d.acharChrome(); } catch { /* sem módulo */ }
|
|
283
|
+
poe(!!chrome, 'Navegador', chrome || 'nenhum Chrome/Edge/Brave encontrado',
|
|
284
|
+
'instale o Google Chrome (google.com/chrome), ou aponte PRIMOCODE_CHROME para o seu');
|
|
285
|
+
|
|
286
|
+
// Cursor (Python + PyObjC) — só existe no macOS
|
|
287
|
+
if (d.plataforma !== 'darwin') {
|
|
288
|
+
poe(null, 'Cursor do Primo', 'só no macOS');
|
|
289
|
+
} else {
|
|
290
|
+
const q = d.pythonQuartz();
|
|
291
|
+
if (!q.py) poe(false, 'Cursor do Primo', 'sem Python 3', 'instale o Python 3 em python.org e abra o terminal de novo');
|
|
292
|
+
else poe(!!q.tem, 'Cursor do Primo', q.tem ? `${q.py} com PyObjC` : `${q.py} sem PyObjC`,
|
|
293
|
+
`ele tenta instalar sozinho no primeiro uso; se falhar: ${q.py} -m pip install pyobjc-framework-Quartz pyobjc-framework-Cocoa`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Disco
|
|
297
|
+
if (d.raiz) {
|
|
298
|
+
const livre = d.espaco(d.raiz);
|
|
299
|
+
if (livre == null) poe(null, 'Disco', 'não consegui medir aqui');
|
|
300
|
+
else poe(livre >= UM_GB, 'Disco', `${fmtTamanho(livre)} livres`, 'menos de 1 GB livre: apague projetos velhos em ~/primocode ou esvazie a lixeira');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Config
|
|
304
|
+
if (d.arquivoConfig) {
|
|
305
|
+
if (!fs.existsSync(d.arquivoConfig)) poe(true, 'Config', 'ainda não existe (usa o padrão)');
|
|
306
|
+
else {
|
|
307
|
+
try { JSON.parse(fs.readFileSync(d.arquivoConfig, 'utf8')); poe(true, 'Config', d.arquivoConfig); }
|
|
308
|
+
catch (e) { poe(false, 'Config', `não consegui ler: ${e.message}`, `apague ${d.arquivoConfig} — ele nasce de novo no padrão`); }
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return linhas;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── /custo ───────────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
/* O contador da sessão. Cada pedido ao modelo passa por lib/api.js; em vez
|
|
317
|
+
de mexer lá dentro, as funções exportadas são embrulhadas uma vez na
|
|
318
|
+
abertura. O act.js chama `motor.requestAct` com `motor = api` (o próprio
|
|
319
|
+
objeto do módulo), então o embrulho pega o agente, o chat, o resumo e o
|
|
320
|
+
plano — todo mundo que conversa com o modelo.
|
|
321
|
+
|
|
322
|
+
Token exato só quando o provedor manda `usage`. Sem isso, a conta é a de
|
|
323
|
+
bolso: 4 caracteres ≈ 1 token. O /custo diz qual das duas está mostrando. */
|
|
324
|
+
function novoContador() { return { porProvedor: {}, desde: Date.now() }; }
|
|
325
|
+
|
|
326
|
+
const tam = (x) => { try { return typeof x === 'string' ? x.length : JSON.stringify(x || '').length; } catch { return 0; } };
|
|
327
|
+
|
|
328
|
+
function registrar(cont, provedor) {
|
|
329
|
+
const b = cont.porProvedor[provedor] || (cont.porProvedor[provedor] = {
|
|
330
|
+
pedidos: 0, charsEnviados: 0, charsRecebidos: 0, tokEnviados: 0, tokRecebidos: 0, exatos: 0,
|
|
331
|
+
});
|
|
332
|
+
b.pedidos++;
|
|
333
|
+
return b;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function medirEnvio(b, payload) {
|
|
337
|
+
// O token da conta e o sinal de abortar não são conteúdo: não vão pro modelo.
|
|
338
|
+
const { token, sinal, ...corpo } = payload || {};
|
|
339
|
+
b.charsEnviados += tam(corpo);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function medirEvento(b, evt) {
|
|
343
|
+
if (!evt) return;
|
|
344
|
+
if (evt.delta) b.charsRecebidos += String(evt.delta).length;
|
|
345
|
+
if (evt.tool_call) b.charsRecebidos += tam(evt.tool_call);
|
|
346
|
+
const u = evt.usage;
|
|
347
|
+
if (u && (u.prompt_tokens || u.completion_tokens || u.input_tokens || u.output_tokens)) {
|
|
348
|
+
b.tokEnviados += Number(u.prompt_tokens || u.input_tokens || 0);
|
|
349
|
+
b.tokRecebidos += Number(u.completion_tokens || u.output_tokens || 0);
|
|
350
|
+
b.exatos++;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function instalarContador(api, cont, quemAtende) {
|
|
355
|
+
if (!api || api.__contador) return;
|
|
356
|
+
api.__contador = cont;
|
|
357
|
+
for (const nome of ['requestAct', 'requestChat']) {
|
|
358
|
+
const original = api[nome];
|
|
359
|
+
if (typeof original !== 'function') continue;
|
|
360
|
+
api[nome] = function (servidor, payload, onEvent) {
|
|
361
|
+
const b = registrar(cont, quemAtende());
|
|
362
|
+
medirEnvio(b, payload);
|
|
363
|
+
return original.call(this, servidor, payload, (evt) => {
|
|
364
|
+
medirEvento(b, evt);
|
|
365
|
+
return onEvent ? onEvent(evt) : undefined;
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
for (const nome of ['requestResumo', 'requestPlano', 'requestRefino']) {
|
|
370
|
+
const original = api[nome];
|
|
371
|
+
if (typeof original !== 'function') continue;
|
|
372
|
+
api[nome] = function (servidor, payload) {
|
|
373
|
+
const b = registrar(cont, quemAtende());
|
|
374
|
+
medirEnvio(b, payload);
|
|
375
|
+
return Promise.resolve(original.call(this, servidor, payload)).then((r) => {
|
|
376
|
+
b.charsRecebidos += tam(r);
|
|
377
|
+
if (r && r.usage) medirEvento(b, { usage: r.usage });
|
|
378
|
+
return r;
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** As linhas do /custo: por provedor, e o total. */
|
|
385
|
+
function resumoCusto(cont) {
|
|
386
|
+
const linhas = [];
|
|
387
|
+
let total = { pedidos: 0, enviados: 0, recebidos: 0 };
|
|
388
|
+
for (const [provedor, b] of Object.entries(cont.porProvedor)) {
|
|
389
|
+
// Com `usage` em todos os pedidos, vale o número do provedor; senão a
|
|
390
|
+
// estimativa inteira, para não somar exato com chute.
|
|
391
|
+
const exato = b.exatos > 0 && b.exatos === b.pedidos;
|
|
392
|
+
const enviados = exato ? b.tokEnviados : Math.ceil(b.charsEnviados / 4);
|
|
393
|
+
const recebidos = exato ? b.tokRecebidos : Math.ceil(b.charsRecebidos / 4);
|
|
394
|
+
linhas.push({ provedor, pedidos: b.pedidos, enviados, recebidos, exato });
|
|
395
|
+
total.pedidos += b.pedidos; total.enviados += enviados; total.recebidos += recebidos;
|
|
396
|
+
}
|
|
397
|
+
return { linhas, total };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function fmtNumero(n) { return Number(n || 0).toLocaleString('pt-BR'); }
|
|
401
|
+
|
|
402
|
+
// ── /copiar ──────────────────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
/* Quem copia em cada sistema. No Linux há dois mundos (Wayland e X11) e três
|
|
405
|
+
programas; tenta na ordem e diz qual instalar se nenhum existir. */
|
|
406
|
+
function copiadores(plataforma) {
|
|
407
|
+
if (plataforma === 'darwin') return [['pbcopy', []]];
|
|
408
|
+
if (plataforma === 'win32') return [['clip', []]];
|
|
409
|
+
return [['wl-copy', []], ['xclip', ['-selection', 'clipboard']], ['xsel', ['--clipboard', '--input']]];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function copiar(texto, { plataforma = process.platform, rodar = spawnSync } = {}) {
|
|
413
|
+
if (!texto) return { ok: false, error: 'ainda não há resposta para copiar' };
|
|
414
|
+
for (const [cmd, args] of copiadores(plataforma)) {
|
|
415
|
+
let r;
|
|
416
|
+
// O clip do Windows lê UTF-16 com BOM; em UTF-8 os acentos viram lixo.
|
|
417
|
+
const entrada = plataforma === 'win32' ? Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(texto, 'utf16le')]) : texto;
|
|
418
|
+
try { r = rodar(cmd, args, { input: entrada, timeout: 5000 }); } catch { continue; }
|
|
419
|
+
if (r && !r.error && r.status === 0) return { ok: true, via: cmd };
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
ok: false,
|
|
423
|
+
error: plataforma === 'linux'
|
|
424
|
+
? 'não achei wl-copy, xclip nem xsel — instale um (ex.: sudo apt install xclip)'
|
|
425
|
+
: 'a área de transferência não respondeu neste computador',
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ── /historico ───────────────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
/* A busca é sem acento e sem caixa: quem procura "botao" quer achar "Botão". */
|
|
432
|
+
const sem = (x) => String(x || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
|
|
433
|
+
|
|
434
|
+
function buscarHistorico(arquivo, termo, limite = 20) {
|
|
435
|
+
let texto = '';
|
|
436
|
+
try { texto = fs.readFileSync(arquivo, 'utf8'); } catch { return []; }
|
|
437
|
+
const palavras = sem(termo).split(/\s+/).filter(Boolean);
|
|
438
|
+
const achados = [];
|
|
439
|
+
const linhas = texto.split('\n');
|
|
440
|
+
// Do fim para o começo: o mais recente é o que a pessoa quase sempre quer.
|
|
441
|
+
for (let i = linhas.length - 1; i >= 0 && achados.length < limite; i--) {
|
|
442
|
+
if (!linhas[i].trim()) continue;
|
|
443
|
+
let e;
|
|
444
|
+
try { e = JSON.parse(linhas[i]); } catch { continue; }
|
|
445
|
+
if (!e || !e.prompt) continue;
|
|
446
|
+
const alvo = sem(`${e.prompt} ${e.project || ''}`);
|
|
447
|
+
if (palavras.every((p) => alvo.includes(p))) achados.push(e);
|
|
448
|
+
}
|
|
449
|
+
return achados;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ── /atalhos ─────────────────────────────────────────────────────────────
|
|
453
|
+
|
|
454
|
+
/* Tirado do lib/entrada.js (a caixa de digitar) e do main() do CLI — não de
|
|
455
|
+
memória. Se uma tecla mudar lá, é aqui que tem de mudar também. */
|
|
456
|
+
const ATALHOS = [
|
|
457
|
+
['Enter', 'manda o pedido'],
|
|
458
|
+
['Alt+Enter · Ctrl+J', 'quebra a linha sem mandar (ou \\ no fim da linha)'],
|
|
459
|
+
['Esc Esc', 'para o agente no meio do trabalho'],
|
|
460
|
+
['Esc', 'fecha o menu de comandos'],
|
|
461
|
+
['!comando', 'roda direto no shell, sem passar pelo agente'],
|
|
462
|
+
['@arquivo', 'anexa um arquivo do projeto só a este pedido'],
|
|
463
|
+
['/', 'abre o menu de comandos'],
|
|
464
|
+
['Tab · →', 'completa o comando sugerido'],
|
|
465
|
+
['↑ ↓', 'pedidos anteriores (ou anda no menu, se aberto)'],
|
|
466
|
+
['Ctrl+C', 'limpa o que foi digitado; com a caixa vazia, sai'],
|
|
467
|
+
['Ctrl+D', 'sai (com a caixa vazia)'],
|
|
468
|
+
['Ctrl+L', 'limpa a tela'],
|
|
469
|
+
['Ctrl+A · Ctrl+E', 'vai para o começo · o fim da linha'],
|
|
470
|
+
['Ctrl+U · Ctrl+K', 'apaga antes · depois do cursor'],
|
|
471
|
+
['Ctrl+W', 'apaga a palavra antes do cursor'],
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
module.exports = {
|
|
475
|
+
fmtTamanho, quandoFoi, medirPasta, listarProjetos,
|
|
476
|
+
nomeDoNovo, pedidoDoNovo,
|
|
477
|
+
portasDoProjeto, sondarPorta, acharAlvoAbrir,
|
|
478
|
+
diagnosticar, versaoMinima, espacoLivre,
|
|
479
|
+
novoContador, instalarContador, resumoCusto, fmtNumero,
|
|
480
|
+
copiar, copiadores, buscarHistorico, ATALHOS,
|
|
481
|
+
};
|
package/lib/config.js
CHANGED
|
@@ -295,6 +295,9 @@ function loadConfig() {
|
|
|
295
295
|
currentProject: file.currentProject || null,
|
|
296
296
|
userName: file.userName || null,
|
|
297
297
|
mascot: file.mascot || 'primo',
|
|
298
|
+
// O rostinho do mascote depois da resposta. Ligado por padrão; quem acha
|
|
299
|
+
// que é enfeite demais desliga com /mascot reacoes off.
|
|
300
|
+
reacoes: file.reacoes !== false,
|
|
298
301
|
theme: file.theme || 'primo',
|
|
299
302
|
// Modo escolhido dura entre sessões: quem liga o auto quer o auto amanhã.
|
|
300
303
|
mode: process.env.PRIMOCODE_MODE || file.mode || 'auto',
|
|
@@ -310,8 +313,8 @@ function saveConfig(partial) {
|
|
|
310
313
|
ensureDir(CONFIG_DIR);
|
|
311
314
|
const cur = (() => { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; } })();
|
|
312
315
|
// Só persiste o que faz sentido guardar — nada de campos derivados.
|
|
313
|
-
const { server, model, effort, token, autoApply, currentProject, userName, mascot, theme, mode, permissoes } = { ...cur, ...partial };
|
|
314
|
-
const next = { server, model, effort, token, autoApply, currentProject, userName, mascot, theme, mode, permissoes };
|
|
316
|
+
const { server, model, effort, token, autoApply, currentProject, userName, mascot, reacoes, theme, mode, permissoes } = { ...cur, ...partial };
|
|
317
|
+
const next = { server, model, effort, token, autoApply, currentProject, userName, mascot, reacoes, theme, mode, permissoes };
|
|
315
318
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8');
|
|
316
319
|
return next;
|
|
317
320
|
}
|