primocode 10.5.0 → 10.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/primocode.js +117 -24
- package/lib/act.js +141 -9
- package/lib/agents-md.js +140 -0
- package/lib/auditoria.js +122 -0
- package/lib/browser.js +48 -1
- package/lib/catalogo.js +29 -6
- package/lib/comandos-extra.js +2 -0
- package/lib/config.js +32 -4
- package/lib/entrada.js +33 -11
- package/lib/estudio/cenarios.js +91 -0
- package/lib/estudio/colecao.js +35 -1
- package/lib/estudio/diretor.js +1 -0
- package/lib/estudio/falantes.js +30 -6
- package/lib/estudio/hibrido.js +144 -8
- package/lib/estudio/index.js +196 -63
- package/lib/estudio/kit/mundo.jsx +1060 -0
- package/lib/estudio/kit/voxel.jsx +315 -70
- package/lib/estudio/kit.js +1 -1
- package/lib/estudio/kokoro.js +18 -4
- package/lib/estudio/margem.js +1 -1
- package/lib/estudio/movimentos.js +212 -0
- package/lib/estudio/mundo.js +328 -0
- package/lib/estudio/personagem.js +164 -11
- package/lib/estudio/plano.js +6 -0
- package/lib/estudio/producao.js +107 -17
- package/lib/estudio/projeto.js +77 -22
- package/lib/leitura.js +176 -0
- package/lib/local/cerebro.json +180 -3
- package/lib/local/entrada.js +61 -4
- package/lib/mcp.js +296 -12
- package/lib/memoria-conversa.js +32 -3
- package/lib/mencoes.js +5 -1
- package/lib/nome-curto.js +126 -0
- package/lib/nome-projeto.js +5 -2
- package/lib/outra-pasta.js +161 -0
- package/lib/perigo.js +34 -2
- package/lib/referencia.js +40 -11
- package/lib/repeticao.js +20 -1
- package/lib/tela.js +1 -1
- package/lib/tools.js +76 -6
- package/lib/travado.js +141 -0
- package/lib/ui.js +1 -0
- package/package.json +2 -2
package/lib/mcp.js
CHANGED
|
@@ -15,6 +15,17 @@
|
|
|
15
15
|
*
|
|
16
16
|
* O cliente é o mínimo do protocolo por stdio: JSON-RPC 2.0, uma mensagem por
|
|
17
17
|
* linha — initialize, notifications/initialized, tools/list, tools/call.
|
|
18
|
+
*
|
|
19
|
+
* ── MCP PRIMEIRO, MOUSE POR ÚLTIMO ───────────────────────────────────────
|
|
20
|
+
* "Para a IA não ter que fazer as coisas manualmente no computador do
|
|
21
|
+
* usuário. Sempre procurar um MCP primeiro; se não tiver, pesquisar se a
|
|
22
|
+
* ferramenta tem MCP e, se tiver, conectar." (11/10)
|
|
23
|
+
*
|
|
24
|
+
* Clicar no Notion pela tela são vinte ações que quebram com um pop-up; a
|
|
25
|
+
* API do Notion é uma. Por isso `procurar` (a lista curada daqui + o registro
|
|
26
|
+
* oficial em registry.modelcontextprotocol.io) e `conectarNovo` (grava, sobe,
|
|
27
|
+
* lista as tools — e desfaz se não subir). Hoje viram tools nativas
|
|
28
|
+
* (mcp_procurar · mcp_conectar · mcp_usar), sem passar pelo run_shell.
|
|
18
29
|
*/
|
|
19
30
|
|
|
20
31
|
'use strict';
|
|
@@ -46,9 +57,17 @@ function servidores(pasta = process.cwd()) {
|
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
/** Abre um servidor, faz o aperto de mão e devolve {pedir, fechar}. */
|
|
60
|
+
/* `${VAR}` no argumento vira o valor do env. Sem shell no meio (spawn puro),
|
|
61
|
+
ninguém expandiria — e é assim que o token entra no header do mcp-remote
|
|
62
|
+
sem ficar escrito duas vezes no mcp.json. */
|
|
63
|
+
function expandir(args, env) {
|
|
64
|
+
return (args || []).map((a) => String(a).replace(/\$\{([A-Z0-9_]+)\}/g, (_, k) => (env[k] != null ? env[k] : '')));
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
function conectar(def, { tetoMs = 60000 } = {}) {
|
|
50
68
|
return new Promise((resolve, reject) => {
|
|
51
|
-
const
|
|
69
|
+
const env = { ...process.env, ...(def.env || {}) };
|
|
70
|
+
const p = spawn(def.command, expandir(def.args, env), { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
52
71
|
let buf = '', id = 0;
|
|
53
72
|
const espera = new Map();
|
|
54
73
|
const relogio = setTimeout(() => { try { p.kill(); } catch {} reject(new Error('o servidor MCP não respondeu')); }, tetoMs);
|
|
@@ -75,6 +94,21 @@ function conectar(def, { tetoMs = 60000 } = {}) {
|
|
|
75
94
|
});
|
|
76
95
|
}
|
|
77
96
|
|
|
97
|
+
function resumirTools(tools) {
|
|
98
|
+
return (tools || []).map((t) => ({ nome: t.name, descricao: String(t.description || '').split(/\n|\. /)[0].slice(0, 120), args: Object.keys((t.inputSchema && t.inputSchema.properties) || {}) }));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function gravarCache(mudar) {
|
|
102
|
+
try {
|
|
103
|
+
const c = lerJson(CACHE) || { servidores: {} };
|
|
104
|
+
c.servidores = c.servidores || {};
|
|
105
|
+
mudar(c.servidores);
|
|
106
|
+
c.quando = Date.now();
|
|
107
|
+
fs.mkdirSync(CASA, { recursive: true });
|
|
108
|
+
fs.writeFileSync(CACHE, JSON.stringify(c, null, 2));
|
|
109
|
+
} catch { /* sem cache */ }
|
|
110
|
+
}
|
|
111
|
+
|
|
78
112
|
async function listar(pasta) {
|
|
79
113
|
const lista = servidores(pasta);
|
|
80
114
|
const out = {};
|
|
@@ -83,10 +117,10 @@ async function listar(pasta) {
|
|
|
83
117
|
const c = await conectar(def);
|
|
84
118
|
const r = await c.pedir('tools/list');
|
|
85
119
|
c.fechar();
|
|
86
|
-
out[nome] = (r.tools
|
|
120
|
+
out[nome] = resumirTools(r.tools);
|
|
87
121
|
} catch (e) { out[nome] = { erro: e.message }; }
|
|
88
122
|
}));
|
|
89
|
-
|
|
123
|
+
gravarCache((sv) => { for (const k of Object.keys(sv)) delete sv[k]; Object.assign(sv, out); });
|
|
90
124
|
return out;
|
|
91
125
|
}
|
|
92
126
|
|
|
@@ -100,6 +134,232 @@ async function chamar(servidor, tool, args = {}, pasta) {
|
|
|
100
134
|
} finally { c.fechar(); }
|
|
101
135
|
}
|
|
102
136
|
|
|
137
|
+
/* ── A LISTA CURADA ─────────────────────────────────────────────────────
|
|
138
|
+
* Vem antes do registro, e funciona sem internet. O registro tem milhares de
|
|
139
|
+
* entradas, e na busca "github" as primeiras são de terceiros que pedem a
|
|
140
|
+
* chave da Smithery — o servidor OFICIAL do GitHub nem aparece. Aqui ficam os
|
|
141
|
+
* conhecidos, conferidos um a um (npm view / registro em 11/10): pacote que
|
|
142
|
+
* existe, não deprecado, e a chave que ele pede com onde pegar.
|
|
143
|
+
* `<...>` nos args é o que o usuário precisa dizer (a pasta, o banco). */
|
|
144
|
+
const REMOTO = (url, header) => ({ command: 'npx', args: ['-y', 'mcp-remote', url, ...(header ? ['--header', header] : [])] });
|
|
145
|
+
const CURADOS = [
|
|
146
|
+
{ nome: 'github', termos: /git ?hub|reposit|pull request|\bpr\b|issue/i, descricao: 'GitHub oficial: repos, issues, PRs, Actions',
|
|
147
|
+
...REMOTO('https://api.githubcopilot.com/mcp/', 'Authorization:Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}'),
|
|
148
|
+
precisa: [{ nome: 'GITHUB_PERSONAL_ACCESS_TOKEN', onde: 'github.com/settings/tokens' }] },
|
|
149
|
+
{ nome: 'notion', termos: /notion/i, descricao: 'Notion oficial: páginas, bancos, busca',
|
|
150
|
+
command: 'npx', args: ['-y', '@notionhq/notion-mcp-server'],
|
|
151
|
+
precisa: [{ nome: 'NOTION_TOKEN', onde: 'notion.so/profile/integrations (integração interna, ntn_…)' }] },
|
|
152
|
+
{ nome: 'slack', termos: /slack/i, descricao: 'Slack: canais, mensagens, busca',
|
|
153
|
+
command: 'npx', args: ['-y', 'slack-mcp-server@latest', '--transport', 'stdio'],
|
|
154
|
+
precisa: [{ nome: 'SLACK_MCP_XOXP_TOKEN', onde: 'api.slack.com/apps → OAuth & Permissions (User OAuth Token xoxp-…)' }] },
|
|
155
|
+
{ nome: 'google-drive', termos: /google ?drive|\bdrive\b|planilha|sheets|google docs/i, descricao: 'Google Drive, Docs e Sheets',
|
|
156
|
+
command: 'npx', args: ['-y', '@piotr-agier/google-drive-mcp'],
|
|
157
|
+
precisa: [{ nome: 'GOOGLE_DRIVE_OAUTH_CREDENTIALS', onde: 'caminho do JSON de credencial OAuth (console.cloud.google.com → Credenciais)' }] },
|
|
158
|
+
{ nome: 'figma', termos: /figma/i, descricao: 'Figma: lê arquivos, frames e estilos',
|
|
159
|
+
command: 'npx', args: ['-y', 'figma-developer-mcp', '--stdio'],
|
|
160
|
+
precisa: [{ nome: 'FIGMA_API_KEY', onde: 'figma.com → Settings → Personal access tokens' }] },
|
|
161
|
+
{ nome: 'postgres', termos: /postgres|banco de dados|\bsql\b|\bbanco\b/i, descricao: 'Postgres: consulta, esquema, saúde do banco',
|
|
162
|
+
command: 'uvx', args: ['postgres-mcp', '--access-mode=restricted'],
|
|
163
|
+
precisa: [{ nome: 'DATABASE_URI', onde: 'a URL do banco (postgresql://usuario:senha@host:5432/banco)' }] },
|
|
164
|
+
{ nome: 'sqlite', termos: /sqlite|\.db\b/i, descricao: 'SQLite: consulta um arquivo .db',
|
|
165
|
+
command: 'npx', args: ['-y', 'mcp-server-sqlite-npx', '<caminho do arquivo .db>'], precisa: [] },
|
|
166
|
+
{ nome: 'filesystem', termos: /arquivos? (de|em) outra pasta|filesystem|sistema de arquivos/i, descricao: 'Arquivos de uma pasta fora do projeto',
|
|
167
|
+
command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', '<pasta>'], precisa: [] },
|
|
168
|
+
{ nome: 'playwright', termos: /playwright|navegador|browser|site|p[áa]gina/i, descricao: 'Playwright (Microsoft): navegador por acessibilidade, sem pixel',
|
|
169
|
+
command: 'npx', args: ['-y', '@playwright/mcp@latest'], precisa: [] },
|
|
170
|
+
{ nome: 'supabase', termos: /supabase/i, descricao: 'Supabase oficial: projetos, tabelas, SQL, edge functions',
|
|
171
|
+
command: 'npx', args: ['-y', '@supabase/mcp-server-supabase@latest'],
|
|
172
|
+
precisa: [{ nome: 'SUPABASE_ACCESS_TOKEN', onde: 'supabase.com/dashboard/account/tokens' }] },
|
|
173
|
+
{ nome: 'stripe', termos: /stripe|pagamento|cobran[çc]a/i, descricao: 'Stripe oficial: clientes, cobranças, links de pagamento',
|
|
174
|
+
command: 'npx', args: ['-y', '@stripe/mcp', '--tools=all'],
|
|
175
|
+
precisa: [{ nome: 'STRIPE_SECRET_KEY', onde: 'dashboard.stripe.com/apikeys (de preferência uma restricted key)' }] },
|
|
176
|
+
{ nome: 'linear', termos: /linear/i, descricao: 'Linear oficial: issues, projetos (login pelo navegador)',
|
|
177
|
+
...REMOTO('https://mcp.linear.app/mcp'), precisa: [] },
|
|
178
|
+
{ nome: 'sentry', termos: /sentry/i, descricao: 'Sentry oficial: erros, issues, releases',
|
|
179
|
+
command: 'npx', args: ['-y', '@sentry/mcp-server@latest'],
|
|
180
|
+
precisa: [{ nome: 'SENTRY_ACCESS_TOKEN', onde: 'sentry.io → Settings → Auth Tokens' }] },
|
|
181
|
+
{ nome: 'vercel', termos: /vercel/i, descricao: 'Vercel oficial: projetos, deploys, logs (login pelo navegador)',
|
|
182
|
+
...REMOTO('https://mcp.vercel.com'), precisa: [] },
|
|
183
|
+
{ nome: 'cloudflare', termos: /cloudflare|workers?\b/i, descricao: 'Cloudflare oficial: Workers, KV, R2, D1 (login pelo navegador)',
|
|
184
|
+
...REMOTO('https://bindings.mcp.cloudflare.com/mcp'), precisa: [] },
|
|
185
|
+
{ nome: 'firebase', termos: /firebase|firestore/i, descricao: 'Firebase oficial (firebase-tools): projetos, Firestore, Auth',
|
|
186
|
+
command: 'npx', args: ['-y', 'firebase-tools@latest', 'mcp'], precisa: [] },
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
const REGISTRO = 'https://registry.modelcontextprotocol.io/v0/servers';
|
|
190
|
+
|
|
191
|
+
/* O nome que fica no mcp.json: o pedaço final, curto. "io.github.github/
|
|
192
|
+
github-mcp-server" vira "github-mcp-server". */
|
|
193
|
+
function nomeCurto(n) {
|
|
194
|
+
return String(n || '').split('/').pop().replace(/[^a-zA-Z0-9_.-]+/g, '-').toLowerCase().slice(0, 40) || 'mcp';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Uma entrada do registro vira candidatos de instalação. Cada formato tem o
|
|
198
|
+
seu lançador: npm → npx, pypi → uvx, oci → docker; remoto (http/sse) entra
|
|
199
|
+
pelo mcp-remote, que fala stdio deste lado. */
|
|
200
|
+
function doRegistro(item) {
|
|
201
|
+
const s = (item && item.server) || item || {};
|
|
202
|
+
const instalar = [];
|
|
203
|
+
for (const pk of s.packages || []) {
|
|
204
|
+
const env = (pk.environmentVariables || []).map((e) => ({ nome: e.name, descricao: String(e.description || '').slice(0, 140), obrigatorio: !!e.isRequired, segredo: !!e.isSecret }));
|
|
205
|
+
const t = pk.transport && pk.transport.type;
|
|
206
|
+
if (t && t !== 'stdio') continue;
|
|
207
|
+
const extra = (pk.packageArguments || []).filter((a) => a.isRequired).flatMap((a) => {
|
|
208
|
+
const v = a.value || a.default || `<${a.valueHint || a.name || 'valor'}>`;
|
|
209
|
+
return a.type === 'named' ? [a.name, v] : [v];
|
|
210
|
+
});
|
|
211
|
+
if (pk.registryType === 'npm') instalar.push({ tipo: 'npm', command: 'npx', args: ['-y', pk.identifier, ...extra], env });
|
|
212
|
+
else if (pk.registryType === 'pypi') instalar.push({ tipo: 'pypi', command: 'uvx', args: [pk.identifier, ...extra], env });
|
|
213
|
+
else if (pk.registryType === 'oci') instalar.push({ tipo: 'oci', command: 'docker', args: ['run', '-i', '--rm', ...env.flatMap((e) => ['-e', e.nome]), pk.identifier, ...extra], env });
|
|
214
|
+
}
|
|
215
|
+
for (const r of s.remotes || []) {
|
|
216
|
+
if (!/^https:\/\//.test(r.url || '') || /\{/.test(r.url)) continue; // URL com molde pede dado que não temos
|
|
217
|
+
const env = [];
|
|
218
|
+
const args = ['-y', 'mcp-remote', r.url];
|
|
219
|
+
for (const h of r.headers || []) {
|
|
220
|
+
if (!h.isRequired && !h.isSecret) continue;
|
|
221
|
+
// "Bearer {smithery_api_key}" → "Bearer ${SMITHERY_API_KEY}"; sem molde, o header inteiro vem do env.
|
|
222
|
+
let valor = h.value ? String(h.value).replace(/\{([a-zA-Z0-9_]+)\}/g, (_, k) => {
|
|
223
|
+
const n = k.toUpperCase();
|
|
224
|
+
env.push({ nome: n, descricao: String(h.description || '').slice(0, 140), obrigatorio: true, segredo: true });
|
|
225
|
+
return '${' + n + '}';
|
|
226
|
+
}) : null;
|
|
227
|
+
if (!valor) {
|
|
228
|
+
const n = `MCP_${String(h.name).toUpperCase().replace(/[^A-Z0-9]+/g, '_')}`;
|
|
229
|
+
env.push({ nome: n, descricao: `valor do header ${h.name} (ex: Bearer <token>)`, obrigatorio: true, segredo: true });
|
|
230
|
+
valor = '${' + n + '}';
|
|
231
|
+
}
|
|
232
|
+
args.push('--header', `${h.name}:${valor}`);
|
|
233
|
+
}
|
|
234
|
+
instalar.push({ tipo: r.type || 'remoto', command: 'npx', args, env });
|
|
235
|
+
}
|
|
236
|
+
return { nome: nomeCurto(s.name), registro: s.name, descricao: String(s.title ? `${s.title} — ${s.description || ''}` : s.description || '').slice(0, 160), versao: s.version, instalar };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function doCurado(c) {
|
|
240
|
+
return { nome: c.nome, fonte: 'curado', descricao: c.descricao,
|
|
241
|
+
instalar: [{ tipo: 'curado', command: c.command, args: c.args, env: (c.precisa || []).map((p) => ({ nome: p.nome, descricao: p.onde, obrigatorio: true, segredo: true })) }] };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Procura um servidor MCP para o serviço: a lista curada primeiro, depois o
|
|
246
|
+
* registro oficial. `buscar` é o fetch (o teste troca; sem rede, só a curada).
|
|
247
|
+
*/
|
|
248
|
+
async function procurar(q, { buscar = globalThis.fetch, limite = 8, tetoMs = 8000 } = {}) {
|
|
249
|
+
const busca = String(q || '').trim();
|
|
250
|
+
if (!busca) return { ok: false, error: 'Diga o serviço ou app a procurar (ex: "notion", "github").' };
|
|
251
|
+
const ja = servidores();
|
|
252
|
+
const curados = CURADOS.filter((c) => c.termos.test(busca) || busca.toLowerCase().includes(c.nome)).map(doCurado);
|
|
253
|
+
let registro = [], aviso;
|
|
254
|
+
if (typeof buscar === 'function') {
|
|
255
|
+
const ctl = typeof AbortController === 'function' ? new AbortController() : null;
|
|
256
|
+
const relogio = ctl ? setTimeout(() => ctl.abort(), tetoMs) : null;
|
|
257
|
+
try {
|
|
258
|
+
const url = `${REGISTRO}?search=${encodeURIComponent(busca)}&limit=${Math.max(limite * 3, 20)}&version=latest`;
|
|
259
|
+
const r = await buscar(url, ctl ? { signal: ctl.signal } : {});
|
|
260
|
+
if (!r.ok) throw new Error(`registro respondeu ${r.status}`);
|
|
261
|
+
const j = await r.json();
|
|
262
|
+
const termo = busca.toLowerCase().split(/\s+/)[0];
|
|
263
|
+
registro = (j.servers || [])
|
|
264
|
+
// version=latest já filtra; a marca isLatest é a rede caso o parâmetro mude.
|
|
265
|
+
.filter((x) => { const m = x._meta && x._meta['io.modelcontextprotocol.registry/official']; return !m || m.isLatest !== false; })
|
|
266
|
+
.map(doRegistro)
|
|
267
|
+
.filter((x) => x.instalar.length)
|
|
268
|
+
// A busca do registro casa até a URL do repositório ("github" traz
|
|
269
|
+
// tudo que mora no GitHub). Fica o que fala do serviço no nome ou na descrição.
|
|
270
|
+
// O prefixo "io.github.<dono>/" não conta: é onde mora, não o que faz.
|
|
271
|
+
.filter((x) => `${x.nome} ${x.descricao}`.toLowerCase().includes(termo))
|
|
272
|
+
.map((x) => ({ ...x, fonte: 'registro',
|
|
273
|
+
// Relevância: o nome casa com a busca, e tem pacote que roda local (sem chave de intermediário).
|
|
274
|
+
pontos: (x.nome.includes(termo) ? 2 : 0) + (x.instalar.some((i) => i.tipo === 'npm') ? 1 : 0) - (/smithery/.test(x.registro) ? 2 : 0) }))
|
|
275
|
+
.sort((a, b) => b.pontos - a.pontos)
|
|
276
|
+
.map(({ pontos, ...x }) => x);
|
|
277
|
+
} catch (e) { aviso = `registro indisponível (${e.name === 'AbortError' ? 'demorou' : e.message}) — só a lista curada`; }
|
|
278
|
+
finally { if (relogio) clearTimeout(relogio); }
|
|
279
|
+
}
|
|
280
|
+
const vistos = new Set();
|
|
281
|
+
const resultados = [...curados, ...registro].filter((x) => !vistos.has(x.nome) && vistos.add(x.nome)).slice(0, limite)
|
|
282
|
+
.map((x) => ({ ...x, jaConectado: !!ja[x.nome] }));
|
|
283
|
+
return { ok: true, busca, resultados,
|
|
284
|
+
...(aviso ? { aviso } : {}),
|
|
285
|
+
proximo: resultados.length
|
|
286
|
+
? 'Escolha o primeiro que serve e chame mcp_conectar {nome, comando, args, env} com o "instalar" dele (curado: só {nome} basta). Env obrigatório sem valor: PEÇA a chave ao usuário antes.'
|
|
287
|
+
: 'Nenhum MCP para isso. Tente API/CLI pelo run_shell; o mouse (desktop_*) é o último recurso.' };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function lerConfig(arq) { const j = lerJson(arq) || {}; j.mcpServers = j.mcpServers || {}; return j; }
|
|
291
|
+
function gravarConfig(arq, j) {
|
|
292
|
+
fs.mkdirSync(path.dirname(arq), { recursive: true });
|
|
293
|
+
// Pode levar token: só o dono lê.
|
|
294
|
+
fs.writeFileSync(arq, JSON.stringify(j, null, 2), { mode: 0o600 });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Grava, sobe e valida um servidor novo. Não salva o que não subiria: sem a
|
|
299
|
+
* chave obrigatória, devolve o que perguntar ao usuário; se não responder ao
|
|
300
|
+
* tools/list, desfaz a gravação.
|
|
301
|
+
*/
|
|
302
|
+
async function conectarNovo({ nome, command, comando, args, env } = {}, { config = CONFIG, tetoMs = 120000 } = {}) {
|
|
303
|
+
const n = String(nome || '').trim().toLowerCase();
|
|
304
|
+
if (!/^[a-z0-9][a-z0-9_.-]{0,40}$/.test(n)) return { ok: false, error: 'nome inválido: letras, números, - _ . (ex: "notion")' };
|
|
305
|
+
const curado = CURADOS.find((c) => c.nome === n);
|
|
306
|
+
let cmd = command || comando;
|
|
307
|
+
let lista = Array.isArray(args) ? args.map(String) : (typeof args === 'string' && args.trim() ? args.trim().split(/\s+/) : null);
|
|
308
|
+
if (!cmd && curado) { cmd = curado.command; lista = lista || curado.args.slice(); }
|
|
309
|
+
if (!cmd) return { ok: false, error: `não conheço "${n}" de cor — chame mcp_procurar primeiro e passe comando e args do "instalar".` };
|
|
310
|
+
lista = lista || [];
|
|
311
|
+
const vars = {};
|
|
312
|
+
for (const [k, v] of Object.entries(env || {})) if (v != null && String(v).trim() !== '') vars[k] = String(v);
|
|
313
|
+
|
|
314
|
+
const molde = lista.filter((a) => /<[^>]+>/.test(a));
|
|
315
|
+
if (molde.length) return { ok: false, salvo: false, falta: molde, error: `Falta preencher ${molde.join(', ')} nos args. Pergunte ao usuário e chame de novo.` };
|
|
316
|
+
|
|
317
|
+
// A chave que falta: a da curadoria e a que os args citam como ${VAR}.
|
|
318
|
+
const pedidas = new Map((curado && cmd === curado.command ? curado.precisa : []).map((p) => [p.nome, p.onde]));
|
|
319
|
+
for (const a of lista) for (const m of a.matchAll(/\$\{([A-Z0-9_]+)\}/g)) if (!pedidas.has(m[1])) pedidas.set(m[1], '');
|
|
320
|
+
const faltam = [...pedidas].filter(([k]) => !vars[k] && !process.env[k]).map(([k, onde]) => ({ nome: k, onde }));
|
|
321
|
+
if (faltam.length) {
|
|
322
|
+
return { ok: false, salvo: false, precisaDe: faltam,
|
|
323
|
+
error: `O MCP "${n}" precisa de ${faltam.map((f) => f.nome).join(', ')}. NÃO salvei nada. Peça ao usuário`
|
|
324
|
+
+ `${faltam.some((f) => f.onde) ? ` (onde pegar: ${faltam.filter((f) => f.onde).map((f) => `${f.nome} → ${f.onde}`).join('; ')})` : ''}`
|
|
325
|
+
+ ' e chame mcp_conectar de novo com env.' };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const def = { command: cmd, args: lista, ...(Object.keys(vars).length ? { env: vars } : {}) };
|
|
329
|
+
const j = lerConfig(config);
|
|
330
|
+
const antes = j.mcpServers[n];
|
|
331
|
+
j.mcpServers[n] = def;
|
|
332
|
+
gravarConfig(config, j);
|
|
333
|
+
let c;
|
|
334
|
+
try {
|
|
335
|
+
c = await conectar(def, { tetoMs });
|
|
336
|
+
const r = await c.pedir('tools/list');
|
|
337
|
+
const ferramentas = resumirTools(r.tools);
|
|
338
|
+
gravarCache((sv) => { sv[n] = ferramentas; });
|
|
339
|
+
return { ok: true, servidor: n, ferramentas,
|
|
340
|
+
proximo: `Conectado. Use mcp_usar {servidor:"${n}", ferramenta, argumentos}.` };
|
|
341
|
+
} catch (e) {
|
|
342
|
+
// Não subiu: o mcp.json volta a ser o que era.
|
|
343
|
+
const k = lerConfig(config);
|
|
344
|
+
if (antes) k.mcpServers[n] = antes; else delete k.mcpServers[n];
|
|
345
|
+
gravarConfig(config, k);
|
|
346
|
+
return { ok: false, salvo: false, error: `o servidor "${n}" não subiu: ${e.message}. Nada ficou salvo.` };
|
|
347
|
+
} finally { if (c) c.fechar(); }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function textoDe(r) {
|
|
351
|
+
return (r.content || []).map((x) => (x.type === 'text' ? x.text : `[${x.type}]`)).join('\n');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** A tool nativa mcp_usar: chama e devolve o texto, no formato das outras tools. */
|
|
355
|
+
async function usar(servidor, ferramenta, argumentos = {}, pasta) {
|
|
356
|
+
if (!servidor || !ferramenta) return { ok: false, error: 'Diga servidor e ferramenta (veja [mcp] no contexto, ou mcp_procurar).' };
|
|
357
|
+
let a = argumentos;
|
|
358
|
+
if (typeof a === 'string') { try { a = JSON.parse(a || '{}'); } catch { return { ok: false, error: 'argumentos não é JSON válido' }; } }
|
|
359
|
+
const r = await chamar(servidor, ferramenta, a || {}, pasta);
|
|
360
|
+
return { ok: !r.isError, servidor, ferramenta, resultado: textoDe(r).slice(0, 20000) };
|
|
361
|
+
}
|
|
362
|
+
|
|
103
363
|
/** A linha do contexto: só se houver servidor configurado, a partir do cache. */
|
|
104
364
|
function dica(pasta) {
|
|
105
365
|
const lista = Object.keys(servidores(pasta));
|
|
@@ -109,8 +369,9 @@ function dica(pasta) {
|
|
|
109
369
|
const tools = c && c.servidores && Array.isArray(c.servidores[n]) ? c.servidores[n].map((t) => t.nome).slice(0, 8).join(', ') : '';
|
|
110
370
|
return tools ? `${n} (${tools})` : n;
|
|
111
371
|
}).join(' · ');
|
|
112
|
-
return `[mcp] Servidores MCP disponíveis: ${det}. Use
|
|
113
|
-
+
|
|
372
|
+
return `[mcp] Servidores MCP disponíveis: ${det}. Use com mcp_usar {servidor, ferramenta, argumentos}. `
|
|
373
|
+
+ 'Serviço sem MCP aqui: mcp_procurar → mcp_conectar.'
|
|
374
|
+
+ (c ? '' : ' (primocode mcp list mostra as tools.)');
|
|
114
375
|
}
|
|
115
376
|
|
|
116
377
|
/** `primocode mcp …` */
|
|
@@ -122,14 +383,15 @@ async function main(argv) {
|
|
|
122
383
|
console.log('primocode mcp list — servidores e tools (de ~/.primocode/mcp.json e .mcp.json)');
|
|
123
384
|
console.log("primocode mcp call <servidor> <tool> --json '{...}' — chama uma tool; sai o resultado em JSON");
|
|
124
385
|
console.log('primocode mcp add <nome> -- <comando> [args…] — guarda um servidor em ~/.primocode/mcp.json');
|
|
386
|
+
console.log('primocode mcp procurar <serviço> — acha o MCP de um serviço (lista curada + registro oficial)');
|
|
387
|
+
console.log('primocode mcp conectar <nome> [--env K=V…] [-- <comando> [args…]] — grava, sobe e valida');
|
|
125
388
|
return 0;
|
|
126
389
|
}
|
|
127
390
|
if (op === 'list') { console.log(JSON.stringify(await listar(process.cwd()))); return 0; }
|
|
128
391
|
if (op === 'call') {
|
|
129
392
|
if (json === null) { console.log(JSON.stringify({ ok: false, error: '--json inválido' })); return 1; }
|
|
130
393
|
const r = await chamar(resto[0], resto[1], json, process.cwd());
|
|
131
|
-
|
|
132
|
-
console.log(JSON.stringify({ ok: !r.isError, resultado: texto.slice(0, 20000) }));
|
|
394
|
+
console.log(JSON.stringify({ ok: !r.isError, resultado: textoDe(r).slice(0, 20000) }));
|
|
133
395
|
return r.isError ? 1 : 0;
|
|
134
396
|
}
|
|
135
397
|
if (op === 'add') {
|
|
@@ -137,14 +399,36 @@ async function main(argv) {
|
|
|
137
399
|
const nome = resto[0];
|
|
138
400
|
const cmd = i >= 0 ? resto.slice(i + 1) : resto.slice(1);
|
|
139
401
|
if (!nome || !cmd.length) { console.log('uso: primocode mcp add <nome> -- <comando> [args…]'); return 1; }
|
|
140
|
-
const j =
|
|
141
|
-
j.mcpServers = j.mcpServers || {};
|
|
402
|
+
const j = lerConfig(CONFIG);
|
|
142
403
|
j.mcpServers[nome] = { command: cmd[0], args: cmd.slice(1) };
|
|
143
|
-
|
|
144
|
-
fs.writeFileSync(CONFIG, JSON.stringify(j, null, 2));
|
|
404
|
+
gravarConfig(CONFIG, j);
|
|
145
405
|
console.log(JSON.stringify({ ok: true, servidor: nome }));
|
|
146
406
|
return 0;
|
|
147
407
|
}
|
|
408
|
+
if (op === 'procurar' || op === 'search') {
|
|
409
|
+
const r = await procurar(resto.join(' '));
|
|
410
|
+
if (!r.ok) { console.log(r.error); return 1; }
|
|
411
|
+
for (const x of r.resultados) {
|
|
412
|
+
const i = x.instalar[0];
|
|
413
|
+
const chaves = i.env.filter((e) => e.obrigatorio).map((e) => e.nome);
|
|
414
|
+
console.log(`${x.jaConectado ? '✔' : '·'} ${x.nome} [${x.fonte}] ${x.descricao}`);
|
|
415
|
+
console.log(` ${i.command} ${i.args.join(' ')}${chaves.length ? ` (precisa: ${chaves.join(', ')})` : ''}`);
|
|
416
|
+
}
|
|
417
|
+
if (!r.resultados.length) console.log('nada encontrado.');
|
|
418
|
+
if (r.aviso) console.log(r.aviso);
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
if (op === 'conectar' || op === 'connect') {
|
|
422
|
+
const i = resto.indexOf('--');
|
|
423
|
+
const antes = i >= 0 ? resto.slice(0, i) : resto;
|
|
424
|
+
const cmd = i >= 0 ? resto.slice(i + 1) : [];
|
|
425
|
+
const env = {};
|
|
426
|
+
antes.forEach((x, k) => { if (x === '--env' && antes[k + 1]) { const [a, ...b] = antes[k + 1].split('='); env[a] = b.join('='); } });
|
|
427
|
+
const r = await conectarNovo({ nome: antes[0], command: cmd[0], args: cmd.length ? cmd.slice(1) : undefined, env });
|
|
428
|
+
if (r.ok) console.log(`conectado: ${r.servidor} — ${r.ferramentas.length} tools (${r.ferramentas.map((t) => t.nome).slice(0, 10).join(', ')})`);
|
|
429
|
+
else console.log(r.error);
|
|
430
|
+
return r.ok ? 0 : 1;
|
|
431
|
+
}
|
|
148
432
|
console.log(JSON.stringify({ ok: false, error: `operação desconhecida: ${op}` }));
|
|
149
433
|
return 1;
|
|
150
434
|
} catch (e) {
|
|
@@ -153,4 +437,4 @@ async function main(argv) {
|
|
|
153
437
|
}
|
|
154
438
|
}
|
|
155
439
|
|
|
156
|
-
module.exports = { servidores, conectar, listar, chamar, dica, main };
|
|
440
|
+
module.exports = { servidores, conectar, listar, chamar, usar, dica, main, procurar, doRegistro, conectarNovo, CURADOS, expandir };
|
package/lib/memoria-conversa.js
CHANGED
|
@@ -79,16 +79,45 @@ async function atualizar(memoria, history, resumir) {
|
|
|
79
79
|
const novas = pendentes(memoria, history);
|
|
80
80
|
if (!novas.length) return memoria;
|
|
81
81
|
|
|
82
|
+
// A memória da pasta (AGENTS.md) entra como parte do "resumo de antes":
|
|
83
|
+
// é assim que a sessão nova CONTINUA a de ontem em vez de recomeçar — e o
|
|
84
|
+
// AGENTS.md seguinte não apaga o que a anterior tinha anotado.
|
|
85
|
+
const semente = memoria.semente && memoria.semente.texto
|
|
86
|
+
? `Memória da pasta ${memoria.semente.pasta} (sessões anteriores):\n${memoria.semente.texto}` : '';
|
|
87
|
+
const antes = [memoria.resumo, semente].filter(Boolean).join('\n\n');
|
|
88
|
+
|
|
82
89
|
let texto;
|
|
83
90
|
try {
|
|
84
|
-
texto = await resumir({ resumo:
|
|
91
|
+
texto = await resumir({ resumo: antes, mensagens: novas });
|
|
85
92
|
} catch {
|
|
86
93
|
return memoria;
|
|
87
94
|
}
|
|
88
95
|
const limpo = String(texto || '').trim();
|
|
89
96
|
if (!limpo) return memoria;
|
|
90
97
|
|
|
91
|
-
return {
|
|
98
|
+
return {
|
|
99
|
+
resumo: limpo,
|
|
100
|
+
resumidasAte: (history || []).length - CRUAS,
|
|
101
|
+
dobrada: memoria.semente ? memoria.semente.pasta : (memoria.dobrada || null),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ── A MEMÓRIA DA PASTA ───────────────────────────────────────────────────
|
|
106
|
+
* `semear` guarda o que o AGENTS.md da pasta trazia, para o próximo resumo
|
|
107
|
+
* dobrar junto. `jaDobrou` diz se o resumo atual já carrega a memória
|
|
108
|
+
* daquela pasta — aí ela não precisa ir de novo no contexto (seria pagar duas
|
|
109
|
+
* vezes pelo mesmo texto). Ver lib/agents-md.js. */
|
|
110
|
+
function semear(memoria, pasta, texto) {
|
|
111
|
+
const base = memoria || nova();
|
|
112
|
+
if (!pasta || base.dobrada === pasta) return base;
|
|
113
|
+
const t = String(texto || '').trim();
|
|
114
|
+
// Pasta sem memória: não há o que dobrar, e ela conta como já dobrada.
|
|
115
|
+
if (!t) return { ...base, semente: null, dobrada: pasta };
|
|
116
|
+
return { ...base, semente: { pasta, texto: t } };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function jaDobrou(memoria, pasta) {
|
|
120
|
+
return !!(memoria && pasta && memoria.dobrada === pasta);
|
|
92
121
|
}
|
|
93
122
|
|
|
94
123
|
/**
|
|
@@ -153,4 +182,4 @@ function economia(memoria, history) {
|
|
|
153
182
|
return { cheio, enxuto, poupado: Math.max(0, cheio - enxuto) };
|
|
154
183
|
}
|
|
155
184
|
|
|
156
|
-
module.exports = { nova, pendentes, usoDoContexto, TETO_CONVERSA, precisaResumir, atualizar, paraEnviar, economia, tokensDe, inicioSeguro, CRUAS, LIMITE_TOKENS };
|
|
185
|
+
module.exports = { nova, semear, jaDobrou, pendentes, usoDoContexto, TETO_CONVERSA, precisaResumir, atualizar, paraEnviar, economia, tokensDe, inicioSeguro, CRUAS, LIMITE_TOKENS };
|
package/lib/mencoes.js
CHANGED
|
@@ -37,7 +37,11 @@ function instrucoesDoProjeto(projectDir, limite = LIMITE_INSTRUCOES) {
|
|
|
37
37
|
if (global.trim()) partes.push(`[~/.primocode/PRIMO.md — suas instruções para todo projeto]\n${cortar(global, limite)}`);
|
|
38
38
|
if (projectDir) {
|
|
39
39
|
for (const nome of NOMES_INSTRUCOES) {
|
|
40
|
-
|
|
40
|
+
/* O bloco de memória que o PrimoCode escreve no AGENTS.md
|
|
41
|
+
(lib/agents-md.js) não é instrução de ninguém: sai daqui e entra
|
|
42
|
+
à parte, como memória. Um AGENTS.md que só tem o bloco conta
|
|
43
|
+
como vazio — e o CLAUDE.md do repo ainda tem a vez dele. */
|
|
44
|
+
const texto = require('./agents-md.js').semBloco(lerSeExiste(path.join(projectDir, nome)));
|
|
41
45
|
if (!texto.trim()) continue;
|
|
42
46
|
partes.push(`[${nome} — instruções deste projeto: siga]\n${cortar(texto, limite)}`);
|
|
43
47
|
break;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nome-curto.js — o nome da pasta é para ler, não para guardar a frase.
|
|
3
|
+
*
|
|
4
|
+
* "Essa estrutura de pastas está horrível. Os nomes de pasta que a IA
|
|
5
|
+
* decide estão horríveis." (11/10/2026)
|
|
6
|
+
*
|
|
7
|
+
* O que saía: `2026-10-11-claude-e-humano-conversam-sobre-o-segund` — a data
|
|
8
|
+
* na frente, a frase inteira com "e", "sobre", "o", e o corte no meio da
|
|
9
|
+
* palavra aos 40 caracteres. Ninguém acha nada numa lista assim.
|
|
10
|
+
*
|
|
11
|
+
* ── A REGRA ─────────────────────────────────────────────────────────────
|
|
12
|
+
* · de 1 a 4 palavras que dizem o assunto, em kebab-case, sem acento;
|
|
13
|
+
* · palavra de ligação (de, do, com, sobre, o, a, e…) e verbo de pedido
|
|
14
|
+
* (faça, crie, quero…) saem — não dizem O QUE é;
|
|
15
|
+
* · número solto (data, "2026") sai: a data mora no projeto.json;
|
|
16
|
+
* · NUNCA corta palavra no meio. Se não cabe em 28 letras, sai uma
|
|
17
|
+
* palavra inteira — e a escolha prefere guardar a primeira (o sujeito)
|
|
18
|
+
* e a última (quase sempre o assunto: "segundo CÉREBRO").
|
|
19
|
+
*
|
|
20
|
+
* Puro e sem dependência: o config.js e o Estúdio usam, e nenhum dos dois
|
|
21
|
+
* pode puxar o outro.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const MAXIMO = 28;
|
|
27
|
+
const PALAVRAS = 4;
|
|
28
|
+
|
|
29
|
+
// Ligação e artigo: aparecem em toda frase e não dizem nada sozinhas.
|
|
30
|
+
const LIGACAO = new Set([
|
|
31
|
+
'de', 'do', 'da', 'dos', 'das', 'd', 'com', 'sem', 'sobre', 'o', 'a', 'os', 'as', 'e', 'ou',
|
|
32
|
+
'em', 'no', 'na', 'nos', 'nas', 'num', 'numa', 'um', 'uma', 'uns', 'umas', 'para', 'pra', 'pro',
|
|
33
|
+
'por', 'pelo', 'pela', 'que', 'se', 'ao', 'aos', 'mais', 'muito', 'bem', 'entre', 'ate', 'como',
|
|
34
|
+
'meu', 'minha', 'meus', 'minhas', 'seu', 'sua', 'nosso', 'nossa', 'esse', 'essa', 'este', 'esta',
|
|
35
|
+
'isso', 'isto', 'aquele', 'aquela', 'the', 'of', 'and', 'to', 'for', 'with', 'in', 'on',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
// Pedido e narração: o que a pessoa quer FAZER, não o que a coisa É.
|
|
39
|
+
const PEDIDO = new Set([
|
|
40
|
+
'faca', 'faz', 'fazer', 'crie', 'cria', 'criar', 'gere', 'gera', 'gerar', 'monte', 'monta', 'montar',
|
|
41
|
+
'quero', 'queria', 'preciso', 'bora', 'vamos', 'agora', 'novo', 'nova', 'outro', 'outra',
|
|
42
|
+
'conversam', 'conversa', 'conversando', 'falam', 'fala', 'falando', 'explica', 'explicando',
|
|
43
|
+
'mostra', 'mostrando', 'apresenta', 'apresentando', 'tipo', 'estilo', 'simples', 'rapido',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
function semAcento(s) {
|
|
47
|
+
return String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** As palavras que dizem o assunto, na ordem em que vieram. */
|
|
51
|
+
function palavrasDoAssunto(texto) {
|
|
52
|
+
return semAcento(texto).toLowerCase()
|
|
53
|
+
.split(/[^a-z0-9]+/)
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.filter((p) => !LIGACAO.has(p) && !PEDIDO.has(p))
|
|
56
|
+
// Número solto é data, hora ou contagem ("24 09 2026", "21h36" fica).
|
|
57
|
+
.filter((p) => !/^\d+$/.test(p));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* O melhor nome que cabe: o maior número de palavras inteiras, e entre os
|
|
62
|
+
* empates o que guarda a primeira e a última. Com no máximo 5 candidatas são
|
|
63
|
+
* 31 combinações — conta de cabeça, não de algoritmo.
|
|
64
|
+
*/
|
|
65
|
+
function escolher(palavras, maximo, limite) {
|
|
66
|
+
const cand = palavras.slice(0, Math.max(limite + 1, 5));
|
|
67
|
+
if (!cand.length) return '';
|
|
68
|
+
let melhor = null;
|
|
69
|
+
const n = cand.length;
|
|
70
|
+
for (let mascara = 1; mascara < (1 << n); mascara++) {
|
|
71
|
+
const idx = [];
|
|
72
|
+
for (let i = 0; i < n; i++) if (mascara & (1 << i)) idx.push(i);
|
|
73
|
+
if (idx.length > limite) continue;
|
|
74
|
+
const nome = idx.map((i) => cand[i]).join('-');
|
|
75
|
+
if (nome.length > maximo) continue;
|
|
76
|
+
let juntas = 0;
|
|
77
|
+
for (let k = 1; k < idx.length; k++) if (idx[k] === idx[k - 1] + 1) juntas++;
|
|
78
|
+
const pontos = idx.length * 1000
|
|
79
|
+
+ (idx[0] === 0 ? 100 : 0)
|
|
80
|
+
+ (idx[idx.length - 1] === n - 1 ? 50 : 0)
|
|
81
|
+
// as duas últimas juntas costumam ser um nome só ("segundo cérebro")
|
|
82
|
+
+ (n > 1 && idx.includes(n - 2) && idx.includes(n - 1) ? 20 : 0)
|
|
83
|
+
// a frase como veio, sem pular, lê melhor
|
|
84
|
+
+ juntas * 10
|
|
85
|
+
// mais palavras do começo da frase (o pedido diz o principal antes)
|
|
86
|
+
- idx.reduce((a, b) => a + b, 0);
|
|
87
|
+
if (!melhor || pontos > melhor.pontos || (pontos === melhor.pontos && nome.length > melhor.nome.length)) {
|
|
88
|
+
melhor = { nome, pontos };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Nem uma palavra coube: é uma palavra só, gigante. Aí corta — é o único
|
|
92
|
+
// caso, e o hífen do fim não fica.
|
|
93
|
+
if (!melhor) return cand[0].slice(0, maximo).replace(/-+$/, '');
|
|
94
|
+
return melhor.nome;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* "Claude e humano conversam sobre o segundo cérebro" → "claude-segundo-cerebro".
|
|
99
|
+
* Devolve '' quando não sobra palavra nenhuma (quem chama decide a reserva).
|
|
100
|
+
*/
|
|
101
|
+
function nomeCurto(texto, { maximo = MAXIMO, palavras = PALAVRAS } = {}) {
|
|
102
|
+
return escolher(palavrasDoAssunto(texto), maximo, palavras);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Nome que já existe: primeiro tenta uma palavra a mais da própria frase
|
|
107
|
+
* (`anuncio-imersao` → `anuncio-imersao-paris`), que diz QUAL é; só sem
|
|
108
|
+
* palavra que caiba vem o -2, -3.
|
|
109
|
+
*/
|
|
110
|
+
function semColisao(nome, tomados, texto = '', { maximo = MAXIMO } = {}) {
|
|
111
|
+
const usados = new Set([...(tomados || [])].map((n) => String(n).toLowerCase()));
|
|
112
|
+
if (!usados.has(nome)) return nome;
|
|
113
|
+
const ja = new Set(nome.split('-'));
|
|
114
|
+
for (const extra of palavrasDoAssunto(texto)) {
|
|
115
|
+
if (ja.has(extra)) continue;
|
|
116
|
+
const outro = `${nome}-${extra}`;
|
|
117
|
+
if (outro.length <= maximo && !usados.has(outro)) return outro;
|
|
118
|
+
}
|
|
119
|
+
for (let i = 2; i < 1000; i++) {
|
|
120
|
+
const outro = `${nome}-${i}`;
|
|
121
|
+
if (!usados.has(outro)) return outro;
|
|
122
|
+
}
|
|
123
|
+
return `${nome}-${Date.now()}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { nomeCurto, semColisao, palavrasDoAssunto, MAXIMO, LIGACAO, PEDIDO };
|
package/lib/nome-projeto.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
'use strict';
|
|
23
23
|
|
|
24
24
|
const { slugify } = require('./config.js');
|
|
25
|
+
const { nomeCurto } = require('./nome-curto.js');
|
|
25
26
|
|
|
26
27
|
// Nome que não diz nada: o modelo devolveu isso, é como não ter respondido.
|
|
27
28
|
const GENERICOS = new Set(['projeto', 'project', 'app', 'site', 'novo', 'teste', 'test', 'meu-projeto', 'my-project']);
|
|
@@ -48,7 +49,7 @@ function pergunta(pedido, nomes) {
|
|
|
48
49
|
: 'Use kebab-case em português, de 1 a 3 palavras.';
|
|
49
50
|
return 'Dê um nome curto para a pasta do projeto deste pedido. O nome diz O QUE é '
|
|
50
51
|
+ '(ex.: site-padaria, jogo-de-carro, loja-roupas), nunca o que a pessoa pediu para fazer. '
|
|
51
|
-
+ `${jeito} Responda SÓ com o nome, sem aspas e sem explicação.\n\nPEDIDO: ${String(pedido || '').slice(0, 600)}`;
|
|
52
|
+
+ `${jeito} No máximo 28 letras, sem data e sem "de/da/com". Responda SÓ com o nome, sem aspas e sem explicação.\n\nPEDIDO: ${String(pedido || '').slice(0, 600)}`;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
/** Limpa a resposta do modelo. Devolve null quando não dá para confiar nela. */
|
|
@@ -58,7 +59,9 @@ function limpar(resposta, nomes) {
|
|
|
58
59
|
.replace(/^(nome|pasta)\s*:\s*/i, '');
|
|
59
60
|
if (!primeira || primeira.length > 60 || primeira.split(/\s+/).length > 5) return null;
|
|
60
61
|
const c = convencao(nomes);
|
|
61
|
-
|
|
62
|
+
// A mesma régua das pastas do Estúdio (lib/nome-curto.js): sem "de/da",
|
|
63
|
+
// sem corte no meio da palavra, até 28 letras.
|
|
64
|
+
let nome = nomeCurto(primeira) || slugify(primeira).split('-').filter(Boolean).slice(0, 4).join('-');
|
|
62
65
|
if (!nome || GENERICOS.has(nome)) return null;
|
|
63
66
|
if (c && c.separador === '_') nome = nome.replace(/-/g, '_');
|
|
64
67
|
return semColisao(nome, nomes);
|