kafox-cli 0.2.1 → 0.2.2
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 +6 -5
- package/package.json +1 -1
- package/src/cli.js +2 -2
- package/src/gateway.js +1 -1
- package/src/tui.js +168 -50
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.2.
|
|
15
|
+
npm install -g https://kafox.pypag.com/static/downloads/kafox-cli-0.2.2.tgz
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Requisito: Node.js 22.5+.
|
|
@@ -31,7 +31,7 @@ navegador, entre na sua conta e clique em **Autorizar**. A sessao fica em
|
|
|
31
31
|
|
|
32
32
|
| Comando | O que faz |
|
|
33
33
|
| --- | --- |
|
|
34
|
-
| `kafox` | abre
|
|
34
|
+
| `kafox` | abre a interface de tela cheia |
|
|
35
35
|
| `kafox login` / `logout` / `whoami` | sessao da conta |
|
|
36
36
|
| `kafox models` | lista modelos e precos por 1M de tokens |
|
|
37
37
|
| `kafox pricing` | pacotes de recarga |
|
|
@@ -43,9 +43,10 @@ navegador, entre na sua conta e clique em **Autorizar**. A sessao fica em
|
|
|
43
43
|
| `kafox background [url]` | imagem de fundo do terminal |
|
|
44
44
|
| `kafox compact` / `clear` | compacta / limpa o contexto do REPL |
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`/
|
|
46
|
+
Na interface: digite `/` para o menu (↑/↓ navega, Tab completa, Enter executa
|
|
47
|
+
), `/model` abre o seletor de modelo (↑/↓ + Enter). Demais comandos: `/help`,
|
|
48
|
+
`/models`, `/balance`, `/pricing`, `/key`, `/login`, `/logout`, `/whoami`,
|
|
49
|
+
`/theme`, `/background`, `/clear`, `/compact`, `/status`, `/exit`.
|
|
49
50
|
|
|
50
51
|
## Configuracao
|
|
51
52
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ const { version: VERSION } = require("../package.json");
|
|
|
13
13
|
const COMMANDS = {
|
|
14
14
|
"/help": "show this help",
|
|
15
15
|
"/models": "list models and prices",
|
|
16
|
-
"/model
|
|
16
|
+
"/model": "choose the default model (picker)",
|
|
17
17
|
"/balance": "show balance",
|
|
18
18
|
"/pricing": "show recharge packages",
|
|
19
19
|
"/key": "create an API key",
|
|
@@ -21,7 +21,7 @@ const COMMANDS = {
|
|
|
21
21
|
"/logout": "end the session",
|
|
22
22
|
"/whoami": "show the linked account",
|
|
23
23
|
"/theme": "open theme selector",
|
|
24
|
-
"/background
|
|
24
|
+
"/background": "set background image (modal asks for URL)",
|
|
25
25
|
"/clear": "clear conversation context",
|
|
26
26
|
"/compact": "compact conversation context",
|
|
27
27
|
"/status": "show account, balance and gateway status",
|
package/src/gateway.js
CHANGED
|
@@ -120,7 +120,7 @@ async function ask(question, options = {}) {
|
|
|
120
120
|
console.log(chalk.green("\nAnswer:"));
|
|
121
121
|
console.log(result.text);
|
|
122
122
|
if (result.ms) {
|
|
123
|
-
console.log(chalk.dim(`\n(${result.ms}ms
|
|
123
|
+
console.log(chalk.dim(`\n(${result.ms}ms)`));
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
package/src/tui.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Interface de terminal do kafox, no estilo opencode:
|
|
3
|
-
* tela cheia, mascote, menu de comandos "/",
|
|
3
|
+
* tela cheia, mascote, menu de comandos "/", seletor de modelo, input e rodape.
|
|
4
4
|
*
|
|
5
5
|
* Tudo e desenhado a mao (sem dependencia de TUI) para manter o pacote leve e
|
|
6
6
|
* funcionar no Windows Terminal. Sem TTY, o cli.js usa o REPL simples.
|
|
@@ -9,7 +9,6 @@ const readline = require("readline");
|
|
|
9
9
|
const chalk = require("chalk");
|
|
10
10
|
const storage = require("./storage");
|
|
11
11
|
const { mascot } = require("./mascot");
|
|
12
|
-
const { paint } = require("./theme");
|
|
13
12
|
const { ensureSession, fetchApi } = require("./auth");
|
|
14
13
|
const { askModel } = require("./gateway");
|
|
15
14
|
|
|
@@ -72,6 +71,9 @@ function initialState() {
|
|
|
72
71
|
busy: false,
|
|
73
72
|
frame: 0,
|
|
74
73
|
menuIndex: 0,
|
|
74
|
+
menuDismissed: false,
|
|
75
|
+
picker: null,
|
|
76
|
+
models: [],
|
|
75
77
|
balance: null,
|
|
76
78
|
model: cfg.model || null,
|
|
77
79
|
theme: cfg.theme || "default",
|
|
@@ -82,18 +84,22 @@ function initialState() {
|
|
|
82
84
|
|
|
83
85
|
/* ------------------------------------------------------------------ layout */
|
|
84
86
|
|
|
85
|
-
function headerLines(state, width, rows) {
|
|
86
|
-
if (state.messages.length) return [];
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
87
|
+
function headerLines(state, width, rows, footerCount, compact) {
|
|
88
|
+
if (state.messages.length || state.picker) return [];
|
|
89
|
+
|
|
90
|
+
const art = mascot.split("\n").map((line) => chalk.white(line));
|
|
91
|
+
const space = rows - 2 - footerCount - (compact ? 0 : 1);
|
|
92
|
+
|
|
93
|
+
if (space >= art.length + 4) {
|
|
94
|
+
return [...art, "", center(chalk.bold("O que voce quer fazer?"), width),
|
|
95
|
+
center(chalk.dim("Use / para comandos | Enter envia | Ctrl+C sai"), width), ""];
|
|
96
|
+
}
|
|
97
|
+
if (space >= art.length + 2) {
|
|
98
|
+
return [...art, "", center(chalk.bold("O que voce quer fazer?"), width), ""];
|
|
99
|
+
}
|
|
100
|
+
if (space >= art.length + 1) return [...art, ""];
|
|
101
|
+
if (space >= art.length) return art;
|
|
102
|
+
return art.slice(0, Math.max(0, space));
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
function messageLines(state, width) {
|
|
@@ -109,12 +115,7 @@ function messageLines(state, width) {
|
|
|
109
115
|
} else if (message.role === "assistant") {
|
|
110
116
|
lines.push("");
|
|
111
117
|
for (const line of wrap(message.text, width - 2)) lines.push(" " + line);
|
|
112
|
-
const meta =
|
|
113
|
-
message.ms ? `${(message.ms / 1000).toFixed(1)}s` : null,
|
|
114
|
-
message.provider,
|
|
115
|
-
]
|
|
116
|
-
.filter(Boolean)
|
|
117
|
-
.join(" | ");
|
|
118
|
+
const meta = message.ms ? `${(message.ms / 1000).toFixed(1)}s` : "";
|
|
118
119
|
if (meta) lines.push(chalk.dim(" " + meta));
|
|
119
120
|
} else {
|
|
120
121
|
lines.push("");
|
|
@@ -130,11 +131,15 @@ function messageLines(state, width) {
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
function filteredCommands(state, commands) {
|
|
134
|
+
if (state.menuDismissed) return [];
|
|
133
135
|
if (!state.input.startsWith("/") || state.input.includes(" ")) return [];
|
|
134
136
|
const query = state.input.toLowerCase();
|
|
135
|
-
return Object.entries(commands)
|
|
136
|
-
name.toLowerCase().startsWith(query)
|
|
137
|
-
|
|
137
|
+
return Object.entries(commands)
|
|
138
|
+
.filter(([name]) => name.toLowerCase().startsWith(query))
|
|
139
|
+
.sort(
|
|
140
|
+
(a, b) =>
|
|
141
|
+
Number(b[0].toLowerCase() === query) - Number(a[0].toLowerCase() === query)
|
|
142
|
+
);
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
function menuLines(state, commands, width) {
|
|
@@ -147,8 +152,33 @@ function menuLines(state, commands, width) {
|
|
|
147
152
|
});
|
|
148
153
|
}
|
|
149
154
|
|
|
155
|
+
function pickerLines(state, width) {
|
|
156
|
+
const picker = state.picker;
|
|
157
|
+
if (!picker) return [];
|
|
158
|
+
|
|
159
|
+
const lines = [
|
|
160
|
+
chalk.bold(" Escolha o modelo"),
|
|
161
|
+
chalk.dim(" ↑/↓ navega | Enter seleciona | Esc cancela"),
|
|
162
|
+
"",
|
|
163
|
+
];
|
|
164
|
+
if (picker.loading) return [...lines, chalk.dim(" carregando modelos..."), ""];
|
|
165
|
+
|
|
166
|
+
const height = 6;
|
|
167
|
+
const start = clamp(picker.index - Math.floor(height / 2), 0, Math.max(0, picker.items.length - height));
|
|
168
|
+
picker.items.slice(start, start + height).forEach((item, offset) => {
|
|
169
|
+
const real = start + offset;
|
|
170
|
+
const price = `in ${(item.input / 1_000_000).toFixed(2)} out ${(item.output / 1_000_000).toFixed(2)} R$/1M`;
|
|
171
|
+
const available = Math.max(20, width - price.length - 2);
|
|
172
|
+
const label = padRight(truncate(` ${real === picker.index ? ">" : " "} ${item.id}`, available), available);
|
|
173
|
+
const line = label + " " + chalk.dim(price);
|
|
174
|
+
lines.push(real === picker.index ? chalk.inverse(padRight(line, width)) : line);
|
|
175
|
+
});
|
|
176
|
+
lines.push("");
|
|
177
|
+
return lines;
|
|
178
|
+
}
|
|
179
|
+
|
|
150
180
|
function footerLines(state, width) {
|
|
151
|
-
const name =
|
|
181
|
+
const name = `kafox ${state.version}`;
|
|
152
182
|
const left = chalk.dim(truncate(`${name} | ${state.model || "auto"}`, width - 16));
|
|
153
183
|
const right = chalk.green(
|
|
154
184
|
state.balance == null ? "saldo --" : `R$ ${(state.balance / 1_000_000).toFixed(2)}`
|
|
@@ -165,10 +195,19 @@ function buildFrame(state, cols, rows, commands) {
|
|
|
165
195
|
const width = Math.min(cols - 2, 108);
|
|
166
196
|
const margin = Math.floor((cols - width) / 2);
|
|
167
197
|
const gutter = " ".repeat(Math.max(0, margin));
|
|
198
|
+
const compact = rows < 34;
|
|
168
199
|
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
|
|
200
|
+
const footer = footerLines(state, width).slice(0, compact ? 1 : 3);
|
|
201
|
+
const panel = state.picker
|
|
202
|
+
? pickerLines(state, width)
|
|
203
|
+
: menuLines(state, commands, width);
|
|
204
|
+
const header = headerLines(state, width, rows, footer.length, compact);
|
|
205
|
+
|
|
206
|
+
const reserved = 2 + footer.length + (compact ? 0 : 1) + panel.length;
|
|
207
|
+
const available = Math.max(0, rows - header.length - reserved);
|
|
208
|
+
let body = messageLines(state, width);
|
|
209
|
+
if (body.length > available) body = body.slice(body.length - available);
|
|
210
|
+
while (body.length < available) body.unshift("");
|
|
172
211
|
|
|
173
212
|
const inputWidth = width - 2;
|
|
174
213
|
const start = state.input.length > inputWidth
|
|
@@ -180,17 +219,20 @@ function buildFrame(state, cols, rows, commands) {
|
|
|
180
219
|
: chalk.cyan("> ") + chalk.dim(truncate("Pergunte alguma coisa...", inputWidth));
|
|
181
220
|
const rule = chalk.dim("-".repeat(width));
|
|
182
221
|
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
222
|
+
const middle = compact ? [] : [""];
|
|
223
|
+
const raw = [
|
|
224
|
+
...header,
|
|
225
|
+
...body,
|
|
226
|
+
...middle,
|
|
227
|
+
...panel,
|
|
228
|
+
inputLine,
|
|
229
|
+
rule,
|
|
230
|
+
...footer,
|
|
231
|
+
];
|
|
190
232
|
while (raw.length < rows) raw.push("");
|
|
191
233
|
const lines = raw.slice(0, rows).map((line) => gutter + padRight(line, width));
|
|
192
234
|
|
|
193
|
-
const inputRow = header.length + available +
|
|
235
|
+
const inputRow = header.length + available + middle.length + panel.length + 1;
|
|
194
236
|
const cursorCol = Math.max(1, margin + 2 + (state.cursor - start) + 1);
|
|
195
237
|
return { lines, cursorRow: Math.min(rows, inputRow), cursorCol };
|
|
196
238
|
}
|
|
@@ -247,6 +289,46 @@ async function runTUI({ commands = {}, onCommand, interactiveCommands = new Set(
|
|
|
247
289
|
return collected.join("\n");
|
|
248
290
|
}
|
|
249
291
|
|
|
292
|
+
async function loadModels() {
|
|
293
|
+
if (state.models.length) return state.models;
|
|
294
|
+
const pricing = await fetchApi("/api/public/pricing")
|
|
295
|
+
.then((res) => res.json())
|
|
296
|
+
.catch(() => null);
|
|
297
|
+
state.models = (pricing?.pricing || []).map((row) => ({
|
|
298
|
+
id: row.model,
|
|
299
|
+
input: row.input_micros_per_1m,
|
|
300
|
+
output: row.output_micros_per_1m,
|
|
301
|
+
}));
|
|
302
|
+
return state.models;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function openModelPicker() {
|
|
306
|
+
state.menuDismissed = true;
|
|
307
|
+
state.picker = { items: state.models, index: 0, loading: true };
|
|
308
|
+
render();
|
|
309
|
+
const models = await loadModels();
|
|
310
|
+
if (!models.length) {
|
|
311
|
+
state.picker = null;
|
|
312
|
+
state.messages.push({ role: "system", text: "Nao consegui carregar os modelos.", error: true });
|
|
313
|
+
return render();
|
|
314
|
+
}
|
|
315
|
+
const current = models.findIndex((item) => item.id === state.model);
|
|
316
|
+
state.picker = { items: models, index: current >= 0 ? current : 0, loading: false };
|
|
317
|
+
render();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function selectModel() {
|
|
321
|
+
const picker = state.picker;
|
|
322
|
+
if (!picker || picker.loading) return;
|
|
323
|
+
const item = picker.items[picker.index];
|
|
324
|
+
if (!item) return;
|
|
325
|
+
storage.setConfig({ model: item.id });
|
|
326
|
+
state.model = item.id;
|
|
327
|
+
state.picker = null;
|
|
328
|
+
state.messages.push({ role: "system", text: `Modelo: ${item.id}` });
|
|
329
|
+
render();
|
|
330
|
+
}
|
|
331
|
+
|
|
250
332
|
async function runOutside(line) {
|
|
251
333
|
state.suspended = true;
|
|
252
334
|
leaveScreen();
|
|
@@ -265,12 +347,14 @@ async function runTUI({ commands = {}, onCommand, interactiveCommands = new Set(
|
|
|
265
347
|
}
|
|
266
348
|
|
|
267
349
|
async function runCommand(line) {
|
|
268
|
-
const
|
|
350
|
+
const parts = line.trim().split(/\s+/);
|
|
351
|
+
const name = parts[0];
|
|
269
352
|
if (name === "/exit" || name === "/quit") return quit();
|
|
270
353
|
|
|
354
|
+
if (name === "/model" && !parts[1]) return openModelPicker();
|
|
355
|
+
|
|
271
356
|
if (name === "/clear") {
|
|
272
357
|
state.messages = [];
|
|
273
|
-
state.status = "";
|
|
274
358
|
return render();
|
|
275
359
|
}
|
|
276
360
|
if (name === "/compact") {
|
|
@@ -279,10 +363,7 @@ async function runTUI({ commands = {}, onCommand, interactiveCommands = new Set(
|
|
|
279
363
|
} else {
|
|
280
364
|
const total = state.messages.length;
|
|
281
365
|
state.messages = [
|
|
282
|
-
{
|
|
283
|
-
role: "system",
|
|
284
|
-
text: `Contexto compactado: ${total} mensagens resumidas.`,
|
|
285
|
-
},
|
|
366
|
+
{ role: "system", text: `Contexto compactado: ${total} mensagens resumidas.` },
|
|
286
367
|
...state.messages.slice(-6),
|
|
287
368
|
];
|
|
288
369
|
}
|
|
@@ -311,6 +392,7 @@ async function runTUI({ commands = {}, onCommand, interactiveCommands = new Set(
|
|
|
311
392
|
state.input = "";
|
|
312
393
|
state.cursor = 0;
|
|
313
394
|
state.menuIndex = 0;
|
|
395
|
+
state.menuDismissed = false;
|
|
314
396
|
|
|
315
397
|
if (line.startsWith("/")) return runCommand(line);
|
|
316
398
|
|
|
@@ -339,6 +421,7 @@ async function runTUI({ commands = {}, onCommand, interactiveCommands = new Set(
|
|
|
339
421
|
state.input = value;
|
|
340
422
|
state.cursor = cursor == null ? value.length : clamp(cursor, 0, value.length);
|
|
341
423
|
state.menuIndex = 0;
|
|
424
|
+
state.menuDismissed = false;
|
|
342
425
|
render();
|
|
343
426
|
}
|
|
344
427
|
|
|
@@ -346,10 +429,46 @@ async function runTUI({ commands = {}, onCommand, interactiveCommands = new Set(
|
|
|
346
429
|
if (state.suspended) return;
|
|
347
430
|
if (key.ctrl && key.name === "c") return quit();
|
|
348
431
|
|
|
432
|
+
// Seletor de modelo tem prioridade sobre o resto.
|
|
433
|
+
if (state.picker) {
|
|
434
|
+
if (state.picker.loading) return;
|
|
435
|
+
if (key.name === "up" || key.name === "down") {
|
|
436
|
+
const delta = key.name === "up" ? -1 : 1;
|
|
437
|
+
state.picker.index = clamp(
|
|
438
|
+
state.picker.index + delta,
|
|
439
|
+
0,
|
|
440
|
+
Math.max(0, state.picker.items.length - 1)
|
|
441
|
+
);
|
|
442
|
+
return render();
|
|
443
|
+
}
|
|
444
|
+
if (key.name === "return") return selectModel();
|
|
445
|
+
if (key.name === "escape") {
|
|
446
|
+
state.picker = null;
|
|
447
|
+
return render();
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
349
452
|
const items = filteredCommands(state, commands);
|
|
350
453
|
const menuOpen = items.length > 0;
|
|
351
454
|
|
|
352
|
-
if (key.name === "return")
|
|
455
|
+
if (key.name === "return") {
|
|
456
|
+
// Se o texto ja e um comando exato, ele vence a lista (ex.: /model vs /models).
|
|
457
|
+
const exact = Object.keys(commands).find(
|
|
458
|
+
(name) => name.toLowerCase() === state.input.trim().toLowerCase()
|
|
459
|
+
);
|
|
460
|
+
if (exact) {
|
|
461
|
+
state.input = exact;
|
|
462
|
+
state.cursor = exact.length;
|
|
463
|
+
} else if (menuOpen) {
|
|
464
|
+
// Enter executa o item destacado, nao o texto parcial digitado.
|
|
465
|
+
const selected = items[state.menuIndex % items.length][0];
|
|
466
|
+
state.input = selected;
|
|
467
|
+
state.cursor = selected.length;
|
|
468
|
+
state.menuIndex = 0;
|
|
469
|
+
}
|
|
470
|
+
return submit();
|
|
471
|
+
}
|
|
353
472
|
if (key.name === "tab") {
|
|
354
473
|
if (menuOpen) {
|
|
355
474
|
const selected = items[state.menuIndex % items.length];
|
|
@@ -358,7 +477,10 @@ async function runTUI({ commands = {}, onCommand, interactiveCommands = new Set(
|
|
|
358
477
|
return;
|
|
359
478
|
}
|
|
360
479
|
if (key.name === "escape") {
|
|
361
|
-
if (menuOpen)
|
|
480
|
+
if (menuOpen) {
|
|
481
|
+
state.menuDismissed = true;
|
|
482
|
+
return render();
|
|
483
|
+
}
|
|
362
484
|
return;
|
|
363
485
|
}
|
|
364
486
|
if (key.name === "up" || key.name === "down") {
|
|
@@ -374,12 +496,8 @@ async function runTUI({ commands = {}, onCommand, interactiveCommands = new Set(
|
|
|
374
496
|
if (next === state.history.length) return setInput("");
|
|
375
497
|
return setInput(state.history[next]);
|
|
376
498
|
}
|
|
377
|
-
if (key.name === "left")
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
if (key.name === "right") {
|
|
381
|
-
return setInput(state.input, state.cursor + 1);
|
|
382
|
-
}
|
|
499
|
+
if (key.name === "left") return setInput(state.input, state.cursor - 1);
|
|
500
|
+
if (key.name === "right") return setInput(state.input, state.cursor + 1);
|
|
383
501
|
if (key.name === "home") return setInput(state.input, 0);
|
|
384
502
|
if (key.name === "end") return setInput(state.input, state.input.length);
|
|
385
503
|
if (key.name === "backspace") {
|
|
@@ -450,4 +568,4 @@ async function runTUI({ commands = {}, onCommand, interactiveCommands = new Set(
|
|
|
450
568
|
return { state, quit, render, submit };
|
|
451
569
|
}
|
|
452
570
|
|
|
453
|
-
module.exports = { runTUI, buildFrame, wrap, initialState, filteredCommands };
|
|
571
|
+
module.exports = { runTUI, buildFrame, wrap, initialState, filteredCommands, pickerLines };
|