kafox-cli 0.3.0 → 0.7.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.
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Historico de conversas do CLI.
3
+ *
4
+ * Cada sessao vira um arquivo em ~/.kafox/chats/<codigo>.json e o codigo curto
5
+ * e o que aparece no `kafox resume <codigo>` (e no `kafox history`).
6
+ */
7
+ const crypto = require("crypto");
8
+ const fs = require("fs");
9
+ const os = require("os");
10
+ const path = require("path");
11
+ const storage = require("./storage");
12
+
13
+ // Poda: com muitos arquivos a pasta vira lixo e o `history` precisa listar rapido.
14
+ const MAX_SESSIONS = 60;
15
+ const TITLE_MAX = 60;
16
+
17
+ const FALLBACK_DIR = process.env.KAFOX_HOME
18
+ ? path.resolve(process.env.KAFOX_HOME)
19
+ : path.join(os.homedir(), ".kafox");
20
+
21
+ // storage.DIR vem do modulo real; em teste ele pode ser stub, entao nunca
22
+ // confiar cegamente (path.join(undefined) estoura).
23
+ function baseDir() {
24
+ return storage.DIR || FALLBACK_DIR;
25
+ }
26
+
27
+ function chatsDir() {
28
+ return path.join(baseDir(), "chats");
29
+ }
30
+
31
+ function ensureDir() {
32
+ fs.mkdirSync(chatsDir(), { recursive: true, mode: 0o700 });
33
+ }
34
+
35
+ function newId() {
36
+ return crypto.randomBytes(4).toString("hex");
37
+ }
38
+
39
+ function fileFor(id) {
40
+ return path.join(chatsDir(), `${id}.json`);
41
+ }
42
+
43
+ /** Titulo curto: primeira mensagem do usuario, em uma linha. */
44
+ function titleFrom(messages, fallback = "") {
45
+ const first = (messages || []).find(
46
+ (message) => message?.role === "user" && String(message.text || "").trim()
47
+ );
48
+ const text = String(first?.text || fallback || "")
49
+ .replace(/\s+/g, " ")
50
+ .trim();
51
+ if (!text) return "conversa sem titulo";
52
+ return text.length > TITLE_MAX ? `${text.slice(0, TITLE_MAX - 1).trimEnd()}…` : text;
53
+ }
54
+
55
+ function save({ id, cwd, model, theme, messages, context, startedAt, title }) {
56
+ if (!id) return null;
57
+ ensureDir();
58
+ const now = new Date().toISOString();
59
+ const list = Array.isArray(messages) ? messages : [];
60
+ const data = {
61
+ id,
62
+ title: title || titleFrom(list),
63
+ cwd: cwd || process.cwd(),
64
+ model: model || null,
65
+ theme: theme || null,
66
+ startedAt: startedAt || now,
67
+ updatedAt: now,
68
+ messages: list,
69
+ context: Array.isArray(context) ? context : [],
70
+ };
71
+ const tmp = `${fileFor(id)}.tmp`;
72
+ fs.writeFileSync(tmp, JSON.stringify(data), { mode: 0o600 });
73
+ fs.renameSync(tmp, fileFor(id));
74
+ prune();
75
+ return data;
76
+ }
77
+
78
+ function load(id) {
79
+ if (!id) return null;
80
+ try {
81
+ const data = JSON.parse(fs.readFileSync(fileFor(id), "utf8"));
82
+ if (!data || typeof data !== "object") return null;
83
+ // Arquivo antigo pode nao ter titulo: deriva das mensagens.
84
+ if (!data.title) data.title = titleFrom(data.messages);
85
+ return data;
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ function list() {
92
+ let names = [];
93
+ try {
94
+ names = fs.readdirSync(chatsDir()).filter((name) => name.endsWith(".json"));
95
+ } catch {
96
+ return [];
97
+ }
98
+ const chats = [];
99
+ for (const name of names) {
100
+ const id = name.replace(/\.json$/, "");
101
+ const data = load(id);
102
+ if (!data) continue;
103
+ chats.push({
104
+ id,
105
+ title: data.title || titleFrom(data.messages),
106
+ cwd: data.cwd,
107
+ model: data.model,
108
+ updatedAt: data.updatedAt,
109
+ messages: (data.messages || []).length,
110
+ });
111
+ }
112
+ return chats.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
113
+ }
114
+
115
+ function latest() {
116
+ return list()[0] || null;
117
+ }
118
+
119
+ function prune() {
120
+ for (const chat of list().slice(MAX_SESSIONS)) {
121
+ try {
122
+ fs.unlinkSync(fileFor(chat.id));
123
+ } catch {}
124
+ }
125
+ }
126
+
127
+ module.exports = {
128
+ MAX_SESSIONS,
129
+ TITLE_MAX,
130
+ chatsDir,
131
+ newId,
132
+ titleFrom,
133
+ save,
134
+ load,
135
+ list,
136
+ latest,
137
+ prune,
138
+ };
package/src/storage.js CHANGED
@@ -4,7 +4,8 @@ const path = require("path");
4
4
  const os = require("os");
5
5
 
6
6
  const HOME = os.homedir();
7
- const DIR = path.join(HOME, ".kafox");
7
+ // KAFOX_HOME aponta a pasta de configuracao direto (testes e uso portatil).
8
+ const DIR = process.env.KAFOX_HOME ? path.resolve(process.env.KAFOX_HOME) : path.join(HOME, ".kafox");
8
9
  const TOKEN_FILE = path.join(DIR, "token.json");
9
10
  const CONFIG_FILE = path.join(DIR, "config.json");
10
11
 
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Subagentes: um agente dentro do agente.
3
+ *
4
+ * O modelo chama `spawn_subagent` com um objetivo; cada tarefa roda em contexto
5
+ * proprio (nao polui a conversa principal) e devolve apenas o resultado final.
6
+ * Várias tarefas rodam em paralelo, o que serve para investigar pontos
7
+ * diferentes do projeto ao mesmo tempo (auditoria, testes, levantamento).
8
+ */
9
+ const SUBAGENT_TOOL_NAME = "spawn_subagent";
10
+ const MAX_TASKS = 3;
11
+ const MAX_DEPTH = 1;
12
+ // Subagente tambem nao tem teto por padrao: roda ate entregar o objetivo.
13
+ const DEFAULT_MAX_STEPS = Infinity;
14
+ const READ_ONLY_TOOLS = ["read_file", "list_dir"];
15
+ const ALL_LOCAL_TOOLS = ["read_file", "list_dir", "write_file", "edit_file", "run_command"];
16
+
17
+ const SUBAGENT_TOOL = {
18
+ type: "function",
19
+ function: {
20
+ name: SUBAGENT_TOOL_NAME,
21
+ description:
22
+ "Lanca um ou mais subagentes com objetivo proprio. Cada subagente roda em contexto separado, usa as ferramentas liberadas (padrao: read_file e list_dir) e devolve so o resultado final. Use para dividir trabalho, investigar varios pontos em paralelo (auditoria, testes, busca de arquivos) ou tarefas longas que nao precisam sujar esta conversa.",
23
+ parameters: {
24
+ type: "object",
25
+ properties: {
26
+ tasks: {
27
+ type: "array",
28
+ description: `Lista de tarefas (1 a ${MAX_TASKS}). Independentes rodam em paralelo.`,
29
+ items: {
30
+ type: "object",
31
+ properties: {
32
+ objective: {
33
+ type: "string",
34
+ description: "O que o subagente deve entregar, com o criterio de pronto",
35
+ },
36
+ context: {
37
+ type: "string",
38
+ description: "Contexto extra: arquivos, caminhos, comandos, restricoes",
39
+ },
40
+ tools: {
41
+ type: "array",
42
+ items: { type: "string" },
43
+ description:
44
+ "Ferramentas liberadas para esta tarefa (read_file, list_dir, write_file, edit_file, run_command). Padrao: read_file e list_dir.",
45
+ },
46
+ max_steps: {
47
+ type: "integer",
48
+ description: "Limite de passos (opcional; sem ele o subagente roda ate terminar)",
49
+ },
50
+ },
51
+ required: ["objective"],
52
+ },
53
+ },
54
+ objective: {
55
+ type: "string",
56
+ description: "Atalho para uma unica tarefa (equivalente a tasks: [{objective}])",
57
+ },
58
+ context: { type: "string", description: "Contexto extra para a tarefa unica" },
59
+ model: { type: "string", description: "Modelo dos subagentes (padrao: o da sessao)" },
60
+ },
61
+ },
62
+ },
63
+ };
64
+
65
+ function normalizeTasks(args = {}) {
66
+ const list = [];
67
+ if (Array.isArray(args.tasks)) {
68
+ for (const item of args.tasks) {
69
+ if (!item) continue;
70
+ if (typeof item === "string") list.push({ objective: item });
71
+ else if (item.objective) list.push(item);
72
+ }
73
+ } else if (args.objective) {
74
+ list.push({ objective: args.objective, context: args.context, tools: args.tools });
75
+ }
76
+ return list.slice(0, MAX_TASKS).map((task) => ({
77
+ objective: String(task.objective).trim(),
78
+ context: task.context ? String(task.context) : "",
79
+ tools: allowedTools(task.tools),
80
+ maxSteps: Number(task.max_steps || task.maxSteps) > 0 ? Number(task.max_steps || task.maxSteps) : Infinity,
81
+ }));
82
+ }
83
+
84
+ function allowedTools(requested) {
85
+ if (!Array.isArray(requested) || !requested.length) return [...READ_ONLY_TOOLS];
86
+ const chosen = requested
87
+ .map((name) => String(name).trim())
88
+ .filter((name) => ALL_LOCAL_TOOLS.includes(name));
89
+ return chosen.length ? [...new Set(chosen)] : [...READ_ONLY_TOOLS];
90
+ }
91
+
92
+ function subagentPrompt({ objective, context, cwd, depth }) {
93
+ return [
94
+ "Voce e um subagente do kafox CLI, rodando em contexto separado do agente principal.",
95
+ `Diretorio de trabalho: ${cwd}`,
96
+ `Objetivo: ${objective}`,
97
+ context ? `Contexto: ${context}` : "",
98
+ depth >= MAX_DEPTH ? "Nao lance outros subagentes." : "",
99
+ "Trabalhe so no objetivo recebido, use as ferramentas disponiveis e pare quando tiver a resposta.",
100
+ "Devolva um relatorio curto e direto em Markdown: o que encontrou, evidencias (arquivo:linha, saida de comando) e o que ficou em duvida.",
101
+ "Nao invente nada: se nao conseguiu verificar, diga isso explicitamente.",
102
+ ]
103
+ .filter(Boolean)
104
+ .join("\n");
105
+ }
106
+
107
+ function describeSubagentCall(args = {}) {
108
+ const tasks = normalizeTasks(args);
109
+ if (!tasks.length) return "(sem objetivo)";
110
+ if (tasks.length === 1) return tasks[0].objective.slice(0, 120);
111
+ return `${tasks.length} tarefas: ${tasks.map((task) => task.objective.slice(0, 40)).join(" | ")}`;
112
+ }
113
+
114
+ /* ------------------------- historico da sessao ------------------------- */
115
+ // O que os subagentes desta sessao fizeram: alimenta o `/agents`.
116
+ const SESSION_RUNS = [];
117
+ const MAX_RUNS = 50;
118
+
119
+ function recordRun({ objective, tools = [], ok, ms = 0, steps = 0, report = "" }) {
120
+ SESSION_RUNS.push({
121
+ objective: String(objective || "(sem objetivo)"),
122
+ tools: [...tools],
123
+ ok: Boolean(ok),
124
+ ms: Number(ms) || 0,
125
+ steps: Number(steps) || 0,
126
+ report: String(report || ""),
127
+ at: new Date().toISOString(),
128
+ });
129
+ if (SESSION_RUNS.length > MAX_RUNS) SESSION_RUNS.splice(0, SESSION_RUNS.length - MAX_RUNS);
130
+ return SESSION_RUNS[SESSION_RUNS.length - 1];
131
+ }
132
+
133
+ function listRuns() {
134
+ return SESSION_RUNS.map((run) => ({ ...run }));
135
+ }
136
+
137
+ function clearRuns() {
138
+ SESSION_RUNS.length = 0;
139
+ }
140
+
141
+ module.exports = {
142
+ SUBAGENT_TOOL,
143
+ SUBAGENT_TOOL_NAME,
144
+ MAX_TASKS,
145
+ MAX_DEPTH,
146
+ DEFAULT_MAX_STEPS,
147
+ READ_ONLY_TOOLS,
148
+ ALL_LOCAL_TOOLS,
149
+ normalizeTasks,
150
+ allowedTools,
151
+ subagentPrompt,
152
+ describeSubagentCall,
153
+ recordRun,
154
+ listRuns,
155
+ clearRuns,
156
+ };
package/src/theme.js CHANGED
@@ -2,7 +2,316 @@ const inquirer = require("inquirer");
2
2
  const chalk = require("chalk");
3
3
  const storage = require("./storage");
4
4
 
5
- const THEMES = ["default", "dark", "light", "brutalist", "neon", "paper"];
5
+ // Ordem da lista do /theme: primeiro os que seguem o terminal, depois os
6
+ // escuros, os claros e por fim os de contraste forte.
7
+ const THEMES = [
8
+ "default",
9
+ "dark",
10
+ "light",
11
+ "brutalist",
12
+ "neon",
13
+ "paper",
14
+ "dracula",
15
+ "nord",
16
+ "gruvbox",
17
+ "monokai",
18
+ "tokyo-night",
19
+ "catppuccin",
20
+ "solarized",
21
+ "midnight",
22
+ "ocean",
23
+ "forest",
24
+ "sunset",
25
+ "plum",
26
+ "synthwave",
27
+ "cyberpunk",
28
+ "amoled",
29
+ "matrix",
30
+ "terminal",
31
+ "solarized-light",
32
+ "rose",
33
+ "mint",
34
+ "sepia",
35
+ "ice",
36
+ "high-contrast",
37
+ ];
38
+
39
+ // Cores por tema: `accent` destaca negrito/titulos, `bg`/`fg` pintam a tela
40
+ // inteira (o preview do /theme aparece ja com o fundo do tema escolhido).
41
+ // `panel` pinta a caixa de input; `accent` destaca negrito/titulos; `border`
42
+ // desenha reguas e molduras do modal.
43
+ const THEME_DEFS = {
44
+ default: { accent: "yellow", bg: null, fg: null, panel: null, border: "#4b5563", mascot: "white" },
45
+ dark: {
46
+ accent: "yellowBright",
47
+ bg: "#0e1116",
48
+ fg: "#e8e8e8",
49
+ panel: "#171c24",
50
+ border: "#3d4653",
51
+ mascot: "white",
52
+ },
53
+ light: {
54
+ accent: "#8a5a00",
55
+ bg: "#f4f4f2",
56
+ fg: "#1b1b1b",
57
+ panel: "#e6e6e2",
58
+ border: "#b8b8b2",
59
+ mascot: "black",
60
+ },
61
+ brutalist: {
62
+ accent: "#111111",
63
+ bg: "#f5e663",
64
+ fg: "#111111",
65
+ panel: "#efe04d",
66
+ border: "#111111",
67
+ mascot: "black",
68
+ },
69
+ neon: {
70
+ accent: "magentaBright",
71
+ bg: "#150022",
72
+ fg: "#f2e9ff",
73
+ panel: "#24003a",
74
+ border: "#7a3bff",
75
+ mascot: "magenta",
76
+ },
77
+ paper: {
78
+ accent: "#7a4b00",
79
+ bg: "#f4ecd8",
80
+ fg: "#3a2f1f",
81
+ panel: "#eadfc4",
82
+ border: "#b9a67e",
83
+ mascot: "black",
84
+ },
85
+
86
+ /* ---------------------------------------------------------- escuros */
87
+ dracula: {
88
+ accent: "#bd93f9",
89
+ bg: "#282a36",
90
+ fg: "#f8f8f2",
91
+ panel: "#343746",
92
+ border: "#6272a4",
93
+ mascot: "magenta",
94
+ },
95
+ nord: {
96
+ accent: "#88c0d0",
97
+ bg: "#2e3440",
98
+ fg: "#eceff4",
99
+ panel: "#3b4252",
100
+ border: "#4c566a",
101
+ mascot: "cyan",
102
+ },
103
+ gruvbox: {
104
+ accent: "#fabd2f",
105
+ bg: "#282828",
106
+ fg: "#ebdbb2",
107
+ panel: "#3c3836",
108
+ border: "#665c54",
109
+ mascot: "yellow",
110
+ },
111
+ monokai: {
112
+ accent: "#f92672",
113
+ bg: "#272822",
114
+ fg: "#f8f8f2",
115
+ panel: "#3b3a34",
116
+ border: "#75715e",
117
+ mascot: "magenta",
118
+ },
119
+ "tokyo-night": {
120
+ accent: "#7aa2f7",
121
+ bg: "#1a1b26",
122
+ fg: "#c0caf5",
123
+ panel: "#24283b",
124
+ border: "#414868",
125
+ mascot: "blue",
126
+ },
127
+ catppuccin: {
128
+ accent: "#cba6f7",
129
+ bg: "#1e1e2e",
130
+ fg: "#cdd6f4",
131
+ panel: "#313244",
132
+ border: "#585b70",
133
+ mascot: "magenta",
134
+ },
135
+ solarized: {
136
+ accent: "#b58900",
137
+ bg: "#002b36",
138
+ fg: "#93a1a1",
139
+ panel: "#073642",
140
+ border: "#586e75",
141
+ mascot: "yellow",
142
+ },
143
+ midnight: {
144
+ accent: "#38bdf8",
145
+ bg: "#0f172a",
146
+ fg: "#e2e8f0",
147
+ panel: "#1e293b",
148
+ border: "#334155",
149
+ mascot: "cyan",
150
+ },
151
+ ocean: {
152
+ accent: "#4cc9f0",
153
+ bg: "#0b2545",
154
+ fg: "#d6e4f0",
155
+ panel: "#123a6b",
156
+ border: "#2a628f",
157
+ mascot: "cyan",
158
+ },
159
+ forest: {
160
+ accent: "#7ee787",
161
+ bg: "#10231a",
162
+ fg: "#d4e8d4",
163
+ panel: "#17352a",
164
+ border: "#2f5d43",
165
+ mascot: "green",
166
+ },
167
+ sunset: {
168
+ accent: "#ff8c42",
169
+ bg: "#2b1b17",
170
+ fg: "#ffd9b3",
171
+ panel: "#3a2620",
172
+ border: "#7a4a35",
173
+ mascot: "yellow",
174
+ },
175
+ plum: {
176
+ accent: "#d8b4fe",
177
+ bg: "#2a1a33",
178
+ fg: "#f0e6f6",
179
+ panel: "#3a2447",
180
+ border: "#6b4a7d",
181
+ mascot: "magenta",
182
+ },
183
+ synthwave: {
184
+ accent: "#ff7edb",
185
+ bg: "#241b2f",
186
+ fg: "#ffe6ff",
187
+ panel: "#2f2138",
188
+ border: "#7b2fbe",
189
+ mascot: "magenta",
190
+ },
191
+ cyberpunk: {
192
+ accent: "#ff2a6d",
193
+ bg: "#0d0221",
194
+ fg: "#f2f2f2",
195
+ panel: "#1a0b33",
196
+ border: "#05d9e8",
197
+ mascot: "magenta",
198
+ },
199
+ amoled: {
200
+ accent: "#00d7ff",
201
+ bg: "#000000",
202
+ fg: "#e6e6e6",
203
+ panel: "#0a0a0a",
204
+ border: "#2b2b2b",
205
+ mascot: "cyan",
206
+ },
207
+ matrix: {
208
+ accent: "#00ff41",
209
+ bg: "#000000",
210
+ fg: "#33ff33",
211
+ panel: "#081c08",
212
+ border: "#00a300",
213
+ mascot: "green",
214
+ },
215
+ terminal: {
216
+ accent: "#00ff00",
217
+ bg: "#000000",
218
+ fg: "#c0c0c0",
219
+ panel: "#0c0c0c",
220
+ border: "#303030",
221
+ mascot: "green",
222
+ },
223
+
224
+ /* ----------------------------------------------------------- claros */
225
+ "solarized-light": {
226
+ accent: "#cb4b16",
227
+ bg: "#fdf6e3",
228
+ fg: "#586e75",
229
+ panel: "#eee8d5",
230
+ border: "#93a1a1",
231
+ mascot: "black",
232
+ },
233
+ rose: {
234
+ accent: "#c9184a",
235
+ bg: "#fff0f3",
236
+ fg: "#4a1220",
237
+ panel: "#ffe0e7",
238
+ border: "#e29aa9",
239
+ mascot: "black",
240
+ },
241
+ mint: {
242
+ accent: "#0f766e",
243
+ bg: "#eefbf4",
244
+ fg: "#10392c",
245
+ panel: "#d9f4e6",
246
+ border: "#8fcfb6",
247
+ mascot: "black",
248
+ },
249
+ sepia: {
250
+ accent: "#8a6d3b",
251
+ bg: "#e8ddc4",
252
+ fg: "#4a3b23",
253
+ panel: "#ddd0b1",
254
+ border: "#b3a17b",
255
+ mascot: "black",
256
+ },
257
+ ice: {
258
+ accent: "#1d4ed8",
259
+ bg: "#f2f7ff",
260
+ fg: "#17293f",
261
+ panel: "#e2ecff",
262
+ border: "#9db8e0",
263
+ mascot: "black",
264
+ },
265
+ "high-contrast": {
266
+ accent: "#ffff00",
267
+ bg: "#000000",
268
+ fg: "#ffffff",
269
+ panel: "#111111",
270
+ border: "#ffffff",
271
+ mascot: "white",
272
+ },
273
+ };
274
+
275
+ function themeColors(theme) {
276
+ return THEME_DEFS[theme] || THEME_DEFS.default;
277
+ }
278
+
279
+ /** Pinta uma linha inteira com o fundo/texto do tema (chalk cuida do aninhamento). */
280
+ function paintLine(theme, text) {
281
+ const { bg, fg } = themeColors(theme);
282
+ let out = String(text);
283
+ if (fg) out = chalk.hex(fg)(out);
284
+ if (bg) out = chalk.bgHex(bg)(out);
285
+ return out;
286
+ }
287
+
288
+ function mascotColor(theme) {
289
+ const color = themeColors(theme).mascot;
290
+ if (typeof chalk[color] === "function") return chalk[color];
291
+ if (typeof color === "string" && color.startsWith("#")) return chalk.hex(color);
292
+ return chalk.white;
293
+ }
294
+
295
+ /** Cor das reguas e molduras (dim quando o tema nao define uma). */
296
+ function borderColor(theme) {
297
+ const { border } = themeColors(theme);
298
+ if (!border) return chalk.dim;
299
+ return chalk.hex(border);
300
+ }
301
+
302
+ /** Destaca titulos/negrito com a cor de destaque do tema. */
303
+ function accentPainter(theme) {
304
+ const { accent } = themeColors(theme);
305
+ if (typeof chalk[accent] === "function") return chalk[accent];
306
+ return chalk.hex(accent);
307
+ }
308
+
309
+ /** Pinta o interior da caixa de input (usa o bg do tema quando nao ha panel). */
310
+ function paintPanel(theme, text) {
311
+ const { bg, panel } = themeColors(theme);
312
+ const target = panel || bg;
313
+ return target ? chalk.bgHex(target)(text) : text;
314
+ }
6
315
 
7
316
  function paint(theme, text) {
8
317
  switch (theme) {
@@ -16,8 +325,11 @@ function paint(theme, text) {
16
325
  return chalk.black(text);
17
326
  case "dark":
18
327
  return chalk.cyan(text);
19
- default:
328
+ case "default":
20
329
  return chalk.green(text);
330
+ default:
331
+ // Qualquer tema novo usa o proprio destaque.
332
+ return accentPainter(theme)(text);
21
333
  }
22
334
  }
23
335
 
@@ -61,4 +373,16 @@ async function setContentBackground(url) {
61
373
  );
62
374
  }
63
375
 
64
- module.exports = { runTheme, setContentBackground, THEMES, paint };
376
+ module.exports = {
377
+ runTheme,
378
+ setContentBackground,
379
+ THEMES,
380
+ THEME_DEFS,
381
+ themeColors,
382
+ paintLine,
383
+ paintPanel,
384
+ mascotColor,
385
+ borderColor,
386
+ accentPainter,
387
+ paint,
388
+ };