deepagents-cli 0.0.3__py3-none-any.whl → 0.0.5__py3-none-any.whl

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.

Potentially problematic release.


This version of deepagents-cli might be problematic. Click here for more details.

Files changed (42) hide show
  1. deepagents_cli/__init__.py +5 -0
  2. deepagents_cli/__main__.py +6 -0
  3. deepagents_cli/agent.py +278 -0
  4. deepagents_cli/cli.py +13 -0
  5. deepagents_cli/commands.py +89 -0
  6. deepagents_cli/config.py +138 -0
  7. deepagents_cli/execution.py +644 -0
  8. deepagents_cli/file_ops.py +347 -0
  9. deepagents_cli/input.py +249 -0
  10. deepagents_cli/main.py +226 -0
  11. deepagents_cli/py.typed +0 -0
  12. deepagents_cli/token_utils.py +63 -0
  13. deepagents_cli/tools.py +140 -0
  14. deepagents_cli/ui.py +489 -0
  15. deepagents_cli-0.0.5.dist-info/METADATA +18 -0
  16. deepagents_cli-0.0.5.dist-info/RECORD +19 -0
  17. deepagents_cli-0.0.5.dist-info/entry_points.txt +3 -0
  18. deepagents_cli-0.0.5.dist-info/top_level.txt +1 -0
  19. deepagents/__init__.py +0 -7
  20. deepagents/cli.py +0 -567
  21. deepagents/default_agent_prompt.md +0 -64
  22. deepagents/graph.py +0 -144
  23. deepagents/memory/__init__.py +0 -17
  24. deepagents/memory/backends/__init__.py +0 -15
  25. deepagents/memory/backends/composite.py +0 -250
  26. deepagents/memory/backends/filesystem.py +0 -330
  27. deepagents/memory/backends/state.py +0 -206
  28. deepagents/memory/backends/store.py +0 -351
  29. deepagents/memory/backends/utils.py +0 -319
  30. deepagents/memory/protocol.py +0 -164
  31. deepagents/middleware/__init__.py +0 -13
  32. deepagents/middleware/agent_memory.py +0 -207
  33. deepagents/middleware/filesystem.py +0 -615
  34. deepagents/middleware/patch_tool_calls.py +0 -44
  35. deepagents/middleware/subagents.py +0 -481
  36. deepagents/pretty_cli.py +0 -289
  37. deepagents_cli-0.0.3.dist-info/METADATA +0 -551
  38. deepagents_cli-0.0.3.dist-info/RECORD +0 -24
  39. deepagents_cli-0.0.3.dist-info/entry_points.txt +0 -2
  40. deepagents_cli-0.0.3.dist-info/licenses/LICENSE +0 -21
  41. deepagents_cli-0.0.3.dist-info/top_level.txt +0 -1
  42. {deepagents_cli-0.0.3.dist-info → deepagents_cli-0.0.5.dist-info}/WHEEL +0 -0
@@ -1,330 +0,0 @@
1
- """FilesystemBackend: Read and write files directly from the filesystem."""
2
-
3
- import os
4
- import re
5
- from datetime import datetime
6
- from pathlib import Path
7
- from typing import Any, Optional, TYPE_CHECKING
8
- from langgraph.types import Command
9
-
10
- if TYPE_CHECKING:
11
- from langchain.tools import ToolRuntime
12
-
13
- from .utils import check_empty_content, format_content_with_line_numbers, perform_string_replacement
14
-
15
-
16
- class FilesystemBackend:
17
- """Backend that reads and writes files directly from the filesystem.
18
-
19
- Files are accessed using their actual filesystem paths. Relative paths are
20
- resolved relative to the current working directory. Content is read/written
21
- as plain text, and metadata (timestamps) are derived from filesystem stats.
22
- """
23
-
24
- def __init__(
25
- self,
26
- root_dir: Optional[str | Path] = None,
27
- virtual_mode: bool = False
28
- ) -> None:
29
- """Initialize filesystem backend.
30
-
31
- Args:
32
- root_dir: Optional root directory for file operations. If provided,
33
- all file paths will be resolved relative to this directory.
34
- If not provided, uses the current working directory.
35
- """
36
- self.cwd = Path(root_dir) if root_dir else Path.cwd()
37
- self.virtual_mode = virtual_mode
38
-
39
- def _resolve_path(self, key: str) -> Path:
40
- """Resolve a file path relative to cwd if not absolute.
41
-
42
- Args:
43
- key: File path (absolute or relative)
44
-
45
- Returns:
46
- Resolved absolute Path object
47
- """
48
- if self.virtual_mode:
49
- return self.cwd / key.lstrip('/')
50
- path = Path(key)
51
- if path.is_absolute():
52
- return path
53
- return self.cwd / path
54
-
55
- def ls(self, path: str) -> list[str]:
56
- """List files from filesystem.
57
-
58
- Args:
59
- path: Absolute directory path to list files from.
60
-
61
- Returns:
62
- List of absolute file paths.
63
- """
64
- dir_path = self._resolve_path(path)
65
- if not dir_path.exists() or not dir_path.is_dir():
66
- return []
67
-
68
- results: list[str] = []
69
-
70
- # Convert cwd to string for comparison
71
- cwd_str = str(self.cwd)
72
- if not cwd_str.endswith("/"):
73
- cwd_str += "/"
74
-
75
- # Walk the directory tree
76
- try:
77
- for path in dir_path.rglob("*"):
78
- if path.is_file():
79
- abs_path = str(path)
80
- if not self.virtual_mode:
81
- results.append(abs_path)
82
- continue
83
- # Strip the cwd prefix if present
84
- if abs_path.startswith(cwd_str):
85
- relative_path = abs_path[len(cwd_str):]
86
- elif abs_path.startswith(str(self.cwd)):
87
- # Handle case where cwd doesn't end with /
88
- relative_path = abs_path[len(str(self.cwd)):].lstrip("/")
89
- else:
90
- # Path is outside cwd, return as-is or skip
91
- relative_path = abs_path
92
-
93
- results.append("/" + relative_path)
94
- except (OSError, PermissionError):
95
- pass
96
-
97
- return sorted(results)
98
-
99
- def read(
100
- self,
101
- file_path: str,
102
- offset: int = 0,
103
- limit: int = 2000,
104
- ) -> str:
105
- """Read file content with line numbers.
106
-
107
- Args:
108
- file_path: Absolute or relative file path
109
- offset: Line offset to start reading from (0-indexed)
110
- limit: Maximum number of lines to readReturns:
111
- Formatted file content with line numbers, or error message.
112
- """
113
- resolved_path = self._resolve_path(file_path)
114
-
115
- if not resolved_path.exists() or not resolved_path.is_file():
116
- return f"Error: File '{file_path}' not found"
117
-
118
- try:
119
- with open(resolved_path, "r", encoding="utf-8") as f:
120
- content = f.read()
121
-
122
- empty_msg = check_empty_content(content)
123
- if empty_msg:
124
- return empty_msg
125
-
126
- lines = content.splitlines()
127
- start_idx = offset
128
- end_idx = min(start_idx + limit, len(lines))
129
-
130
- if start_idx >= len(lines):
131
- return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
132
-
133
- selected_lines = lines[start_idx:end_idx]
134
- return format_content_with_line_numbers(selected_lines, start_line=start_idx + 1)
135
- except (OSError, UnicodeDecodeError) as e:
136
- return f"Error reading file '{file_path}': {e}"
137
-
138
- def write(
139
- self,
140
- file_path: str,
141
- content: str,
142
- ) -> Command | str:
143
- """Create a new file with content.
144
-
145
- Args:
146
- file_path: Absolute or relative file path
147
- content: File content as a stringReturns:
148
- Success message or error if file already exists.
149
- """
150
- resolved_path = self._resolve_path(file_path)
151
-
152
- if resolved_path.exists():
153
- return f"Cannot write to {file_path} because it already exists. Read and then make an edit, or write to a new path."
154
-
155
- try:
156
- # Create parent directories if needed
157
- resolved_path.parent.mkdir(parents=True, exist_ok=True)
158
-
159
- with open(resolved_path, "w", encoding="utf-8") as f:
160
- f.write(content)
161
-
162
- return f"Updated file {file_path}"
163
- except (OSError, UnicodeEncodeError) as e:
164
- return f"Error writing file '{file_path}': {e}"
165
-
166
- def edit(
167
- self,
168
- file_path: str,
169
- old_string: str,
170
- new_string: str,
171
- replace_all: bool = False,
172
- ) -> Command | str:
173
- """Edit a file by replacing string occurrences.
174
-
175
- Args:
176
- file_path: Absolute or relative file path
177
- old_string: String to find and replace
178
- new_string: Replacement string
179
- replace_all: If True, replace all occurrencesReturns:
180
- Success message or error message on failure.
181
- """
182
- resolved_path = self._resolve_path(file_path)
183
-
184
- if not resolved_path.exists() or not resolved_path.is_file():
185
- return f"Error: File '{file_path}' not found"
186
-
187
- try:
188
- with open(resolved_path, "r", encoding="utf-8") as f:
189
- content = f.read()
190
-
191
- result = perform_string_replacement(content, old_string, new_string, replace_all)
192
-
193
- if isinstance(result, str):
194
- return result
195
-
196
- new_content, occurrences = result
197
-
198
- with open(resolved_path, "w", encoding="utf-8") as f:
199
- f.write(new_content)
200
-
201
- return f"Successfully replaced {occurrences} instance(s) of the string in '{file_path}'"
202
- except (OSError, UnicodeDecodeError, UnicodeEncodeError) as e:
203
- return f"Error editing file '{file_path}': {e}"
204
-
205
- def delete(self, file_path: str) -> Command | None:
206
- """Delete file from filesystem.
207
-
208
- Args:
209
- file_path: File path to delete (absolute or relative to cwd)Returns:
210
- None (direct filesystem modification)
211
- """
212
- resolved_path = self._resolve_path(file_path)
213
-
214
- if resolved_path.exists() and resolved_path.is_file():
215
- resolved_path.unlink()
216
-
217
- return None
218
-
219
- def grep(
220
- self,
221
- pattern: str,
222
- path: str = "/",
223
- glob: Optional[str] = None,
224
- output_mode: str = "files_with_matches",
225
- ) -> str:
226
- """Search for a pattern in files.
227
-
228
- Args:
229
- pattern: String pattern to search for
230
- path: Path to search in (default "/")
231
- glob: Optional glob pattern to filter files (e.g., "*.py")
232
- output_mode: Output format - "files_with_matches", "content", or "count"Returns:
233
- Formatted search results based on output_mode.
234
- """
235
- regex = re.compile(re.escape(pattern))
236
-
237
- if glob:
238
- files_to_search = self.glob(glob)
239
- else:
240
- files_to_search = self.ls(path if path != "/" else None)
241
-
242
- if path != "/":
243
- files_to_search = [f for f in files_to_search if f.startswith(path)]
244
-
245
- file_matches = {}
246
-
247
- for fp in files_to_search:
248
- resolved_path = self._resolve_path(fp)
249
-
250
- if not resolved_path.exists() or not resolved_path.is_file():
251
- continue
252
-
253
- try:
254
- with open(resolved_path, "r", encoding="utf-8") as f:
255
- lines = f.readlines()
256
-
257
- matches = []
258
- for line_num, line in enumerate(lines, start=1):
259
- if regex.search(line):
260
- matches.append((line_num, line.rstrip()))
261
-
262
- if matches:
263
- file_matches[fp] = matches
264
- except (OSError, UnicodeDecodeError):
265
- continue
266
-
267
- if not file_matches:
268
- return f"No matches found for pattern: '{pattern}'"
269
-
270
- if output_mode == "files_with_matches":
271
- return "\n".join(sorted(file_matches.keys()))
272
- elif output_mode == "count":
273
- results = []
274
- for fp in sorted(file_matches.keys()):
275
- count = len(file_matches[fp])
276
- results.append(f"{fp}: {count}")
277
- return "\n".join(results)
278
- else:
279
- results = []
280
- for fp in sorted(file_matches.keys()):
281
- results.append(f"{fp}:")
282
- for line_num, line in file_matches[fp]:
283
- results.append(f" {line_num}: {line}")
284
- return "\n".join(results)
285
-
286
- def glob(self, pattern: str, path: str = "/") -> list[str]:
287
- """Find files matching a glob pattern.
288
-
289
- Args:
290
- pattern: Glob pattern (e.g., "**/*.py", "*.txt", "/subdir/**/*.md")
291
- path: Base path to search from (default "/")Returns:
292
- List of absolute file paths matching the pattern.
293
- """
294
- if pattern.startswith("/"):
295
- pattern = pattern.lstrip("/")
296
-
297
- if path == "/":
298
- search_path = self.cwd
299
- else:
300
- search_path = self._resolve_path(path)
301
-
302
- if not search_path.exists() or not search_path.is_dir():
303
- return []
304
-
305
- results = []
306
-
307
- try:
308
- for matched_path in search_path.glob(pattern):
309
- if matched_path.is_file():
310
- abs_path = str(matched_path)
311
- if not self.virtual_mode:
312
- results.append(abs_path)
313
- continue
314
-
315
- cwd_str = str(self.cwd)
316
- if not cwd_str.endswith("/"):
317
- cwd_str += "/"
318
-
319
- if abs_path.startswith(cwd_str):
320
- relative_path = abs_path[len(cwd_str):]
321
- elif abs_path.startswith(str(self.cwd)):
322
- relative_path = abs_path[len(str(self.cwd)):].lstrip("/")
323
- else:
324
- relative_path = abs_path
325
-
326
- results.append("/" + relative_path)
327
- except (OSError, ValueError):
328
- pass
329
-
330
- return sorted(results)
@@ -1,206 +0,0 @@
1
- """StateBackend: Store files in LangGraph agent state (ephemeral)."""
2
-
3
- import re
4
- from typing import Any, Literal, Optional, TYPE_CHECKING
5
-
6
- if TYPE_CHECKING:
7
- from langchain.tools import ToolRuntime
8
-
9
- from langchain_core.messages import ToolMessage
10
- from langgraph.types import Command
11
-
12
- from .utils import (
13
- create_file_data,
14
- update_file_data,
15
- file_data_to_string,
16
- format_read_response,
17
- perform_string_replacement,
18
- _glob_search_files,
19
- _grep_search_files,
20
- )
21
-
22
-
23
- class StateBackend:
24
- """Backend that stores files in agent state (ephemeral).
25
-
26
- Uses LangGraph's state management and checkpointing. Files persist within
27
- a conversation thread but not across threads. State is automatically
28
- checkpointed after each agent step.
29
-
30
- Special handling: Since LangGraph state must be updated via Command objects
31
- (not direct mutation), operations return Command objects instead of None.
32
- This is indicated by the uses_state=True flag.
33
- """
34
-
35
- def __init__(self, runtime: "ToolRuntime"):
36
- """Initialize StateBackend with runtime.
37
-
38
- Args:"""
39
- self.runtime = runtime
40
-
41
- def ls(self, path: str) -> list[str]:
42
- """List files from state.
43
-
44
- Args:
45
- path: Absolute path to directory.
46
-
47
- Returns:
48
- List of file paths.
49
- """
50
- files = self.runtime.state.get("files", {})
51
- keys = list(files.keys())
52
- keys = [k for k in keys if k.startswith(path)]
53
- return keys
54
-
55
- def read(
56
- self,
57
- file_path: str,
58
- offset: int = 0,
59
- limit: int = 2000,
60
- ) -> str:
61
- """Read file content with line numbers.
62
-
63
- Args:
64
- file_path: Absolute file path
65
- offset: Line offset to start reading from (0-indexed)
66
- limit: Maximum number of lines to readReturns:
67
- Formatted file content with line numbers, or error message.
68
- """
69
- files = self.runtime.state.get("files", {})
70
- file_data = files.get(file_path)
71
-
72
- if file_data is None:
73
- return f"Error: File '{file_path}' not found"
74
-
75
- return format_read_response(file_data, offset, limit)
76
-
77
- def write(
78
- self,
79
- file_path: str,
80
- content: str,
81
- ) -> Command | str:
82
- """Create a new file with content.
83
-
84
- Args:
85
- file_path: Absolute file path
86
- content: File content as a stringReturns:
87
- Command object to update state, or error message if file exists.
88
- """
89
- files = self.runtime.state.get("files", {})
90
-
91
- if file_path in files:
92
- return f"Cannot write to {file_path} because it already exists. Read and then make an edit, or write to a new path."
93
-
94
- new_file_data = create_file_data(content)
95
- tool_call_id = self.runtime.tool_call_id
96
-
97
- return Command(
98
- update={
99
- "files": {file_path: new_file_data},
100
- "messages": [
101
- ToolMessage(
102
- content=f"Updated file {file_path}",
103
- tool_call_id=tool_call_id,
104
- )
105
- ],
106
- }
107
- )
108
-
109
- def edit(
110
- self,
111
- file_path: str,
112
- old_string: str,
113
- new_string: str,
114
- replace_all: bool = False,
115
- ) -> Command | str:
116
- """Edit a file by replacing string occurrences.
117
-
118
- Args:
119
- file_path: Absolute file path
120
- old_string: String to find and replace
121
- new_string: Replacement string
122
- replace_all: If True, replace all occurrencesReturns:
123
- Command object to update state, or error message on failure.
124
- """
125
- files = self.runtime.state.get("files", {})
126
- file_data = files.get(file_path)
127
-
128
- if file_data is None:
129
- return f"Error: File '{file_path}' not found"
130
-
131
- content = file_data_to_string(file_data)
132
- result = perform_string_replacement(content, old_string, new_string, replace_all)
133
-
134
- if isinstance(result, str):
135
- return result
136
-
137
- new_content, occurrences = result
138
- new_file_data = update_file_data(file_data, new_content)
139
- tool_call_id = self.runtime.tool_call_id
140
-
141
- return Command(
142
- update={
143
- "files": {file_path: new_file_data},
144
- "messages": [
145
- ToolMessage(
146
- content=f"Successfully replaced {occurrences} instance(s) of the string in '{file_path}'",
147
- tool_call_id=tool_call_id,
148
- )
149
- ],
150
- }
151
- )
152
-
153
- def delete(self, file_path: str) -> Command | None:
154
- """Delete file from state via Command.
155
-
156
- Args:
157
- file_path: File path to deleteReturns:
158
- Command object to update state (sets file to None for deletion).
159
- """
160
- tool_call_id = self.runtime.tool_call_id
161
- return Command(
162
- update={
163
- "files": {file_path: None},
164
- "messages": [
165
- ToolMessage(
166
- content=f"Deleted file {file_path}",
167
- tool_call_id=tool_call_id,
168
- )
169
- ],
170
- }
171
- )
172
-
173
- def grep(
174
- self,
175
- pattern: str,
176
- path: str = "/",
177
- glob: Optional[str] = None,
178
- output_mode: Literal["files_with_matches", "content", "count"] = "files_with_matches",
179
- ) -> str:
180
- """Search for a pattern in files.
181
-
182
- Args:
183
- pattern: String pattern to search for
184
- path: Path to search in (default "/")
185
- glob: Optional glob pattern to filter files (e.g., "*.py")
186
- output_mode: Output format - "files_with_matches", "content", or "count"Returns:
187
- Formatted search results based on output_mode.
188
- """
189
- files = self.runtime.state.get("files", {})
190
-
191
- return _grep_search_files(files, pattern, path, glob, output_mode)
192
-
193
- def glob(self, pattern: str, path: str = "/") -> list[str]:
194
- """Find files matching a glob pattern.
195
-
196
- Args:
197
- pattern: Glob pattern (e.g., "**/*.py", "*.txt", "/subdir/**/*.md")
198
- path: Base path to search from (default "/")Returns:
199
- List of absolute file paths matching the pattern.
200
- """
201
- files = self.runtime.state.get("files", {})
202
-
203
- result = _glob_search_files(files, pattern, path)
204
- if result == "No files found":
205
- return []
206
- return result.split("\n")