@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.
- package/package.json +1 -1
- package/src/agent.js +13 -0
- package/src/tools.js +486 -423
package/src/tools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/tools.js —
|
|
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
|
|
13
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
34
|
-
content: { type: "STRING", description: "Complete file content
|
|
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
|
|
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
|
|
46
|
-
old_str: { type: "STRING", description: "Exact unique string to find (with surrounding context
|
|
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
|
|
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: "
|
|
66
|
-
description: "
|
|
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
|
-
|
|
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: ["
|
|
96
|
+
required: ["path"]
|
|
99
97
|
}
|
|
100
98
|
},
|
|
101
99
|
{
|
|
102
|
-
name: "
|
|
103
|
-
description: "
|
|
100
|
+
name: "move_file",
|
|
101
|
+
description: "Move or rename a file or directory.",
|
|
104
102
|
parameters: {
|
|
105
103
|
type: "OBJECT",
|
|
106
104
|
properties: {
|
|
107
|
-
|
|
108
|
-
|
|
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: ["
|
|
108
|
+
required: ["from", "to"]
|
|
112
109
|
}
|
|
113
110
|
},
|
|
114
111
|
{
|
|
115
|
-
name: "
|
|
116
|
-
description: "
|
|
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: "
|
|
117
|
+
path: { type: "STRING", description: "File or directory path" }
|
|
121
118
|
},
|
|
122
119
|
required: ["path"]
|
|
123
120
|
}
|
|
124
121
|
},
|
|
125
122
|
{
|
|
126
|
-
name: "
|
|
127
|
-
description: "
|
|
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
|
-
|
|
128
|
+
file_a: { type: "STRING", description: "First file path (original)" },
|
|
129
|
+
file_b: { type: "STRING", description: "Second file path (modified)" }
|
|
132
130
|
},
|
|
133
|
-
required: ["
|
|
131
|
+
required: ["file_a", "file_b"]
|
|
134
132
|
}
|
|
135
133
|
},
|
|
134
|
+
|
|
135
|
+
// ── Directory ────────────────────────────────────────────────
|
|
136
136
|
{
|
|
137
|
-
name: "
|
|
138
|
-
description: "
|
|
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
|
-
|
|
143
|
-
|
|
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: "
|
|
150
|
-
description: "
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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: "
|
|
168
|
-
description: "
|
|
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
|
-
|
|
173
|
-
|
|
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: ["
|
|
168
|
+
required: ["pattern"]
|
|
177
169
|
}
|
|
178
170
|
},
|
|
179
171
|
{
|
|
180
|
-
name: "
|
|
181
|
-
description: "
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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: ["
|
|
182
|
+
required: ["pattern"]
|
|
190
183
|
}
|
|
191
184
|
},
|
|
192
185
|
{
|
|
193
|
-
name: "
|
|
194
|
-
description: "
|
|
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:
|
|
199
|
-
|
|
200
|
-
|
|
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: "
|
|
206
|
-
description: "
|
|
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
|
-
|
|
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: ["
|
|
208
|
+
required: ["command"]
|
|
213
209
|
}
|
|
214
210
|
},
|
|
211
|
+
|
|
212
|
+
// ── Git ──────────────────────────────────────────────────────
|
|
215
213
|
{
|
|
216
|
-
name: "
|
|
217
|
-
description: "
|
|
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
|
-
|
|
222
|
-
|
|
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: ["
|
|
223
|
+
required: ["action"]
|
|
225
224
|
}
|
|
226
225
|
},
|
|
226
|
+
|
|
227
|
+
// ── Network ──────────────────────────────────────────────────
|
|
227
228
|
{
|
|
228
229
|
name: "http_request",
|
|
229
|
-
description: "Make an HTTP request (GET
|
|
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
|
|
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: "
|
|
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: "
|
|
255
|
-
description: "
|
|
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
|
-
|
|
260
|
-
|
|
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: ["
|
|
266
|
+
required: ["input"]
|
|
263
267
|
}
|
|
264
268
|
},
|
|
269
|
+
|
|
270
|
+
// ── System ───────────────────────────────────────────────────
|
|
265
271
|
{
|
|
266
|
-
name: "
|
|
267
|
-
description: "
|
|
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: "
|
|
280
|
-
description: "
|
|
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
|
-
|
|
285
|
-
|
|
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: ["
|
|
287
|
+
required: ["query"]
|
|
289
288
|
}
|
|
290
289
|
},
|
|
291
290
|
{
|
|
292
|
-
name: "
|
|
293
|
-
description: "
|
|
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
|
-
|
|
298
|
-
|
|
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: ["
|
|
299
|
+
required: ["location"]
|
|
301
300
|
}
|
|
302
301
|
},
|
|
303
302
|
{
|
|
304
|
-
name: "
|
|
305
|
-
description: "
|
|
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
|
-
|
|
310
|
-
|
|
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: ["
|
|
312
|
+
required: ["query"]
|
|
313
313
|
}
|
|
314
314
|
},
|
|
315
315
|
{
|
|
316
|
-
name: "
|
|
317
|
-
description: "
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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: "
|
|
330
|
-
description: "
|
|
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
|
-
|
|
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: "
|
|
340
|
-
description: "
|
|
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
|
-
|
|
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: "
|
|
350
|
-
description: "
|
|
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
|
-
|
|
355
|
-
|
|
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
|
-
//
|
|
361
|
+
// Gemini API tools payload
|
|
364
362
|
export const GEMINI_TOOLS = [{ functionDeclarations: FUNCTION_DECLARATIONS }];
|
|
365
363
|
|
|
366
364
|
// ─────────────────────────────────────────────────────────────────
|
|
367
|
-
//
|
|
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("#
|
|
378
|
-
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
|
-
//
|
|
384
|
+
// Tool executor
|
|
386
385
|
// ─────────────────────────────────────────────────────────────────
|
|
387
386
|
export async function executeTool(name, args = {}, { autoApprove = false } = {}) {
|
|
388
387
|
if (DESTRUCTIVE.has(name)) {
|
|
389
|
-
const label =
|
|
390
|
-
|
|
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
|
|
401
|
-
const MAX
|
|
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
|
|
407
|
-
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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 "
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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 "
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
const
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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 "
|
|
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
|
-
|
|
586
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
const
|
|
603
|
-
const
|
|
604
|
-
|
|
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
|
|
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
|
|
617
|
-
|
|
618
|
-
|
|
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
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
}
|
|
663
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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 "
|
|
735
|
-
const
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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 "
|
|
744
|
-
const
|
|
745
|
-
const
|
|
746
|
-
const { stdout
|
|
747
|
-
|
|
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 "
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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)
|
|
781
|
-
if (n < 1048576) return (n/1024).toFixed(1) + "
|
|
782
|
-
return (n/1048576).toFixed(1) + "
|
|
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
|
}
|