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/README.md
CHANGED
|
@@ -20,12 +20,45 @@ npx ps-claw@latest web
|
|
|
20
20
|
|
|
21
21
|
Pronto! A interface web abre em **http://localhost:3000** 🎉
|
|
22
22
|
|
|
23
|
+
No **primeiro uso** (web ou terminal), o PS Claw abre o **Quickstart guiado** — você escolhe o provedor (Google Gemini é grátis), cola a API key, testa, escolhe o modelo e está pronto para conversar.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 💻 Conversar pelo terminal
|
|
28
|
+
|
|
29
|
+
O PS Claw agora tem **chat no terminal** — não precisa mais só da interface web!
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Quickstart guiado (primeiro uso, ou para reconfigurar)
|
|
33
|
+
npx ps-claw quickstart
|
|
34
|
+
|
|
35
|
+
# Conversar direto no terminal
|
|
36
|
+
npx ps-claw chat
|
|
37
|
+
|
|
38
|
+
# Ver modelos disponíveis
|
|
39
|
+
npx ps-claw models
|
|
40
|
+
|
|
41
|
+
# Ver configuração atual
|
|
42
|
+
npx ps-claw config
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Comandos dentro do `chat`
|
|
46
|
+
|
|
47
|
+
| Comando | Ação |
|
|
48
|
+
|---------|------|
|
|
49
|
+
| `/ajuda` | Lista os comandos |
|
|
50
|
+
| `/sair` | Encerra o chat |
|
|
51
|
+
| `/limpar` | Limpa o histórico |
|
|
52
|
+
| `/modelo` | Lista os modelos |
|
|
53
|
+
|
|
54
|
+
Config salva em `~/.ps-claw/config.json`.
|
|
55
|
+
|
|
23
56
|
---
|
|
24
57
|
|
|
25
58
|
## 🌐 Interface Web
|
|
26
59
|
|
|
27
60
|
### O que é?
|
|
28
|
-
Interface visual estilo ChatGPT para conversar com o agente de IA. Salva histórico no navegador, permite trocar modelos, configurar gateways
|
|
61
|
+
Interface visual estilo ChatGPT para conversar com o agente de IA. Salva histórico no navegador, permite trocar modelos, configurar gateways, instalar plugins da **Loja Claw Hub** e rodar tarefas com **Browser Use**.
|
|
29
62
|
|
|
30
63
|
### Como usar?
|
|
31
64
|
|
|
@@ -39,24 +72,60 @@ npx ps-claw all
|
|
|
39
72
|
|
|
40
73
|
Acesse: **http://localhost:3000**
|
|
41
74
|
|
|
75
|
+
### Abas da interface
|
|
76
|
+
|
|
77
|
+
| Aba | Função |
|
|
78
|
+
|-----|--------|
|
|
79
|
+
| 💬 **Chat** | Conversa com a IA, histórico persistente |
|
|
80
|
+
| 🔑 **API Keys** | Configure suas chaves (Anthropic, OpenAI, Google, Mistral) |
|
|
81
|
+
| 🤖 **Modelos** | Selecione o modelo padrão |
|
|
82
|
+
| 🛒 **Loja** | Catálogo do Claw Hub — instale MCPs, plugins e skills com 1 clique |
|
|
83
|
+
| 🌐 **Browser** | Integração Browser Use — IA controla o navegador |
|
|
84
|
+
| 🔌 **Gateways** | Conecte a gateways externos (opcional) |
|
|
85
|
+
| ⚙️ **Config** | Ajustes de perfil, system prompt, temperatura, etc. |
|
|
86
|
+
|
|
42
87
|
### Configuração na interface
|
|
43
88
|
|
|
44
|
-
1.
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
89
|
+
1. O **Quickstart** aparece automaticamente no primeiro uso — basta seguir os passos.
|
|
90
|
+
2. Ou configure manualmente:
|
|
91
|
+
- Aba **🔑 API Keys** → cole sua chave
|
|
92
|
+
- Aba **🤖 Modelos** → clique no modelo desejado
|
|
93
|
+
- Aba **💬 Chat** → comece a conversar!
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 🛒 Loja Claw Hub
|
|
98
|
+
|
|
99
|
+
A aba **🛒 Loja** é uma loja estilo Play Store com **plugins, MCPs e skills** curados do ecossistema Claw Hub. Com 1 clique você instala:
|
|
100
|
+
|
|
101
|
+
- **🌐 Automação** — Browser Use, Playwright MCP, Puppeteer MCP
|
|
102
|
+
- **🧠 Memória** — Memory Server, SQLite, Chroma
|
|
103
|
+
- **📁 Documentos** — Filesystem, GitHub, GitLab, Notion
|
|
104
|
+
- **🔎 Busca** — Brave Search, Fetch, Tavily
|
|
105
|
+
- **💼 Produtividade** — Google Drive, Slack, Gmail
|
|
106
|
+
- **🐳 DevOps** — Docker, PostgreSQL
|
|
107
|
+
- **🤖 IA** — Ollama Local, OpenRouter, LM Studio
|
|
108
|
+
- **🎙️ Mídia** — Whisper, YouTube
|
|
109
|
+
- **💻 Skills** — Coder Pro, Writer, Data Analyst, Translator
|
|
110
|
+
|
|
111
|
+
Cada plugin tem **ícone, descrição, rating, número de downloads** e botão **⬇️ Instalar**. A instalação roda `npm install -g`, `pip install --user` ou cria uma skill local — tudo automático.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 🌐 Browser Use
|
|
116
|
+
|
|
117
|
+
A aba **🌐 Browser** integra a biblioteca [browser-use](https://github.com/browser-use/browser-use) — a IA controla um navegador real para executar tarefas:
|
|
118
|
+
|
|
119
|
+
1. Clique em **⬇️ Instalar Browser Use** (instala `browser-use` + `playwright` via pip)
|
|
120
|
+
2. Digite a **URL inicial** (opcional) e a **tarefa em linguagem natural**
|
|
121
|
+
3. Clique em **▶️ Executar Tarefa**
|
|
122
|
+
|
|
123
|
+
**Exemplos de tarefas:**
|
|
124
|
+
- "Liste as 5 notícias mais populares do Hacker News"
|
|
125
|
+
- "Pesquise por 'PS Claw AI agent' e liste os 3 primeiros resultados"
|
|
126
|
+
- "Vá em GitHub Trending e liste os 5 repositórios do dia"
|
|
127
|
+
|
|
128
|
+
> Requer API key da OpenAI configurada (browser-use usa LangChain por padrão).
|
|
60
129
|
|
|
61
130
|
---
|
|
62
131
|
|
package/cli.mjs
CHANGED
|
@@ -2,11 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* PS Claw CLI — standalone, sem dependência do dist/
|
|
5
|
+
*
|
|
6
|
+
* Comandos:
|
|
7
|
+
* npx ps-claw chat Conversa com a IA direto no terminal
|
|
8
|
+
* npx ps-claw quickstart Configuração guiada (primeiro uso)
|
|
9
|
+
* npx ps-claw web Abre a interface web em http://localhost:3000
|
|
10
|
+
* npx ps-claw start Inicia o agente (requer dist/ ou cai pro web)
|
|
11
|
+
* npx ps-claw all Inicia tudo
|
|
12
|
+
* npx ps-claw update Atualiza o PS Claw
|
|
13
|
+
* npx ps-claw help Esta mensagem
|
|
5
14
|
*/
|
|
6
15
|
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
8
|
-
import { existsSync } from "node:fs";
|
|
16
|
+
import { spawn, execSync, spawnSync } from "node:child_process";
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs";
|
|
9
18
|
import path from "node:path";
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import readline from "node:readline";
|
|
21
|
+
import https from "node:https";
|
|
22
|
+
import http from "node:http";
|
|
10
23
|
import { fileURLToPath } from "node:url";
|
|
11
24
|
|
|
12
25
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -21,8 +34,182 @@ const C = {
|
|
|
21
34
|
red: "\x1b[31m",
|
|
22
35
|
bold: "\x1b[1m",
|
|
23
36
|
dim: "\x1b[2m",
|
|
37
|
+
magenta:"\x1b[35m",
|
|
38
|
+
blue: "\x1b[34m",
|
|
24
39
|
};
|
|
25
40
|
|
|
41
|
+
// ─── Config ────────────────────────────────────────────────────────────────
|
|
42
|
+
const CONFIG_DIR = path.join(os.homedir(), ".ps-claw");
|
|
43
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
44
|
+
|
|
45
|
+
const DEFAULT_CFG = {
|
|
46
|
+
version: 1,
|
|
47
|
+
onboarded: false,
|
|
48
|
+
model: null,
|
|
49
|
+
keys: { anthropic:"", openai:"", google:"", mistral:"" },
|
|
50
|
+
cfg: { name:"Você", agent:"PS Claw", sys:"", temp:"0.7", tok:"4096" },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function loadCfg() {
|
|
54
|
+
try {
|
|
55
|
+
if (!existsSync(CONFIG_FILE)) return { ...DEFAULT_CFG };
|
|
56
|
+
const raw = readFileSync(CONFIG_FILE, "utf8");
|
|
57
|
+
const parsed = JSON.parse(raw);
|
|
58
|
+
return { ...DEFAULT_CFG, ...parsed, keys: {...DEFAULT_CFG.keys, ...(parsed.keys||{})}, cfg: {...DEFAULT_CFG.cfg, ...(parsed.cfg||{})} };
|
|
59
|
+
} catch {
|
|
60
|
+
return { ...DEFAULT_CFG };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function saveCfg(cfg) {
|
|
65
|
+
try {
|
|
66
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
67
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
|
|
68
|
+
return true;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error(`${C.red}❌ Não foi possível salvar ${CONFIG_FILE}: ${e.message}${C.reset}`);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isFirstUse(cfg) {
|
|
76
|
+
return !cfg.onboarded && !cfg.model && !Object.values(cfg.keys).some(Boolean);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Catálogo de modelos (espelha o da web-ui) ────────────────────────────
|
|
80
|
+
const MODELS = [
|
|
81
|
+
{ id:'claude-sonnet-4-5', name:'Claude Sonnet 4.5', p:'anthropic', desc:'Rápido e inteligente · uso geral' },
|
|
82
|
+
{ id:'claude-haiku-4-5', name:'Claude Haiku 4.5', p:'anthropic', desc:'Ultra rápido · econômico' },
|
|
83
|
+
{ id:'claude-opus-4-5', name:'Claude Opus 4.5', p:'anthropic', desc:'Mais poderoso · raciocínio complexo' },
|
|
84
|
+
{ id:'gpt-4o', name:'GPT-4o', p:'openai', desc:'Flagship multimodal da OpenAI' },
|
|
85
|
+
{ id:'gpt-4o-mini', name:'GPT-4o mini', p:'openai', desc:'Rápido e barato · uso geral' },
|
|
86
|
+
{ id:'o3-mini', name:'o3-mini', p:'openai', desc:'Raciocínio avançado · lógica' },
|
|
87
|
+
{ id:'gemini-2.5-flash', name:'Gemini 2.5 Flash', p:'google', desc:'Rápido · contexto longo · gratuito' },
|
|
88
|
+
{ id:'gemini-1.5-flash', name:'Gemini 1.5 Flash', p:'google', desc:'Estável e confiável · gratuito' },
|
|
89
|
+
{ id:'mistral-large-latest', name:'Mistral Large', p:'mistral', desc:'Poderoso · multilingual' },
|
|
90
|
+
{ id:'mistral-small-latest', name:'Mistral Small', p:'mistral', desc:'Rápido · barato' },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
// ─── HTTP helper ───────────────────────────────────────────────────────────
|
|
94
|
+
function httpReq(url, { method='GET', headers={}, body=null, timeout=60000 } = {}) {
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
try {
|
|
97
|
+
const u = new URL(url);
|
|
98
|
+
const lib = u.protocol === 'https:' ? https : http;
|
|
99
|
+
const opts = {
|
|
100
|
+
hostname: u.hostname,
|
|
101
|
+
port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
|
102
|
+
path: u.pathname + u.search,
|
|
103
|
+
method,
|
|
104
|
+
headers: { ...headers, host: u.hostname },
|
|
105
|
+
};
|
|
106
|
+
const req = lib.request(opts, (res) => {
|
|
107
|
+
let data = '';
|
|
108
|
+
res.on('data', c => data += c);
|
|
109
|
+
res.on('end', () => resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, body: data }));
|
|
110
|
+
});
|
|
111
|
+
req.on('error', reject);
|
|
112
|
+
req.setTimeout(timeout, () => { req.destroy(new Error('timeout')); });
|
|
113
|
+
if (body) req.write(body);
|
|
114
|
+
req.end();
|
|
115
|
+
} catch (e) { reject(e); }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function httpJson(url, opts) {
|
|
120
|
+
const r = await httpReq(url, opts);
|
|
121
|
+
try { return { ...r, json: JSON.parse(r.body) }; }
|
|
122
|
+
catch { return { ...r, json: null }; }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── Providers ─────────────────────────────────────────────────────────────
|
|
126
|
+
async function callAnthropic(model, messages, sys, key, maxTok, temp) {
|
|
127
|
+
const msgs = messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.content}));
|
|
128
|
+
const r = await httpJson('https://api.anthropic.com/v1/messages', {
|
|
129
|
+
method:'POST',
|
|
130
|
+
headers:{'Content-Type':'application/json','x-api-key':key,'anthropic-version':'2023-06-01'},
|
|
131
|
+
body: JSON.stringify({ model: model.id, max_tokens: maxTok, temperature: temp, system: sys, messages: msgs }),
|
|
132
|
+
});
|
|
133
|
+
if (!r.ok) throw new Error(r.json?.error?.message || `HTTP ${r.status}`);
|
|
134
|
+
return r.json?.content?.[0]?.text || JSON.stringify(r.json);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function callOpenAI(model, messages, sys, key, maxTok, temp, overrideUrl) {
|
|
138
|
+
const url = overrideUrl || 'https://api.openai.com/v1/chat/completions';
|
|
139
|
+
const msgs = [{role:'system',content:sys}, ...messages];
|
|
140
|
+
const r = await httpJson(url, {
|
|
141
|
+
method:'POST',
|
|
142
|
+
headers:{'Content-Type':'application/json','Authorization':'Bearer '+key},
|
|
143
|
+
body: JSON.stringify({ model: model.id, max_tokens: maxTok, temperature: temp, messages: msgs }),
|
|
144
|
+
});
|
|
145
|
+
if (!r.ok) throw new Error(r.json?.error?.message || `HTTP ${r.status}`);
|
|
146
|
+
return r.json?.choices?.[0]?.message?.content || JSON.stringify(r.json);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function callGoogle(model, messages, sys, key, maxTok, temp) {
|
|
150
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model.id}:generateContent?key=${key}`;
|
|
151
|
+
const contents = messages.map(m => ({
|
|
152
|
+
role: m.role === 'assistant' ? 'model' : 'user',
|
|
153
|
+
parts: [{ text: m.content }],
|
|
154
|
+
}));
|
|
155
|
+
const r = await httpJson(url, {
|
|
156
|
+
method:'POST',
|
|
157
|
+
headers:{'Content-Type':'application/json'},
|
|
158
|
+
body: JSON.stringify({
|
|
159
|
+
contents,
|
|
160
|
+
systemInstruction: { parts: [{ text: sys }] },
|
|
161
|
+
generationConfig: { maxOutputTokens: maxTok, temperature: temp },
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
if (!r.ok) throw new Error(r.json?.error?.message || `HTTP ${r.status}`);
|
|
165
|
+
return r.json?.candidates?.[0]?.content?.parts?.[0]?.text || JSON.stringify(r.json);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function callLLM(cfg) {
|
|
169
|
+
const model = MODELS.find(m => m.id === cfg.model);
|
|
170
|
+
if (!model) throw new Error('Nenhum modelo selecionado. Rode `npx ps-claw quickstart`.');
|
|
171
|
+
const key = cfg.keys[model.p];
|
|
172
|
+
if (!key && model.p !== 'local') {
|
|
173
|
+
throw new Error(`API key para ${model.p} não configurada. Rode \`npx ps-claw quickstart\`.`);
|
|
174
|
+
}
|
|
175
|
+
const sys = cfg.cfg.sys || 'Você é a PS Claw, uma assistente de IA inteligente e prestativa. Seja direta, útil e proativa.';
|
|
176
|
+
const temp = parseFloat(cfg.cfg.temp) || 0.7;
|
|
177
|
+
const maxTok = parseInt(cfg.cfg.tok) || 4096;
|
|
178
|
+
|
|
179
|
+
const messages = cfg._history || [];
|
|
180
|
+
switch (model.p) {
|
|
181
|
+
case 'anthropic': return await callAnthropic(model, messages, sys, key, maxTok, temp);
|
|
182
|
+
case 'openai': return await callOpenAI(model, messages, sys, key, maxTok, temp);
|
|
183
|
+
case 'google': return await callGoogle(model, messages, sys, key, maxTok, temp);
|
|
184
|
+
case 'mistral': return await callOpenAI(model, messages, sys, key, maxTok, temp, 'https://api.mistral.ai/v1/chat/completions');
|
|
185
|
+
default: throw new Error('Provedor desconhecido: ' + model.p);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Test API key (validação rápida) ────────────────────────────────────────
|
|
190
|
+
async function testKey(provider, key) {
|
|
191
|
+
try {
|
|
192
|
+
if (provider === 'anthropic') {
|
|
193
|
+
const r = await httpJson('https://api.anthropic.com/v1/models', { headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' } });
|
|
194
|
+
return r.ok;
|
|
195
|
+
}
|
|
196
|
+
if (provider === 'openai') {
|
|
197
|
+
const r = await httpJson('https://api.openai.com/v1/models', { headers: { Authorization: 'Bearer ' + key } });
|
|
198
|
+
return r.ok;
|
|
199
|
+
}
|
|
200
|
+
if (provider === 'google') {
|
|
201
|
+
const r = await httpJson(`https://generativelanguage.googleapis.com/v1beta/models?key=${key}`);
|
|
202
|
+
return r.ok;
|
|
203
|
+
}
|
|
204
|
+
if (provider === 'mistral') {
|
|
205
|
+
const r = await httpJson('https://api.mistral.ai/v1/models', { headers: { Authorization: 'Bearer ' + key } });
|
|
206
|
+
return r.ok;
|
|
207
|
+
}
|
|
208
|
+
} catch { return false; }
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── UI helpers ────────────────────────────────────────────────────────────
|
|
26
213
|
function banner() {
|
|
27
214
|
console.log(`
|
|
28
215
|
${C.cyan}${C.bold} ██████╗ ███████╗ ██████╗██╗ █████╗ ██╗ ██╗${C.reset}
|
|
@@ -31,7 +218,7 @@ ${C.cyan}${C.bold} ██████╔╝███████╗ ██
|
|
|
31
218
|
${C.cyan}${C.bold} ██╔═══╝ ╚════██║ ██║ ██║ ██╔══██║██║███╗██║${C.reset}
|
|
32
219
|
${C.cyan}${C.bold} ██║ ███████║ ╚██████╗███████╗██║ ██║╚███╔███╔╝${C.reset}
|
|
33
220
|
${C.cyan}${C.bold} ╚═╝ ╚══════╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝${C.reset}
|
|
34
|
-
${C.dim}
|
|
221
|
+
${C.dim}Lightweight AI Agent Gateway${C.reset}
|
|
35
222
|
`);
|
|
36
223
|
}
|
|
37
224
|
|
|
@@ -39,16 +226,276 @@ function help() {
|
|
|
39
226
|
banner();
|
|
40
227
|
console.log(` ${C.bold}Comandos:${C.reset}
|
|
41
228
|
|
|
42
|
-
${C.green}npx ps-claw
|
|
43
|
-
${C.green}npx ps-claw
|
|
44
|
-
${C.green}npx ps-claw
|
|
45
|
-
${C.green}npx ps-claw
|
|
46
|
-
${C.green}npx ps-claw
|
|
229
|
+
${C.green}npx ps-claw chat${C.reset} Conversa com a IA direto no terminal
|
|
230
|
+
${C.green}npx ps-claw quickstart${C.reset} Configuração guiada no primeiro uso
|
|
231
|
+
${C.green}npx ps-claw web${C.reset} Abre a interface web em http://localhost:3000
|
|
232
|
+
${C.green}npx ps-claw start${C.reset} Inicia o agente PS Claw (requer dist/)
|
|
233
|
+
${C.green}npx ps-claw all${C.reset} Inicia tudo junto
|
|
234
|
+
${C.green}npx ps-claw update${C.reset} Atualiza o PS Claw
|
|
235
|
+
${C.green}npx ps-claw models${C.reset} Lista modelos disponíveis
|
|
236
|
+
${C.green}npx ps-claw config${C.reset} Mostra configuração atual
|
|
237
|
+
${C.green}npx ps-claw help${C.reset} Esta mensagem
|
|
47
238
|
|
|
48
239
|
${C.bold}Interface web:${C.reset} http://localhost:3000
|
|
240
|
+
${C.dim}Config em:${C.reset} ${CONFIG_FILE}
|
|
241
|
+
`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function ask(rl, q) { return new Promise(r => rl.question(q, a => r(a.trim()))) }
|
|
245
|
+
function askHidden(rl, q) {
|
|
246
|
+
// fallback simples sem esconde-senha nativa — apenas ecoa como pontos
|
|
247
|
+
return new Promise(r => rl.question(q, a => r(a.trim())))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ─── Quickstart (onboarding no primeiro uso) ───────────────────────────────
|
|
251
|
+
async function quickstart() {
|
|
252
|
+
banner();
|
|
253
|
+
const cfg = loadCfg();
|
|
254
|
+
|
|
255
|
+
console.log(`${C.bold}${C.magenta}🚀 Quickstart — PS Claw${C.reset}
|
|
256
|
+
Vamos configurar tudo em menos de 1 minuto. Você só precisa de ${C.bold}uma${C.reset} API key.
|
|
257
|
+
|
|
258
|
+
${C.dim}Recomendado:${C.reset} ${C.green}Google Gemini${C.reset} (grátis para sempre)
|
|
259
|
+
${C.dim}Alternativas:${C.reset} Anthropic Claude · OpenAI GPT-4o · Mistral
|
|
260
|
+
|
|
261
|
+
Obtenha sua chave gratuita em:
|
|
262
|
+
${C.cyan}https://aistudio.google.com/apikey${C.reset} (Google Gemini — recomendado)
|
|
263
|
+
${C.cyan}https://console.anthropic.com/api-keys${C.reset} (Anthropic Claude)
|
|
264
|
+
${C.cyan}https://platform.openai.com/api-keys${C.reset} (OpenAI GPT-4o)
|
|
265
|
+
${C.cyan}https://console.mistral.ai/api-keys${C.reset} (Mistral)
|
|
49
266
|
`);
|
|
267
|
+
|
|
268
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
269
|
+
|
|
270
|
+
// 1) escolher provedor
|
|
271
|
+
console.log(`\n${C.bold}1️⃣ Escolha o provedor padrão:${C.reset}`);
|
|
272
|
+
console.log(` ${C.green}1${C.reset}) ${C.blue}🔵 Google Gemini${C.reset} — grátis para sempre (recomendado)`);
|
|
273
|
+
console.log(` ${C.green}2${C.reset}) ${C.yellow}🟠 Anthropic Claude${C.reset} — US$5 crédito inicial`);
|
|
274
|
+
console.log(` ${C.green}3${C.reset}) ${C.green}🟢 OpenAI GPT-4o${C.reset} — US$5 crédito inicial`);
|
|
275
|
+
console.log(` ${C.green}4${C.reset}) ${C.magenta}🟣 Mistral${C.reset} — trial de crédito`);
|
|
276
|
+
const provChoice = (await ask(rl, '\nOpção [1-4] (padrão 1): ')) || '1';
|
|
277
|
+
const provMap = { '1':'google', '2':'anthropic', '3':'openai', '4':'mistral' };
|
|
278
|
+
const provName = { '1':'Google Gemini', '2':'Anthropic Claude', '3':'OpenAI GPT-4o', '4':'Mistral' };
|
|
279
|
+
const provider = provMap[provChoice] || 'google';
|
|
280
|
+
if (!provMap[provChoice]) {
|
|
281
|
+
console.log(`${C.yellow}⚠️ Opção inválida, usando Google Gemini.${C.reset}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 2) colar API key
|
|
285
|
+
console.log(`\n${C.bold}2️⃣ Cole sua API key do ${provName[provChoice] || 'Google'}:${C.reset}`);
|
|
286
|
+
console.log(`${C.dim} (vou testar antes de salvar — não pode ter espaços)${C.reset}`);
|
|
287
|
+
let key = '';
|
|
288
|
+
while (!key) {
|
|
289
|
+
key = await askHidden(rl, ` API Key${provider==='google'?' (AIzaSy...)':provider==='anthropic'?' (sk-ant-...)':' (sk-...)'}: `);
|
|
290
|
+
if (!key) { console.log(`${C.red} ⚠️ Vazio. Tente de novo.${C.reset}`); continue; }
|
|
291
|
+
if (key.length < 10) { console.log(`${C.red} ⚠️ Chave parece curta demais.${C.reset}`); key=''; continue; }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 3) testar key
|
|
295
|
+
process.stdout.write(`\n${C.dim} Testando chave...${C.reset}`);
|
|
296
|
+
const ok = await testKey(provider, key);
|
|
297
|
+
if (!ok) {
|
|
298
|
+
console.log(`\r${C.red} ❌ Chave inválida ou API indisponível.${C.reset}`);
|
|
299
|
+
const cont = await ask(rl, ' Salvar mesmo assim? [s/N]: ');
|
|
300
|
+
if (cont.toLowerCase() !== 's') {
|
|
301
|
+
console.log(`${C.yellow}Tente novamente com: npx ps-claw quickstart${C.reset}`);
|
|
302
|
+
rl.close();
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
console.log(`\r${C.green} ✅ Chave válida!${C.reset} `);
|
|
307
|
+
}
|
|
308
|
+
cfg.keys[provider] = key;
|
|
309
|
+
|
|
310
|
+
// 4) escolher modelo padrão
|
|
311
|
+
console.log(`\n${C.bold}3️⃣ Escolha o modelo padrão:${C.reset}`);
|
|
312
|
+
const provModels = MODELS.filter(m => m.p === provider);
|
|
313
|
+
provModels.forEach((m, i) => {
|
|
314
|
+
console.log(` ${C.green}${i+1}${C.reset}) ${C.bold}${m.name}${C.reset} — ${C.dim}${m.desc}${C.reset}`);
|
|
315
|
+
});
|
|
316
|
+
const modelChoice = (await ask(rl, `\nOpção [1-${provModels.length}] (padrão 1): `)) || '1';
|
|
317
|
+
const mi = parseInt(modelChoice, 10) - 1;
|
|
318
|
+
cfg.model = provModels[mi] ? provModels[mi].id : provModels[0].id;
|
|
319
|
+
console.log(`${C.green}✅ Modelo selecionado: ${provModels.find(m=>m.id===cfg.model).name}${C.reset}`);
|
|
320
|
+
|
|
321
|
+
// 5) perfil
|
|
322
|
+
console.log(`\n${C.bold}4️⃣ Perfil (opcional — Enter para pular):${C.reset}`);
|
|
323
|
+
const name = await ask(rl, ` Seu nome [${cfg.cfg.name}]: `);
|
|
324
|
+
if (name) cfg.cfg.name = name;
|
|
325
|
+
const agent = await ask(rl, ` Nome do agente [${cfg.cfg.agent}]: `);
|
|
326
|
+
if (agent) cfg.cfg.agent = agent;
|
|
327
|
+
|
|
328
|
+
cfg.onboarded = true;
|
|
329
|
+
saveCfg(cfg);
|
|
330
|
+
|
|
331
|
+
console.log(`\n${C.green}${C.bold}🎉 Pronto! Configuração salva em:${C.reset}`);
|
|
332
|
+
console.log(` ${C.dim}${CONFIG_FILE}${C.reset}`);
|
|
333
|
+
console.log(`\n${C.bold}Agora você pode:${C.reset}`);
|
|
334
|
+
console.log(` ${C.cyan}npx ps-claw chat${C.reset} — conversar no terminal`);
|
|
335
|
+
console.log(` ${C.cyan}npx ps-claw web${C.reset} — abrir interface web`);
|
|
336
|
+
console.log(` ${C.cyan}npx ps-claw models${C.reset} — ver todos os modelos`);
|
|
337
|
+
|
|
338
|
+
const startNow = (await ask(rl, '\nIniciar chat agora? [S/n]: ')).toLowerCase();
|
|
339
|
+
rl.close();
|
|
340
|
+
if (startNow !== 'n') {
|
|
341
|
+
console.log();
|
|
342
|
+
startChat();
|
|
343
|
+
} else {
|
|
344
|
+
process.exit(0);
|
|
345
|
+
}
|
|
50
346
|
}
|
|
51
347
|
|
|
348
|
+
// ─── Listar modelos ────────────────────────────────────────────────────────
|
|
349
|
+
function listModels() {
|
|
350
|
+
const cfg = loadCfg();
|
|
351
|
+
console.log(`${C.bold}Modelos disponíveis${C.reset} ${C.dim}(config em ${CONFIG_FILE})${C.reset}\n`);
|
|
352
|
+
const byProv = {};
|
|
353
|
+
for (const m of MODELS) (byProv[m.p] = byProv[m.p] || []).push(m);
|
|
354
|
+
const labels = { anthropic:'🟠 Anthropic', openai:'🟢 OpenAI', google:'🔵 Google', mistral:'🟣 Mistral', local:'⚫ Local' };
|
|
355
|
+
for (const [p, list] of Object.entries(byProv)) {
|
|
356
|
+
console.log(` ${C.bold}${labels[p] || p}${C.reset} ${C.dim}(${cfg.keys[p] ? '✓ key configurada' : 'sem key'})${C.reset}`);
|
|
357
|
+
for (const m of list) {
|
|
358
|
+
const isSel = m.id === cfg.model;
|
|
359
|
+
console.log(` ${isSel ? C.green + '▸' : ' '} ${m.name} ${C.dim}— ${m.desc}${C.reset}${isSel ? ` ${C.green}(ativo)${C.reset}` : ''}`);
|
|
360
|
+
}
|
|
361
|
+
console.log();
|
|
362
|
+
}
|
|
363
|
+
console.log(`${C.dim}Troque com: npx ps-claw quickstart${C.reset}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ─── Mostrar config ────────────────────────────────────────────────────────
|
|
367
|
+
function showConfig() {
|
|
368
|
+
const cfg = loadCfg();
|
|
369
|
+
console.log(`${C.bold}Config atual${C.reset} ${C.dim}(${CONFIG_FILE})${C.reset}\n`);
|
|
370
|
+
console.log(` Onboarded : ${cfg.onboarded ? C.green+'sim'+C.reset : C.yellow+'não'+C.reset}`);
|
|
371
|
+
console.log(` Modelo : ${cfg.model || C.red+'(nenhum)'+C.reset}`);
|
|
372
|
+
console.log(` Nome : ${cfg.cfg.name}`);
|
|
373
|
+
console.log(` Agente : ${cfg.cfg.agent}`);
|
|
374
|
+
console.log(` Temp : ${cfg.cfg.temp} · Max tokens: ${cfg.cfg.tok}`);
|
|
375
|
+
console.log(` Keys:`);
|
|
376
|
+
for (const [p, k] of Object.entries(cfg.keys)) {
|
|
377
|
+
const status = k ? `${C.green}✓ ${k.slice(0,4)}...${k.slice(-3)}${C.reset}` : `${C.dim}vazio${C.reset}`;
|
|
378
|
+
console.log(` ${p.padEnd(10)}: ${status}`);
|
|
379
|
+
}
|
|
380
|
+
if (!cfg.onboarded) {
|
|
381
|
+
console.log(`\n${C.yellow}Rode: npx ps-claw quickstart${C.reset}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ─── Chat no terminal ──────────────────────────────────────────────────────
|
|
386
|
+
async function startChat() {
|
|
387
|
+
let cfg = loadCfg();
|
|
388
|
+
|
|
389
|
+
// 1) garantir que tem config
|
|
390
|
+
if (isFirstUse(cfg)) {
|
|
391
|
+
console.log(`${C.yellow}🚀 Primeiro uso detectado. Iniciando quickstart...${C.reset}\n`);
|
|
392
|
+
return quickstart();
|
|
393
|
+
}
|
|
394
|
+
if (!cfg.model) {
|
|
395
|
+
console.log(`${C.yellow}⚠️ Nenhum modelo selecionado. Rode: npx ps-claw quickstart${C.reset}`);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
const model = MODELS.find(m => m.id === cfg.model);
|
|
399
|
+
if (!model) {
|
|
400
|
+
console.log(`${C.red}❌ Modelo "${cfg.model}" não reconhecido. Rode: npx ps-claw quickstart${C.reset}`);
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
if (!cfg.keys[model.p] && model.p !== 'local') {
|
|
404
|
+
console.log(`${C.yellow}⚠️ Sem API key para ${model.p}. Rode: npx ps-claw quickstart${C.reset}`);
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
banner();
|
|
409
|
+
console.log(` ${C.bold}Chat iniciado${C.reset} · ${C.cyan}${model.name}${C.reset} · ${C.dim}(${model.p})${C.reset}`);
|
|
410
|
+
console.log(` ${C.dim}Comandos: /sair (exit), /limpar (clear), /modelo (lista), /ajuda (help)${C.reset}\n`);
|
|
411
|
+
|
|
412
|
+
const history = [];
|
|
413
|
+
const sys = cfg.cfg.sys || 'Você é a PS Claw, uma assistente de IA inteligente e preativa. Responda em português, seja direta e útil.';
|
|
414
|
+
const temp = parseFloat(cfg.cfg.temp) || 0.7;
|
|
415
|
+
const maxTok = parseInt(cfg.cfg.tok) || 4096;
|
|
416
|
+
|
|
417
|
+
const rl = readline.createInterface({
|
|
418
|
+
input: process.stdin,
|
|
419
|
+
output: process.stdout,
|
|
420
|
+
prompt: `${C.green}${C.bold}você${C.reset}${C.dim}> ${C.reset}`,
|
|
421
|
+
historySize: 100,
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
rl.prompt();
|
|
425
|
+
|
|
426
|
+
rl.on('line', async (line) => {
|
|
427
|
+
const text = line.trim();
|
|
428
|
+
if (!text) { rl.prompt(); return; }
|
|
429
|
+
|
|
430
|
+
// comandos
|
|
431
|
+
if (text === '/sair' || text === '/exit' || text === '/quit') {
|
|
432
|
+
console.log(`${C.dim}Até mais! 🦞${C.reset}`);
|
|
433
|
+
process.exit(0);
|
|
434
|
+
}
|
|
435
|
+
if (text === '/limpar' || text === '/clear') {
|
|
436
|
+
history.length = 0;
|
|
437
|
+
console.clear();
|
|
438
|
+
rl.prompt();
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (text === '/ajuda' || text === '/help') {
|
|
442
|
+
console.log(` ${C.bold}Comandos:${C.reset}
|
|
443
|
+
/sair Sair do chat
|
|
444
|
+
/limpar Limpar histórico
|
|
445
|
+
/modelo Listar modelos
|
|
446
|
+
/ajuda Esta ajuda
|
|
447
|
+
${C.dim}Digite sua mensagem e Enter para conversar.${C.reset}`);
|
|
448
|
+
rl.prompt();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (text === '/modelo' || text === '/model') {
|
|
452
|
+
listModels();
|
|
453
|
+
rl.prompt();
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (text.startsWith('/')) {
|
|
457
|
+
console.log(`${C.red}Comando desconhecido: ${text}${C.reset} (tente /ajuda)`);
|
|
458
|
+
rl.prompt();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// mensagem normal
|
|
463
|
+
history.push({ role: 'user', content: text });
|
|
464
|
+
|
|
465
|
+
process.stdout.write(`\n${C.magenta}${C.bold}🦞 ps-claw${C.reset}${C.dim}> ${C.reset}`);
|
|
466
|
+
process.stdout.write(`${C.dim}(pensando...)${C.reset}`);
|
|
467
|
+
|
|
468
|
+
const t0 = Date.now();
|
|
469
|
+
try {
|
|
470
|
+
// construímos um cfg temporário com histórico embutido
|
|
471
|
+
const tmp = { ...cfg, _history: [...history] };
|
|
472
|
+
const reply = await callLLM(tmp);
|
|
473
|
+
// apaga "pensando..."
|
|
474
|
+
process.stdout.write('\r\x1b[K');
|
|
475
|
+
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
|
476
|
+
console.log(`${C.magenta}${C.bold}🦞 ps-claw${C.reset}${C.dim}> ${C.reset}${reply}`);
|
|
477
|
+
console.log(`${C.dim} ── ${elapsed}s · ${model.name}${C.reset}\n`);
|
|
478
|
+
history.push({ role: 'assistant', content: reply });
|
|
479
|
+
} catch (e) {
|
|
480
|
+
process.stdout.write('\r\x1b[K');
|
|
481
|
+
console.log(`${C.red}❌ Erro: ${e.message}${C.reset}`);
|
|
482
|
+
// remove a msg do usuário do histórico pois falhou
|
|
483
|
+
history.pop();
|
|
484
|
+
console.log(`${C.dim}Dica: rode 'npx ps-claw quickstart' para reconfigurar.${C.reset}\n`);
|
|
485
|
+
}
|
|
486
|
+
rl.prompt();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
rl.on('close', () => {
|
|
490
|
+
console.log(`\n${C.dim}Até mais! 🦞${C.reset}`);
|
|
491
|
+
process.exit(0);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// mensagem inicial de boas-vindas
|
|
495
|
+
console.log(`${C.dim}Olá ${cfg.cfg.name}! Sou ${cfg.cfg.agent}. Como posso ajudar hoje?${C.reset}\n`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ─── Web server ────────────────────────────────────────────────────────────
|
|
52
499
|
function startWeb() {
|
|
53
500
|
const srv = path.join(__dirname, "web-ui", "server.mjs");
|
|
54
501
|
if (!existsSync(srv)) {
|
|
@@ -61,15 +508,15 @@ function startWeb() {
|
|
|
61
508
|
proc.on("exit", code => process.exit(code ?? 0));
|
|
62
509
|
}
|
|
63
510
|
|
|
511
|
+
// ─── Agent (requer dist/) ──────────────────────────────────────────────────
|
|
64
512
|
function startAgent() {
|
|
65
|
-
// Usa o ps-claw.mjs original do OpenClaw se existir e tiver dist/
|
|
66
|
-
// Caso contrário, avisa o usuário e abre a web
|
|
67
513
|
const distEntry = path.join(__dirname, "dist", "entry.mjs");
|
|
68
514
|
const distEntryJs = path.join(__dirname, "dist", "entry.js");
|
|
69
515
|
|
|
70
516
|
if (!existsSync(distEntry) && !existsSync(distEntryJs)) {
|
|
71
|
-
console.log(`${C.yellow}⚠️ O agente requer
|
|
72
|
-
console.log(`${C.dim} Para usar a interface web, execute: npx ps-claw web${C.reset}
|
|
517
|
+
console.log(`${C.yellow}⚠️ O agente completo requer dist/ (build do TypeScript).${C.reset}`);
|
|
518
|
+
console.log(`${C.dim} Para usar a interface web, execute: npx ps-claw web${C.reset}`);
|
|
519
|
+
console.log(`${C.dim} Para conversar no terminal, execute: npx ps-claw chat${C.reset}\n`);
|
|
73
520
|
console.log(`${C.green}🌐 Iniciando Interface Web automaticamente...${C.reset}`);
|
|
74
521
|
console.log(`${C.cyan} Acesse: http://localhost:3000${C.reset}\n`);
|
|
75
522
|
startWeb();
|
|
@@ -95,10 +542,29 @@ function update() {
|
|
|
95
542
|
});
|
|
96
543
|
}
|
|
97
544
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
545
|
+
// ─── Dispatch ──────────────────────────────────────────────────────────────
|
|
546
|
+
async function main() {
|
|
547
|
+
switch (cmd) {
|
|
548
|
+
case "chat": await startChat(); break;
|
|
549
|
+
case "quickstart": await quickstart(); break;
|
|
550
|
+
case "start": startAgent(); break;
|
|
551
|
+
case "web": startWeb(); break;
|
|
552
|
+
case "all": startAll(); break;
|
|
553
|
+
case "update": update(); break;
|
|
554
|
+
case "models": listModels(); break;
|
|
555
|
+
case "config": showConfig(); break;
|
|
556
|
+
case "version":
|
|
557
|
+
case "-v":
|
|
558
|
+
case "--version":
|
|
559
|
+
console.log('PS Claw 1.2.0'); break;
|
|
560
|
+
case "help":
|
|
561
|
+
case "-h":
|
|
562
|
+
case "--help":
|
|
563
|
+
default: help(); break;
|
|
564
|
+
}
|
|
104
565
|
}
|
|
566
|
+
|
|
567
|
+
main().catch(e => {
|
|
568
|
+
console.error(`${C.red}❌ Erro: ${e.message}${C.reset}`);
|
|
569
|
+
process.exit(1);
|
|
570
|
+
});
|