Skip to content

Commit 7aa71fa

Browse files
committed
file write/edit/delete in filesystem toolkit
1 parent 16d49c4 commit 7aa71fa

File tree

9 files changed

+758
-0
lines changed

9 files changed

+758
-0
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace NeuronAI\Tools\Toolkits\FileSystem;
6+
7+
use NeuronAI\Tools\PropertyType;
8+
use NeuronAI\Tools\Tool;
9+
use NeuronAI\Tools\ToolProperty;
10+
11+
use function fclose;
12+
use function getcwd;
13+
use function is_dir;
14+
use function proc_close;
15+
use function proc_open;
16+
use function stream_get_contents;
17+
18+
/**
19+
* Execute a bash command and return its output.
20+
*/
21+
class BashTool extends Tool
22+
{
23+
public function __construct()
24+
{
25+
parent::__construct(
26+
name: 'bash',
27+
description: 'Execute a bash command and return its output. Use for running scripts, build tools, tests, linters, or any shell operation.',
28+
);
29+
}
30+
31+
protected function properties(): array
32+
{
33+
return [
34+
ToolProperty::make(
35+
name: 'command',
36+
type: PropertyType::STRING,
37+
description: 'The bash command to execute.',
38+
required: true,
39+
),
40+
ToolProperty::make(
41+
name: 'working_directory',
42+
type: PropertyType::STRING,
43+
description: 'The working directory to run the command in. Defaults to the current working directory.',
44+
required: false,
45+
),
46+
];
47+
}
48+
49+
public function __invoke(string $command, ?string $working_directory = null): array
50+
{
51+
$cwd = $working_directory ?? getcwd();
52+
53+
if ($working_directory !== null && !is_dir($working_directory)) {
54+
return [
55+
'status' => 'error',
56+
'operation' => 'bash',
57+
'command' => $command,
58+
'output' => '',
59+
'exit_code' => 1,
60+
'working_directory' => $working_directory,
61+
'message' => "Working directory '{$working_directory}' does not exist.",
62+
];
63+
}
64+
65+
$descriptors = [
66+
0 => ['pipe', 'r'],
67+
1 => ['pipe', 'w'],
68+
2 => ['pipe', 'w'],
69+
];
70+
71+
$process = proc_open($command, $descriptors, $pipes, $cwd);
72+
73+
if ($process === false) {
74+
return [
75+
'status' => 'error',
76+
'operation' => 'bash',
77+
'command' => $command,
78+
'output' => '',
79+
'exit_code' => 1,
80+
'working_directory' => $cwd,
81+
'message' => 'Failed to start process.',
82+
];
83+
}
84+
85+
fclose($pipes[0]);
86+
87+
$stdout = stream_get_contents($pipes[1]);
88+
$stderr = stream_get_contents($pipes[2]);
89+
fclose($pipes[1]);
90+
fclose($pipes[2]);
91+
92+
$exitCode = proc_close($process);
93+
94+
$output = $stdout !== false ? $stdout : '';
95+
if ($stderr !== false && $stderr !== '') {
96+
$output .= ($output !== '' ? "\n" : '') . $stderr;
97+
}
98+
99+
$status = $exitCode === 0 ? 'success' : 'error';
100+
$message = $exitCode === 0
101+
? 'Command executed successfully.'
102+
: "Command exited with code {$exitCode}.";
103+
104+
return [
105+
'status' => $status,
106+
'operation' => 'bash',
107+
'command' => $command,
108+
'output' => $output,
109+
'exit_code' => $exitCode,
110+
'working_directory' => $cwd,
111+
'message' => $message,
112+
];
113+
}
114+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace NeuronAI\Tools\Toolkits\FileSystem;
6+
7+
use NeuronAI\Tools\PropertyType;
8+
use NeuronAI\Tools\Tool;
9+
use NeuronAI\Tools\ToolProperty;
10+
11+
use function file_exists;
12+
use function is_file;
13+
use function unlink;
14+
15+
/**
16+
* Delete a file from the filesystem.
17+
*/
18+
class DeleteFileTool extends Tool
19+
{
20+
public function __construct()
21+
{
22+
parent::__construct(
23+
name: 'delete_file',
24+
description: 'Delete a file from the filesystem. This action is irreversible.',
25+
);
26+
}
27+
28+
protected function properties(): array
29+
{
30+
return [
31+
ToolProperty::make(
32+
name: 'file_path',
33+
type: PropertyType::STRING,
34+
description: 'Absolute or relative path to the file to delete.',
35+
required: true,
36+
),
37+
];
38+
}
39+
40+
public function __invoke(string $file_path): array
41+
{
42+
if (!file_exists($file_path)) {
43+
return [
44+
'status' => 'error',
45+
'operation' => 'delete_file',
46+
'file_path' => $file_path,
47+
'message' => "File '{$file_path}' does not exist.",
48+
];
49+
}
50+
51+
if (!is_file($file_path)) {
52+
return [
53+
'status' => 'error',
54+
'operation' => 'delete_file',
55+
'file_path' => $file_path,
56+
'message' => "'{$file_path}' is not a file. Directories cannot be deleted with this tool.",
57+
];
58+
}
59+
60+
if (!unlink($file_path)) {
61+
return [
62+
'status' => 'error',
63+
'operation' => 'delete_file',
64+
'file_path' => $file_path,
65+
'message' => "Failed to delete file '{$file_path}'.",
66+
];
67+
}
68+
69+
return [
70+
'status' => 'success',
71+
'operation' => 'delete_file',
72+
'file_path' => $file_path,
73+
'message' => "File '{$file_path}' deleted successfully.",
74+
];
75+
}
76+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace NeuronAI\Tools\Toolkits\FileSystem;
6+
7+
use NeuronAI\Tools\PropertyType;
8+
use NeuronAI\Tools\Tool;
9+
use NeuronAI\Tools\ToolProperty;
10+
11+
use function file_get_contents;
12+
use function file_put_contents;
13+
use function is_file;
14+
use function is_readable;
15+
use function is_writable;
16+
use function str_contains;
17+
use function str_replace;
18+
19+
/**
20+
* Edit a file by applying a search-and-replace operation.
21+
*/
22+
class EditFileTool extends Tool
23+
{
24+
public function __construct()
25+
{
26+
parent::__construct(
27+
name: 'edit_file',
28+
description: 'Edit a file by replacing an exact string or block of text with new content. The search string must match exactly (including whitespace and indentation). Use write_file if you need to replace the entire file.',
29+
);
30+
}
31+
32+
protected function properties(): array
33+
{
34+
return [
35+
ToolProperty::make(
36+
name: 'file_path',
37+
type: PropertyType::STRING,
38+
description: 'Absolute or relative path to the file to edit.',
39+
required: true,
40+
),
41+
ToolProperty::make(
42+
name: 'search',
43+
type: PropertyType::STRING,
44+
description: 'The exact text to search for in the file. Must match the file content precisely.',
45+
required: true,
46+
),
47+
ToolProperty::make(
48+
name: 'replace',
49+
type: PropertyType::STRING,
50+
description: 'The text to replace the matched content with.',
51+
required: true,
52+
),
53+
];
54+
}
55+
56+
public function __invoke(string $file_path, string $search, string $replace): array
57+
{
58+
if (!is_file($file_path)) {
59+
return [
60+
'status' => 'error',
61+
'operation' => 'edit_file',
62+
'file_path' => $file_path,
63+
'message' => "File '{$file_path}' does not exist.",
64+
];
65+
}
66+
67+
if (!is_readable($file_path)) {
68+
return [
69+
'status' => 'error',
70+
'operation' => 'edit_file',
71+
'file_path' => $file_path,
72+
'message' => "File '{$file_path}' is not readable.",
73+
];
74+
}
75+
76+
$current = file_get_contents($file_path);
77+
if ($current === false) {
78+
return [
79+
'status' => 'error',
80+
'operation' => 'edit_file',
81+
'file_path' => $file_path,
82+
'message' => "Failed to read file '{$file_path}'.",
83+
];
84+
}
85+
86+
if (!str_contains($current, $search)) {
87+
return [
88+
'status' => 'error',
89+
'operation' => 'edit_file',
90+
'file_path' => $file_path,
91+
'message' => "Search string not found in '{$file_path}'. Ensure the text matches exactly.",
92+
];
93+
}
94+
95+
if (!is_writable($file_path)) {
96+
return [
97+
'status' => 'error',
98+
'operation' => 'edit_file',
99+
'file_path' => $file_path,
100+
'message' => "File '{$file_path}' is not writable.",
101+
];
102+
}
103+
104+
$updated = str_replace($search, $replace, $current);
105+
$result = file_put_contents($file_path, $updated);
106+
107+
if ($result === false) {
108+
return [
109+
'status' => 'error',
110+
'operation' => 'edit_file',
111+
'file_path' => $file_path,
112+
'message' => "Failed to write changes to '{$file_path}'.",
113+
];
114+
}
115+
116+
return [
117+
'status' => 'success',
118+
'operation' => 'edit_file',
119+
'file_path' => $file_path,
120+
'message' => "File '{$file_path}' edited successfully.",
121+
];
122+
}
123+
}

src/Tools/Toolkits/FileSystem/FileSystemToolkit.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ public function provide(): array
2525
GlobPathTool::make(),
2626
PreviewFileTool::make(),
2727
ParseFileTool::make(),
28+
WriteFileTool::make(),
29+
DeleteFileTool::make(),
30+
EditFileTool::make(),
31+
BashTool::make(),
2832
];
2933
}
3034
}

0 commit comments

Comments
 (0)