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 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) 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
+ * (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 { 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}`;
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] é 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 cmdChat() {
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
- console.log(paint('bold', '\n🦅 Liberty Code') + paint('gray', ` — ${process.cwd()}`));
160
- console.log(paint('gray', 'Digite seu pedido. Ctrl+C para sair.\n'));
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 rl = readline.createInterface({ input: process.stdin, output: process.stdout });
163
- const messages = [];
114
+ const { start } = require('../src/repl');
115
+ await start(config, opts);
116
+ }
164
117
 
165
- const promptUser = () => rl.question(paint('cyan', '› '), async (input) => {
166
- if (!input.trim()) return promptUser();
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
- 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..."
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
- 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
- }
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 em: default | acceptEdits | bypassPermissions | plan
130
+ --dangerously-skip-permissions Atalho pra bypassPermissions (pede confirmação explícita)
188
131
 
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
- });
132
+ Dentro do chat, digite /help pra ver os comandos (memória, sessões, skills, MCP, modo de permissão etc).
198
133
 
199
- rl.on('close', () => { console.log(paint('gray', '\nAté mais!')); process.exit(0); });
200
- promptUser();
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 [, , cmd, arg] = process.argv;
206
- if (cmd === 'login') await cmdLogin(arg);
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('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
- })();
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": "1.0.0",
4
- "description": "Liberty Code — o agente do LibertyChat operando no terminal, sobre projetos de código reais.",
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": ["libertychat", "cli", "ai", "agent", "terminal"]
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 };