kafox-cli 0.3.0 → 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 +78 -8
- 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 +1536 -212
package/src/gateway.js
CHANGED
|
@@ -3,26 +3,20 @@ const storage = require("./storage");
|
|
|
3
3
|
const { ensureSession, fetchApi } = require("./auth");
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* A
|
|
6
|
+
* Garante sessao, saldo, modelo e chave antes de qualquer chamada.
|
|
7
|
+
* A chave `kafox_live_...` e da propria conta: o gateway cobra por ela.
|
|
8
8
|
*/
|
|
9
|
-
async function
|
|
10
|
-
const text = typeof question === "string" ? question : (question || []).join(" ");
|
|
11
|
-
if (!text.trim()) {
|
|
12
|
-
return { ok: false, error: 'Usage: kafox ask "your question" [-m model]' };
|
|
13
|
-
}
|
|
14
|
-
|
|
9
|
+
async function ensureReady(options = {}) {
|
|
15
10
|
const token = await ensureSession();
|
|
16
11
|
if (!token) {
|
|
17
12
|
return { ok: false, error: "Login first: `kafox login`" };
|
|
18
13
|
}
|
|
19
14
|
|
|
20
|
-
// Saldo decide se a CLI responde: sem credito, nem chega no gateway.
|
|
21
15
|
const boot = await fetchApi("/api/bootstrap");
|
|
22
16
|
if (!boot.ok) {
|
|
23
17
|
return { ok: false, error: "Session invalid. Run `kafox login`" };
|
|
24
18
|
}
|
|
25
|
-
const account = await boot.json();
|
|
19
|
+
const account = await boot.json().catch(() => null);
|
|
26
20
|
const balance = Number(account?.user?.balance_micros || 0);
|
|
27
21
|
if (balance <= 0) {
|
|
28
22
|
return {
|
|
@@ -38,9 +32,7 @@ async function askModel(question, options = {}) {
|
|
|
38
32
|
.then((response) => response.json())
|
|
39
33
|
.catch(() => null);
|
|
40
34
|
model = pricing?.pricing?.[0]?.model;
|
|
41
|
-
if (!model) {
|
|
42
|
-
return { ok: false, error: "Could not load model catalog." };
|
|
43
|
-
}
|
|
35
|
+
if (!model) return { ok: false, error: "Could not load model catalog." };
|
|
44
36
|
}
|
|
45
37
|
|
|
46
38
|
let keyCreated = false;
|
|
@@ -59,51 +51,152 @@ async function askModel(question, options = {}) {
|
|
|
59
51
|
keyCreated = true;
|
|
60
52
|
}
|
|
61
53
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
54
|
+
return { ok: true, model, apiKey, keyCreated, balance };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function mergeToolCalls(target, deltas) {
|
|
58
|
+
for (const delta of deltas) {
|
|
59
|
+
const index = Number.isInteger(delta?.index) ? delta.index : target.length;
|
|
60
|
+
const current = target[index] || {
|
|
61
|
+
id: "",
|
|
62
|
+
type: "function",
|
|
63
|
+
function: { name: "", arguments: "" },
|
|
64
|
+
};
|
|
65
|
+
if (delta.id) current.id = delta.id;
|
|
66
|
+
if (delta.type) current.type = delta.type;
|
|
67
|
+
if (delta.function?.name) current.function.name += delta.function.name;
|
|
68
|
+
if (delta.function?.arguments) current.function.arguments += delta.function.arguments;
|
|
69
|
+
target[index] = current;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function errorFrom(data, status) {
|
|
74
|
+
const message =
|
|
75
|
+
data?.error?.message || data?.error || data?.body?.error?.message || `gateway error (${status})`;
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
code: data?.code || (status === 402 ? "no_balance" : "gateway_error"),
|
|
79
|
+
error: String(message),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Chat completions com stream (SSE) via /api/chat.
|
|
85
|
+
* onDelta/onReasoning recebem o texto conforme chega; no fim devolve tudo junto.
|
|
86
|
+
*/
|
|
87
|
+
async function chat({ messages, model, tools, onDelta, onReasoning, signal } = {}) {
|
|
88
|
+
const ready = await ensureReady({ model });
|
|
89
|
+
if (!ready.ok) return ready;
|
|
90
|
+
|
|
91
|
+
const started = Date.now();
|
|
92
|
+
let response;
|
|
93
|
+
try {
|
|
94
|
+
response = await fetchApi("/api/chat", {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: { accept: "text/event-stream" },
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
api_key: ready.apiKey,
|
|
99
|
+
model: ready.model,
|
|
100
|
+
messages,
|
|
101
|
+
tools: tools && tools.length ? tools : undefined,
|
|
102
|
+
stream: true,
|
|
103
|
+
}),
|
|
104
|
+
signal,
|
|
105
|
+
});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error?.name === "AbortError") return { ok: false, code: "aborted", error: "Cancelado." };
|
|
108
|
+
return { ok: false, error: `Falha de rede: ${error.message}` };
|
|
109
|
+
}
|
|
67
110
|
|
|
68
111
|
if (!response.ok) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
ok: false,
|
|
72
|
-
code: "no_balance",
|
|
73
|
-
error: "Sem saldo para responder. Recarregue em https://kafox.pypag.com/dashboard",
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
return { ok: false, error: `Failed: ${data.error || "gateway error"}` };
|
|
112
|
+
const data = await response.json().catch(() => ({}));
|
|
113
|
+
return { ...errorFrom(data, response.status), status: response.status, keyCreated: ready.keyCreated };
|
|
77
114
|
}
|
|
78
115
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
116
|
+
const decoder = new TextDecoder();
|
|
117
|
+
let buffer = "";
|
|
118
|
+
let text = "";
|
|
119
|
+
let reasoning = "";
|
|
120
|
+
let usage = null;
|
|
121
|
+
const toolCalls = [];
|
|
122
|
+
|
|
123
|
+
const handleLine = (line) => {
|
|
124
|
+
if (!line.startsWith("data:")) return null;
|
|
125
|
+
const data = line.slice(5).trim();
|
|
126
|
+
if (!data || data === "[DONE]") return null;
|
|
127
|
+
let event;
|
|
128
|
+
try {
|
|
129
|
+
event = JSON.parse(data);
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (event?.error) {
|
|
134
|
+
return { error: event.error.message || String(event.error) };
|
|
135
|
+
}
|
|
136
|
+
if (event?.usage) usage = event.usage;
|
|
137
|
+
const delta = event?.choices?.[0]?.delta;
|
|
138
|
+
if (!delta) return null;
|
|
139
|
+
// Cada provedor manda o "pensamento" com um nome diferente.
|
|
140
|
+
const think = delta.reasoning_content ?? delta.reasoning ?? delta.thinking;
|
|
141
|
+
if (typeof think === "string" && think) {
|
|
142
|
+
reasoning += think;
|
|
143
|
+
onReasoning?.(think);
|
|
144
|
+
}
|
|
145
|
+
if (delta.content) {
|
|
146
|
+
text += delta.content;
|
|
147
|
+
onDelta?.(delta.content);
|
|
148
|
+
}
|
|
149
|
+
if (Array.isArray(delta.tool_calls)) mergeToolCalls(toolCalls, delta.tool_calls);
|
|
150
|
+
return null;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
for await (const chunk of response.body) {
|
|
155
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
156
|
+
let index = buffer.indexOf("\n");
|
|
157
|
+
while (index >= 0) {
|
|
158
|
+
const line = buffer.slice(0, index).replace(/\r$/, "");
|
|
159
|
+
buffer = buffer.slice(index + 1);
|
|
160
|
+
const failure = handleLine(line);
|
|
161
|
+
if (failure) return { ok: false, error: failure.error };
|
|
162
|
+
index = buffer.indexOf("\n");
|
|
163
|
+
}
|
|
88
164
|
}
|
|
89
|
-
|
|
165
|
+
if (buffer.trim()) handleLine(buffer.trim());
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error?.name === "AbortError") {
|
|
168
|
+
return { ok: true, text, reasoning, toolCalls, ms: Date.now() - started, aborted: true };
|
|
169
|
+
}
|
|
170
|
+
return { ok: false, error: `Stream interrompido: ${error.message}`, text };
|
|
90
171
|
}
|
|
91
172
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
173
|
+
return {
|
|
174
|
+
ok: true,
|
|
175
|
+
text,
|
|
176
|
+
reasoning,
|
|
177
|
+
toolCalls: toolCalls.filter(Boolean),
|
|
178
|
+
usage,
|
|
179
|
+
ms: Date.now() - started,
|
|
180
|
+
model: ready.model,
|
|
181
|
+
keyCreated: ready.keyCreated,
|
|
182
|
+
provider: response.headers?.get?.("x-kafox-provider") || "",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
99
185
|
|
|
186
|
+
/** Compatibilidade: resposta unica, sem callbacks (usada por `kafox ask`). */
|
|
187
|
+
async function askModel(question, options = {}) {
|
|
188
|
+
const text = typeof question === "string" ? question : (question || []).join(" ");
|
|
189
|
+
if (!text.trim()) {
|
|
190
|
+
return { ok: false, error: 'Usage: kafox ask "your question" [-m model]' };
|
|
191
|
+
}
|
|
192
|
+
const result = await chat({ messages: [{ role: "user", content: text }], model: options.model });
|
|
193
|
+
if (!result.ok) return result;
|
|
100
194
|
return {
|
|
101
195
|
ok: true,
|
|
102
|
-
text: String(
|
|
103
|
-
ms:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
keyCreated,
|
|
196
|
+
text: String(result.text || "").trim() || "(resposta vazia)",
|
|
197
|
+
ms: result.ms,
|
|
198
|
+
model: result.model,
|
|
199
|
+
keyCreated: result.keyCreated,
|
|
107
200
|
};
|
|
108
201
|
}
|
|
109
202
|
|
|
@@ -124,4 +217,4 @@ async function ask(question, options = {}) {
|
|
|
124
217
|
}
|
|
125
218
|
}
|
|
126
219
|
|
|
127
|
-
module.exports = { ask, askModel };
|
|
220
|
+
module.exports = { ask, askModel, chat, ensureReady, mergeToolCalls };
|
package/src/logo.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logo do kafox: a arte "KAFOX" em blocos (fundo azul), adaptada por tema.
|
|
3
|
+
*
|
|
4
|
+
* A arte original vem com codigos ANSI por trecho; aqui ela e decodificada em
|
|
5
|
+
* segmentos {fg, bg, texto} e repintada com chalk, para que o fundo do tema
|
|
6
|
+
* entre nos espacos transparentes e o texto continue legivel no tema claro.
|
|
7
|
+
*/
|
|
8
|
+
const chalk = require("chalk");
|
|
9
|
+
const { themeColors } = require("./theme");
|
|
10
|
+
|
|
11
|
+
const SGR = /\x1b\[([0-9;]*)m/g;
|
|
12
|
+
|
|
13
|
+
// Paleta da arte. 97 = tinta clara, 94 = azul, 34 = azul fundo, 37 = neutro,
|
|
14
|
+
// 44 = fundo azul (aplicado junto de 94).
|
|
15
|
+
const PALETTES = {
|
|
16
|
+
dark: { ink: "#ffffff", gray: "#c8c8c8", blue: "#3f6ef0", deep: "#1c2fc4", fill: "#1c2fc4" },
|
|
17
|
+
light: { ink: "#2323b8", gray: "#6f6f6f", blue: "#4b52e8", deep: "#2a2ab0", fill: "#c7cfff" },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const STYLE_KEYS = {
|
|
21
|
+
37: { fg: "gray" },
|
|
22
|
+
34: { fg: "deep" },
|
|
23
|
+
94: { fg: "blue" },
|
|
24
|
+
97: { fg: "ink" },
|
|
25
|
+
44: { bg: "fill" },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const RAW_LOGO = [
|
|
29
|
+
"\x1b[0;97m█\x1b[0;94m█\x1b[0;94;44m▓▓\x1b[0;37m \x1b[0;94;44m█▓▒░\x1b[0;37m \x1b[0;97m▄\x1b[0;94m█\x1b[0;94;44m▓▓▒░\x1b[0;34m███▄\x1b[0;37m \x1b[0;97m▄\x1b[0;94m█\x1b[0;94;44m▓▓▒░\x1b[0;34m████\x1b[0;37m \x1b[0;97m▄\x1b[0;94m█\x1b[0;94;44m▓▓▒░\x1b[0;34m███▄\x1b[0;37m \x1b[0;97m█\x1b[0;94m█\x1b[0;94;44m▓▓\x1b[0;37m \x1b[0;94;44m▓▒░\x1b[0;34m█\x1b[0m",
|
|
30
|
+
"\x1b[0;94;44m▓▓▒▒\x1b[0;37m \x1b[0;94;44m▓▒░\x1b[0;34m█\x1b[0;37m \x1b[0;94;44m▓▓▒▒\x1b[0;37m \x1b[0;34m█\x1b[0;94;44m \x1b[0;34m██\x1b[0;37m \x1b[0;94;44m▓▓▒▒\x1b[0;37m \x1b[0;34m█\x1b[0;94;44m▓▓\x1b[0;34m█\x1b[0;37m \x1b[0;94;44m▓▓▒▒\x1b[0;37m \x1b[0;34m█\x1b[0;94;44m░░\x1b[0;34m█\x1b[0;37m \x1b[0;94;44m▓▓▒▒\x1b[0;37m \x1b[0;94;44m▒░░\x1b[0;34m█\x1b[0m",
|
|
31
|
+
"\x1b[0;94;44m▒▒░░\x1b[0;34m█\x1b[0;94;44m░▒░▒\x1b[0;34m▄\x1b[0;37m \x1b[0;94;44m▒▒░░\x1b[0;34m██████\x1b[0;37m \x1b[0;94;44m▒▒░░\x1b[0;34m█\x1b[0;37m \x1b[0;94;44m▒▒░░\x1b[0;37m \x1b[0;34m█\x1b[0;94;44m▒▒\x1b[0;34m█\x1b[0;37m \x1b[0;94m▀\x1b[0;94;44m▒░░\x1b[0;34m██\x1b[0;94;44m░▒▒\x1b[0;94m▀\x1b[0m",
|
|
32
|
+
"\x1b[0;94;44m░░\x1b[0;34m██\x1b[0;37m \x1b[0;94;44m░▓▓ \x1b[0;37m \x1b[0;94;44m░░\x1b[0;34m██\x1b[0;37m \x1b[0;34m█\x1b[0;94;44m▓▓\x1b[0;34m█\x1b[0;37m \x1b[0;94;44m░░\x1b[0;34m██\x1b[0;37m \x1b[0;94;44m░░\x1b[0;34m██\x1b[0;37m \x1b[0;34m█\x1b[0;94;44m▓▓ \x1b[0;37m \x1b[0;94;44m░░\x1b[0;34m██\x1b[0;37m \x1b[0;34m█\x1b[0;94;44m▓▓ \x1b[0m",
|
|
33
|
+
"\x1b[0;34m████\x1b[0;37m \x1b[0;34m████\x1b[0;37m \x1b[0;34m████\x1b[0;37m \x1b[0;34m████\x1b[0;37m \x1b[0;34m████\x1b[0;37m \x1b[0;34m▀████████▀\x1b[0;37m \x1b[0;34m████\x1b[0;37m \x1b[0;34m████\x1b[0m",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
37
|
+
|
|
38
|
+
/** Decodifica uma linha crua em segmentos com estilo e texto. */
|
|
39
|
+
function parseLine(raw) {
|
|
40
|
+
const segments = [];
|
|
41
|
+
let style = { fg: null, bg: null };
|
|
42
|
+
let last = 0;
|
|
43
|
+
let match;
|
|
44
|
+
SGR.lastIndex = 0;
|
|
45
|
+
|
|
46
|
+
while ((match = SGR.exec(raw)) !== null) {
|
|
47
|
+
if (match.index > last) {
|
|
48
|
+
segments.push({ ...style, text: raw.slice(last, match.index) });
|
|
49
|
+
}
|
|
50
|
+
const codes = match[1].split(";").filter(Boolean).map(Number);
|
|
51
|
+
for (const code of codes) {
|
|
52
|
+
if (code === 0) style = { fg: null, bg: null };
|
|
53
|
+
else if (STYLE_KEYS[code]) Object.assign(style, STYLE_KEYS[code]);
|
|
54
|
+
}
|
|
55
|
+
last = match.index + match[0].length;
|
|
56
|
+
}
|
|
57
|
+
if (last < raw.length) segments.push({ ...style, text: raw.slice(last) });
|
|
58
|
+
return segments;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const SEGMENTS = RAW_LOGO.map(parseLine);
|
|
62
|
+
|
|
63
|
+
const LOGO_TEXT = SEGMENTS.map((line) => line.map((part) => part.text).join(""));
|
|
64
|
+
const LOGO_WIDTH = Math.max(...LOGO_TEXT.map((line) => line.length));
|
|
65
|
+
const LOGO_HEIGHT = LOGO_TEXT.length;
|
|
66
|
+
|
|
67
|
+
/** Temas de fundo claro precisam de tinta escura para o desenho nao sumir. */
|
|
68
|
+
function paletteFor(theme) {
|
|
69
|
+
const { bg } = themeColors(theme);
|
|
70
|
+
if (!bg) return PALETTES.dark;
|
|
71
|
+
const [r, g, b] = [1, 3, 5].map((index) => parseInt(bg.slice(index, index + 2), 16));
|
|
72
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
73
|
+
return luminance > 0.6 ? PALETTES.light : PALETTES.dark;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function paintSegment(text, style, palette) {
|
|
77
|
+
if (!text) return "";
|
|
78
|
+
const fg = style.fg ? palette[style.fg] : null;
|
|
79
|
+
const bg = style.bg ? palette[style.bg] : null;
|
|
80
|
+
if (fg && bg) return chalk.bgHex(bg).hex(fg)(text);
|
|
81
|
+
if (bg) return chalk.bgHex(bg)(text);
|
|
82
|
+
if (fg) return chalk.hex(fg)(text);
|
|
83
|
+
return text;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Linhas da arte ja coloridas (sem a cor de fundo da tela). */
|
|
87
|
+
function logoLines(theme = "default") {
|
|
88
|
+
const palette = paletteFor(theme);
|
|
89
|
+
return SEGMENTS.map((segments) =>
|
|
90
|
+
segments.map((part) => paintSegment(part.text, part, palette)).join("")
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Versao em texto puro (sem cor), para saidas nao-TTY e testes. */
|
|
95
|
+
function logoText() {
|
|
96
|
+
return LOGO_TEXT.join("\n");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
logoLines,
|
|
101
|
+
logoText,
|
|
102
|
+
parseLine,
|
|
103
|
+
LOGO_WIDTH,
|
|
104
|
+
LOGO_HEIGHT,
|
|
105
|
+
PALETTES,
|
|
106
|
+
paletteFor,
|
|
107
|
+
stripAnsi: (value) => String(value).replace(ANSI_RE, ""),
|
|
108
|
+
};
|
package/src/mascot.js
CHANGED
|
@@ -1,32 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
const
|
|
3
|
-
"████ ████",
|
|
4
|
-
" ▒▒░░░░▓▓ ▓▓░░░░▒▒",
|
|
5
|
-
" ██░░▒▒░░██ ██░░▒▒░░██",
|
|
6
|
-
" ██░░▓▓▒▒░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░▒▒▓▓░░██",
|
|
7
|
-
" ██░░▓▓▓▓▒▒▓▓ ▓▓▒▒▓▓▓▓░░██",
|
|
8
|
-
" ██░░▒▒▒▒▓▓░░░░░░░░░░░░░░▓▓▒▒▒▒░░██",
|
|
9
|
-
" ██▒▒▓▓▓▓░░░░░░░░░░░░░░░░░░▓▓▓▓▒▒██",
|
|
10
|
-
"▒▒ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ▓▓",
|
|
11
|
-
"██ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ██",
|
|
12
|
-
"██ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ██",
|
|
13
|
-
"██ ░░▓▓▓▓░░░░░░░░░░░░░░░░░░▓▓▓▓░░ ██",
|
|
14
|
-
"██ ░░░░░░▓▓░░░░░░▒▒░░░░░░▓▓░░░░░░ ██",
|
|
15
|
-
"██░░░░░░░░░░▓▓░░▒▒▒▒▒▒░░▓▓░░░░░░░░░░██",
|
|
16
|
-
"██░░ ░░░░░░░░░░░░░░ ░░██ ▒▒▓▓",
|
|
17
|
-
" ██ ▓▓▓▓▓▓ ██ ██ ██",
|
|
18
|
-
" ▓▓ ▓▓ ▓▓ ▓▓▒▒ ██",
|
|
19
|
-
" ██ ▓▓ ▓▓ ██ ██ ██",
|
|
20
|
-
" ▓▓▓▓ ▓▓▓▓ ▓▓░░░░ ██",
|
|
21
|
-
" ██▒▒░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░▒▒██░░▒▒▒▒░░░░░░██",
|
|
22
|
-
" ██░░░░ ░░░░▓▓▒▒▒▒▒▒▒▒░░░░██",
|
|
23
|
-
" ██░░░░░░ ░░░░░░▓▓▒▒▒▒▒▒░░██",
|
|
24
|
-
" ██░░░░ ▒▒ ▒▒ ░░░░▓▓▒▒▒▒▒▒▓▓",
|
|
25
|
-
" ██░░░░ ▒▒ ▒▒ ░░░░▓▓▒▒████",
|
|
26
|
-
" ██▒▒░░ ▓▓ ▒▒ ▓▓ ░░▒▒██▓▓",
|
|
27
|
-
" ████ ▓▓ ▓▓ ▓▓ ████",
|
|
28
|
-
" ▒▒██▓▓ ▓▓ ▓▓██▒▒",
|
|
29
|
-
" ██████████",
|
|
30
|
-
].join("\n");
|
|
1
|
+
// Compatibilidade: o "mascote" agora e o logo KAFOX em blocos (logo.js).
|
|
2
|
+
const { logoText, logoLines } = require("./logo");
|
|
31
3
|
|
|
32
|
-
module.exports = { mascot:
|
|
4
|
+
module.exports = { mascot: logoText(), logoLines };
|