primocode 10.12.0 → 10.14.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 +74 -12
- package/bin/primocode.js +148 -8
- package/lib/act.js +48 -1
- package/lib/catalogo.js +12 -1
- package/lib/config.js +1 -0
- package/lib/estudio/assinaturas/gerativo.json +1 -1
- package/lib/estudio/camadas.js +4 -0
- package/lib/estudio/cota.js +150 -0
- package/lib/estudio/dados/projetos/clube-ad5.json +105 -0
- package/lib/estudio/dados/projetos/clube-ae-sem-memoria.json +107 -0
- package/lib/estudio/dados/projetos/clube-agent.json +120 -0
- package/lib/estudio/dados/projetos/clube-antes-e-agora.json +86 -0
- package/lib/estudio/dados/projetos/clube-apresentando-clube.json +110 -0
- package/lib/estudio/dados/projetos/clube-bluevisa.json +106 -0
- package/lib/estudio/dados/projetos/clube-caught-in-the-fire.json +127 -0
- package/lib/estudio/dados/projetos/clube-chat-cliente-satisfeito.json +98 -0
- package/lib/estudio/dados/projetos/clube-chatgpt-anuncios.json +120 -0
- package/lib/estudio/dados/projetos/clube-claude-dj.json +140 -0
- package/lib/estudio/dados/projetos/clube-claude-mcp.json +142 -0
- package/lib/estudio/dados/projetos/clube-cleanui.json +106 -0
- package/lib/estudio/dados/projetos/clube-clinica-take-unico.json +128 -0
- package/lib/estudio/dados/projetos/clube-clinica.json +104 -0
- package/lib/estudio/dados/projetos/clube-clube-opener.json +125 -0
- package/lib/estudio/dados/projetos/clube-clube.json +108 -0
- package/lib/estudio/dados/projetos/clube-codigo.json +101 -0
- package/lib/estudio/dados/projetos/clube-distilbook.json +104 -0
- package/lib/estudio/dados/projetos/clube-drive-upload.json +115 -0
- package/lib/estudio/dados/projetos/clube-dub.json +119 -0
- package/lib/estudio/dados/projetos/clube-enxovaly.json +105 -0
- package/lib/estudio/dados/projetos/clube-fecha7.json +118 -0
- package/lib/estudio/dados/projetos/clube-filatov.json +128 -0
- package/lib/estudio/dados/projetos/clube-frame.json +108 -0
- package/lib/estudio/dados/projetos/clube-glass.json +118 -0
- package/lib/estudio/dados/projetos/clube-letra-em-apps.json +84 -0
- package/lib/estudio/dados/projetos/clube-letra-globo-3d.json +111 -0
- package/lib/estudio/dados/projetos/clube-letra-nos-apps.json +146 -0
- package/lib/estudio/dados/projetos/clube-letra-seis-mundos.json +100 -0
- package/lib/estudio/dados/projetos/clube-lula-3.json +146 -0
- package/lib/estudio/dados/projetos/clube-morph.json +119 -0
- package/lib/estudio/dados/projetos/clube-motion-designer.json +115 -0
- package/lib/estudio/dados/projetos/clube-motion-pacote.json +112 -0
- package/lib/estudio/dados/projetos/clube-movimento.json +103 -0
- package/lib/estudio/dados/projetos/clube-not-like-us.json +117 -0
- package/lib/estudio/dados/projetos/clube-pinterest-inspiracao.json +86 -0
- package/lib/estudio/dados/projetos/clube-pixel-e-tinta.json +108 -0
- package/lib/estudio/dados/projetos/clube-ponte.json +118 -0
- package/lib/estudio/dados/projetos/clube-processo-edit-tonal.json +126 -0
- package/lib/estudio/dados/projetos/clube-processo-motion-100-ia.json +109 -0
- package/lib/estudio/dados/projetos/clube-promo.json +114 -0
- package/lib/estudio/dados/projetos/clube-proposito.json +115 -0
- package/lib/estudio/dados/projetos/clube-rankhog.json +128 -0
- package/lib/estudio/dados/projetos/clube-recursao.json +116 -0
- package/lib/estudio/dados/projetos/clube-revelar.json +118 -0
- package/lib/estudio/dados/projetos/clube-robo.json +125 -0
- package/lib/estudio/dados/projetos/clube-shelby-ep2.json +144 -0
- package/lib/estudio/dados/projetos/clube-shelby.json +97 -0
- package/lib/estudio/dados/projetos/clube-solar-eclipse.json +96 -0
- package/lib/estudio/dados/projetos/clube-soldier.json +128 -0
- package/lib/estudio/dados/projetos/clube-som-de-cena.json +108 -0
- package/lib/estudio/dados/projetos/clube-spotify-candidatura.json +112 -0
- package/lib/estudio/dados/projetos/clube-tda-app.json +118 -0
- package/lib/estudio/dados/projetos/clube-tda-ia.json +75 -0
- package/lib/estudio/dados/projetos/clube-telegram-chat.json +118 -0
- package/lib/estudio/dados/projetos/clube-telegram-ilha.json +106 -0
- package/lib/estudio/dados/projetos/clube-tennessee-nike.json +132 -0
- package/lib/estudio/dados/projetos/clube-tudo-se-move.json +112 -0
- package/lib/estudio/dados/projetos/clube-uiad.json +107 -0
- package/lib/estudio/dados/projetos/clube-viralgrow.json +108 -0
- package/lib/estudio/dados/projetos/clube-whose-side.json +140 -0
- package/lib/estudio/dados/projetos/clube-youtube-inscrever.json +108 -0
- package/lib/estudio/dados/projetos/elementos-do-site-em-movimento.json +49 -0
- package/lib/estudio/dados/projetos/explainer-narrado.json +51 -0
- package/lib/estudio/dados/projetos/lancamento-de-produto.json +51 -0
- package/lib/estudio/dados/projetos/motion-so-com-sons.json +50 -0
- package/lib/estudio/dados/projetos/titulo-em-after-effects.json +51 -0
- package/lib/estudio/dados/projetos/tour-de-saas.json +50 -0
- package/lib/estudio/dados/projetos-ae/antes-e-agora/ae/cenas.jsx +733 -0
- package/lib/estudio/dados/projetos-ae/antes-e-agora/assets/fazer_assets.py +88 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/ae/cenas.jsx +402 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/preparar_assets.py +17 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/svg_para_ae.py +25 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/BRIEF-AGENTES.md +31 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/README.md +53 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ROTEIRO-CENAS.md +69 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/A-agenda.jsx +148 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/B-docs.jsx +174 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/C-alttab.jsx +113 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/D-spotify.jsx +118 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/E-notas.jsx +146 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/F-jornal1.jsx +144 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/G-google.jsx +153 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/H-mapa.jsx +176 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/I-imagens.jsx +132 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/J-jornal2.jsx +191 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/K-maps.jsx +184 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera-movimentos.jsx +48 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera.jsx +57 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/card-spotify.jsx +98 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/digitar.jsx +38 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/jornal-comum.jsx +196 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra.jsx +23 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra_automatica.py +83 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/montagem.jsx +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/render_html.py +13 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/util.jsx +162 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-midia-comum.jsx +114 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/wg-util.jsx +360 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/F-jornal1/render_html.py +13 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/render_w-midia.py +26 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/inclin.py +30 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_d.py +36 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_e.py +27 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ajustar_movimentos.py +61 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ancorar.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/assar.py +47 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/camera3d_jornal.py +172 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/comparar.py +32 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/dados_ae_jornal.py +46 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/folha_contato.py +28 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/homografia_jornal.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/integrar.py +44 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/medir_quadro_a_quadro.py +32 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/mesa_2d.py +64 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/plot_cenas.py +11 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/retificar_quadros.py +38 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/assar_ab.py +52 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/medir_ab.py +59 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ajustar_h.py +111 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_corrige_cam.py +17 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ecc_cadeia.py +80 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_finalizar_cam.py +65 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_mosaico.py +61 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/01-gramatica-e-roteiro.md +55 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/02-camera.md +66 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/03-montagem-no-ae.md +50 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/04-render-e-conferencia.md +43 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/05-abertura-celular-mergulho.md +43 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces.md +42 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/atraso_imagem.py +31 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/fundo.py +56 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/juntar.py +45 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/montar.py +177 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/rastreio.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/sincronia.py +18 -0
- package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/ae/cenas.jsx +530 -0
- package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/assets/fazer_assets.py +107 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/assets/pessoas/figurinha.py +46 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/BRIEF-A.md +83 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/audio.py +179 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/confere.py +20 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte1.js +1 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte2.js +1 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/gerar_emojis.py +11 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/img/filmes/_indice.js +122 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/base.js +380 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/main.js +86 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1a.js +169 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1b.js +455 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1c.js +389 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p2.js +379 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/video.py +62 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/README.md +65 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/oraculo_take.py +137 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/render_take.py +388 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/take.js +235 -0
- package/lib/estudio/diretor.js +2 -1
- package/lib/estudio/editor.js +285 -0
- package/lib/estudio/efeitos-fabrica/terminal-digitando.jsx +2 -1
- package/lib/estudio/effectcraft.js +152 -0
- package/lib/estudio/exportar.js +181 -0
- package/lib/estudio/gerativo.js +44 -15
- package/lib/estudio/index.js +126 -5
- package/lib/estudio/kit/gerativo.jsx +15 -0
- package/lib/estudio/kit-ae/base.jsx +1146 -0
- package/lib/estudio/kit-ae/cenas.jsx +395 -0
- package/lib/estudio/kit-ae/primitivas.jsx +911 -0
- package/lib/estudio/kokoro.js +3 -1
- package/lib/estudio/maquina-diretor.js +393 -0
- package/lib/estudio/maquina-elementos.js +134 -0
- package/lib/estudio/maquina-layout.js +198 -0
- package/lib/estudio/maquina-som.js +478 -0
- package/lib/estudio/maquina-trilha.js +160 -0
- package/lib/estudio/maquina-voz.js +429 -0
- package/lib/estudio/maquina.js +516 -0
- package/lib/estudio/projetos.js +60 -0
- package/lib/estudio/remotion.js +6 -4
- package/lib/estudio/revisor.js +61 -0
- package/lib/estudio/voicestudio.js +81 -0
- package/lib/headless.js +71 -0
- package/lib/hooks.js +72 -0
- package/lib/local/cerebro.json +398 -12
- package/lib/local/entrada.js +46 -2
- package/lib/marca-kit.js +309 -0
- package/lib/mcp.js +286 -6
- package/lib/perigo.js +31 -3
- package/lib/planos.js +47 -19
- package/lib/preparar.js +111 -0
- package/lib/retomar.js +55 -0
- package/lib/sandbox.js +197 -0
- package/lib/skills.js +3 -1
- package/lib/tools.js +13 -4
- package/lib/ui.js +1 -1
- package/lib/video/auto.js +18 -3
- package/package.json +4 -2
- package/skills/after-effects/SKILL.md +52 -0
- package/skills/maquina-de-motion/SKILL.md +84 -0
- package/skills/motion-de-primeira/SKILL.md +33 -51
package/lib/mcp.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
* ~/.primocode/mcp.json e <pasta do projeto>/.mcp.json
|
|
14
14
|
* { "mcpServers": { "nome": { "command": "npx", "args": [...], "env": {...} } } }
|
|
15
15
|
*
|
|
16
|
+
* Servidor remoto: { "type": "http", "url": "…", "headers": {…} } (HTTP streamable,
|
|
17
|
+
* com login OAuth: `primocode mcp login <nome>`).
|
|
18
|
+
*
|
|
16
19
|
* O cliente é o mínimo do protocolo por stdio: JSON-RPC 2.0, uma mensagem por
|
|
17
20
|
* linha — initialize, notifications/initialized, tools/list, tools/call.
|
|
18
21
|
*
|
|
@@ -51,7 +54,7 @@ function servidores(pasta = process.cwd()) {
|
|
|
51
54
|
const out = process.env.PRIMOCODE_SEM_MCP === '1' ? {} : { ...PADRAO };
|
|
52
55
|
for (const arq of [CONFIG, path.join(pasta, '.mcp.json')]) {
|
|
53
56
|
const j = lerJson(arq);
|
|
54
|
-
for (const [nome, s] of Object.entries((j && j.mcpServers) || {})) if (s && s.command) out[nome] = s;
|
|
57
|
+
for (const [nome, s] of Object.entries((j && j.mcpServers) || {})) if (s && (s.command || s.url)) out[nome] = s;
|
|
55
58
|
}
|
|
56
59
|
return out;
|
|
57
60
|
}
|
|
@@ -64,7 +67,12 @@ function expandir(args, env) {
|
|
|
64
67
|
return (args || []).map((a) => String(a).replace(/\$\{([A-Z0-9_]+)\}/g, (_, k) => (env[k] != null ? env[k] : '')));
|
|
65
68
|
}
|
|
66
69
|
|
|
67
|
-
|
|
70
|
+
/** Sobe/abre o servidor: `url` → HTTP, senão stdio. Mesma interface: {pedir, fechar}. */
|
|
71
|
+
function conectar(def, opcoes = {}) {
|
|
72
|
+
return def && def.url ? conectarHttp(def, opcoes) : conectarStdio(def, opcoes);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function conectarStdio(def, { tetoMs = 60000 } = {}) {
|
|
68
76
|
return new Promise((resolve, reject) => {
|
|
69
77
|
const env = { ...process.env, ...(def.env || {}) };
|
|
70
78
|
const p = spawn(def.command, expandir(def.args, env), { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
@@ -94,6 +102,229 @@ function conectar(def, { tetoMs = 60000 } = {}) {
|
|
|
94
102
|
});
|
|
95
103
|
}
|
|
96
104
|
|
|
105
|
+
/* ── MCP REMOTO (HTTP "streamable") ─────────────────────────────────────
|
|
106
|
+
* Entrada de config: { "type": "http", "url": "https://…/mcp", "headers": {…} }.
|
|
107
|
+
* Cada mensagem é um POST com JSON-RPC; a resposta vem como JSON ou como SSE.
|
|
108
|
+
* O servidor pode dar um `mcp-session-id` no initialize — vai em todo POST
|
|
109
|
+
* depois. 401 → login OAuth (`primocode mcp login <nome>`), mais abaixo. */
|
|
110
|
+
const TOKENS = path.join(CASA, 'mcp-tokens.json');
|
|
111
|
+
const PROTOCOLO = '2025-06-18';
|
|
112
|
+
|
|
113
|
+
function lerTokens() { return lerJson(TOKENS) || {}; }
|
|
114
|
+
function gravarTokens(mudar) {
|
|
115
|
+
const t = lerTokens();
|
|
116
|
+
mudar(t);
|
|
117
|
+
fs.mkdirSync(CASA, { recursive: true });
|
|
118
|
+
fs.writeFileSync(TOKENS, JSON.stringify(t, null, 2), { mode: 0o600 });
|
|
119
|
+
try { fs.chmodSync(TOKENS, 0o600); } catch { /* sem chmod */ }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Lê uma resposta SSE até achar a mensagem do id pedido (sem esperar o stream fechar). */
|
|
123
|
+
async function lerSse(res, id) {
|
|
124
|
+
const dec = new TextDecoder();
|
|
125
|
+
let buf = '', achada = null;
|
|
126
|
+
const tratar = (bloco) => {
|
|
127
|
+
const dados = bloco.split(/\r?\n/).filter((l) => l.startsWith('data:')).map((l) => l.slice(5).replace(/^ /, '')).join('\n');
|
|
128
|
+
if (!dados) return;
|
|
129
|
+
let m; try { m = JSON.parse(dados); } catch { return; }
|
|
130
|
+
if (m && m.id === id && (m.result !== undefined || m.error)) achada = m;
|
|
131
|
+
};
|
|
132
|
+
const rd = res.body.getReader();
|
|
133
|
+
try {
|
|
134
|
+
for (;;) {
|
|
135
|
+
const { done, value } = await rd.read();
|
|
136
|
+
if (value) buf += dec.decode(value, { stream: !done });
|
|
137
|
+
let i;
|
|
138
|
+
while ((i = buf.search(/\r?\n\r?\n/)) >= 0) {
|
|
139
|
+
tratar(buf.slice(0, i));
|
|
140
|
+
buf = buf.slice(i).replace(/^\r?\n\r?\n/, '');
|
|
141
|
+
if (achada) return achada;
|
|
142
|
+
}
|
|
143
|
+
if (done) break;
|
|
144
|
+
}
|
|
145
|
+
if (buf.trim()) tratar(buf);
|
|
146
|
+
} finally { try { await rd.cancel(); } catch { /* já fechou */ } }
|
|
147
|
+
return achada;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function conectarHttp(def, { tetoMs = 60000, nome, buscar = globalThis.fetch } = {}) {
|
|
151
|
+
let sessao = null, id = 0;
|
|
152
|
+
const cabecalhos = () => {
|
|
153
|
+
const h = { 'content-type': 'application/json', accept: 'application/json, text/event-stream', 'mcp-protocol-version': PROTOCOLO };
|
|
154
|
+
const env = { ...process.env, ...(def.env || {}) };
|
|
155
|
+
// ID token da conta do PrimoCode já logada (o preset primocode-clube usa no lugar de OAuth).
|
|
156
|
+
if (env.PRIMOCODE_ID_TOKEN == null) { try { env.PRIMOCODE_ID_TOKEN = require('./config.js').tokenDaConta() || ''; } catch { env.PRIMOCODE_ID_TOKEN = ''; } }
|
|
157
|
+
for (const [k, v] of Object.entries(def.headers || {})) h[k.toLowerCase()] = expandir([v], env)[0];
|
|
158
|
+
const tk = nome && lerTokens()[nome];
|
|
159
|
+
if (tk && tk.access_token && !h.authorization) h.authorization = `Bearer ${tk.access_token}`;
|
|
160
|
+
if (sessao) h['mcp-session-id'] = sessao;
|
|
161
|
+
return h;
|
|
162
|
+
};
|
|
163
|
+
const postar = async (corpo, jaRenovou = false) => {
|
|
164
|
+
const res = await buscar(def.url, { method: 'POST', headers: cabecalhos(), body: JSON.stringify(corpo), signal: AbortSignal.timeout(tetoMs) });
|
|
165
|
+
if (res.status === 401) {
|
|
166
|
+
try { await res.arrayBuffer(); } catch { /* sem corpo */ }
|
|
167
|
+
if (!jaRenovou && nome && await renovarToken(nome, { buscar })) return postar(corpo, true);
|
|
168
|
+
const e = new Error(`o servidor "${nome || def.url}" pede login — rode: primocode mcp login ${nome || '<nome>'}`);
|
|
169
|
+
e.precisaLogin = true; e.wwwAuthenticate = res.headers.get('www-authenticate') || '';
|
|
170
|
+
throw e;
|
|
171
|
+
}
|
|
172
|
+
return res;
|
|
173
|
+
};
|
|
174
|
+
const pedir = async (method, params = {}) => {
|
|
175
|
+
const meu = ++id;
|
|
176
|
+
const res = await postar({ jsonrpc: '2.0', id: meu, method, params });
|
|
177
|
+
const s = res.headers.get('mcp-session-id');
|
|
178
|
+
if (s) sessao = s;
|
|
179
|
+
if (!res.ok) throw new Error(`o servidor MCP respondeu HTTP ${res.status}`);
|
|
180
|
+
const tipo = String(res.headers.get('content-type') || '');
|
|
181
|
+
const m = /event-stream/i.test(tipo) ? await lerSse(res, meu) : await res.json();
|
|
182
|
+
if (!m) throw new Error('o servidor MCP não respondeu à chamada');
|
|
183
|
+
if (m.error) throw new Error(m.error.message || 'erro do MCP');
|
|
184
|
+
return m.result;
|
|
185
|
+
};
|
|
186
|
+
const avisar = async (method, params = {}) => {
|
|
187
|
+
const res = await postar({ jsonrpc: '2.0', method, params });
|
|
188
|
+
try { await res.arrayBuffer(); } catch { /* 202 sem corpo */ }
|
|
189
|
+
};
|
|
190
|
+
const fechar = () => {
|
|
191
|
+
if (!sessao) return;
|
|
192
|
+
buscar(def.url, { method: 'DELETE', headers: cabecalhos(), signal: AbortSignal.timeout(3000) }).catch(() => {});
|
|
193
|
+
};
|
|
194
|
+
return pedir('initialize', { protocolVersion: PROTOCOLO, capabilities: {}, clientInfo: { name: 'primocode', version: '10' } })
|
|
195
|
+
.then(async () => { await avisar('notifications/initialized'); return { pedir, fechar }; });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* ── LOGIN OAUTH 2.1 (PKCE + registro dinâmico) ─────────────────────────
|
|
199
|
+
* 401 → /.well-known/oauth-protected-resource → servidor de autorização →
|
|
200
|
+
* /.well-known/oauth-authorization-server. Registra o cliente (DCR) com
|
|
201
|
+
* redirect em 127.0.0.1:<porta>/callback, abre o navegador, recebe o code,
|
|
202
|
+
* troca por token. Token só em ~/.primocode/mcp-tokens.json (0600): nunca no
|
|
203
|
+
* repo, nunca no log. */
|
|
204
|
+
const b64url = (b) => Buffer.from(b).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
205
|
+
|
|
206
|
+
function abrirUrl(url) {
|
|
207
|
+
const [cmd, args] = process.platform === 'darwin' ? ['open', [url]]
|
|
208
|
+
: process.platform === 'win32' ? ['cmd', ['/c', 'start', '""', url]]
|
|
209
|
+
: ['xdg-open', [url]];
|
|
210
|
+
try { spawn(cmd, args, { stdio: 'ignore', detached: true }).on('error', () => {}).unref(); return true; }
|
|
211
|
+
catch { return false; }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function jsonDe(buscar, url, opcoes) {
|
|
215
|
+
const r = await buscar(url, { signal: AbortSignal.timeout(15000), ...opcoes });
|
|
216
|
+
let j = null; try { j = await r.json(); } catch { /* sem JSON */ }
|
|
217
|
+
return { ok: r.ok, status: r.status, j: j || {} };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function descobrirOAuth(def, wwwAuth, buscar) {
|
|
221
|
+
const u = new URL(def.url);
|
|
222
|
+
const dica = /resource_metadata="?([^",\s]+)"?/i.exec(wwwAuth || '');
|
|
223
|
+
const candidatos = [dica && dica[1], `${u.origin}/.well-known/oauth-protected-resource${u.pathname === '/' ? '' : u.pathname}`, `${u.origin}/.well-known/oauth-protected-resource`].filter(Boolean);
|
|
224
|
+
let recurso = null;
|
|
225
|
+
for (const c of candidatos) { const r = await jsonDe(buscar, c).catch(() => null); if (r && r.ok && r.j.authorization_servers) { recurso = r.j; break; } }
|
|
226
|
+
const emissor = String((recurso && recurso.authorization_servers && recurso.authorization_servers[0]) || u.origin).replace(/\/$/, '');
|
|
227
|
+
let meta = null;
|
|
228
|
+
for (const p of ['/.well-known/oauth-authorization-server', '/.well-known/openid-configuration']) {
|
|
229
|
+
const r = await jsonDe(buscar, emissor + p).catch(() => null);
|
|
230
|
+
if (r && r.ok && r.j.authorization_endpoint && r.j.token_endpoint) { meta = r.j; break; }
|
|
231
|
+
}
|
|
232
|
+
if (!meta) throw new Error(`não achei os metadados OAuth de ${emissor}`);
|
|
233
|
+
return { meta, escopos: (recurso && recurso.scopes_supported) || [], recurso: (recurso && recurso.resource) || def.url };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Espera o navegador voltar em 127.0.0.1:<porta>/callback. Devolve {porta, esperar, fechar}. */
|
|
237
|
+
function ouvirCallback(estado, prazoMs) {
|
|
238
|
+
const http = require('http');
|
|
239
|
+
return new Promise((resolve, reject) => {
|
|
240
|
+
let fim;
|
|
241
|
+
const esperando = new Promise((res, rej) => { fim = { res, rej }; });
|
|
242
|
+
esperando.catch(() => {});
|
|
243
|
+
const srv = http.createServer((req, resp) => {
|
|
244
|
+
const q = new URL(req.url, 'http://127.0.0.1').searchParams;
|
|
245
|
+
if (!req.url.startsWith('/callback')) { resp.statusCode = 404; return resp.end(); }
|
|
246
|
+
const ok = q.get('code') && q.get('state') === estado;
|
|
247
|
+
resp.writeHead(ok ? 200 : 400, { 'content-type': 'text/html; charset=utf-8' });
|
|
248
|
+
resp.end(`<!doctype html><meta charset="utf-8"><title>PrimoCode</title><body style="font:16px system-ui;text-align:center;padding:4rem">${ok ? 'Pronto, pode fechar esta aba e voltar ao PrimoCode.' : 'Login não concluído. Volte ao terminal.'}`);
|
|
249
|
+
if (ok) fim.res(q.get('code')); else fim.rej(new Error(q.get('error_description') || q.get('error') || 'resposta de login inválida'));
|
|
250
|
+
});
|
|
251
|
+
srv.on('error', reject);
|
|
252
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
253
|
+
const t = setTimeout(() => fim.rej(new Error('o login demorou demais')), prazoMs);
|
|
254
|
+
resolve({ porta: srv.address().port, esperar: () => esperando, fechar: () => { clearTimeout(t); srv.close(); } });
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* `primocode mcp login <nome>`. `abrir` e `buscar` são injetáveis (o teste não abre navegador).
|
|
261
|
+
* @returns {Promise<{ok:boolean, servidor?:string, error?:string}>}
|
|
262
|
+
*/
|
|
263
|
+
async function entrar(nome, { pasta, abrir = abrirUrl, aviso = () => {}, buscar = globalThis.fetch, prazoMs = 5 * 60000 } = {}) {
|
|
264
|
+
const def = servidores(pasta)[nome];
|
|
265
|
+
if (!def || !def.url) return { ok: false, error: `"${nome}" não é um servidor MCP remoto (type http com url) — veja primocode mcp list` };
|
|
266
|
+
let cb;
|
|
267
|
+
try {
|
|
268
|
+
// Sem token (o do header manual não conta): o 401 diz onde está o OAuth.
|
|
269
|
+
let www = '';
|
|
270
|
+
try { await conectarHttp({ ...def, headers: {} }, { buscar }).then((c) => c.fechar()); }
|
|
271
|
+
catch (e) { if (!e.precisaLogin) throw e; www = e.wwwAuthenticate; }
|
|
272
|
+
const { meta, escopos, recurso } = await descobrirOAuth(def, www, buscar);
|
|
273
|
+
const estado = b64url(require('crypto').randomBytes(16));
|
|
274
|
+
cb = await ouvirCallback(estado, prazoMs);
|
|
275
|
+
const redirect = `http://127.0.0.1:${cb.porta}/callback`;
|
|
276
|
+
if (!meta.registration_endpoint) throw new Error('o servidor não aceita registro dinâmico de cliente');
|
|
277
|
+
const reg = await jsonDe(buscar, meta.registration_endpoint, { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
278
|
+
body: JSON.stringify({ client_name: 'PrimoCode', redirect_uris: [redirect], grant_types: ['authorization_code', 'refresh_token'],
|
|
279
|
+
response_types: ['code'], token_endpoint_auth_method: 'none', ...(escopos.length ? { scope: escopos.join(' ') } : {}) }) });
|
|
280
|
+
if (!reg.j.client_id) throw new Error(`o registro do cliente falhou (HTTP ${reg.status})`);
|
|
281
|
+
const verificador = b64url(require('crypto').randomBytes(32));
|
|
282
|
+
const desafio = b64url(require('crypto').createHash('sha256').update(verificador).digest());
|
|
283
|
+
const auth = new URL(meta.authorization_endpoint);
|
|
284
|
+
for (const [k, v] of Object.entries({ response_type: 'code', client_id: reg.j.client_id, redirect_uri: redirect, state: estado,
|
|
285
|
+
code_challenge: desafio, code_challenge_method: 'S256', resource: recurso, ...(escopos.length ? { scope: escopos.join(' ') } : {}) })) auth.searchParams.set(k, v);
|
|
286
|
+
aviso(auth.toString());
|
|
287
|
+
await abrir(auth.toString());
|
|
288
|
+
const code = await cb.esperar();
|
|
289
|
+
const tk = await jsonDe(buscar, meta.token_endpoint, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
290
|
+
body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: redirect, client_id: reg.j.client_id, code_verifier: verificador, resource: recurso }).toString() });
|
|
291
|
+
if (!tk.j.access_token) throw new Error(`a troca do code falhou: ${tk.j.error_description || tk.j.error || 'HTTP ' + tk.status}`);
|
|
292
|
+
gravarTokens((t) => { t[nome] = { access_token: tk.j.access_token, refresh_token: tk.j.refresh_token || null,
|
|
293
|
+
expira_em: tk.j.expires_in ? Date.now() + Number(tk.j.expires_in) * 1000 : null,
|
|
294
|
+
client_id: reg.j.client_id, token_endpoint: meta.token_endpoint, revocation_endpoint: meta.revocation_endpoint || null, resource: recurso }; });
|
|
295
|
+
return { ok: true, servidor: nome };
|
|
296
|
+
} catch (e) { return { ok: false, error: e.message }; }
|
|
297
|
+
finally { if (cb) cb.fechar(); }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Troca o refresh_token por um access_token novo. Devolve false se não deu. */
|
|
301
|
+
async function renovarToken(nome, { buscar = globalThis.fetch } = {}) {
|
|
302
|
+
const tk = lerTokens()[nome];
|
|
303
|
+
if (!tk || !tk.refresh_token || !tk.token_endpoint) return false;
|
|
304
|
+
try {
|
|
305
|
+
const r = await jsonDe(buscar, tk.token_endpoint, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
306
|
+
body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: tk.refresh_token, client_id: tk.client_id, ...(tk.resource ? { resource: tk.resource } : {}) }).toString() });
|
|
307
|
+
if (!r.j.access_token) return false;
|
|
308
|
+
gravarTokens((t) => { t[nome] = { ...tk, access_token: r.j.access_token, refresh_token: r.j.refresh_token || tk.refresh_token,
|
|
309
|
+
expira_em: r.j.expires_in ? Date.now() + Number(r.j.expires_in) * 1000 : null }; });
|
|
310
|
+
return true;
|
|
311
|
+
} catch { return false; }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** `primocode mcp logout <nome>`: revoga (se o servidor deixa) e apaga o token local. */
|
|
315
|
+
async function sair(nome, { buscar = globalThis.fetch } = {}) {
|
|
316
|
+
const tk = lerTokens()[nome];
|
|
317
|
+
if (!tk) return { ok: false, error: `não há login guardado para "${nome}"` };
|
|
318
|
+
if (tk.revocation_endpoint) {
|
|
319
|
+
const corpo = (token, tipo) => jsonDe(buscar, tk.revocation_endpoint, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
320
|
+
body: new URLSearchParams({ token, token_type_hint: tipo, client_id: tk.client_id }).toString() }).catch(() => null);
|
|
321
|
+
if (tk.refresh_token) await corpo(tk.refresh_token, 'refresh_token');
|
|
322
|
+
await corpo(tk.access_token, 'access_token');
|
|
323
|
+
}
|
|
324
|
+
gravarTokens((t) => { delete t[nome]; });
|
|
325
|
+
return { ok: true, servidor: nome };
|
|
326
|
+
}
|
|
327
|
+
|
|
97
328
|
function resumirTools(tools) {
|
|
98
329
|
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
330
|
}
|
|
@@ -114,7 +345,7 @@ async function listar(pasta) {
|
|
|
114
345
|
const out = {};
|
|
115
346
|
await Promise.all(Object.entries(lista).map(async ([nome, def]) => {
|
|
116
347
|
try {
|
|
117
|
-
const c = await conectar(def);
|
|
348
|
+
const c = await conectar(def, { nome });
|
|
118
349
|
const r = await c.pedir('tools/list');
|
|
119
350
|
c.fechar();
|
|
120
351
|
out[nome] = resumirTools(r.tools);
|
|
@@ -127,7 +358,7 @@ async function listar(pasta) {
|
|
|
127
358
|
async function chamar(servidor, tool, args = {}, pasta) {
|
|
128
359
|
const def = servidores(pasta)[servidor];
|
|
129
360
|
if (!def) throw new Error(`não há servidor MCP "${servidor}" — veja primocode mcp list`);
|
|
130
|
-
const c = await conectar(def);
|
|
361
|
+
const c = await conectar(def, { nome: servidor });
|
|
131
362
|
try {
|
|
132
363
|
const r = await c.pedir('tools/call', { name: tool, arguments: args });
|
|
133
364
|
return r;
|
|
@@ -173,6 +404,15 @@ const CURADOS = [
|
|
|
173
404
|
{ nome: 'stripe', termos: /stripe|pagamento|cobran[çc]a/i, descricao: 'Stripe oficial: clientes, cobranças, links de pagamento',
|
|
174
405
|
command: 'npx', args: ['-y', '@stripe/mcp', '--tools=all'],
|
|
175
406
|
precisa: [{ nome: 'STRIPE_SECRET_KEY', onde: 'dashboard.stripe.com/apikeys (de preferência uma restricted key)' }] },
|
|
407
|
+
{ nome: 'clube-do-motion', termos: /clube do motion|motion|anima[çc][aã]o|refer[êe]ncias? de motion/i,
|
|
408
|
+
descricao: 'projetos de motion completos, referências, skills e músicas (precisa de conta assinante do Clube do Motion)',
|
|
409
|
+
type: 'http', url: 'https://clubedomotion.com/mcp', precisa: [] },
|
|
410
|
+
/* O Clube do próprio PrimoCode (server/lib/clube-mcp.js). Sem OAuth por ora: o header leva o
|
|
411
|
+
ID token da conta já logada no CLI (${PRIMOCODE_ID_TOKEN}, preenchido em conectarHttp). TODO: OAuth. */
|
|
412
|
+
{ nome: 'primocode-clube', termos: /primocode clube|clube do primocode/i,
|
|
413
|
+
descricao: 'Clube do PrimoCode: projetos, skills, referências e músicas (grátis vê resumo; completo é do plano pago)',
|
|
414
|
+
type: 'http', url: 'https://primocode-server-production.up.railway.app/mcp',
|
|
415
|
+
headers: { Authorization: 'Bearer ${PRIMOCODE_ID_TOKEN}' }, precisa: [] },
|
|
176
416
|
{ nome: 'linear', termos: /linear/i, descricao: 'Linear oficial: issues, projetos (login pelo navegador)',
|
|
177
417
|
...REMOTO('https://mcp.linear.app/mcp'), precisa: [] },
|
|
178
418
|
{ nome: 'sentry', termos: /sentry/i, descricao: 'Sentry oficial: erros, issues, releases',
|
|
@@ -237,6 +477,8 @@ function doRegistro(item) {
|
|
|
237
477
|
}
|
|
238
478
|
|
|
239
479
|
function doCurado(c) {
|
|
480
|
+
if (c.url) return { nome: c.nome, fonte: 'curado', descricao: c.descricao,
|
|
481
|
+
instalar: [{ tipo: 'curado', command: 'primocode', args: ['mcp', 'conectar', c.nome], env: [] }] };
|
|
240
482
|
return { nome: c.nome, fonte: 'curado', descricao: c.descricao,
|
|
241
483
|
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
484
|
}
|
|
@@ -304,6 +546,28 @@ async function conectarNovo({ nome, command, comando, args, env } = {}, { config
|
|
|
304
546
|
if (!/^[a-z0-9][a-z0-9_.-]{0,40}$/.test(n)) return { ok: false, error: 'nome inválido: letras, números, - _ . (ex: "notion")' };
|
|
305
547
|
const curado = CURADOS.find((c) => c.nome === n);
|
|
306
548
|
let cmd = command || comando;
|
|
549
|
+
if (!cmd && curado && curado.url) {
|
|
550
|
+
// Servidor remoto (HTTP): grava, tenta; se pede login, o registro fica e o usuário entra.
|
|
551
|
+
const def = { type: 'http', url: curado.url };
|
|
552
|
+
if (curado.headers) def.headers = curado.headers;
|
|
553
|
+
const j = lerConfig(config);
|
|
554
|
+
const antes = j.mcpServers[n];
|
|
555
|
+
j.mcpServers[n] = def;
|
|
556
|
+
gravarConfig(config, j);
|
|
557
|
+
let c;
|
|
558
|
+
try {
|
|
559
|
+
c = await conectar(def, { tetoMs, nome: n });
|
|
560
|
+
const ferramentas = resumirTools((await c.pedir('tools/list')).tools);
|
|
561
|
+
gravarCache((sv) => { sv[n] = ferramentas; });
|
|
562
|
+
return { ok: true, servidor: n, ferramentas, proximo: `Conectado. Use mcp_usar {servidor:"${n}", ferramenta, argumentos}.` };
|
|
563
|
+
} catch (e) {
|
|
564
|
+
if (e.precisaLogin) return { ok: false, salvo: true, precisaLogin: true, error: `"${n}" foi salvo e precisa de login. Peça ao usuário para rodar: primocode mcp login ${n}` };
|
|
565
|
+
const k = lerConfig(config);
|
|
566
|
+
if (antes) k.mcpServers[n] = antes; else delete k.mcpServers[n];
|
|
567
|
+
gravarConfig(config, k);
|
|
568
|
+
return { ok: false, salvo: false, error: `o servidor "${n}" não subiu: ${e.message}. Nada ficou salvo.` };
|
|
569
|
+
} finally { if (c) c.fechar(); }
|
|
570
|
+
}
|
|
307
571
|
let lista = Array.isArray(args) ? args.map(String) : (typeof args === 'string' && args.trim() ? args.trim().split(/\s+/) : null);
|
|
308
572
|
if (!cmd && curado) { cmd = curado.command; lista = lista || curado.args.slice(); }
|
|
309
573
|
if (!cmd) return { ok: false, error: `não conheço "${n}" de cor — chame mcp_procurar primeiro e passe comando e args do "instalar".` };
|
|
@@ -332,7 +596,7 @@ async function conectarNovo({ nome, command, comando, args, env } = {}, { config
|
|
|
332
596
|
gravarConfig(config, j);
|
|
333
597
|
let c;
|
|
334
598
|
try {
|
|
335
|
-
c = await conectar(def, { tetoMs });
|
|
599
|
+
c = await conectar(def, { tetoMs, nome: n });
|
|
336
600
|
const r = await c.pedir('tools/list');
|
|
337
601
|
const ferramentas = resumirTools(r.tools);
|
|
338
602
|
gravarCache((sv) => { sv[n] = ferramentas; });
|
|
@@ -385,8 +649,24 @@ async function main(argv) {
|
|
|
385
649
|
console.log('primocode mcp add <nome> -- <comando> [args…] — guarda um servidor em ~/.primocode/mcp.json');
|
|
386
650
|
console.log('primocode mcp procurar <serviço> — acha o MCP de um serviço (lista curada + registro oficial)');
|
|
387
651
|
console.log('primocode mcp conectar <nome> [--env K=V…] [-- <comando> [args…]] — grava, sobe e valida');
|
|
652
|
+
console.log('primocode mcp login <nome> — entra num servidor remoto (OAuth pelo navegador); ex: clube-do-motion');
|
|
653
|
+
console.log('primocode mcp logout <nome> — apaga o login guardado em ~/.primocode/mcp-tokens.json');
|
|
388
654
|
return 0;
|
|
389
655
|
}
|
|
656
|
+
if (op === 'login' || op === 'logout') {
|
|
657
|
+
if (!resto[0]) { console.log(`uso: primocode mcp ${op} <nome>`); return 1; }
|
|
658
|
+
if (op === 'login') {
|
|
659
|
+
// Servidor da lista curada ainda não gravado: grava antes de entrar.
|
|
660
|
+
const cur = CURADOS.find((c) => c.nome === resto[0] && c.url);
|
|
661
|
+
if (cur && !servidores(process.cwd())[cur.nome]) { const j = lerConfig(CONFIG); j.mcpServers[cur.nome] = { type: 'http', url: cur.url }; gravarConfig(CONFIG, j); }
|
|
662
|
+
const r = await entrar(resto[0], { pasta: process.cwd(), aviso: (u) => console.log(`abrindo o navegador… se não abrir, cole este link:\n${u}`) });
|
|
663
|
+
console.log(r.ok ? `login feito: ${r.servidor}` : r.error);
|
|
664
|
+
return r.ok ? 0 : 1;
|
|
665
|
+
}
|
|
666
|
+
const r = await sair(resto[0]);
|
|
667
|
+
console.log(r.ok ? `login apagado: ${r.servidor}` : r.error);
|
|
668
|
+
return r.ok ? 0 : 1;
|
|
669
|
+
}
|
|
390
670
|
if (op === 'list') { console.log(JSON.stringify(await listar(process.cwd()))); return 0; }
|
|
391
671
|
if (op === 'call') {
|
|
392
672
|
if (json === null) { console.log(JSON.stringify({ ok: false, error: '--json inválido' })); return 1; }
|
|
@@ -437,4 +717,4 @@ async function main(argv) {
|
|
|
437
717
|
}
|
|
438
718
|
}
|
|
439
719
|
|
|
440
|
-
module.exports = { servidores, conectar, listar, chamar, usar, dica, main, procurar, doRegistro, conectarNovo, CURADOS, expandir };
|
|
720
|
+
module.exports = { servidores, conectar, listar, chamar, usar, dica, main, procurar, doRegistro, conectarNovo, CURADOS, expandir, entrar, sair, renovarToken };
|
package/lib/perigo.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* auto — o padrão. Roda tudo, mas SEMPRE pergunta antes do perigoso.
|
|
18
18
|
* perguntar — pergunta antes de todo comando e de toda escrita de arquivo.
|
|
19
19
|
* livre — nunca pergunta. Nem o perigoso.
|
|
20
|
+
* plano — SÓ LÊ: recusa escrita de arquivo, comando que muda coisa e MCP novo.
|
|
21
|
+
* O agente investiga e entrega o plano; a pessoa executa em outro modo.
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
24
|
'use strict';
|
|
@@ -24,13 +26,13 @@
|
|
|
24
26
|
const os = require('os');
|
|
25
27
|
const path = require('path');
|
|
26
28
|
|
|
27
|
-
const MODOS = ['auto', 'perguntar', 'livre'];
|
|
29
|
+
const MODOS = ['auto', 'perguntar', 'livre', 'plano'];
|
|
28
30
|
const COMANDOS = new Set(['run_shell', 'run_in_new_terminal']);
|
|
29
31
|
const ESCRITAS = new Set(['write_file', 'edit_file', 'multi_edit']);
|
|
30
32
|
|
|
31
33
|
function normalizarModo(m) {
|
|
32
34
|
const s = String(m || '').trim().toLowerCase();
|
|
33
|
-
const apelido = { ask: 'perguntar', suggest: 'perguntar', full: 'livre', 'full-auto': 'livre', yolo: 'livre' };
|
|
35
|
+
const apelido = { ask: 'perguntar', suggest: 'perguntar', full: 'livre', 'full-auto': 'livre', yolo: 'livre', plan: 'plano', 'read-only': 'plano' };
|
|
34
36
|
const n = apelido[s] || s;
|
|
35
37
|
return MODOS.includes(n) ? n : 'auto';
|
|
36
38
|
}
|
|
@@ -230,6 +232,24 @@ function classificar(nome, args, { projectDir } = {}) {
|
|
|
230
232
|
return { perigoso: !!motivo, motivo: motivo || '' };
|
|
231
233
|
}
|
|
232
234
|
|
|
235
|
+
/* Comando que só olha: todo trecho começa por uma palavra de leitura, sem
|
|
236
|
+
redirecionar para arquivo. Na dúvida, não é leitura. */
|
|
237
|
+
const LEITURA = new Set([...SO_LE, 'pwd', 'echo', 'find', 'tree', 'which', 'diff', 'sort', 'uniq', 'du', 'df']);
|
|
238
|
+
const GIT_LE = new Set(['status', 'diff', 'log', 'show', 'branch', 'ls-files', 'blame', 'rev-parse']);
|
|
239
|
+
function somenteLeitura(comando) {
|
|
240
|
+
const s = String(comando || '');
|
|
241
|
+
if (/[>`]|\$\(|\bfind\b[^;&|]*(-delete|-exec)/.test(s)) return false;
|
|
242
|
+
const ts = trechos(s);
|
|
243
|
+
if (!ts.length) return false;
|
|
244
|
+
return ts.every((t) => {
|
|
245
|
+
const p = t.palavras.filter((x) => !PREFIXOS.has(x) && !/^[A-Za-z_]\w*=/.test(x));
|
|
246
|
+
if (!p.length) return false;
|
|
247
|
+
const cmd = path.basename(p[0]);
|
|
248
|
+
if (cmd === 'git') return GIT_LE.has(p.slice(1).find((x) => !x.startsWith('-')));
|
|
249
|
+
return LEITURA.has(cmd);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
233
253
|
function novo() { return { liberados: new Set(), negados: new Set() }; }
|
|
234
254
|
|
|
235
255
|
/* Uma pergunta por vez. Subagentes rodam em paralelo (Promise.all) e dois
|
|
@@ -246,6 +266,14 @@ function emFila(fn) { const p = fila.then(fn, fn); fila = p.catch(() => {}); ret
|
|
|
246
266
|
async function portao(estado, { modo, nome, args, projectDir, subagent = false, perguntar = null }) {
|
|
247
267
|
const m = normalizarModo(modo);
|
|
248
268
|
if (m === 'livre') return { ok: true };
|
|
269
|
+
if (m === 'plano') {
|
|
270
|
+
const mexe = ESCRITAS.has(nome) || nome === 'mcp_conectar'
|
|
271
|
+
|| (COMANDOS.has(nome) && !somenteLeitura((args || {}).command));
|
|
272
|
+
if (mexe) {
|
|
273
|
+
return { ok: false, motivo: 'modo plano', error: 'Recusado: o modo plano só lê. Investigue com read_file, grep e comandos de leitura, e entregue o plano; a pessoa muda de modo (/permissoes auto) para executar.' };
|
|
274
|
+
}
|
|
275
|
+
return { ok: true };
|
|
276
|
+
}
|
|
249
277
|
const { perigoso, motivo: motivoPerigo } = classificar(nome, args, { projectDir });
|
|
250
278
|
const tipo = perigoso ? 'perigo' : (m === 'perguntar' && (COMANDOS.has(nome) || nome === 'mcp_usar')) ? 'comando'
|
|
251
279
|
: (m === 'perguntar' && ESCRITAS.has(nome)) ? 'arquivo' : null;
|
|
@@ -285,4 +313,4 @@ async function portao(estado, { modo, nome, args, projectDir, subagent = false,
|
|
|
285
313
|
return recusa(negado);
|
|
286
314
|
}
|
|
287
315
|
|
|
288
|
-
module.exports = { classificar, portao, novo, normalizarModo, trechos, MODOS };
|
|
316
|
+
module.exports = { somenteLeitura, classificar, portao, novo, normalizarModo, trechos, MODOS };
|
package/lib/planos.js
CHANGED
|
@@ -15,17 +15,20 @@
|
|
|
15
15
|
const https = require('https');
|
|
16
16
|
const http = require('http');
|
|
17
17
|
|
|
18
|
+
const R = (videosMes, resolucao, marcaDagua, narracaoPremium, clubeCompleto, scriptsAE, prioridade, subagentes) =>
|
|
19
|
+
({ videosMes, resolucao, marcaDagua, narracaoPremium, clubeCompleto, scriptsAE, prioridade, subagentes });
|
|
20
|
+
const GRATIS = { id: 'free', nome: 'Grátis', preco: 0, recursos: R(3, 720, true, false, false, false, 0, 0), inclui: [] };
|
|
18
21
|
const RESERVA = [
|
|
19
|
-
{ id: 'primo', nome: 'Primo', preco: 69.9,
|
|
20
|
-
{ id: 'pro', nome: 'Primo Pro', preco: 129.9,
|
|
21
|
-
{ id: 'max', nome: 'Primo Max', preco: 299.9,
|
|
22
|
-
{ id: 'studio', nome: 'Primo Studio', preco: 599.9,
|
|
22
|
+
{ id: 'primo', nome: 'Primo', preco: 69.9, recursos: R(15, 720, false, false, true, false, 0, 0), inclui: [] },
|
|
23
|
+
{ id: 'pro', nome: 'Primo Pro', preco: 129.9, recursos: R(60, 1080, false, true, true, true, 1, 1), inclui: [] },
|
|
24
|
+
{ id: 'max', nome: 'Primo Max', preco: 299.9, recursos: R(200, 2160, false, true, true, true, 2, 3), inclui: [] },
|
|
25
|
+
{ id: 'studio', nome: 'Primo Studio', preco: 599.9, recursos: R(800, 2160, false, true, true, true, 3, 8), inclui: [] },
|
|
23
26
|
];
|
|
24
27
|
|
|
25
28
|
function buscarPlanos(servidor, { timeout = 8000 } = {}) {
|
|
26
29
|
return new Promise((resolve) => {
|
|
27
30
|
let url;
|
|
28
|
-
try { url = new URL('/api/planos', servidor); } catch { return resolve({ ok: false, planos: RESERVA }); }
|
|
31
|
+
try { url = new URL('/api/planos', servidor); } catch { return resolve({ ok: false, planos: RESERVA, gratis: GRATIS }); }
|
|
29
32
|
const lib = url.protocol === 'https:' ? https : http;
|
|
30
33
|
const req = lib.get(url, { timeout }, (res) => {
|
|
31
34
|
let t = '';
|
|
@@ -35,37 +38,62 @@ function buscarPlanos(servidor, { timeout = 8000 } = {}) {
|
|
|
35
38
|
try {
|
|
36
39
|
const j = JSON.parse(t);
|
|
37
40
|
const lista = Array.isArray(j) ? j : j && j.planos;
|
|
38
|
-
if (res.statusCode === 200 && Array.isArray(lista) && lista.length) return resolve({ ok: true, planos: lista });
|
|
41
|
+
if (res.statusCode === 200 && Array.isArray(lista) && lista.length) return resolve({ ok: true, planos: lista, gratis: (j && j.gratis) || GRATIS });
|
|
39
42
|
} catch { /* cai na reserva */ }
|
|
40
|
-
resolve({ ok: false, planos: RESERVA });
|
|
43
|
+
resolve({ ok: false, planos: RESERVA, gratis: GRATIS });
|
|
41
44
|
});
|
|
42
45
|
});
|
|
43
46
|
req.on('timeout', () => req.destroy());
|
|
44
|
-
req.on('error', () => resolve({ ok: false, planos: RESERVA }));
|
|
47
|
+
req.on('error', () => resolve({ ok: false, planos: RESERVA, gratis: GRATIS }));
|
|
45
48
|
});
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
const reais = (n) => `R$ ${Number(n).toFixed(2).replace('.', ',')}`;
|
|
49
52
|
|
|
53
|
+
const ALTURA = { 720: '720p', 1080: '1080p', 2160: '4K' };
|
|
54
|
+
/* As linhas da tabela: o que cada degrau compra. Cada função lê o recurso de um plano. */
|
|
55
|
+
const LINHAS = [
|
|
56
|
+
['Vídeos por mês', (r) => String(r.videosMes || 'sem teto')],
|
|
57
|
+
['Resolução', (r) => ALTURA[r.resolucao] || `${r.resolucao}p`],
|
|
58
|
+
['Marca d\'água', (r) => (r.marcaDagua ? 'sim' : 'sem')],
|
|
59
|
+
['Narração premium', (r) => (r.narracaoPremium ? '✓' : '—')],
|
|
60
|
+
['Projetos do Clube', (r) => (r.clubeCompleto ? 'completos' : 'resumo')],
|
|
61
|
+
['Scripts AE modelo', (r) => (r.scriptsAE ? '✓' : '—')],
|
|
62
|
+
['Prioridade na cadeia', (r) => (r.prioridade ? '●'.repeat(r.prioridade) : '—')],
|
|
63
|
+
['Subagentes', (r) => (r.subagentes ? String(r.subagentes) : '—')],
|
|
64
|
+
];
|
|
65
|
+
|
|
50
66
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
67
|
+
* A tabela comparativa, um plano por coluna. `atual` é o id do plano da pessoa.
|
|
68
|
+
* Cada coluna tem 13 caracteres; o plano da pessoa leva ● no título.
|
|
69
|
+
* @param {{planos:object[], gratis?:object, ok:boolean}} r
|
|
53
70
|
* @param {{cores:object, atual?:string, site:string}} op
|
|
54
71
|
*/
|
|
55
|
-
function desenhar({ planos, ok }, { cores: c, atual = null, site }) {
|
|
72
|
+
function desenhar({ planos, gratis, ok }, { cores: c, atual = null, site }) {
|
|
73
|
+
const todos = [gratis || GRATIS, ...planos].filter((p) => p.recursos);
|
|
74
|
+
const L = 21, W = 13;
|
|
75
|
+
const ce = (t, n) => { t = String(t); const f = Math.max(0, n - t.length); return ' '.repeat(f >> 1) + t + ' '.repeat(f - (f >> 1)); };
|
|
76
|
+
const meu = (p) => (atual ? p.id === atual : p.id === 'free');
|
|
56
77
|
const linhas = ['', c.white(' Planos do PrimoCode') + c.dim(' · por mês, cancele quando quiser'), ''];
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
linhas.push('
|
|
62
|
-
|
|
78
|
+
linhas.push(' ' + ' '.repeat(L) + todos.map((p) => c.brand(ce((meu(p) ? '● ' : '') + p.nome.replace('Primo ', ''), W))).join(''));
|
|
79
|
+
linhas.push(' ' + ' '.repeat(L) + todos.map((p) => c.white(ce(p.preco ? reais(p.preco) : 'R$ 0', W))).join(''));
|
|
80
|
+
linhas.push(c.dim(' ' + '─'.repeat(L + W * todos.length)));
|
|
81
|
+
for (const [nome, f] of LINHAS) {
|
|
82
|
+
linhas.push(' ' + c.muted(nome.padEnd(L)) + todos.map((p) => c.white(ce(f(p.recursos), W))).join(''));
|
|
83
|
+
}
|
|
84
|
+
linhas.push('');
|
|
85
|
+
if (ok) {
|
|
86
|
+
for (const p of planos) {
|
|
87
|
+
if (atual && p.id === atual) continue;
|
|
88
|
+
linhas.push(' ' + c.dim('assinar ') + c.brand(p.nome.padEnd(13)) + (p.checkout ? c.brand(p.checkout) : c.dim('checkout em breve')));
|
|
89
|
+
}
|
|
63
90
|
linhas.push('');
|
|
91
|
+
} else {
|
|
92
|
+
linhas.push(c.dim(' (sem resposta do servidor: valores de referência, sem link de compra)'), '');
|
|
64
93
|
}
|
|
65
|
-
if (!ok) linhas.push(c.dim(' (sem resposta do servidor: preços de referência, sem link de compra)'));
|
|
66
94
|
linhas.push(c.dim(' Também em ') + c.brand(`${site}/planos`) + c.dim(' · já assinou? ') + c.brand('/account') + c.dim(' atualiza.'));
|
|
67
95
|
linhas.push('');
|
|
68
96
|
return linhas;
|
|
69
97
|
}
|
|
70
98
|
|
|
71
|
-
module.exports = { buscarPlanos, desenhar, RESERVA, reais };
|
|
99
|
+
module.exports = { buscarPlanos, desenhar, RESERVA, GRATIS, reais };
|