ommlds 0.0.0.dev440__py3-none-any.whl → 0.0.0.dev480__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.
- ommlds/.omlish-manifests.json +332 -35
- ommlds/__about__.py +15 -9
- ommlds/_hacks/__init__.py +4 -0
- ommlds/_hacks/funcs.py +110 -0
- ommlds/_hacks/names.py +158 -0
- ommlds/_hacks/params.py +73 -0
- ommlds/_hacks/patches.py +0 -3
- ommlds/backends/anthropic/protocol/_marshal.py +2 -2
- ommlds/backends/anthropic/protocol/sse/_marshal.py +1 -1
- ommlds/backends/anthropic/protocol/sse/assemble.py +23 -7
- ommlds/backends/anthropic/protocol/sse/events.py +13 -0
- ommlds/backends/anthropic/protocol/types.py +30 -9
- ommlds/backends/google/protocol/__init__.py +3 -0
- ommlds/backends/google/protocol/_marshal.py +16 -0
- ommlds/backends/google/protocol/types.py +626 -0
- ommlds/backends/groq/_marshal.py +23 -0
- ommlds/backends/groq/protocol.py +249 -0
- ommlds/backends/mlx/generation.py +1 -1
- ommlds/backends/mlx/loading.py +58 -1
- ommlds/backends/ollama/__init__.py +0 -0
- ommlds/backends/ollama/protocol.py +170 -0
- ommlds/backends/openai/protocol/__init__.py +9 -28
- ommlds/backends/openai/protocol/_common.py +18 -0
- ommlds/backends/openai/protocol/_marshal.py +27 -0
- ommlds/backends/openai/protocol/chatcompletion/chunk.py +58 -31
- ommlds/backends/openai/protocol/chatcompletion/contentpart.py +49 -44
- ommlds/backends/openai/protocol/chatcompletion/message.py +55 -43
- ommlds/backends/openai/protocol/chatcompletion/request.py +114 -66
- ommlds/backends/openai/protocol/chatcompletion/response.py +71 -45
- ommlds/backends/openai/protocol/chatcompletion/responseformat.py +27 -20
- ommlds/backends/openai/protocol/chatcompletion/tokenlogprob.py +16 -7
- ommlds/backends/openai/protocol/completionusage.py +24 -15
- ommlds/backends/tavily/__init__.py +0 -0
- ommlds/backends/tavily/protocol.py +301 -0
- ommlds/backends/tinygrad/models/llama3/__init__.py +22 -14
- ommlds/backends/transformers/__init__.py +0 -0
- ommlds/backends/transformers/filecache.py +109 -0
- ommlds/backends/transformers/streamers.py +73 -0
- ommlds/cli/asyncs.py +30 -0
- ommlds/cli/backends/catalog.py +93 -0
- ommlds/cli/backends/configs.py +9 -0
- ommlds/cli/backends/inject.py +31 -36
- ommlds/cli/backends/injection.py +16 -0
- ommlds/cli/backends/types.py +46 -0
- ommlds/cli/content/__init__.py +0 -0
- ommlds/cli/content/messages.py +34 -0
- ommlds/cli/content/strings.py +42 -0
- ommlds/cli/inject.py +15 -32
- ommlds/cli/inputs/__init__.py +0 -0
- ommlds/cli/inputs/asyncs.py +32 -0
- ommlds/cli/inputs/sync.py +75 -0
- ommlds/cli/main.py +270 -110
- ommlds/cli/rendering/__init__.py +0 -0
- ommlds/cli/rendering/configs.py +9 -0
- ommlds/cli/rendering/inject.py +31 -0
- ommlds/cli/rendering/markdown.py +52 -0
- ommlds/cli/rendering/raw.py +73 -0
- ommlds/cli/rendering/types.py +21 -0
- ommlds/cli/secrets.py +21 -0
- ommlds/cli/sessions/base.py +1 -1
- ommlds/cli/sessions/chat/chat/__init__.py +0 -0
- ommlds/cli/sessions/chat/chat/ai/__init__.py +0 -0
- ommlds/cli/sessions/chat/chat/ai/configs.py +11 -0
- ommlds/cli/sessions/chat/chat/ai/inject.py +74 -0
- ommlds/cli/sessions/chat/chat/ai/injection.py +14 -0
- ommlds/cli/sessions/chat/chat/ai/rendering.py +70 -0
- ommlds/cli/sessions/chat/chat/ai/services.py +79 -0
- ommlds/cli/sessions/chat/chat/ai/tools.py +44 -0
- ommlds/cli/sessions/chat/chat/ai/types.py +28 -0
- ommlds/cli/sessions/chat/chat/state/__init__.py +0 -0
- ommlds/cli/sessions/chat/chat/state/configs.py +11 -0
- ommlds/cli/sessions/chat/chat/state/inject.py +36 -0
- ommlds/cli/sessions/chat/chat/state/inmemory.py +33 -0
- ommlds/cli/sessions/chat/chat/state/storage.py +52 -0
- ommlds/cli/sessions/chat/chat/state/types.py +38 -0
- ommlds/cli/sessions/chat/chat/user/__init__.py +0 -0
- ommlds/cli/sessions/chat/chat/user/configs.py +17 -0
- ommlds/cli/sessions/chat/chat/user/inject.py +62 -0
- ommlds/cli/sessions/chat/chat/user/interactive.py +31 -0
- ommlds/cli/sessions/chat/chat/user/oneshot.py +25 -0
- ommlds/cli/sessions/chat/chat/user/types.py +15 -0
- ommlds/cli/sessions/chat/configs.py +27 -0
- ommlds/cli/sessions/chat/driver.py +43 -0
- ommlds/cli/sessions/chat/inject.py +33 -65
- ommlds/cli/sessions/chat/phases/__init__.py +0 -0
- ommlds/cli/sessions/chat/phases/inject.py +27 -0
- ommlds/cli/sessions/chat/phases/injection.py +14 -0
- ommlds/cli/sessions/chat/phases/manager.py +29 -0
- ommlds/cli/sessions/chat/phases/types.py +29 -0
- ommlds/cli/sessions/chat/session.py +27 -0
- ommlds/cli/sessions/chat/tools/__init__.py +0 -0
- ommlds/cli/sessions/chat/tools/configs.py +22 -0
- ommlds/cli/sessions/chat/tools/confirmation.py +46 -0
- ommlds/cli/sessions/chat/tools/execution.py +66 -0
- ommlds/cli/sessions/chat/tools/fs/__init__.py +0 -0
- ommlds/cli/sessions/chat/tools/fs/configs.py +12 -0
- ommlds/cli/sessions/chat/tools/fs/inject.py +35 -0
- ommlds/cli/sessions/chat/tools/inject.py +88 -0
- ommlds/cli/sessions/chat/tools/injection.py +44 -0
- ommlds/cli/sessions/chat/tools/rendering.py +58 -0
- ommlds/cli/sessions/chat/tools/todo/__init__.py +0 -0
- ommlds/cli/sessions/chat/tools/todo/configs.py +12 -0
- ommlds/cli/sessions/chat/tools/todo/inject.py +31 -0
- ommlds/cli/sessions/chat/tools/weather/__init__.py +0 -0
- ommlds/cli/sessions/chat/tools/weather/configs.py +12 -0
- ommlds/cli/sessions/chat/tools/weather/inject.py +22 -0
- ommlds/cli/{tools/weather.py → sessions/chat/tools/weather/tools.py} +1 -1
- ommlds/cli/sessions/completion/configs.py +21 -0
- ommlds/cli/sessions/completion/inject.py +42 -0
- ommlds/cli/sessions/completion/session.py +35 -0
- ommlds/cli/sessions/embedding/configs.py +21 -0
- ommlds/cli/sessions/embedding/inject.py +42 -0
- ommlds/cli/sessions/embedding/session.py +33 -0
- ommlds/cli/sessions/inject.py +28 -11
- ommlds/cli/state/__init__.py +0 -0
- ommlds/cli/state/inject.py +28 -0
- ommlds/cli/{state.py → state/storage.py} +41 -24
- ommlds/minichain/__init__.py +84 -24
- ommlds/minichain/_marshal.py +49 -9
- ommlds/minichain/_typedvalues.py +2 -4
- ommlds/minichain/backends/catalogs/base.py +20 -1
- ommlds/minichain/backends/catalogs/simple.py +2 -2
- ommlds/minichain/backends/catalogs/strings.py +10 -8
- ommlds/minichain/backends/impls/anthropic/chat.py +65 -27
- ommlds/minichain/backends/impls/anthropic/names.py +10 -8
- ommlds/minichain/backends/impls/anthropic/protocol.py +109 -0
- ommlds/minichain/backends/impls/anthropic/stream.py +111 -43
- ommlds/minichain/backends/impls/duckduckgo/search.py +1 -1
- ommlds/minichain/backends/impls/dummy/__init__.py +0 -0
- ommlds/minichain/backends/impls/dummy/chat.py +69 -0
- ommlds/minichain/backends/impls/google/chat.py +114 -22
- ommlds/minichain/backends/impls/google/search.py +7 -2
- ommlds/minichain/backends/impls/google/stream.py +219 -0
- ommlds/minichain/backends/impls/google/tools.py +149 -0
- ommlds/minichain/backends/impls/groq/__init__.py +0 -0
- ommlds/minichain/backends/impls/groq/chat.py +75 -0
- ommlds/minichain/backends/impls/groq/names.py +48 -0
- ommlds/minichain/backends/impls/groq/protocol.py +143 -0
- ommlds/minichain/backends/impls/groq/stream.py +125 -0
- ommlds/minichain/backends/impls/llamacpp/chat.py +33 -18
- ommlds/minichain/backends/impls/llamacpp/completion.py +1 -1
- ommlds/minichain/backends/impls/llamacpp/format.py +4 -2
- ommlds/minichain/backends/impls/llamacpp/stream.py +37 -20
- ommlds/minichain/backends/impls/mistral.py +20 -5
- ommlds/minichain/backends/impls/mlx/chat.py +96 -22
- ommlds/minichain/backends/impls/ollama/__init__.py +0 -0
- ommlds/minichain/backends/impls/ollama/chat.py +199 -0
- ommlds/minichain/backends/impls/openai/chat.py +18 -8
- ommlds/minichain/backends/impls/openai/completion.py +10 -3
- ommlds/minichain/backends/impls/openai/embedding.py +10 -3
- ommlds/minichain/backends/impls/openai/format.py +131 -106
- ommlds/minichain/backends/impls/openai/names.py +31 -5
- ommlds/minichain/backends/impls/openai/stream.py +43 -25
- ommlds/minichain/backends/impls/tavily.py +66 -0
- ommlds/minichain/backends/impls/tinygrad/chat.py +23 -16
- ommlds/minichain/backends/impls/transformers/sentence.py +1 -1
- ommlds/minichain/backends/impls/transformers/tokens.py +1 -1
- ommlds/minichain/backends/impls/transformers/transformers.py +155 -34
- ommlds/minichain/backends/strings/parsing.py +1 -1
- ommlds/minichain/backends/strings/resolving.py +4 -1
- ommlds/minichain/chat/_marshal.py +16 -9
- ommlds/minichain/chat/choices/adapters.py +4 -4
- ommlds/minichain/chat/choices/services.py +1 -1
- ommlds/minichain/chat/choices/stream/__init__.py +0 -0
- ommlds/minichain/chat/choices/stream/adapters.py +35 -0
- ommlds/minichain/chat/choices/stream/joining.py +31 -0
- ommlds/minichain/chat/choices/stream/services.py +45 -0
- ommlds/minichain/chat/choices/stream/types.py +43 -0
- ommlds/minichain/chat/choices/types.py +2 -2
- ommlds/minichain/chat/history.py +3 -3
- ommlds/minichain/chat/messages.py +55 -19
- ommlds/minichain/chat/services.py +3 -3
- ommlds/minichain/chat/stream/_marshal.py +16 -0
- ommlds/minichain/chat/stream/joining.py +85 -0
- ommlds/minichain/chat/stream/services.py +15 -21
- ommlds/minichain/chat/stream/types.py +32 -19
- ommlds/minichain/chat/tools/execution.py +8 -7
- ommlds/minichain/chat/tools/ids.py +9 -15
- ommlds/minichain/chat/tools/parsing.py +17 -26
- ommlds/minichain/chat/transforms/base.py +29 -38
- ommlds/minichain/chat/transforms/metadata.py +30 -4
- ommlds/minichain/chat/transforms/services.py +9 -11
- ommlds/minichain/content/_marshal.py +44 -20
- ommlds/minichain/content/json.py +13 -0
- ommlds/minichain/content/materialize.py +14 -21
- ommlds/minichain/content/prepare.py +4 -0
- ommlds/minichain/content/transforms/interleave.py +1 -1
- ommlds/minichain/content/transforms/squeeze.py +1 -1
- ommlds/minichain/content/transforms/stringify.py +1 -1
- ommlds/minichain/json.py +20 -0
- ommlds/minichain/lib/code/__init__.py +0 -0
- ommlds/minichain/lib/code/prompts.py +6 -0
- ommlds/minichain/lib/fs/binfiles.py +108 -0
- ommlds/minichain/lib/fs/context.py +126 -0
- ommlds/minichain/lib/fs/errors.py +101 -0
- ommlds/minichain/lib/fs/suggestions.py +36 -0
- ommlds/minichain/lib/fs/tools/__init__.py +0 -0
- ommlds/minichain/lib/fs/tools/edit.py +104 -0
- ommlds/minichain/lib/fs/tools/ls.py +38 -0
- ommlds/minichain/lib/fs/tools/read.py +115 -0
- ommlds/minichain/lib/fs/tools/recursivels/__init__.py +0 -0
- ommlds/minichain/lib/fs/tools/recursivels/execution.py +40 -0
- ommlds/minichain/lib/todo/__init__.py +0 -0
- ommlds/minichain/lib/todo/context.py +54 -0
- ommlds/minichain/lib/todo/tools/__init__.py +0 -0
- ommlds/minichain/lib/todo/tools/read.py +44 -0
- ommlds/minichain/lib/todo/tools/write.py +335 -0
- ommlds/minichain/lib/todo/types.py +60 -0
- ommlds/minichain/llms/_marshal.py +25 -17
- ommlds/minichain/llms/types.py +4 -0
- ommlds/minichain/registries/globals.py +18 -4
- ommlds/minichain/resources.py +66 -43
- ommlds/minichain/search.py +1 -1
- ommlds/minichain/services/_marshal.py +46 -39
- ommlds/minichain/services/facades.py +3 -3
- ommlds/minichain/services/services.py +1 -1
- ommlds/minichain/standard.py +8 -0
- ommlds/minichain/stream/services.py +152 -38
- ommlds/minichain/stream/wrap.py +22 -24
- ommlds/minichain/tools/_marshal.py +1 -1
- ommlds/minichain/tools/execution/catalog.py +2 -1
- ommlds/minichain/tools/execution/context.py +34 -14
- ommlds/minichain/tools/execution/errors.py +15 -0
- ommlds/minichain/tools/execution/executors.py +8 -3
- ommlds/minichain/tools/execution/reflect.py +40 -5
- ommlds/minichain/tools/fns.py +46 -9
- ommlds/minichain/tools/jsonschema.py +14 -5
- ommlds/minichain/tools/reflect.py +54 -18
- ommlds/minichain/tools/types.py +33 -1
- ommlds/minichain/utils.py +27 -0
- ommlds/minichain/vectors/_marshal.py +11 -10
- ommlds/nanochat/LICENSE +21 -0
- ommlds/nanochat/__init__.py +0 -0
- ommlds/nanochat/rustbpe/LICENSE +21 -0
- ommlds/nanochat/tokenizers.py +406 -0
- ommlds/server/server.py +3 -3
- ommlds/specs/__init__.py +0 -0
- ommlds/specs/mcp/__init__.py +0 -0
- ommlds/specs/mcp/_marshal.py +23 -0
- ommlds/specs/mcp/protocol.py +266 -0
- ommlds/tools/git.py +27 -10
- ommlds/tools/ocr.py +8 -9
- ommlds/wiki/analyze.py +2 -2
- ommlds/wiki/text/mfh.py +1 -5
- ommlds/wiki/text/wtp.py +1 -3
- ommlds/wiki/utils/xml.py +5 -5
- {ommlds-0.0.0.dev440.dist-info → ommlds-0.0.0.dev480.dist-info}/METADATA +24 -21
- ommlds-0.0.0.dev480.dist-info/RECORD +427 -0
- ommlds/cli/backends/standard.py +0 -20
- ommlds/cli/sessions/chat/base.py +0 -42
- ommlds/cli/sessions/chat/interactive.py +0 -73
- ommlds/cli/sessions/chat/printing.py +0 -96
- ommlds/cli/sessions/chat/prompt.py +0 -143
- ommlds/cli/sessions/chat/state.py +0 -109
- ommlds/cli/sessions/chat/tools.py +0 -91
- ommlds/cli/sessions/completion/completion.py +0 -44
- ommlds/cli/sessions/embedding/embedding.py +0 -42
- ommlds/cli/tools/config.py +0 -13
- ommlds/cli/tools/inject.py +0 -64
- ommlds/minichain/chat/stream/adapters.py +0 -69
- ommlds/minichain/lib/fs/ls/execution.py +0 -32
- ommlds-0.0.0.dev440.dist-info/RECORD +0 -303
- /ommlds/{cli/tools → backends/google}/__init__.py +0 -0
- /ommlds/{minichain/lib/fs/ls → backends/groq}/__init__.py +0 -0
- /ommlds/{huggingface.py → backends/huggingface.py} +0 -0
- /ommlds/minichain/lib/fs/{ls → tools/recursivels}/rendering.py +0 -0
- /ommlds/minichain/lib/fs/{ls → tools/recursivels}/running.py +0 -0
- {ommlds-0.0.0.dev440.dist-info → ommlds-0.0.0.dev480.dist-info}/WHEEL +0 -0
- {ommlds-0.0.0.dev440.dist-info → ommlds-0.0.0.dev480.dist-info}/entry_points.txt +0 -0
- {ommlds-0.0.0.dev440.dist-info → ommlds-0.0.0.dev480.dist-info}/licenses/LICENSE +0 -0
- {ommlds-0.0.0.dev440.dist-info → ommlds-0.0.0.dev480.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
import typing as ta
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
##
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
IMAGE_FILE_EXTENSIONS: ta.AbstractSet[str] = frozenset([
|
|
9
|
+
'jpg',
|
|
10
|
+
'jpeg',
|
|
11
|
+
'png',
|
|
12
|
+
'gif',
|
|
13
|
+
'bmp',
|
|
14
|
+
'webp',
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ARCHIVE_FILE_EXTENSIONS: ta.AbstractSet[str] = frozenset([
|
|
19
|
+
'zip',
|
|
20
|
+
'tar',
|
|
21
|
+
'gz',
|
|
22
|
+
'xz',
|
|
23
|
+
'7z',
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DOC_FILE_EXTENSIONS: ta.AbstractSet[str] = frozenset([
|
|
28
|
+
'doc',
|
|
29
|
+
'docx',
|
|
30
|
+
'xls',
|
|
31
|
+
'xlsx',
|
|
32
|
+
'ppt',
|
|
33
|
+
'pptx',
|
|
34
|
+
'odt',
|
|
35
|
+
'ods',
|
|
36
|
+
'odp',
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
EXECUTABLE_FILE_EXTENSIONS: ta.AbstractSet[str] = frozenset([
|
|
41
|
+
'exe',
|
|
42
|
+
'dll',
|
|
43
|
+
'so',
|
|
44
|
+
|
|
45
|
+
'obj',
|
|
46
|
+
'o',
|
|
47
|
+
'a',
|
|
48
|
+
'lib',
|
|
49
|
+
|
|
50
|
+
'class',
|
|
51
|
+
'jar',
|
|
52
|
+
'war',
|
|
53
|
+
|
|
54
|
+
'wasm',
|
|
55
|
+
|
|
56
|
+
'pyc',
|
|
57
|
+
'pyo',
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
BLOB_FILE_EXTENSIONS: ta.AbstractSet[str] = frozenset([
|
|
62
|
+
'bin',
|
|
63
|
+
'dat',
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
BINARY_FILE_EXTENSIONS: ta.AbstractSet[str] = frozenset([
|
|
68
|
+
*IMAGE_FILE_EXTENSIONS,
|
|
69
|
+
*ARCHIVE_FILE_EXTENSIONS,
|
|
70
|
+
*DOC_FILE_EXTENSIONS,
|
|
71
|
+
*EXECUTABLE_FILE_EXTENSIONS,
|
|
72
|
+
*BLOB_FILE_EXTENSIONS,
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def has_binary_file_extension(file_path: str) -> bool:
|
|
77
|
+
return os.path.basename(file_path).partition('.')[-1] in BINARY_FILE_EXTENSIONS
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
##
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def is_binary_file(
|
|
84
|
+
file_path: str,
|
|
85
|
+
*,
|
|
86
|
+
chunk_size: int = 0x1000,
|
|
87
|
+
non_printable_cutoff: float = .3,
|
|
88
|
+
|
|
89
|
+
st: os.stat_result | None = None,
|
|
90
|
+
) -> bool:
|
|
91
|
+
if st is None:
|
|
92
|
+
try:
|
|
93
|
+
st = os.stat(file_path)
|
|
94
|
+
except OSError:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
if not st.st_size:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
with open(file_path, 'rb') as f:
|
|
101
|
+
chunk = f.read(chunk_size)
|
|
102
|
+
|
|
103
|
+
if 0 in chunk:
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
# Count "non-printable" ASCII-ish control chars (excluding TAB/LF/CR)
|
|
107
|
+
np = sum(1 for b in chunk if b < 9 or (13 < b < 32))
|
|
108
|
+
return (np / len(chunk)) > non_printable_cutoff
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
import stat
|
|
3
|
+
|
|
4
|
+
from omlish import check
|
|
5
|
+
|
|
6
|
+
from ...tools.execution.context import tool_context
|
|
7
|
+
from .binfiles import has_binary_file_extension
|
|
8
|
+
from .binfiles import is_binary_file
|
|
9
|
+
from .errors import RequestedPathDoesNotExistError
|
|
10
|
+
from .errors import RequestedPathOutsideRootDirError
|
|
11
|
+
from .errors import RequestedPathWriteNotPermittedError
|
|
12
|
+
from .errors import RequestedPathWrongTypeError
|
|
13
|
+
from .suggestions import get_path_suggestions
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FsContext:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
root_dir: str | None = None,
|
|
24
|
+
writes_permitted: bool = False,
|
|
25
|
+
) -> None:
|
|
26
|
+
super().__init__()
|
|
27
|
+
|
|
28
|
+
self._root_dir = root_dir
|
|
29
|
+
self._writes_permitted = writes_permitted
|
|
30
|
+
|
|
31
|
+
self._abs_root_dir = os.path.abspath(root_dir) if root_dir is not None else None
|
|
32
|
+
|
|
33
|
+
#
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def writes_permitted(self) -> bool:
|
|
37
|
+
return self._writes_permitted
|
|
38
|
+
|
|
39
|
+
#
|
|
40
|
+
|
|
41
|
+
def check_requested_path(self, req_path: str) -> None:
|
|
42
|
+
abs_req_path = os.path.abspath(req_path)
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
self._abs_root_dir is None or
|
|
46
|
+
not (
|
|
47
|
+
abs_req_path == self._abs_root_dir or
|
|
48
|
+
abs_req_path.startswith(self._abs_root_dir + os.path.sep)
|
|
49
|
+
)
|
|
50
|
+
):
|
|
51
|
+
raise RequestedPathOutsideRootDirError(
|
|
52
|
+
req_path,
|
|
53
|
+
root_dir=check.not_none(self._root_dir),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
#
|
|
57
|
+
|
|
58
|
+
def check_stat_dir(
|
|
59
|
+
self,
|
|
60
|
+
req_path: str,
|
|
61
|
+
) -> os.stat_result:
|
|
62
|
+
self.check_requested_path(req_path)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
st = os.stat(req_path)
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
raise RequestedPathDoesNotExistError(
|
|
68
|
+
req_path,
|
|
69
|
+
suggested_paths=get_path_suggestions(
|
|
70
|
+
req_path,
|
|
71
|
+
filter=lambda e: e.is_dir(),
|
|
72
|
+
),
|
|
73
|
+
) from None
|
|
74
|
+
|
|
75
|
+
if not stat.S_ISDIR(st.st_mode):
|
|
76
|
+
raise RequestedPathWrongTypeError(
|
|
77
|
+
req_path,
|
|
78
|
+
expected_type='dir',
|
|
79
|
+
**(dict(actual_type='file') if stat.S_ISREG(st.st_mode) else {}),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return st
|
|
83
|
+
|
|
84
|
+
def check_stat_file(
|
|
85
|
+
self,
|
|
86
|
+
req_path: str,
|
|
87
|
+
*,
|
|
88
|
+
text: bool = False,
|
|
89
|
+
write: bool = False,
|
|
90
|
+
) -> os.stat_result:
|
|
91
|
+
self.check_requested_path(req_path)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
st = os.stat(req_path)
|
|
95
|
+
except FileNotFoundError:
|
|
96
|
+
raise RequestedPathDoesNotExistError(
|
|
97
|
+
req_path,
|
|
98
|
+
suggested_paths=get_path_suggestions(
|
|
99
|
+
req_path,
|
|
100
|
+
filter=lambda e: (e.is_file() and not (text and has_binary_file_extension(e.name))),
|
|
101
|
+
),
|
|
102
|
+
) from None
|
|
103
|
+
|
|
104
|
+
if not stat.S_ISREG(st.st_mode):
|
|
105
|
+
is_dir = stat.S_ISDIR(st.st_mode)
|
|
106
|
+
raise RequestedPathWrongTypeError(
|
|
107
|
+
req_path,
|
|
108
|
+
expected_type='file',
|
|
109
|
+
**(dict(actual_type='dir') if is_dir else {}),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if text and is_binary_file(req_path, st=st):
|
|
113
|
+
raise RequestedPathWrongTypeError(
|
|
114
|
+
req_path,
|
|
115
|
+
expected_type='text file',
|
|
116
|
+
actual_type='binary file',
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if write and not self._writes_permitted:
|
|
120
|
+
raise RequestedPathWriteNotPermittedError(req_path)
|
|
121
|
+
|
|
122
|
+
return st
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def tool_fs_context() -> FsContext:
|
|
126
|
+
return tool_context()[FsContext]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import typing as ta
|
|
2
|
+
|
|
3
|
+
from omlish import lang
|
|
4
|
+
|
|
5
|
+
from ...tools.execution.errors import ToolExecutionError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
##
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FsToolExecutionError(ToolExecutionError, lang.Abstract):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RequestedPathError(FsToolExecutionError, lang.Abstract):
|
|
19
|
+
def __init__(self, requested_path: str, *args: ta.Any) -> None:
|
|
20
|
+
super().__init__(requested_path, *args)
|
|
21
|
+
|
|
22
|
+
self.requested_path = requested_path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RequestedPathMustBeAbsoluteError(RequestedPathError):
|
|
26
|
+
@property
|
|
27
|
+
def content(self) -> str:
|
|
28
|
+
return f'Requested path {self.requested_path!r} must be absolute.'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RequestedPathOutsideRootDirError(RequestedPathError):
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
requested_path: str,
|
|
35
|
+
*,
|
|
36
|
+
root_dir: str,
|
|
37
|
+
) -> None:
|
|
38
|
+
super().__init__(
|
|
39
|
+
requested_path,
|
|
40
|
+
root_dir,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.root_dir = root_dir
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def content(self) -> str:
|
|
47
|
+
return f'Requested path {self.requested_path!r} was outside of permitted root directory {self.root_dir!r}.'
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RequestedPathWrongTypeError(RequestedPathError):
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
requested_path: str,
|
|
54
|
+
*,
|
|
55
|
+
expected_type: str,
|
|
56
|
+
actual_type: str | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
super().__init__(
|
|
59
|
+
requested_path,
|
|
60
|
+
expected_type,
|
|
61
|
+
actual_type,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
self.expected_type = expected_type
|
|
65
|
+
self.actual_type = actual_type
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def content(self) -> str:
|
|
69
|
+
return ''.join([
|
|
70
|
+
f'Requested path {self.requested_path!r} must be of type {self.expected_type!r}',
|
|
71
|
+
*([f', but it is actually of type {self.actual_type!r}'] if self.actual_type is not None else []),
|
|
72
|
+
'.',
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class RequestedPathDoesNotExistError(RequestedPathError):
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
requested_path: str,
|
|
80
|
+
*,
|
|
81
|
+
suggested_paths: ta.Sequence[str] | None = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
super().__init__(
|
|
84
|
+
requested_path,
|
|
85
|
+
suggested_paths,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self.suggested_paths = suggested_paths
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def content(self) -> str:
|
|
92
|
+
return ''.join([
|
|
93
|
+
f'Requested path {self.requested_path!r} does not exist.',
|
|
94
|
+
*([f' Did you mean one of these valid paths: {self.suggested_paths!r}?'] if self.suggested_paths else []),
|
|
95
|
+
])
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class RequestedPathWriteNotPermittedError(RequestedPathError):
|
|
99
|
+
@property
|
|
100
|
+
def content(self) -> str:
|
|
101
|
+
return f'Writes are not permitted to requested path {self.requested_path!r}.'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import difflib
|
|
2
|
+
import os.path
|
|
3
|
+
import typing as ta
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
##
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_path_suggestions(
|
|
10
|
+
bad_path: str,
|
|
11
|
+
n: int = 3,
|
|
12
|
+
*,
|
|
13
|
+
filter: ta.Callable[[os.DirEntry], bool] | None = None, # noqa
|
|
14
|
+
cutoff: float = .6,
|
|
15
|
+
) -> ta.Sequence[str] | None:
|
|
16
|
+
dn = os.path.dirname(bad_path)
|
|
17
|
+
try:
|
|
18
|
+
sdi = os.scandir(dn)
|
|
19
|
+
except FileNotFoundError:
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
fl = [
|
|
23
|
+
e.name
|
|
24
|
+
for e in sdi
|
|
25
|
+
if (filter is None or filter(e))
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
return [
|
|
29
|
+
os.path.join(dn, sn)
|
|
30
|
+
for sn in difflib.get_close_matches(
|
|
31
|
+
os.path.basename(bad_path),
|
|
32
|
+
fl,
|
|
33
|
+
n,
|
|
34
|
+
cutoff=cutoff,
|
|
35
|
+
)
|
|
36
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- must read file before editing
|
|
4
|
+
- must re-read file if file has been modified
|
|
5
|
+
- loosened replacer helpers
|
|
6
|
+
- accept diff format impl
|
|
7
|
+
- injectable confirmation, diff format
|
|
8
|
+
"""
|
|
9
|
+
from omlish import lang
|
|
10
|
+
|
|
11
|
+
from ....tools.execution.catalog import ToolCatalogEntry
|
|
12
|
+
from ....tools.execution.reflect import reflect_tool_catalog_entry
|
|
13
|
+
from ..context import tool_fs_context
|
|
14
|
+
from ..errors import RequestedPathError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EditToolError(RequestedPathError, lang.Abstract):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EmptyNewStringError(EditToolError):
|
|
25
|
+
@property
|
|
26
|
+
def content(self) -> str:
|
|
27
|
+
return f'The requested edit to {self.requested_path!r} was given an empty "old_string" parameter.'
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OldStringNotPresentError(EditToolError):
|
|
31
|
+
@property
|
|
32
|
+
def content(self) -> str:
|
|
33
|
+
return f'The requested edit to {self.requested_path!r} did not contain the given "old_string" parameter.'
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OldStringPresentMultipleTimesError(EditToolError):
|
|
37
|
+
@property
|
|
38
|
+
def content(self) -> str:
|
|
39
|
+
return f'The requested edit to {self.requested_path!r} contained the given "old_string" parameter multiple times.' # noqa
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def execute_edit_tool(
|
|
46
|
+
*,
|
|
47
|
+
file_path: str,
|
|
48
|
+
old_string: str,
|
|
49
|
+
new_string: str,
|
|
50
|
+
replace_all: bool = False,
|
|
51
|
+
) -> str:
|
|
52
|
+
"""
|
|
53
|
+
Edits the given file by replacing the string given by the 'old_string' parameter with the string given by the
|
|
54
|
+
'new_string' parameter.
|
|
55
|
+
|
|
56
|
+
The file must exist, must be a valid text file, and must be given as an absolute path.
|
|
57
|
+
|
|
58
|
+
If the 'replace_all' parameter is false (the default) then 'new_string' must be present exactly once in the file,
|
|
59
|
+
otherwise the operation will fail. If 'replace_all' is true then all instances of 'old_string' will be replaced by
|
|
60
|
+
'new_string', but the operation will fail if there are no instances of 'old_string'
|
|
61
|
+
present in the file.
|
|
62
|
+
|
|
63
|
+
For the operation to succeed, both 'old_string' and 'new_string' must be EXACT, including all exact indentation and
|
|
64
|
+
other whitespace. This *includes* trailing newlines - this operates on the file as a single string, not a list of
|
|
65
|
+
lines.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
file_path: The path of the file to edit. Must be an absolute path.
|
|
69
|
+
old_string: The old string to be replaced. May not be empty, and must be exact, including all exact whitespace.
|
|
70
|
+
new_string: The new string to replace the old string with.
|
|
71
|
+
replace_all: If false (the default) then exactly one instance of 'old_string' must be present in the file to be
|
|
72
|
+
replaced. If true then all instances of 'old_string' will be replaced by 'new_string', but at least one
|
|
73
|
+
instance of 'old_string' must be present in the file.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
if not old_string:
|
|
77
|
+
raise EmptyNewStringError(file_path)
|
|
78
|
+
|
|
79
|
+
ctx = tool_fs_context()
|
|
80
|
+
ctx.check_stat_file(file_path, text=True, write=True)
|
|
81
|
+
|
|
82
|
+
with open(file_path) as f:
|
|
83
|
+
old_file = f.read()
|
|
84
|
+
|
|
85
|
+
n = old_file.count(old_string)
|
|
86
|
+
if not n:
|
|
87
|
+
raise OldStringNotPresentError(file_path)
|
|
88
|
+
|
|
89
|
+
if not replace_all and n != 1:
|
|
90
|
+
raise OldStringPresentMultipleTimesError(file_path)
|
|
91
|
+
|
|
92
|
+
new_file = old_file.replace(old_string, new_string)
|
|
93
|
+
|
|
94
|
+
# FIXME: confirm lol
|
|
95
|
+
|
|
96
|
+
with open(file_path, 'w') as f:
|
|
97
|
+
f.write(new_file)
|
|
98
|
+
|
|
99
|
+
return 'The file has been edited successfully.'
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@lang.cached_function
|
|
103
|
+
def edit_tool() -> ToolCatalogEntry:
|
|
104
|
+
return reflect_tool_catalog_entry(execute_edit_tool)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from omlish import lang
|
|
5
|
+
|
|
6
|
+
from ....tools.execution.catalog import ToolCatalogEntry
|
|
7
|
+
from ....tools.execution.reflect import reflect_tool_catalog_entry
|
|
8
|
+
from ..context import tool_fs_context
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
##
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def execute_ls_tool(
|
|
15
|
+
dir_path: str,
|
|
16
|
+
) -> str:
|
|
17
|
+
"""
|
|
18
|
+
Lists the contents of the specified dir.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
dir_path: The dir to list the contents of. Must be an absolute path.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
ctx = tool_fs_context()
|
|
25
|
+
ctx.check_stat_dir(dir_path)
|
|
26
|
+
|
|
27
|
+
out = io.StringIO()
|
|
28
|
+
out.write('<dir>\n')
|
|
29
|
+
for e in sorted(os.scandir(dir_path), key=lambda e: e.name): # noqa
|
|
30
|
+
out.write(f'{e.name}{"/" if e.is_dir() else ""}\n')
|
|
31
|
+
out.write('</dir>\n')
|
|
32
|
+
|
|
33
|
+
return out.getvalue()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@lang.cached_function
|
|
37
|
+
def ls_tool() -> ToolCatalogEntry:
|
|
38
|
+
return reflect_tool_catalog_entry(execute_ls_tool)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- better bad unicode handling
|
|
4
|
+
- read whole file if < some size, report filesize / num lines / mtime inline
|
|
5
|
+
- fs cache
|
|
6
|
+
- track changes
|
|
7
|
+
"""
|
|
8
|
+
import io
|
|
9
|
+
import itertools
|
|
10
|
+
|
|
11
|
+
from omlish import lang
|
|
12
|
+
|
|
13
|
+
from ....tools.execution.catalog import ToolCatalogEntry
|
|
14
|
+
from ....tools.execution.reflect import reflect_tool_catalog_entry
|
|
15
|
+
from ....tools.reflect import tool_spec_override
|
|
16
|
+
from ....tools.types import ToolParam
|
|
17
|
+
from ..context import tool_fs_context
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
DEFAULT_MAX_NUM_LINES = 2_000
|
|
24
|
+
|
|
25
|
+
MAX_LINE_LENGTH = 2_000
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@tool_spec_override(
|
|
29
|
+
desc=f"""
|
|
30
|
+
Reads a file from the local filesystem. You can access any file directly by using this tool.
|
|
31
|
+
|
|
32
|
+
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that
|
|
33
|
+
path is valid. It is okay to read a file that does not exist; an error will be returned.
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
- The file_path parameter must be an absolute path, not a relative path.
|
|
37
|
+
- By default, it reads up to {DEFAULT_MAX_NUM_LINES} lines starting from the beginning of the file.
|
|
38
|
+
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to
|
|
39
|
+
read the whole file by not providing these parameters.
|
|
40
|
+
- Any lines longer than {MAX_LINE_LENGTH} characters will be truncated with "...".
|
|
41
|
+
- Invalid unicode characters will be replaced with the unicode replacement character "\\ufffd".
|
|
42
|
+
- Results are returned using cat -n format, with line numbers starting at 1 and suffixed with a pipe character
|
|
43
|
+
"|".
|
|
44
|
+
- This tool cannot read binary files, including images.
|
|
45
|
+
""",
|
|
46
|
+
params=[
|
|
47
|
+
ToolParam(
|
|
48
|
+
'file_path',
|
|
49
|
+
desc='The absolute path to the file to read.',
|
|
50
|
+
),
|
|
51
|
+
ToolParam(
|
|
52
|
+
'line_offset',
|
|
53
|
+
desc='The line number to start reading from (0-based).',
|
|
54
|
+
),
|
|
55
|
+
ToolParam(
|
|
56
|
+
'num_lines',
|
|
57
|
+
desc=f'The number of lines to read (defaults to {DEFAULT_MAX_NUM_LINES}).',
|
|
58
|
+
),
|
|
59
|
+
],
|
|
60
|
+
)
|
|
61
|
+
def execute_read_tool(
|
|
62
|
+
file_path: str,
|
|
63
|
+
*,
|
|
64
|
+
line_offset: int = 0,
|
|
65
|
+
num_lines: int = DEFAULT_MAX_NUM_LINES,
|
|
66
|
+
) -> str:
|
|
67
|
+
ctx = tool_fs_context()
|
|
68
|
+
ctx.check_stat_file(file_path, text=True)
|
|
69
|
+
|
|
70
|
+
out = io.StringIO()
|
|
71
|
+
out.write('<file>\n')
|
|
72
|
+
|
|
73
|
+
zp = len(str(line_offset + num_lines))
|
|
74
|
+
n = line_offset
|
|
75
|
+
has_trunc = False # noqa
|
|
76
|
+
with open(file_path, errors='replace') as f:
|
|
77
|
+
fi = iter(f)
|
|
78
|
+
|
|
79
|
+
for line in itertools.islice(fi, line_offset, line_offset + num_lines):
|
|
80
|
+
out.write(f'{str(n + 1).zfill(zp):}|')
|
|
81
|
+
line = line.removesuffix('\n')
|
|
82
|
+
if len(line) > MAX_LINE_LENGTH:
|
|
83
|
+
has_trunc = True # noqa
|
|
84
|
+
out.write(line[:MAX_LINE_LENGTH])
|
|
85
|
+
out.write('...')
|
|
86
|
+
else:
|
|
87
|
+
out.write(line)
|
|
88
|
+
out.write('\n')
|
|
89
|
+
n += 1
|
|
90
|
+
|
|
91
|
+
# tl = n
|
|
92
|
+
# if (ml := lang.ilen(fi)):
|
|
93
|
+
# check.state(n == num_lines)
|
|
94
|
+
# tl += ml
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
next(fi)
|
|
98
|
+
except StopIteration:
|
|
99
|
+
has_more = False
|
|
100
|
+
else:
|
|
101
|
+
has_more = True
|
|
102
|
+
|
|
103
|
+
out.write(f'</file>\n')
|
|
104
|
+
|
|
105
|
+
if has_more:
|
|
106
|
+
out.write(
|
|
107
|
+
f'\n(File has more lines. Use "line_offset" parameter to read beyond line {line_offset + num_lines}.)\n',
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return out.getvalue()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@lang.cached_function
|
|
114
|
+
def read_tool() -> ToolCatalogEntry:
|
|
115
|
+
return reflect_tool_catalog_entry(execute_read_tool)
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from omlish import lang
|
|
2
|
+
|
|
3
|
+
from .....tools.execution.catalog import ToolCatalogEntry
|
|
4
|
+
from .....tools.execution.reflect import reflect_tool_catalog_entry
|
|
5
|
+
from ...context import tool_fs_context
|
|
6
|
+
from .rendering import LsLinesRenderer
|
|
7
|
+
from .running import LsRunner
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def execute_recursive_ls_tool(
|
|
14
|
+
base_path: str,
|
|
15
|
+
) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Recursively lists the directory contents of the given base path.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
base_path: The path of the directory to list the contents of. Must be absolute.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
A formatted string of the recursive directory contents.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
ft_ctx = tool_fs_context()
|
|
27
|
+
ft_ctx.check_requested_path(base_path)
|
|
28
|
+
|
|
29
|
+
root = LsRunner().run(base_path)
|
|
30
|
+
lines = LsLinesRenderer().render(root)
|
|
31
|
+
return '\n'.join([
|
|
32
|
+
'<dir>',
|
|
33
|
+
*lines.lines,
|
|
34
|
+
'</dir>',
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@lang.cached_function
|
|
39
|
+
def recursive_ls_tool() -> ToolCatalogEntry:
|
|
40
|
+
return reflect_tool_catalog_entry(execute_recursive_ls_tool)
|
|
File without changes
|