ps-claw 1.1.1 → 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 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. para a aba **🔌 Gateways**
45
- - Clique **+ Adicionar**
46
- - URL: `http://localhost:18789` (PS Claw local) ou qualquer API OpenAI-compatível
47
- - Nome: "Meu Gateway"
48
- - Clique **Adicionar**
49
-
50
- 2. Vá para a aba **🤖 Modelos & Provedores**
51
- - Adicione suas **chaves de API**:
52
- - **Claude** (Anthropic): `sk-ant-...`
53
- - **GPT-4** (OpenAI): `sk-...`
54
- - **Gemini** (Google): `AIza...`
55
- - **Mistral**: sua chave
56
- - Selecione um modelo
57
- - Clique no modelo para usá-lo
58
-
59
- 3. Volte para **💬 Chat** e comece a conversar! 💬
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}v1.0.8 — Lightweight AI Agent Gateway${C.reset}
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 web${C.reset} Abre a interface web em http://localhost:3000
43
- ${C.green}npx ps-claw start${C.reset} Inicia o agente PS Claw
44
- ${C.green}npx ps-claw all${C.reset} Inicia tudo junto
45
- ${C.green}npx ps-claw update${C.reset} Atualiza o PS Claw
46
- ${C.green}npx ps-claw help${C.reset} Esta mensagem
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 configuração adicional (dist/).${C.reset}`);
72
- console.log(`${C.dim} Para usar a interface web, execute: npx ps-claw web${C.reset}\n`);
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
- switch (cmd) {
99
- case "start": startAgent(); break;
100
- case "web": startWeb(); break;
101
- case "all": startAll(); break;
102
- case "update": update(); break;
103
- default: help(); break;
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
+ });