kafox-cli 0.9.0 → 0.9.5
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 +2 -2
- package/package.json +1 -1
- package/src/auth.js +2 -1
- package/src/gateway.js +20 -8
- package/src/storage.js +7 -2
- package/src/tui.js +37 -26
- package/src/wallpaper.js +35 -6
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.
|
|
15
|
+
npm install -g https://kafox.dev/static/downloads/kafox-cli-0.9.5.tgz
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Requisito: Node.js 22.5+.
|
|
@@ -24,7 +24,7 @@ kafox login
|
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
O terminal vira uma tela de login (arte grande em cima, link no centro): abra a
|
|
27
|
-
URL `https://kafox.
|
|
27
|
+
URL `https://kafox.dev/cli?code=...`, entre na sua conta e clique em
|
|
28
28
|
**Autorizar**. A sessao fica em `~/.kafox/token.json` (permissao 600). No primeiro
|
|
29
29
|
`kafox` sem sessao ele pergunta se quer entrar e abre a mesma tela.
|
|
30
30
|
|
package/package.json
CHANGED
package/src/auth.js
CHANGED
|
@@ -35,7 +35,8 @@ async function login() {
|
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// A pagina de autorizacao fica no site, nao no host da API.
|
|
39
|
+
const url = `${storage.getWebUrl()}/cli?code=${data.user_code}`;
|
|
39
40
|
|
|
40
41
|
if (process.stdout.isTTY && process.stdin.isTTY) {
|
|
41
42
|
return loginWithScreen({ url, data });
|
package/src/gateway.js
CHANGED
|
@@ -2,6 +2,14 @@ const chalk = require("chalk");
|
|
|
2
2
|
const storage = require("./storage");
|
|
3
3
|
const { ensureSession, fetchApi } = require("./auth");
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Modelo do free tier: o nome carrega o sufixo " (free)" (ou ":free"). Esses
|
|
7
|
+
* modelos gastam a cota diaria, nao o saldo, entao funcionam com saldo zero.
|
|
8
|
+
*/
|
|
9
|
+
function isFreeModel(model) {
|
|
10
|
+
return /(^free$|\s\(free\)$|:free$)/i.test(String(model || "").trim());
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
/**
|
|
6
14
|
* Garante sessao, saldo, modelo e chave antes de qualquer chamada.
|
|
7
15
|
* A chave `kafox_live_...` e da propria conta: o gateway cobra por ela.
|
|
@@ -18,13 +26,6 @@ async function ensureReady(options = {}) {
|
|
|
18
26
|
}
|
|
19
27
|
const account = await boot.json().catch(() => null);
|
|
20
28
|
const balance = Number(account?.user?.balance_micros || 0);
|
|
21
|
-
if (balance <= 0) {
|
|
22
|
-
return {
|
|
23
|
-
ok: false,
|
|
24
|
-
code: "no_balance",
|
|
25
|
-
error: "Sem saldo para responder. Recarregue em https://kafox.pypag.com/dashboard",
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
29
|
|
|
29
30
|
let model = options.model || storage.getConfig().model;
|
|
30
31
|
if (!model) {
|
|
@@ -35,6 +36,17 @@ async function ensureReady(options = {}) {
|
|
|
35
36
|
if (!model) return { ok: false, error: "Could not load model catalog." };
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
// Free tier nao desconta saldo: com a conta zerada o modelo (free) responde.
|
|
40
|
+
if (balance <= 0 && !isFreeModel(model)) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
code: "no_balance",
|
|
44
|
+
error:
|
|
45
|
+
`Sem saldo para responder. Recarregue em ${storage.getWebUrl()}/dashboard ` +
|
|
46
|
+
"ou use um modelo (free): `kafox model`.",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
let keyCreated = false;
|
|
39
51
|
let apiKey = storage.getConfig().api_key;
|
|
40
52
|
if (!apiKey) {
|
|
@@ -235,7 +247,6 @@ async function attemptChat({ messages, model, tools, onDelta, onReasoning, signa
|
|
|
235
247
|
ms: Date.now() - started,
|
|
236
248
|
model: ready.model,
|
|
237
249
|
keyCreated: ready.keyCreated,
|
|
238
|
-
provider: response.headers?.get?.("x-kafox-provider") || "",
|
|
239
250
|
};
|
|
240
251
|
}
|
|
241
252
|
|
|
@@ -307,6 +318,7 @@ module.exports = {
|
|
|
307
318
|
askModel,
|
|
308
319
|
chat,
|
|
309
320
|
ensureReady,
|
|
321
|
+
isFreeModel,
|
|
310
322
|
mergeToolCalls,
|
|
311
323
|
backoffMs,
|
|
312
324
|
RETRY_MAX,
|
package/src/storage.js
CHANGED
|
@@ -28,7 +28,12 @@ function writeJSON(file, data) {
|
|
|
28
28
|
|
|
29
29
|
function getApiUrl() {
|
|
30
30
|
const cfg = readJSON(CONFIG_FILE, {});
|
|
31
|
-
return cfg.api_url || process.env.KAFOX_API_URL || "https://kafox.
|
|
31
|
+
return cfg.api_url || process.env.KAFOX_API_URL || "https://backend.kafox.dev";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Endereco do site (painel, login, documentacao) — a API mora em outro host. */
|
|
35
|
+
function getWebUrl() {
|
|
36
|
+
return process.env.KAFOX_WEB_URL || "https://kafox.dev";
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
function getToken() {
|
|
@@ -87,6 +92,6 @@ function parseCookies(headers) {
|
|
|
87
92
|
module.exports = {
|
|
88
93
|
DIR, TOKEN_FILE, CONFIG_FILE,
|
|
89
94
|
ensureDir, readJSON, writeJSON,
|
|
90
|
-
getApiUrl, getToken, setToken, clearToken,
|
|
95
|
+
getApiUrl, getWebUrl, getToken, setToken, clearToken,
|
|
91
96
|
getConfig, setConfig, getAuthHeaders, parseCookies,
|
|
92
97
|
};
|
package/src/tui.js
CHANGED
|
@@ -817,10 +817,10 @@ function messageLines(state, width) {
|
|
|
817
817
|
} else if (message.role === "assistant") {
|
|
818
818
|
lines.push("");
|
|
819
819
|
lines.push(...markdownLines(message.text, width - 2, state.theme));
|
|
820
|
+
// Só latência: a rota usada por trás é da Kafox, não do cliente.
|
|
820
821
|
const meta = [];
|
|
821
822
|
if (message.interrupted) meta.push("interrompido");
|
|
822
823
|
if (message.ms) meta.push(`${(message.ms / 1000).toFixed(1)}s`);
|
|
823
|
-
if (message.provider) meta.push(message.provider);
|
|
824
824
|
if (meta.length) lines.push("");
|
|
825
825
|
if (meta.length) lines.push(indent(2) + chalk.dim(meta.join(" · ")));
|
|
826
826
|
} else if (message.role === "thinking") {
|
|
@@ -975,9 +975,6 @@ function modelLine(state, width) {
|
|
|
975
975
|
let label = `${model} · kafox v${state.version}`;
|
|
976
976
|
if (state.escArmed) {
|
|
977
977
|
label = `${model} · Esc de novo para parar`;
|
|
978
|
-
} else if (state.retry) {
|
|
979
|
-
const left = Math.max(1, Math.ceil(((state.retry.endsAt || 0) - Date.now()) / 1000));
|
|
980
|
-
label = `${model} · reconectando em ${left}s (tentativa ${state.retry.attempt}/${state.retry.maxAttempts})`;
|
|
981
978
|
}
|
|
982
979
|
return paintPanel(
|
|
983
980
|
state.theme,
|
|
@@ -994,14 +991,8 @@ function inputBlock(state, width, { hero = false, tight = false } = {}) {
|
|
|
994
991
|
const shown = state.input.slice(start, start + available);
|
|
995
992
|
|
|
996
993
|
const prompt = chalk.cyan("▌ ");
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
body = prompt + shown;
|
|
1000
|
-
} else {
|
|
1001
|
-
// Enquanto a IA trabalha os pontinhos correm; parada, a caixa fica limpa.
|
|
1002
|
-
const placeholder = state.busy ? `trabalhando${".".repeat(state.frame % 4)}` : "";
|
|
1003
|
-
body = prompt + chalk.dim(truncate(placeholder, available));
|
|
1004
|
-
}
|
|
994
|
+
// A caixa fica sempre limpa: o "trabalhando" ja aparece na conversa, acima.
|
|
995
|
+
const body = state.input ? prompt + shown : prompt;
|
|
1005
996
|
|
|
1006
997
|
const lines = [
|
|
1007
998
|
rule(state, width),
|
|
@@ -1029,16 +1020,27 @@ function statusLines(state, width, { bare = false } = {}) {
|
|
|
1029
1020
|
return [left + gap + right];
|
|
1030
1021
|
}
|
|
1031
1022
|
|
|
1023
|
+
/**
|
|
1024
|
+
* Linha de status do rodape. A lista fixa de atalhos saiu: agora o espaco e
|
|
1025
|
+
* usado para avisar problema de conexao (em vermelho) e posicao da rolagem.
|
|
1026
|
+
*/
|
|
1032
1027
|
function hintsLines(state, width, { bare = false } = {}) {
|
|
1033
1028
|
if (bare) return [];
|
|
1034
|
-
|
|
1035
|
-
if (state.
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1029
|
+
|
|
1030
|
+
if (state.retry) {
|
|
1031
|
+
const left = Math.max(1, Math.ceil(((state.retry.endsAt || 0) - Date.now()) / 1000));
|
|
1032
|
+
const attempt = state.retry.maxAttempts
|
|
1033
|
+
? ` (tentativa ${state.retry.attempt}/${state.retry.maxAttempts})`
|
|
1034
|
+
: "";
|
|
1035
|
+
const label = truncate(`⚠ ${state.retry.error || "fetch failed"} · reconectando em ${left}s${attempt}`, width);
|
|
1036
|
+
return [chalk.red(label)];
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (state.scroll > 0) {
|
|
1040
|
+
return [chalk.dim(truncate(`↑ ${state.scroll} linhas`, width))];
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
return [];
|
|
1042
1044
|
}
|
|
1043
1045
|
|
|
1044
1046
|
/* ------------------------------------------------------------------ overlay */
|
|
@@ -1466,7 +1468,12 @@ async function runTUI({
|
|
|
1466
1468
|
const rows = Math.max(14, stdout.rows || 24);
|
|
1467
1469
|
const frame = buildFrame(state, cols, rows, commands);
|
|
1468
1470
|
const grid = wallpaperGrid(cols, rows);
|
|
1469
|
-
if (grid)
|
|
1471
|
+
if (grid) {
|
|
1472
|
+
// A tela inteira vem pintada com o bg do tema; so essa cor e tratada como
|
|
1473
|
+
// area livre para a foto. Paineis e caixas (outra cor) continuam solidos.
|
|
1474
|
+
const base = wallpaper.hexToRgb(themeColors(state.theme).bg);
|
|
1475
|
+
frame.lines = wallpaper.paint(frame.lines, grid, { base });
|
|
1476
|
+
}
|
|
1470
1477
|
paintFrame(frame, { cols, rows });
|
|
1471
1478
|
}
|
|
1472
1479
|
|
|
@@ -1477,17 +1484,18 @@ async function runTUI({
|
|
|
1477
1484
|
function wallpaperGrid(cols, rows) {
|
|
1478
1485
|
const bg = state.wallpaper;
|
|
1479
1486
|
if (!bg?.image) return null;
|
|
1480
|
-
if (!bg.grid || bg.cols !== cols || bg.rows !== rows) {
|
|
1487
|
+
if (!bg.grid || bg.cols !== cols || bg.rows !== rows || bg.theme !== state.theme) {
|
|
1481
1488
|
const base = wallpaper.hexToRgb(themeColors(state.theme).bg) || [12, 12, 16];
|
|
1482
1489
|
bg.grid = wallpaper.buildGrid(bg.image, cols, rows, { dim: 0.45, base });
|
|
1483
1490
|
bg.cols = cols;
|
|
1484
1491
|
bg.rows = rows;
|
|
1492
|
+
bg.theme = state.theme;
|
|
1485
1493
|
}
|
|
1486
1494
|
return bg.grid;
|
|
1487
1495
|
}
|
|
1488
1496
|
|
|
1489
1497
|
/** `/background <arquivo|url>` desenha a foto atras da conversa. */
|
|
1490
|
-
async function setBackground(source) {
|
|
1498
|
+
async function setBackground(source, { quiet = false } = {}) {
|
|
1491
1499
|
const value = String(source || "").trim();
|
|
1492
1500
|
if (!value || ["off", "none", "0", "limpar"].includes(value.toLowerCase())) {
|
|
1493
1501
|
state.wallpaper = null;
|
|
@@ -1505,7 +1513,11 @@ async function runTUI({
|
|
|
1505
1513
|
storage.setConfig({ background: value });
|
|
1506
1514
|
} catch (error) {
|
|
1507
1515
|
state.wallpaper = null;
|
|
1508
|
-
|
|
1516
|
+
// No inicio da sessao o fundo salvo pode falhar (offline, arquivo movido):
|
|
1517
|
+
// nao vale abrir um modal por isso, so avisa quando o usuario pediu.
|
|
1518
|
+
if (!quiet) {
|
|
1519
|
+
state.modal = { title: "Fundo", text: `Nao consegui carregar ${value}.\n${error.message}` };
|
|
1520
|
+
}
|
|
1509
1521
|
}
|
|
1510
1522
|
return render();
|
|
1511
1523
|
}
|
|
@@ -1943,7 +1955,6 @@ async function runTUI({
|
|
|
1943
1955
|
role: "assistant",
|
|
1944
1956
|
text,
|
|
1945
1957
|
ms: result.ms,
|
|
1946
|
-
provider: result.provider || "",
|
|
1947
1958
|
});
|
|
1948
1959
|
}
|
|
1949
1960
|
if (result.messages) state.context = result.messages;
|
|
@@ -2345,7 +2356,7 @@ async function runTUI({
|
|
|
2345
2356
|
if (resumePick) openChatPicker();
|
|
2346
2357
|
connectMcp().catch(() => {});
|
|
2347
2358
|
const savedBackground = storage.getConfig().background;
|
|
2348
|
-
if (savedBackground) setBackground(savedBackground).catch(() => {});
|
|
2359
|
+
if (savedBackground) setBackground(savedBackground, { quiet: true }).catch(() => {});
|
|
2349
2360
|
|
|
2350
2361
|
spinner = setInterval(() => {
|
|
2351
2362
|
if (!state.busy || closed) return;
|
package/src/wallpaper.js
CHANGED
|
@@ -312,11 +312,39 @@ function trackStyle(sequence, style) {
|
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
|
|
316
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Cor de fundo ativa no momento. O tema pinta a tela inteira com o proprio bg,
|
|
317
|
+
* entao essa cor conta como "fundo livre": a imagem pode entrar no lugar dela.
|
|
318
|
+
* Caixas e paineis usam outra cor e ficam intactos.
|
|
319
|
+
*/
|
|
320
|
+
function activeBackground(style) {
|
|
321
|
+
let bg = null;
|
|
322
|
+
for (let index = 0; index < style.length; index += 1) {
|
|
323
|
+
const part = style[index];
|
|
324
|
+
if (part === "48" && style[index + 1] === "2") {
|
|
325
|
+
bg = [Number(style[index + 2]), Number(style[index + 3]), Number(style[index + 4])];
|
|
326
|
+
index += 4;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (part === "48") {
|
|
330
|
+
// 48;5;n (paleta) ou 48;... qualquer outra variante: nao e o bg do tema.
|
|
331
|
+
bg = "outro";
|
|
332
|
+
index += 1;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
317
335
|
const code = Number(part);
|
|
318
|
-
|
|
319
|
-
}
|
|
336
|
+
if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107)) bg = "outro";
|
|
337
|
+
}
|
|
338
|
+
return bg;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Aceita o espaco quando nao ha fundo ou quando o fundo e o bg do tema. */
|
|
342
|
+
function backgroundFree(style, base) {
|
|
343
|
+
const bg = activeBackground(style);
|
|
344
|
+
if (!bg) return true;
|
|
345
|
+
if (bg === "outro") return false;
|
|
346
|
+
if (!base) return false;
|
|
347
|
+
return bg[0] === base[0] && bg[1] === base[1] && bg[2] === base[2];
|
|
320
348
|
}
|
|
321
349
|
|
|
322
350
|
function cellEscape({ top, bottom }) {
|
|
@@ -327,7 +355,7 @@ function cellEscape({ top, bottom }) {
|
|
|
327
355
|
* Troca os espacos vazios do frame pelas celulas da imagem. Texto e celulas com
|
|
328
356
|
* fundo proprio (caixas, paineis) ficam como estao.
|
|
329
357
|
*/
|
|
330
|
-
function paint(lines, grid) {
|
|
358
|
+
function paint(lines, grid, { base = null } = {}) {
|
|
331
359
|
if (!grid?.cells?.length) return lines;
|
|
332
360
|
|
|
333
361
|
return lines.map((line, row) => {
|
|
@@ -353,7 +381,7 @@ function paint(lines, grid) {
|
|
|
353
381
|
|
|
354
382
|
const char = line[index];
|
|
355
383
|
const cell = cells[col];
|
|
356
|
-
if (char === " " && cell &&
|
|
384
|
+
if (char === " " && cell && backgroundFree(style, base)) {
|
|
357
385
|
const restore = style.length ? `\x1b[${style.join(";")}m` : "";
|
|
358
386
|
out += `\x1b[0m${cellEscape(cell)}${HALF_BLOCK}${restore}`;
|
|
359
387
|
} else {
|
|
@@ -373,6 +401,7 @@ module.exports = {
|
|
|
373
401
|
decodePng,
|
|
374
402
|
buildGrid,
|
|
375
403
|
paint,
|
|
404
|
+
activeBackground,
|
|
376
405
|
coverRegion,
|
|
377
406
|
hexToRgb,
|
|
378
407
|
HALF_BLOCK,
|