kafox-cli 0.9.8 → 0.10.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/bin/kafox.js +78 -0
- package/package.json +18 -1
- package/src/agent.js +24 -0
- package/src/auth.js +25 -6
- package/src/cli.js +3 -0
- package/src/compact.js +346 -0
- package/src/gateway.js +119 -53
- package/src/mobile/auth.js +352 -0
- package/src/mobile/bridge.js +285 -0
- package/src/mobile/index.js +538 -0
- package/src/mobile/protocol.js +51 -0
- package/src/mobile/qr.js +487 -0
- package/src/mobile/server.js +297 -0
- package/src/mobile/tui-bridge.js +289 -0
- package/src/mobile/view.js +734 -0
- package/src/models.js +19 -0
- package/src/providers-cli.js +82 -0
- package/src/providers.js +87 -0
- package/src/storage.js +39 -0
- package/src/tools.js +13 -2
- package/src/tui.js +549 -44
package/src/models.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
const chalk = require("chalk");
|
|
2
2
|
const storage = require("./storage");
|
|
3
3
|
const { ensureSession, fetchApi } = require("./auth");
|
|
4
|
+
const { listProviders } = require("./providers");
|
|
4
5
|
|
|
5
6
|
function fmtBRL(micros) {
|
|
6
7
|
return `${(micros / 1_000_000).toFixed(2)} R$`;
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
function fmtPrice(value) {
|
|
11
|
+
return value === null || value === undefined ? "—" : `R$ ${value.toFixed(2)}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
async function listModels() {
|
|
10
15
|
const res = await fetchApi("/api/public/pricing");
|
|
11
16
|
if (!res.ok) {
|
|
@@ -19,6 +24,20 @@ async function listModels() {
|
|
|
19
24
|
console.log(` ${chalk.bold(p.model)}`);
|
|
20
25
|
}
|
|
21
26
|
console.log(chalk.dim(`\nGateway: ${data.gateway_url}`));
|
|
27
|
+
|
|
28
|
+
// Provedores alternativos (OpenAI-compatible) tem catalogo local com precos.
|
|
29
|
+
for (const provider of listProviders()) {
|
|
30
|
+
if (!provider.models?.length) continue;
|
|
31
|
+
console.log(chalk.cyan(`\n${provider.label} models (${provider.baseUrl}):\n`));
|
|
32
|
+
for (const model of provider.models) {
|
|
33
|
+
console.log(
|
|
34
|
+
` ${chalk.bold(`${provider.id}/${model.id}`)} ${chalk.dim(
|
|
35
|
+
`in ${fmtPrice(model.brl.in)} · out ${fmtPrice(model.brl.out)} · cache ${fmtPrice(model.brl.cache)}`
|
|
36
|
+
)}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
console.log("");
|
|
22
41
|
}
|
|
23
42
|
|
|
24
43
|
async function showBalance() {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const chalk = require("chalk");
|
|
2
|
+
const storage = require("./storage");
|
|
3
|
+
const { listProviders, getProvider } = require("./providers");
|
|
4
|
+
|
|
5
|
+
function fmtBrl(value) {
|
|
6
|
+
return value === null || value === undefined ? "—" : `R$ ${value.toFixed(2)}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Imprime a tabela de modelos de um provedor, em BRL por 1M tokens. */
|
|
10
|
+
function printModels(provider) {
|
|
11
|
+
const models = provider.models || [];
|
|
12
|
+
if (!models.length) {
|
|
13
|
+
console.log(chalk.dim(" (sem catálogo local)"));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
console.log(chalk.bold(`\n ${provider.label} ${chalk.dim(provider.baseUrl || "")}`));
|
|
17
|
+
console.log(
|
|
18
|
+
chalk.dim(
|
|
19
|
+
` ${"modelo".padEnd(44)} ${"in".padStart(9)} ${"out".padStart(9)} ${"cache".padStart(9)} ctx`
|
|
20
|
+
)
|
|
21
|
+
);
|
|
22
|
+
for (const model of models) {
|
|
23
|
+
const id = model.id.length > 44 ? `${model.id.slice(0, 41)}...` : model.id;
|
|
24
|
+
const tools = model.tools ? "" : chalk.dim(" (sem tools)");
|
|
25
|
+
console.log(
|
|
26
|
+
` ${id.padEnd(44)} ${fmtBrl(model.brl.in).padStart(9)} ${fmtBrl(model.brl.out).padStart(9)} ${fmtBrl(
|
|
27
|
+
model.brl.cache
|
|
28
|
+
).padStart(9)} ${model.ctx}${tools}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `kafox provider` — lista provedores; `kafox provider <id> <chave>` guarda a
|
|
35
|
+
* chave; `kafox provider <id> --off` remove. A chave fica em
|
|
36
|
+
* ~/.kafox/config.json (provider_keys) ou na env `PROVIDER_API_KEY`.
|
|
37
|
+
*/
|
|
38
|
+
async function runProvider({ provider, chave, off, list } = {}) {
|
|
39
|
+
if (!provider || list) {
|
|
40
|
+
console.log(chalk.cyan("\nProvedores:\n"));
|
|
41
|
+
for (const item of listProviders()) {
|
|
42
|
+
const key = item.id === "kafox" ? "(conta kafox)" : storage.getProviderKey(item.id) ? "chave ok" : "sem chave";
|
|
43
|
+
console.log(` ${chalk.bold(item.id.padEnd(8))} ${item.label.padEnd(10)} ${chalk.dim(key)}`);
|
|
44
|
+
}
|
|
45
|
+
for (const item of listProviders()) {
|
|
46
|
+
if (item.models?.length) printModels(item);
|
|
47
|
+
}
|
|
48
|
+
console.log(
|
|
49
|
+
chalk.dim("\nUse `kafox provider <id> <chave>` para registrar. Modelo no chat: `<id>/<modelo>`.\n")
|
|
50
|
+
);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const target = getProvider(provider);
|
|
55
|
+
if (!target || target.id === "kafox") {
|
|
56
|
+
console.log(chalk.red(`\nProvedor desconhecido: ${provider}`));
|
|
57
|
+
console.log(chalk.dim("Veja `kafox provider` para a lista.\n"));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (off) {
|
|
62
|
+
storage.setProviderKey(target.id, null);
|
|
63
|
+
console.log(chalk.green(`\nChave de ${target.label} removida.\n`));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!chave) {
|
|
68
|
+
const current = storage.getProviderKey(target.id);
|
|
69
|
+
console.log(
|
|
70
|
+
current
|
|
71
|
+
? chalk.dim(`\n${target.label}: chave já salva. Use \`kafox provider ${target.id} <nova>\` para trocar.\n`)
|
|
72
|
+
: chalk.yellow(`\nInforme a chave: kafox provider ${target.id} <chave>\n`)
|
|
73
|
+
);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
storage.setProviderKey(target.id, chave.trim());
|
|
78
|
+
console.log(chalk.green(`\nChave de ${target.label} salva.`));
|
|
79
|
+
console.log(chalk.dim(`Use os modelos com: kafox --model ${target.id}/${target.models?.[0]?.id || "modelo"}\n`));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { runProvider, printModels };
|
package/src/providers.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalogo de provedores do kafox.
|
|
3
|
+
*
|
|
4
|
+
* O caminho normal e o proprio gateway kafox (`kafox`), servido em
|
|
5
|
+
* `${api_url}/api/chat`. Os modelos "sem censura" e os pagos tambem passam por
|
|
6
|
+
* ele: quem atende por baixo e assunto do backend, e o cliente nunca ve a chave
|
|
7
|
+
* nem o provedor de origem.
|
|
8
|
+
*
|
|
9
|
+
* A estrutura abaixo continua generica para provedores alternativos
|
|
10
|
+
* OpenAI-compatible: se um dia alguem registrar um, a CLI chama
|
|
11
|
+
* `{base_url}/chat/completions` direto, com a chave do usuario. Hoje so existe
|
|
12
|
+
* o kafox, entao nada disso e usado no fluxo padrao.
|
|
13
|
+
*
|
|
14
|
+
* Cada modelo guarda o preco em USD (fonte) e em BRL (o que a gente cobra).
|
|
15
|
+
* Regra da casa: BRL = USD * USD_BRL * MARKUP, arredondado a 2 casas.
|
|
16
|
+
*/
|
|
17
|
+
const USD_BRL = 5.35;
|
|
18
|
+
const MARKUP = 2;
|
|
19
|
+
|
|
20
|
+
/** Converte um preco em USD por 1M tokens para o preco cobrado em BRL. */
|
|
21
|
+
function usdToBrl(usd) {
|
|
22
|
+
if (usd === null || usd === undefined) return null;
|
|
23
|
+
return Math.round(usd * USD_BRL * MARKUP * 100) / 100;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const PROVIDERS = {
|
|
27
|
+
kafox: {
|
|
28
|
+
id: "kafox",
|
|
29
|
+
label: "Kafox",
|
|
30
|
+
kind: "kafox",
|
|
31
|
+
baseUrl: null, // resolvido do api_url em ~/.kafox/config.json
|
|
32
|
+
envKey: "KAFOX_API_KEY",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Lista os provedores (com os modelos, quando conhecidos offline). */
|
|
37
|
+
function listProviders() {
|
|
38
|
+
return Object.values(PROVIDERS);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getProvider(id) {
|
|
42
|
+
return PROVIDERS[id] || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Descobre a qual provedor um modelo pertence, olhando os ids conhecidos.
|
|
47
|
+
* Prefixo explicito "provedor/modelo" (ex.: "acme/modelo-x") vence.
|
|
48
|
+
*/
|
|
49
|
+
function providerForModel(model) {
|
|
50
|
+
const raw = String(model || "").trim();
|
|
51
|
+
if (!raw) return { provider: PROVIDERS.kafox, model: raw };
|
|
52
|
+
const slash = raw.indexOf("/");
|
|
53
|
+
if (slash > 0) {
|
|
54
|
+
const candidate = PROVIDERS[raw.slice(0, slash)];
|
|
55
|
+
if (candidate) return { provider: candidate, model: raw.slice(slash + 1) };
|
|
56
|
+
}
|
|
57
|
+
for (const provider of Object.values(PROVIDERS)) {
|
|
58
|
+
if ((provider.models || []).some((item) => item.id === raw)) {
|
|
59
|
+
return { provider, model: raw };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { provider: PROVIDERS.kafox, model: raw };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Ficha do modelo conhecido (nome, precos, contexto) ou null. */
|
|
66
|
+
function findModel(model) {
|
|
67
|
+
const { provider, model: bare } = providerForModel(model);
|
|
68
|
+
return (provider.models || []).find((item) => item.id === bare) || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Preco cobrado do modelo, em BRL por 1M tokens. */
|
|
72
|
+
function modelCost(model) {
|
|
73
|
+
const info = findModel(model);
|
|
74
|
+
return info ? info.brl : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
USD_BRL,
|
|
79
|
+
MARKUP,
|
|
80
|
+
usdToBrl,
|
|
81
|
+
PROVIDERS,
|
|
82
|
+
listProviders,
|
|
83
|
+
getProvider,
|
|
84
|
+
providerForModel,
|
|
85
|
+
findModel,
|
|
86
|
+
modelCost,
|
|
87
|
+
};
|
package/src/storage.js
CHANGED
|
@@ -31,6 +31,22 @@ function getApiUrl() {
|
|
|
31
31
|
return cfg.api_url || process.env.KAFOX_API_URL || "https://backend.kafox.dev";
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Hosts alternativos para a API, na ordem de tentativa. Todos apontam para o
|
|
36
|
+
* MESMO backend (o Caddy serve backend.kafox.dev e back.kafox.dev iguais).
|
|
37
|
+
* Serve para o login nao morrer quando um nome estiver com DNS/cache ruim:
|
|
38
|
+
* se o primeiro der timeout/conexao recusada, tenta o proximo.
|
|
39
|
+
*/
|
|
40
|
+
function apiUrlCandidates() {
|
|
41
|
+
const primary = getApiUrl();
|
|
42
|
+
const list = [primary];
|
|
43
|
+
// Fallbacks conhecidos do mesmo backend (so quando o host e kafox.dev).
|
|
44
|
+
if (/\/\/backend\.kafox\.dev/.test(primary)) list.push(primary.replace("backend.kafox.dev", "back.kafox.dev"));
|
|
45
|
+
if (/\/\/backend\.kafox\.dev/.test(primary)) list.push(primary.replace("backend.kafox.dev", "api.kafox.dev"));
|
|
46
|
+
// Dedup preservando a ordem.
|
|
47
|
+
return [...new Set(list)];
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
/** Endereco do site (painel, login, documentacao) — a API mora em outro host. */
|
|
35
51
|
function getWebUrl() {
|
|
36
52
|
return process.env.KAFOX_WEB_URL || "https://kafox.dev";
|
|
@@ -51,6 +67,27 @@ function clearApiKey() {
|
|
|
51
67
|
writeJSON(CONFIG_FILE, cfg);
|
|
52
68
|
}
|
|
53
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Chaves de provedores alternativos: { <id>: "<chave>" }.
|
|
72
|
+
* O provedor padrao kafox usa `api_key` (criada pelo backend), nao entra aqui.
|
|
73
|
+
*/
|
|
74
|
+
function getProviderKeys() {
|
|
75
|
+
const cfg = readJSON(CONFIG_FILE, {});
|
|
76
|
+
return { ...(cfg.provider_keys || {}) };
|
|
77
|
+
}
|
|
78
|
+
function getProviderKey(provider) {
|
|
79
|
+
const env = process.env[`${String(provider || "").toUpperCase()}_API_KEY`];
|
|
80
|
+
if (env) return env;
|
|
81
|
+
return getProviderKeys()[provider] || null;
|
|
82
|
+
}
|
|
83
|
+
function setProviderKey(provider, key) {
|
|
84
|
+
const cfg = readJSON(CONFIG_FILE, {});
|
|
85
|
+
const keys = { ...(cfg.provider_keys || {}) };
|
|
86
|
+
if (key) keys[provider] = key;
|
|
87
|
+
else delete keys[provider];
|
|
88
|
+
writeJSON(CONFIG_FILE, { ...cfg, provider_keys: keys });
|
|
89
|
+
}
|
|
90
|
+
|
|
54
91
|
function getConfig() {
|
|
55
92
|
return readJSON(CONFIG_FILE, {});
|
|
56
93
|
}
|
|
@@ -98,5 +135,7 @@ module.exports = {
|
|
|
98
135
|
DIR, TOKEN_FILE, CONFIG_FILE,
|
|
99
136
|
ensureDir, readJSON, writeJSON,
|
|
100
137
|
getApiUrl, getWebUrl, getToken, setToken, clearToken,
|
|
138
|
+
apiUrlCandidates,
|
|
101
139
|
getConfig, setConfig, clearApiKey, getAuthHeaders, parseCookies,
|
|
140
|
+
getProviderKeys, getProviderKey, setProviderKey,
|
|
102
141
|
};
|
package/src/tools.js
CHANGED
|
@@ -122,9 +122,20 @@ function toolMeta(name, hub) {
|
|
|
122
122
|
|
|
123
123
|
function resolveInside(cwd, target) {
|
|
124
124
|
const root = path.resolve(cwd);
|
|
125
|
-
const
|
|
125
|
+
const raw = String(target ?? ".");
|
|
126
|
+
// Caminho absoluto de outro sistema (ex.: "C:\Windows\..." no Linux) e
|
|
127
|
+
// NUL nunca valem: sem isso o `path.resolve` trata como nome de arquivo.
|
|
128
|
+
if (raw.includes("\0")) throw new Error(`caminho invalido: ${raw}`);
|
|
129
|
+
if (path.win32.isAbsolute(raw) || path.posix.isAbsolute(raw)) {
|
|
130
|
+
const full = path.isAbsolute(raw) ? path.resolve(raw) : null;
|
|
131
|
+
if (!full || (full !== root && !full.startsWith(root + path.sep))) {
|
|
132
|
+
throw new Error(`caminho fora do diretorio de trabalho: ${raw}`);
|
|
133
|
+
}
|
|
134
|
+
return full;
|
|
135
|
+
}
|
|
136
|
+
const full = path.resolve(root, raw);
|
|
126
137
|
if (full !== root && !full.startsWith(root + path.sep)) {
|
|
127
|
-
throw new Error(`caminho fora do diretorio de trabalho: ${
|
|
138
|
+
throw new Error(`caminho fora do diretorio de trabalho: ${raw}`);
|
|
128
139
|
}
|
|
129
140
|
return full;
|
|
130
141
|
}
|