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