kafox-cli 0.2.3 → 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/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
+ };
package/src/tools.js ADDED
@@ -0,0 +1,359 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { spawnSync } = require("child_process");
4
+
5
+ // Limites para nao estourar o contexto nem travar a TUI.
6
+ const MAX_READ_CHARS = 40_000;
7
+ const MAX_READ_LINES = 400;
8
+ const MAX_WRITE_CHARS = 120_000;
9
+ const MAX_LIST_ENTRIES = 300;
10
+ const MAX_OUTPUT_CHARS = 12_000;
11
+ const COMMAND_TIMEOUT_MS = 60_000;
12
+
13
+ const TOOL_SPECS = [
14
+ {
15
+ type: "function",
16
+ function: {
17
+ name: "read_file",
18
+ description:
19
+ "Le um arquivo de texto do diretorio de trabalho. Use offset/limit para trechos grandes.",
20
+ parameters: {
21
+ type: "object",
22
+ properties: {
23
+ path: { type: "string", description: "Caminho relativo ao diretorio de trabalho" },
24
+ offset: { type: "integer", description: "Linha inicial (1-based)" },
25
+ limit: { type: "integer", description: "Quantidade de linhas (max 400)" },
26
+ },
27
+ required: ["path"],
28
+ },
29
+ },
30
+ },
31
+ {
32
+ type: "function",
33
+ function: {
34
+ name: "list_dir",
35
+ description: "Lista arquivos e pastas de um diretorio do projeto.",
36
+ parameters: {
37
+ type: "object",
38
+ properties: {
39
+ path: { type: "string", description: "Diretorio relativo (padrao: raiz do projeto)" },
40
+ },
41
+ },
42
+ },
43
+ },
44
+ {
45
+ type: "function",
46
+ function: {
47
+ name: "write_file",
48
+ description:
49
+ "Cria um arquivo novo ou reescreve um arquivo inteiro no diretorio de trabalho. Cria as pastas necessarias.",
50
+ parameters: {
51
+ type: "object",
52
+ properties: {
53
+ path: { type: "string", description: "Caminho relativo ao diretorio de trabalho" },
54
+ content: { type: "string", description: "Conteudo completo do arquivo" },
55
+ },
56
+ required: ["path", "content"],
57
+ },
58
+ },
59
+ },
60
+ {
61
+ type: "function",
62
+ function: {
63
+ name: "edit_file",
64
+ description:
65
+ "Troca um trecho exato de um arquivo existente por outro. Prefira esta ferramenta a reescrever o arquivo inteiro.",
66
+ parameters: {
67
+ type: "object",
68
+ properties: {
69
+ path: { type: "string", description: "Caminho relativo ao diretorio de trabalho" },
70
+ old_string: { type: "string", description: "Trecho atual, exatamente como esta no arquivo" },
71
+ new_string: { type: "string", description: "Trecho que substitui o anterior" },
72
+ replace_all: {
73
+ type: "boolean",
74
+ description: "Troca todas as ocorrencias (padrao: so quando for unica)",
75
+ },
76
+ },
77
+ required: ["path", "old_string", "new_string"],
78
+ },
79
+ },
80
+ },
81
+ {
82
+ type: "function",
83
+ function: {
84
+ name: "run_command",
85
+ description:
86
+ "Executa um comando de shell no diretorio de trabalho e devolve stdout, stderr e o codigo de saida.",
87
+ parameters: {
88
+ type: "object",
89
+ properties: {
90
+ command: { type: "string", description: "Comando a executar" },
91
+ timeout_ms: { type: "integer", description: "Timeout em ms (padrao 60000, max 120000)" },
92
+ },
93
+ required: ["command"],
94
+ },
95
+ },
96
+ },
97
+ ];
98
+
99
+ const LOCAL_TOOL_NAMES = new Set(TOOL_SPECS.map((tool) => tool.function.name));
100
+ const TOOL_NAMES = LOCAL_TOOL_NAMES;
101
+ // Ferramentas que mudam o disco ou executam codigo pedem aprovacao.
102
+ const APPROVAL_REQUIRED = new Set(["write_file", "edit_file", "run_command"]);
103
+
104
+ const TOOL_META = {
105
+ read_file: { kind: "read", label: "Read" },
106
+ list_dir: { kind: "list", label: "List" },
107
+ write_file: { kind: "write", label: "Write" },
108
+ edit_file: { kind: "edit", label: "Edit" },
109
+ run_command: { kind: "bash", label: "Bash" },
110
+ };
111
+
112
+ function toolMeta(name, hub) {
113
+ if (TOOL_META[name]) return { ...TOOL_META[name], mcp: null };
114
+ const entry = hub?.describe?.(name);
115
+ if (entry) return { kind: "mcp", label: "MCP", server: entry.server, tool: entry.tool };
116
+ return { kind: "tool", label: name || "tool" };
117
+ }
118
+
119
+ function resolveInside(cwd, target) {
120
+ const root = path.resolve(cwd);
121
+ const full = path.resolve(root, target || ".");
122
+ if (full !== root && !full.startsWith(root + path.sep)) {
123
+ throw new Error(`caminho fora do diretorio de trabalho: ${target}`);
124
+ }
125
+ return full;
126
+ }
127
+
128
+ function relative(cwd, full) {
129
+ const rel = path.relative(path.resolve(cwd), full);
130
+ return rel === "" ? "." : rel;
131
+ }
132
+
133
+ function truncate(text, max) {
134
+ if (text.length <= max) return text;
135
+ return `${text.slice(0, max)}\n... (cortado, ${text.length - max} caracteres a mais)`;
136
+ }
137
+
138
+ function readTextFile(cwd, target) {
139
+ const full = resolveInside(cwd, target);
140
+ if (!fs.existsSync(full)) throw new Error(`arquivo nao encontrado: ${target}`);
141
+ if (fs.statSync(full).isDirectory()) throw new Error(`${target} e um diretorio`);
142
+ return { full, content: fs.readFileSync(full, "utf8") };
143
+ }
144
+
145
+ function readFile(cwd, args) {
146
+ const { full, content: raw } = readTextFile(cwd, args.path);
147
+
148
+ const offset = Math.max(1, Number(args.offset) || 1);
149
+ const limit = Math.min(MAX_READ_LINES, Math.max(1, Number(args.limit) || MAX_READ_LINES));
150
+ const lines = raw.split("\n");
151
+ const slice = lines.slice(offset - 1, offset - 1 + limit);
152
+ const header =
153
+ offset > 1 || slice.length < lines.length
154
+ ? `# ${relative(cwd, full)} linhas ${offset}-${offset + slice.length - 1} de ${lines.length}\n`
155
+ : "";
156
+ return truncate(header + slice.join("\n"), MAX_READ_CHARS);
157
+ }
158
+
159
+ function writeFile(cwd, args) {
160
+ const full = resolveInside(cwd, args.path);
161
+ const content = String(args.content ?? "");
162
+ if (content.length > MAX_WRITE_CHARS) {
163
+ throw new Error(`conteudo grande demais (${content.length} chars, max ${MAX_WRITE_CHARS})`);
164
+ }
165
+ fs.mkdirSync(path.dirname(full), { recursive: true });
166
+ fs.writeFileSync(full, content, "utf8");
167
+ return `ok: ${relative(cwd, full)} (${content.length} caracteres escritos)`;
168
+ }
169
+
170
+ /** Aplica a troca de trecho; separado da escrita para permitir o preview. */
171
+ function applyEdit(cwd, args) {
172
+ const oldString = String(args.old_string ?? "");
173
+ const newString = String(args.new_string ?? "");
174
+ if (!oldString) throw new Error("old_string vazio: diga qual trecho deve sair");
175
+
176
+ const { full, content } = readTextFile(cwd, args.path);
177
+ const occurrences = content.split(oldString).length - 1;
178
+ if (!occurrences) throw new Error(`trecho nao encontrado em ${args.path}`);
179
+ if (occurrences > 1 && !args.replace_all) {
180
+ throw new Error(
181
+ `o trecho aparece ${occurrences} vezes em ${args.path}: inclua mais contexto ou use replace_all`
182
+ );
183
+ }
184
+
185
+ const after = args.replace_all
186
+ ? content.split(oldString).join(newString)
187
+ : content.replace(oldString, newString);
188
+ if (after.length > MAX_WRITE_CHARS) {
189
+ throw new Error(`resultado grande demais (${after.length} chars, max ${MAX_WRITE_CHARS})`);
190
+ }
191
+ return { full, before: content, after, occurrences };
192
+ }
193
+
194
+ function editFile(cwd, args) {
195
+ const { full, after, occurrences } = applyEdit(cwd, args);
196
+ fs.writeFileSync(full, after, "utf8");
197
+ return `ok: ${relative(cwd, full)} editado (${occurrences} trecho${occurrences > 1 ? "s" : ""})`;
198
+ }
199
+
200
+ function listDir(cwd, args) {
201
+ const full = resolveInside(cwd, args.path);
202
+ if (!fs.existsSync(full)) throw new Error(`diretorio nao encontrado: ${args.path || "."}`);
203
+ const entries = fs.readdirSync(full, { withFileTypes: true });
204
+ const rows = entries
205
+ .sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name))
206
+ .slice(0, MAX_LIST_ENTRIES)
207
+ .map((entry) => (entry.isDirectory() ? `${entry.name}/` : entry.name));
208
+ const extra = entries.length > rows.length ? `\n... (+${entries.length - rows.length})` : "";
209
+ return `${relative(cwd, full)}\n${rows.join("\n")}${extra}`;
210
+ }
211
+
212
+ function runCommand(cwd, args) {
213
+ const command = String(args.command || "").trim();
214
+ if (!command) throw new Error("comando vazio");
215
+ const timeout = Math.min(120_000, Math.max(1_000, Number(args.timeout_ms) || COMMAND_TIMEOUT_MS));
216
+
217
+ const result = spawnSync(command, {
218
+ shell: true,
219
+ cwd: path.resolve(cwd),
220
+ timeout,
221
+ encoding: "utf8",
222
+ maxBuffer: 8 * 1024 * 1024,
223
+ env: { ...process.env, KAFOX_CLI: "1" },
224
+ });
225
+
226
+ const stdout = truncate(result.stdout || "", MAX_OUTPUT_CHARS);
227
+ const stderr = truncate(result.stderr || "", MAX_OUTPUT_CHARS);
228
+ const parts = [`exit: ${result.status == null ? "?" : result.status}`];
229
+ if (result.error?.code === "ETIMEDOUT") parts.push(`timeout: ${timeout}ms`);
230
+ if (stdout.trim()) parts.push(`stdout:\n${stdout.trimEnd()}`);
231
+ if (stderr.trim()) parts.push(`stderr:\n${stderr.trimEnd()}`);
232
+ return parts.join("\n");
233
+ }
234
+
235
+ async function executeTool(name, args = {}, context = {}) {
236
+ const cwd = context.cwd || process.cwd();
237
+ if (!LOCAL_TOOL_NAMES.has(name)) {
238
+ if (context.mcp?.has?.(name)) return context.mcp.call(name, args);
239
+ throw new Error(`ferramenta desconhecida: ${name}`);
240
+ }
241
+ switch (name) {
242
+ case "read_file":
243
+ return readFile(cwd, args);
244
+ case "write_file":
245
+ return writeFile(cwd, args);
246
+ case "edit_file":
247
+ return editFile(cwd, args);
248
+ case "list_dir":
249
+ return listDir(cwd, args);
250
+ case "run_command":
251
+ return runCommand(cwd, args);
252
+ default:
253
+ throw new Error(`ferramenta desconhecida: ${name}`);
254
+ }
255
+ }
256
+
257
+ /**
258
+ * O que a ferramenta vai mudar no arquivo, sem escrever nada.
259
+ * Usado no modal de aprovacao e no diff mostrado na conversa.
260
+ */
261
+ function previewToolChange(name, args = {}, context = {}) {
262
+ const cwd = context.cwd || process.cwd();
263
+ if (name === "write_file") {
264
+ const full = resolveInside(cwd, args.path);
265
+ const exists = fs.existsSync(full) && !fs.statSync(full).isDirectory();
266
+ return {
267
+ kind: exists ? "update" : "create",
268
+ path: relative(cwd, full),
269
+ before: exists ? fs.readFileSync(full, "utf8") : null,
270
+ after: String(args.content ?? ""),
271
+ };
272
+ }
273
+ if (name === "edit_file") {
274
+ try {
275
+ const { full, before, after, occurrences } = applyEdit(cwd, args);
276
+ return { kind: "edit", path: relative(cwd, full), before, after, occurrences };
277
+ } catch (error) {
278
+ return {
279
+ kind: "edit",
280
+ path: String(args.path || ""),
281
+ before: null,
282
+ after: null,
283
+ error: error.message,
284
+ };
285
+ }
286
+ }
287
+ if (name === "run_command") {
288
+ return { kind: "command", command: String(args.command || "") };
289
+ }
290
+ return null;
291
+ }
292
+
293
+ /** Resumo curto da chamada para mostrar na conversa. */
294
+ function describeToolCall(name, args = {}) {
295
+ if (name === "read_file") return args.path || "(arquivo)";
296
+ if (name === "write_file") return `${args.path || "(arquivo)"} (${String(args.content || "").length} chars)`;
297
+ if (name === "edit_file") return `${args.path || "(arquivo)"}`;
298
+ if (name === "list_dir") return args.path || ".";
299
+ if (name === "run_command") return args.command || "(comando)";
300
+ return "";
301
+ }
302
+
303
+ /**
304
+ * Separa a saida de run_command no que a TUI precisa mostrar:
305
+ * codigo de saida, se deu timeout, stdout e stderr.
306
+ */
307
+ function parseCommandOutput(output) {
308
+ const result = { exit: null, timedOut: false, stdout: [], stderr: [] };
309
+ let bucket = null;
310
+
311
+ for (const raw of String(output ?? "").split("\n")) {
312
+ const line = raw.replace(/\s+$/, "");
313
+ const exit = line.match(/^exit:\s*(.+)$/);
314
+ if (exit) {
315
+ result.exit = exit[1].trim() === "?" ? null : Number(exit[1]);
316
+ continue;
317
+ }
318
+ if (/^timeout:\s*\d+ms$/.test(line)) {
319
+ result.timedOut = true;
320
+ continue;
321
+ }
322
+ if (line === "stdout:") {
323
+ bucket = "stdout";
324
+ continue;
325
+ }
326
+ if (line === "stderr:") {
327
+ bucket = "stderr";
328
+ continue;
329
+ }
330
+ if (bucket) result[bucket].push(line);
331
+ }
332
+
333
+ return result;
334
+ }
335
+
336
+ /** Uma linha de resumo do resultado (primeira linha util da saida). */
337
+ function summarizeOutput(output) {
338
+ const first = String(output || "")
339
+ .split("\n")
340
+ .map((line) => line.trim())
341
+ .filter(Boolean)[0];
342
+ return first ? first.slice(0, 200) : "(sem saida)";
343
+ }
344
+
345
+ module.exports = {
346
+ TOOL_SPECS,
347
+ TOOL_NAMES,
348
+ LOCAL_TOOL_NAMES,
349
+ APPROVAL_REQUIRED,
350
+ TOOL_META,
351
+ toolMeta,
352
+ executeTool,
353
+ previewToolChange,
354
+ describeToolCall,
355
+ summarizeOutput,
356
+ parseCommandOutput,
357
+ resolveInside,
358
+ MAX_WRITE_CHARS,
359
+ };