@ikyyofc/gemini-cli 4.0.0 → 4.0.2

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 CHANGED
@@ -1,4 +1,4 @@
1
- // src/tools.js — tool definitions (native Gemini schema) + executor
1
+ // src/tools.js — Tool definitions + executor
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import { promisify } from "util";
@@ -9,117 +9,80 @@ import chalk from "chalk";
9
9
  const execAsync = promisify(exec);
10
10
 
11
11
  // ─────────────────────────────────────────────────────────────────
12
- // FUNCTION DECLARATIONS (sent directly in Gemini API payload)
13
- // Format: OpenAPI-subset schema — type MUST be uppercase STRING/OBJECT/etc.
12
+ // FUNCTION DECLARATIONS (sent to Gemini API as native tools)
13
+ //
14
+ // Groups:
15
+ // File I/O — read, write, edit, delete, move files
16
+ // Directory — list, find, tree, search
17
+ // Execute — run_shell (universal)
18
+ // Git — version control operations
19
+ // Network — HTTP requests, download
20
+ // Data — JSON query
21
+ // System — environment info
22
+ // Real-time — web search, weather, news, finance, IP
14
23
  // ─────────────────────────────────────────────────────────────────
15
24
  export const FUNCTION_DECLARATIONS = [
25
+
26
+ // ── File I/O ────────────────────────────────────────────────
16
27
  {
17
28
  name: "read_file",
18
- description: "Read the full contents of a file. Returns numbered lines. Always read a file before editing it.",
29
+ description: "Read a file and return its content with line numbers. Always call this before editing a file.",
19
30
  parameters: {
20
31
  type: "OBJECT",
21
32
  properties: {
22
- path: { type: "STRING", description: "Absolute or relative path to the file" }
33
+ path: { type: "STRING", description: "Absolute or relative file path" }
23
34
  },
24
35
  required: ["path"]
25
36
  }
26
37
  },
27
38
  {
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.",
39
+ name: "read_file_lines",
40
+ description: "Read a specific range of lines from a file. Use for large files when you only need part of it.",
42
41
  parameters: {
43
42
  type: "OBJECT",
44
43
  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)" }
44
+ path: { type: "STRING", description: "File path" },
45
+ start: { type: "NUMBER", description: "Start line number (1-based)" },
46
+ end: { type: "NUMBER", description: "End line number (inclusive)" }
48
47
  },
49
- required: ["path", "old_str", "new_str"]
48
+ required: ["path", "start", "end"]
50
49
  }
51
50
  },
52
51
  {
53
- name: "append_file",
54
- description: "Append text to the end of an existing file (or create it).",
52
+ name: "write_file",
53
+ description: "Create or completely overwrite a file. Creates parent directories automatically. Use patch_file for small edits.",
55
54
  parameters: {
56
55
  type: "OBJECT",
57
56
  properties: {
58
57
  path: { type: "STRING", description: "File path" },
59
- content: { type: "STRING", description: "Content to append" }
58
+ content: { type: "STRING", description: "Complete file content" }
60
59
  },
61
60
  required: ["path", "content"]
62
61
  }
63
62
  },
64
63
  {
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.",
64
+ name: "patch_file",
65
+ description: "Replace a specific unique string inside a file. Safer than rewriting the whole file. The old_str must appear exactly once — add more surrounding context if needed.",
104
66
  parameters: {
105
67
  type: "OBJECT",
106
68
  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)" }
69
+ path: { type: "STRING", description: "File path" },
70
+ old_str: { type: "STRING", description: "Exact unique string to find (with surrounding context)" },
71
+ new_str: { type: "STRING", description: "Replacement string (can be empty to delete)" }
110
72
  },
111
- required: ["command"]
73
+ required: ["path", "old_str", "new_str"]
112
74
  }
113
75
  },
114
76
  {
115
- name: "create_dir",
116
- description: "Create a directory (and all parent directories). Equivalent to mkdir -p.",
77
+ name: "append_file",
78
+ description: "Append content to the end of a file (or create it if it doesn't exist).",
117
79
  parameters: {
118
80
  type: "OBJECT",
119
81
  properties: {
120
- path: { type: "STRING", description: "Directory path to create" }
82
+ path: { type: "STRING", description: "File path" },
83
+ content: { type: "STRING", description: "Content to append" }
121
84
  },
122
- required: ["path"]
85
+ required: ["path", "content"]
123
86
  }
124
87
  },
125
88
  {
@@ -128,7 +91,7 @@ export const FUNCTION_DECLARATIONS = [
128
91
  parameters: {
129
92
  type: "OBJECT",
130
93
  properties: {
131
- path: { type: "STRING", description: "Path to delete" }
94
+ path: { type: "STRING", description: "File or directory path to delete" }
132
95
  },
133
96
  required: ["path"]
134
97
  }
@@ -146,348 +109,317 @@ export const FUNCTION_DECLARATIONS = [
146
109
  }
147
110
  },
148
111
  {
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.",
112
+ name: "file_info",
113
+ description: "Get metadata of a file or directory: size, permissions, modified date, type.",
156
114
  parameters: {
157
115
  type: "OBJECT",
158
116
  properties: {
159
- url: { type: "STRING", description: "URL to fetch" },
160
- headers: { type: "STRING", description: "JSON string of HTTP headers e.g. '{\"Authorization\":\"Bearer token\"}'" }
117
+ path: { type: "STRING", description: "File or directory path" }
161
118
  },
162
- required: ["url"]
119
+ required: ["path"]
163
120
  }
164
121
  },
165
- // ── NEW TOOLS ────────────────────────────────────────────────
166
122
  {
167
- name: "copy_file",
168
- description: "Copy a file or directory to a new location.",
123
+ name: "diff_files",
124
+ description: "Show line-by-line differences between two files (like git diff).",
169
125
  parameters: {
170
126
  type: "OBJECT",
171
127
  properties: {
172
- from: { type: "STRING", description: "Source path" },
173
- to: { type: "STRING", description: "Destination path" },
174
- recursive: { type: "BOOLEAN", description: "Copy directory recursively (default true)" }
128
+ file_a: { type: "STRING", description: "First file path (original)" },
129
+ file_b: { type: "STRING", description: "Second file path (modified)" }
175
130
  },
176
- required: ["from", "to"]
131
+ required: ["file_a", "file_b"]
177
132
  }
178
133
  },
134
+
135
+ // ── Directory ────────────────────────────────────────────────
179
136
  {
180
- name: "read_file_lines",
181
- description: "Read a specific range of lines from a file. Use for large files.",
137
+ name: "list_dir",
138
+ description: "List files and subdirectories in a path. Use before navigating or editing a project.",
182
139
  parameters: {
183
140
  type: "OBJECT",
184
141
  properties: {
185
- path: { type: "STRING", description: "File path" },
186
- start: { type: "NUMBER", description: "Start line number (1-based)" },
187
- end: { type: "NUMBER", description: "End line number (inclusive)" }
188
- },
189
- required: ["path", "start", "end"]
142
+ path: { type: "STRING", description: "Directory path (default: current directory)" },
143
+ show_hidden: { type: "BOOLEAN", description: "Include hidden files (default: false)" }
144
+ }
190
145
  }
191
146
  },
192
147
  {
193
148
  name: "tree",
194
- description: "Show directory structure as a tree. Good for understanding project layout.",
149
+ description: "Show a project's directory structure as a visual tree. Great for understanding a codebase layout.",
195
150
  parameters: {
196
151
  type: "OBJECT",
197
152
  properties: {
198
153
  path: { type: "STRING", description: "Root directory (default: .)" },
199
- depth: { type: "NUMBER", description: "Max depth (default: 3)" },
154
+ depth: { type: "NUMBER", description: "Max depth to show (default: 3)" },
200
155
  show_hidden: { type: "BOOLEAN", description: "Include hidden files" }
201
156
  }
202
157
  }
203
158
  },
204
159
  {
205
- name: "file_info",
206
- description: "Get file or directory metadata: size, permissions, modified date, type.",
207
- parameters: {
208
- type: "OBJECT",
209
- properties: {
210
- path: { type: "STRING", description: "File or directory path" }
211
- },
212
- required: ["path"]
213
- }
214
- },
215
- {
216
- name: "diff_files",
217
- description: "Show line-by-line differences between two files.",
218
- parameters: {
219
- type: "OBJECT",
220
- properties: {
221
- file_a: { type: "STRING", description: "First file path" },
222
- file_b: { type: "STRING", description: "Second file path" }
223
- },
224
- required: ["file_a", "file_b"]
225
- }
226
- },
227
- {
228
- name: "http_request",
229
- description: "Make an HTTP request (GET/POST/PUT/PATCH/DELETE) with headers and body.",
160
+ name: "find_files",
161
+ description: "Find files matching a name pattern (glob) recursively. Excludes node_modules and .git.",
230
162
  parameters: {
231
163
  type: "OBJECT",
232
164
  properties: {
233
- url: { type: "STRING", description: "URL to request" },
234
- method: { type: "STRING", description: "HTTP method (default: GET)" },
235
- headers: { type: "STRING", description: "JSON string of request headers" },
236
- body: { type: "STRING", description: "Request body for POST/PUT/PATCH" }
165
+ pattern: { type: "STRING", description: "Name glob e.g. '*.js', 'index.*', '*.config.*'" },
166
+ dir: { type: "STRING", description: "Root directory to search (default: .)" }
237
167
  },
238
- required: ["url"]
168
+ required: ["pattern"]
239
169
  }
240
170
  },
241
171
  {
242
- name: "download_file",
243
- description: "Download a file from a URL and save it to disk.",
172
+ name: "search_in_files",
173
+ description: "Search for a text pattern (grep) across files recursively. Returns matching lines with filenames and line numbers.",
244
174
  parameters: {
245
175
  type: "OBJECT",
246
176
  properties: {
247
- url: { type: "STRING", description: "URL to download" },
248
- dest: { type: "STRING", description: "Destination file path" }
177
+ pattern: { type: "STRING", description: "Text or regex to search" },
178
+ dir: { type: "STRING", description: "Directory to search (default: .)" },
179
+ extension: { type: "STRING", description: "File extension filter e.g. '.js', '.py'" },
180
+ case_insensitive: { type: "BOOLEAN", description: "Case-insensitive search" }
249
181
  },
250
- required: ["url", "dest"]
182
+ required: ["pattern"]
251
183
  }
252
184
  },
253
185
  {
254
- name: "hash_file",
255
- description: "Calculate hash of a file (md5, sha1, sha256).",
186
+ name: "create_dir",
187
+ description: "Create a directory and all necessary parent directories (mkdir -p).",
256
188
  parameters: {
257
189
  type: "OBJECT",
258
190
  properties: {
259
- path: { type: "STRING", description: "File path" },
260
- algorithm: { type: "STRING", description: "md5 | sha1 | sha256 (default: sha256)" }
191
+ path: { type: "STRING", description: "Directory path to create" }
261
192
  },
262
193
  required: ["path"]
263
194
  }
264
195
  },
196
+
197
+ // ── Execute ──────────────────────────────────────────────────
265
198
  {
266
- name: "base64",
267
- description: "Encode or decode base64. Works on strings or files.",
199
+ name: "run_shell",
200
+ description: "Execute any shell command and return output. Use for: npm/pip install, tests, builds, git, system commands, file operations, compression, hashing, etc.",
268
201
  parameters: {
269
202
  type: "OBJECT",
270
203
  properties: {
271
- action: { type: "STRING", description: "encode or decode" },
272
- input: { type: "STRING", description: "String to process, or file path if is_file=true" },
273
- is_file: { type: "BOOLEAN", description: "If true, input is treated as a file path" }
204
+ command: { type: "STRING", description: "Shell command to run" },
205
+ cwd: { type: "STRING", description: "Working directory (default: current)" },
206
+ timeout: { type: "NUMBER", description: "Timeout in ms (default: 30000)" }
274
207
  },
275
- required: ["action", "input"]
208
+ required: ["command"]
276
209
  }
277
210
  },
211
+
212
+ // ── Git ──────────────────────────────────────────────────────
278
213
  {
279
- name: "json_query",
280
- description: "Parse and query JSON using a dot-notation key path. e.g. 'user.name', 'items[0].id'.",
214
+ name: "git",
215
+ description: "Run git operations with structured output. Actions: status, log, diff, add, commit, push, pull, clone, checkout, branch, init, stash, merge.",
281
216
  parameters: {
282
217
  type: "OBJECT",
283
218
  properties: {
284
- input: { type: "STRING", description: "JSON file path or raw JSON string" },
285
- query: { type: "STRING", description: "Key path e.g. 'user.name' (empty = pretty-print all)" },
286
- is_file: { type: "BOOLEAN", description: "If true, input is a file path" }
219
+ action: { type: "STRING", description: "status | log | diff | add | commit | push | pull | clone | checkout | branch | init | stash | merge" },
220
+ args: { type: "STRING", description: "Additional arguments (commit message, branch name, remote URL, etc.)" },
221
+ cwd: { type: "STRING", description: "Working directory (default: current)" }
287
222
  },
288
- required: ["input"]
223
+ required: ["action"]
289
224
  }
290
225
  },
226
+
227
+ // ── Network ──────────────────────────────────────────────────
291
228
  {
292
- name: "extract",
293
- description: "Extract a zip, tar, tar.gz, or tar.bz2 archive.",
229
+ name: "http_request",
230
+ description: "Make an HTTP request (GET, POST, PUT, PATCH, DELETE) with custom method, headers, and body. Use for REST APIs, webhooks, GraphQL, etc.",
294
231
  parameters: {
295
232
  type: "OBJECT",
296
233
  properties: {
297
- archive: { type: "STRING", description: "Path to the archive file" },
298
- dest: { type: "STRING", description: "Destination directory (default: same as archive)" }
234
+ url: { type: "STRING", description: "URL to request" },
235
+ method: { type: "STRING", description: "HTTP method: GET POST PUT PATCH DELETE (default: GET)" },
236
+ headers: { type: "STRING", description: "JSON string of request headers e.g. '{\"Authorization\":\"Bearer token\"}'" },
237
+ body: { type: "STRING", description: "Request body (for POST/PUT/PATCH)" }
299
238
  },
300
- required: ["archive"]
239
+ required: ["url"]
301
240
  }
302
241
  },
303
242
  {
304
- name: "compress",
305
- description: "Compress files or a directory into a zip or tar.gz archive.",
243
+ name: "download_file",
244
+ description: "Download a file from a URL and save it to a local path.",
306
245
  parameters: {
307
246
  type: "OBJECT",
308
247
  properties: {
309
- source: { type: "STRING", description: "File or directory to compress" },
310
- dest: { type: "STRING", description: "Output archive path (.zip or .tar.gz)" }
248
+ url: { type: "STRING", description: "URL to download" },
249
+ dest: { type: "STRING", description: "Local destination file path" }
311
250
  },
312
- required: ["source", "dest"]
251
+ required: ["url", "dest"]
313
252
  }
314
253
  },
254
+
255
+ // ── Data ─────────────────────────────────────────────────────
315
256
  {
316
- name: "git",
317
- description: "Run git operations: status, log, diff, add, commit, push, pull, clone, checkout, branch, init, stash.",
257
+ name: "json_query",
258
+ description: "Parse and query a JSON file or string using dot-notation path. e.g. 'user.name', 'items[0].id', 'data.results'. Returns the value at that path.",
318
259
  parameters: {
319
260
  type: "OBJECT",
320
261
  properties: {
321
- action: { type: "STRING", description: "status | log | diff | add | commit | push | pull | clone | checkout | branch | init | stash" },
322
- args: { type: "STRING", description: "Additional arguments (commit message, branch name, remote, etc.)" },
323
- cwd: { type: "STRING", description: "Working directory (default: current)" }
262
+ input: { type: "STRING", description: "JSON file path (if is_file=true) or raw JSON string" },
263
+ query: { type: "STRING", description: "Dot-notation path e.g. 'user.name' (empty = pretty-print all)" },
264
+ is_file: { type: "BOOLEAN", description: "If true, input is treated as a file path" }
324
265
  },
325
- required: ["action"]
326
- }
327
- },
328
- {
329
- name: "process_list",
330
- description: "List running processes. Filter by name optionally.",
331
- parameters: {
332
- type: "OBJECT",
333
- properties: {
334
- filter: { type: "STRING", description: "Filter by process name (optional)" }
335
- }
336
- }
337
- },
338
- {
339
- name: "disk_usage",
340
- description: "Check disk usage of a path (du) or total disk space (df). Use path='/' for overall.",
341
- parameters: {
342
- type: "OBJECT",
343
- properties: {
344
- path: { type: "STRING", description: "Path to check (default: current dir)" }
345
- }
266
+ required: ["input"]
346
267
  }
347
268
  },
269
+
270
+ // ── System ───────────────────────────────────────────────────
348
271
  {
349
- name: "chmod",
350
- description: "Change file permissions. e.g. '755', '+x', 'a+r'.",
351
- parameters: {
352
- type: "OBJECT",
353
- properties: {
354
- path: { type: "STRING", description: "File or directory path" },
355
- permissions: { type: "STRING", description: "Permission string: '755', '+x', 'a+r', etc." },
356
- recursive: { type: "BOOLEAN", description: "Apply recursively" }
357
- },
358
- required: ["path", "permissions"]
359
- }
272
+ name: "get_env",
273
+ description: "Get current environment info: working directory, platform, Node.js version, git branch, shell, home directory.",
274
+ parameters: { type: "OBJECT", properties: {} }
360
275
  },
361
276
 
362
- // ── REAL-TIME TOOLS ──────────────────────────────────────────
277
+ // ── Real-time ────────────────────────────────────────────────
363
278
  {
364
279
  name: "web_search",
365
- description: "Search the web for current information, news, facts, or anything up-to-date. Use this when you need real-time or recent information.",
280
+ description: "Search the web for current, real-time information. Use when the user asks about recent events, news, or anything that requires up-to-date data.",
366
281
  parameters: {
367
282
  type: "OBJECT",
368
283
  properties: {
369
284
  query: { type: "STRING", description: "Search query" },
370
- region: { type: "STRING", description: "Region code e.g. id-id, us-en, my-en (default: id-id)" }
285
+ region: { type: "STRING", description: "Region code e.g. id-id, us-en (default: id-id)" }
371
286
  },
372
287
  required: ["query"]
373
288
  }
374
289
  },
375
290
  {
376
291
  name: "get_weather",
377
- description: "Get current weather and forecast for any city or location.",
292
+ description: "Get current weather and conditions for any city or location.",
378
293
  parameters: {
379
294
  type: "OBJECT",
380
295
  properties: {
381
- location: { type: "STRING", description: "City name or location e.g. 'Jakarta', 'New York', 'Tokyo'" },
382
- format: { type: "STRING", description: "simple (1 line) | full (detailed JSON) — default: simple" }
296
+ location: { type: "STRING", description: "City or location name e.g. 'Jakarta', 'Surabaya', 'New York'" },
297
+ format: { type: "STRING", description: "simple (one line) | full (detailed) — default: simple" }
383
298
  },
384
299
  required: ["location"]
385
300
  }
386
301
  },
387
302
  {
388
303
  name: "get_news",
389
- description: "Get latest news headlines on any topic from around the web.",
304
+ description: "Fetch latest news headlines for any topic or keyword.",
390
305
  parameters: {
391
306
  type: "OBJECT",
392
307
  properties: {
393
- query: { type: "STRING", description: "Topic or keyword to search news for (e.g. 'AI', 'Indonesia', 'bitcoin')" },
394
- language: { type: "STRING", description: "Language code: id, en, etc. (default: id)" },
395
- limit: { type: "NUMBER", description: "Number of results (default: 10, max: 20)" }
308
+ query: { type: "STRING", description: "Topic or keyword e.g. 'teknologi', 'Indonesia', 'AI'" },
309
+ language: { type: "STRING", description: "Language: id or en (default: id)" },
310
+ limit: { type: "NUMBER", description: "Number of articles (default: 10)" }
396
311
  },
397
312
  required: ["query"]
398
313
  }
399
314
  },
400
315
  {
401
316
  name: "get_exchange_rate",
402
- description: "Get current currency exchange rates. Supports all major currencies.",
317
+ description: "Get current currency exchange rates for any pair.",
403
318
  parameters: {
404
319
  type: "OBJECT",
405
320
  properties: {
406
321
  from: { type: "STRING", description: "Base currency code e.g. USD, IDR, EUR (default: USD)" },
407
- to: { type: "STRING", description: "Target currency or comma-separated list e.g. 'IDR,EUR,JPY'" }
322
+ to: { type: "STRING", description: "Target currency or comma-separated e.g. 'IDR,EUR,JPY'" }
408
323
  }
409
324
  }
410
325
  },
411
326
  {
412
327
  name: "get_stock",
413
- description: "Get real-time or latest stock price and info for a ticker symbol.",
328
+ description: "Get real-time stock price and info by ticker symbol. Supports IDX (e.g. BBCA.JK) and US markets.",
414
329
  parameters: {
415
330
  type: "OBJECT",
416
331
  properties: {
417
- symbol: { type: "STRING", description: "Stock ticker symbol e.g. AAPL, GOOGL, BBCA.JK, TLKM.JK" }
332
+ symbol: { type: "STRING", description: "Ticker symbol e.g. AAPL, GOOGL, BBCA.JK, TLKM.JK" }
418
333
  },
419
334
  required: ["symbol"]
420
335
  }
421
336
  },
422
337
  {
423
338
  name: "get_crypto",
424
- description: "Get current cryptocurrency prices and market data.",
339
+ description: "Get real-time cryptocurrency prices and market data from CoinGecko.",
425
340
  parameters: {
426
341
  type: "OBJECT",
427
342
  properties: {
428
- coins: { type: "STRING", description: "Coin IDs comma-separated e.g. 'bitcoin,ethereum,solana'" },
429
- currency: { type: "STRING", description: "Fiat currency for prices e.g. usd, idr (default: usd)" }
343
+ coins: { type: "STRING", description: "CoinGecko coin IDs comma-separated e.g. 'bitcoin,ethereum,solana'" },
344
+ currency: { type: "STRING", description: "Fiat currency: usd, idr (default: usd)" }
430
345
  },
431
346
  required: ["coins"]
432
347
  }
433
348
  },
434
349
  {
435
350
  name: "get_ip_info",
436
- description: "Get geolocation and network info for an IP address, or your own public IP if none given.",
351
+ description: "Get geolocation and network info for an IP address. Leave ip empty to look up your own public IP.",
437
352
  parameters: {
438
353
  type: "OBJECT",
439
354
  properties: {
440
- ip: { type: "STRING", description: "IP address to look up (leave empty for your own IP)" }
355
+ ip: { type: "STRING", description: "IP address to look up (optional empty = your own IP)" }
441
356
  }
442
357
  }
443
358
  }
444
359
  ];
445
360
 
446
- // The tools array to pass directly into the Gemini API payload
361
+ // Gemini API tools payload
447
362
  export const GEMINI_TOOLS = [{ functionDeclarations: FUNCTION_DECLARATIONS }];
448
363
 
449
364
  // ─────────────────────────────────────────────────────────────────
450
- // CONFIRMATION PROMPT
365
+ // Confirmation for destructive actions
451
366
  // ─────────────────────────────────────────────────────────────────
452
- const DESTRUCTIVE = new Set(["write_file","patch_file","append_file","run_shell","create_dir","delete_file","move_file"]);
367
+ const DESTRUCTIVE = new Set(["write_file","patch_file","append_file","run_shell","create_dir","delete_file","move_file","download_file"]);
453
368
 
454
369
  export async function askConfirm(label, autoApprove) {
455
370
  if (autoApprove) return true;
456
371
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
457
372
  return new Promise(res => {
458
373
  rl.question(
459
- "\n" +
460
- chalk.hex("#DCDCAA")(" ") + label + "\n" +
461
- chalk.hex("#858585")(" Allow? ") + chalk.hex("#4EC9B0")("[y]") + chalk.hex("#858585")("/") + chalk.hex("#F44747")("[n]") + chalk.hex("#858585")(" › "),
374
+ "\n" + chalk.hex("#DCDCAA")(" ⚡ ") + label + "\n" +
375
+ chalk.hex("#858585")(" Allow? ") + chalk.hex("#4EC9B0")("[y]") +
376
+ chalk.hex("#858585")("/") + chalk.hex("#F44747")("[n]") +
377
+ chalk.hex("#858585")(" › "),
462
378
  ans => { rl.close(); res(ans.trim().toLowerCase() !== "n"); }
463
379
  );
464
380
  });
465
381
  }
466
382
 
467
383
  // ─────────────────────────────────────────────────────────────────
468
- // TOOL EXECUTOR
384
+ // Tool executor
469
385
  // ─────────────────────────────────────────────────────────────────
470
386
  export async function executeTool(name, args = {}, { autoApprove = false } = {}) {
471
387
  if (DESTRUCTIVE.has(name)) {
472
- const label = buildConfirmLabel(name, args);
473
- const ok = await askConfirm(label, autoApprove);
474
- if (!ok) return { error: `Tool "${name}" declined by user.` };
388
+ const label = confirmLabel(name, args);
389
+ if (!await askConfirm(label, autoApprove)) return { error: `Tool "${name}" declined by user.` };
475
390
  }
476
391
 
477
392
  try {
478
393
  switch (name) {
394
+
395
+ // ── File I/O ─────────────────────────────────────────
479
396
  case "read_file": {
480
397
  const p = path.resolve(args.path);
481
398
  if (!fs.existsSync(p)) return { error: `File not found: ${p}` };
482
399
  const content = fs.readFileSync(p, "utf8");
483
- const lines = content.split("\n");
484
- const MAX = 400;
485
- const numbered = lines
486
- .slice(0, MAX)
400
+ const lines = content.split("\n");
401
+ const MAX = 400;
402
+ const numbered = lines.slice(0, MAX)
487
403
  .map((l, i) => `${String(i+1).padStart(4)} │ ${l}`)
488
404
  .join("\n");
489
- const suffix = lines.length > MAX ? `\n\n[...${lines.length - MAX} more lines not shown]` : "";
490
- return { result: numbered + suffix, lines: lines.length, path: p };
405
+ const suffix = lines.length > MAX
406
+ ? `\n\n[...${lines.length - MAX} more lines use read_file_lines to read further]`
407
+ : "";
408
+ return { result: numbered + suffix, total_lines: lines.length, path: p };
409
+ }
410
+
411
+ case "read_file_lines": {
412
+ const p = path.resolve(args.path);
413
+ if (!fs.existsSync(p)) return { error: `File not found: ${p}` };
414
+ const lines = fs.readFileSync(p, "utf8").split("\n");
415
+ const start = Math.max(1, args.start) - 1;
416
+ const end = Math.min(lines.length, args.end);
417
+ const slice = lines.slice(start, end);
418
+ return {
419
+ result: slice.map((l, i) => `${String(start+i+1).padStart(4)} │ ${l}`).join("\n"),
420
+ total_lines: lines.length,
421
+ showing: `${start+1}-${end}`
422
+ };
491
423
  }
492
424
 
493
425
  case "write_file": {
@@ -495,7 +427,7 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
495
427
  fs.mkdirSync(path.dirname(p), { recursive: true });
496
428
  fs.writeFileSync(p, args.content, "utf8");
497
429
  const lines = (args.content.match(/\n/g) ?? []).length + 1;
498
- return { result: `Written ${lines} lines to ${p}` };
430
+ return { result: `Written ${lines} lines ${p}` };
499
431
  }
500
432
 
501
433
  case "patch_file": {
@@ -503,12 +435,12 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
503
435
  if (!fs.existsSync(p)) return { error: `File not found: ${p}` };
504
436
  const src = fs.readFileSync(p, "utf8");
505
437
  if (!src.includes(args.old_str))
506
- return { error: "old_str not found in file. Make sure it matches exactly (whitespace, newlines)." };
438
+ return { error: "old_str not found. Make sure it matches exactly (including whitespace and newlines)." };
507
439
  const count = src.split(args.old_str).length - 1;
508
440
  if (count > 1)
509
441
  return { error: `old_str appears ${count} times — must be unique. Add more surrounding context.` };
510
442
  fs.writeFileSync(p, src.replace(args.old_str, args.new_str), "utf8");
511
- return { result: `Patched ${p} (1 replacement)` };
443
+ return { result: `Patched ${p} (1 occurrence replaced)` };
512
444
  }
513
445
 
514
446
  case "append_file": {
@@ -518,59 +450,6 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
518
450
  return { result: `Appended to ${p}` };
519
451
  }
520
452
 
521
- case "list_dir": {
522
- const p = path.resolve(args.path || ".");
523
- if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
524
- const items = fs.readdirSync(p, { withFileTypes: true });
525
- const filtered = args.show_hidden ? items : items.filter(i => !i.name.startsWith("."));
526
- const SKIP = new Set(["node_modules", ".git"]);
527
- const rows = filtered
528
- .filter(i => !SKIP.has(i.name))
529
- .map(i => {
530
- const st = fs.statSync(path.join(p, i.name));
531
- const size = i.isDirectory() ? "<DIR>" : fmtBytes(st.size);
532
- return `${size.padStart(8)} ${i.isDirectory() ? chalk.hex("#569CD6")(i.name+"/") : i.name}`;
533
- });
534
- return { result: `${p}\n${"─".repeat(50)}\n${rows.join("\n")}\n\n${rows.length} items` };
535
- }
536
-
537
- case "find_files": {
538
- const dir = path.resolve(args.dir || ".");
539
- const { stdout } = await execAsync(
540
- `find "${dir}" -name "${args.pattern}" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | head -200`
541
- );
542
- return { result: stdout.trim() || "No files found." };
543
- }
544
-
545
- case "search_in_files": {
546
- const dir = path.resolve(args.dir || ".");
547
- const flag = args.case_insensitive ? "-i" : "";
548
- const ext = args.extension ? `--include="*${args.extension}"` : "";
549
- const { stdout } = await execAsync(
550
- `grep -rn ${flag} ${ext} "${args.pattern}" "${dir}" --exclude-dir=node_modules --exclude-dir=.git 2>/dev/null | head -100`
551
- ).catch(e => ({ stdout: e.stdout || "" }));
552
- return { result: stdout.trim() || "No matches found." };
553
- }
554
-
555
- case "run_shell": {
556
- const cwd = args.cwd ? path.resolve(args.cwd) : process.cwd();
557
- const timeout = args.timeout || 30000;
558
- try {
559
- const { stdout, stderr } = await execAsync(args.command, { cwd, timeout, maxBuffer: 5*1024*1024 });
560
- const out = [stdout?.trim(), stderr?.trim() ? `[stderr]\n${stderr.trim()}` : null].filter(Boolean).join("\n");
561
- return { result: out || "(no output)", exitCode: 0 };
562
- } catch (err) {
563
- const out = [err.stdout?.trim(), err.stderr?.trim() ? `[stderr]\n${err.stderr.trim()}` : null, `[exit ${err.code}]`].filter(Boolean).join("\n");
564
- return { result: out, exitCode: err.code };
565
- }
566
- }
567
-
568
- case "create_dir": {
569
- const p = path.resolve(args.path);
570
- fs.mkdirSync(p, { recursive: true });
571
- return { result: `Created: ${p}` };
572
- }
573
-
574
453
  case "delete_file": {
575
454
  const p = path.resolve(args.path);
576
455
  if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
@@ -579,70 +458,60 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
579
458
  }
580
459
 
581
460
  case "move_file": {
582
- const from = path.resolve(args.from), to = path.resolve(args.to);
461
+ const from = path.resolve(args.from);
462
+ const to = path.resolve(args.to);
463
+ if (!fs.existsSync(from)) return { error: `Not found: ${from}` };
583
464
  fs.mkdirSync(path.dirname(to), { recursive: true });
584
465
  fs.renameSync(from, to);
585
466
  return { result: `Moved: ${from} → ${to}` };
586
467
  }
587
468
 
588
- case "get_env": {
589
- let branch = "";
590
- try { branch = (await execAsync("git branch --show-current 2>/dev/null")).stdout.trim(); } catch {}
469
+ case "file_info": {
470
+ const p = path.resolve(args.path);
471
+ if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
472
+ const st = fs.statSync(p);
591
473
  return {
592
474
  result: JSON.stringify({
593
- cwd: process.cwd(),
594
- platform: process.platform,
595
- node: process.version,
596
- git_branch: branch || null,
597
- home: process.env.HOME,
598
- shell: process.env.SHELL
475
+ path: p,
476
+ type: st.isDirectory() ? "directory" : "file",
477
+ size: fmtBytes(st.size),
478
+ bytes: st.size,
479
+ mode: "0" + (st.mode & 0o777).toString(8),
480
+ modified: st.mtime.toISOString(),
481
+ created: st.birthtime.toISOString(),
599
482
  }, null, 2)
600
483
  };
601
484
  }
602
485
 
603
- case "read_url": {
604
- let hdrs = "";
605
- if (args.headers) {
606
- try {
607
- const obj = JSON.parse(args.headers);
608
- hdrs = Object.entries(obj).map(([k,v]) => `-H "${k}: ${v}"`).join(" ");
609
- } catch {}
610
- }
611
- const { stdout } = await execAsync(`curl -sL --max-time 15 ${hdrs} "${args.url}" | head -c 51200`);
612
- return { result: stdout.trim() || "(empty response)" };
613
- }
614
-
615
- // ── NEW TOOLS ──────────────────────────────────────────────
616
- case "copy_file": {
617
- const from = path.resolve(args.from);
618
- const to = path.resolve(args.to);
619
- if (!fs.existsSync(from)) return { error: `Not found: ${from}` };
620
- fs.mkdirSync(path.dirname(to), { recursive: true });
621
- const rec = args.recursive !== false;
622
- const flag = rec ? "-r" : "";
623
- await execAsync(`cp ${flag} "${from}" "${to}"`);
624
- return { result: `Copied: ${from} → ${to}` };
486
+ case "diff_files": {
487
+ const a = path.resolve(args.file_a);
488
+ const b = path.resolve(args.file_b);
489
+ const { stdout } = await execAsync(`diff -u "${a}" "${b}"`).catch(e => ({ stdout: e.stdout || "" }));
490
+ return { result: stdout.trim() || "Files are identical." };
625
491
  }
626
492
 
627
- case "read_file_lines": {
628
- const p = path.resolve(args.path);
629
- if (!fs.existsSync(p)) return { error: `File not found: ${p}` };
630
- const lines = fs.readFileSync(p, "utf8").split("\n");
631
- const start = Math.max(1, args.start) - 1;
632
- const end = Math.min(lines.length, args.end);
633
- const slice = lines.slice(start, end);
634
- const numbered = slice.map((l, i) =>
635
- `${String(start + i + 1).padStart(4)} ${l}`
636
- ).join("\n");
637
- return { result: numbered, total_lines: lines.length };
493
+ // ── Directory ────────────────────────────────────────
494
+ case "list_dir": {
495
+ const p = path.resolve(args.path || ".");
496
+ if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
497
+ const SKIP = new Set(["node_modules", ".git"]);
498
+ const items = fs.readdirSync(p, { withFileTypes: true });
499
+ const filtered = (args.show_hidden ? items : items.filter(i => !i.name.startsWith(".")))
500
+ .filter(i => !SKIP.has(i.name));
501
+ const rows = filtered.map(i => {
502
+ const st = fs.statSync(path.join(p, i.name));
503
+ const size = i.isDirectory() ? "<DIR>" : fmtBytes(st.size);
504
+ return `${size.padStart(8)} ${i.isDirectory() ? i.name + "/" : i.name}`;
505
+ });
506
+ return { result: `${p}\n${"─".repeat(50)}\n${rows.join("\n")}\n\n${rows.length} items` };
638
507
  }
639
508
 
640
509
  case "tree": {
641
510
  const p = path.resolve(args.path || ".");
642
511
  const depth = args.depth ?? 3;
643
512
  const showH = args.show_hidden ?? false;
644
- const SKIP = new Set(["node_modules", ".git", "dist", "build"]);
645
- const lines = [];
513
+ const SKIP = new Set(["node_modules", ".git", "dist", "build", ".next", "__pycache__"]);
514
+ const lines = [p];
646
515
 
647
516
  const walk = (dir, prefix, d) => {
648
517
  if (d > depth) return;
@@ -657,48 +526,97 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
657
526
  if (e.isDirectory()) walk(path.join(dir, e.name), prefix + child, d + 1);
658
527
  });
659
528
  };
660
-
661
- lines.push(p);
662
529
  walk(p, "", 0);
663
530
  return { result: lines.join("\n") };
664
531
  }
665
532
 
666
- case "file_info": {
533
+ case "find_files": {
534
+ const dir = path.resolve(args.dir || ".");
535
+ const { stdout } = await execAsync(
536
+ `find "${dir}" -name "${args.pattern}" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | head -100`
537
+ );
538
+ return { result: stdout.trim() || "No files found." };
539
+ }
540
+
541
+ case "search_in_files": {
542
+ const dir = path.resolve(args.dir || ".");
543
+ const flag = args.case_insensitive ? "-i" : "";
544
+ const ext = args.extension ? `--include="*${args.extension}"` : "";
545
+ const { stdout } = await execAsync(
546
+ `grep -rn ${flag} ${ext} "${args.pattern}" "${dir}" --exclude-dir=node_modules --exclude-dir=.git 2>/dev/null | head -100`
547
+ ).catch(e => ({ stdout: e.stdout || "" }));
548
+ return { result: stdout.trim() || "No matches found." };
549
+ }
550
+
551
+ case "create_dir": {
667
552
  const p = path.resolve(args.path);
668
- if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
669
- const st = fs.statSync(p);
670
- return {
671
- result: JSON.stringify({
672
- path: p,
673
- type: st.isDirectory() ? "directory" : "file",
674
- size: fmtBytes(st.size),
675
- bytes: st.size,
676
- mode: "0" + (st.mode & 0o777).toString(8),
677
- modified: st.mtime.toISOString(),
678
- created: st.birthtime.toISOString(),
679
- }, null, 2)
680
- };
553
+ fs.mkdirSync(p, { recursive: true });
554
+ return { result: `Created: ${p}` };
681
555
  }
682
556
 
683
- case "diff_files": {
684
- const a = path.resolve(args.file_a);
685
- const b = path.resolve(args.file_b);
686
- const { stdout } = await execAsync(`diff -u "${a}" "${b}"`).catch(e => ({ stdout: e.stdout || "" }));
687
- return { result: stdout || "Files are identical." };
557
+ // ── Execute ──────────────────────────────────────────
558
+ case "run_shell": {
559
+ const cwd = args.cwd ? path.resolve(args.cwd) : process.cwd();
560
+ const timeout = args.timeout || 30000;
561
+ try {
562
+ const { stdout, stderr } = await execAsync(args.command, {
563
+ cwd, timeout, maxBuffer: 5 * 1024 * 1024
564
+ });
565
+ const out = [stdout?.trim(), stderr?.trim() ? `[stderr]\n${stderr.trim()}` : null]
566
+ .filter(Boolean).join("\n");
567
+ return { result: out || "(no output)", exitCode: 0 };
568
+ } catch (err) {
569
+ const out = [
570
+ err.stdout?.trim(),
571
+ err.stderr?.trim() ? `[stderr]\n${err.stderr.trim()}` : null,
572
+ `[exit ${err.code}]`
573
+ ].filter(Boolean).join("\n");
574
+ return { result: out, exitCode: err.code };
575
+ }
576
+ }
577
+
578
+ // ── Git ──────────────────────────────────────────────
579
+ case "git": {
580
+ const cwd = args.cwd ? path.resolve(args.cwd) : process.cwd();
581
+ const extra = args.args ?? "";
582
+ const cmds = {
583
+ status: `git status`,
584
+ log: `git log --oneline -20 ${extra}`,
585
+ diff: `git diff ${extra}`,
586
+ add: `git add ${extra || "."}`,
587
+ commit: `git commit -m "${extra}"`,
588
+ push: `git push ${extra}`,
589
+ pull: `git pull ${extra}`,
590
+ clone: `git clone ${extra}`,
591
+ checkout: `git checkout ${extra}`,
592
+ branch: `git branch ${extra}`,
593
+ init: `git init ${extra}`,
594
+ stash: `git stash ${extra}`,
595
+ merge: `git merge ${extra}`,
596
+ };
597
+ const cmd = cmds[args.action?.toLowerCase()];
598
+ if (!cmd) return { error: `Unknown git action: ${args.action}. Valid: ${Object.keys(cmds).join(", ")}` };
599
+ const { stdout, stderr } = await execAsync(cmd, { cwd })
600
+ .catch(e => ({ stdout: e.stdout || "", stderr: e.stderr || "" }));
601
+ return { result: (stdout + (stderr ? "\n" + stderr : "")).trim() || "(no output)" };
688
602
  }
689
603
 
604
+ // ── Network ──────────────────────────────────────────
690
605
  case "http_request": {
691
- const method = (args.method ?? "GET").toUpperCase();
606
+ const method = (args.method ?? "GET").toUpperCase();
692
607
  let hdrs = "";
693
608
  if (args.headers) {
694
609
  try {
695
610
  const obj = JSON.parse(args.headers);
696
- hdrs = Object.entries(obj).map(([k,v]) => `-H "${k}: ${v}"`).join(" ");
611
+ hdrs = Object.entries(obj).map(([k, v]) => `-H "${k}: ${v}"`).join(" ");
697
612
  } catch {}
698
613
  }
699
- const bodyFlag = args.body ? `-d '${args.body.replace(/'/g, "'\\''")}'` : "";
700
- const cmd = `curl -s -X ${method} ${hdrs} ${bodyFlag} --max-time 20 "${args.url}" | head -c 102400`;
701
- const { stdout } = await execAsync(cmd);
614
+ const bodyFlag = args.body
615
+ ? `-d '${args.body.replace(/'/g, "'\\''")}'`
616
+ : "";
617
+ const { stdout } = await execAsync(
618
+ `curl -s -X ${method} ${hdrs} ${bodyFlag} --max-time 20 "${args.url}" | head -c 102400`
619
+ );
702
620
  return { result: stdout.trim() || "(empty response)" };
703
621
  }
704
622
 
@@ -707,199 +625,93 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
707
625
  fs.mkdirSync(path.dirname(dest), { recursive: true });
708
626
  await execAsync(`curl -L --max-time 60 -o "${dest}" "${args.url}"`);
709
627
  const size = fmtBytes(fs.statSync(dest).size);
710
- return { result: `Downloaded to ${dest} (${size})` };
711
- }
712
-
713
- case "hash_file": {
714
- const p = path.resolve(args.path);
715
- if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
716
- const alg = args.algorithm ?? "sha256";
717
- const cmd = alg === "md5"
718
- ? `md5sum "${p}" 2>/dev/null || md5 -q "${p}"`
719
- : `sha${alg === "sha256" ? "256" : alg === "sha1" ? "1" : "256"}sum "${p}" 2>/dev/null || shasum -a ${alg === "sha1" ? "1" : "256"} "${p}"`;
720
- const { stdout } = await execAsync(cmd);
721
- return { result: stdout.trim().split(" ")[0], algorithm: alg, path: p };
722
- }
723
-
724
- case "base64": {
725
- const action = args.action.toLowerCase();
726
- let input = args.input;
727
- if (args.is_file) {
728
- const p = path.resolve(input);
729
- if (!fs.existsSync(p)) return { error: `File not found: ${p}` };
730
- input = fs.readFileSync(p, "utf8");
731
- }
732
- if (action === "encode") {
733
- return { result: Buffer.from(input).toString("base64") };
734
- } else {
735
- return { result: Buffer.from(input, "base64").toString("utf8") };
736
- }
628
+ return { result: `Downloaded ${dest} (${size})` };
737
629
  }
738
630
 
631
+ // ── Data ─────────────────────────────────────────────
739
632
  case "json_query": {
740
633
  let data;
741
- if (args.is_file) {
742
- const p = path.resolve(args.input);
743
- if (!fs.existsSync(p)) return { error: `File not found: ${p}` };
744
- data = JSON.parse(fs.readFileSync(p, "utf8"));
745
- } else {
746
- data = JSON.parse(args.input);
634
+ try {
635
+ data = args.is_file
636
+ ? JSON.parse(fs.readFileSync(path.resolve(args.input), "utf8"))
637
+ : JSON.parse(args.input);
638
+ } catch (e) {
639
+ return { error: `JSON parse error: ${e.message}` };
747
640
  }
748
641
  if (!args.query) return { result: JSON.stringify(data, null, 2) };
749
- // Simple dot+bracket notation traversal
750
642
  const keys = args.query.replace(/\[(\d+)\]/g, ".$1").split(".");
751
643
  let val = data;
752
644
  for (const k of keys) {
753
- if (val == null) return { error: `Key "${k}" not found` };
645
+ if (val == null) return { error: `Key "${k}" not found at path "${args.query}"` };
754
646
  val = val[k];
755
647
  }
756
648
  return { result: typeof val === "object" ? JSON.stringify(val, null, 2) : String(val) };
757
649
  }
758
650
 
759
- case "extract": {
760
- const archive = path.resolve(args.archive);
761
- if (!fs.existsSync(archive)) return { error: `Archive not found: ${archive}` };
762
- const dest = path.resolve(args.dest ?? path.dirname(archive));
763
- fs.mkdirSync(dest, { recursive: true });
764
- const ext = archive.toLowerCase();
765
- let cmd;
766
- if (ext.endsWith(".zip")) cmd = `unzip -o "${archive}" -d "${dest}"`;
767
- else if (ext.endsWith(".tar.gz") || ext.endsWith(".tgz")) cmd = `tar -xzf "${archive}" -C "${dest}"`;
768
- else if (ext.endsWith(".tar.bz2")) cmd = `tar -xjf "${archive}" -C "${dest}"`;
769
- else if (ext.endsWith(".tar")) cmd = `tar -xf "${archive}" -C "${dest}"`;
770
- else if (ext.endsWith(".gz")) cmd = `gunzip -k "${archive}"`;
771
- else return { error: `Unsupported archive format: ${archive}` };
772
- const { stdout, stderr } = await execAsync(cmd);
773
- return { result: `Extracted to ${dest}` };
774
- }
775
-
776
- case "compress": {
777
- const source = path.resolve(args.source);
778
- const dest = path.resolve(args.dest);
779
- if (!fs.existsSync(source)) return { error: `Not found: ${source}` };
780
- fs.mkdirSync(path.dirname(dest), { recursive: true });
781
- let cmd;
782
- if (dest.endsWith(".zip")) cmd = `zip -r "${dest}" "${source}"`;
783
- else if (dest.endsWith(".tar.gz") || dest.endsWith(".tgz")) cmd = `tar -czf "${dest}" "${source}"`;
784
- else if (dest.endsWith(".tar")) cmd = `tar -cf "${dest}" "${source}"`;
785
- else return { error: "Unsupported format. Use .zip or .tar.gz" };
786
- await execAsync(cmd);
787
- const size = fmtBytes(fs.statSync(dest).size);
788
- return { result: `Compressed to ${dest} (${size})` };
789
- }
790
-
791
- case "git": {
792
- const cwd = args.cwd ? path.resolve(args.cwd) : process.cwd();
793
- const a = args.action.toLowerCase();
794
- const extra = args.args ?? "";
795
- const gitCmds = {
796
- status: `git status`,
797
- log: `git log --oneline -20 ${extra}`,
798
- diff: `git diff ${extra}`,
799
- add: `git add ${extra || "."}`,
800
- commit: `git commit -m "${extra}"`,
801
- push: `git push ${extra}`,
802
- pull: `git pull ${extra}`,
803
- clone: `git clone ${extra}`,
804
- checkout: `git checkout ${extra}`,
805
- branch: `git branch ${extra}`,
806
- init: `git init ${extra}`,
807
- stash: `git stash ${extra}`,
651
+ // ── System ───────────────────────────────────────────
652
+ case "get_env": {
653
+ let branch = "";
654
+ try { branch = (await execAsync("git branch --show-current 2>/dev/null")).stdout.trim(); } catch {}
655
+ return {
656
+ result: JSON.stringify({
657
+ cwd: process.cwd(),
658
+ platform: process.platform,
659
+ node: process.version,
660
+ git_branch: branch || null,
661
+ home: process.env.HOME,
662
+ shell: process.env.SHELL,
663
+ user: process.env.USER,
664
+ }, null, 2)
808
665
  };
809
- const cmd = gitCmds[a];
810
- if (!cmd) return { error: `Unknown git action: ${a}` };
811
- const { stdout, stderr } = await execAsync(cmd, { cwd }).catch(e => ({
812
- stdout: e.stdout || "", stderr: e.stderr || ""
813
- }));
814
- return { result: (stdout + (stderr ? "\n" + stderr : "")).trim() || "(no output)" };
815
- }
816
-
817
- case "process_list": {
818
- const filter = args.filter ?? "";
819
- const cmd = filter
820
- ? `ps aux | grep -i "${filter}" | grep -v grep`
821
- : `ps aux | head -30`;
822
- const { stdout } = await execAsync(cmd).catch(e => ({ stdout: e.stdout || "" }));
823
- return { result: stdout.trim() || "No processes found." };
824
- }
825
-
826
- case "disk_usage": {
827
- const p = args.path ? path.resolve(args.path) : process.cwd();
828
- const { stdout: du } = await execAsync(`du -sh "${p}" 2>/dev/null`).catch(() => ({ stdout: "" }));
829
- const { stdout: df } = await execAsync(`df -h "${p}" 2>/dev/null`).catch(() => ({ stdout: "" }));
830
- return { result: [du.trim(), df.trim()].filter(Boolean).join("\n\n") || "(unavailable)" };
831
- }
832
-
833
- case "chmod": {
834
- const p = path.resolve(args.path);
835
- if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
836
- const rec = args.recursive ? "-R" : "";
837
- await execAsync(`chmod ${rec} ${args.permissions} "${p}"`);
838
- return { result: `chmod ${args.permissions} applied to ${p}` };
839
666
  }
840
667
 
841
- // ── REAL-TIME TOOLS ──────────────────────────────────────
668
+ // ── Real-time ─────────────────────────────────────────
842
669
  case "web_search": {
843
- const q = encodeURIComponent(args.query);
844
- const reg = args.region ?? "id-id";
845
- // DuckDuckGo Instant Answer API
670
+ const q = encodeURIComponent(args.query);
846
671
  const { stdout: ddg } = await execAsync(
847
672
  `curl -sL --max-time 10 "https://api.duckduckgo.com/?q=${q}&format=json&no_redirect=1&no_html=1&skip_disambig=1"`
848
673
  ).catch(() => ({ stdout: "" }));
849
-
850
- // Also get HTML search results via scraping (lite version)
851
674
  const { stdout: html } = await execAsync(
852
675
  `curl -sL --max-time 10 -H "User-Agent: Mozilla/5.0" "https://html.duckduckgo.com/html/?q=${q}" | grep -oP '(?<=class="result__snippet">)[^<]+' | head -8`
853
676
  ).catch(() => ({ stdout: "" }));
854
677
 
855
678
  let out = "";
856
679
  try {
857
- const data = JSON.parse(ddg);
858
- if (data.AbstractText) out += `📖 ${data.AbstractText}\n Source: ${data.AbstractURL}\n\n`;
859
- if (data.Answer) out += `💡 ${data.Answer}\n\n`;
860
- if (data.RelatedTopics?.length) {
680
+ const d = JSON.parse(ddg);
681
+ if (d.AbstractText) out += `${d.AbstractText}\nSource: ${d.AbstractURL}\n\n`;
682
+ if (d.Answer) out += `Answer: ${d.Answer}\n\n`;
683
+ if (d.RelatedTopics?.length) {
861
684
  out += "Related:\n";
862
- data.RelatedTopics.slice(0, 5).forEach(t => {
863
- if (t.Text) out += ` · ${t.Text}\n`;
864
- });
685
+ d.RelatedTopics.slice(0, 5).forEach(t => { if (t.Text) out += ` · ${t.Text}\n`; });
865
686
  }
866
687
  } catch {}
867
-
868
- if (html.trim()) out += "\nSearch snippets:\n" + html.trim().split("\n").map(l => ` · ${l.trim()}`).join("\n");
869
-
870
- return { result: out.trim() || "No results found. Try a different query." };
688
+ if (html.trim()) out += "\nSnippets:\n" + html.trim().split("\n").map(l => ` · ${l.trim()}`).join("\n");
689
+ return { result: out.trim() || "No results found." };
871
690
  }
872
691
 
873
692
  case "get_weather": {
874
693
  const loc = encodeURIComponent(args.location);
875
694
  if (args.format === "full") {
876
- const { stdout } = await execAsync(
877
- `curl -sL --max-time 10 "https://wttr.in/${loc}?format=j1"`
878
- );
695
+ const { stdout } = await execAsync(`curl -sL --max-time 10 "https://wttr.in/${loc}?format=j1"`);
879
696
  try {
880
- const data = JSON.parse(stdout);
881
- const current = data.current_condition[0];
882
- const area = data.nearest_area[0];
883
- const city = area.areaName[0].value;
884
- const country = area.country[0].value;
697
+ const d = JSON.parse(stdout);
698
+ const c = d.current_condition[0];
699
+ const a = d.nearest_area[0];
885
700
  return {
886
701
  result: JSON.stringify({
887
- location: `${city}, ${country}`,
888
- temp_c: current.temp_C + "°C",
889
- feels_like: current.FeelsLikeC + "°C",
890
- humidity: current.humidity + "%",
891
- wind_kmph: current.windspeedKmph + " km/h",
892
- description: current.weatherDesc[0].value,
893
- visibility: current.visibility + " km",
894
- uv_index: current.uvIndex,
702
+ location: `${a.areaName[0].value}, ${a.country[0].value}`,
703
+ temp_c: c.temp_C + "°C",
704
+ feels_like: c.FeelsLikeC + "°C",
705
+ humidity: c.humidity + "%",
706
+ wind: c.windspeedKmph + " km/h",
707
+ description: c.weatherDesc[0].value,
708
+ visibility: c.visibility + " km",
709
+ uv_index: c.uvIndex,
895
710
  }, null, 2)
896
711
  };
897
712
  } catch {}
898
713
  }
899
- // Simple one-line format
900
- const { stdout } = await execAsync(
901
- `curl -sL --max-time 10 "https://wttr.in/${loc}?format=3"`
902
- );
714
+ const { stdout } = await execAsync(`curl -sL --max-time 10 "https://wttr.in/${loc}?format=3"`);
903
715
  return { result: stdout.trim() || "Could not fetch weather." };
904
716
  }
905
717
 
@@ -907,57 +719,37 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
907
719
  const q = encodeURIComponent(args.query);
908
720
  const lang = args.language ?? "id";
909
721
  const lim = Math.min(args.limit ?? 10, 20);
910
-
911
- // Use HackerNews Algolia API for tech news, BBC/Reuters RSS for general
912
722
  const results = [];
913
723
 
914
- // Try GNews RSS (free, no key)
915
724
  const { stdout: hn } = await execAsync(
916
- `curl -sL --max-time 12 "https://hn.algolia.com/api/v1/search?query=${q}&tags=story&hitsPerPage=${lim}" 2>/dev/null`
725
+ `curl -sL --max-time 12 "https://hn.algolia.com/api/v1/search?query=${q}&tags=story&hitsPerPage=${lim}"`
917
726
  ).catch(() => ({ stdout: "" }));
918
-
919
727
  try {
920
- const data = JSON.parse(hn);
921
- if (data.hits?.length) {
922
- data.hits.forEach((h, i) => {
923
- results.push(`${i+1}. ${h.title}\n 🔗 ${h.url || "https://news.ycombinator.com/item?id="+h.objectID}\n ⏱ ${h.created_at}`);
924
- });
925
- }
728
+ JSON.parse(hn).hits?.forEach((h, i) => {
729
+ results.push(`${i+1}. ${h.title}\n ${h.url || "https://news.ycombinator.com/item?id="+h.objectID}`);
730
+ });
926
731
  } catch {}
927
732
 
928
- // Also try Google News RSS if lang is id
929
733
  if (results.length < 5) {
930
734
  const { stdout: rss } = await execAsync(
931
735
  `curl -sL --max-time 12 "https://news.google.com/rss/search?q=${q}&hl=${lang}&gl=ID&ceid=ID:${lang}" | grep -oP '(?<=<title>)[^<]+' | grep -v "Google News" | head -${lim}`
932
736
  ).catch(() => ({ stdout: "" }));
933
- if (rss.trim()) {
934
- const headlines = rss.trim().split("\n").filter(Boolean);
935
- headlines.forEach((h, i) => {
936
- if (!results.find(r => r.includes(h)))
937
- results.push(`${results.length+1}. ${h}`);
938
- });
939
- }
737
+ rss.trim().split("\n").filter(Boolean).forEach((h, i) => {
738
+ results.push(`${results.length+1}. ${h}`);
739
+ });
940
740
  }
941
-
942
741
  return { result: results.slice(0, lim).join("\n\n") || "No news found." };
943
742
  }
944
743
 
945
744
  case "get_exchange_rate": {
946
745
  const from = (args.from ?? "USD").toUpperCase();
947
- const to = args.to ? args.to.toUpperCase() : "";
948
- const url = to
949
- ? `https://api.frankfurter.app/latest?from=${from}&to=${to}`
950
- : `https://api.frankfurter.app/latest?from=${from}`;
951
- const { stdout } = await execAsync(`curl -sL --max-time 10 "${url}"`);
746
+ const to = args.to ? `&to=${args.to.toUpperCase()}` : "";
747
+ const { stdout } = await execAsync(`curl -sL --max-time 10 "https://api.frankfurter.app/latest?from=${from}${to}"`);
952
748
  try {
953
- const data = JSON.parse(stdout);
954
- const rates = Object.entries(data.rates)
955
- .map(([k, v]) => ` ${k}: ${v}`)
956
- .join("\n");
957
- return { result: `1 ${from} =\n${rates}\n\nDate: ${data.date}` };
958
- } catch {
959
- return { error: "Could not fetch exchange rates." };
960
- }
749
+ const d = JSON.parse(stdout);
750
+ const rates = Object.entries(d.rates).map(([k, v]) => ` ${k}: ${v}`).join("\n");
751
+ return { result: `1 ${from} =\n${rates}\n\nDate: ${d.date}` };
752
+ } catch { return { error: "Could not fetch exchange rates." }; }
961
753
  }
962
754
 
963
755
  case "get_stock": {
@@ -966,49 +758,45 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
966
758
  `curl -sL --max-time 12 "https://query1.finance.yahoo.com/v8/finance/chart/${sym}?range=1d&interval=1d"`
967
759
  );
968
760
  try {
969
- const data = JSON.parse(stdout);
970
- const meta = data.chart.result[0].meta;
761
+ const meta = JSON.parse(stdout).chart.result[0].meta;
762
+ const chg = meta.regularMarketPrice - meta.previousClose;
971
763
  return {
972
764
  result: JSON.stringify({
973
- symbol: meta.symbol,
974
- name: meta.longName ?? meta.shortName ?? meta.symbol,
975
- price: meta.regularMarketPrice,
976
- prev_close: meta.previousClose,
977
- change: (meta.regularMarketPrice - meta.previousClose).toFixed(2),
978
- change_pct: (((meta.regularMarketPrice - meta.previousClose) / meta.previousClose) * 100).toFixed(2) + "%",
979
- currency: meta.currency,
980
- exchange: meta.exchangeName,
981
- market_state: meta.marketState,
982
- volume: meta.regularMarketVolume,
765
+ symbol: meta.symbol,
766
+ name: meta.longName ?? meta.shortName ?? meta.symbol,
767
+ price: meta.regularMarketPrice,
768
+ prev_close: meta.previousClose,
769
+ change: chg.toFixed(2),
770
+ change_pct: ((chg / meta.previousClose) * 100).toFixed(2) + "%",
771
+ currency: meta.currency,
772
+ exchange: meta.exchangeName,
773
+ market_state: meta.marketState,
774
+ volume: meta.regularMarketVolume,
983
775
  }, null, 2)
984
776
  };
985
- } catch {
986
- return { error: `Could not fetch stock data for ${args.symbol}.` };
987
- }
777
+ } catch { return { error: `Could not fetch stock: ${args.symbol}` }; }
988
778
  }
989
779
 
990
780
  case "get_crypto": {
991
781
  const ids = encodeURIComponent(args.coins.toLowerCase().replace(/\s/g, ""));
992
782
  const cur = (args.currency ?? "usd").toLowerCase();
993
- const url = `https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=${cur}&include_24hr_change=true&include_market_cap=true&include_24hr_vol=true`;
994
- const { stdout } = await execAsync(`curl -sL --max-time 12 "${url}"`);
783
+ const { stdout } = await execAsync(
784
+ `curl -sL --max-time 12 "https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=${cur}&include_24hr_change=true&include_market_cap=true"`
785
+ );
995
786
  try {
996
- const data = JSON.parse(stdout);
997
- const lines = Object.entries(data).map(([coin, info]) => {
998
- const price = info[cur];
999
- const change = info[`${cur}_24h_change`]?.toFixed(2);
1000
- const mcap = info[`${cur}_market_cap`];
1001
- return `${coin.toUpperCase()}: ${cur.toUpperCase()} ${price?.toLocaleString()} (24h: ${change}%) mcap: ${mcap?.toLocaleString()}`;
1002
- });
787
+ const data = JSON.parse(stdout);
788
+ const lines = Object.entries(data).map(([coin, info]) =>
789
+ `${coin.toUpperCase()}: ${cur.toUpperCase()} ${info[cur]?.toLocaleString()}` +
790
+ ` (24h: ${info[`${cur}_24h_change`]?.toFixed(2)}%)` +
791
+ ` mcap: ${info[`${cur}_market_cap`]?.toLocaleString()}`
792
+ );
1003
793
  return { result: lines.join("\n") };
1004
- } catch {
1005
- return { error: "Could not fetch crypto prices." };
1006
- }
794
+ } catch { return { error: "Could not fetch crypto prices." }; }
1007
795
  }
1008
796
 
1009
797
  case "get_ip_info": {
1010
- const target = args.ip ? `https://ipapi.co/${args.ip}/json/` : "https://ipapi.co/json/";
1011
- const { stdout } = await execAsync(`curl -sL --max-time 10 "${target}"`);
798
+ const url = args.ip ? `https://ipapi.co/${args.ip}/json/` : "https://ipapi.co/json/";
799
+ const { stdout } = await execAsync(`curl -sL --max-time 10 "${url}"`);
1012
800
  try {
1013
801
  const d = JSON.parse(stdout);
1014
802
  return {
@@ -1023,9 +811,7 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
1023
811
  lon: d.longitude,
1024
812
  }, null, 2)
1025
813
  };
1026
- } catch {
1027
- return { error: "Could not fetch IP info." };
1028
- }
814
+ } catch { return { error: "Could not fetch IP info." }; }
1029
815
  }
1030
816
 
1031
817
  default:
@@ -1036,21 +822,25 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
1036
822
  }
1037
823
  }
1038
824
 
1039
- function buildConfirmLabel(name, args) {
1040
- switch (name) {
1041
- case "write_file": return `Write file: ${chalk.yellow(args.path)}`;
1042
- case "patch_file": return `Patch file: ${chalk.yellow(args.path)}`;
1043
- case "append_file": return `Append to: ${chalk.yellow(args.path)}`;
1044
- case "run_shell": return `Run shell: ${chalk.cyan(args.command)}`;
1045
- case "create_dir": return `Create dir: ${chalk.yellow(args.path)}`;
1046
- case "delete_file": return `${chalk.red("DELETE")}: ${chalk.yellow(args.path)}`;
1047
- case "move_file": return `Move: ${chalk.yellow(args.from)} ${chalk.yellow(args.to)}`;
1048
- default: return `Execute: ${name}`;
1049
- }
825
+ // ─────────────────────────────────────────────────────────────────
826
+ // Helpers
827
+ // ─────────────────────────────────────────────────────────────────
828
+ function confirmLabel(name, args) {
829
+ const labels = {
830
+ write_file: () => `Write file: ${chalk.yellow(args.path)}`,
831
+ patch_file: () => `Patch file: ${chalk.yellow(args.path)}`,
832
+ append_file: () => `Append to: ${chalk.yellow(args.path)}`,
833
+ run_shell: () => `Run shell: ${chalk.cyan(args.command)}`,
834
+ create_dir: () => `Create dir: ${chalk.yellow(args.path)}`,
835
+ delete_file: () => `${chalk.red("DELETE")}: ${chalk.yellow(args.path)}`,
836
+ move_file: () => `Move: ${chalk.yellow(args.from)} → ${chalk.yellow(args.to)}`,
837
+ download_file: () => `Download: ${chalk.cyan(args.url)} → ${chalk.yellow(args.dest)}`,
838
+ };
839
+ return labels[name]?.() ?? `Execute: ${name}`;
1050
840
  }
1051
841
 
1052
842
  function fmtBytes(n) {
1053
- if (n < 1024) return n + "B";
1054
- if (n < 1048576) return (n/1024).toFixed(1) + "K";
1055
- return (n/1048576).toFixed(1) + "M";
843
+ if (n < 1024) return n + "B";
844
+ if (n < 1048576) return (n / 1024).toFixed(1) + "KB";
845
+ return (n / 1048576).toFixed(1) + "MB";
1056
846
  }