kafox-cli 0.2.3 → 0.6.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 +83 -5
- package/bin/kafox.js +5 -1
- package/package.json +4 -3
- package/src/agent.js +190 -0
- package/src/cli.js +51 -8
- package/src/diff.js +193 -0
- package/src/gateway.js +142 -49
- package/src/logo.js +108 -0
- package/src/mascot.js +3 -31
- package/src/mcp.js +557 -0
- package/src/theme.js +327 -3
- package/src/tools.js +359 -0
- package/src/tui.js +1741 -183
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ npm install -g kafox-cli
|
|
|
12
12
|
O comando instalado e `kafox`. Alternativa sem npm:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
npm install -g https://kafox.pypag.com/static/downloads/kafox-cli-0.
|
|
15
|
+
npm install -g https://kafox.pypag.com/static/downloads/kafox-cli-0.3.0.tgz
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Requisito: Node.js 22.5+.
|
|
@@ -32,6 +32,7 @@ navegador, entre na sua conta e clique em **Autorizar**. A sessao fica em
|
|
|
32
32
|
| Comando | O que faz |
|
|
33
33
|
| --- | --- |
|
|
34
34
|
| `kafox` | abre a interface de tela cheia |
|
|
35
|
+
| `kafox --allow-all` | abre ja aprovando tudo (mesmo que `KAFOX_ALLOW_ALL=1`) |
|
|
35
36
|
| `kafox login` / `logout` / `whoami` | sessao da conta |
|
|
36
37
|
| `kafox models` | lista modelos e precos por 1M de tokens |
|
|
37
38
|
| `kafox pricing` | pacotes de recarga |
|
|
@@ -43,16 +44,93 @@ navegador, entre na sua conta e clique em **Autorizar**. A sessao fica em
|
|
|
43
44
|
| `kafox background [url]` | imagem de fundo do terminal |
|
|
44
45
|
| `kafox compact` / `clear` | compacta / limpa o contexto do REPL |
|
|
45
46
|
|
|
46
|
-
Na interface: digite `/` para o menu (↑/↓ navega, Tab completa, Enter executa
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
Na interface: digite `/` para o menu (↑/↓ navega, Tab completa, Enter executa),
|
|
48
|
+
`/model` abre o seletor de modelo e `/theme` o de tema (↑/↓, clique do mouse,
|
|
49
|
+
Enter aplica, Esc cancela). A resposta chega em streaming, com bloco `Thinking`
|
|
50
|
+
quando o modelo manda o raciocinio. PgUp/PgDn ou a roda do mouse rolam a conversa;
|
|
51
|
+
Ctrl+Y copia a ultima resposta e F2 solta o mouse para selecionar texto.
|
|
52
|
+
|
|
53
|
+
Nao precisa esperar a resposta para escrever de novo: **Enter enfileira** a
|
|
54
|
+
mensagem (a fila aparece acima do input e roda em ordem assim que a atual
|
|
55
|
+
termina). Para parar a IA na hora, **Esc duas vezes** — o primeiro Esc so avisa,
|
|
56
|
+
o segundo interrompe o passo em andamento e guarda na conversa o que ja tinha
|
|
57
|
+
chegado.
|
|
58
|
+
|
|
59
|
+
Demais comandos: `/help`, `/models`, `/balance`, `/pricing`, `/key`, `/login`,
|
|
60
|
+
`/logout`, `/whoami`, `/mcp`, `/allow`, `/theme`, `/background`, `/clear`,
|
|
61
|
+
`/compact`, `/status`, `/exit`.
|
|
62
|
+
|
|
63
|
+
O `/theme` traz 29 temas (default, dark, light, brutalist, neon, paper,
|
|
64
|
+
dracula, nord, gruvbox, monokai, tokyo-night, catppuccin, solarized, midnight,
|
|
65
|
+
ocean, forest, sunset, plum, synthwave, cyberpunk, amoled, matrix, terminal,
|
|
66
|
+
solarized-light, rose, mint, sepia, ice e high-contrast).
|
|
67
|
+
|
|
68
|
+
## Ferramentas do agente
|
|
69
|
+
|
|
70
|
+
Com o saldo em dia a TUI deixa o modelo ler e mexer no projeto. Cada chamada
|
|
71
|
+
aparece como um card na conversa, com o diff do que foi editado.
|
|
72
|
+
|
|
73
|
+
| Ferramenta | O que faz |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `read_file` | le um arquivo (com `offset`/`limit`) |
|
|
76
|
+
| `list_dir` | lista uma pasta |
|
|
77
|
+
| `write_file` | cria ou reescreve um arquivo |
|
|
78
|
+
| `edit_file` | troca um trecho exato do arquivo |
|
|
79
|
+
| `run_command` | roda um comando no diretorio de trabalho |
|
|
80
|
+
|
|
81
|
+
`run_command` sai como uma janela de terminal: o comando em `$ ...`, a saida
|
|
82
|
+
dentro da janela (erro em vermelho) e o rodape com `exit` e o tempo; enquanto
|
|
83
|
+
roda, aparece o spinner com o tempo decorrido. Ferramentas de MCP usam a mesma
|
|
84
|
+
janela, com os argumentos e o resultado.
|
|
85
|
+
|
|
86
|
+
Quem escreve, edita ou executa pede aprovacao num modal com tres opcoes:
|
|
87
|
+
**Permitir uma vez**, **Permitir sempre** (roda tudo nesta sessao, sem perguntar
|
|
88
|
+
de novo — inclusive as proximas ferramentas do mesmo passo) e **Negar**. Navegue
|
|
89
|
+
com ↑/↓ (ou clique) e confirme com Enter; `a` libera sempre, `y` uma vez,
|
|
90
|
+
`n`/Esc nega. Todo caminho fica travado dentro do diretorio de trabalho.
|
|
91
|
+
|
|
92
|
+
Para ja abrir liberado, use `kafox --allow-all` (ou `KAFOX_ALLOW_ALL=1`), e dentro
|
|
93
|
+
da sessao `/allow` liga e `/allow off` volta a perguntar. Com o modo ligado a
|
|
94
|
+
barra lateral mostra **rodando tudo**.
|
|
95
|
+
|
|
96
|
+
## Servidores MCP
|
|
97
|
+
|
|
98
|
+
Ferramentas de servidores MCP entram no mesmo loop, com o prefixo
|
|
99
|
+
`mcp__servidor__ferramenta`. Configure em `~/.kafox/mcp.json` (ou `./mcp.json` do
|
|
100
|
+
projeto, que vence):
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"mcp": {
|
|
105
|
+
"playwright": {
|
|
106
|
+
"type": "local",
|
|
107
|
+
"command": ["npx", "-y", "@playwright/mcp@latest"]
|
|
108
|
+
},
|
|
109
|
+
"remoto": {
|
|
110
|
+
"type": "remote",
|
|
111
|
+
"url": "https://exemplo.com/mcp",
|
|
112
|
+
"headers": { "authorization": "Bearer ..." }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
O servidor local roda por stdio e o remoto fala HTTP/SSE. Use `enabled: false`
|
|
119
|
+
para desligar um servidor. Na TUI a barra lateral mostra o status de cada um (e o
|
|
120
|
+
motivo do erro quando falha) e `/mcp` lista tudo; `/mcp reload` reconecta.
|
|
50
121
|
|
|
51
122
|
## Configuracao
|
|
52
123
|
|
|
53
124
|
- `KAFOX_API_URL` ou `~/.kafox/config.json` (`api_url`) troca a API.
|
|
54
125
|
- `~/.kafox/config.json` guarda `model`, `theme`, `background` e a API key criada
|
|
55
126
|
automaticamente no primeiro `ask`.
|
|
127
|
+
- `~/.kafox/mcp.json` guarda os servidores MCP.
|
|
56
128
|
|
|
57
129
|
A CLI so responde se a conta tiver saldo; sem credito ela avisa e nao chama o
|
|
58
130
|
gateway.
|
|
131
|
+
|
|
132
|
+
## Escopo
|
|
133
|
+
|
|
134
|
+
O kafox autentica na conta, lista modelos, mostra saldo, conversa pelo gateway e
|
|
135
|
+
roda tarefas no projeto: le e edita arquivos, executa comandos (sempre com
|
|
136
|
+
aprovacao) e chama servidores MCP configurados.
|
package/bin/kafox.js
CHANGED
|
@@ -12,6 +12,10 @@ const program = new Command();
|
|
|
12
12
|
program
|
|
13
13
|
.name("kafox")
|
|
14
14
|
.description("Kafox CLI in opencode style: terminal UI, account login, models, balance, themes")
|
|
15
|
+
.option(
|
|
16
|
+
"-y, --allow-all",
|
|
17
|
+
"abre ja aprovando tudo: ferramentas de arquivo, comando e MCP sem pedir aprovacao"
|
|
18
|
+
)
|
|
15
19
|
.version(version);
|
|
16
20
|
|
|
17
21
|
program.command("login").description("Login with your Kafox account (device code SSO)").action(login);
|
|
@@ -28,5 +32,5 @@ program.command("compact").description("Compact the current conversation context
|
|
|
28
32
|
program.command("clear").description("Clear the conversation context").action(clearContext);
|
|
29
33
|
program.command("status").description("Show account, balance and gateway status").action(showStatus);
|
|
30
34
|
|
|
31
|
-
program.action(runREPL);
|
|
35
|
+
program.action((options) => runREPL({ allowAll: Boolean(options.allowAll) }));
|
|
32
36
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kafox-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Kafox CLI
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Kafox CLI: TUI com streaming, thinking, edit com diff, agent tools (files/shell), MCP e temas",
|
|
5
5
|
"bin": {
|
|
6
6
|
"kafox": "bin/kafox.js"
|
|
7
7
|
},
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"inquirer": "8.2.6"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"test": "node --test \"test/*.test.js\""
|
|
23
|
+
"test": "node --test \"test/*.test.js\"",
|
|
24
|
+
"smoke": "node test/e2e/tui.js"
|
|
24
25
|
}
|
|
25
26
|
}
|
package/src/agent.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
const { chat } = require("./gateway");
|
|
2
|
+
const {
|
|
3
|
+
TOOL_SPECS,
|
|
4
|
+
APPROVAL_REQUIRED,
|
|
5
|
+
executeTool,
|
|
6
|
+
describeToolCall,
|
|
7
|
+
previewToolChange,
|
|
8
|
+
summarizeOutput,
|
|
9
|
+
} = require("./tools");
|
|
10
|
+
|
|
11
|
+
const MAX_STEPS = 10;
|
|
12
|
+
|
|
13
|
+
function parseArgs(raw) {
|
|
14
|
+
if (!raw) return {};
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
18
|
+
} catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function systemPrompt(cwd, model) {
|
|
24
|
+
return [
|
|
25
|
+
"Voce e o kafox, uma CLI de terminal que conversa e executa tarefas no projeto do usuario.",
|
|
26
|
+
`Diretorio de trabalho: ${cwd}`,
|
|
27
|
+
`Modelo: ${model || "auto"}`,
|
|
28
|
+
"Ferramentas locais: read_file, list_dir, write_file, edit_file, run_command.",
|
|
29
|
+
"Para mudar um arquivo existente use edit_file (troca de trecho exato); write_file e para arquivo novo ou reescrita completa.",
|
|
30
|
+
"Ferramentas com nome mcp__<servidor>__<ferramenta> vem de servidores MCP conectados: use quando ajudarem.",
|
|
31
|
+
"Nunca invente conteudo de arquivo: leia antes de editar. Prefira caminhos relativos.",
|
|
32
|
+
"Quem escreve ou executa passa por aprovacao do usuario, entao explique em uma frase o que vai fazer.",
|
|
33
|
+
"Responda no idioma do usuario, de forma direta, com Markdown.",
|
|
34
|
+
].join("\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Roda o loop: modelo -> tool calls -> resultados -> modelo, ate a resposta final.
|
|
39
|
+
* Ferramenta que escreve, edita, executa ou vem de MCP passa por `approve`
|
|
40
|
+
* (async, true/false), a menos que `autoApprove` esteja ligado ("rodar tudo").
|
|
41
|
+
*/
|
|
42
|
+
async function runAgent({
|
|
43
|
+
messages = [],
|
|
44
|
+
model,
|
|
45
|
+
cwd = process.cwd(),
|
|
46
|
+
approve,
|
|
47
|
+
autoApprove = false,
|
|
48
|
+
extraTools = [],
|
|
49
|
+
mcp = null,
|
|
50
|
+
signal,
|
|
51
|
+
maxSteps = MAX_STEPS,
|
|
52
|
+
onText,
|
|
53
|
+
onReasoning,
|
|
54
|
+
onToolStart,
|
|
55
|
+
onToolResult,
|
|
56
|
+
} = {}) {
|
|
57
|
+
const conversation = messages.map((message) => ({ ...message }));
|
|
58
|
+
if (!conversation.length || conversation[0].role !== "system") {
|
|
59
|
+
conversation.unshift({ role: "system", content: systemPrompt(cwd, model) });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const tools = extraTools.length ? [...TOOL_SPECS, ...extraTools] : TOOL_SPECS;
|
|
63
|
+
// `autoApprove` pode ser um valor ou uma funcao: o "Permitir sempre" da TUI
|
|
64
|
+
// liga o modo no meio do loop, e o resto da execucao ja nao pergunta mais.
|
|
65
|
+
const autoAllowed = typeof autoApprove === "function" ? autoApprove : () => Boolean(autoApprove);
|
|
66
|
+
let last = { text: "", reasoning: "", ms: 0, usage: null };
|
|
67
|
+
|
|
68
|
+
for (let step = 0; step < maxSteps; step += 1) {
|
|
69
|
+
// Esc duas vezes na TUI aborta o signal: nao comeca outro passo.
|
|
70
|
+
if (signal?.aborted) {
|
|
71
|
+
return { ok: true, aborted: true, text: last.text, messages: conversation, steps: step };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const started = Date.now();
|
|
75
|
+
const result = await chat({
|
|
76
|
+
messages: conversation,
|
|
77
|
+
model,
|
|
78
|
+
tools,
|
|
79
|
+
signal,
|
|
80
|
+
onDelta: onText,
|
|
81
|
+
onReasoning,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!result.ok) {
|
|
85
|
+
return { ...result, aborted: result.code === "aborted", messages: conversation, text: last.text };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Stream cortado no meio: guarda o que ja chegou e encerra o loop.
|
|
89
|
+
if (result.aborted) {
|
|
90
|
+
return {
|
|
91
|
+
ok: true,
|
|
92
|
+
aborted: true,
|
|
93
|
+
text: result.text || "",
|
|
94
|
+
reasoning: result.reasoning || "",
|
|
95
|
+
ms: result.ms,
|
|
96
|
+
messages: conversation,
|
|
97
|
+
steps: step + 1,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
last = {
|
|
102
|
+
text: result.text,
|
|
103
|
+
reasoning: result.reasoning,
|
|
104
|
+
ms: result.ms,
|
|
105
|
+
usage: result.usage,
|
|
106
|
+
thoughtMs: Date.now() - started,
|
|
107
|
+
};
|
|
108
|
+
const assistant = { role: "assistant", content: result.text || "" };
|
|
109
|
+
if (result.toolCalls.length) assistant.tool_calls = result.toolCalls;
|
|
110
|
+
conversation.push(assistant);
|
|
111
|
+
|
|
112
|
+
if (!result.toolCalls.length) {
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
...last,
|
|
116
|
+
keyCreated: result.keyCreated,
|
|
117
|
+
messages: conversation,
|
|
118
|
+
steps: step + 1,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const call of result.toolCalls) {
|
|
123
|
+
const name = call?.function?.name || "";
|
|
124
|
+
const args = parseArgs(call?.function?.arguments);
|
|
125
|
+
const fromMcp = Boolean(mcp?.has?.(name));
|
|
126
|
+
const needsApproval = APPROVAL_REQUIRED.has(name) || fromMcp;
|
|
127
|
+
|
|
128
|
+
let change = null;
|
|
129
|
+
try {
|
|
130
|
+
change = previewToolChange(name, args, { cwd });
|
|
131
|
+
} catch {
|
|
132
|
+
change = null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
onToolStart?.({
|
|
136
|
+
name,
|
|
137
|
+
args,
|
|
138
|
+
label: describeToolCall(name, args),
|
|
139
|
+
change,
|
|
140
|
+
fromMcp,
|
|
141
|
+
needsApproval,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const gate = needsApproval && !autoAllowed();
|
|
145
|
+
let allowed = true;
|
|
146
|
+
if (gate) {
|
|
147
|
+
allowed = approve ? Boolean(await approve(name, args, { change, fromMcp })) : false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const toolStarted = Date.now();
|
|
151
|
+
let output;
|
|
152
|
+
if (!allowed) {
|
|
153
|
+
output = "O usuario negou a execucao desta ferramenta. Peca instrucoes se precisar.";
|
|
154
|
+
} else {
|
|
155
|
+
try {
|
|
156
|
+
output = await executeTool(name, args, { cwd, mcp });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
output = `Erro: ${error.message}`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
onToolResult?.({
|
|
163
|
+
name,
|
|
164
|
+
args,
|
|
165
|
+
output,
|
|
166
|
+
change,
|
|
167
|
+
ok: allowed && !String(output).startsWith("Erro:"),
|
|
168
|
+
skipped: !allowed,
|
|
169
|
+
fromMcp,
|
|
170
|
+
ms: Date.now() - toolStarted,
|
|
171
|
+
summary: summarizeOutput(output),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
conversation.push({
|
|
175
|
+
role: "tool",
|
|
176
|
+
tool_call_id: call.id || `call_${step}`,
|
|
177
|
+
content: String(output),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
error: `Limite de ${maxSteps} passos do agente atingido. Tente dividir o pedido.`,
|
|
185
|
+
...last,
|
|
186
|
+
messages: conversation,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { runAgent, MAX_STEPS, systemPrompt };
|
package/src/cli.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
const readline = require("readline");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
2
4
|
const inquirer = require("inquirer");
|
|
3
5
|
const chalk = require("chalk");
|
|
4
6
|
const storage = require("./storage");
|
|
5
7
|
const { ensureSession, fetchApi } = require("./auth");
|
|
6
8
|
const { ask } = require("./gateway");
|
|
7
9
|
const { runTUI } = require("./tui");
|
|
8
|
-
const { runTheme, setContentBackground
|
|
9
|
-
const {
|
|
10
|
+
const { runTheme, setContentBackground } = require("./theme");
|
|
11
|
+
const { logoText, logoLines } = require("./logo");
|
|
12
|
+
|
|
13
|
+
const mascot = logoText();
|
|
10
14
|
|
|
11
15
|
const { version: VERSION } = require("../package.json");
|
|
12
16
|
|
|
@@ -21,6 +25,8 @@ const COMMANDS = {
|
|
|
21
25
|
"/logout": "end the session",
|
|
22
26
|
"/whoami": "show the linked account",
|
|
23
27
|
"/theme": "open theme selector",
|
|
28
|
+
"/mcp": "list MCP servers and tools (/mcp reload reconecta)",
|
|
29
|
+
"/allow": "aprovacao automatica nesta sessao (/allow off volta a perguntar)",
|
|
24
30
|
"/background": "set background image (modal asks for URL)",
|
|
25
31
|
"/clear": "clear conversation context",
|
|
26
32
|
"/compact": "compact conversation context",
|
|
@@ -45,6 +51,13 @@ async function handleCommand(line) {
|
|
|
45
51
|
for (const [key, value] of Object.entries(COMMANDS)) {
|
|
46
52
|
console.log(` ${chalk.bold(key)} ${chalk.dim(value)}`);
|
|
47
53
|
}
|
|
54
|
+
console.log(chalk.cyan("\nKeys:\n"));
|
|
55
|
+
console.log(` ${chalk.bold("Enter")} ${chalk.dim("manda a mensagem; durante a resposta ela entra na fila")}`);
|
|
56
|
+
console.log(` ${chalk.bold("Esc Esc")} ${chalk.dim("para a IA na hora (o primeiro Esc avisa)")}`);
|
|
57
|
+
console.log(` ${chalk.bold("PgUp/PgDn")} ${chalk.dim("ou a roda do mouse rola a conversa")}`);
|
|
58
|
+
console.log(` ${chalk.bold("Ctrl+Y")} ${chalk.dim("copia a ultima resposta para a area de transferencia")}`);
|
|
59
|
+
console.log(` ${chalk.bold("F2")} ${chalk.dim("solta o mouse para selecionar texto com o terminal")}`);
|
|
60
|
+
console.log(` ${chalk.bold("Ctrl+C")} ${chalk.dim("sai")}`);
|
|
48
61
|
console.log("");
|
|
49
62
|
return true;
|
|
50
63
|
case "/models": {
|
|
@@ -94,6 +107,34 @@ async function handleCommand(line) {
|
|
|
94
107
|
case "/theme":
|
|
95
108
|
await runTheme();
|
|
96
109
|
return true;
|
|
110
|
+
case "/allow": {
|
|
111
|
+
console.log(
|
|
112
|
+
chalk.dim(
|
|
113
|
+
"Na interface, /allow liga e /allow off desliga a aprovacao automatica.\n" +
|
|
114
|
+
"Para ja abrir assim: kafox --allow-all (ou KAFOX_ALLOW_ALL=1)."
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
case "/mcp": {
|
|
120
|
+
const { McpHub } = require("./mcp");
|
|
121
|
+
const hub = new McpHub({ cwd: process.cwd() });
|
|
122
|
+
const statuses = hub.statuses();
|
|
123
|
+
hub.close();
|
|
124
|
+
if (!statuses.length) {
|
|
125
|
+
console.log(chalk.dim("\nNenhum servidor MCP configurado."));
|
|
126
|
+
const file = path.join(os.homedir(), ".kafox", "mcp.json");
|
|
127
|
+
console.log(chalk.dim(`Crie ${file} com:`));
|
|
128
|
+
console.log(chalk.dim('{ "mcp": { "playwright": { "type": "local", "command": ["npx","-y","@playwright/mcp@latest"] } } }'));
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
console.log(chalk.cyan("\nServidores MCP:\n"));
|
|
132
|
+
for (const status of statuses) {
|
|
133
|
+
console.log(` ${chalk.bold(status.name)} ${chalk.dim(status.kind)}`);
|
|
134
|
+
}
|
|
135
|
+
console.log(chalk.dim("\nAbra a TUI (kafox) e use /mcp para conectar e ver as ferramentas."));
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
97
138
|
case "/background":
|
|
98
139
|
await setContentBackground(arg || null);
|
|
99
140
|
return true;
|
|
@@ -168,8 +209,8 @@ async function runPlainREPL() {
|
|
|
168
209
|
const cfg = storage.getConfig();
|
|
169
210
|
const theme = cfg.theme || "default";
|
|
170
211
|
|
|
171
|
-
console.log(
|
|
172
|
-
console.log(
|
|
212
|
+
console.log(logoLines(theme).join("\n"));
|
|
213
|
+
console.log(chalk.bold(`\n Kafox CLI v${VERSION}`));
|
|
173
214
|
console.log(chalk.dim(" Type /help for commands. Ctrl+C to exit.\n"));
|
|
174
215
|
|
|
175
216
|
const token = await ensureSession().catch(() => null);
|
|
@@ -224,7 +265,7 @@ async function runPlainREPL() {
|
|
|
224
265
|
});
|
|
225
266
|
}
|
|
226
267
|
|
|
227
|
-
async function runREPL() {
|
|
268
|
+
async function runREPL(options = {}) {
|
|
228
269
|
// Sem TTY (pipe/CI), mantem o REPL linha a linha.
|
|
229
270
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return runPlainREPL();
|
|
230
271
|
|
|
@@ -232,8 +273,8 @@ async function runREPL() {
|
|
|
232
273
|
const token = await ensureSession().catch(() => null);
|
|
233
274
|
|
|
234
275
|
if (!token) {
|
|
235
|
-
console.log(
|
|
236
|
-
console.log(
|
|
276
|
+
console.log(logoLines(cfg.theme || "default").join("\n"));
|
|
277
|
+
console.log(chalk.bold(`\n Kafox CLI v${VERSION}`));
|
|
237
278
|
console.log(chalk.yellow(" You are not logged in."));
|
|
238
279
|
const { doLogin } = await inquirer.prompt([
|
|
239
280
|
{ type: "confirm", name: "doLogin", message: "Login now?", default: true },
|
|
@@ -246,7 +287,9 @@ async function runREPL() {
|
|
|
246
287
|
return runTUI({
|
|
247
288
|
commands: COMMANDS,
|
|
248
289
|
onCommand: handleCommand,
|
|
249
|
-
|
|
290
|
+
// /theme roda dentro da TUI (modal com preview); os demais abrem prompt no terminal.
|
|
291
|
+
interactiveCommands: new Set(["/login", "/key", "/background"]),
|
|
292
|
+
autoApprove: Boolean(options?.allowAll),
|
|
250
293
|
});
|
|
251
294
|
}
|
|
252
295
|
|
package/src/diff.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff de linhas para o preview de edicao (estilo `git diff`, enxuto).
|
|
3
|
+
*
|
|
4
|
+
* Usa LCS quando os trechos sao pequenos e cai para um resumo grosseiro em
|
|
5
|
+
* arquivos grandes, para nao travar a TUI.
|
|
6
|
+
*/
|
|
7
|
+
const MAX_LCS_LINES = 700;
|
|
8
|
+
|
|
9
|
+
function splitLines(text) {
|
|
10
|
+
return String(text ?? "").replace(/\r\n?/g, "\n").split("\n");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function commonPrefix(a, b) {
|
|
14
|
+
const limit = Math.min(a.length, b.length);
|
|
15
|
+
let index = 0;
|
|
16
|
+
while (index < limit && a[index] === b[index]) index += 1;
|
|
17
|
+
return index;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function commonSuffix(a, b, prefix) {
|
|
21
|
+
const limit = Math.min(a.length, b.length) - prefix;
|
|
22
|
+
let count = 0;
|
|
23
|
+
while (count < limit && a[a.length - 1 - count] === b[b.length - 1 - count]) count += 1;
|
|
24
|
+
return count;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** LCS classico: devolve as operacoes (del/add) do trecho que mudou. */
|
|
28
|
+
function lcsOps(left, right) {
|
|
29
|
+
const rows = left.length;
|
|
30
|
+
const cols = right.length;
|
|
31
|
+
const table = new Uint16Array((rows + 1) * (cols + 1));
|
|
32
|
+
const width = cols + 1;
|
|
33
|
+
|
|
34
|
+
for (let i = rows - 1; i >= 0; i -= 1) {
|
|
35
|
+
for (let j = cols - 1; j >= 0; j -= 1) {
|
|
36
|
+
table[i * width + j] =
|
|
37
|
+
left[i] === right[j]
|
|
38
|
+
? table[(i + 1) * width + j + 1] + 1
|
|
39
|
+
: Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ops = [];
|
|
44
|
+
let i = 0;
|
|
45
|
+
let j = 0;
|
|
46
|
+
while (i < rows && j < cols) {
|
|
47
|
+
if (left[i] === right[j]) {
|
|
48
|
+
ops.push({ type: "ctx", text: left[i] });
|
|
49
|
+
i += 1;
|
|
50
|
+
j += 1;
|
|
51
|
+
} else if (table[(i + 1) * width + j] >= table[i * width + j + 1]) {
|
|
52
|
+
ops.push({ type: "del", text: left[i] });
|
|
53
|
+
i += 1;
|
|
54
|
+
} else {
|
|
55
|
+
ops.push({ type: "add", text: right[j] });
|
|
56
|
+
j += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
while (i < rows) ops.push({ type: "del", text: left[i++] });
|
|
60
|
+
while (j < cols) ops.push({ type: "add", text: right[j++] });
|
|
61
|
+
return ops;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function coarseOps(left, right) {
|
|
65
|
+
return [
|
|
66
|
+
...left.map((text) => ({ type: "del", text })),
|
|
67
|
+
...right.map((text) => ({ type: "add", text })),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Todas as operacoes, com numero de linha nos dois lados.
|
|
73
|
+
* `truncated` avisa que o miolo foi tratado de forma grosseira.
|
|
74
|
+
*/
|
|
75
|
+
function buildOps(before, after) {
|
|
76
|
+
const left = splitLines(before);
|
|
77
|
+
const right = splitLines(after);
|
|
78
|
+
const prefix = commonPrefix(left, right);
|
|
79
|
+
const suffix = commonSuffix(left, right, prefix);
|
|
80
|
+
|
|
81
|
+
const leftMiddle = left.slice(prefix, left.length - suffix);
|
|
82
|
+
const rightMiddle = right.slice(prefix, right.length - suffix);
|
|
83
|
+
const truncated = leftMiddle.length > MAX_LCS_LINES || rightMiddle.length > MAX_LCS_LINES;
|
|
84
|
+
const middle = truncated ? coarseOps(leftMiddle, rightMiddle) : lcsOps(leftMiddle, rightMiddle);
|
|
85
|
+
|
|
86
|
+
const ops = [];
|
|
87
|
+
for (let index = 0; index < prefix; index += 1) {
|
|
88
|
+
ops.push({ type: "ctx", text: left[index], beforeNo: index + 1, afterNo: index + 1 });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let beforeNo = prefix + 1;
|
|
92
|
+
let afterNo = prefix + 1;
|
|
93
|
+
let changed = false;
|
|
94
|
+
for (const op of middle) {
|
|
95
|
+
if (op.type === "ctx") {
|
|
96
|
+
ops.push({ ...op, beforeNo, afterNo });
|
|
97
|
+
beforeNo += 1;
|
|
98
|
+
afterNo += 1;
|
|
99
|
+
} else if (op.type === "del") {
|
|
100
|
+
ops.push({ ...op, beforeNo });
|
|
101
|
+
beforeNo += 1;
|
|
102
|
+
changed = true;
|
|
103
|
+
} else {
|
|
104
|
+
ops.push({ ...op, afterNo });
|
|
105
|
+
afterNo += 1;
|
|
106
|
+
changed = true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (let index = 0; index < suffix; index += 1) {
|
|
111
|
+
const line = left.length - suffix + index;
|
|
112
|
+
ops.push({
|
|
113
|
+
type: "ctx",
|
|
114
|
+
text: left[line],
|
|
115
|
+
beforeNo: line + 1,
|
|
116
|
+
afterNo: right.length - suffix + index + 1,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { ops, truncated, changed };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function count(lines) {
|
|
124
|
+
return {
|
|
125
|
+
added: lines.filter((line) => line.type === "add").length,
|
|
126
|
+
removed: lines.filter((line) => line.type === "del").length,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Mantem so as mudancas e `context` linhas em volta (com o indice original). */
|
|
131
|
+
function keptWithContext(ops, context) {
|
|
132
|
+
const keep = new Set();
|
|
133
|
+
ops.forEach((op, index) => {
|
|
134
|
+
if (op.type === "ctx") return;
|
|
135
|
+
for (let cursor = index - context; cursor <= index + context; cursor += 1) {
|
|
136
|
+
if (cursor >= 0 && cursor < ops.length) keep.add(cursor);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
return ops.map((op, index) => ({ op, index })).filter((entry) => keep.has(entry.index));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Diff enxuto, com numeros de linha e `context` linhas de contexto.
|
|
144
|
+
* Arquivo identico devolve lista vazia.
|
|
145
|
+
*/
|
|
146
|
+
function diffLines(before, after, { context = 2 } = {}) {
|
|
147
|
+
const { ops, truncated, changed } = buildOps(before, after);
|
|
148
|
+
if (!changed) return { lines: [], added: 0, removed: 0, truncated: false };
|
|
149
|
+
const lines = keptWithContext(ops, context).map((entry) => entry.op);
|
|
150
|
+
return { lines, ...count(lines), truncated };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Diff agrupado em blocos separados (cada regiao que mudou vira um bloco),
|
|
155
|
+
* limitado a `maxLines` linhas para caber no preview.
|
|
156
|
+
*/
|
|
157
|
+
function diffBlocks(before, after, { context = 2, maxLines = 60 } = {}) {
|
|
158
|
+
const { ops, truncated, changed } = buildOps(before, after);
|
|
159
|
+
if (!changed) return { blocks: [], added: 0, removed: 0, truncated: false, total: 0 };
|
|
160
|
+
|
|
161
|
+
const kept = keptWithContext(ops, context);
|
|
162
|
+
const blocks = [];
|
|
163
|
+
let current = null;
|
|
164
|
+
let previousIndex = null;
|
|
165
|
+
|
|
166
|
+
for (const entry of kept) {
|
|
167
|
+
if (previousIndex == null || entry.index !== previousIndex + 1) {
|
|
168
|
+
current = [];
|
|
169
|
+
blocks.push(current);
|
|
170
|
+
}
|
|
171
|
+
current.push(entry.op);
|
|
172
|
+
previousIndex = entry.index;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let budget = maxLines;
|
|
176
|
+
const limited = [];
|
|
177
|
+
for (const block of blocks) {
|
|
178
|
+
if (budget <= 0) break;
|
|
179
|
+
const slice = block.slice(0, budget);
|
|
180
|
+
limited.push(slice);
|
|
181
|
+
budget -= slice.length;
|
|
182
|
+
if (slice.length < block.length) limited.push([{ type: "gap", text: "..." }]);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
blocks: limited,
|
|
187
|
+
...count(kept.map((entry) => entry.op)),
|
|
188
|
+
truncated,
|
|
189
|
+
total: blocks.length,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = { diffLines, diffBlocks, splitLines, lcsOps, buildOps };
|