primocode 10.13.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.
Files changed (195) hide show
  1. package/README.md +66 -12
  2. package/bin/primocode.js +148 -8
  3. package/lib/act.js +48 -1
  4. package/lib/catalogo.js +12 -1
  5. package/lib/config.js +1 -0
  6. package/lib/estudio/cota.js +150 -0
  7. package/lib/estudio/dados/projetos/clube-ad5.json +105 -0
  8. package/lib/estudio/dados/projetos/clube-ae-sem-memoria.json +107 -0
  9. package/lib/estudio/dados/projetos/clube-agent.json +120 -0
  10. package/lib/estudio/dados/projetos/clube-antes-e-agora.json +86 -0
  11. package/lib/estudio/dados/projetos/clube-apresentando-clube.json +110 -0
  12. package/lib/estudio/dados/projetos/clube-bluevisa.json +106 -0
  13. package/lib/estudio/dados/projetos/clube-caught-in-the-fire.json +127 -0
  14. package/lib/estudio/dados/projetos/clube-chat-cliente-satisfeito.json +98 -0
  15. package/lib/estudio/dados/projetos/clube-chatgpt-anuncios.json +120 -0
  16. package/lib/estudio/dados/projetos/clube-claude-dj.json +140 -0
  17. package/lib/estudio/dados/projetos/clube-claude-mcp.json +142 -0
  18. package/lib/estudio/dados/projetos/clube-cleanui.json +106 -0
  19. package/lib/estudio/dados/projetos/clube-clinica-take-unico.json +128 -0
  20. package/lib/estudio/dados/projetos/clube-clinica.json +104 -0
  21. package/lib/estudio/dados/projetos/clube-clube-opener.json +125 -0
  22. package/lib/estudio/dados/projetos/clube-clube.json +108 -0
  23. package/lib/estudio/dados/projetos/clube-codigo.json +101 -0
  24. package/lib/estudio/dados/projetos/clube-distilbook.json +104 -0
  25. package/lib/estudio/dados/projetos/clube-drive-upload.json +115 -0
  26. package/lib/estudio/dados/projetos/clube-dub.json +119 -0
  27. package/lib/estudio/dados/projetos/clube-enxovaly.json +105 -0
  28. package/lib/estudio/dados/projetos/clube-fecha7.json +118 -0
  29. package/lib/estudio/dados/projetos/clube-filatov.json +128 -0
  30. package/lib/estudio/dados/projetos/clube-frame.json +108 -0
  31. package/lib/estudio/dados/projetos/clube-glass.json +118 -0
  32. package/lib/estudio/dados/projetos/clube-letra-em-apps.json +84 -0
  33. package/lib/estudio/dados/projetos/clube-letra-globo-3d.json +111 -0
  34. package/lib/estudio/dados/projetos/clube-letra-nos-apps.json +146 -0
  35. package/lib/estudio/dados/projetos/clube-letra-seis-mundos.json +100 -0
  36. package/lib/estudio/dados/projetos/clube-lula-3.json +146 -0
  37. package/lib/estudio/dados/projetos/clube-morph.json +119 -0
  38. package/lib/estudio/dados/projetos/clube-motion-designer.json +115 -0
  39. package/lib/estudio/dados/projetos/clube-motion-pacote.json +112 -0
  40. package/lib/estudio/dados/projetos/clube-movimento.json +103 -0
  41. package/lib/estudio/dados/projetos/clube-not-like-us.json +117 -0
  42. package/lib/estudio/dados/projetos/clube-pinterest-inspiracao.json +86 -0
  43. package/lib/estudio/dados/projetos/clube-pixel-e-tinta.json +108 -0
  44. package/lib/estudio/dados/projetos/clube-ponte.json +118 -0
  45. package/lib/estudio/dados/projetos/clube-processo-edit-tonal.json +126 -0
  46. package/lib/estudio/dados/projetos/clube-processo-motion-100-ia.json +109 -0
  47. package/lib/estudio/dados/projetos/clube-promo.json +114 -0
  48. package/lib/estudio/dados/projetos/clube-proposito.json +115 -0
  49. package/lib/estudio/dados/projetos/clube-rankhog.json +128 -0
  50. package/lib/estudio/dados/projetos/clube-recursao.json +116 -0
  51. package/lib/estudio/dados/projetos/clube-revelar.json +118 -0
  52. package/lib/estudio/dados/projetos/clube-robo.json +125 -0
  53. package/lib/estudio/dados/projetos/clube-shelby-ep2.json +144 -0
  54. package/lib/estudio/dados/projetos/clube-shelby.json +97 -0
  55. package/lib/estudio/dados/projetos/clube-solar-eclipse.json +96 -0
  56. package/lib/estudio/dados/projetos/clube-soldier.json +128 -0
  57. package/lib/estudio/dados/projetos/clube-som-de-cena.json +108 -0
  58. package/lib/estudio/dados/projetos/clube-spotify-candidatura.json +112 -0
  59. package/lib/estudio/dados/projetos/clube-tda-app.json +118 -0
  60. package/lib/estudio/dados/projetos/clube-tda-ia.json +75 -0
  61. package/lib/estudio/dados/projetos/clube-telegram-chat.json +118 -0
  62. package/lib/estudio/dados/projetos/clube-telegram-ilha.json +106 -0
  63. package/lib/estudio/dados/projetos/clube-tennessee-nike.json +132 -0
  64. package/lib/estudio/dados/projetos/clube-tudo-se-move.json +112 -0
  65. package/lib/estudio/dados/projetos/clube-uiad.json +107 -0
  66. package/lib/estudio/dados/projetos/clube-viralgrow.json +108 -0
  67. package/lib/estudio/dados/projetos/clube-whose-side.json +140 -0
  68. package/lib/estudio/dados/projetos/clube-youtube-inscrever.json +108 -0
  69. package/lib/estudio/dados/projetos/elementos-do-site-em-movimento.json +49 -0
  70. package/lib/estudio/dados/projetos/explainer-narrado.json +51 -0
  71. package/lib/estudio/dados/projetos/lancamento-de-produto.json +51 -0
  72. package/lib/estudio/dados/projetos/motion-so-com-sons.json +50 -0
  73. package/lib/estudio/dados/projetos/titulo-em-after-effects.json +51 -0
  74. package/lib/estudio/dados/projetos/tour-de-saas.json +50 -0
  75. package/lib/estudio/dados/projetos-ae/antes-e-agora/ae/cenas.jsx +733 -0
  76. package/lib/estudio/dados/projetos-ae/antes-e-agora/assets/fazer_assets.py +88 -0
  77. package/lib/estudio/dados/projetos-ae/letra-em-apps/ae/cenas.jsx +402 -0
  78. package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/preparar_assets.py +17 -0
  79. package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/svg_para_ae.py +25 -0
  80. package/lib/estudio/dados/projetos-ae/letra-nos-apps/BRIEF-AGENTES.md +31 -0
  81. package/lib/estudio/dados/projetos-ae/letra-nos-apps/README.md +53 -0
  82. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ROTEIRO-CENAS.md +69 -0
  83. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/A-agenda.jsx +148 -0
  84. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/B-docs.jsx +174 -0
  85. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/C-alttab.jsx +113 -0
  86. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/D-spotify.jsx +118 -0
  87. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/E-notas.jsx +146 -0
  88. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/F-jornal1.jsx +144 -0
  89. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/G-google.jsx +153 -0
  90. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/H-mapa.jsx +176 -0
  91. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/I-imagens.jsx +132 -0
  92. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/J-jornal2.jsx +191 -0
  93. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/K-maps.jsx +184 -0
  94. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera-movimentos.jsx +48 -0
  95. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera.jsx +57 -0
  96. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/card-spotify.jsx +98 -0
  97. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/digitar.jsx +38 -0
  98. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/jornal-comum.jsx +196 -0
  99. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra.jsx +23 -0
  100. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra_automatica.py +83 -0
  101. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/montagem.jsx +74 -0
  102. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/render_html.py +13 -0
  103. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/util.jsx +162 -0
  104. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-midia-comum.jsx +114 -0
  105. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/wg-util.jsx +360 -0
  106. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/F-jornal1/render_html.py +13 -0
  107. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/render_w-midia.py +26 -0
  108. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/inclin.py +30 -0
  109. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_d.py +36 -0
  110. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_e.py +27 -0
  111. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ajustar_movimentos.py +61 -0
  112. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ancorar.py +74 -0
  113. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/assar.py +47 -0
  114. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/camera3d_jornal.py +172 -0
  115. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/comparar.py +32 -0
  116. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/dados_ae_jornal.py +46 -0
  117. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/folha_contato.py +28 -0
  118. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/homografia_jornal.py +74 -0
  119. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/integrar.py +44 -0
  120. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/medir_quadro_a_quadro.py +32 -0
  121. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/mesa_2d.py +64 -0
  122. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/plot_cenas.py +11 -0
  123. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/retificar_quadros.py +38 -0
  124. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/assar_ab.py +52 -0
  125. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/medir_ab.py +59 -0
  126. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ajustar_h.py +111 -0
  127. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_corrige_cam.py +17 -0
  128. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ecc_cadeia.py +80 -0
  129. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_finalizar_cam.py +65 -0
  130. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_mosaico.py +61 -0
  131. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/01-gramatica-e-roteiro.md +55 -0
  132. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/02-camera.md +66 -0
  133. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/03-montagem-no-ae.md +50 -0
  134. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/04-render-e-conferencia.md +43 -0
  135. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/05-abertura-celular-mergulho.md +43 -0
  136. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces.md +42 -0
  137. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/atraso_imagem.py +31 -0
  138. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/fundo.py +56 -0
  139. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/juntar.py +45 -0
  140. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/montar.py +177 -0
  141. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/rastreio.py +74 -0
  142. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/sincronia.py +18 -0
  143. package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/ae/cenas.jsx +530 -0
  144. package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/assets/fazer_assets.py +107 -0
  145. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/assets/pessoas/figurinha.py +46 -0
  146. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/BRIEF-A.md +83 -0
  147. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/audio.py +179 -0
  148. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/confere.py +20 -0
  149. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte1.js +1 -0
  150. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte2.js +1 -0
  151. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/gerar_emojis.py +11 -0
  152. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/img/filmes/_indice.js +122 -0
  153. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/base.js +380 -0
  154. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/main.js +86 -0
  155. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1a.js +169 -0
  156. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1b.js +455 -0
  157. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1c.js +389 -0
  158. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p2.js +379 -0
  159. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/video.py +62 -0
  160. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/README.md +65 -0
  161. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/oraculo_take.py +137 -0
  162. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/render_take.py +388 -0
  163. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/take.js +235 -0
  164. package/lib/estudio/editor.js +285 -0
  165. package/lib/estudio/effectcraft.js +152 -0
  166. package/lib/estudio/exportar.js +181 -0
  167. package/lib/estudio/index.js +90 -2
  168. package/lib/estudio/kit-ae/base.jsx +1146 -0
  169. package/lib/estudio/kit-ae/cenas.jsx +395 -0
  170. package/lib/estudio/kit-ae/primitivas.jsx +911 -0
  171. package/lib/estudio/maquina-diretor.js +393 -0
  172. package/lib/estudio/maquina-elementos.js +134 -0
  173. package/lib/estudio/maquina-layout.js +198 -0
  174. package/lib/estudio/maquina-som.js +478 -0
  175. package/lib/estudio/maquina-trilha.js +160 -0
  176. package/lib/estudio/maquina-voz.js +429 -0
  177. package/lib/estudio/maquina.js +516 -0
  178. package/lib/estudio/projetos.js +60 -0
  179. package/lib/headless.js +71 -0
  180. package/lib/hooks.js +72 -0
  181. package/lib/local/cerebro.json +398 -12
  182. package/lib/local/entrada.js +46 -2
  183. package/lib/marca-kit.js +309 -0
  184. package/lib/mcp.js +286 -6
  185. package/lib/perigo.js +31 -3
  186. package/lib/planos.js +47 -19
  187. package/lib/preparar.js +111 -0
  188. package/lib/retomar.js +55 -0
  189. package/lib/sandbox.js +197 -0
  190. package/lib/skills.js +3 -1
  191. package/lib/tools.js +13 -4
  192. package/lib/ui.js +1 -1
  193. package/package.json +4 -2
  194. package/skills/after-effects/SKILL.md +52 -0
  195. package/skills/maquina-de-motion/SKILL.md +84 -0
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
- function conectar(def, { tetoMs = 60000 } = {}) {
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, inclui: ['app e site sem configurar nada', 'modelos do servidor', 'vídeo em 720p'] },
20
- { id: 'pro', nome: 'Primo Pro', preco: 129.9, inclui: ['limite alto', 'estúdio de vídeo completo, em 1080p', 'cenários salvos'] },
21
- { id: 'max', nome: 'Primo Max', preco: 299.9, inclui: ['sem limite por hora', 'prioridade na fila', 'os melhores modelos', 'vídeos longos'] },
22
- { id: 'studio', nome: 'Primo Studio', preco: 599.9, inclui: ['para agência e empresa', 'várias pessoas', 'marca própria nos vídeos'] },
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
- * As linhas da tela. `atual` é o id do plano da pessoa (ou null).
52
- * @param {{planos:object[], ok:boolean}} r
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
- for (const p of planos) {
58
- const meu = atual && p.id === atual;
59
- linhas.push(' ' + (meu ? c.ok('● ') : ' ') + c.brand(p.nome.padEnd(14)) + c.white(reais(p.preco).padStart(11))
60
- + c.dim(' /mês') + (meu ? c.ok(' ← o seu') : ''));
61
- linhas.push(' ' + c.muted((p.inclui || []).join(' · ')));
62
- if (ok && !meu) linhas.push(' ' + (p.checkout ? c.dim('assinar: ') + c.brand(p.checkout) : c.dim('checkout em breve')));
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 };