@ikyyofc/gemini-cli 3.0.7 โ†’ 3.0.9

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Gemini CLI ๐Ÿค–
2
2
 
3
- > AI Agent CLI โ€” native function calling ยท GEMINI.md context ยท extension system
3
+ > AI Agent CLI โ€” native function calling ยท Skills ยท GEMINI.md context ยท extension system
4
4
 
5
5
  ```
6
6
  โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
@@ -8,19 +8,7 @@
8
8
  โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
9
9
  ```
10
10
 
11
- Gemini CLI adalah asisten terminal bertenaga AI yang menggunakan model Gemini dari Google. CLI ini bukan sekadar antarmuka chat biasa, melainkan sebuah **AI Agent** yang dapat berinteraksi langsung dengan sistem file dan environment lokal Anda menggunakan *native function calling*.
12
-
13
- ---
14
-
15
- ## ๐Ÿ“š Dokumentasi Lengkap
16
-
17
- Untuk detail lebih lanjut mengenai arsitektur dan fitur spesifik, silakan baca dokumentasi berikut:
18
-
19
- - [**Architecture Overview**](./docs/ARCHITECTURE.md) - Penjelasan tentang ReAct loop dan cara kerja agent.
20
- - [**API Reference**](./docs/API.md) - Referensi fungsi utama (`callGemini`, `chat`).
21
- - [**Tools (Function Calling)**](./docs/TOOLS.md) - Daftar lengkap tool yang tersedia untuk agent (baca file, jalankan shell, dll).
22
- - [**Extensions System**](./docs/EXTENSIONS.md) - Cara membuat dan mengelola ekstensi serta custom commands.
23
- - [**Memory & Context**](./docs/MEMORY.md) - Panduan menggunakan `GEMINI.md` untuk memberikan konteks pada agent.
11
+ Gemini CLI adalah asisten terminal bertenaga AI yang menggunakan model Gemini dari Google. CLI ini bukan sekadar antarmuka chat biasa, melainkan sebuah **AI Agent** yang dapat berinteraksi langsung dengan sistem file, menjalankan perintah shell, dan menggunakan *skills* pihak ketiga secara mandiri (*native function calling*).
24
12
 
25
13
  ---
26
14
 
@@ -39,23 +27,27 @@ npm link # optional: pakai sebagai `gemini` di terminal
39
27
  Anda dapat menggunakan Gemini CLI dalam mode interaktif (REPL) atau mode *one-shot* langsung dari terminal.
40
28
 
41
29
  ```bash
42
- gemini # Masuk ke mode interactive agent
43
- gemini "buatkan REST API di ./api" # One-shot task
30
+ gemini # Masuk ke mode interactive agent
31
+ gemini "buatkan REST API di ./api" # One-shot task
44
32
  gemini --system "Kamu senior backend engineer" # Set system prompt
45
33
  gemini --file ./app.js "jelaskan kode ini" # Lampirkan file
46
34
  gemini --yolo "refactor semua file di src/" # Skip semua konfirmasi tool (HATI-HATI)
47
- gemini --chat # Plain chat tanpa tools (bukan agent)
35
+ gemini --chat # Plain chat tanpa tools (bukan agent)
48
36
  ```
49
37
 
50
38
  ### Interactive Commands
51
39
 
52
40
  Saat berada di dalam mode interaktif, Anda dapat menggunakan perintah berikut:
53
41
 
54
- ```
42
+ ```text
55
43
  /agent โ†’ Toggle agent mode (tools on/off)
56
44
  /yolo โ†’ Skip all tool confirmations
57
45
  /file <path> โ†’ Attach file to next message
58
46
  /system <text> โ†’ Set system instruction
47
+ /skill โ†’ Manage skills (/skill list, add, remove, find, update, init)
48
+ /memory โ†’ Manage memory/context (/memory show, reload, add)
49
+ /ext โ†’ Manage extensions (/ext list, install, uninstall, enable, disable)
50
+ /proxy โ†’ Toggle proxy rotation (/proxy on, /proxy off)
59
51
  /history โ†’ Show conversation turns
60
52
  /export <file> โ†’ Export history to JSON
61
53
  /cd <path> โ†’ Change working directory
@@ -71,34 +63,42 @@ Saat berada di dalam mode interaktif, Anda dapat menggunakan perintah berikut:
71
63
  ## ๐Ÿง  Fitur Utama
72
64
 
73
65
  ### 1. Native Function Calling (Tools)
74
- Agent dapat membaca file, menulis file, menjalankan perintah shell, dan mencari file secara mandiri untuk menyelesaikan tugas yang Anda berikan. [Baca selengkapnya](./docs/TOOLS.md).
66
+ Agent dapat membaca file, menulis file, menjalankan perintah shell, dan mencari file secara mandiri untuk menyelesaikan tugas yang Anda berikan.
67
+
68
+ ### 2. Skills System
69
+ Gemini CLI mendukung integrasi *Skills* melalui `npx skills`. Anda dapat menginstal keahlian tambahan yang akan otomatis dimuat ke dalam prompt system agent.
70
+ Contoh: `/skill add anthropics/skills --skill frontend-design`
71
+
72
+ ### 3. Hierarchical Context (`GEMINI.md`)
73
+ Berikan instruksi spesifik proyek atau global menggunakan file `GEMINI.md`. Agent akan memuat konteks ini secara otomatis. Gunakan `/memory` untuk mengatur teks konteks di memori secara dinamis selama sesi berlangsung.
75
74
 
76
- ### 2. Hierarchical Context (`GEMINI.md`)
77
- Anda dapat memberikan instruksi spesifik proyek atau global menggunakan file `GEMINI.md`. Agent akan memuat konteks ini secara otomatis. [Baca selengkapnya](./docs/MEMORY.md).
75
+ ### 4. Extension System
76
+ Perluas kemampuan CLI dengan membuat ekstensi yang berisi custom commands dan konteks tambahan. Kelola menggunakan perintah `/ext`.
78
77
 
79
- ### 3. Extension System
80
- Perluas kemampuan CLI dengan membuat ekstensi yang berisi custom commands dan konteks tambahan. [Baca selengkapnya](./docs/EXTENSIONS.md).
78
+ ### 5. Proxy Rotation
79
+ Sistem menggunakan proxy rotasi secara otomatis (`src/utils/proxy.js`) sehingga requests Anda stabil. Status dapat diatur menggunakan perintah `/proxy`.
81
80
 
82
81
  ---
83
82
 
84
83
  ## ๐Ÿ“‚ Struktur Direktori
85
84
 
86
- ```
85
+ ```text
87
86
  gemini-cli/
88
87
  โ”œโ”€โ”€ index.js โ† CLI entry + REPL + commands
89
88
  โ”œโ”€โ”€ package.json
90
- โ”œโ”€โ”€ docs/ โ† Dokumentasi lengkap
91
89
  โ”œโ”€โ”€ src/
92
90
  โ”‚ โ”œโ”€โ”€ gemini.js โ† API client (native function calling)
93
91
  โ”‚ โ”œโ”€โ”€ tools.js โ† functionDeclarations + executor
94
92
  โ”‚ โ”œโ”€โ”€ agent.js โ† ReAct loop
95
93
  โ”‚ โ”œโ”€โ”€ memory.js โ† GEMINI.md hierarchy loader
96
94
  โ”‚ โ”œโ”€โ”€ extensions.js โ† Extension manager
95
+ โ”‚ โ”œโ”€โ”€ skills.js โ† Skills manager via npx skills
97
96
  โ”‚ โ”œโ”€โ”€ renderer.js โ† Terminal UI + markdown
98
- โ”‚ โ””โ”€โ”€ input.js โ† Bracketed paste via Transform stream
99
- โ””โ”€โ”€ utils/
100
- โ””โ”€โ”€ proxy-manager.js โ† Global proxy manager
101
-
97
+ โ”‚ โ”œโ”€โ”€ input.js โ† Bracketed paste via Transform stream
98
+ โ”‚ โ””โ”€โ”€ utils/
99
+ โ”‚ โ”œโ”€โ”€ proxy.js โ† Global proxy manager
100
+ โ”‚ โ””โ”€โ”€ spinner.js โ† Loading spinner
101
+ โ”‚
102
102
  ~/.gemini/ โ† Global config dir (dibuat otomatis)
103
103
  โ”œโ”€โ”€ GEMINI.md โ† Global context
104
104
  โ”œโ”€โ”€ extensions/ โ† Folder instalasi ekstensi
package/index.js CHANGED
@@ -27,7 +27,7 @@ import { Spinner } from "./src/utils/spinner.js";
27
27
  import {
28
28
  listInstalledSkills, installSkill, removeSkillNpx,
29
29
  findSkills, listNpxSkills, updateSkill, initSkill,
30
- ensureSkillsDirs, loadSkills,
30
+ ensureSkillsDirs, loadSkills, cleanLockFile,
31
31
  } from "./src/skills.js";
32
32
 
33
33
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -35,6 +35,7 @@ import {
35
35
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
36
36
  ensureGlobalDir();
37
37
  ensureSkillsDirs();
38
+ cleanLockFile(); // remove skills-lock.json from cwd on every startup
38
39
  setupGlobalProxy(); // aktifkan rotasi proxy sebelum request apapun
39
40
 
40
41
  let extensions = loadExtensions();
@@ -133,10 +134,23 @@ function attachFile(fp) {
133
134
  // Send message
134
135
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
135
136
  async function send(rawLine) {
136
- // Decode \x00 โ†’ \n from paste encoding
137
137
  const userText = restorePaste(rawLine).trim();
138
138
  if (!userText) return;
139
139
 
140
+ // Clear terminal when there's previous history so it stays light.
141
+ // Keep last exchange visible by reprinting it first.
142
+ if (history.length >= 2) {
143
+ process.stdout.write("\x1Bc");
144
+ // Reprint the last assistant response as context
145
+ const lastAssistant = history.filter(m => m.role === "assistant").at(-1);
146
+ if (lastAssistant) {
147
+ process.stdout.write(chalk.hex("#4A4A5E").dim(
148
+ ` (${Math.ceil(history.length / 2)} turns in memory)\n`
149
+ ));
150
+ printAssistant(lastAssistant.content);
151
+ }
152
+ }
153
+
140
154
  printUser(userText + (pendingFile ? chalk.dim(` [${path.basename(pendingPath)}]`) : ""));
141
155
 
142
156
  if (agentMode) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikyyofc/gemini-cli",
3
- "version": "3.0.7",
3
+ "version": "3.0.9",
4
4
  "description": "AI Agent CLI โ€” native function calling ยท GEMINI.md context ยท extensions",
5
5
  "type": "module",
6
6
  "bin": { "gemini": "./index.js" },
package/src/skills.js CHANGED
@@ -25,6 +25,14 @@ export function ensureSkillsDirs() {
25
25
  fs.mkdirSync(AGENTS_DIR, { recursive: true });
26
26
  }
27
27
 
28
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
29
+ // Delete skills-lock.json from cwd (npx skills drops this on install)
30
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
31
+ export function cleanLockFile(cwd = process.cwd()) {
32
+ const p = path.join(cwd, "skills-lock.json");
33
+ try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch {}
34
+ }
35
+
28
36
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
29
37
  // Scan a dir tree for SKILL.md files
30
38
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -127,6 +135,9 @@ export async function installSkill(rawSource, opts = {}) {
127
135
 
128
136
  const { stdout, stderr } = await npxSkills(parts.join(" "));
129
137
 
138
+ // Cleanup skills-lock.json that npx skills drops in cwd
139
+ cleanLockFile();
140
+
130
141
  // Report what was newly installed
131
142
  const after = loadSkills().map(s => s.slug);
132
143
  const newSlugs = after.filter(s => !before.has(s));
package/src/tools.js CHANGED
@@ -161,6 +161,202 @@ export const FUNCTION_DECLARATIONS = [
161
161
  },
162
162
  required: ["url"]
163
163
  }
164
+ },
165
+ // โ”€โ”€ NEW TOOLS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
166
+ {
167
+ name: "copy_file",
168
+ description: "Copy a file or directory to a new location.",
169
+ parameters: {
170
+ type: "OBJECT",
171
+ properties: {
172
+ from: { type: "STRING", description: "Source path" },
173
+ to: { type: "STRING", description: "Destination path" },
174
+ recursive: { type: "BOOLEAN", description: "Copy directory recursively (default true)" }
175
+ },
176
+ required: ["from", "to"]
177
+ }
178
+ },
179
+ {
180
+ name: "read_file_lines",
181
+ description: "Read a specific range of lines from a file. Use for large files.",
182
+ parameters: {
183
+ type: "OBJECT",
184
+ properties: {
185
+ path: { type: "STRING", description: "File path" },
186
+ start: { type: "NUMBER", description: "Start line number (1-based)" },
187
+ end: { type: "NUMBER", description: "End line number (inclusive)" }
188
+ },
189
+ required: ["path", "start", "end"]
190
+ }
191
+ },
192
+ {
193
+ name: "tree",
194
+ description: "Show directory structure as a tree. Good for understanding project layout.",
195
+ parameters: {
196
+ type: "OBJECT",
197
+ properties: {
198
+ path: { type: "STRING", description: "Root directory (default: .)" },
199
+ depth: { type: "NUMBER", description: "Max depth (default: 3)" },
200
+ show_hidden: { type: "BOOLEAN", description: "Include hidden files" }
201
+ }
202
+ }
203
+ },
204
+ {
205
+ name: "file_info",
206
+ description: "Get file or directory metadata: size, permissions, modified date, type.",
207
+ parameters: {
208
+ type: "OBJECT",
209
+ properties: {
210
+ path: { type: "STRING", description: "File or directory path" }
211
+ },
212
+ required: ["path"]
213
+ }
214
+ },
215
+ {
216
+ name: "diff_files",
217
+ description: "Show line-by-line differences between two files.",
218
+ parameters: {
219
+ type: "OBJECT",
220
+ properties: {
221
+ file_a: { type: "STRING", description: "First file path" },
222
+ file_b: { type: "STRING", description: "Second file path" }
223
+ },
224
+ required: ["file_a", "file_b"]
225
+ }
226
+ },
227
+ {
228
+ name: "http_request",
229
+ description: "Make an HTTP request (GET/POST/PUT/PATCH/DELETE) with headers and body.",
230
+ parameters: {
231
+ type: "OBJECT",
232
+ properties: {
233
+ url: { type: "STRING", description: "URL to request" },
234
+ method: { type: "STRING", description: "HTTP method (default: GET)" },
235
+ headers: { type: "STRING", description: "JSON string of request headers" },
236
+ body: { type: "STRING", description: "Request body for POST/PUT/PATCH" }
237
+ },
238
+ required: ["url"]
239
+ }
240
+ },
241
+ {
242
+ name: "download_file",
243
+ description: "Download a file from a URL and save it to disk.",
244
+ parameters: {
245
+ type: "OBJECT",
246
+ properties: {
247
+ url: { type: "STRING", description: "URL to download" },
248
+ dest: { type: "STRING", description: "Destination file path" }
249
+ },
250
+ required: ["url", "dest"]
251
+ }
252
+ },
253
+ {
254
+ name: "hash_file",
255
+ description: "Calculate hash of a file (md5, sha1, sha256).",
256
+ parameters: {
257
+ type: "OBJECT",
258
+ properties: {
259
+ path: { type: "STRING", description: "File path" },
260
+ algorithm: { type: "STRING", description: "md5 | sha1 | sha256 (default: sha256)" }
261
+ },
262
+ required: ["path"]
263
+ }
264
+ },
265
+ {
266
+ name: "base64",
267
+ description: "Encode or decode base64. Works on strings or files.",
268
+ parameters: {
269
+ type: "OBJECT",
270
+ properties: {
271
+ action: { type: "STRING", description: "encode or decode" },
272
+ input: { type: "STRING", description: "String to process, or file path if is_file=true" },
273
+ is_file: { type: "BOOLEAN", description: "If true, input is treated as a file path" }
274
+ },
275
+ required: ["action", "input"]
276
+ }
277
+ },
278
+ {
279
+ name: "json_query",
280
+ description: "Parse and query JSON using a dot-notation key path. e.g. 'user.name', 'items[0].id'.",
281
+ parameters: {
282
+ type: "OBJECT",
283
+ properties: {
284
+ input: { type: "STRING", description: "JSON file path or raw JSON string" },
285
+ query: { type: "STRING", description: "Key path e.g. 'user.name' (empty = pretty-print all)" },
286
+ is_file: { type: "BOOLEAN", description: "If true, input is a file path" }
287
+ },
288
+ required: ["input"]
289
+ }
290
+ },
291
+ {
292
+ name: "extract",
293
+ description: "Extract a zip, tar, tar.gz, or tar.bz2 archive.",
294
+ parameters: {
295
+ type: "OBJECT",
296
+ properties: {
297
+ archive: { type: "STRING", description: "Path to the archive file" },
298
+ dest: { type: "STRING", description: "Destination directory (default: same as archive)" }
299
+ },
300
+ required: ["archive"]
301
+ }
302
+ },
303
+ {
304
+ name: "compress",
305
+ description: "Compress files or a directory into a zip or tar.gz archive.",
306
+ parameters: {
307
+ type: "OBJECT",
308
+ properties: {
309
+ source: { type: "STRING", description: "File or directory to compress" },
310
+ dest: { type: "STRING", description: "Output archive path (.zip or .tar.gz)" }
311
+ },
312
+ required: ["source", "dest"]
313
+ }
314
+ },
315
+ {
316
+ name: "git",
317
+ description: "Run git operations: status, log, diff, add, commit, push, pull, clone, checkout, branch, init, stash.",
318
+ parameters: {
319
+ type: "OBJECT",
320
+ properties: {
321
+ action: { type: "STRING", description: "status | log | diff | add | commit | push | pull | clone | checkout | branch | init | stash" },
322
+ args: { type: "STRING", description: "Additional arguments (commit message, branch name, remote, etc.)" },
323
+ cwd: { type: "STRING", description: "Working directory (default: current)" }
324
+ },
325
+ required: ["action"]
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
+ }
346
+ }
347
+ },
348
+ {
349
+ name: "chmod",
350
+ description: "Change file permissions. e.g. '755', '+x', 'a+r'.",
351
+ parameters: {
352
+ type: "OBJECT",
353
+ properties: {
354
+ path: { type: "STRING", description: "File or directory path" },
355
+ permissions: { type: "STRING", description: "Permission string: '755', '+x', 'a+r', etc." },
356
+ recursive: { type: "BOOLEAN", description: "Apply recursively" }
357
+ },
358
+ required: ["path", "permissions"]
359
+ }
164
360
  }
165
361
  ];
166
362
 
@@ -333,6 +529,232 @@ export async function executeTool(name, args = {}, { autoApprove = false } = {})
333
529
  return { result: stdout.trim() || "(empty response)" };
334
530
  }
335
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}` };
542
+ }
543
+
544
+ case "read_file_lines": {
545
+ const p = path.resolve(args.path);
546
+ if (!fs.existsSync(p)) return { error: `File not found: ${p}` };
547
+ const lines = fs.readFileSync(p, "utf8").split("\n");
548
+ const start = Math.max(1, args.start) - 1;
549
+ const end = Math.min(lines.length, args.end);
550
+ const slice = lines.slice(start, end);
551
+ const numbered = slice.map((l, i) =>
552
+ `${String(start + i + 1).padStart(4)} โ”‚ ${l}`
553
+ ).join("\n");
554
+ return { result: numbered, total_lines: lines.length };
555
+ }
556
+
557
+ case "tree": {
558
+ const p = path.resolve(args.path || ".");
559
+ const depth = args.depth ?? 3;
560
+ const showH = args.show_hidden ?? false;
561
+ const SKIP = new Set(["node_modules", ".git", "dist", "build"]);
562
+ const lines = [];
563
+
564
+ const walk = (dir, prefix, d) => {
565
+ if (d > depth) return;
566
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
567
+ .filter(e => showH || !e.name.startsWith("."))
568
+ .filter(e => !SKIP.has(e.name));
569
+ entries.forEach((e, i) => {
570
+ const last = i === entries.length - 1;
571
+ const branch = last ? "โ””โ”€โ”€ " : "โ”œโ”€โ”€ ";
572
+ const child = last ? " " : "โ”‚ ";
573
+ lines.push(prefix + branch + (e.isDirectory() ? e.name + "/" : e.name));
574
+ if (e.isDirectory()) walk(path.join(dir, e.name), prefix + child, d + 1);
575
+ });
576
+ };
577
+
578
+ lines.push(p);
579
+ walk(p, "", 0);
580
+ return { result: lines.join("\n") };
581
+ }
582
+
583
+ case "file_info": {
584
+ const p = path.resolve(args.path);
585
+ if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
586
+ const st = fs.statSync(p);
587
+ return {
588
+ result: JSON.stringify({
589
+ path: p,
590
+ type: st.isDirectory() ? "directory" : "file",
591
+ size: fmtBytes(st.size),
592
+ bytes: st.size,
593
+ mode: "0" + (st.mode & 0o777).toString(8),
594
+ modified: st.mtime.toISOString(),
595
+ created: st.birthtime.toISOString(),
596
+ }, null, 2)
597
+ };
598
+ }
599
+
600
+ case "diff_files": {
601
+ const a = path.resolve(args.file_a);
602
+ const b = path.resolve(args.file_b);
603
+ const { stdout } = await execAsync(`diff -u "${a}" "${b}"`).catch(e => ({ stdout: e.stdout || "" }));
604
+ return { result: stdout || "Files are identical." };
605
+ }
606
+
607
+ case "http_request": {
608
+ const method = (args.method ?? "GET").toUpperCase();
609
+ let hdrs = "";
610
+ if (args.headers) {
611
+ try {
612
+ const obj = JSON.parse(args.headers);
613
+ hdrs = Object.entries(obj).map(([k,v]) => `-H "${k}: ${v}"`).join(" ");
614
+ } catch {}
615
+ }
616
+ const bodyFlag = args.body ? `-d '${args.body.replace(/'/g, "'\\''")}'` : "";
617
+ const cmd = `curl -s -X ${method} ${hdrs} ${bodyFlag} --max-time 20 "${args.url}" | head -c 102400`;
618
+ const { stdout } = await execAsync(cmd);
619
+ return { result: stdout.trim() || "(empty response)" };
620
+ }
621
+
622
+ case "download_file": {
623
+ const dest = path.resolve(args.dest);
624
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
625
+ await execAsync(`curl -L --max-time 60 -o "${dest}" "${args.url}"`);
626
+ const size = fmtBytes(fs.statSync(dest).size);
627
+ return { result: `Downloaded to ${dest} (${size})` };
628
+ }
629
+
630
+ case "hash_file": {
631
+ const p = path.resolve(args.path);
632
+ if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
633
+ const alg = args.algorithm ?? "sha256";
634
+ const cmd = alg === "md5"
635
+ ? `md5sum "${p}" 2>/dev/null || md5 -q "${p}"`
636
+ : `sha${alg === "sha256" ? "256" : alg === "sha1" ? "1" : "256"}sum "${p}" 2>/dev/null || shasum -a ${alg === "sha1" ? "1" : "256"} "${p}"`;
637
+ const { stdout } = await execAsync(cmd);
638
+ return { result: stdout.trim().split(" ")[0], algorithm: alg, path: p };
639
+ }
640
+
641
+ case "base64": {
642
+ const action = args.action.toLowerCase();
643
+ let input = args.input;
644
+ if (args.is_file) {
645
+ const p = path.resolve(input);
646
+ if (!fs.existsSync(p)) return { error: `File not found: ${p}` };
647
+ input = fs.readFileSync(p, "utf8");
648
+ }
649
+ if (action === "encode") {
650
+ return { result: Buffer.from(input).toString("base64") };
651
+ } else {
652
+ return { result: Buffer.from(input, "base64").toString("utf8") };
653
+ }
654
+ }
655
+
656
+ case "json_query": {
657
+ let data;
658
+ if (args.is_file) {
659
+ const p = path.resolve(args.input);
660
+ if (!fs.existsSync(p)) return { error: `File not found: ${p}` };
661
+ data = JSON.parse(fs.readFileSync(p, "utf8"));
662
+ } else {
663
+ data = JSON.parse(args.input);
664
+ }
665
+ if (!args.query) return { result: JSON.stringify(data, null, 2) };
666
+ // Simple dot+bracket notation traversal
667
+ const keys = args.query.replace(/\[(\d+)\]/g, ".$1").split(".");
668
+ let val = data;
669
+ for (const k of keys) {
670
+ if (val == null) return { error: `Key "${k}" not found` };
671
+ val = val[k];
672
+ }
673
+ return { result: typeof val === "object" ? JSON.stringify(val, null, 2) : String(val) };
674
+ }
675
+
676
+ case "extract": {
677
+ const archive = path.resolve(args.archive);
678
+ if (!fs.existsSync(archive)) return { error: `Archive not found: ${archive}` };
679
+ const dest = path.resolve(args.dest ?? path.dirname(archive));
680
+ fs.mkdirSync(dest, { recursive: true });
681
+ const ext = archive.toLowerCase();
682
+ let cmd;
683
+ if (ext.endsWith(".zip")) cmd = `unzip -o "${archive}" -d "${dest}"`;
684
+ else if (ext.endsWith(".tar.gz") || ext.endsWith(".tgz")) cmd = `tar -xzf "${archive}" -C "${dest}"`;
685
+ else if (ext.endsWith(".tar.bz2")) cmd = `tar -xjf "${archive}" -C "${dest}"`;
686
+ else if (ext.endsWith(".tar")) cmd = `tar -xf "${archive}" -C "${dest}"`;
687
+ else if (ext.endsWith(".gz")) cmd = `gunzip -k "${archive}"`;
688
+ else return { error: `Unsupported archive format: ${archive}` };
689
+ const { stdout, stderr } = await execAsync(cmd);
690
+ return { result: `Extracted to ${dest}` };
691
+ }
692
+
693
+ case "compress": {
694
+ const source = path.resolve(args.source);
695
+ const dest = path.resolve(args.dest);
696
+ if (!fs.existsSync(source)) return { error: `Not found: ${source}` };
697
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
698
+ let cmd;
699
+ if (dest.endsWith(".zip")) cmd = `zip -r "${dest}" "${source}"`;
700
+ else if (dest.endsWith(".tar.gz") || dest.endsWith(".tgz")) cmd = `tar -czf "${dest}" "${source}"`;
701
+ else if (dest.endsWith(".tar")) cmd = `tar -cf "${dest}" "${source}"`;
702
+ else return { error: "Unsupported format. Use .zip or .tar.gz" };
703
+ await execAsync(cmd);
704
+ const size = fmtBytes(fs.statSync(dest).size);
705
+ return { result: `Compressed to ${dest} (${size})` };
706
+ }
707
+
708
+ case "git": {
709
+ const cwd = args.cwd ? path.resolve(args.cwd) : process.cwd();
710
+ const a = args.action.toLowerCase();
711
+ const extra = args.args ?? "";
712
+ const gitCmds = {
713
+ status: `git status`,
714
+ log: `git log --oneline -20 ${extra}`,
715
+ diff: `git diff ${extra}`,
716
+ add: `git add ${extra || "."}`,
717
+ commit: `git commit -m "${extra}"`,
718
+ push: `git push ${extra}`,
719
+ pull: `git pull ${extra}`,
720
+ clone: `git clone ${extra}`,
721
+ checkout: `git checkout ${extra}`,
722
+ branch: `git branch ${extra}`,
723
+ init: `git init ${extra}`,
724
+ stash: `git stash ${extra}`,
725
+ };
726
+ const cmd = gitCmds[a];
727
+ if (!cmd) return { error: `Unknown git action: ${a}` };
728
+ const { stdout, stderr } = await execAsync(cmd, { cwd }).catch(e => ({
729
+ stdout: e.stdout || "", stderr: e.stderr || ""
730
+ }));
731
+ return { result: (stdout + (stderr ? "\n" + stderr : "")).trim() || "(no output)" };
732
+ }
733
+
734
+ case "process_list": {
735
+ const filter = args.filter ?? "";
736
+ const cmd = filter
737
+ ? `ps aux | grep -i "${filter}" | grep -v grep`
738
+ : `ps aux | head -30`;
739
+ const { stdout } = await execAsync(cmd).catch(e => ({ stdout: e.stdout || "" }));
740
+ return { result: stdout.trim() || "No processes found." };
741
+ }
742
+
743
+ case "disk_usage": {
744
+ const p = args.path ? path.resolve(args.path) : process.cwd();
745
+ const { stdout: du } = await execAsync(`du -sh "${p}" 2>/dev/null`).catch(() => ({ stdout: "" }));
746
+ const { stdout: df } = await execAsync(`df -h "${p}" 2>/dev/null`).catch(() => ({ stdout: "" }));
747
+ return { result: [du.trim(), df.trim()].filter(Boolean).join("\n\n") || "(unavailable)" };
748
+ }
749
+
750
+ case "chmod": {
751
+ const p = path.resolve(args.path);
752
+ if (!fs.existsSync(p)) return { error: `Not found: ${p}` };
753
+ const rec = args.recursive ? "-R" : "";
754
+ await execAsync(`chmod ${rec} ${args.permissions} "${p}"`);
755
+ return { result: `chmod ${args.permissions} applied to ${p}` };
756
+ }
757
+
336
758
  default:
337
759
  return { error: `Unknown tool: ${name}` };
338
760
  }