liberty-code-cli 1.0.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/bin/liberty.js +219 -0
- package/package.json +17 -0
package/bin/liberty.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* Liberty Code — CLI do LibertyChat.
|
|
5
|
+
*
|
|
6
|
+
* Instalação: npm install -g liberty-code-cli
|
|
7
|
+
* Login: liberty login <token> (gere o token em Configurações → Liberty Code)
|
|
8
|
+
* Uso: cd seu-projeto && liberty
|
|
9
|
+
*
|
|
10
|
+
* Arquitetura: o modelo roda no servidor do LibertyChat; as ferramentas
|
|
11
|
+
* (ler/escrever arquivo, listar diretório, rodar comando) executam AQUI,
|
|
12
|
+
* localmente, no processo deste CLI — o servidor só decide QUAL ferramenta
|
|
13
|
+
* usar e com quais argumentos. Nada do seu código é enviado além do que você
|
|
14
|
+
* explicitamente perguntar sobre (conteúdo de arquivos que o agente pede pra
|
|
15
|
+
* ler, e a saída dos comandos que você confirmar rodar).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const readline = require('readline');
|
|
22
|
+
const { execSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
// ── Configuração ────────────────────────────────────────────────────────────
|
|
25
|
+
const CONFIG_DIR = path.join(os.homedir(), '.libertycode');
|
|
26
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
27
|
+
const SERVER_URL = process.env.LIBERTY_SERVER_URL || 'https://libertychat.onrender.com';
|
|
28
|
+
const MAX_FILE_READ_BYTES = 200 * 1024; // 200KB — protege contexto do modelo contra arquivos enormes
|
|
29
|
+
const MAX_TOOL_ITERATIONS = 12; // evita loop infinito se o modelo insistir em ferramentas
|
|
30
|
+
|
|
31
|
+
// ── Cores ANSI (sem dependências externas, de propósito) ───────────────────
|
|
32
|
+
const c = {
|
|
33
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
34
|
+
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
|
|
35
|
+
blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', gray: '\x1b[90m',
|
|
36
|
+
};
|
|
37
|
+
const paint = (color, text) => `${c[color]}${text}${c.reset}`;
|
|
38
|
+
|
|
39
|
+
function loadConfig() {
|
|
40
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return null; }
|
|
41
|
+
}
|
|
42
|
+
function saveConfig(cfg) {
|
|
43
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
44
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── HTTP helper ──────────────────────────────────────────────────────────────
|
|
48
|
+
async function apiChat(messages, token) {
|
|
49
|
+
const res = await fetch(`${SERVER_URL}/api/cli/chat`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
|
52
|
+
body: JSON.stringify({ messages, cwd: process.cwd() }),
|
|
53
|
+
});
|
|
54
|
+
const data = await res.json().catch(() => ({}));
|
|
55
|
+
if (!res.ok) throw new Error(data.error || `Erro do servidor (${res.status})`);
|
|
56
|
+
return data;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Ferramentas locais (executam de verdade no disco do usuário) ───────────
|
|
60
|
+
function safeResolve(relPath) {
|
|
61
|
+
// Nunca deixa o modelo escapar do diretório do projeto via ../../
|
|
62
|
+
const resolved = path.resolve(process.cwd(), relPath || '.');
|
|
63
|
+
if (!resolved.startsWith(process.cwd())) {
|
|
64
|
+
throw new Error('Caminho fora do diretório do projeto não é permitido.');
|
|
65
|
+
}
|
|
66
|
+
return resolved;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toolListDirectory(args) {
|
|
70
|
+
const dir = safeResolve(args.path || '.');
|
|
71
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
72
|
+
.filter(e => !['node_modules', '.git', '.next', 'dist', 'build'].includes(e.name))
|
|
73
|
+
.map(e => (e.isDirectory() ? `${e.name}/` : e.name));
|
|
74
|
+
return entries.join('\n') || '(diretório vazio)';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toolReadFile(args) {
|
|
78
|
+
const filePath = safeResolve(args.path);
|
|
79
|
+
const stat = fs.statSync(filePath);
|
|
80
|
+
if (stat.size > MAX_FILE_READ_BYTES) {
|
|
81
|
+
return `[Arquivo muito grande: ${(stat.size / 1024).toFixed(0)}KB — limite de ${MAX_FILE_READ_BYTES / 1024}KB. Peça uma parte específica.]`;
|
|
82
|
+
}
|
|
83
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toolWriteFile(args, rl) {
|
|
87
|
+
const filePath = safeResolve(args.path);
|
|
88
|
+
const exists = fs.existsSync(filePath);
|
|
89
|
+
console.log(paint('yellow', `\n${exists ? '✏️ Sobrescrever' : '📄 Criar'} arquivo: ${path.relative(process.cwd(), filePath)}`));
|
|
90
|
+
console.log(paint('gray', ` (${(args.content || '').length} caracteres)`));
|
|
91
|
+
return askConfirmation(rl, ' Permitir?').then(ok => {
|
|
92
|
+
if (!ok) return '[Usuário negou a escrita deste arquivo.]';
|
|
93
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
94
|
+
fs.writeFileSync(filePath, args.content, 'utf8');
|
|
95
|
+
return `Arquivo salvo com sucesso: ${path.relative(process.cwd(), filePath)}`;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toolRunCommand(args, rl) {
|
|
100
|
+
console.log(paint('yellow', `\n💻 Executar comando: `) + paint('bold', args.command));
|
|
101
|
+
return askConfirmation(rl, ' Permitir?').then(ok => {
|
|
102
|
+
if (!ok) return '[Usuário negou a execução deste comando.]';
|
|
103
|
+
try {
|
|
104
|
+
const output = execSync(args.command, { cwd: process.cwd(), encoding: 'utf8', timeout: 60000, maxBuffer: 5 * 1024 * 1024 });
|
|
105
|
+
return output || '(comando executado sem output)';
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return `Erro (exit ${e.status ?? '?'}):\n${e.stdout || ''}${e.stderr || e.message}`;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function askConfirmation(rl, label) {
|
|
113
|
+
return new Promise(resolve => {
|
|
114
|
+
rl.question(`${label} ${paint('gray', '[y/N] ')}`, answer => {
|
|
115
|
+
resolve(['y', 'yes', 's', 'sim'].includes(answer.trim().toLowerCase()));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function executeLocalTool(call, rl) {
|
|
121
|
+
try {
|
|
122
|
+
switch (call.name) {
|
|
123
|
+
case 'list_directory': return toolListDirectory(call.arguments);
|
|
124
|
+
case 'read_file': return toolReadFile(call.arguments);
|
|
125
|
+
case 'write_file': return await toolWriteFile(call.arguments, rl);
|
|
126
|
+
case 'run_command': return await toolRunCommand(call.arguments, rl);
|
|
127
|
+
default: return `Ferramenta desconhecida: ${call.name}`;
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
return `Erro ao executar ${call.name}: ${e.message}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Comandos de CLI ─────────────────────────────────────────────────────────
|
|
135
|
+
async function cmdLogin(token) {
|
|
136
|
+
if (!token) {
|
|
137
|
+
console.log(paint('red', 'Uso: liberty login <token>'));
|
|
138
|
+
console.log(paint('gray', 'Gere um token em Configurações → Liberty Code no LibertyChat.'));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
saveConfig({ token, serverUrl: SERVER_URL });
|
|
142
|
+
console.log(paint('green', '✅ Login salvo com sucesso.'));
|
|
143
|
+
console.log(paint('gray', `Token guardado em ${CONFIG_FILE}`));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function cmdLogout() {
|
|
147
|
+
try { fs.unlinkSync(CONFIG_FILE); console.log(paint('green', '✅ Logout feito.')); }
|
|
148
|
+
catch { console.log(paint('gray', 'Nenhuma sessão ativa.')); }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function cmdChat() {
|
|
152
|
+
const config = loadConfig();
|
|
153
|
+
if (!config?.token) {
|
|
154
|
+
console.log(paint('red', 'Você ainda não está logado.'));
|
|
155
|
+
console.log(paint('gray', 'Rode: liberty login <token> (gere o token em Configurações → Liberty Code)'));
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(paint('bold', '\n🦅 Liberty Code') + paint('gray', ` — ${process.cwd()}`));
|
|
160
|
+
console.log(paint('gray', 'Digite seu pedido. Ctrl+C para sair.\n'));
|
|
161
|
+
|
|
162
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
163
|
+
const messages = [];
|
|
164
|
+
|
|
165
|
+
const promptUser = () => rl.question(paint('cyan', '› '), async (input) => {
|
|
166
|
+
if (!input.trim()) return promptUser();
|
|
167
|
+
messages.push({ role: 'user', content: input });
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
let iterations = 0;
|
|
171
|
+
while (iterations++ < MAX_TOOL_ITERATIONS) {
|
|
172
|
+
process.stdout.write(paint('gray', '⏳ pensando...\r'));
|
|
173
|
+
const response = await apiChat(messages, config.token);
|
|
174
|
+
process.stdout.write(' \r'); // limpa a linha "pensando..."
|
|
175
|
+
|
|
176
|
+
if (response.type === 'tool_calls') {
|
|
177
|
+
messages.push({
|
|
178
|
+
role: 'assistant', content: response.content || null,
|
|
179
|
+
tool_calls: response.toolCalls.map(tc => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.arguments) } })),
|
|
180
|
+
});
|
|
181
|
+
for (const call of response.toolCalls) {
|
|
182
|
+
const result = await executeLocalTool(call, rl);
|
|
183
|
+
console.log(paint('gray', ` → ${String(result).slice(0, 200)}${String(result).length > 200 ? '...' : ''}`));
|
|
184
|
+
messages.push({ role: 'tool', tool_call_id: call.id, content: String(result).slice(0, 8000) });
|
|
185
|
+
}
|
|
186
|
+
continue; // manda o resultado de volta e deixa o modelo decidir o próximo passo
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log('\n' + response.content + '\n');
|
|
190
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
} catch (e) {
|
|
194
|
+
console.log(paint('red', `\n❌ ${e.message}\n`));
|
|
195
|
+
}
|
|
196
|
+
promptUser();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
rl.on('close', () => { console.log(paint('gray', '\nAté mais!')); process.exit(0); });
|
|
200
|
+
promptUser();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Entry point ──────────────────────────────────────────────────────────────
|
|
204
|
+
(async () => {
|
|
205
|
+
const [, , cmd, arg] = process.argv;
|
|
206
|
+
if (cmd === 'login') await cmdLogin(arg);
|
|
207
|
+
else if (cmd === 'logout') cmdLogout();
|
|
208
|
+
else if (cmd === '--version' || cmd === '-v') console.log('liberty-code 1.0.0');
|
|
209
|
+
else if (cmd === '--help' || cmd === '-h') {
|
|
210
|
+
console.log(`Liberty Code — agente do LibertyChat no terminal
|
|
211
|
+
|
|
212
|
+
Uso:
|
|
213
|
+
liberty login <token> Autentica com um token gerado em Configurações → Liberty Code
|
|
214
|
+
liberty logout Remove a sessão salva
|
|
215
|
+
liberty Inicia o chat no diretório atual (é o seu projeto)
|
|
216
|
+
`);
|
|
217
|
+
}
|
|
218
|
+
else await cmdChat();
|
|
219
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "liberty-code-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Liberty Code — o agente do LibertyChat operando no terminal, sobre projetos de código reais.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"liberty": "bin/liberty.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "commonjs",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18.0.0"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"keywords": ["libertychat", "cli", "ai", "agent", "terminal"]
|
|
17
|
+
}
|