deepagents 0.2.3__tar.gz → 0.2.5__tar.gz

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.
Files changed (46) hide show
  1. {deepagents-0.2.3/src/deepagents.egg-info → deepagents-0.2.5}/PKG-INFO +7 -7
  2. {deepagents-0.2.3/src → deepagents-0.2.5/libs}/deepagents/backends/composite.py +32 -42
  3. {deepagents-0.2.3/src → deepagents-0.2.5/libs}/deepagents/backends/filesystem.py +92 -86
  4. {deepagents-0.2.3/src → deepagents-0.2.5/libs}/deepagents/backends/protocol.py +39 -13
  5. {deepagents-0.2.3/src → deepagents-0.2.5/libs}/deepagents/backends/state.py +59 -58
  6. {deepagents-0.2.3/src → deepagents-0.2.5/libs}/deepagents/backends/store.py +74 -67
  7. {deepagents-0.2.3/src → deepagents-0.2.5/libs}/deepagents/backends/utils.py +7 -21
  8. {deepagents-0.2.3/src → deepagents-0.2.5/libs}/deepagents/graph.py +1 -1
  9. {deepagents-0.2.3/src → deepagents-0.2.5/libs}/deepagents/middleware/filesystem.py +49 -47
  10. {deepagents-0.2.3/src → deepagents-0.2.5/libs}/deepagents/middleware/resumable_shell.py +5 -4
  11. {deepagents-0.2.3/src → deepagents-0.2.5/libs}/deepagents/middleware/subagents.py +1 -2
  12. deepagents-0.2.5/libs/deepagents-cli/README.md +3 -0
  13. deepagents-0.2.5/libs/deepagents-cli/deepagents_cli/README.md +196 -0
  14. deepagents-0.2.5/libs/deepagents-cli/deepagents_cli/__init__.py +5 -0
  15. deepagents-0.2.5/libs/deepagents-cli/deepagents_cli/__main__.py +6 -0
  16. deepagents-0.2.5/libs/deepagents-cli/deepagents_cli/agent.py +278 -0
  17. {deepagents-0.2.3/src/deepagents/middleware → deepagents-0.2.5/libs/deepagents-cli/deepagents_cli}/agent_memory.py +16 -12
  18. deepagents-0.2.5/libs/deepagents-cli/deepagents_cli/commands.py +89 -0
  19. deepagents-0.2.5/libs/deepagents-cli/deepagents_cli/config.py +118 -0
  20. deepagents-0.2.5/libs/deepagents-cli/deepagents_cli/execution.py +636 -0
  21. deepagents-0.2.5/libs/deepagents-cli/deepagents_cli/file_ops.py +347 -0
  22. deepagents-0.2.5/libs/deepagents-cli/deepagents_cli/input.py +270 -0
  23. deepagents-0.2.5/libs/deepagents-cli/deepagents_cli/main.py +226 -0
  24. deepagents-0.2.5/libs/deepagents-cli/deepagents_cli/py.typed +0 -0
  25. deepagents-0.2.5/libs/deepagents-cli/deepagents_cli/token_utils.py +63 -0
  26. deepagents-0.2.5/libs/deepagents-cli/deepagents_cli/tools.py +140 -0
  27. deepagents-0.2.5/libs/deepagents-cli/deepagents_cli/ui.py +489 -0
  28. deepagents-0.2.5/libs/deepagents-cli/tests/test_file_ops.py +119 -0
  29. deepagents-0.2.5/libs/deepagents-cli/tests/test_placeholder.py +5 -0
  30. {deepagents-0.2.3 → deepagents-0.2.5/libs/deepagents.egg-info}/PKG-INFO +7 -7
  31. deepagents-0.2.5/libs/deepagents.egg-info/SOURCES.txt +41 -0
  32. {deepagents-0.2.3/src → deepagents-0.2.5/libs}/deepagents.egg-info/requires.txt +0 -7
  33. deepagents-0.2.5/libs/deepagents.egg-info/top_level.txt +2 -0
  34. {deepagents-0.2.3 → deepagents-0.2.5}/pyproject.toml +24 -15
  35. deepagents-0.2.3/src/deepagents.egg-info/SOURCES.txt +0 -25
  36. deepagents-0.2.3/src/deepagents.egg-info/top_level.txt +0 -1
  37. deepagents-0.2.3/tests/test_middleware.py +0 -1134
  38. {deepagents-0.2.3 → deepagents-0.2.5}/LICENSE +0 -0
  39. {deepagents-0.2.3 → deepagents-0.2.5}/README.md +0 -0
  40. {deepagents-0.2.3/src → deepagents-0.2.5/libs}/deepagents/__init__.py +0 -0
  41. {deepagents-0.2.3/src → deepagents-0.2.5/libs}/deepagents/backends/__init__.py +1 -1
  42. {deepagents-0.2.3/src → deepagents-0.2.5/libs}/deepagents/middleware/__init__.py +0 -0
  43. {deepagents-0.2.3/src → deepagents-0.2.5/libs}/deepagents/middleware/patch_tool_calls.py +0 -0
  44. {deepagents-0.2.3/src/deepagents → deepagents-0.2.5/libs/deepagents-cli/deepagents_cli}/default_agent_prompt.md +0 -0
  45. {deepagents-0.2.3/src → deepagents-0.2.5/libs}/deepagents.egg-info/dependency_links.txt +0 -0
  46. {deepagents-0.2.3 → deepagents-0.2.5}/setup.cfg +0 -0
@@ -1,8 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagents
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: General purpose 'deep agent' with sub-agent spawning, todo list capabilities, and mock file system. Built on LangGraph.
5
5
  License: MIT
6
+ Project-URL: Homepage, https://docs.langchain.com/oss/python/deepagents/overview
7
+ Project-URL: Documentation, https://reference.langchain.com/python/deepagents/
8
+ Project-URL: Source, https://github.com/langchain-ai/deepagents
9
+ Project-URL: Twitter, https://x.com/LangChainAI
10
+ Project-URL: Slack, https://www.langchain.com/join-community
11
+ Project-URL: Reddit, https://www.reddit.com/r/LangChain/
6
12
  Requires-Python: <4.0,>=3.11
7
13
  Description-Content-Type: text/markdown
8
14
  License-File: LICENSE
@@ -10,12 +16,6 @@ Requires-Dist: langchain-anthropic<2.0.0,>=1.0.0
10
16
  Requires-Dist: langchain<2.0.0,>=1.0.2
11
17
  Requires-Dist: langchain-core<2.0.0,>=1.0.0
12
18
  Requires-Dist: wcmatch
13
- Provides-Extra: dev
14
- Requires-Dist: pytest; extra == "dev"
15
- Requires-Dist: pytest-cov; extra == "dev"
16
- Requires-Dist: build; extra == "dev"
17
- Requires-Dist: twine; extra == "dev"
18
- Requires-Dist: langchain-openai; extra == "dev"
19
19
  Dynamic: license-file
20
20
 
21
21
  # 🧠🤖Deep Agents
@@ -1,14 +1,11 @@
1
1
  """CompositeBackend: Route operations to different backends based on path prefix."""
2
2
 
3
- from typing import Optional
4
-
5
- from deepagents.backends.protocol import BackendProtocol, WriteResult, EditResult
3
+ from deepagents.backends.protocol import BackendProtocol, EditResult, WriteResult
6
4
  from deepagents.backends.state import StateBackend
7
5
  from deepagents.backends.utils import FileInfo, GrepMatch
8
6
 
9
7
 
10
8
  class CompositeBackend:
11
-
12
9
  def __init__(
13
10
  self,
14
11
  default: BackendProtocol | StateBackend,
@@ -19,16 +16,16 @@ class CompositeBackend:
19
16
 
20
17
  # Virtual routes
21
18
  self.routes = routes
22
-
19
+
23
20
  # Sort routes by length (longest first) for correct prefix matching
24
21
  self.sorted_routes = sorted(routes.items(), key=lambda x: len(x[0]), reverse=True)
25
22
 
26
23
  def _get_backend_and_key(self, key: str) -> tuple[BackendProtocol, str]:
27
24
  """Determine which backend handles this key and strip prefix.
28
-
25
+
29
26
  Args:
30
27
  key: Original file path
31
-
28
+
32
29
  Returns:
33
30
  Tuple of (backend, stripped_key) where stripped_key has the route
34
31
  prefix removed (but keeps leading slash).
@@ -38,12 +35,12 @@ class CompositeBackend:
38
35
  if key.startswith(prefix):
39
36
  # Strip full prefix and ensure a leading slash remains
40
37
  # e.g., "/memories/notes.txt" → "/notes.txt"; "/memories/" → "/"
41
- suffix = key[len(prefix):]
38
+ suffix = key[len(prefix) :]
42
39
  stripped_key = f"/{suffix}" if suffix else "/"
43
40
  return backend, stripped_key
44
-
41
+
45
42
  return self.default, key
46
-
43
+
47
44
  def ls_info(self, path: str) -> list[FileInfo]:
48
45
  """List files and directories in the specified directory (non-recursive).
49
46
 
@@ -58,7 +55,7 @@ class CompositeBackend:
58
55
  for route_prefix, backend in self.sorted_routes:
59
56
  if path.startswith(route_prefix.rstrip("/")):
60
57
  # Query only the matching routed backend
61
- suffix = path[len(route_prefix):]
58
+ suffix = path[len(route_prefix) :]
62
59
  search_path = f"/{suffix}" if suffix else "/"
63
60
  infos = backend.ls_info(search_path)
64
61
  prefixed: list[FileInfo] = []
@@ -74,12 +71,14 @@ class CompositeBackend:
74
71
  results.extend(self.default.ls_info(path))
75
72
  for route_prefix, backend in self.sorted_routes:
76
73
  # Add the route itself as a directory (e.g., /memories/)
77
- results.append({
78
- "path": route_prefix,
79
- "is_dir": True,
80
- "size": 0,
81
- "modified_at": "",
82
- })
74
+ results.append(
75
+ {
76
+ "path": route_prefix,
77
+ "is_dir": True,
78
+ "size": 0,
79
+ "modified_at": "",
80
+ }
81
+ )
83
82
 
84
83
  results.sort(key=lambda x: x.get("path", ""))
85
84
  return results
@@ -87,15 +86,14 @@ class CompositeBackend:
87
86
  # Path doesn't match a route: query only default backend
88
87
  return self.default.ls_info(path)
89
88
 
90
-
91
89
  def read(
92
- self,
90
+ self,
93
91
  file_path: str,
94
92
  offset: int = 0,
95
93
  limit: int = 2000,
96
94
  ) -> str:
97
95
  """Read file content, routing to appropriate backend.
98
-
96
+
99
97
  Args:
100
98
  file_path: Absolute file path
101
99
  offset: Line offset to start reading from (0-indexed)
@@ -105,17 +103,16 @@ class CompositeBackend:
105
103
  backend, stripped_key = self._get_backend_and_key(file_path)
106
104
  return backend.read(stripped_key, offset=offset, limit=limit)
107
105
 
108
-
109
106
  def grep_raw(
110
107
  self,
111
108
  pattern: str,
112
- path: Optional[str] = None,
113
- glob: Optional[str] = None,
109
+ path: str | None = None,
110
+ glob: str | None = None,
114
111
  ) -> list[GrepMatch] | str:
115
112
  # If path targets a specific route, search only that backend
116
113
  for route_prefix, backend in self.sorted_routes:
117
114
  if path is not None and path.startswith(route_prefix.rstrip("/")):
118
- search_path = path[len(route_prefix) - 1:]
115
+ search_path = path[len(route_prefix) - 1 :]
119
116
  raw = backend.grep_raw(pattern, search_path if search_path else "/", glob)
120
117
  if isinstance(raw, str):
121
118
  return raw
@@ -137,19 +134,16 @@ class CompositeBackend:
137
134
  all_matches.extend({**m, "path": f"{route_prefix[:-1]}{m['path']}"} for m in raw)
138
135
 
139
136
  return all_matches
140
-
137
+
141
138
  def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
142
139
  results: list[FileInfo] = []
143
140
 
144
141
  # Route based on path, not pattern
145
142
  for route_prefix, backend in self.sorted_routes:
146
143
  if path.startswith(route_prefix.rstrip("/")):
147
- search_path = path[len(route_prefix) - 1:]
144
+ search_path = path[len(route_prefix) - 1 :]
148
145
  infos = backend.glob_info(pattern, search_path if search_path else "/")
149
- return [
150
- {**fi, "path": f"{route_prefix[:-1]}{fi['path']}"}
151
- for fi in infos
152
- ]
146
+ return [{**fi, "path": f"{route_prefix[:-1]}{fi['path']}"} for fi in infos]
153
147
 
154
148
  # Path doesn't match any specific route - search default backend AND all routed backends
155
149
  results.extend(self.default.glob_info(pattern, path))
@@ -162,11 +156,10 @@ class CompositeBackend:
162
156
  results.sort(key=lambda x: x.get("path", ""))
163
157
  return results
164
158
 
165
-
166
159
  def write(
167
- self,
168
- file_path: str,
169
- content: str,
160
+ self,
161
+ file_path: str,
162
+ content: str,
170
163
  ) -> WriteResult:
171
164
  """Create a new file, routing to appropriate backend.
172
165
 
@@ -191,11 +184,11 @@ class CompositeBackend:
191
184
  return res
192
185
 
193
186
  def edit(
194
- self,
195
- file_path: str,
196
- old_string: str,
197
- new_string: str,
198
- replace_all: bool = False,
187
+ self,
188
+ file_path: str,
189
+ old_string: str,
190
+ new_string: str,
191
+ replace_all: bool = False,
199
192
  ) -> EditResult:
200
193
  """Edit a file, routing to appropriate backend.
201
194
 
@@ -219,6 +212,3 @@ class CompositeBackend:
219
212
  except Exception:
220
213
  pass
221
214
  return res
222
-
223
-
224
-
@@ -7,26 +7,24 @@ Security and search upgrades:
7
7
  and optional glob include filtering, while preserving virtual path behavior
8
8
  """
9
9
 
10
+ import json
10
11
  import os
11
12
  import re
12
- import json
13
13
  import subprocess
14
14
  from datetime import datetime
15
15
  from pathlib import Path
16
- from typing import Optional
17
16
 
18
- from .utils import (
17
+ import wcmatch.glob as wcglob
18
+
19
+ from deepagents.backends.protocol import BackendProtocol, EditResult, FileInfo, GrepMatch, WriteResult
20
+ from deepagents.backends.utils import (
19
21
  check_empty_content,
20
22
  format_content_with_line_numbers,
21
23
  perform_string_replacement,
22
24
  )
23
- import wcmatch.glob as wcglob
24
- from deepagents.backends.utils import FileInfo, GrepMatch
25
- from deepagents.backends.protocol import WriteResult, EditResult
26
-
27
25
 
28
26
 
29
- class FilesystemBackend:
27
+ class FilesystemBackend(BackendProtocol):
30
28
  """Backend that reads and writes files directly from the filesystem.
31
29
 
32
30
  Files are accessed using their actual filesystem paths. Relative paths are
@@ -35,13 +33,13 @@ class FilesystemBackend:
35
33
  """
36
34
 
37
35
  def __init__(
38
- self,
39
- root_dir: Optional[str | Path] = None,
36
+ self,
37
+ root_dir: str | Path | None = None,
40
38
  virtual_mode: bool = False,
41
39
  max_file_size_mb: int = 10,
42
40
  ) -> None:
43
41
  """Initialize filesystem backend.
44
-
42
+
45
43
  Args:
46
44
  root_dir: Optional root directory for file operations. If provided,
47
45
  all file paths will be resolved relative to this directory.
@@ -118,32 +116,36 @@ class FilesystemBackend:
118
116
  if is_file:
119
117
  try:
120
118
  st = child_path.stat()
121
- results.append({
122
- "path": abs_path,
123
- "is_dir": False,
124
- "size": int(st.st_size),
125
- "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
126
- })
119
+ results.append(
120
+ {
121
+ "path": abs_path,
122
+ "is_dir": False,
123
+ "size": int(st.st_size),
124
+ "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
125
+ }
126
+ )
127
127
  except OSError:
128
128
  results.append({"path": abs_path, "is_dir": False})
129
129
  elif is_dir:
130
130
  try:
131
131
  st = child_path.stat()
132
- results.append({
133
- "path": abs_path + "/",
134
- "is_dir": True,
135
- "size": 0,
136
- "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
137
- })
132
+ results.append(
133
+ {
134
+ "path": abs_path + "/",
135
+ "is_dir": True,
136
+ "size": 0,
137
+ "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
138
+ }
139
+ )
138
140
  except OSError:
139
141
  results.append({"path": abs_path + "/", "is_dir": True})
140
142
  else:
141
143
  # Virtual mode: strip cwd prefix
142
144
  if abs_path.startswith(cwd_str):
143
- relative_path = abs_path[len(cwd_str):]
145
+ relative_path = abs_path[len(cwd_str) :]
144
146
  elif abs_path.startswith(str(self.cwd)):
145
147
  # Handle case where cwd doesn't end with /
146
- relative_path = abs_path[len(str(self.cwd)):].lstrip("/")
148
+ relative_path = abs_path[len(str(self.cwd)) :].lstrip("/")
147
149
  else:
148
150
  # Path is outside cwd, return as-is or skip
149
151
  relative_path = abs_path
@@ -153,23 +155,27 @@ class FilesystemBackend:
153
155
  if is_file:
154
156
  try:
155
157
  st = child_path.stat()
156
- results.append({
157
- "path": virt_path,
158
- "is_dir": False,
159
- "size": int(st.st_size),
160
- "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
161
- })
158
+ results.append(
159
+ {
160
+ "path": virt_path,
161
+ "is_dir": False,
162
+ "size": int(st.st_size),
163
+ "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
164
+ }
165
+ )
162
166
  except OSError:
163
167
  results.append({"path": virt_path, "is_dir": False})
164
168
  elif is_dir:
165
169
  try:
166
170
  st = child_path.stat()
167
- results.append({
168
- "path": virt_path + "/",
169
- "is_dir": True,
170
- "size": 0,
171
- "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
172
- })
171
+ results.append(
172
+ {
173
+ "path": virt_path + "/",
174
+ "is_dir": True,
175
+ "size": 0,
176
+ "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
177
+ }
178
+ )
173
179
  except OSError:
174
180
  results.append({"path": virt_path + "/", "is_dir": True})
175
181
  except (OSError, PermissionError):
@@ -180,15 +186,15 @@ class FilesystemBackend:
180
186
  return results
181
187
 
182
188
  # Removed legacy ls() convenience to keep lean surface
183
-
189
+
184
190
  def read(
185
- self,
191
+ self,
186
192
  file_path: str,
187
193
  offset: int = 0,
188
194
  limit: int = 2000,
189
195
  ) -> str:
190
196
  """Read file content with line numbers.
191
-
197
+
192
198
  Args:
193
199
  file_path: Absolute or relative file path
194
200
  offset: Line offset to start reading from (0-indexed)
@@ -196,10 +202,10 @@ class FilesystemBackend:
196
202
  Formatted file content with line numbers, or error message.
197
203
  """
198
204
  resolved_path = self._resolve_path(file_path)
199
-
205
+
200
206
  if not resolved_path.exists() or not resolved_path.is_file():
201
207
  return f"Error: File '{file_path}' not found"
202
-
208
+
203
209
  try:
204
210
  # Open with O_NOFOLLOW where available to avoid symlink traversal
205
211
  try:
@@ -208,27 +214,27 @@ class FilesystemBackend:
208
214
  content = f.read()
209
215
  except OSError:
210
216
  # Fallback to normal open if O_NOFOLLOW unsupported or fails
211
- with open(resolved_path, "r", encoding="utf-8") as f:
217
+ with open(resolved_path, encoding="utf-8") as f:
212
218
  content = f.read()
213
-
219
+
214
220
  empty_msg = check_empty_content(content)
215
221
  if empty_msg:
216
222
  return empty_msg
217
-
223
+
218
224
  lines = content.splitlines()
219
225
  start_idx = offset
220
226
  end_idx = min(start_idx + limit, len(lines))
221
-
227
+
222
228
  if start_idx >= len(lines):
223
229
  return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
224
-
230
+
225
231
  selected_lines = lines[start_idx:end_idx]
226
232
  return format_content_with_line_numbers(selected_lines, start_line=start_idx + 1)
227
233
  except (OSError, UnicodeDecodeError) as e:
228
234
  return f"Error reading file '{file_path}': {e}"
229
-
235
+
230
236
  def write(
231
- self,
237
+ self,
232
238
  file_path: str,
233
239
  content: str,
234
240
  ) -> WriteResult:
@@ -236,10 +242,10 @@ class FilesystemBackend:
236
242
  Returns WriteResult. External storage sets files_update=None.
237
243
  """
238
244
  resolved_path = self._resolve_path(file_path)
239
-
245
+
240
246
  if resolved_path.exists():
241
247
  return WriteResult(error=f"Cannot write to {file_path} because it already exists. Read and then make an edit, or write to a new path.")
242
-
248
+
243
249
  try:
244
250
  # Create parent directories if needed
245
251
  resolved_path.parent.mkdir(parents=True, exist_ok=True)
@@ -251,13 +257,13 @@ class FilesystemBackend:
251
257
  fd = os.open(resolved_path, flags, 0o644)
252
258
  with os.fdopen(fd, "w", encoding="utf-8") as f:
253
259
  f.write(content)
254
-
260
+
255
261
  return WriteResult(path=file_path, files_update=None)
256
262
  except (OSError, UnicodeEncodeError) as e:
257
263
  return WriteResult(error=f"Error writing file '{file_path}': {e}")
258
-
264
+
259
265
  def edit(
260
- self,
266
+ self,
261
267
  file_path: str,
262
268
  old_string: str,
263
269
  new_string: str,
@@ -267,10 +273,10 @@ class FilesystemBackend:
267
273
  Returns EditResult. External storage sets files_update=None.
268
274
  """
269
275
  resolved_path = self._resolve_path(file_path)
270
-
276
+
271
277
  if not resolved_path.exists() or not resolved_path.is_file():
272
278
  return EditResult(error=f"Error: File '{file_path}' not found")
273
-
279
+
274
280
  try:
275
281
  # Read securely
276
282
  try:
@@ -278,16 +284,16 @@ class FilesystemBackend:
278
284
  with os.fdopen(fd, "r", encoding="utf-8") as f:
279
285
  content = f.read()
280
286
  except OSError:
281
- with open(resolved_path, "r", encoding="utf-8") as f:
287
+ with open(resolved_path, encoding="utf-8") as f:
282
288
  content = f.read()
283
-
289
+
284
290
  result = perform_string_replacement(content, old_string, new_string, replace_all)
285
-
291
+
286
292
  if isinstance(result, str):
287
293
  return EditResult(error=result)
288
-
294
+
289
295
  new_content, occurrences = result
290
-
296
+
291
297
  # Write securely
292
298
  flags = os.O_WRONLY | os.O_TRUNC
293
299
  if hasattr(os, "O_NOFOLLOW"):
@@ -295,18 +301,18 @@ class FilesystemBackend:
295
301
  fd = os.open(resolved_path, flags)
296
302
  with os.fdopen(fd, "w", encoding="utf-8") as f:
297
303
  f.write(new_content)
298
-
304
+
299
305
  return EditResult(path=file_path, files_update=None, occurrences=int(occurrences))
300
306
  except (OSError, UnicodeDecodeError, UnicodeEncodeError) as e:
301
307
  return EditResult(error=f"Error editing file '{file_path}': {e}")
302
-
308
+
303
309
  # Removed legacy grep() convenience to keep lean surface
304
310
 
305
311
  def grep_raw(
306
312
  self,
307
313
  pattern: str,
308
- path: Optional[str] = None,
309
- glob: Optional[str] = None,
314
+ path: str | None = None,
315
+ glob: str | None = None,
310
316
  ) -> list[GrepMatch] | str:
311
317
  # Validate regex
312
318
  try:
@@ -334,9 +340,7 @@ class FilesystemBackend:
334
340
  matches.append({"path": fpath, "line": int(line_num), "text": line_text})
335
341
  return matches
336
342
 
337
- def _ripgrep_search(
338
- self, pattern: str, base_full: Path, include_glob: Optional[str]
339
- ) -> Optional[dict[str, list[tuple[int, str]]]]:
343
+ def _ripgrep_search(self, pattern: str, base_full: Path, include_glob: str | None) -> dict[str, list[tuple[int, str]]] | None:
340
344
  cmd = ["rg", "--json"]
341
345
  if include_glob:
342
346
  cmd.extend(["--glob", include_glob])
@@ -381,9 +385,7 @@ class FilesystemBackend:
381
385
 
382
386
  return results
383
387
 
384
- def _python_search(
385
- self, pattern: str, base_full: Path, include_glob: Optional[str]
386
- ) -> dict[str, list[tuple[int, str]]]:
388
+ def _python_search(self, pattern: str, base_full: Path, include_glob: str | None) -> dict[str, list[tuple[int, str]]]:
387
389
  try:
388
390
  regex = re.compile(pattern)
389
391
  except re.error:
@@ -418,7 +420,7 @@ class FilesystemBackend:
418
420
  results.setdefault(virt_path, []).append((line_num, line))
419
421
 
420
422
  return results
421
-
423
+
422
424
  def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
423
425
  if pattern.startswith("/"):
424
426
  pattern = pattern.lstrip("/")
@@ -441,12 +443,14 @@ class FilesystemBackend:
441
443
  if not self.virtual_mode:
442
444
  try:
443
445
  st = matched_path.stat()
444
- results.append({
445
- "path": abs_path,
446
- "is_dir": False,
447
- "size": int(st.st_size),
448
- "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
449
- })
446
+ results.append(
447
+ {
448
+ "path": abs_path,
449
+ "is_dir": False,
450
+ "size": int(st.st_size),
451
+ "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
452
+ }
453
+ )
450
454
  except OSError:
451
455
  results.append({"path": abs_path, "is_dir": False})
452
456
  else:
@@ -454,20 +458,22 @@ class FilesystemBackend:
454
458
  if not cwd_str.endswith("/"):
455
459
  cwd_str += "/"
456
460
  if abs_path.startswith(cwd_str):
457
- relative_path = abs_path[len(cwd_str):]
461
+ relative_path = abs_path[len(cwd_str) :]
458
462
  elif abs_path.startswith(str(self.cwd)):
459
- relative_path = abs_path[len(str(self.cwd)):].lstrip("/")
463
+ relative_path = abs_path[len(str(self.cwd)) :].lstrip("/")
460
464
  else:
461
465
  relative_path = abs_path
462
466
  virt = "/" + relative_path
463
467
  try:
464
468
  st = matched_path.stat()
465
- results.append({
466
- "path": virt,
467
- "is_dir": False,
468
- "size": int(st.st_size),
469
- "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
470
- })
469
+ results.append(
470
+ {
471
+ "path": virt,
472
+ "is_dir": False,
473
+ "size": int(st.st_size),
474
+ "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
475
+ }
476
+ )
471
477
  except OSError:
472
478
  results.append({"path": virt, "is_dir": False})
473
479
  except (OSError, ValueError):