ps-claw 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web-ui/server.mjs CHANGED
@@ -2,20 +2,366 @@
2
2
 
3
3
  /**
4
4
  * PS Claw — Servidor Web
5
- * Proxy reverso para APIs de IA (Anthropic, OpenAI, Google, etc.)
6
- * Evita CORS bloqueando chamadas diretas do navegador
5
+ * - Servidor estático para a interface (public/)
6
+ * - Proxy genérico para APIs externas (evita CORS)
7
+ * - Endpoints de Loja (Claw Hub) com instalação 1-clique
8
+ * - Endpoints de integração Browser Use
7
9
  */
8
10
 
9
11
  import http from "node:http";
10
12
  import https from "node:https";
11
13
  import fs from "node:fs";
12
14
  import path from "node:path";
15
+ import os from "node:os";
16
+ import { spawn, spawnSync, execSync } from "node:child_process";
13
17
  import { fileURLToPath } from "node:url";
14
18
 
15
19
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
20
  const WEB_PORT = process.env.PS_CLAW_WEB_PORT || 3000;
17
21
 
18
- // Proxy genérico para APIs externas
22
+ const CONFIG_DIR = path.join(os.homedir(), ".ps-claw");
23
+ const PLUGINS_DIR = path.join(CONFIG_DIR, "plugins");
24
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
25
+
26
+ function ensureDir(d) {
27
+ try { if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); } catch {}
28
+ }
29
+ ensureDir(CONFIG_DIR);
30
+ ensureDir(PLUGINS_DIR);
31
+
32
+ function loadInstalledPlugins() {
33
+ try {
34
+ const idx = path.join(PLUGINS_DIR, "installed.json");
35
+ if (!fs.existsSync(idx)) return [];
36
+ return JSON.parse(fs.readFileSync(idx, "utf8") || "[]");
37
+ } catch { return []; }
38
+ }
39
+ function saveInstalledPlugins(list) {
40
+ ensureDir(PLUGINS_DIR);
41
+ fs.writeFileSync(path.join(PLUGINS_DIR, "installed.json"), JSON.stringify(list, null, 2));
42
+ }
43
+
44
+ // ─── Catálogo Claw Hub (curado) ────────────────────────────────────────────
45
+ const CLAW_HUB_CATALOG = [
46
+ // ── Categoria: Browser & Automação ──
47
+ { id:"browser-use", name:"Browser Use", category:"Automação", icon:"🌐", rating:4.9, downloads:"48k", featured:true,
48
+ desc:"Controle de navegador por IA. A IA abre sites, clica, preenche formulários e extrai dados.",
49
+ install:{type:"pip", pkg:"browser-use"}, homepage:"https://github.com/browser-use/browser-use" },
50
+ { id:"playwright-mcp", name:"Playwright MCP", category:"Automação", icon:"🎭", rating:4.8, downloads:"32k", featured:true,
51
+ desc:"Model Context Provider para Playwright — automação de browser headless via LLM.",
52
+ install:{type:"npm", pkg:"@playwright/mcp"}, homepage:"https://github.com/microsoft/playwright-mcp" },
53
+ { id:"puppeteer-mcp", name:"Puppeteer MCP", category:"Automação", icon:"🪄", rating:4.6, downloads:"21k",
54
+ desc:"Automação de Chrome/Chromium via MCP para tarefas de scraping e RPA.",
55
+ install:{type:"npm", pkg:"@modelcontextprotocol/server-puppeteer"}, homepage:"https://github.com/modelcontextprotocol/servers" },
56
+
57
+ // ── Categoria: Memória & Conhecimento ──
58
+ { id:"memory-mcp", name:"Memory Server", category:"Memória", icon:"🧠", rating:4.7, downloads:"56k", featured:true,
59
+ desc:"Memória persistente entre sessões via grafo de conhecimento.",
60
+ install:{type:"npm", pkg:"@modelcontextprotocol/server-memory"}, homepage:"https://github.com/modelcontextprotocol/servers" },
61
+ { id:"sqlite-mcp", name:"SQLite Database", category:"Memória", icon:"💾", rating:4.5, downloads:"18k",
62
+ desc:"Acesso a banco SQLite local para guardar logs, transcrições e dados estruturados.",
63
+ install:{type:"npm", pkg:"@modelcontextprotocol/server-sqlite"}, homepage:"https://github.com/modelcontextprotocol/servers" },
64
+ { id:"chroma-mcp", name:"Chroma Vector DB", category:"Memória", icon:"🔍", rating:4.4, downloads:"9k",
65
+ desc:"Banco vetorial Chroma para RAG e embeddings semânticos.",
66
+ install:{type:"pip", pkg:"chromadb"}, homepage:"https://github.com/chroma-core/chroma" },
67
+
68
+ // ── Categoria: Documentos ──
69
+ { id:"filesystem-mcp", name:"Filesystem", category:"Documentos", icon:"📁", rating:4.9, downloads:"72k", featured:true,
70
+ desc:"Leitura/escrita de arquivos controlada. Define pastas permitidas.",
71
+ install:{type:"npm", pkg:"@modelcontextprotocol/server-filesystem"}, homepage:"https://github.com/modelcontextprotocol/servers" },
72
+ { id:"github-mcp", name:"GitHub", category:"Documentos", icon:"🐙", rating:4.8, downloads:"61k", featured:true,
73
+ desc:"Acesse repositórios, issues, PRs e commits do GitHub direto do agente.",
74
+ install:{type:"npm", pkg:"@modelcontextprotocol/server-github"}, homepage:"https://github.com/modelcontextprotocol/servers" },
75
+ { id:"gitlab-mcp", name:"GitLab", category:"Documentos", icon:"🦊", rating:4.3, downloads:"8k",
76
+ desc:"Integração com GitLab — merge requests, pipelines e issues.",
77
+ install:{type:"npm", pkg:"@modelcontextprotocol/server-gitlab"}, homepage:"https://github.com/modelcontextprotocol/servers" },
78
+ { id:"notion-mcp", name:"Notion", category:"Documentos", icon:"📝", rating:4.5, downloads:"27k",
79
+ desc:"Acesse e crie páginas no Notion. Requer token de integração.",
80
+ install:{type:"npm", pkg:"@modelcontextprotocol/server-notion"}, homepage:"https://github.com/modelcontextprotocol/servers" },
81
+
82
+ // ── Categoria: Busca & Web ──
83
+ { id:"brave-search", name:"Brave Search", category:"Busca", icon:"🦁", rating:4.6, downloads:"34k", featured:true,
84
+ desc:"Busca na web privada via Brave Search API. Requer API key gratuita.",
85
+ install:{type:"npm", pkg:"@modelcontextprotocol/server-brave-search"}, homepage:"https://brave.com/search/api/" },
86
+ { id:"fetch-mcp", name:"Fetch Web", category:"Busca", icon:"🌐", rating:4.4, downloads:"22k",
87
+ desc:"Busca e extrai conteúdo de qualquer URL — Markdown automático.",
88
+ install:{type:"npm", pkg:"@modelcontextprotocol/server-fetch"}, homepage:"https://github.com/modelcontextprotocol/servers" },
89
+ { id:"tavily-mcp", name:"Tavily AI Search", category:"Busca", icon:"🔎", rating:4.5, downloads:"14k",
90
+ desc:"Busca semântica com IA otimizada para agentes. Requer API key.",
91
+ install:{type:"pip", pkg:"tavily-python"}, homepage:"https://tavily.com" },
92
+
93
+ // ── Categoria: Produtividade ──
94
+ { id:"google-drive", name:"Google Drive", category:"Produtividade",icon:"📁", rating:4.2, downloads:"19k",
95
+ desc:"Acesse arquivos do Google Drive via OAuth.",
96
+ install:{type:"npm", pkg:"@modelcontextprotocol/server-google-drive"}, homepage:"https://github.com/modelcontextprotocol/servers" },
97
+ { id:"slack-mcp", name:"Slack", category:"Produtividade",icon:"💬", rating:4.4, downloads:"16k",
98
+ desc:"Envie e leia mensagens no Slack. Requer token de bot.",
99
+ install:{type:"npm", pkg:"@modelcontextprotocol/server-slack"}, homepage:"https://github.com/modelcontextprotocol/servers" },
100
+ { id:"gmail-mcp", name:"Gmail", category:"Produtividade",icon:"✉️", rating:4.1, downloads:"23k",
101
+ desc:"Leia e envie emails do Gmail via OAuth.",
102
+ install:{type:"npm", pkg:"@modelcontextprotocol/server-gmail"}, homepage:"https://github.com/modelcontextprotocol/servers" },
103
+
104
+ // ── Categoria: DevOps ──
105
+ { id:"docker-mcp", name:"Docker", category:"DevOps", icon:"🐳", rating:4.6, downloads:"12k",
106
+ desc:"Gerencie containers, imagens e volumes do Docker.",
107
+ install:{type:"npm", pkg:"@modelcontextprotocol/server-docker"}, homepage:"https://github.com/modelcontextprotocol/servers" },
108
+ { id:"postgres-mcp", name:"PostgreSQL", category:"DevOps", icon:"🐘", rating:4.5, downloads:"15k",
109
+ desc:"Conexão com banco PostgreSQL para consultas e DDL.",
110
+ install:{type:"npm", pkg:"@modelcontextprotocol/server-postgres"}, homepage:"https://github.com/modelcontextprotocol/servers" },
111
+
112
+ // ── Categoria: IA & Modelos ──
113
+ { id:"ollama-mcp", name:"Ollama Local", category:"IA", icon:"🦙", rating:4.7, downloads:"41k", featured:true,
114
+ desc:"Rode modelos locais (Llama, Mistral, Phi) via Ollama — sem custo de API.",
115
+ install:{type:"npm", pkg:"@modelcontextprotocol/server-ollama"}, homepage:"https://ollama.com" },
116
+ { id:"openrouter", name:"OpenRouter Gateway", category:"IA", icon:"🔀", rating:4.4, downloads:"18k",
117
+ desc:"Acesso unificado a 100+ modelos via OpenRouter.",
118
+ install:{type:"npm", pkg:"@openrouter/ai-sdk-provider"}, homepage:"https://openrouter.ai" },
119
+ { id:"lm-studio", name:"LM Studio", category:"IA", icon:"🖥️", rating:4.5, downloads:"11k",
120
+ desc:"Servidor de inferência local compatível com OpenAI API.",
121
+ install:{type:"manual", cmd:"Baixe em https://lmstudio.ai"}, homepage:"https://lmstudio.ai" },
122
+
123
+ // ── Categoria: Mídia ──
124
+ { id:"whisper", name:"Whisper Transcribe", category:"Mídia", icon:"🎙️", rating:4.8, downloads:"28k",
125
+ desc:"Transcrição de áudio/vídeo via Whisper local ou API.",
126
+ install:{type:"pip", pkg:"openai-whisper"}, homepage:"https://github.com/openai/whisper" },
127
+ { id:"yt-mcp", name:"YouTube", category:"Mídia", icon:"📺", rating:4.3, downloads:"9k",
128
+ desc:"Busca e metadados do YouTube via API.",
129
+ install:{type:"npm", pkg:"@modelcontextprotocol/server-youtube"}, homepage:"https://github.com/modelcontextprotocol/servers" },
130
+
131
+ // ── Categoria: Skills PS Claw ──
132
+ { id:"skill-coder", name:"Skill: Coder Pro", category:"Skills", icon:"💻", rating:4.9, downloads:"37k", featured:true,
133
+ desc:"Skill especializada em escrever, refatorar e debugar código.",
134
+ install:{type:"ps-skill", pkg:"coder-pro"}, homepage:"https://github.com/Pedro21062014/Ps-Claw" },
135
+ { id:"skill-writer", name:"Skill: Writer", category:"Skills", icon:"✍️", rating:4.7, downloads:"24k",
136
+ desc:"Skill para criar textos, emails e documentos profissionais.",
137
+ install:{type:"ps-skill", pkg:"writer"}, homepage:"https://github.com/Pedro21062014/Ps-Claw" },
138
+ { id:"skill-analyst", name:"Skill: Data Analyst", category:"Skills", icon:"📊", rating:4.8, downloads:"19k", featured:true,
139
+ desc:"Skill para análise de dados, geração de gráficos e relatórios.",
140
+ install:{type:"ps-skill", pkg:"data-analyst"}, homepage:"https://github.com/Pedro21062014/Ps-Claw" },
141
+ { id:"skill-translator",name:"Skill: Translator", category:"Skills", icon:"🌍", rating:4.6, downloads:"14k",
142
+ desc:"Skill multilíngue para tradução técnica e literária.",
143
+ install:{type:"ps-skill", pkg:"translator"}, homepage:"https://github.com/Pedro21062014/Ps-Claw" },
144
+ ];
145
+
146
+ function getStoreCatalog() {
147
+ const installed = loadInstalledPlugins();
148
+ const installedIds = new Set(installed.map(p => p.id));
149
+ return {
150
+ categories: [...new Set(CLAW_HUB_CATALOG.map(p => p.category))],
151
+ plugins: CLAW_HUB_CATALOG.map(p => ({ ...p, installed: installedIds.has(p.id) })),
152
+ featured: CLAW_HUB_CATALOG.filter(p => p.featured).map(p => ({ ...p, installed: installedIds.has(p.id) })),
153
+ };
154
+ }
155
+
156
+ // ─── Instalação de plugins ─────────────────────────────────────────────────
157
+ function runCmd(cmd, args, opts = {}) {
158
+ return new Promise((resolve) => {
159
+ try {
160
+ const proc = spawn(cmd, args, {
161
+ stdio: ['ignore', 'pipe', 'pipe'],
162
+ shell: process.platform === 'win32',
163
+ ...opts,
164
+ });
165
+ let out = '', err = '';
166
+ proc.stdout.on('data', d => out += d.toString());
167
+ proc.stderr.on('data', d => err += d.toString());
168
+ proc.on('error', e => resolve({ ok: false, error: e.message, stdout: out, stderr: err }));
169
+ proc.on('exit', code => resolve({ ok: code === 0, code, stdout: out, stderr: err }));
170
+ } catch (e) {
171
+ resolve({ ok: false, error: e.message, stdout: '', stderr: '' });
172
+ }
173
+ });
174
+ }
175
+
176
+ function which(cmd) {
177
+ try {
178
+ const r = spawnSync(process.platform === 'win32' ? 'where' : 'which', [cmd], { encoding: 'utf8' });
179
+ return r.status === 0 && r.stdout.trim();
180
+ } catch { return false; }
181
+ }
182
+
183
+ async function installPlugin(plugin) {
184
+ const inst = plugin.install || {};
185
+ let result = { ok: false, message: 'Tipo de instalação desconhecido' };
186
+
187
+ if (inst.type === 'npm') {
188
+ const npmBin = which('npm');
189
+ if (!npmBin) {
190
+ return { ok: false, message: 'npm não encontrado. Instale o Node.js: https://nodejs.org' };
191
+ }
192
+ const r = await runCmd(npmBin, ['install', '-g', inst.pkg]);
193
+ result = r.ok
194
+ ? { ok: true, message: `${plugin.name} instalado via npm` }
195
+ : { ok: false, message: `npm falhou: ${(r.stderr || r.error || '').slice(0, 300)}` };
196
+ } else if (inst.type === 'pip') {
197
+ const pipBin = which('pip') || which('pip3') || which('python3');
198
+ if (!pipBin) {
199
+ return { ok: false, message: 'pip não encontrado. Instale Python: https://python.org' };
200
+ }
201
+ const args = (which('pip') || which('pip3')) ? ['install', '--user', inst.pkg] : ['-m', 'pip', 'install', '--user', inst.pkg];
202
+ const r = await runCmd(pipBin, args);
203
+ result = r.ok
204
+ ? { ok: true, message: `${plugin.name} instalado via pip` }
205
+ : { ok: false, message: `pip falhou: ${(r.stderr || r.error || '').slice(0, 300)}` };
206
+ } else if (inst.type === 'ps-skill') {
207
+ // Skill do PS Claw: cria um arquivo .md em ~/.ps-claw/skills/
208
+ const skillsDir = path.join(CONFIG_DIR, 'skills');
209
+ ensureDir(skillsDir);
210
+ const skillFile = path.join(skillsDir, `${inst.pkg}.md`);
211
+ const body = `# Skill: ${plugin.name}
212
+
213
+ ${plugin.desc}
214
+
215
+ ## Como usar
216
+ Esta skill é carregada automaticamente pelo PS Claw quando instalada.
217
+ Use o comando \`/skill ${inst.pkg}\` no chat para ativar.
218
+
219
+ ## Fonte
220
+ ${plugin.homepage || 'https://github.com/Pedro21062014/Ps-Claw'}
221
+ `;
222
+ try {
223
+ fs.writeFileSync(skillFile, body);
224
+ result = { ok: true, message: `Skill salva em ${skillFile}` };
225
+ } catch (e) {
226
+ result = { ok: false, message: `Erro ao salvar skill: ${e.message}` };
227
+ }
228
+ } else if (inst.type === 'manual') {
229
+ result = { ok: false, message: `Instalação manual necessária: ${inst.cmd || plugin.homepage}` };
230
+ }
231
+
232
+ // registra como instalado
233
+ if (result.ok) {
234
+ const list = loadInstalledPlugins();
235
+ if (!list.find(p => p.id === plugin.id)) {
236
+ list.push({ id: plugin.id, name: plugin.name, installedAt: new Date().toISOString(), type: inst.type, pkg: inst.pkg });
237
+ saveInstalledPlugins(list);
238
+ }
239
+ }
240
+ return result;
241
+ }
242
+
243
+ async function uninstallPlugin(plugin) {
244
+ const inst = plugin.install || {};
245
+ let result = { ok: true, message: 'Removido do registro local' };
246
+ if (inst.type === 'npm') {
247
+ const npmBin = which('npm');
248
+ if (npmBin) {
249
+ const r = await runCmd(npmBin, ['uninstall', '-g', inst.pkg]);
250
+ result = r.ok ? { ok: true, message: 'Desinstalado via npm' } : { ok: false, message: 'npm falhou (removido do registro)' };
251
+ }
252
+ } else if (inst.type === 'pip') {
253
+ const pipBin = which('pip') || which('pip3');
254
+ if (pipBin) {
255
+ const r = await runCmd(pipBin, ['uninstall', '-y', inst.pkg]);
256
+ result = r.ok ? { ok: true, message: 'Desinstalado via pip' } : { ok: false, message: 'pip falhou (removido do registro)' };
257
+ }
258
+ } else if (inst.type === 'ps-skill') {
259
+ try {
260
+ const f = path.join(CONFIG_DIR, 'skills', `${inst.pkg}.md`);
261
+ if (fs.existsSync(f)) fs.unlinkSync(f);
262
+ result = { ok: true, message: 'Skill removida' };
263
+ } catch (e) { result = { ok: false, message: e.message }; }
264
+ }
265
+ // remove do registro
266
+ const list = loadInstalledPlugins().filter(p => p.id !== plugin.id);
267
+ saveInstalledPlugins(list);
268
+ return result;
269
+ }
270
+
271
+ // ─── Browser Use integration ───────────────────────────────────────────────
272
+ async function browserUseStatus() {
273
+ const py = which('python3') || which('python');
274
+ const pip = which('pip') || which('pip3');
275
+ if (!py) {
276
+ return { installed: false, ready: false, message: 'Python não encontrado. Instale em https://python.org' };
277
+ }
278
+ // checa se browser-use está instalado
279
+ const r = spawnSync(py, ['-c', 'import browser_use; print(browser_use.__version__)'], { encoding: 'utf8' });
280
+ if (r.status === 0) {
281
+ return {
282
+ installed: true,
283
+ ready: true,
284
+ version: r.stdout.trim(),
285
+ python: py,
286
+ message: `browser-use ${r.stdout.trim()} pronto`,
287
+ };
288
+ }
289
+ return {
290
+ installed: false,
291
+ ready: !!pip,
292
+ message: 'browser-use não instalado. Clique em "Instalar Browser Use".',
293
+ python: py,
294
+ };
295
+ }
296
+
297
+ async function browserUseInstall() {
298
+ const pip = which('pip') || which('pip3');
299
+ if (!pip) {
300
+ return { ok: false, message: 'pip não encontrado. Instale Python primeiro.' };
301
+ }
302
+ const r = await runCmd(pip, ['install', '--user', 'browser-use', 'playwright']);
303
+ if (!r.ok) {
304
+ return { ok: false, message: `pip falhou: ${(r.stderr || '').slice(0, 300)}` };
305
+ }
306
+ // instala browsers do playwright
307
+ const py = which('python3') || which('python');
308
+ if (py) {
309
+ await runCmd(py, ['-m', 'playwright', 'install', 'chromium']);
310
+ }
311
+ return { ok: true, message: 'browser-use instalado. Agora você pode rodar tarefas de browser.' };
312
+ }
313
+
314
+ async function browserUseRun(task, url, cfg) {
315
+ const status = await browserUseStatus();
316
+ if (!status.ready) {
317
+ return { ok: false, message: status.message };
318
+ }
319
+ const py = status.python;
320
+ // script python embutido que usa browser-use
321
+ const script = `
322
+ import asyncio
323
+ import sys
324
+ try:
325
+ from browser_use import Agent
326
+ from langchain_openai import ChatOpenAI
327
+ except ImportError as e:
328
+ print("ERR_DEP:" + str(e))
329
+ sys.exit(2)
330
+
331
+ async def main():
332
+ llm = ChatOpenAI(model="${cfg.model || 'gpt-4o'}", api_key="${cfg.keys?.openai || ''}")
333
+ agent = Agent(task="""${(task || '').replace(/"/g, '\\"').replace(/\n/g, ' ')}""", llm=llm${url ? `, initial_url="${url}"` : ''})
334
+ result = await agent.run()
335
+ print("RESULT:")
336
+ print(result)
337
+
338
+ asyncio.run(main())
339
+ `.trim();
340
+ const tmpFile = path.join(os.tmpdir(), `ps-claw-bu-${Date.now()}.py`);
341
+ try {
342
+ fs.writeFileSync(tmpFile, script);
343
+ } catch (e) {
344
+ return { ok: false, message: `Falha ao criar script: ${e.message}` };
345
+ }
346
+ const r = await runCmd(py, [tmpFile], { timeout: 120000 });
347
+ try { fs.unlinkSync(tmpFile); } catch {}
348
+ if (!r.ok) {
349
+ return { ok: false, message: `Erro: ${(r.stderr || r.error || '').slice(0, 500)}` };
350
+ }
351
+ const out = r.stdout || '';
352
+ const idx = out.indexOf('RESULT:');
353
+ return { ok: true, message: idx >= 0 ? out.slice(idx + 7).trim() : out };
354
+ }
355
+
356
+ // ─── Carrega config para o endpoint de browser-use ─────────────────────────
357
+ function loadWebCfg() {
358
+ try {
359
+ if (!fs.existsSync(CONFIG_FILE)) return { keys: {}, model: null };
360
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8") || "{}");
361
+ } catch { return { keys: {}, model: null }; }
362
+ }
363
+
364
+ // ─── Proxy genérico para APIs externas ─────────────────────────────────────
19
365
  function proxyRequest(targetUrl, method, headers, body, res) {
20
366
  const url = new URL(targetUrl);
21
367
  const isHttps = url.protocol === "https:";
@@ -46,34 +392,115 @@ function proxyRequest(targetUrl, method, headers, body, res) {
46
392
  req.end();
47
393
  }
48
394
 
49
- const server = http.createServer((req, res) => {
395
+ function sendJson(res, code, data) {
396
+ res.writeHead(code, { "Content-Type": "application/json; charset=utf-8", "access-control-allow-origin": "*" });
397
+ res.end(JSON.stringify(data));
398
+ }
399
+
400
+ function readBody(req) {
401
+ return new Promise((resolve) => {
402
+ let b = '';
403
+ req.on('data', c => b += c);
404
+ req.on('end', () => {
405
+ try { resolve(JSON.parse(b || '{}')); }
406
+ catch { resolve({}); }
407
+ });
408
+ });
409
+ }
410
+
411
+ // ─── Server ────────────────────────────────────────────────────────────────
412
+ const server = http.createServer(async (req, res) => {
50
413
  res.setHeader("Access-Control-Allow-Origin", "*");
51
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
414
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
52
415
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, anthropic-version");
53
416
 
54
417
  if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
55
418
 
56
- // Proxy para APIs externas: /proxy?url=https://api.anthropic.com/...
57
- if (req.url.startsWith("/proxy")) {
58
- const params = new URL(req.url, `http://localhost`).searchParams;
59
- const target = params.get("url");
60
- if (!target) { res.writeHead(400); res.end("Missing url param"); return; }
419
+ const url = new URL(req.url, `http://localhost`);
420
+ const pathname = url.pathname;
61
421
 
422
+ // ── /proxy?url=... ──
423
+ if (pathname === "/proxy") {
424
+ const target = url.searchParams.get("url");
425
+ if (!target) { res.writeHead(400); res.end("Missing url param"); return; }
62
426
  let body = "";
63
427
  req.on("data", c => body += c);
64
428
  req.on("end", () => proxyRequest(target, req.method, req.headers, body, res));
65
429
  return;
66
430
  }
67
431
 
68
- // Servir arquivos estáticos
69
- const filePath = req.url === "/" ? "/index.html" : req.url;
432
+ // ── /api/store (GET catalog) ──
433
+ if (pathname === "/api/store" && req.method === "GET") {
434
+ return sendJson(res, 200, getStoreCatalog());
435
+ }
436
+
437
+ // ── /api/store/installed (GET) ──
438
+ if (pathname === "/api/store/installed" && req.method === "GET") {
439
+ return sendJson(res, 200, { plugins: loadInstalledPlugins() });
440
+ }
441
+
442
+ // ── /api/store/install (POST) ──
443
+ if (pathname === "/api/store/install" && req.method === "POST") {
444
+ const body = await readBody(req);
445
+ const plugin = CLAW_HUB_CATALOG.find(p => p.id === body.id);
446
+ if (!plugin) return sendJson(res, 404, { ok: false, message: 'Plugin não encontrado' });
447
+ const result = await installPlugin(plugin);
448
+ return sendJson(res, result.ok ? 200 : 500, result);
449
+ }
450
+
451
+ // ── /api/store/uninstall (POST) ──
452
+ if (pathname === "/api/store/uninstall" && req.method === "POST") {
453
+ const body = await readBody(req);
454
+ const plugin = CLAW_HUB_CATALOG.find(p => p.id === body.id);
455
+ if (!plugin) return sendJson(res, 404, { ok: false, message: 'Plugin não encontrado' });
456
+ const result = await uninstallPlugin(plugin);
457
+ return sendJson(res, 200, result);
458
+ }
459
+
460
+ // ── /api/browser-use/status (GET) ──
461
+ if (pathname === "/api/browser-use/status" && req.method === "GET") {
462
+ const s = await browserUseStatus();
463
+ return sendJson(res, 200, s);
464
+ }
465
+
466
+ // ── /api/browser-use/install (POST) ──
467
+ if (pathname === "/api/browser-use/install" && req.method === "POST") {
468
+ const r = await browserUseInstall();
469
+ return sendJson(res, r.ok ? 200 : 500, r);
470
+ }
471
+
472
+ // ── /api/browser-use/run (POST) ──
473
+ if (pathname === "/api/browser-use/run" && req.method === "POST") {
474
+ const body = await readBody(req);
475
+ if (!body.task) return sendJson(res, 400, { ok: false, message: 'task é obrigatória' });
476
+ const cfg = loadWebCfg();
477
+ if (!cfg.keys?.openai) {
478
+ return sendJson(res, 400, { ok: false, message: 'Configure sua API key da OpenAI (necessária para browser-use) na aba API Keys ou rode npx ps-claw quickstart.' });
479
+ }
480
+ const r = await browserUseRun(body.task, body.url, cfg);
481
+ return sendJson(res, r.ok ? 200 : 500, r);
482
+ }
483
+
484
+ // ── /api/quickstart (POST: salva config do dashboard) ──
485
+ if (pathname === "/api/config" && req.method === "GET") {
486
+ return sendJson(res, 200, loadWebCfg());
487
+ }
488
+
489
+ // ── Arquivos estáticos ──
490
+ const filePath = pathname === "/" ? "/index.html" : pathname;
70
491
  const fullPath = path.join(__dirname, "public", filePath);
492
+ if (!fullPath.startsWith(path.join(__dirname, "public"))) {
493
+ res.writeHead(403); res.end("Forbidden"); return;
494
+ }
71
495
  const ext = path.extname(fullPath);
72
496
  const mime = {
73
497
  ".html": "text/html; charset=utf-8",
74
498
  ".js": "application/javascript",
75
499
  ".css": "text/css",
76
500
  ".json": "application/json",
501
+ ".svg": "image/svg+xml",
502
+ ".png": "image/png",
503
+ ".ico": "image/x-icon",
77
504
  };
78
505
 
79
506
  try {
@@ -91,5 +518,7 @@ server.listen(WEB_PORT, () => {
91
518
  console.log(" ────────────────────────────────");
92
519
  console.log(` ✅ Rodando em: http://localhost:${WEB_PORT}`);
93
520
  console.log(" 📖 Abra o navegador e configure sua API key");
521
+ console.log(" 🛒 Loja Claw Hub disponível na aba 'Loja'");
522
+ console.log(" 🌐 Integração Browser Use disponível na aba 'Browser'");
94
523
  console.log("");
95
524
  });