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/README.md +86 -17
- package/cli.mjs +484 -18
- package/package.json +1 -1
- package/web-ui/public/index.html +495 -1
- package/web-ui/server.mjs +441 -12
package/web-ui/server.mjs
CHANGED
|
@@ -2,20 +2,366 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* PS Claw — Servidor Web
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
//
|
|
69
|
-
|
|
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
|
});
|