@ikyyofc/gemini-cli 1.0.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/tools.js ADDED
@@ -0,0 +1,361 @@
1
+ // src/tools.js — tool definitions (native Gemini schema) + executor
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { promisify } from "util";
5
+ import { exec } from "child_process";
6
+ import readline from "readline";
7
+ import chalk from "chalk";
8
+
9
+ const execAsync = promisify(exec);
10
+
11
+ // ─────────────────────────────────────────────────────────────────
12
+ // FUNCTION DECLARATIONS (sent directly in Gemini API payload)
13
+ // Format: OpenAPI-subset schema — type MUST be uppercase STRING/OBJECT/etc.
14
+ // ─────────────────────────────────────────────────────────────────
15
+ export const FUNCTION_DECLARATIONS = [
16
+ {
17
+ name: "read_file",
18
+ description: "Read the full contents of a file. Returns numbered lines. Always read a file before editing it.",
19
+ parameters: {
20
+ type: "OBJECT",
21
+ properties: {
22
+ path: { type: "STRING", description: "Absolute or relative path to the file" }
23
+ },
24
+ required: ["path"]
25
+ }
26
+ },
27
+ {
28
+ name: "write_file",
29
+ description: "Create or overwrite a file with the provided content. Creates parent directories automatically.",
30
+ parameters: {
31
+ type: "OBJECT",
32
+ properties: {
33
+ path: { type: "STRING", description: "File path to write" },
34
+ content: { type: "STRING", description: "Complete file content to write" }
35
+ },
36
+ required: ["path", "content"]
37
+ }
38
+ },
39
+ {
40
+ name: "patch_file",
41
+ description: "Replace a specific unique string in a file with a new string. Use for targeted edits — much safer than rewriting whole files. The old_str must appear exactly once.",
42
+ parameters: {
43
+ type: "OBJECT",
44
+ properties: {
45
+ path: { type: "STRING", description: "File path to patch" },
46
+ old_str: { type: "STRING", description: "Exact unique string to find (with surrounding context if needed)" },
47
+ new_str: { type: "STRING", description: "Replacement string (can be empty to delete)" }
48
+ },
49
+ required: ["path", "old_str", "new_str"]
50
+ }
51
+ },
52
+ {
53
+ name: "append_file",
54
+ description: "Append text to the end of an existing file (or create it).",
55
+ parameters: {
56
+ type: "OBJECT",
57
+ properties: {
58
+ path: { type: "STRING", description: "File path" },
59
+ content: { type: "STRING", description: "Content to append" }
60
+ },
61
+ required: ["path", "content"]
62
+ }
63
+ },
64
+ {
65
+ name: "list_dir",
66
+ description: "List files and subdirectories in a directory. Excludes node_modules and .git by default.",
67
+ parameters: {
68
+ type: "OBJECT",
69
+ properties: {
70
+ path: { type: "STRING", description: "Directory path (default: current working directory)" },
71
+ show_hidden: { type: "BOOLEAN", description: "Include hidden files (default false)" }
72
+ }
73
+ }
74
+ },
75
+ {
76
+ name: "find_files",
77
+ description: "Find files matching a name pattern (glob) recursively. Excludes node_modules and .git.",
78
+ parameters: {
79
+ type: "OBJECT",
80
+ properties: {
81
+ pattern: { type: "STRING", description: "Name glob e.g. '*.js', 'index.*', 'GEMINI.md'" },
82
+ dir: { type: "STRING", description: "Root directory to search (default: .)" }
83
+ },
84
+ required: ["pattern"]
85
+ }
86
+ },
87
+ {
88
+ name: "search_in_files",
89
+ description: "Search for a text pattern (grep) inside files recursively. Returns matching lines with filenames and line numbers.",
90
+ parameters: {
91
+ type: "OBJECT",
92
+ properties: {
93
+ pattern: { type: "STRING", description: "Text or regex pattern to search" },
94
+ dir: { type: "STRING", description: "Directory to search (default: .)" },
95
+ extension: { type: "STRING", description: "File extension filter e.g. '.js', '.py'" },
96
+ case_insensitive: { type: "BOOLEAN", description: "Case-insensitive search (default false)" }
97
+ },
98
+ required: ["pattern"]
99
+ }
100
+ },
101
+ {
102
+ name: "run_shell",
103
+ description: "Execute any shell command. Use for: npm/pip install, git operations, running tests, building, compiling, curl, etc. Returns stdout and stderr.",
104
+ parameters: {
105
+ type: "OBJECT",
106
+ properties: {
107
+ command: { type: "STRING", description: "Shell command to execute" },
108
+ cwd: { type: "STRING", description: "Working directory (default: current)" },
109
+ timeout: { type: "NUMBER", description: "Timeout in milliseconds (default 30000)" }
110
+ },
111
+ required: ["command"]
112
+ }
113
+ },
114
+ {
115
+ name: "create_dir",
116
+ description: "Create a directory (and all parent directories). Equivalent to mkdir -p.",
117
+ parameters: {
118
+ type: "OBJECT",
119
+ properties: {
120
+ path: { type: "STRING", description: "Directory path to create" }
121
+ },
122
+ required: ["path"]
123
+ }
124
+ },
125
+ {
126
+ name: "delete_file",
127
+ description: "Delete a file or empty directory permanently.",
128
+ parameters: {
129
+ type: "OBJECT",
130
+ properties: {
131
+ path: { type: "STRING", description: "Path to delete" }
132
+ },
133
+ required: ["path"]
134
+ }
135
+ },
136
+ {
137
+ name: "move_file",
138
+ description: "Move or rename a file or directory.",
139
+ parameters: {
140
+ type: "OBJECT",
141
+ properties: {
142
+ from: { type: "STRING", description: "Source path" },
143
+ to: { type: "STRING", description: "Destination path" }
144
+ },
145
+ required: ["from", "to"]
146
+ }
147
+ },
148
+ {
149
+ name: "get_env",
150
+ description: "Get information about the current environment: working directory, platform, node version, git branch, etc.",
151
+ parameters: { type: "OBJECT", properties: {} }
152
+ },
153
+ {
154
+ name: "read_url",
155
+ description: "Fetch the raw content of a URL (web page, REST API, raw file). Returns up to 50KB.",
156
+ parameters: {
157
+ type: "OBJECT",
158
+ properties: {
159
+ url: { type: "STRING", description: "URL to fetch" },
160
+ headers: { type: "STRING", description: "JSON string of HTTP headers e.g. '{\"Authorization\":\"Bearer token\"}'" }
161
+ },
162
+ required: ["url"]
163
+ }
164
+ }
165
+ ];
166
+
167
+ // The tools array to pass directly into the Gemini API payload
168
+ export const GEMINI_TOOLS = [{ functionDeclarations: FUNCTION_DECLARATIONS }];
169
+
170
+ // ─────────────────────────────────────────────────────────────────
171
+ // CONFIRMATION PROMPT
172
+ // ─────────────────────────────────────────────────────────────────
173
+ const DESTRUCTIVE = new Set(["write_file","patch_file","append_file","run_shell","create_dir","delete_file","move_file"]);
174
+
175
+ export async function askConfirm(label, autoApprove) {
176
+ if (autoApprove) return true;
177
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
178
+ return new Promise(res => {
179
+ rl.question(
180
+ "\n" +
181
+ chalk.hex("#DCDCAA")(" ⚡ ") + label + "\n" +
182
+ chalk.hex("#858585")(" Allow? ") + chalk.hex("#4EC9B0")("[y]") + chalk.hex("#858585")("/") + chalk.hex("#F44747")("[n]") + chalk.hex("#858585")(" › "),
183
+ ans => { rl.close(); res(ans.trim().toLowerCase() !== "n"); }
184
+ );
185
+ });
186
+ }
187
+
188
+ // ─────────────────────────────────────────────────────────────────
189
+ // TOOL EXECUTOR
190
+ // ─────────────────────────────────────────────────────────────────
191
+ export async function executeTool(name, args = {}, { autoApprove = false } = {}) {
192
+ if (DESTRUCTIVE.has(name)) {
193
+ const label = buildConfirmLabel(name, args);
194
+ const ok = await askConfirm(label, autoApprove);
195
+ if (!ok) return { error: `Tool "${name}" declined by user.` };
196
+ }
197
+
198
+ try {
199
+ switch (name) {
200
+ case "read_file": {
201
+ const p = path.resolve(args.path);
202
+ if (!fs.existsSync(p)) return { error: `File not found: ${p}` };
203
+ const content = fs.readFileSync(p, "utf8");
204
+ const lines = content.split("\n");
205
+ const MAX = 400;
206
+ const numbered = lines
207
+ .slice(0, MAX)
208
+ .map((l, i) => `${String(i+1).padStart(4)} │ ${l}`)
209
+ .join("\n");
210
+ const suffix = lines.length > MAX ? `\n\n[...${lines.length - MAX} more lines not shown]` : "";
211
+ return { result: numbered + suffix, lines: lines.length, path: p };
212
+ }
213
+
214
+ case "write_file": {
215
+ const p = path.resolve(args.path);
216
+ fs.mkdirSync(path.dirname(p), { recursive: true });
217
+ fs.writeFileSync(p, args.content, "utf8");
218
+ const lines = (args.content.match(/\n/g) ?? []).length + 1;
219
+ return { result: `Written ${lines} lines to ${p}` };
220
+ }
221
+
222
+ case "patch_file": {
223
+ const p = path.resolve(args.path);
224
+ if (!fs.existsSync(p)) return { error: `File not found: ${p}` };
225
+ const src = fs.readFileSync(p, "utf8");
226
+ if (!src.includes(args.old_str))
227
+ return { error: "old_str not found in file. Make sure it matches exactly (whitespace, newlines)." };
228
+ const count = src.split(args.old_str).length - 1;
229
+ if (count > 1)
230
+ return { error: `old_str appears ${count} times — must be unique. Add more surrounding context.` };
231
+ fs.writeFileSync(p, src.replace(args.old_str, args.new_str), "utf8");
232
+ return { result: `Patched ${p} (1 replacement)` };
233
+ }
234
+
235
+ case "append_file": {
236
+ const p = path.resolve(args.path);
237
+ fs.mkdirSync(path.dirname(p), { recursive: true });
238
+ fs.appendFileSync(p, args.content, "utf8");
239
+ return { result: `Appended to ${p}` };
240
+ }
241
+
242
+ case "list_dir": {
243
+ const p = path.resolve(args.path || ".");
244
+ if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
245
+ const items = fs.readdirSync(p, { withFileTypes: true });
246
+ const filtered = args.show_hidden ? items : items.filter(i => !i.name.startsWith("."));
247
+ const SKIP = new Set(["node_modules", ".git"]);
248
+ const rows = filtered
249
+ .filter(i => !SKIP.has(i.name))
250
+ .map(i => {
251
+ const st = fs.statSync(path.join(p, i.name));
252
+ const size = i.isDirectory() ? "<DIR>" : fmtBytes(st.size);
253
+ return `${size.padStart(8)} ${i.isDirectory() ? chalk.hex("#569CD6")(i.name+"/") : i.name}`;
254
+ });
255
+ return { result: `${p}\n${"─".repeat(50)}\n${rows.join("\n")}\n\n${rows.length} items` };
256
+ }
257
+
258
+ case "find_files": {
259
+ const dir = path.resolve(args.dir || ".");
260
+ const { stdout } = await execAsync(
261
+ `find "${dir}" -name "${args.pattern}" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | head -200`
262
+ );
263
+ return { result: stdout.trim() || "No files found." };
264
+ }
265
+
266
+ case "search_in_files": {
267
+ const dir = path.resolve(args.dir || ".");
268
+ const flag = args.case_insensitive ? "-i" : "";
269
+ const ext = args.extension ? `--include="*${args.extension}"` : "";
270
+ const { stdout } = await execAsync(
271
+ `grep -rn ${flag} ${ext} "${args.pattern}" "${dir}" --exclude-dir=node_modules --exclude-dir=.git 2>/dev/null | head -100`
272
+ ).catch(e => ({ stdout: e.stdout || "" }));
273
+ return { result: stdout.trim() || "No matches found." };
274
+ }
275
+
276
+ case "run_shell": {
277
+ const cwd = args.cwd ? path.resolve(args.cwd) : process.cwd();
278
+ const timeout = args.timeout || 30000;
279
+ try {
280
+ const { stdout, stderr } = await execAsync(args.command, { cwd, timeout, maxBuffer: 5*1024*1024 });
281
+ const out = [stdout?.trim(), stderr?.trim() ? `[stderr]\n${stderr.trim()}` : null].filter(Boolean).join("\n");
282
+ return { result: out || "(no output)", exitCode: 0 };
283
+ } catch (err) {
284
+ const out = [err.stdout?.trim(), err.stderr?.trim() ? `[stderr]\n${err.stderr.trim()}` : null, `[exit ${err.code}]`].filter(Boolean).join("\n");
285
+ return { result: out, exitCode: err.code };
286
+ }
287
+ }
288
+
289
+ case "create_dir": {
290
+ const p = path.resolve(args.path);
291
+ fs.mkdirSync(p, { recursive: true });
292
+ return { result: `Created: ${p}` };
293
+ }
294
+
295
+ case "delete_file": {
296
+ const p = path.resolve(args.path);
297
+ if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
298
+ fs.statSync(p).isDirectory() ? fs.rmdirSync(p) : fs.unlinkSync(p);
299
+ return { result: `Deleted: ${p}` };
300
+ }
301
+
302
+ case "move_file": {
303
+ const from = path.resolve(args.from), to = path.resolve(args.to);
304
+ fs.mkdirSync(path.dirname(to), { recursive: true });
305
+ fs.renameSync(from, to);
306
+ return { result: `Moved: ${from} → ${to}` };
307
+ }
308
+
309
+ case "get_env": {
310
+ let branch = "";
311
+ try { branch = (await execAsync("git branch --show-current 2>/dev/null")).stdout.trim(); } catch {}
312
+ return {
313
+ result: JSON.stringify({
314
+ cwd: process.cwd(),
315
+ platform: process.platform,
316
+ node: process.version,
317
+ git_branch: branch || null,
318
+ home: process.env.HOME,
319
+ shell: process.env.SHELL
320
+ }, null, 2)
321
+ };
322
+ }
323
+
324
+ case "read_url": {
325
+ let hdrs = "";
326
+ if (args.headers) {
327
+ try {
328
+ const obj = JSON.parse(args.headers);
329
+ hdrs = Object.entries(obj).map(([k,v]) => `-H "${k}: ${v}"`).join(" ");
330
+ } catch {}
331
+ }
332
+ const { stdout } = await execAsync(`curl -sL --max-time 15 ${hdrs} "${args.url}" | head -c 51200`);
333
+ return { result: stdout.trim() || "(empty response)" };
334
+ }
335
+
336
+ default:
337
+ return { error: `Unknown tool: ${name}` };
338
+ }
339
+ } catch (err) {
340
+ return { error: `${name} failed: ${err.message}` };
341
+ }
342
+ }
343
+
344
+ function buildConfirmLabel(name, args) {
345
+ switch (name) {
346
+ case "write_file": return `Write file: ${chalk.yellow(args.path)}`;
347
+ case "patch_file": return `Patch file: ${chalk.yellow(args.path)}`;
348
+ case "append_file": return `Append to: ${chalk.yellow(args.path)}`;
349
+ case "run_shell": return `Run shell: ${chalk.cyan(args.command)}`;
350
+ case "create_dir": return `Create dir: ${chalk.yellow(args.path)}`;
351
+ case "delete_file": return `${chalk.red("DELETE")}: ${chalk.yellow(args.path)}`;
352
+ case "move_file": return `Move: ${chalk.yellow(args.from)} → ${chalk.yellow(args.to)}`;
353
+ default: return `Execute: ${name}`;
354
+ }
355
+ }
356
+
357
+ function fmtBytes(n) {
358
+ if (n < 1024) return n + "B";
359
+ if (n < 1048576) return (n/1024).toFixed(1) + "K";
360
+ return (n/1048576).toFixed(1) + "M";
361
+ }