primocode 9.0.0 → 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -17
- package/bin/primocode.js +88 -15
- package/lib/act.js +2 -2
- package/lib/boas-vindas.js +13 -5
- package/lib/catalogo.js +40 -1
- package/lib/entrada.js +40 -9
- package/lib/{acervo.js → estudio/acervo.js} +1 -1
- package/lib/estudio/assets.js +1 -1
- package/lib/estudio/identidade.js +2 -2
- package/lib/estudio/index.js +1 -1
- package/lib/{marca.js → estudio/marca.js} +1 -1
- package/lib/estudio/remotion.js +40 -2
- package/lib/fala.js +36 -5
- package/lib/markdown.js +376 -0
- package/lib/pasta.js +15 -4
- package/lib/tools.js +55 -6
- package/lib/voz.js +104 -4
- package/package.json +2 -2
- package/studio/pessoa.py +33 -1
- package/voz/primo_voz/__main__.py +138 -16
- package/voz/primo_voz/desperta.py +195 -0
- package/voz/primo_voz/escuta.py +5 -4
- package/voz/primo_voz/orbe.py +42 -0
- /package/lib/{diretor.js → estudio/diretor.js} +0 -0
package/lib/markdown.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markdown.js — a resposta do modelo, desenhada no terminal.
|
|
3
|
+
*
|
|
4
|
+
* ── POR QUE ISTO PRECISOU EXISTIR ────────────────────────────────────────
|
|
5
|
+
* O modelo escreve em Markdown; é o que ele sabe fazer, e é o que o prompt
|
|
6
|
+
* pede. Só que a resposta saía CRUA na tela: `**importante**` aparecia com os
|
|
7
|
+
* asteriscos, a tabela virava um monte de barras verticais desalinhadas, e o
|
|
8
|
+
* bloco de código vinha com as três crases à mostra. Quem lê recebe o material
|
|
9
|
+
* bruto e faz a formatação de cabeça — que é justamente o trabalho que a
|
|
10
|
+
* ferramenta deveria poupar.
|
|
11
|
+
*
|
|
12
|
+
* ── O QUE ESTE ARQUIVO NÃO É ─────────────────────────────────────────────
|
|
13
|
+
* Não é um parser de Markdown. Markdown completo tem aninhamento, HTML embutido
|
|
14
|
+
* e ambiguidade de sobra, e nada disso aparece numa resposta de terminal. Aqui
|
|
15
|
+
* é linha a linha, com três exceções que PRECISAM de bloco: a cerca de código,
|
|
16
|
+
* a tabela e a lista que continua na linha seguinte. O resto é uma passada de
|
|
17
|
+
* expressões regulares sobre o texto da linha.
|
|
18
|
+
*
|
|
19
|
+
* A vantagem prática é o FLUXO: como quase tudo se resolve por linha, a
|
|
20
|
+
* resposta pode ser desenhada enquanto chega, sem esperar terminar. Quem usa
|
|
21
|
+
* vê o texto aparecendo, e não uma tela parada que de repente enche.
|
|
22
|
+
*
|
|
23
|
+
* ── A REGRA DE OURO DO ALINHAMENTO ───────────────────────────────────────
|
|
24
|
+
* Código de cor NÃO ocupa coluna. Toda conta de largura (tabela, régua, quebra)
|
|
25
|
+
* usa `visibleWidth`, nunca `.length` — senão a tabela com uma célula colorida
|
|
26
|
+
* sai torta, e ninguém entende por quê.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
const { c, visibleWidth, stripAnsi } = require('./ui.js');
|
|
32
|
+
|
|
33
|
+
/* Zero dependências, então as cores que faltam no ui.js nascem aqui. O
|
|
34
|
+
itálico e o sublinhado não estão lá porque nada mais no programa usa. */
|
|
35
|
+
const ANSI = (n) => (t) => `\x1b[${n}m${t}\x1b[0m`;
|
|
36
|
+
const italico = ANSI(3);
|
|
37
|
+
const riscado = ANSI(9);
|
|
38
|
+
const inverso = ANSI(7);
|
|
39
|
+
|
|
40
|
+
// ── o que se reconhece numa linha ────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const CERCA = /^\s{0,3}(```|~~~)\s*([a-zA-Z0-9+#._-]*)\s*$/;
|
|
43
|
+
const TITULO = /^\s{0,3}(#{1,6})\s+(.*)$/;
|
|
44
|
+
const MARCADOR = /^(\s*)([-*+])\s+(.*)$/;
|
|
45
|
+
const NUMERADA = /^(\s*)(\d{1,3})([.)])\s+(.*)$/;
|
|
46
|
+
const TAREFA = /^(\s*)[-*+]\s+\[([ xX])\]\s+(.*)$/;
|
|
47
|
+
const CITACAO = /^\s{0,3}>\s?(.*)$/;
|
|
48
|
+
const REGUA = /^\s{0,3}([-*_])\s*(?:\1\s*){2,}$/;
|
|
49
|
+
const LINHA_TABELA = /^\s*\|.*\|\s*$/;
|
|
50
|
+
const SEPARADOR_TABELA = /^\s*\|(?:\s*:?-{2,}:?\s*\|)+\s*$/;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* As marcas DENTRO de uma linha: negrito, itálico, código, link, riscado.
|
|
54
|
+
*
|
|
55
|
+
* O código inline é resolvido PRIMEIRO e sai de cena: um `**` dentro de
|
|
56
|
+
* `código` é código, não negrito, e resolver na ordem errada come as marcas do
|
|
57
|
+
* próprio trecho literal. Os pedaços saem, o resto é formatado, e eles voltam.
|
|
58
|
+
*/
|
|
59
|
+
function inline(texto) {
|
|
60
|
+
const guardados = [];
|
|
61
|
+
let t = String(texto)
|
|
62
|
+
.replace(/`([^`\n]+)`/g, (_, codigo) => {
|
|
63
|
+
guardados.push(inverso(' ' + codigo + ' '));
|
|
64
|
+
return `\u0000${guardados.length - 1}\u0000`;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
t = t
|
|
68
|
+
// Link: o texto fica, o endereço vai junto em cinza — num terminal
|
|
69
|
+
// ninguém clica no texto, então esconder a URL é esconder a fonte.
|
|
70
|
+
.replace(/\[([^\]\n]+)\]\(([^)\s]+)[^)]*\)/g, (_, rotulo, url) => c.white(rotulo) + ' ' + c.dim(`(${url})`))
|
|
71
|
+
.replace(/(\*\*\*|___)([^\s].*?[^\s]|\S)\1/g, (_, __, x) => c.bold(italico(x)))
|
|
72
|
+
.replace(/(\*\*|__)([^\s].*?[^\s]|\S)\1/g, (_, __, x) => c.bold(c.white(x)))
|
|
73
|
+
.replace(/~~([^\s].*?[^\s]|\S)~~/g, (_, x) => riscado(x))
|
|
74
|
+
// Itálico com UM asterisco/sublinhado. O sublinhado só conta entre
|
|
75
|
+
// limites de palavra: nome_de_variavel_assim não é itálico, e tratá-lo
|
|
76
|
+
// como itálico embaralha todo texto sobre código.
|
|
77
|
+
.replace(/(^|[^*\w])\*([^\s*][^*]*?)\*(?![*\w])/g, (_, antes, x) => antes + italico(x))
|
|
78
|
+
.replace(/(^|[^_\w])_([^\s_][^_]*?)_(?![_\w])/g, (_, antes, x) => antes + italico(x));
|
|
79
|
+
|
|
80
|
+
return t.replace(/\u0000(\d+)\u0000/g, (_, i) => guardados[Number(i)]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── blocos ───────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/* As palavras que ganham cor dentro de um bloco de código. Não é um
|
|
86
|
+
analisador de sintaxe: é a lista das palavras que aparecem em quase toda
|
|
87
|
+
linguagem, mais os literais. Colorir demais fica pior que não colorir, e
|
|
88
|
+
inventar um parser por linguagem é trabalho sem fim para ganho pequeno. */
|
|
89
|
+
const PALAVRAS = new RegExp('\\b(' + [
|
|
90
|
+
'function', 'return', 'const', 'let', 'var', 'class', 'extends', 'new', 'this',
|
|
91
|
+
'if', 'else', 'for', 'while', 'switch', 'case', 'break', 'continue', 'try', 'catch',
|
|
92
|
+
'finally', 'throw', 'async', 'await', 'import', 'from', 'export', 'default',
|
|
93
|
+
'def', 'elif', 'lambda', 'pass', 'with', 'as', 'in', 'is', 'not', 'and', 'or',
|
|
94
|
+
'self', 'None', 'True', 'False', 'null', 'undefined', 'true', 'false',
|
|
95
|
+
'public', 'private', 'static', 'void', 'int', 'string', 'bool', 'struct', 'type',
|
|
96
|
+
'func', 'package', 'interface', 'require', 'module',
|
|
97
|
+
].join('|') + ')\\b', 'g');
|
|
98
|
+
|
|
99
|
+
/* UMA passada só, com alternância — e isso não é preciosismo.
|
|
100
|
+
Na primeira versão eram três `replace` em sequência, e o terceiro (números)
|
|
101
|
+
pintava os dígitos DENTRO dos códigos de cor que os dois primeiros tinham
|
|
102
|
+
acabado de inserir: o "38;5;214" de um escape virava três números coloridos,
|
|
103
|
+
o escape se desmanchava e o terminal cuspia "[38;5;45m" como texto. Com uma
|
|
104
|
+
expressão só, cada pedaço da linha é consumido uma vez e nada pinta o que já
|
|
105
|
+
foi pintado. */
|
|
106
|
+
const CODIGO = new RegExp([
|
|
107
|
+
'(["\'`])(?:\\\\.|(?!\\1)[^\\\\])*\\1', // texto entre aspas
|
|
108
|
+
'(?://|#|--)[^\\n]*$', // comentário até o fim da linha
|
|
109
|
+
PALAVRAS.source, // palavra-chave
|
|
110
|
+
'\\b\\d+(?:\\.\\d+)?\\b', // número
|
|
111
|
+
].join('|'), 'g');
|
|
112
|
+
|
|
113
|
+
function pintarCodigo(linha) {
|
|
114
|
+
return String(linha).replace(CODIGO, (achado) => {
|
|
115
|
+
if (/^["'`]/.test(achado)) return c.ok(achado);
|
|
116
|
+
if (/^(\/\/|#|--)/.test(achado)) return c.dim(achado);
|
|
117
|
+
if (/^\d/.test(achado)) return c.cyan(achado);
|
|
118
|
+
return c.magenta(achado);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** O bloco de código, com a linguagem no canto e uma barra à esquerda. */
|
|
123
|
+
function blocoDeCodigo(linhas, linguagem, largura) {
|
|
124
|
+
const fora = [];
|
|
125
|
+
const barra = c.dim('│');
|
|
126
|
+
if (linguagem) fora.push(' ' + c.dim(linguagem));
|
|
127
|
+
for (const l of linhas) {
|
|
128
|
+
// Corta o que passa da largura em vez de deixar quebrar sozinho: uma
|
|
129
|
+
// linha de código quebrada no meio pela borda do terminal engana quem
|
|
130
|
+
// lê sobre onde ela termina.
|
|
131
|
+
const util = Math.max(20, largura - 4);
|
|
132
|
+
const corpo = visibleWidth(l) > util ? l.slice(0, util - 1) + '…' : l;
|
|
133
|
+
fora.push(' ' + barra + ' ' + pintarCodigo(corpo));
|
|
134
|
+
}
|
|
135
|
+
return fora;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A tabela, com as colunas alinhadas.
|
|
140
|
+
*
|
|
141
|
+
* A largura de cada coluna é a do conteúdo VISÍVEL mais largo — e por isso a
|
|
142
|
+
* conta é com `visibleWidth`: a célula em negrito tem código ANSI dentro, e
|
|
143
|
+
* medir por `.length` faz a coluna crescer sem motivo e desalinhar as outras.
|
|
144
|
+
*/
|
|
145
|
+
function tabela(linhas, largura) {
|
|
146
|
+
const celulas = linhas.map((l) => l.trim().replace(/^\||\|$/g, '').split('|').map((x) => x.trim()));
|
|
147
|
+
const alinhamentos = [];
|
|
148
|
+
let corpo = celulas;
|
|
149
|
+
if (celulas.length > 1 && SEPARADOR_TABELA.test(linhas[1])) {
|
|
150
|
+
for (const s of celulas[1]) {
|
|
151
|
+
alinhamentos.push(/^:.*:$/.test(s) ? 'centro' : /:$/.test(s) ? 'direita' : 'esquerda');
|
|
152
|
+
}
|
|
153
|
+
corpo = [celulas[0], ...celulas.slice(2)];
|
|
154
|
+
}
|
|
155
|
+
const formatadas = corpo.map((linha) => linha.map(inline));
|
|
156
|
+
const colunas = Math.max(...formatadas.map((l) => l.length));
|
|
157
|
+
const larguras = [];
|
|
158
|
+
for (let i = 0; i < colunas; i++) {
|
|
159
|
+
larguras.push(Math.max(...formatadas.map((l) => visibleWidth(l[i] || ''))));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Tabela mais larga que o terminal: encolhe a coluna maior até caber. É
|
|
163
|
+
// melhor que deixar a linha quebrar — quebrada, a tabela deixa de ser
|
|
164
|
+
// tabela e vira um monte de texto solto.
|
|
165
|
+
const gasto = () => larguras.reduce((s, w) => s + w + 3, 1);
|
|
166
|
+
while (gasto() > largura - 2 && Math.max(...larguras) > 6) {
|
|
167
|
+
larguras[larguras.indexOf(Math.max(...larguras))] -= 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const encaixar = (txt, w, alinhamento) => {
|
|
171
|
+
let t = txt;
|
|
172
|
+
if (visibleWidth(t) > w) {
|
|
173
|
+
// Corta contando só o que é visível, para não decepar um código de
|
|
174
|
+
// cor no meio e vazar o escape para a tela.
|
|
175
|
+
const cru = stripAnsi(t).slice(0, Math.max(1, w - 1)) + '…';
|
|
176
|
+
t = cru;
|
|
177
|
+
}
|
|
178
|
+
const falta = w - visibleWidth(t);
|
|
179
|
+
if (alinhamento === 'direita') return ' '.repeat(falta) + t;
|
|
180
|
+
if (alinhamento === 'centro') {
|
|
181
|
+
const e = Math.floor(falta / 2);
|
|
182
|
+
return ' '.repeat(e) + t + ' '.repeat(falta - e);
|
|
183
|
+
}
|
|
184
|
+
return t + ' '.repeat(falta);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const risco = (e, meio, d) => c.dim(e + larguras.map((w) => '─'.repeat(w + 2)).join(meio) + d);
|
|
188
|
+
const fora = [risco('╭', '┬', '╮')];
|
|
189
|
+
formatadas.forEach((linha, i) => {
|
|
190
|
+
const cel = larguras.map((w, j) => encaixar(
|
|
191
|
+
i === 0 ? c.bold(c.white(stripAnsi(linha[j] || ''))) : (linha[j] || ''),
|
|
192
|
+
w, alinhamentos[j] || 'esquerda'));
|
|
193
|
+
fora.push(c.dim('│') + ' ' + cel.join(' ' + c.dim('│') + ' ') + ' ' + c.dim('│'));
|
|
194
|
+
if (i === 0) fora.push(risco('├', '┼', '┤'));
|
|
195
|
+
});
|
|
196
|
+
fora.push(risco('╰', '┴', '╯'));
|
|
197
|
+
return fora;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Quebra um parágrafo na largura, sem partir palavra e sem contar cor. */
|
|
201
|
+
function dobrar(texto, largura, recuo = '') {
|
|
202
|
+
const palavras = String(texto).split(/\s+/).filter(Boolean);
|
|
203
|
+
if (!palavras.length) return [''];
|
|
204
|
+
const fora = [];
|
|
205
|
+
let linha = '';
|
|
206
|
+
for (const p of palavras) {
|
|
207
|
+
const teste = linha ? linha + ' ' + p : p;
|
|
208
|
+
if (visibleWidth(teste) + visibleWidth(recuo) > largura && linha) {
|
|
209
|
+
fora.push(linha);
|
|
210
|
+
linha = p;
|
|
211
|
+
} else {
|
|
212
|
+
linha = teste;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (linha) fora.push(linha);
|
|
216
|
+
return fora;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* O texto inteiro, pronto para a tela.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} texto o Markdown
|
|
223
|
+
* @param {number} largura colunas úteis
|
|
224
|
+
* @returns {string[]} as linhas, já com cor
|
|
225
|
+
*/
|
|
226
|
+
function renderizar(texto, largura = 80) {
|
|
227
|
+
const w = Math.max(24, Number(largura) || 80);
|
|
228
|
+
const linhas = String(texto == null ? '' : texto).split('\n');
|
|
229
|
+
const fora = [];
|
|
230
|
+
|
|
231
|
+
let i = 0;
|
|
232
|
+
while (i < linhas.length) {
|
|
233
|
+
const linha = linhas[i];
|
|
234
|
+
|
|
235
|
+
const cerca = linha.match(CERCA);
|
|
236
|
+
if (cerca) {
|
|
237
|
+
const fim = cerca[1];
|
|
238
|
+
const dentro = [];
|
|
239
|
+
i++;
|
|
240
|
+
while (i < linhas.length && !linhas[i].trim().startsWith(fim)) { dentro.push(linhas[i]); i++; }
|
|
241
|
+
i++; // pula a cerca de baixo
|
|
242
|
+
fora.push(...blocoDeCodigo(dentro, cerca[2], w));
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (LINHA_TABELA.test(linha)) {
|
|
247
|
+
const bloco = [];
|
|
248
|
+
while (i < linhas.length && LINHA_TABELA.test(linhas[i])) { bloco.push(linhas[i]); i++; }
|
|
249
|
+
// Uma linha só entre barras não é tabela: é texto com barra.
|
|
250
|
+
if (bloco.length >= 2) { fora.push(...tabela(bloco, w)); continue; }
|
|
251
|
+
fora.push(inline(bloco[0]));
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (REGUA.test(linha)) { fora.push(c.dim('─'.repeat(Math.min(w, 60)))); i++; continue; }
|
|
256
|
+
|
|
257
|
+
const titulo = linha.match(TITULO);
|
|
258
|
+
if (titulo) {
|
|
259
|
+
const nivel = titulo[1].length;
|
|
260
|
+
const texto1 = stripAnsi(inline(titulo[2]));
|
|
261
|
+
fora.push('');
|
|
262
|
+
fora.push(nivel === 1 ? c.bold(c.brand(texto1.toUpperCase())) : c.bold(c.white(texto1)));
|
|
263
|
+
if (nivel === 1) fora.push(c.dim('─'.repeat(Math.min(w, visibleWidth(texto1)))));
|
|
264
|
+
i++;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const tarefa = linha.match(TAREFA);
|
|
269
|
+
if (tarefa) {
|
|
270
|
+
const marca = /[xX]/.test(tarefa[2]) ? c.ok('✔') : c.dim('○');
|
|
271
|
+
fora.push(...dobrarItem(tarefa[1] + marca + ' ', inline(tarefa[3]), w));
|
|
272
|
+
i++;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const numerada = linha.match(NUMERADA);
|
|
277
|
+
if (numerada) {
|
|
278
|
+
fora.push(...dobrarItem(numerada[1] + c.brand(numerada[2] + '.') + ' ', inline(numerada[4]), w));
|
|
279
|
+
i++;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const marcador = linha.match(MARCADOR);
|
|
284
|
+
if (marcador) {
|
|
285
|
+
// O ponto acompanha o nível: subitem não pode ter a mesma marca do
|
|
286
|
+
// item de cima, ou a hierarquia some.
|
|
287
|
+
const nivel = Math.floor(marcador[1].length / 2);
|
|
288
|
+
const ponto = [c.brand('•'), c.brand('◦'), c.brand('▪')][Math.min(nivel, 2)];
|
|
289
|
+
fora.push(...dobrarItem(marcador[1] + ponto + ' ', inline(marcador[3]), w));
|
|
290
|
+
i++;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const citacao = linha.match(CITACAO);
|
|
295
|
+
if (citacao) {
|
|
296
|
+
for (const l of dobrar(inline(citacao[1]), w - 4)) fora.push(c.dim('▏ ') + c.muted(l));
|
|
297
|
+
i++;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!linha.trim()) { fora.push(''); i++; continue; }
|
|
302
|
+
|
|
303
|
+
for (const l of dobrar(inline(linha), w)) fora.push(l);
|
|
304
|
+
i++;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Linha em branco no fim é espaço morto no terminal.
|
|
308
|
+
while (fora.length && !fora[fora.length - 1].trim()) fora.pop();
|
|
309
|
+
return fora;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Item de lista: a primeira linha leva a marca, as seguintes se alinham a ela. */
|
|
313
|
+
function dobrarItem(marca, conteudo, largura) {
|
|
314
|
+
const recuo = ' '.repeat(visibleWidth(marca));
|
|
315
|
+
const partes = dobrar(conteudo, largura - visibleWidth(marca));
|
|
316
|
+
return partes.map((p, i) => (i === 0 ? marca : recuo) + p);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* O desenhador de FLUXO: recebe a resposta em pedaços e devolve o que já dá
|
|
321
|
+
* para mostrar.
|
|
322
|
+
*
|
|
323
|
+
* A resposta do chat chega em dezenas de pedaços, e esperar o fim para
|
|
324
|
+
* formatar deixaria a tela parada até tudo chegar. Quase tudo aqui se resolve
|
|
325
|
+
* por linha, então a linha é a unidade: fechou, sai formatada.
|
|
326
|
+
*
|
|
327
|
+
* As exceções são os blocos que só fazem sentido inteiros — a cerca de código
|
|
328
|
+
* e a tabela. Enquanto um deles está aberto, as linhas ficam retidas; elas
|
|
329
|
+
* saem juntas quando ele fecha. É o comportamento certo: meia tabela na tela
|
|
330
|
+
* é pior que a tabela um segundo depois.
|
|
331
|
+
*/
|
|
332
|
+
function fluxo(largura = 80) {
|
|
333
|
+
let resto = '';
|
|
334
|
+
let retido = [];
|
|
335
|
+
let dentroDaCerca = false;
|
|
336
|
+
let fimDaCerca = '';
|
|
337
|
+
|
|
338
|
+
const soltar = () => {
|
|
339
|
+
if (!retido.length) return [];
|
|
340
|
+
const saida = renderizar(retido.join('\n'), largura);
|
|
341
|
+
retido = [];
|
|
342
|
+
return saida;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
/** Devolve as linhas prontas para escrever agora. */
|
|
347
|
+
pedaco(texto) {
|
|
348
|
+
resto += String(texto == null ? '' : texto);
|
|
349
|
+
const partes = resto.split('\n');
|
|
350
|
+
resto = partes.pop(); // a última fica para a próxima
|
|
351
|
+
const fora = [];
|
|
352
|
+
for (const linha of partes) {
|
|
353
|
+
const cerca = linha.match(CERCA);
|
|
354
|
+
if (dentroDaCerca) {
|
|
355
|
+
retido.push(linha);
|
|
356
|
+
if (linha.trim().startsWith(fimDaCerca)) { dentroDaCerca = false; fora.push(...soltar()); }
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (cerca) { dentroDaCerca = true; fimDaCerca = cerca[1]; retido.push(linha); continue; }
|
|
360
|
+
if (LINHA_TABELA.test(linha)) { retido.push(linha); continue; }
|
|
361
|
+
// Saiu da tabela: agora ela está inteira.
|
|
362
|
+
if (retido.length) fora.push(...soltar());
|
|
363
|
+
fora.push(...renderizar(linha, largura));
|
|
364
|
+
}
|
|
365
|
+
return fora;
|
|
366
|
+
},
|
|
367
|
+
/** O que sobrou quando a resposta terminou. */
|
|
368
|
+
fim() {
|
|
369
|
+
const pendente = [...retido, ...(resto ? [resto] : [])];
|
|
370
|
+
retido = []; resto = ''; dentroDaCerca = false;
|
|
371
|
+
return pendente.length ? renderizar(pendente.join('\n'), largura) : [];
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
module.exports = { renderizar, fluxo, inline, tabela, dobrar, pintarCodigo, blocoDeCodigo };
|
package/lib/pasta.js
CHANGED
|
@@ -64,16 +64,27 @@ function detectarPasta(texto) {
|
|
|
64
64
|
const s = String(texto || '');
|
|
65
65
|
if (!s.trim()) return null;
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
/* 1. Caminho explícito — `~/...`, absoluto unix com pelo menos dois
|
|
68
|
+
segmentos, ou o caminho do Windows com letra de unidade.
|
|
69
|
+
Um `/algo` solto não conta: seria confundido com comando (`/dir`).
|
|
70
|
+
|
|
71
|
+
O ramo do Windows (`C:\\Users\\joel\\loja`) não existia, e o dois-pontos
|
|
72
|
+
NÃO pode entrar em CARACTERES_DE_CAMINHO para resolver isso: ali ele
|
|
73
|
+
estragaria outros casamentos, como "veja isto: src/app", onde o
|
|
74
|
+
dois-pontos é pontuação da frase e não faz parte do caminho. Por isso
|
|
75
|
+
a unidade é um padrão à parte, ancorado na forma "letra + : + barra". */
|
|
76
|
+
const DRIVE_WIN = `[A-Za-z]:[\\\\/][${CARACTERES_DE_CAMINHO}]*`;
|
|
77
|
+
const explicito = s.match(new RegExp(`(?:^|\\s)(${DRIVE_WIN}|~[${CARACTERES_DE_CAMINHO}]*|/[${CARACTERES_DE_CAMINHO}]+/[${CARACTERES_DE_CAMINHO}]*)`));
|
|
70
78
|
if (explicito) {
|
|
71
79
|
const bruto = limpar(explicito[1]);
|
|
72
80
|
if (bruto && !TEM_EXTENSAO.test(bruto)) return bruto;
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
// 2. Marca explícita de pasta.
|
|
76
|
-
|
|
84
|
+
// A unidade do Windows entra aqui também: sem ela, "abre a pasta
|
|
85
|
+
// C:\\Users\\joel\\loja" casava só o "C" e o agente ia trabalhar numa pasta
|
|
86
|
+
// chamada "C".
|
|
87
|
+
const marcado = s.match(new RegExp(`\\b(?:pasta|diret[óo]rio|dir)\\s+(?:d[eoa]s?\\s+|no\\s+|na\\s+|em\\s+)?["'\`]?(${DRIVE_WIN}|[${CARACTERES_DE_CAMINHO}]+)["'\`]?`, 'i'));
|
|
77
88
|
if (marcado) {
|
|
78
89
|
const bruto = limpar(marcado[1]);
|
|
79
90
|
if (bruto && !TEM_EXTENSAO.test(bruto) && !NAO_E_NOME.has(bruto.toLowerCase())) return bruto;
|
package/lib/tools.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
|
+
const catalogo = require('./catalogo.js');
|
|
11
|
+
|
|
10
12
|
const fs = require('fs');
|
|
11
13
|
const path = require('path');
|
|
12
14
|
const os = require('os');
|
|
@@ -193,14 +195,43 @@ async function runInNewTerminal(command, cwd) {
|
|
|
193
195
|
// mesmo tempo (osascript, cmd.exe, sh) — "command" continua livre de
|
|
194
196
|
// propósito, é o próprio contrato desta tool (abrir terminal com um
|
|
195
197
|
// comando de shell de verdade).
|
|
196
|
-
|
|
198
|
+
/* A CONTRABARRA NÃO PODE ENTRAR NESTA LISTA NO WINDOWS.
|
|
199
|
+
Ela estava, e com isso a tool NUNCA funcionou lá: a pasta de trabalho é
|
|
200
|
+
sempre C:\Users\<nome>\primocode\<projeto> (path.join no win32 usa "\"),
|
|
201
|
+
então TODO caminho casava e a resposta era "Pasta com caractere não
|
|
202
|
+
permitido" — culpando a pasta criada pelo próprio produto, antes de
|
|
203
|
+
tentar abrir terminal nenhum. "sobe o servidor" quebrava 100% das vezes.
|
|
204
|
+
A contrabarra só é perigosa onde ela é ESCAPE (sh, osascript); no cmd.exe
|
|
205
|
+
ela é separador de caminho e nada mais. */
|
|
206
|
+
const proibidos = p === 'win32' ? /["'`;&|%^<>\r\n]/ : /["'`;&|$\\\r\n]/;
|
|
207
|
+
if (proibidos.test(pastaAlvo)) {
|
|
197
208
|
return { ok: false, error: `Pasta com caractere não permitido: ${pastaAlvo}` };
|
|
198
209
|
}
|
|
199
210
|
let cmd;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
211
|
+
let pastaDoProcesso;
|
|
212
|
+
if (p === 'darwin') {
|
|
213
|
+
/* O Terminal.app é outro processo, já rodando: ele NÃO herda a pasta
|
|
214
|
+
daqui, então o `cd` tem de ir no script mesmo. O espaço é escapado
|
|
215
|
+
com contrabarra — "\\ " neste template vira "\ " dentro da string do
|
|
216
|
+
AppleScript, que é o escape que o shell entende. Sem isso, uma pasta
|
|
217
|
+
como ~/Documents/Meus Projetos virava dois argumentos e o cd falhava
|
|
218
|
+
calado: o terminal abria na pasta errada e o comando rodava no lugar
|
|
219
|
+
errado, que é pior que não abrir. */
|
|
220
|
+
cmd = `osascript -e 'tell application "Terminal" to do script "cd ${pastaAlvo.replace(/ /g, '\\\\ ')} && ${command}"'`;
|
|
221
|
+
} else if (p === 'win32') {
|
|
222
|
+
// Aspas em volta do caminho: "C:\Users\Joel Silva\..." é comum, e sem
|
|
223
|
+
// elas o cmd corta no espaço. A aspa dupla já está barrada acima, então
|
|
224
|
+
// não há como sair delas.
|
|
225
|
+
cmd = `start cmd /k "cd /d "${pastaAlvo}" && ${command}"`;
|
|
226
|
+
} else {
|
|
227
|
+
/* No Linux o terminal é filho DESTE processo e herda a pasta — então
|
|
228
|
+
o caminho não precisa entrar na string de shell nenhuma. É a solução
|
|
229
|
+
mais segura das três: o que não é interpolado não pode ser mal
|
|
230
|
+
escapado. */
|
|
231
|
+
cmd = `x-terminal-emulator -e "bash -c '${command}; exec bash'"`;
|
|
232
|
+
pastaDoProcesso = pastaAlvo;
|
|
233
|
+
}
|
|
234
|
+
const r = await runShell(cmd, 60000, pastaDoProcesso);
|
|
204
235
|
return r.ok ? { ok: true, opener: cmd.split(' ')[0] } : { ok: false, error: r.stderr || 'falhou' };
|
|
205
236
|
}
|
|
206
237
|
|
|
@@ -568,7 +599,25 @@ async function despachar(name, args, ctx) {
|
|
|
568
599
|
});
|
|
569
600
|
|
|
570
601
|
default:
|
|
571
|
-
|
|
602
|
+
/* O MODELO PRECISA CONSEGUIR SE CORRIGIR AQUI.
|
|
603
|
+
"Tool não implementada: studio_criar" não diz o que existe,
|
|
604
|
+
e o que acontecia era o agente desistir e responder ao
|
|
605
|
+
usuário que a ferramenta necessária "não está disponível".
|
|
606
|
+
Dizendo QUAIS existem — e a mais parecida — ele chama a
|
|
607
|
+
certa na volta seguinte, na mesma tarefa.
|
|
608
|
+
|
|
609
|
+
Isto é rede, não conserto: a causa é a lista do servidor
|
|
610
|
+
divergir do catálogo daqui, e ela se conserta lá. Mas a rede
|
|
611
|
+
é o que separa uma tarefa que se recupera de uma que morre
|
|
612
|
+
com o usuário esperando. */
|
|
613
|
+
return {
|
|
614
|
+
ok: false,
|
|
615
|
+
error: `A ferramenta "${name}" não existe.`,
|
|
616
|
+
parecidas: catalogo.parecidasCom(name),
|
|
617
|
+
comoResolver: 'chame uma das ferramentas que existem de verdade — as "parecidas" '
|
|
618
|
+
+ 'acima são as de nome mais próximo. Não invente nome de ferramenta, e não '
|
|
619
|
+
+ 'desista da tarefa por causa disto.',
|
|
620
|
+
};
|
|
572
621
|
}
|
|
573
622
|
}
|
|
574
623
|
}
|
package/lib/voz.js
CHANGED
|
@@ -169,10 +169,13 @@ function instalar(py, pacotes, aoFalar = () => {}) {
|
|
|
169
169
|
* @param {string} op.voz
|
|
170
170
|
* @param {(texto:string)=>Promise<string>} op.pensar o agente de verdade
|
|
171
171
|
* @param {(texto:string)=>void} op.mostrar o que aparece no terminal
|
|
172
|
+
* @param {boolean} op.esperando fica dormindo até ouvir o nome
|
|
173
|
+
* @returns {{processo, pronto: Promise}} o processo, e a promessa do fim
|
|
172
174
|
*/
|
|
173
|
-
function abrir({ python: py, servidor, token, voz, pensar, mostrar = () => {} }) {
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
function abrir({ python: py, servidor, token, voz, pensar, mostrar = () => {}, esperando = false }) {
|
|
176
|
+
let p;
|
|
177
|
+
const pronto = new Promise((resolve) => {
|
|
178
|
+
p = spawn(py, ['-m', 'primo_voz'], {
|
|
176
179
|
cwd: PASTA,
|
|
177
180
|
env: {
|
|
178
181
|
...process.env,
|
|
@@ -180,6 +183,8 @@ function abrir({ python: py, servidor, token, voz, pensar, mostrar = () => {} })
|
|
|
180
183
|
PRIMOCODE_SERVER: servidor,
|
|
181
184
|
PRIMOCODE_TOKEN: token || '',
|
|
182
185
|
PRIMOCODE_VOZ: voz || 'antonio',
|
|
186
|
+
// Dormir até ouvir o nome, em vez de já entrar ouvindo.
|
|
187
|
+
PRIMOCODE_VOZ_ESPERA: esperando ? '1' : '',
|
|
183
188
|
PYTHONUNBUFFERED: '1',
|
|
184
189
|
},
|
|
185
190
|
});
|
|
@@ -234,6 +239,101 @@ function abrir({ python: py, servidor, token, voz, pensar, mostrar = () => {} })
|
|
|
234
239
|
p.on('close', () => resolve({ ok: true }));
|
|
235
240
|
p.on('error', (e) => resolve({ ok: false, error: e.message }));
|
|
236
241
|
});
|
|
242
|
+
|
|
243
|
+
/* O PROCESSO VOLTA JUNTO COM A PROMESSA, e não é preciosismo de API.
|
|
244
|
+
No modo de espera o /voice não bloqueia o terminal: ele sobe o ouvinte
|
|
245
|
+
e devolve o prompt. Quem chamou precisa de um jeito de ENCERRAR aquilo
|
|
246
|
+
depois (/voice parar, ou a saída do programa) — e, sem o handle, o
|
|
247
|
+
único jeito seria matar pelo nome do processo, que atropela outra
|
|
248
|
+
sessão do PrimoCode aberta na mesma máquina. */
|
|
249
|
+
return { processo: p, pronto };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
/* ── O RECONHECEDOR LOCAL DO NOME ────────────────────────────────────────
|
|
254
|
+
*
|
|
255
|
+
* Duas funções, e a razão de existirem é a mesma: no modo de espera o
|
|
256
|
+
* microfone fica ABERTO o tempo todo, e há dois jeitos de saber se alguém
|
|
257
|
+
* disse "ei, primo" — mandar tudo o que ele ouve para um servidor, ou
|
|
258
|
+
* reconhecer a palavra aqui dentro.
|
|
259
|
+
*
|
|
260
|
+
* O primeiro é mais fácil de escrever e significa a conversa da casa inteira
|
|
261
|
+
* subindo para a rede o dia inteiro. O segundo custa ~31 MB de modelo, uma
|
|
262
|
+
* vez. Não é escolha difícil, e não é escolha que o programa faça sozinho:
|
|
263
|
+
* `conferirEspera` só RESPONDE o que falta; quem pergunta à pessoa é o CLI.
|
|
264
|
+
*/
|
|
265
|
+
function conferirEspera(py) {
|
|
266
|
+
const perguntar = `
|
|
267
|
+
import json, sys
|
|
268
|
+
sys.path.insert(0, ${JSON.stringify(PASTA)})
|
|
269
|
+
try:
|
|
270
|
+
from primo_voz import desperta
|
|
271
|
+
except Exception as e:
|
|
272
|
+
print(json.dumps({"ok": False, "error": "não consegui ler o módulo: %s" % e})); raise SystemExit
|
|
273
|
+
try:
|
|
274
|
+
import vosk # noqa: F401
|
|
275
|
+
temVosk = True
|
|
276
|
+
except ImportError:
|
|
277
|
+
temVosk = False
|
|
278
|
+
print(json.dumps({
|
|
279
|
+
"ok": bool(temVosk and desperta.tem_modelo()),
|
|
280
|
+
"temVosk": temVosk,
|
|
281
|
+
"temModelo": desperta.tem_modelo(),
|
|
282
|
+
"onde": str(desperta.caminho_do_modelo()),
|
|
283
|
+
}))
|
|
284
|
+
`;
|
|
285
|
+
try {
|
|
286
|
+
const saida = execFileSync(py, ['-c', perguntar],
|
|
287
|
+
{ stdio: ['ignore', 'pipe', 'pipe'], timeout: 30000 }).toString();
|
|
288
|
+
const r = JSON.parse(saida.trim().split('\n').pop());
|
|
289
|
+
if (r.ok) return { ok: true };
|
|
290
|
+
// Falta só o modelo (ou o vosk, que o pip resolve): dá para oferecer.
|
|
291
|
+
return { ok: false, baixavel: true, temVosk: r.temVosk, onde: r.onde,
|
|
292
|
+
error: r.error || 'o reconhecedor local do nome não está instalado' };
|
|
293
|
+
} catch (e) {
|
|
294
|
+
return { ok: false, baixavel: false,
|
|
295
|
+
error: `não consegui conferir o reconhecedor: ${String(e.message || e).slice(-160)}` };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Instala o vosk (se faltar) e baixa o modelo. Uma vez, ~31 MB. */
|
|
300
|
+
function baixarEspera(py) {
|
|
301
|
+
return new Promise((resolve) => {
|
|
302
|
+
const script = `
|
|
303
|
+
import json, subprocess, sys
|
|
304
|
+
sys.path.insert(0, ${JSON.stringify(PASTA)})
|
|
305
|
+
try:
|
|
306
|
+
import vosk # noqa: F401
|
|
307
|
+
except ImportError:
|
|
308
|
+
# As mesmas estratégias do narrar.py: o pip do sistema recusa instalar
|
|
309
|
+
# em Debian/Ubuntu e no Homebrew (PEP 668), e uma tentativa só, calada,
|
|
310
|
+
# é a falha que ninguém vê.
|
|
311
|
+
for args in (["-m","pip","install","--user","--quiet","vosk"],
|
|
312
|
+
["-m","pip","install","--user","--quiet","--break-system-packages","vosk"],
|
|
313
|
+
["-m","pip","install","--quiet","vosk"],
|
|
314
|
+
["-m","pip","install","--quiet","--break-system-packages","vosk"]):
|
|
315
|
+
r = subprocess.run([sys.executable] + args, capture_output=True)
|
|
316
|
+
if r.returncode == 0:
|
|
317
|
+
break
|
|
318
|
+
try:
|
|
319
|
+
import vosk # noqa: F401
|
|
320
|
+
except ImportError as e:
|
|
321
|
+
print(json.dumps({"ok": False, "error": "não consegui instalar o vosk: %s" % e}))
|
|
322
|
+
raise SystemExit
|
|
323
|
+
from primo_voz import desperta
|
|
324
|
+
print(json.dumps(desperta.baixar_modelo()))
|
|
325
|
+
`;
|
|
326
|
+
const p = spawn(py, ['-c', script]);
|
|
327
|
+
let fora = '';
|
|
328
|
+
let erro = '';
|
|
329
|
+
p.stdout.on('data', (d) => { fora += d.toString(); });
|
|
330
|
+
p.stderr.on('data', (d) => { erro += d.toString(); });
|
|
331
|
+
p.on('close', () => {
|
|
332
|
+
try { resolve(JSON.parse(fora.trim().split('\n').pop())); }
|
|
333
|
+
catch { resolve({ ok: false, error: erro.slice(-300).trim() || 'o download não devolveu resposta' }); }
|
|
334
|
+
});
|
|
335
|
+
p.on('error', (e) => resolve({ ok: false, error: e.message }));
|
|
336
|
+
});
|
|
237
337
|
}
|
|
238
338
|
|
|
239
|
-
module.exports = { conferir, instalar, abrir, python, COMO_FALAR, PASTA };
|
|
339
|
+
module.exports = { conferir, instalar, 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.2.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/skills.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/pareamento.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.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/skills.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/pareamento.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"
|