@ikyyofc/gemini-cli 3.0.9 → 4.0.1

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/agent.js +13 -0
  3. package/src/tools.js +486 -423
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,41 +9,65 @@ 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
  },
38
+ {
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.",
41
+ parameters: {
42
+ type: "OBJECT",
43
+ properties: {
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)" }
47
+ },
48
+ required: ["path", "start", "end"]
49
+ }
50
+ },
27
51
  {
28
52
  name: "write_file",
29
- description: "Create or overwrite a file with the provided content. Creates parent directories automatically.",
53
+ description: "Create or completely overwrite a file. Creates parent directories automatically. Use patch_file for small edits.",
30
54
  parameters: {
31
55
  type: "OBJECT",
32
56
  properties: {
33
- path: { type: "STRING", description: "File path to write" },
34
- content: { type: "STRING", description: "Complete file content to write" }
57
+ path: { type: "STRING", description: "File path" },
58
+ content: { type: "STRING", description: "Complete file content" }
35
59
  },
36
60
  required: ["path", "content"]
37
61
  }
38
62
  },
39
63
  {
40
64
  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.",
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.",
42
66
  parameters: {
43
67
  type: "OBJECT",
44
68
  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)" },
69
+ path: { type: "STRING", description: "File path" },
70
+ old_str: { type: "STRING", description: "Exact unique string to find (with surrounding context)" },
47
71
  new_str: { type: "STRING", description: "Replacement string (can be empty to delete)" }
48
72
  },
49
73
  required: ["path", "old_str", "new_str"]
@@ -51,7 +75,7 @@ export const FUNCTION_DECLARATIONS = [
51
75
  },
52
76
  {
53
77
  name: "append_file",
54
- description: "Append text to the end of an existing file (or create it).",
78
+ description: "Append content to the end of a file (or create it if it doesn't exist).",
55
79
  parameters: {
56
80
  type: "OBJECT",
57
81
  properties: {
@@ -62,349 +86,340 @@ export const FUNCTION_DECLARATIONS = [
62
86
  }
63
87
  },
64
88
  {
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.",
89
+ name: "delete_file",
90
+ description: "Delete a file or empty directory permanently.",
90
91
  parameters: {
91
92
  type: "OBJECT",
92
93
  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)" }
94
+ path: { type: "STRING", description: "File or directory path to delete" }
97
95
  },
98
- required: ["pattern"]
96
+ required: ["path"]
99
97
  }
100
98
  },
101
99
  {
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.",
100
+ name: "move_file",
101
+ description: "Move or rename a file or directory.",
104
102
  parameters: {
105
103
  type: "OBJECT",
106
104
  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)" }
105
+ from: { type: "STRING", description: "Source path" },
106
+ to: { type: "STRING", description: "Destination path" }
110
107
  },
111
- required: ["command"]
108
+ required: ["from", "to"]
112
109
  }
113
110
  },
114
111
  {
115
- name: "create_dir",
116
- description: "Create a directory (and all parent directories). Equivalent to mkdir -p.",
112
+ name: "file_info",
113
+ description: "Get metadata of a file or directory: size, permissions, modified date, type.",
117
114
  parameters: {
118
115
  type: "OBJECT",
119
116
  properties: {
120
- path: { type: "STRING", description: "Directory path to create" }
117
+ path: { type: "STRING", description: "File or directory path" }
121
118
  },
122
119
  required: ["path"]
123
120
  }
124
121
  },
125
122
  {
126
- name: "delete_file",
127
- description: "Delete a file or empty directory permanently.",
123
+ name: "diff_files",
124
+ description: "Show line-by-line differences between two files (like git diff).",
128
125
  parameters: {
129
126
  type: "OBJECT",
130
127
  properties: {
131
- path: { type: "STRING", description: "Path to delete" }
128
+ file_a: { type: "STRING", description: "First file path (original)" },
129
+ file_b: { type: "STRING", description: "Second file path (modified)" }
132
130
  },
133
- required: ["path"]
131
+ required: ["file_a", "file_b"]
134
132
  }
135
133
  },
134
+
135
+ // ── Directory ────────────────────────────────────────────────
136
136
  {
137
- name: "move_file",
138
- description: "Move or rename a file or directory.",
137
+ name: "list_dir",
138
+ description: "List files and subdirectories in a path. Use before navigating or editing a project.",
139
139
  parameters: {
140
140
  type: "OBJECT",
141
141
  properties: {
142
- from: { type: "STRING", description: "Source path" },
143
- to: { type: "STRING", description: "Destination path" }
144
- },
145
- required: ["from", "to"]
142
+ path: { type: "STRING", description: "Directory path (default: current directory)" },
143
+ show_hidden: { type: "BOOLEAN", description: "Include hidden files (default: false)" }
144
+ }
146
145
  }
147
146
  },
148
147
  {
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.",
148
+ name: "tree",
149
+ description: "Show a project's directory structure as a visual tree. Great for understanding a codebase layout.",
156
150
  parameters: {
157
151
  type: "OBJECT",
158
152
  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"]
153
+ path: { type: "STRING", description: "Root directory (default: .)" },
154
+ depth: { type: "NUMBER", description: "Max depth to show (default: 3)" },
155
+ show_hidden: { type: "BOOLEAN", description: "Include hidden files" }
156
+ }
163
157
  }
164
158
  },
165
- // ── NEW TOOLS ────────────────────────────────────────────────
166
159
  {
167
- name: "copy_file",
168
- description: "Copy a file or directory to a new location.",
160
+ name: "find_files",
161
+ description: "Find files matching a name pattern (glob) recursively. Excludes node_modules and .git.",
169
162
  parameters: {
170
163
  type: "OBJECT",
171
164
  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)" }
165
+ pattern: { type: "STRING", description: "Name glob e.g. '*.js', 'index.*', '*.config.*'" },
166
+ dir: { type: "STRING", description: "Root directory to search (default: .)" }
175
167
  },
176
- required: ["from", "to"]
168
+ required: ["pattern"]
177
169
  }
178
170
  },
179
171
  {
180
- name: "read_file_lines",
181
- description: "Read a specific range of lines from a file. Use for large files.",
172
+ name: "search_in_files",
173
+ description: "Search for a text pattern (grep) across files recursively. Returns matching lines with filenames and line numbers.",
182
174
  parameters: {
183
175
  type: "OBJECT",
184
176
  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)" }
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" }
188
181
  },
189
- required: ["path", "start", "end"]
182
+ required: ["pattern"]
190
183
  }
191
184
  },
192
185
  {
193
- name: "tree",
194
- description: "Show directory structure as a tree. Good for understanding project layout.",
186
+ name: "create_dir",
187
+ description: "Create a directory and all necessary parent directories (mkdir -p).",
195
188
  parameters: {
196
189
  type: "OBJECT",
197
190
  properties: {
198
- path: { type: "STRING", description: "Root directory (default: .)" },
199
- depth: { type: "NUMBER", description: "Max depth (default: 3)" },
200
- show_hidden: { type: "BOOLEAN", description: "Include hidden files" }
201
- }
191
+ path: { type: "STRING", description: "Directory path to create" }
192
+ },
193
+ required: ["path"]
202
194
  }
203
195
  },
196
+
197
+ // ── Execute ──────────────────────────────────────────────────
204
198
  {
205
- name: "file_info",
206
- description: "Get file or directory metadata: size, permissions, modified date, type.",
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.",
207
201
  parameters: {
208
202
  type: "OBJECT",
209
203
  properties: {
210
- path: { type: "STRING", description: "File or directory 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)" }
211
207
  },
212
- required: ["path"]
208
+ required: ["command"]
213
209
  }
214
210
  },
211
+
212
+ // ── Git ──────────────────────────────────────────────────────
215
213
  {
216
- name: "diff_files",
217
- description: "Show line-by-line differences between two files.",
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.",
218
216
  parameters: {
219
217
  type: "OBJECT",
220
218
  properties: {
221
- file_a: { type: "STRING", description: "First file path" },
222
- file_b: { type: "STRING", description: "Second 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)" }
223
222
  },
224
- required: ["file_a", "file_b"]
223
+ required: ["action"]
225
224
  }
226
225
  },
226
+
227
+ // ── Network ──────────────────────────────────────────────────
227
228
  {
228
229
  name: "http_request",
229
- description: "Make an HTTP request (GET/POST/PUT/PATCH/DELETE) with headers and body.",
230
+ description: "Make an HTTP request (GET, POST, PUT, PATCH, DELETE) with custom method, headers, and body. Use for REST APIs, webhooks, GraphQL, etc.",
230
231
  parameters: {
231
232
  type: "OBJECT",
232
233
  properties: {
233
234
  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" }
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)" }
237
238
  },
238
239
  required: ["url"]
239
240
  }
240
241
  },
241
242
  {
242
243
  name: "download_file",
243
- description: "Download a file from a URL and save it to disk.",
244
+ description: "Download a file from a URL and save it to a local path.",
244
245
  parameters: {
245
246
  type: "OBJECT",
246
247
  properties: {
247
248
  url: { type: "STRING", description: "URL to download" },
248
- dest: { type: "STRING", description: "Destination file path" }
249
+ dest: { type: "STRING", description: "Local destination file path" }
249
250
  },
250
251
  required: ["url", "dest"]
251
252
  }
252
253
  },
254
+
255
+ // ── Data ─────────────────────────────────────────────────────
253
256
  {
254
- name: "hash_file",
255
- description: "Calculate hash of a file (md5, sha1, sha256).",
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.",
256
259
  parameters: {
257
260
  type: "OBJECT",
258
261
  properties: {
259
- path: { type: "STRING", description: "File path" },
260
- algorithm: { type: "STRING", description: "md5 | sha1 | sha256 (default: sha256)" }
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" }
261
265
  },
262
- required: ["path"]
266
+ required: ["input"]
263
267
  }
264
268
  },
269
+
270
+ // ── System ───────────────────────────────────────────────────
265
271
  {
266
- name: "base64",
267
- description: "Encode or decode base64. Works on strings or files.",
268
- parameters: {
269
- type: "OBJECT",
270
- 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" }
274
- },
275
- required: ["action", "input"]
276
- }
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: {} }
277
275
  },
276
+
277
+ // ── Real-time ────────────────────────────────────────────────
278
278
  {
279
- name: "json_query",
280
- description: "Parse and query JSON using a dot-notation key path. e.g. 'user.name', 'items[0].id'.",
279
+ name: "web_search",
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.",
281
281
  parameters: {
282
282
  type: "OBJECT",
283
283
  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" }
284
+ query: { type: "STRING", description: "Search query" },
285
+ region: { type: "STRING", description: "Region code e.g. id-id, us-en (default: id-id)" }
287
286
  },
288
- required: ["input"]
287
+ required: ["query"]
289
288
  }
290
289
  },
291
290
  {
292
- name: "extract",
293
- description: "Extract a zip, tar, tar.gz, or tar.bz2 archive.",
291
+ name: "get_weather",
292
+ description: "Get current weather and conditions for any city or location.",
294
293
  parameters: {
295
294
  type: "OBJECT",
296
295
  properties: {
297
- archive: { type: "STRING", description: "Path to the archive file" },
298
- dest: { type: "STRING", description: "Destination directory (default: same as archive)" }
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" }
299
298
  },
300
- required: ["archive"]
299
+ required: ["location"]
301
300
  }
302
301
  },
303
302
  {
304
- name: "compress",
305
- description: "Compress files or a directory into a zip or tar.gz archive.",
303
+ name: "get_news",
304
+ description: "Fetch latest news headlines for any topic or keyword.",
306
305
  parameters: {
307
306
  type: "OBJECT",
308
307
  properties: {
309
- source: { type: "STRING", description: "File or directory to compress" },
310
- dest: { type: "STRING", description: "Output archive path (.zip or .tar.gz)" }
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)" }
311
311
  },
312
- required: ["source", "dest"]
312
+ required: ["query"]
313
313
  }
314
314
  },
315
315
  {
316
- name: "git",
317
- description: "Run git operations: status, log, diff, add, commit, push, pull, clone, checkout, branch, init, stash.",
316
+ name: "get_exchange_rate",
317
+ description: "Get current currency exchange rates for any pair.",
318
318
  parameters: {
319
319
  type: "OBJECT",
320
320
  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)" }
324
- },
325
- required: ["action"]
321
+ from: { type: "STRING", description: "Base currency code e.g. USD, IDR, EUR (default: USD)" },
322
+ to: { type: "STRING", description: "Target currency or comma-separated e.g. 'IDR,EUR,JPY'" }
323
+ }
326
324
  }
327
325
  },
328
326
  {
329
- name: "process_list",
330
- description: "List running processes. Filter by name optionally.",
327
+ name: "get_stock",
328
+ description: "Get real-time stock price and info by ticker symbol. Supports IDX (e.g. BBCA.JK) and US markets.",
331
329
  parameters: {
332
330
  type: "OBJECT",
333
331
  properties: {
334
- filter: { type: "STRING", description: "Filter by process name (optional)" }
335
- }
332
+ symbol: { type: "STRING", description: "Ticker symbol e.g. AAPL, GOOGL, BBCA.JK, TLKM.JK" }
333
+ },
334
+ required: ["symbol"]
336
335
  }
337
336
  },
338
337
  {
339
- name: "disk_usage",
340
- description: "Check disk usage of a path (du) or total disk space (df). Use path='/' for overall.",
338
+ name: "get_crypto",
339
+ description: "Get real-time cryptocurrency prices and market data from CoinGecko.",
341
340
  parameters: {
342
341
  type: "OBJECT",
343
342
  properties: {
344
- path: { type: "STRING", description: "Path to check (default: current dir)" }
345
- }
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)" }
345
+ },
346
+ required: ["coins"]
346
347
  }
347
348
  },
348
349
  {
349
- name: "chmod",
350
- description: "Change file permissions. e.g. '755', '+x', 'a+r'.",
350
+ name: "get_ip_info",
351
+ description: "Get geolocation and network info for an IP address. Leave ip empty to look up your own public IP.",
351
352
  parameters: {
352
353
  type: "OBJECT",
353
354
  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"]
355
+ ip: { type: "STRING", description: "IP address to look up (optional — empty = your own IP)" }
356
+ }
359
357
  }
360
358
  }
361
359
  ];
362
360
 
363
- // The tools array to pass directly into the Gemini API payload
361
+ // Gemini API tools payload
364
362
  export const GEMINI_TOOLS = [{ functionDeclarations: FUNCTION_DECLARATIONS }];
365
363
 
366
364
  // ─────────────────────────────────────────────────────────────────
367
- // CONFIRMATION PROMPT
365
+ // Confirmation for destructive actions
368
366
  // ─────────────────────────────────────────────────────────────────
369
- 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"]);
370
368
 
371
369
  export async function askConfirm(label, autoApprove) {
372
370
  if (autoApprove) return true;
373
371
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
374
372
  return new Promise(res => {
375
373
  rl.question(
376
- "\n" +
377
- chalk.hex("#DCDCAA")(" ") + label + "\n" +
378
- 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")(" › "),
379
378
  ans => { rl.close(); res(ans.trim().toLowerCase() !== "n"); }
380
379
  );
381
380
  });
382
381
  }
383
382
 
384
383
  // ─────────────────────────────────────────────────────────────────
385
- // TOOL EXECUTOR
384
+ // Tool executor
386
385
  // ─────────────────────────────────────────────────────────────────
387
386
  export async function executeTool(name, args = {}, { autoApprove = false } = {}) {
388
387
  if (DESTRUCTIVE.has(name)) {
389
- const label = buildConfirmLabel(name, args);
390
- const ok = await askConfirm(label, autoApprove);
391
- 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.` };
392
390
  }
393
391
 
394
392
  try {
395
393
  switch (name) {
394
+
395
+ // ── File I/O ─────────────────────────────────────────
396
396
  case "read_file": {
397
397
  const p = path.resolve(args.path);
398
398
  if (!fs.existsSync(p)) return { error: `File not found: ${p}` };
399
399
  const content = fs.readFileSync(p, "utf8");
400
- const lines = content.split("\n");
401
- const MAX = 400;
402
- const numbered = lines
403
- .slice(0, MAX)
400
+ const lines = content.split("\n");
401
+ const MAX = 400;
402
+ const numbered = lines.slice(0, MAX)
404
403
  .map((l, i) => `${String(i+1).padStart(4)} │ ${l}`)
405
404
  .join("\n");
406
- const suffix = lines.length > MAX ? `\n\n[...${lines.length - MAX} more lines not shown]` : "";
407
- 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
+ };
408
423
  }
409
424
 
410
425
  case "write_file": {
@@ -412,7 +427,7 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
412
427
  fs.mkdirSync(path.dirname(p), { recursive: true });
413
428
  fs.writeFileSync(p, args.content, "utf8");
414
429
  const lines = (args.content.match(/\n/g) ?? []).length + 1;
415
- return { result: `Written ${lines} lines to ${p}` };
430
+ return { result: `Written ${lines} lines ${p}` };
416
431
  }
417
432
 
418
433
  case "patch_file": {
@@ -420,12 +435,12 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
420
435
  if (!fs.existsSync(p)) return { error: `File not found: ${p}` };
421
436
  const src = fs.readFileSync(p, "utf8");
422
437
  if (!src.includes(args.old_str))
423
- 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)." };
424
439
  const count = src.split(args.old_str).length - 1;
425
440
  if (count > 1)
426
441
  return { error: `old_str appears ${count} times — must be unique. Add more surrounding context.` };
427
442
  fs.writeFileSync(p, src.replace(args.old_str, args.new_str), "utf8");
428
- return { result: `Patched ${p} (1 replacement)` };
443
+ return { result: `Patched ${p} (1 occurrence replaced)` };
429
444
  }
430
445
 
431
446
  case "append_file": {
@@ -435,59 +450,6 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
435
450
  return { result: `Appended to ${p}` };
436
451
  }
437
452
 
438
- case "list_dir": {
439
- const p = path.resolve(args.path || ".");
440
- if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
441
- const items = fs.readdirSync(p, { withFileTypes: true });
442
- const filtered = args.show_hidden ? items : items.filter(i => !i.name.startsWith("."));
443
- const SKIP = new Set(["node_modules", ".git"]);
444
- const rows = filtered
445
- .filter(i => !SKIP.has(i.name))
446
- .map(i => {
447
- const st = fs.statSync(path.join(p, i.name));
448
- const size = i.isDirectory() ? "<DIR>" : fmtBytes(st.size);
449
- return `${size.padStart(8)} ${i.isDirectory() ? chalk.hex("#569CD6")(i.name+"/") : i.name}`;
450
- });
451
- return { result: `${p}\n${"─".repeat(50)}\n${rows.join("\n")}\n\n${rows.length} items` };
452
- }
453
-
454
- case "find_files": {
455
- const dir = path.resolve(args.dir || ".");
456
- const { stdout } = await execAsync(
457
- `find "${dir}" -name "${args.pattern}" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | head -200`
458
- );
459
- return { result: stdout.trim() || "No files found." };
460
- }
461
-
462
- case "search_in_files": {
463
- const dir = path.resolve(args.dir || ".");
464
- const flag = args.case_insensitive ? "-i" : "";
465
- const ext = args.extension ? `--include="*${args.extension}"` : "";
466
- const { stdout } = await execAsync(
467
- `grep -rn ${flag} ${ext} "${args.pattern}" "${dir}" --exclude-dir=node_modules --exclude-dir=.git 2>/dev/null | head -100`
468
- ).catch(e => ({ stdout: e.stdout || "" }));
469
- return { result: stdout.trim() || "No matches found." };
470
- }
471
-
472
- case "run_shell": {
473
- const cwd = args.cwd ? path.resolve(args.cwd) : process.cwd();
474
- const timeout = args.timeout || 30000;
475
- try {
476
- const { stdout, stderr } = await execAsync(args.command, { cwd, timeout, maxBuffer: 5*1024*1024 });
477
- const out = [stdout?.trim(), stderr?.trim() ? `[stderr]\n${stderr.trim()}` : null].filter(Boolean).join("\n");
478
- return { result: out || "(no output)", exitCode: 0 };
479
- } catch (err) {
480
- const out = [err.stdout?.trim(), err.stderr?.trim() ? `[stderr]\n${err.stderr.trim()}` : null, `[exit ${err.code}]`].filter(Boolean).join("\n");
481
- return { result: out, exitCode: err.code };
482
- }
483
- }
484
-
485
- case "create_dir": {
486
- const p = path.resolve(args.path);
487
- fs.mkdirSync(p, { recursive: true });
488
- return { result: `Created: ${p}` };
489
- }
490
-
491
453
  case "delete_file": {
492
454
  const p = path.resolve(args.path);
493
455
  if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
@@ -496,70 +458,60 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
496
458
  }
497
459
 
498
460
  case "move_file": {
499
- 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}` };
500
464
  fs.mkdirSync(path.dirname(to), { recursive: true });
501
465
  fs.renameSync(from, to);
502
466
  return { result: `Moved: ${from} → ${to}` };
503
467
  }
504
468
 
505
- case "get_env": {
506
- let branch = "";
507
- 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);
508
473
  return {
509
474
  result: JSON.stringify({
510
- cwd: process.cwd(),
511
- platform: process.platform,
512
- node: process.version,
513
- git_branch: branch || null,
514
- home: process.env.HOME,
515
- 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(),
516
482
  }, null, 2)
517
483
  };
518
484
  }
519
485
 
520
- case "read_url": {
521
- let hdrs = "";
522
- if (args.headers) {
523
- try {
524
- const obj = JSON.parse(args.headers);
525
- hdrs = Object.entries(obj).map(([k,v]) => `-H "${k}: ${v}"`).join(" ");
526
- } catch {}
527
- }
528
- const { stdout } = await execAsync(`curl -sL --max-time 15 ${hdrs} "${args.url}" | head -c 51200`);
529
- return { result: stdout.trim() || "(empty response)" };
530
- }
531
-
532
- // ── NEW TOOLS ──────────────────────────────────────────────
533
- case "copy_file": {
534
- const from = path.resolve(args.from);
535
- const to = path.resolve(args.to);
536
- if (!fs.existsSync(from)) return { error: `Not found: ${from}` };
537
- fs.mkdirSync(path.dirname(to), { recursive: true });
538
- const rec = args.recursive !== false;
539
- const flag = rec ? "-r" : "";
540
- await execAsync(`cp ${flag} "${from}" "${to}"`);
541
- 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." };
542
491
  }
543
492
 
544
- case "read_file_lines": {
545
- const p = path.resolve(args.path);
546
- if (!fs.existsSync(p)) return { error: `File not found: ${p}` };
547
- const lines = fs.readFileSync(p, "utf8").split("\n");
548
- const start = Math.max(1, args.start) - 1;
549
- const end = Math.min(lines.length, args.end);
550
- const slice = lines.slice(start, end);
551
- const numbered = slice.map((l, i) =>
552
- `${String(start + i + 1).padStart(4)} ${l}`
553
- ).join("\n");
554
- 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` };
555
507
  }
556
508
 
557
509
  case "tree": {
558
510
  const p = path.resolve(args.path || ".");
559
511
  const depth = args.depth ?? 3;
560
512
  const showH = args.show_hidden ?? false;
561
- const SKIP = new Set(["node_modules", ".git", "dist", "build"]);
562
- const lines = [];
513
+ const SKIP = new Set(["node_modules", ".git", "dist", "build", ".next", "__pycache__"]);
514
+ const lines = [p];
563
515
 
564
516
  const walk = (dir, prefix, d) => {
565
517
  if (d > depth) return;
@@ -574,48 +526,97 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
574
526
  if (e.isDirectory()) walk(path.join(dir, e.name), prefix + child, d + 1);
575
527
  });
576
528
  };
577
-
578
- lines.push(p);
579
529
  walk(p, "", 0);
580
530
  return { result: lines.join("\n") };
581
531
  }
582
532
 
583
- 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": {
584
552
  const p = path.resolve(args.path);
585
- if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
586
- const st = fs.statSync(p);
587
- return {
588
- result: JSON.stringify({
589
- path: p,
590
- type: st.isDirectory() ? "directory" : "file",
591
- size: fmtBytes(st.size),
592
- bytes: st.size,
593
- mode: "0" + (st.mode & 0o777).toString(8),
594
- modified: st.mtime.toISOString(),
595
- created: st.birthtime.toISOString(),
596
- }, null, 2)
597
- };
553
+ fs.mkdirSync(p, { recursive: true });
554
+ return { result: `Created: ${p}` };
598
555
  }
599
556
 
600
- case "diff_files": {
601
- const a = path.resolve(args.file_a);
602
- const b = path.resolve(args.file_b);
603
- const { stdout } = await execAsync(`diff -u "${a}" "${b}"`).catch(e => ({ stdout: e.stdout || "" }));
604
- 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)" };
605
602
  }
606
603
 
604
+ // ── Network ──────────────────────────────────────────
607
605
  case "http_request": {
608
- const method = (args.method ?? "GET").toUpperCase();
606
+ const method = (args.method ?? "GET").toUpperCase();
609
607
  let hdrs = "";
610
608
  if (args.headers) {
611
609
  try {
612
610
  const obj = JSON.parse(args.headers);
613
- hdrs = Object.entries(obj).map(([k,v]) => `-H "${k}: ${v}"`).join(" ");
611
+ hdrs = Object.entries(obj).map(([k, v]) => `-H "${k}: ${v}"`).join(" ");
614
612
  } catch {}
615
613
  }
616
- const bodyFlag = args.body ? `-d '${args.body.replace(/'/g, "'\\''")}'` : "";
617
- const cmd = `curl -s -X ${method} ${hdrs} ${bodyFlag} --max-time 20 "${args.url}" | head -c 102400`;
618
- 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
+ );
619
620
  return { result: stdout.trim() || "(empty response)" };
620
621
  }
621
622
 
@@ -624,135 +625,193 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
624
625
  fs.mkdirSync(path.dirname(dest), { recursive: true });
625
626
  await execAsync(`curl -L --max-time 60 -o "${dest}" "${args.url}"`);
626
627
  const size = fmtBytes(fs.statSync(dest).size);
627
- return { result: `Downloaded to ${dest} (${size})` };
628
- }
629
-
630
- case "hash_file": {
631
- const p = path.resolve(args.path);
632
- if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
633
- const alg = args.algorithm ?? "sha256";
634
- const cmd = alg === "md5"
635
- ? `md5sum "${p}" 2>/dev/null || md5 -q "${p}"`
636
- : `sha${alg === "sha256" ? "256" : alg === "sha1" ? "1" : "256"}sum "${p}" 2>/dev/null || shasum -a ${alg === "sha1" ? "1" : "256"} "${p}"`;
637
- const { stdout } = await execAsync(cmd);
638
- return { result: stdout.trim().split(" ")[0], algorithm: alg, path: p };
639
- }
640
-
641
- case "base64": {
642
- const action = args.action.toLowerCase();
643
- let input = args.input;
644
- if (args.is_file) {
645
- const p = path.resolve(input);
646
- if (!fs.existsSync(p)) return { error: `File not found: ${p}` };
647
- input = fs.readFileSync(p, "utf8");
648
- }
649
- if (action === "encode") {
650
- return { result: Buffer.from(input).toString("base64") };
651
- } else {
652
- return { result: Buffer.from(input, "base64").toString("utf8") };
653
- }
628
+ return { result: `Downloaded ${dest} (${size})` };
654
629
  }
655
630
 
631
+ // ── Data ─────────────────────────────────────────────
656
632
  case "json_query": {
657
633
  let data;
658
- if (args.is_file) {
659
- const p = path.resolve(args.input);
660
- if (!fs.existsSync(p)) return { error: `File not found: ${p}` };
661
- data = JSON.parse(fs.readFileSync(p, "utf8"));
662
- } else {
663
- 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}` };
664
640
  }
665
641
  if (!args.query) return { result: JSON.stringify(data, null, 2) };
666
- // Simple dot+bracket notation traversal
667
642
  const keys = args.query.replace(/\[(\d+)\]/g, ".$1").split(".");
668
643
  let val = data;
669
644
  for (const k of keys) {
670
- if (val == null) return { error: `Key "${k}" not found` };
645
+ if (val == null) return { error: `Key "${k}" not found at path "${args.query}"` };
671
646
  val = val[k];
672
647
  }
673
648
  return { result: typeof val === "object" ? JSON.stringify(val, null, 2) : String(val) };
674
649
  }
675
650
 
676
- case "extract": {
677
- const archive = path.resolve(args.archive);
678
- if (!fs.existsSync(archive)) return { error: `Archive not found: ${archive}` };
679
- const dest = path.resolve(args.dest ?? path.dirname(archive));
680
- fs.mkdirSync(dest, { recursive: true });
681
- const ext = archive.toLowerCase();
682
- let cmd;
683
- if (ext.endsWith(".zip")) cmd = `unzip -o "${archive}" -d "${dest}"`;
684
- else if (ext.endsWith(".tar.gz") || ext.endsWith(".tgz")) cmd = `tar -xzf "${archive}" -C "${dest}"`;
685
- else if (ext.endsWith(".tar.bz2")) cmd = `tar -xjf "${archive}" -C "${dest}"`;
686
- else if (ext.endsWith(".tar")) cmd = `tar -xf "${archive}" -C "${dest}"`;
687
- else if (ext.endsWith(".gz")) cmd = `gunzip -k "${archive}"`;
688
- else return { error: `Unsupported archive format: ${archive}` };
689
- const { stdout, stderr } = await execAsync(cmd);
690
- return { result: `Extracted to ${dest}` };
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)
665
+ };
691
666
  }
692
667
 
693
- case "compress": {
694
- const source = path.resolve(args.source);
695
- const dest = path.resolve(args.dest);
696
- if (!fs.existsSync(source)) return { error: `Not found: ${source}` };
697
- fs.mkdirSync(path.dirname(dest), { recursive: true });
698
- let cmd;
699
- if (dest.endsWith(".zip")) cmd = `zip -r "${dest}" "${source}"`;
700
- else if (dest.endsWith(".tar.gz") || dest.endsWith(".tgz")) cmd = `tar -czf "${dest}" "${source}"`;
701
- else if (dest.endsWith(".tar")) cmd = `tar -cf "${dest}" "${source}"`;
702
- else return { error: "Unsupported format. Use .zip or .tar.gz" };
703
- await execAsync(cmd);
704
- const size = fmtBytes(fs.statSync(dest).size);
705
- return { result: `Compressed to ${dest} (${size})` };
706
- }
668
+ // ── Real-time ─────────────────────────────────────────
669
+ case "web_search": {
670
+ const q = encodeURIComponent(args.query);
671
+ const { stdout: ddg } = await execAsync(
672
+ `curl -sL --max-time 10 "https://api.duckduckgo.com/?q=${q}&format=json&no_redirect=1&no_html=1&skip_disambig=1"`
673
+ ).catch(() => ({ stdout: "" }));
674
+ const { stdout: html } = await execAsync(
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`
676
+ ).catch(() => ({ stdout: "" }));
707
677
 
708
- case "git": {
709
- const cwd = args.cwd ? path.resolve(args.cwd) : process.cwd();
710
- const a = args.action.toLowerCase();
711
- const extra = args.args ?? "";
712
- const gitCmds = {
713
- status: `git status`,
714
- log: `git log --oneline -20 ${extra}`,
715
- diff: `git diff ${extra}`,
716
- add: `git add ${extra || "."}`,
717
- commit: `git commit -m "${extra}"`,
718
- push: `git push ${extra}`,
719
- pull: `git pull ${extra}`,
720
- clone: `git clone ${extra}`,
721
- checkout: `git checkout ${extra}`,
722
- branch: `git branch ${extra}`,
723
- init: `git init ${extra}`,
724
- stash: `git stash ${extra}`,
725
- };
726
- const cmd = gitCmds[a];
727
- if (!cmd) return { error: `Unknown git action: ${a}` };
728
- const { stdout, stderr } = await execAsync(cmd, { cwd }).catch(e => ({
729
- stdout: e.stdout || "", stderr: e.stderr || ""
730
- }));
731
- return { result: (stdout + (stderr ? "\n" + stderr : "")).trim() || "(no output)" };
678
+ let out = "";
679
+ try {
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) {
684
+ out += "Related:\n";
685
+ d.RelatedTopics.slice(0, 5).forEach(t => { if (t.Text) out += ` · ${t.Text}\n`; });
686
+ }
687
+ } catch {}
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." };
690
+ }
691
+
692
+ case "get_weather": {
693
+ const loc = encodeURIComponent(args.location);
694
+ if (args.format === "full") {
695
+ const { stdout } = await execAsync(`curl -sL --max-time 10 "https://wttr.in/${loc}?format=j1"`);
696
+ try {
697
+ const d = JSON.parse(stdout);
698
+ const c = d.current_condition[0];
699
+ const a = d.nearest_area[0];
700
+ return {
701
+ result: JSON.stringify({
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,
710
+ }, null, 2)
711
+ };
712
+ } catch {}
713
+ }
714
+ const { stdout } = await execAsync(`curl -sL --max-time 10 "https://wttr.in/${loc}?format=3"`);
715
+ return { result: stdout.trim() || "Could not fetch weather." };
732
716
  }
733
717
 
734
- case "process_list": {
735
- const filter = args.filter ?? "";
736
- const cmd = filter
737
- ? `ps aux | grep -i "${filter}" | grep -v grep`
738
- : `ps aux | head -30`;
739
- const { stdout } = await execAsync(cmd).catch(e => ({ stdout: e.stdout || "" }));
740
- return { result: stdout.trim() || "No processes found." };
718
+ case "get_news": {
719
+ const q = encodeURIComponent(args.query);
720
+ const lang = args.language ?? "id";
721
+ const lim = Math.min(args.limit ?? 10, 20);
722
+ const results = [];
723
+
724
+ const { stdout: hn } = await execAsync(
725
+ `curl -sL --max-time 12 "https://hn.algolia.com/api/v1/search?query=${q}&tags=story&hitsPerPage=${lim}"`
726
+ ).catch(() => ({ stdout: "" }));
727
+ try {
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
+ });
731
+ } catch {}
732
+
733
+ if (results.length < 5) {
734
+ const { stdout: rss } = await execAsync(
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}`
736
+ ).catch(() => ({ stdout: "" }));
737
+ rss.trim().split("\n").filter(Boolean).forEach((h, i) => {
738
+ results.push(`${results.length+1}. ${h}`);
739
+ });
740
+ }
741
+ return { result: results.slice(0, lim).join("\n\n") || "No news found." };
741
742
  }
742
743
 
743
- case "disk_usage": {
744
- const p = args.path ? path.resolve(args.path) : process.cwd();
745
- const { stdout: du } = await execAsync(`du -sh "${p}" 2>/dev/null`).catch(() => ({ stdout: "" }));
746
- const { stdout: df } = await execAsync(`df -h "${p}" 2>/dev/null`).catch(() => ({ stdout: "" }));
747
- return { result: [du.trim(), df.trim()].filter(Boolean).join("\n\n") || "(unavailable)" };
744
+ case "get_exchange_rate": {
745
+ const from = (args.from ?? "USD").toUpperCase();
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}"`);
748
+ try {
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." }; }
748
753
  }
749
754
 
750
- case "chmod": {
751
- const p = path.resolve(args.path);
752
- if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
753
- const rec = args.recursive ? "-R" : "";
754
- await execAsync(`chmod ${rec} ${args.permissions} "${p}"`);
755
- return { result: `chmod ${args.permissions} applied to ${p}` };
755
+ case "get_stock": {
756
+ const sym = encodeURIComponent(args.symbol.toUpperCase());
757
+ const { stdout } = await execAsync(
758
+ `curl -sL --max-time 12 "https://query1.finance.yahoo.com/v8/finance/chart/${sym}?range=1d&interval=1d"`
759
+ );
760
+ try {
761
+ const meta = JSON.parse(stdout).chart.result[0].meta;
762
+ const chg = meta.regularMarketPrice - meta.previousClose;
763
+ return {
764
+ result: JSON.stringify({
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,
775
+ }, null, 2)
776
+ };
777
+ } catch { return { error: `Could not fetch stock: ${args.symbol}` }; }
778
+ }
779
+
780
+ case "get_crypto": {
781
+ const ids = encodeURIComponent(args.coins.toLowerCase().replace(/\s/g, ""));
782
+ const cur = (args.currency ?? "usd").toLowerCase();
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
+ );
786
+ try {
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
+ );
793
+ return { result: lines.join("\n") };
794
+ } catch { return { error: "Could not fetch crypto prices." }; }
795
+ }
796
+
797
+ case "get_ip_info": {
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}"`);
800
+ try {
801
+ const d = JSON.parse(stdout);
802
+ return {
803
+ result: JSON.stringify({
804
+ ip: d.ip,
805
+ city: d.city,
806
+ region: d.region,
807
+ country: d.country_name,
808
+ timezone: d.timezone,
809
+ isp: d.org,
810
+ lat: d.latitude,
811
+ lon: d.longitude,
812
+ }, null, 2)
813
+ };
814
+ } catch { return { error: "Could not fetch IP info." }; }
756
815
  }
757
816
 
758
817
  default:
@@ -763,21 +822,25 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
763
822
  }
764
823
  }
765
824
 
766
- function buildConfirmLabel(name, args) {
767
- switch (name) {
768
- case "write_file": return `Write file: ${chalk.yellow(args.path)}`;
769
- case "patch_file": return `Patch file: ${chalk.yellow(args.path)}`;
770
- case "append_file": return `Append to: ${chalk.yellow(args.path)}`;
771
- case "run_shell": return `Run shell: ${chalk.cyan(args.command)}`;
772
- case "create_dir": return `Create dir: ${chalk.yellow(args.path)}`;
773
- case "delete_file": return `${chalk.red("DELETE")}: ${chalk.yellow(args.path)}`;
774
- case "move_file": return `Move: ${chalk.yellow(args.from)} ${chalk.yellow(args.to)}`;
775
- default: return `Execute: ${name}`;
776
- }
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}`;
777
840
  }
778
841
 
779
842
  function fmtBytes(n) {
780
- if (n < 1024) return n + "B";
781
- if (n < 1048576) return (n/1024).toFixed(1) + "K";
782
- 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";
783
846
  }