ps-claw 2026.5.30

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.
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PS Claw — Interface Web
5
+ * Servidor leve que serve a UI e faz proxy para o gateway do PS Claw
6
+ * Suporta multiplos gateways, API de modelos e configuracoes
7
+ */
8
+
9
+ import http from "node:http";
10
+ import https from "node:https";
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+
17
+ const WEB_PORT = process.env.PS_CLAW_WEB_PORT || 3000;
18
+ const GATEWAY_PORT = process.env.PS_CLAW_GATEWAY_PORT || 18789;
19
+ const GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN || "";
20
+
21
+ function serveFile(res, filePath, contentType) {
22
+ try {
23
+ const content = fs.readFileSync(filePath);
24
+ res.writeHead(200, { "Content-Type": contentType });
25
+ res.end(content);
26
+ } catch {
27
+ res.writeHead(404);
28
+ res.end("Not found");
29
+ }
30
+ }
31
+
32
+ function proxyToGateway(req, res, bodyData, targetHost, targetPort, targetPath, authToken) {
33
+ const options = {
34
+ hostname: targetHost,
35
+ port: targetPort,
36
+ path: targetPath,
37
+ method: req.method,
38
+ headers: {
39
+ ...req.headers,
40
+ host: `${targetHost}:${targetPort}`,
41
+ ...(authToken && { authorization: `Bearer ${authToken}` }),
42
+ },
43
+ };
44
+
45
+ const proxyReq = http.request(options, (proxyRes) => {
46
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
47
+ proxyRes.pipe(res);
48
+ });
49
+
50
+ proxyReq.on("error", () => {
51
+ res.writeHead(502, { "Content-Type": "application/json" });
52
+ res.end(JSON.stringify({ error: "Gateway do PS Claw nao esta rodando. Execute: ps-claw gateway run" }));
53
+ });
54
+
55
+ if (bodyData) proxyReq.write(bodyData);
56
+ proxyReq.end();
57
+ }
58
+
59
+ function parseUrl(urlStr) {
60
+ try {
61
+ const parsed = new URL(urlStr);
62
+ return {
63
+ protocol: parsed.protocol,
64
+ hostname: parsed.hostname,
65
+ port: parsed.port || (parsed.protocol === "https:" ? "443" : "80"),
66
+ path: parsed.pathname + parsed.search,
67
+ };
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ const server = http.createServer((req, res) => {
74
+ // CORS
75
+ res.setHeader("Access-Control-Allow-Origin", "*");
76
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
77
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
78
+ if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
79
+
80
+ // Proxy para o gateway local (padrao)
81
+ if (req.url.startsWith("/gateway")) {
82
+ let body = "";
83
+ req.on("data", chunk => body += chunk);
84
+ req.on("end", () => proxyToGateway(
85
+ req, res, body || null,
86
+ "127.0.0.1", GATEWAY_PORT,
87
+ req.url.replace("/gateway", ""),
88
+ GATEWAY_TOKEN
89
+ ));
90
+ return;
91
+ }
92
+
93
+ // API: Health check do gateway local
94
+ if (req.url === "/api/health" && req.method === "GET") {
95
+ const healthReq = http.request({
96
+ hostname: "127.0.0.1",
97
+ port: GATEWAY_PORT,
98
+ path: "/health",
99
+ method: "GET",
100
+ ...(GATEWAY_TOKEN && { headers: { authorization: `Bearer ${GATEWAY_TOKEN}` } }),
101
+ }, (healthRes) => {
102
+ let data = "";
103
+ healthRes.on("data", chunk => data += chunk);
104
+ healthRes.on("end", () => {
105
+ res.writeHead(healthRes.statusCode, { "Content-Type": "application/json" });
106
+ res.end(data);
107
+ });
108
+ });
109
+ healthReq.on("error", () => {
110
+ res.writeHead(502, { "Content-Type": "application/json" });
111
+ res.end(JSON.stringify({ status: "offline", error: "Gateway offline" }));
112
+ });
113
+ healthReq.end();
114
+ return;
115
+ }
116
+
117
+ // API: Listar modelos disponiveis
118
+ if (req.url === "/api/models" && req.method === "GET") {
119
+ const models = [
120
+ { id: "gpt-4o", name: "GPT-4o", provider: "openai" },
121
+ { id: "gpt-4o-mini", name: "GPT-4o Mini", provider: "openai" },
122
+ { id: "gpt-4-turbo", name: "GPT-4 Turbo", provider: "openai" },
123
+ { id: "claude-opus-4-5", name: "Claude Opus 4.5", provider: "anthropic" },
124
+ { id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", provider: "anthropic" },
125
+ { id: "claude-3-5-sonnet", name: "Claude 3.5 Sonnet", provider: "anthropic" },
126
+ { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", provider: "google" },
127
+ { id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", provider: "google" },
128
+ { id: "deepseek-chat", name: "DeepSeek Chat", provider: "deepseek" },
129
+ ];
130
+ res.writeHead(200, { "Content-Type": "application/json" });
131
+ res.end(JSON.stringify({ models }));
132
+ return;
133
+ }
134
+
135
+ // API: Proxy para gateway remoto
136
+ if (req.url.startsWith("/api/proxy") && req.method === "POST") {
137
+ let body = "";
138
+ req.on("data", chunk => body += chunk);
139
+ req.on("end", () => {
140
+ try {
141
+ const { url: targetUrl, path: targetPath, token, method, body: reqBody } = JSON.parse(body);
142
+ const parsed = parseUrl(targetUrl);
143
+ if (!parsed) {
144
+ res.writeHead(400, { "Content-Type": "application/json" });
145
+ res.end(JSON.stringify({ error: "URL invalida" }));
146
+ return;
147
+ }
148
+ proxyToGateway(
149
+ { method: method || "GET", headers: {} },
150
+ res, reqBody ? JSON.stringify(reqBody) : null,
151
+ parsed.hostname, parsed.port,
152
+ targetPath || parsed.path,
153
+ token
154
+ );
155
+ } catch {
156
+ res.writeHead(400, { "Content-Type": "application/json" });
157
+ res.end(JSON.stringify({ error: "Requisicao invalida" }));
158
+ }
159
+ });
160
+ return;
161
+ }
162
+
163
+ // Servir arquivos estaticos
164
+ const url = req.url === "/" ? "/index.html" : req.url;
165
+ const filePath = path.join(__dirname, "public", url);
166
+ const ext = path.extname(filePath);
167
+ const mimeTypes = {
168
+ ".html": "text/html; charset=utf-8",
169
+ ".js": "application/javascript",
170
+ ".mjs": "application/javascript",
171
+ ".css": "text/css",
172
+ ".png": "image/png",
173
+ ".svg": "image/svg+xml",
174
+ ".ico": "image/x-icon",
175
+ ".json": "application/json",
176
+ ".woff": "font/woff",
177
+ ".woff2": "font/woff2",
178
+ };
179
+
180
+ serveFile(res, filePath, mimeTypes[ext] || "text/plain");
181
+ });
182
+
183
+ server.listen(WEB_PORT, () => {
184
+ console.log("");
185
+ console.log(" 🦞 PS Claw — Interface Web");
186
+ console.log(" ─────────────────────────────");
187
+ console.log(` ✅ Rodando em: http://localhost:${WEB_PORT}`);
188
+ console.log(` 🔗 Gateway: http://localhost:${GATEWAY_PORT}`);
189
+ console.log("");
190
+ console.log(" Abra o navegador em: http://localhost:" + WEB_PORT);
191
+ console.log("");
192
+ });