@ikyyofc/gemini-cli 3.0.8 → 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/package.json +1 -1
- package/src/tools.js +422 -0
package/package.json
CHANGED
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
|
}
|