liberty-code-cli 1.0.0 → 2.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/README.md +212 -0
- package/bin/liberty.js +101 -171
- package/package.json +21 -4
- package/src/api.js +45 -0
- package/src/configStore.js +14 -0
- package/src/constants.js +77 -0
- package/src/engine.js +64 -0
- package/src/hooks.js +105 -0
- package/src/ignoreEngine.js +46 -0
- package/src/mcp.js +139 -0
- package/src/memory.js +89 -0
- package/src/permissions.js +77 -0
- package/src/repl.js +285 -0
- package/src/session.js +82 -0
- package/src/skills.js +94 -0
- package/src/subagents.js +70 -0
- package/src/tools/exec-tools.js +73 -0
- package/src/tools/fs-tools.js +242 -0
- package/src/tools/index.js +192 -0
- package/src/tools/todo-tool.js +38 -0
- package/src/ui.js +71 -0
package/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Liberty Code
|
|
2
|
+
|
|
3
|
+
Agente de código do LibertyChat, operando no terminal sobre um projeto real
|
|
4
|
+
(sistema de arquivos de verdade, não uma simulação). v2.0.0.
|
|
5
|
+
|
|
6
|
+
## Instalação
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install -g liberty-code-cli
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Login
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
liberty login <token>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Gere o token em **Configurações → Liberty Code** dentro do LibertyChat.
|
|
19
|
+
|
|
20
|
+
## Uso
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cd seu-projeto
|
|
24
|
+
liberty
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Flags:
|
|
28
|
+
|
|
29
|
+
| Flag | Efeito |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `-c`, `--continue` | Retoma a última sessão salva deste projeto |
|
|
32
|
+
| `--resume [id]` | Retoma uma sessão específica; sem id, mostra a lista pra escolher |
|
|
33
|
+
| `--permission-mode <modo>` | Inicia em `default` \| `acceptEdits` \| `bypassPermissions` \| `plan` |
|
|
34
|
+
| `--dangerously-skip-permissions` | Atalho pra `bypassPermissions` (ainda pede confirmação explícita) |
|
|
35
|
+
|
|
36
|
+
## Ferramentas do agente
|
|
37
|
+
|
|
38
|
+
| Ferramenta | O que faz |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `list_directory` | Lista arquivos/pastas (respeita `.gitignore`), com `recursive`/`max_depth` |
|
|
41
|
+
| `read_file` | Lê um arquivo; aceita `offset`/`limit` pra arquivos grandes |
|
|
42
|
+
| `write_file` | Cria ou sobrescreve um arquivo inteiro |
|
|
43
|
+
| `edit_file` | Troca um trecho exato (`old_string` → `new_string`) — preferido para editar arquivo existente |
|
|
44
|
+
| `glob_files` | Encontra arquivos por padrão (`**/*.test.js`) |
|
|
45
|
+
| `grep_search` | Busca por regex dentro do conteúdo dos arquivos |
|
|
46
|
+
| `run_command` | Roda um comando de shell; `run_in_background:true` pra processos longos (log em arquivo) |
|
|
47
|
+
| `update_todos` | Checklist de progresso pra tarefas com vários passos |
|
|
48
|
+
| `task` | Dispara um subagente com contexto isolado pra uma sub-tarefa |
|
|
49
|
+
| `mcp__<servidor>__<ferramenta>` | Ferramentas de servidores MCP configurados (se houver) |
|
|
50
|
+
|
|
51
|
+
Toda escrita de arquivo e todo comando passam pela confirmação do usuário
|
|
52
|
+
(exceto nos modos que auto-aprovam — ver abaixo).
|
|
53
|
+
|
|
54
|
+
## Modos de permissão
|
|
55
|
+
|
|
56
|
+
Trocáveis a qualquer momento com `/mode <nome>`:
|
|
57
|
+
|
|
58
|
+
- **default** — pergunta antes de editar arquivo ou rodar comando (padrão).
|
|
59
|
+
- **acceptEdits** — edita arquivos sem perguntar; comandos ainda pedem confirmação.
|
|
60
|
+
- **bypassPermissions** — aprova tudo sem perguntar. Pede confirmação explícita ao ligar (`/mode bypassPermissions` ou flag `--dangerously-skip-permissions`).
|
|
61
|
+
- **plan** — somente leitura. As ferramentas de escrita **não são nem oferecidas** ao modelo nesse modo — não é uma instrução pra não usar, é ausência real da ferramenta. Ao final da resposta, o CLI pergunta se você quer sair do modo e executar o que foi combinado.
|
|
62
|
+
|
|
63
|
+
Respondendo `a` (always) numa confirmação, aquela ação específica (aquele
|
|
64
|
+
arquivo, ou aquele comando exato) passa a ser auto-aprovada **pelo resto
|
|
65
|
+
desta sessão** — não persiste pra próxima vez que você abrir o `liberty`.
|
|
66
|
+
|
|
67
|
+
## Memória de projeto (LIBERTY.md)
|
|
68
|
+
|
|
69
|
+
Carregada automaticamente no início de cada sessão, em duas camadas:
|
|
70
|
+
|
|
71
|
+
1. **Global** — `~/.libertycode/LIBERTY.md`: preferências suas, valem em
|
|
72
|
+
qualquer projeto (ex: "sempre responda em português").
|
|
73
|
+
2. **Projeto** — `LIBERTY.md` na raiz do projeto: convenções do
|
|
74
|
+
repositório, pensado pra ir pro git e ser compartilhado pelo time.
|
|
75
|
+
|
|
76
|
+
Dentro de qualquer um dos dois, uma linha `@caminho/arquivo.md` importa o
|
|
77
|
+
conteúdo daquele arquivo (1 nível de profundidade).
|
|
78
|
+
|
|
79
|
+
- `/init` — pede pro agente explorar o projeto e escrever/atualizar o `LIBERTY.md` sozinho.
|
|
80
|
+
- `#texto` (digitado direto no chat) — anota `texto` no `LIBERTY.md` do projeto sem gastar uma chamada ao modelo.
|
|
81
|
+
- `/memory` — mostra o que está carregado agora.
|
|
82
|
+
|
|
83
|
+
## Sessões retomáveis
|
|
84
|
+
|
|
85
|
+
Cada turno é salvo em `~/.libertycode/sessions/<projeto>/<id>.json`.
|
|
86
|
+
|
|
87
|
+
- `liberty --continue` — retoma a última sessão deste diretório.
|
|
88
|
+
- `liberty --resume [id]` — retoma uma sessão específica, ou lista pra escolher.
|
|
89
|
+
- `/sessions` e `/resume <id|latest>` — o mesmo, de dentro do chat.
|
|
90
|
+
|
|
91
|
+
## Hooks
|
|
92
|
+
|
|
93
|
+
Configurados em `.liberty/settings.json` (veja o exemplo incluído nesta
|
|
94
|
+
pasta). Eventos disponíveis: `SessionStart`, `UserPromptSubmit`,
|
|
95
|
+
`PreToolUse`, `PostToolUse`, `Stop`. Convenção do código de saída:
|
|
96
|
+
|
|
97
|
+
- `exit 0` → ok; stdout (se houver) só é informativo (ou, em `SessionStart`/`UserPromptSubmit`, vira contexto real pro modelo).
|
|
98
|
+
- `exit 2` → bloqueia a ação (só faz sentido em `PreToolUse`/`UserPromptSubmit`); stderr vira o motivo, devolvido ao modelo.
|
|
99
|
+
- qualquer outro código → aviso, não bloqueia.
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"hooks": {
|
|
104
|
+
"PreToolUse": [{ "matcher": "run_command", "command": "node .liberty/hooks/block-dangerous-commands.js" }],
|
|
105
|
+
"PostToolUse": [{ "matcher": "write_file|edit_file", "command": "npx eslint --fix \"$LIBERTY_FILE\"" }]
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Skills
|
|
111
|
+
|
|
112
|
+
Arquivos `.liberty/skills/<nome>/SKILL.md` (deste projeto) ou
|
|
113
|
+
`~/.libertycode/skills/<nome>/SKILL.md` (globais, reusadas em todos os
|
|
114
|
+
projetos). Só o manifesto (nome + descrição) entra no contexto no início da
|
|
115
|
+
sessão; o modelo lê o `SKILL.md` inteiro (com `read_file`) quando o pedido
|
|
116
|
+
combina com a descrição. `/skills` lista o que foi encontrado.
|
|
117
|
+
|
|
118
|
+
```markdown
|
|
119
|
+
---
|
|
120
|
+
name: revisar-pr
|
|
121
|
+
description: Use ao revisar um Pull Request — checklist de segurança, testes e estilo.
|
|
122
|
+
---
|
|
123
|
+
# conteúdo detalhado, só carregado quando relevante
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Subagentes
|
|
127
|
+
|
|
128
|
+
A ferramenta `task` dispara um subagente com **histórico isolado** (não vê
|
|
129
|
+
a conversa principal, só o `prompt` recebido), ferramentas restritas por
|
|
130
|
+
`subagent_type` (`general` ou `read-only-researcher`), teto de 12 chamadas
|
|
131
|
+
de ferramenta, e devolve só o resumo final pra conversa principal — os
|
|
132
|
+
passos intermediários não poluem o contexto principal. Um subagente não
|
|
133
|
+
pode disparar outro (profundidade máxima 1).
|
|
134
|
+
|
|
135
|
+
## MCP
|
|
136
|
+
|
|
137
|
+
`.liberty/mcp.json` (mesmo formato do `mcpServers` do Claude
|
|
138
|
+
Desktop/Claude Code — copie o bloco de lá se já tiver):
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"mcpServers": {
|
|
143
|
+
"github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "..." } }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Ferramentas descobertas entram como `mcp__github__<ferramenta>`. `/mcp`
|
|
149
|
+
lista os servidores conectados nesta sessão. Falha em um servidor não
|
|
150
|
+
derruba os outros.
|
|
151
|
+
|
|
152
|
+
## Comandos do chat
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
/help Mostra os comandos
|
|
156
|
+
/clear Limpa o histórico (mantém memória/skills carregadas)
|
|
157
|
+
/exit, /quit Sai
|
|
158
|
+
/init Explora o projeto e cria/atualiza o LIBERTY.md
|
|
159
|
+
/memory Mostra a memória carregada agora
|
|
160
|
+
/mode [nome] Mostra ou troca o modo de permissão
|
|
161
|
+
/sessions Lista sessões salvas deste projeto
|
|
162
|
+
/resume <id|latest> Retoma uma sessão salva
|
|
163
|
+
/skills Lista skills carregadas
|
|
164
|
+
/mcp Lista servidores MCP conectados
|
|
165
|
+
/bg Lista processos em background desta sessão
|
|
166
|
+
# texto Anota no LIBERTY.md sem enviar ao modelo
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Arquitetura
|
|
170
|
+
|
|
171
|
+
O **modelo roda no servidor** do LibertyChat (`/api/cli/chat`, Venice AI);
|
|
172
|
+
este CLI decide qual ferramenta oferecer a cada chamada e **executa
|
|
173
|
+
localmente** o que o modelo pede (ler/escrever arquivo, rodar comando) —
|
|
174
|
+
nada do seu código sai da sua máquina além do que você explicitamente
|
|
175
|
+
mandar (conteúdo de arquivo lido, saída de comando confirmado).
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
bin/liberty.js entry point (argv, login/logout, flags)
|
|
179
|
+
src/
|
|
180
|
+
engine.js loop de tool-calling — reusado pela sessão principal E pelos subagentes
|
|
181
|
+
repl.js REPL interativo (comandos de barra, plan mode, autosave)
|
|
182
|
+
api.js cliente HTTP pro /api/cli/chat
|
|
183
|
+
tools/ list_directory, read_file, write_file, edit_file, glob_files, grep_search, run_command, update_todos
|
|
184
|
+
permissions.js modos default/acceptEdits/bypassPermissions/plan
|
|
185
|
+
memory.js LIBERTY.md hierárquico + /init + atalho #
|
|
186
|
+
session.js sessões retomáveis
|
|
187
|
+
hooks.js PreToolUse/PostToolUse/SessionStart/Stop/UserPromptSubmit
|
|
188
|
+
skills.js descoberta de .liberty/skills
|
|
189
|
+
subagents.js ferramenta task
|
|
190
|
+
mcp.js cliente MCP (stdio, SDK oficial)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Comparado ao Claude Code — o que ainda falta
|
|
194
|
+
|
|
195
|
+
Sendo direto, como no pedido original: isto fecha a distância **estrutural**
|
|
196
|
+
(os mesmos conceitos existem e funcionam de verdade — não são só nomes
|
|
197
|
+
imitando a documentação), mas não fecha a distância de **maturidade de
|
|
198
|
+
produto**. Especificamente, não implementado nesta rodada:
|
|
199
|
+
|
|
200
|
+
- **Superfícies além do terminal** (extensão de IDE, app desktop). `engine.js`
|
|
201
|
+
foi desenhado pra ser reutilizável por uma futura superfície, mas construir
|
|
202
|
+
a extensão/app em si é outro projeto.
|
|
203
|
+
- **Allow-list de permissão persistente** entre sessões (hoje `always` vale só
|
|
204
|
+
pra sessão atual, de propósito — é o default mais seguro).
|
|
205
|
+
- **Compactação automática de contexto** em conversas muito longas.
|
|
206
|
+
- **Streaming token-a-token** no terminal (resposta chega de uma vez, como já
|
|
207
|
+
era antes — não é algo que o CLI controla sozinho, depende do transporte
|
|
208
|
+
do servidor).
|
|
209
|
+
|
|
210
|
+
## Requisitos
|
|
211
|
+
|
|
212
|
+
Node.js ≥ 18.
|
package/bin/liberty.js
CHANGED
|
@@ -3,135 +3,55 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Liberty Code — CLI do LibertyChat.
|
|
5
5
|
*
|
|
6
|
+
* v2: este arquivo virou um entry point fino. Toda a lógica (ferramentas,
|
|
7
|
+
* permissões, memória, sessões, hooks, skills, MCP, subagentes, o loop do
|
|
8
|
+
* agente) mora em src/ — bin/liberty.js só decide QUAL comando rodar a
|
|
9
|
+
* partir do argv e chama a função certa. Ver src/repl.js pro coração do CLI.
|
|
10
|
+
*
|
|
6
11
|
* Instalação: npm install -g liberty-code-cli
|
|
7
12
|
* Login: liberty login <token> (gere o token em Configurações → Liberty Code)
|
|
8
13
|
* Uso: cd seu-projeto && liberty
|
|
9
14
|
*
|
|
10
15
|
* Arquitetura: o modelo roda no servidor do LibertyChat; as ferramentas
|
|
11
|
-
* (ler/escrever arquivo, listar diretório, rodar comando)
|
|
12
|
-
* localmente, no processo deste CLI — o servidor só decide
|
|
13
|
-
* usar e com quais argumentos. Nada do seu código é enviado
|
|
14
|
-
* explicitamente
|
|
15
|
-
*
|
|
16
|
+
* (ler/escrever/editar arquivo, listar diretório, buscar, rodar comando)
|
|
17
|
+
* executam AQUI, localmente, no processo deste CLI — o servidor só decide
|
|
18
|
+
* QUAL ferramenta usar e com quais argumentos. Nada do seu código é enviado
|
|
19
|
+
* além do que você explicitamente pedir (conteúdo de arquivos que o agente
|
|
20
|
+
* lê, e a saída de comandos que você confirmar rodar).
|
|
16
21
|
*/
|
|
17
|
-
|
|
18
|
-
const fs = require('fs');
|
|
19
|
-
const path = require('path');
|
|
20
|
-
const os = require('os');
|
|
21
22
|
const readline = require('readline');
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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}`;
|
|
23
|
+
const pkg = require('../package.json');
|
|
24
|
+
const { SERVER_URL, PERMISSION_MODES } = require('../src/constants');
|
|
25
|
+
const { paint } = require('../src/ui');
|
|
26
|
+
const { loadConfig, saveConfig } = require('../src/configStore');
|
|
27
|
+
const sessionStore = require('../src/session');
|
|
28
|
+
|
|
29
|
+
const KNOWN_COMMANDS = new Set(['login', 'logout', '--version', '-v', '--help', '-h']);
|
|
30
|
+
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
// FIX: argv[0] só é tratado como "comando" (login/logout/--version/--help)
|
|
33
|
+
// se for de fato um desses — senão o comando é o chat (default) e o
|
|
34
|
+
// array INTEIRO (incluindo argv[0]) precisa ser escaneado em busca de
|
|
35
|
+
// flags. Antes, "liberty --continue" ou "liberty --permission-mode plan"
|
|
36
|
+
// (flag logo de cara, sem subcomando) tinham a primeira flag engolida
|
|
37
|
+
// como se fosse "cmd" e nunca chegavam a ser reconhecidas.
|
|
38
|
+
const cmd = KNOWN_COMMANDS.has(argv[0]) ? argv[0] : null;
|
|
39
|
+
const rest = cmd ? argv.slice(1) : argv;
|
|
40
|
+
const opts = { permissionMode: null, resumeId: null, dangerouslySkipPermissions: false };
|
|
41
|
+
const positional = [];
|
|
42
|
+
for (let i = 0; i < rest.length; i++) {
|
|
43
|
+
const a = rest[i];
|
|
44
|
+
if (a === '--continue' || a === '-c') opts.resumeId = 'latest';
|
|
45
|
+
else if (a === '--resume') opts.resumeId = rest[i + 1] && !rest[i + 1].startsWith('-') ? rest[++i] : '__pick__';
|
|
46
|
+
else if (a === '--permission-mode') opts.permissionMode = rest[++i];
|
|
47
|
+
else if (a === '--dangerously-skip-permissions') opts.dangerouslySkipPermissions = true;
|
|
48
|
+
else positional.push(a);
|
|
131
49
|
}
|
|
50
|
+
// "login" continua vindo de argv[1] em diante mesmo com essa mudança,
|
|
51
|
+
// já que cmd="login" implica rest=argv.slice(1), igual antes.
|
|
52
|
+
return { cmd: cmd || 'chat', positional, opts };
|
|
132
53
|
}
|
|
133
54
|
|
|
134
|
-
// ── Comandos de CLI ─────────────────────────────────────────────────────────
|
|
135
55
|
async function cmdLogin(token) {
|
|
136
56
|
if (!token) {
|
|
137
57
|
console.log(paint('red', 'Uso: liberty login <token>'));
|
|
@@ -140,15 +60,38 @@ async function cmdLogin(token) {
|
|
|
140
60
|
}
|
|
141
61
|
saveConfig({ token, serverUrl: SERVER_URL });
|
|
142
62
|
console.log(paint('green', '✅ Login salvo com sucesso.'));
|
|
63
|
+
const { CONFIG_FILE } = require('../src/constants');
|
|
143
64
|
console.log(paint('gray', `Token guardado em ${CONFIG_FILE}`));
|
|
144
65
|
}
|
|
145
66
|
|
|
146
67
|
function cmdLogout() {
|
|
68
|
+
const fs = require('fs');
|
|
69
|
+
const { CONFIG_FILE } = require('../src/constants');
|
|
147
70
|
try { fs.unlinkSync(CONFIG_FILE); console.log(paint('green', '✅ Logout feito.')); }
|
|
148
71
|
catch { console.log(paint('gray', 'Nenhuma sessão ativa.')); }
|
|
149
72
|
}
|
|
150
73
|
|
|
151
|
-
async function
|
|
74
|
+
async function pickSessionInteractively(cwd) {
|
|
75
|
+
const list = sessionStore.listSessions(cwd);
|
|
76
|
+
if (!list.length) { console.log(paint('gray', 'Nenhuma sessão salva pra este projeto ainda — começando uma nova.')); return null; }
|
|
77
|
+
console.log(paint('gray', '\nSessões salvas:'));
|
|
78
|
+
list.forEach((s, i) => console.log(paint('gray', ` ${i + 1}) `) + `${s.updatedAt} (${s.messageCount} msgs) ${s.preview}`));
|
|
79
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
80
|
+
const answer = await new Promise(resolve => rl.question(paint('cyan', '\nEscolha um número (Enter pra cancelar e começar do zero): '), a => { rl.close(); resolve(a.trim()); }));
|
|
81
|
+
const idx = parseInt(answer, 10) - 1;
|
|
82
|
+
if (!Number.isInteger(idx) || !list[idx]) return null;
|
|
83
|
+
return list[idx].id;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function confirmDangerousMode() {
|
|
87
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
88
|
+
console.log(paint('red', '\n⚠ --dangerously-skip-permissions liga o modo bypassPermissions:'));
|
|
89
|
+
console.log(paint('red', ' TODA escrita de arquivo e TODO comando serão executados sem perguntar.'));
|
|
90
|
+
const ok = await new Promise(resolve => rl.question(paint('red', 'Confirma? Digite "sim" pra prosseguir: '), a => { rl.close(); resolve(a.trim().toLowerCase() === 'sim'); }));
|
|
91
|
+
if (!ok) { console.log(paint('gray', 'Cancelado.')); process.exit(0); }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function cmdChat(opts) {
|
|
152
95
|
const config = loadConfig();
|
|
153
96
|
if (!config?.token) {
|
|
154
97
|
console.log(paint('red', 'Você ainda não está logado.'));
|
|
@@ -156,64 +99,51 @@ async function cmdChat() {
|
|
|
156
99
|
process.exit(1);
|
|
157
100
|
}
|
|
158
101
|
|
|
159
|
-
|
|
160
|
-
|
|
102
|
+
if (opts.dangerouslySkipPermissions) {
|
|
103
|
+
await confirmDangerousMode();
|
|
104
|
+
opts.permissionMode = PERMISSION_MODES.BYPASS;
|
|
105
|
+
}
|
|
106
|
+
if (opts.permissionMode && !Object.values(PERMISSION_MODES).includes(opts.permissionMode)) {
|
|
107
|
+
console.log(paint('red', `Modo de permissão inválido: "${opts.permissionMode}". Use: ${Object.values(PERMISSION_MODES).join(', ')}`));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
if (opts.resumeId === '__pick__') {
|
|
111
|
+
opts.resumeId = await pickSessionInteractively(process.cwd());
|
|
112
|
+
}
|
|
161
113
|
|
|
162
|
-
const
|
|
163
|
-
|
|
114
|
+
const { start } = require('../src/repl');
|
|
115
|
+
await start(config, opts);
|
|
116
|
+
}
|
|
164
117
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
messages.push({ role: 'user', content: input });
|
|
118
|
+
function printHelp() {
|
|
119
|
+
console.log(`Liberty Code — agente do LibertyChat no terminal (v${pkg.version})
|
|
168
120
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const response = await apiChat(messages, config.token);
|
|
174
|
-
process.stdout.write(' \r'); // limpa a linha "pensando..."
|
|
121
|
+
Uso:
|
|
122
|
+
liberty login <token> Autentica com um token gerado em Configurações → Liberty Code
|
|
123
|
+
liberty logout Remove a sessão salva
|
|
124
|
+
liberty Inicia o chat no diretório atual (é o seu projeto)
|
|
175
125
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
}
|
|
126
|
+
Flags do chat:
|
|
127
|
+
-c, --continue Retoma a última sessão salva deste projeto
|
|
128
|
+
--resume [id] Retoma uma sessão específica (sem id, mostra a lista pra escolher)
|
|
129
|
+
--permission-mode <modo> Inicia já em: default | acceptEdits | bypassPermissions | plan
|
|
130
|
+
--dangerously-skip-permissions Atalho pra bypassPermissions (pede confirmação explícita)
|
|
188
131
|
|
|
189
|
-
|
|
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
|
-
});
|
|
132
|
+
Dentro do chat, digite /help pra ver os comandos (memória, sessões, skills, MCP, modo de permissão etc).
|
|
198
133
|
|
|
199
|
-
|
|
200
|
-
|
|
134
|
+
Dica: crie um arquivo LIBERTY.md na raiz do projeto (ou rode /init de dentro do chat) com
|
|
135
|
+
convenções, comandos de build/teste e o que o agente deve evitar.
|
|
136
|
+
`);
|
|
201
137
|
}
|
|
202
138
|
|
|
203
|
-
// ── Entry point ──────────────────────────────────────────────────────────────
|
|
204
139
|
(async () => {
|
|
205
|
-
const
|
|
206
|
-
if (cmd === 'login') await cmdLogin(
|
|
140
|
+
const { cmd, positional, opts } = parseArgs(process.argv.slice(2));
|
|
141
|
+
if (cmd === 'login') await cmdLogin(positional[0]);
|
|
207
142
|
else if (cmd === 'logout') cmdLogout();
|
|
208
|
-
else if (cmd === '--version' || cmd === '-v') console.log(
|
|
209
|
-
else if (cmd === '--help' || cmd === '-h')
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
liberty Inicia o chat no diretório atual (é o seu projeto)
|
|
216
|
-
`);
|
|
217
|
-
}
|
|
218
|
-
else await cmdChat();
|
|
219
|
-
})();
|
|
143
|
+
else if (cmd === '--version' || cmd === '-v') console.log(`liberty-code-cli ${pkg.version}`);
|
|
144
|
+
else if (cmd === '--help' || cmd === '-h') printHelp();
|
|
145
|
+
else await cmdChat(opts);
|
|
146
|
+
})().catch(e => {
|
|
147
|
+
console.error(paint('red', `\n❌ Erro fatal: ${e.message}`));
|
|
148
|
+
process.exit(1);
|
|
149
|
+
});
|
package/package.json
CHANGED
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "liberty-code-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Liberty Code — o agente do LibertyChat operando no terminal,
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Liberty Code — o agente do LibertyChat operando no terminal: memória de projeto, sessões retomáveis, permissões por modo, hooks, skills, subagentes e MCP.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"liberty": "bin/liberty.js"
|
|
7
7
|
},
|
|
8
8
|
"type": "commonjs",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test"
|
|
11
|
+
},
|
|
9
12
|
"engines": {
|
|
10
13
|
"node": ">=18.0.0"
|
|
11
14
|
},
|
|
12
15
|
"files": [
|
|
13
|
-
"bin/"
|
|
16
|
+
"bin/",
|
|
17
|
+
"src/"
|
|
14
18
|
],
|
|
15
19
|
"license": "MIT",
|
|
16
|
-
"keywords": [
|
|
20
|
+
"keywords": [
|
|
21
|
+
"libertychat",
|
|
22
|
+
"cli",
|
|
23
|
+
"ai",
|
|
24
|
+
"agent",
|
|
25
|
+
"terminal",
|
|
26
|
+
"mcp",
|
|
27
|
+
"skills",
|
|
28
|
+
"subagents"
|
|
29
|
+
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
32
|
+
"ignore": "^7.0.6"
|
|
33
|
+
}
|
|
17
34
|
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* apiChat — v1 mandava só {messages, cwd}. Pra suportar plan mode (menos
|
|
4
|
+
* ferramentas), subagentes (ferramentas + prompt diferentes) e MCP
|
|
5
|
+
* (ferramentas extras), o cliente agora manda seu PRÓPRIO array de tools e
|
|
6
|
+
* um systemPromptSuffix opcional — o server.js precisa aceitar isso (ver
|
|
7
|
+
* FIX correspondente em server.js) em vez de sempre usar o
|
|
8
|
+
* buildLibertyCodeTools() fixo dele.
|
|
9
|
+
*
|
|
10
|
+
* Retrocompatível dos dois lados: se o server ainda for a versão antiga (que
|
|
11
|
+
* ignora campos desconhecidos no body), o CLI simplesmente volta a se
|
|
12
|
+
* comportar como v1 (4 ferramentas fixas, sem suffix) — não quebra.
|
|
13
|
+
*/
|
|
14
|
+
const { SERVER_URL, HISTORY_MESSAGES_SENT } = require('./constants');
|
|
15
|
+
|
|
16
|
+
async function apiChat(messages, token, { tools, systemPromptSuffix, serverUrl, signal } = {}) {
|
|
17
|
+
const url = `${serverUrl || SERVER_URL}/api/cli/chat`;
|
|
18
|
+
const body = { messages: messages.slice(-HISTORY_MESSAGES_SENT), cwd: process.cwd() };
|
|
19
|
+
if (tools) body.tools = tools;
|
|
20
|
+
if (systemPromptSuffix) body.systemPromptSuffix = systemPromptSuffix;
|
|
21
|
+
|
|
22
|
+
const attempt = async () => {
|
|
23
|
+
const res = await fetch(url, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
26
|
+
body: JSON.stringify(body),
|
|
27
|
+
signal,
|
|
28
|
+
});
|
|
29
|
+
const data = await res.json().catch(() => ({}));
|
|
30
|
+
if (!res.ok) throw new Error(data.error || `Erro do servidor (${res.status})`);
|
|
31
|
+
return data;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
return await attempt();
|
|
36
|
+
} catch (e) {
|
|
37
|
+
if (e.name === 'AbortError') throw e; // usuário cancelou (Ctrl+C) — não faz sentido tentar de novo
|
|
38
|
+
if (/^(Erro do servidor|HTTP)/.test(e.message)) throw e; // erro do servidor (4xx/5xx) — repetir não ajuda
|
|
39
|
+
// provável falha de rede transitória (wifi caiu, DNS etc) — uma retentativa curta
|
|
40
|
+
await new Promise(r => setTimeout(r, 1200));
|
|
41
|
+
return attempt();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { apiChat };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { CONFIG_FILE, ensureDir, GLOBAL_DIR } = require('./constants');
|
|
4
|
+
|
|
5
|
+
function loadConfig() {
|
|
6
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return null; }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function saveConfig(cfg) {
|
|
10
|
+
ensureDir(GLOBAL_DIR);
|
|
11
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { loadConfig, saveConfig };
|