llms-py 3.0.15__tar.gz → 3.0.16__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.
- {llms_py-3.0.15/llms_py.egg-info → llms_py-3.0.16}/PKG-INFO +1 -1
- llms_py-3.0.16/llms/extensions/computer/__init__.py +59 -0
- {llms_py-3.0.15/llms/extensions/computer_use → llms_py-3.0.16/llms/extensions/computer}/bash.py +2 -2
- {llms_py-3.0.15/llms/extensions/computer_use → llms_py-3.0.16/llms/extensions/computer}/edit.py +10 -14
- llms_py-3.0.16/llms/extensions/computer/filesystem.py +542 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/__init__.py +0 -38
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/providers/google.py +57 -30
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/skills/ui/index.mjs +27 -0
- llms_py-3.0.16/llms/extensions/tools/__init__.py +67 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/tools/ui/index.mjs +92 -4
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/main.py +208 -31
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/ai.mjs +1 -1
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/app.css +491 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/modules/chat/ChatBody.mjs +64 -9
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/modules/chat/index.mjs +103 -91
- {llms_py-3.0.15 → llms_py-3.0.16/llms_py.egg-info}/PKG-INFO +1 -1
- {llms_py-3.0.15 → llms_py-3.0.16}/llms_py.egg-info/SOURCES.txt +9 -8
- {llms_py-3.0.15 → llms_py-3.0.16}/pyproject.toml +1 -1
- {llms_py-3.0.15 → llms_py-3.0.16}/setup.py +1 -1
- {llms_py-3.0.15 → llms_py-3.0.16}/tests/test_utils.py +66 -14
- llms_py-3.0.15/llms/extensions/computer_use/__init__.py +0 -27
- llms_py-3.0.15/llms/extensions/tools/__init__.py +0 -144
- {llms_py-3.0.15 → llms_py-3.0.16}/LICENSE +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/MANIFEST.in +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/README.md +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/__init__.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/__main__.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/db.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/analytics/ui/index.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/app/README.md +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/app/__init__.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/app/db.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/app/ui/Recents.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/app/ui/index.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/app/ui/threadStore.mjs +0 -0
- {llms_py-3.0.15/llms/extensions/computer_use → llms_py-3.0.16/llms/extensions/computer}/README.md +0 -0
- {llms_py-3.0.15/llms/extensions/computer_use → llms_py-3.0.16/llms/extensions/computer}/base.py +0 -0
- {llms_py-3.0.15/llms/extensions/computer_use → llms_py-3.0.16/llms/extensions/computer}/computer.py +0 -0
- {llms_py-3.0.15/llms/extensions/computer_use → llms_py-3.0.16/llms/extensions/computer}/platform.py +0 -0
- {llms_py-3.0.15/llms/extensions/computer_use → llms_py-3.0.16/llms/extensions/computer}/run.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/CALCULATOR.md +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/codemirror.css +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/codemirror.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/doc/docs.css +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/mode/python/python.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/theme/dracula.css +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/codemirror/theme/mocha.css +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/core_tools/ui/index.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/gallery/README.md +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/gallery/__init__.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/gallery/db.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/gallery/ui/index.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/README.md +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/__init__.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/README.md +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/contrib/auto-render.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/contrib/auto-render.min.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/contrib/auto-render.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/contrib/copy-tex.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/contrib/copy-tex.min.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/contrib/copy-tex.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/contrib/mathtex-script-type.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/contrib/mhchem.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/contrib/mhchem.min.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/contrib/mhchem.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/contrib/render-a11y-string.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/contrib/render-a11y-string.min.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/contrib/render-a11y-string.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/index.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/katex-swap.css +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/katex-swap.min.css +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/katex.css +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/katex.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/katex.min.css +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/katex.min.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/katex.min.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/katex/ui/katex.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/providers/__init__.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/providers/anthropic.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/providers/cerebras.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/providers/chutes.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/providers/nvidia.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/providers/openai.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/providers/openrouter.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/providers/zai.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/skills/LICENSE +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/skills/__init__.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/skills/errors.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/skills/models.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/skills/parser.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/skills/ui/skills/create-plan/SKILL.md +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/skills/validator.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/system_prompts/README.md +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/system_prompts/__init__.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/system_prompts/ui/index.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/extensions/system_prompts/ui/prompts.json +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/index.html +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/llms.json +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/providers-extra.json +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/providers.json +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/App.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/ctx.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/fav.svg +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/index.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/lib/chart.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/lib/charts.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/lib/color.js +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/lib/highlight.min.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/lib/idb.min.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/lib/marked.min.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/lib/servicestack-client.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/lib/servicestack-vue.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/lib/vue-router.min.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/lib/vue.min.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/lib/vue.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/markdown.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/modules/chat/SettingsDialog.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/modules/icons.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/modules/layout.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/modules/model-selector.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/tailwind.input.css +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/typography.css +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms/ui/utils.mjs +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms_py.egg-info/dependency_links.txt +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms_py.egg-info/entry_points.txt +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms_py.egg-info/not-zip-safe +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms_py.egg-info/requires.txt +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/llms_py.egg-info/top_level.txt +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/requirements.txt +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/setup.cfg +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/tests/test_async.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/tests/test_config.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/tests/test_core_tools_direct.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/tests/test_extensions.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/tests/test_gemini_tool_calling.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/tests/test_gemini_upload.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/tests/test_integration.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/tests/test_interleaved_thinking.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/tests/test_provider_checks.py +0 -0
- {llms_py-3.0.15 → llms_py-3.0.16}/tests/test_provider_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: llms-py
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.16
|
|
4
4
|
Summary: A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers
|
|
5
5
|
Home-page: https://github.com/ServiceStack/llms
|
|
6
6
|
Author: ServiceStack
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Anthropic's Computer Use Tools
|
|
3
|
+
https://github.com/anthropics/claude-quickstarts/tree/main/computer-use-demo
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from .bash import open, run_bash
|
|
9
|
+
from .computer import computer
|
|
10
|
+
from .edit import edit
|
|
11
|
+
from .filesystem import (
|
|
12
|
+
create_directory,
|
|
13
|
+
directory_tree,
|
|
14
|
+
edit_file,
|
|
15
|
+
filesystem_init,
|
|
16
|
+
get_file_info,
|
|
17
|
+
list_allowed_directories,
|
|
18
|
+
list_directory,
|
|
19
|
+
list_directory_with_sizes,
|
|
20
|
+
move_file,
|
|
21
|
+
read_media_file,
|
|
22
|
+
read_multiple_files,
|
|
23
|
+
read_text_file,
|
|
24
|
+
search_files,
|
|
25
|
+
write_file,
|
|
26
|
+
)
|
|
27
|
+
from .platform import get_display_num, get_screen_resolution
|
|
28
|
+
|
|
29
|
+
width, height = get_screen_resolution()
|
|
30
|
+
# set enviroment variables
|
|
31
|
+
os.environ["WIDTH"] = str(width)
|
|
32
|
+
os.environ["HEIGHT"] = str(height)
|
|
33
|
+
os.environ["DISPLAY_NUM"] = str(get_display_num())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def install(ctx):
|
|
37
|
+
filesystem_init(ctx)
|
|
38
|
+
|
|
39
|
+
ctx.register_tool(run_bash, group="computer")
|
|
40
|
+
ctx.register_tool(open, group="computer")
|
|
41
|
+
ctx.register_tool(edit, group="computer")
|
|
42
|
+
ctx.register_tool(computer, group="computer")
|
|
43
|
+
|
|
44
|
+
ctx.register_tool(read_text_file, group="filesystem")
|
|
45
|
+
ctx.register_tool(read_media_file, group="filesystem")
|
|
46
|
+
ctx.register_tool(read_multiple_files, group="filesystem")
|
|
47
|
+
ctx.register_tool(write_file, group="filesystem")
|
|
48
|
+
ctx.register_tool(edit_file, group="filesystem")
|
|
49
|
+
ctx.register_tool(create_directory, group="filesystem")
|
|
50
|
+
ctx.register_tool(list_directory, group="filesystem")
|
|
51
|
+
ctx.register_tool(list_directory_with_sizes, group="filesystem")
|
|
52
|
+
ctx.register_tool(directory_tree, group="filesystem")
|
|
53
|
+
ctx.register_tool(move_file, group="filesystem")
|
|
54
|
+
ctx.register_tool(search_files, group="filesystem")
|
|
55
|
+
ctx.register_tool(get_file_info, group="filesystem")
|
|
56
|
+
ctx.register_tool(list_allowed_directories, group="filesystem")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
__install__ = install
|
{llms_py-3.0.15/llms/extensions/computer_use → llms_py-3.0.16/llms/extensions/computer}/bash.py
RENAMED
|
@@ -158,7 +158,7 @@ async def run_bash(
|
|
|
158
158
|
if g_tool is None:
|
|
159
159
|
g_tool = BashTool20241022()
|
|
160
160
|
|
|
161
|
-
result = await g_tool(command, restart)
|
|
161
|
+
result = await g_tool(command=command, restart=restart)
|
|
162
162
|
if isinstance(result, Exception):
|
|
163
163
|
raise result
|
|
164
164
|
else:
|
|
@@ -182,4 +182,4 @@ async def open(target: Annotated[str, "URL or file path to open"]) -> list[dict[
|
|
|
182
182
|
else: # Linux and other Unix-like
|
|
183
183
|
cmd = ["xdg-open", target]
|
|
184
184
|
|
|
185
|
-
return await run_bash(" ".join(cmd))
|
|
185
|
+
return await run_bash(command=" ".join(cmd))
|
{llms_py-3.0.15/llms/extensions/computer_use → llms_py-3.0.16/llms/extensions/computer}/edit.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Annotated, Any, Literal, get_args
|
|
3
|
+
from typing import Annotated, Any, List, Literal, get_args
|
|
4
4
|
|
|
5
5
|
from .base import BaseTool, CLIResult, ToolError, ToolResult
|
|
6
6
|
from .run import maybe_truncate, run
|
|
@@ -272,10 +272,10 @@ async def edit(
|
|
|
272
272
|
command: Command_20250124,
|
|
273
273
|
path: Annotated[str, "The absolute path to the file or directory"],
|
|
274
274
|
file_text: Annotated[str | None, "The content to write to the file (required for create)"] = None,
|
|
275
|
-
view_range: Annotated[
|
|
275
|
+
view_range: Annotated[List[int], "The range of lines to view (e.g. [1, 10])"] = None,
|
|
276
276
|
old_str: Annotated[str | None, "The string to replace (required for str_replace)"] = None,
|
|
277
277
|
new_str: Annotated[str | None, "The replacement string (required for str_replace and insert)"] = None,
|
|
278
|
-
insert_line: Annotated[int
|
|
278
|
+
insert_line: Annotated[int, "The line number after which to insert (required for insert)"] = None,
|
|
279
279
|
) -> list[dict[str, Any]]:
|
|
280
280
|
"""
|
|
281
281
|
An filesystem editor tool that allows the agent to view, create, and edit files.
|
|
@@ -284,18 +284,14 @@ async def edit(
|
|
|
284
284
|
if g_tool is None:
|
|
285
285
|
g_tool = EditTool20250124()
|
|
286
286
|
|
|
287
|
-
view_range_values = None
|
|
288
|
-
if view_range:
|
|
289
|
-
view_range_values = [int(x) for x in view_range]
|
|
290
|
-
|
|
291
287
|
result = await g_tool(
|
|
292
|
-
command,
|
|
293
|
-
path if path else None,
|
|
294
|
-
file_text if file_text else None,
|
|
295
|
-
|
|
296
|
-
old_str if old_str else None,
|
|
297
|
-
new_str if new_str else None,
|
|
298
|
-
int(insert_line) if insert_line else None,
|
|
288
|
+
command=command,
|
|
289
|
+
path=path if path else None,
|
|
290
|
+
file_text=file_text if file_text else None,
|
|
291
|
+
view_range=view_range,
|
|
292
|
+
old_str=old_str if old_str else None,
|
|
293
|
+
new_str=new_str if new_str else None,
|
|
294
|
+
insert_line=int(insert_line) if insert_line else None,
|
|
299
295
|
)
|
|
300
296
|
if isinstance(result, Exception):
|
|
301
297
|
raise result
|
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Anthropic's Filesystem MCP Tools
|
|
3
|
+
https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem
|
|
4
|
+
|
|
5
|
+
filesystem
|
|
6
|
+
{
|
|
7
|
+
"command": "npx",
|
|
8
|
+
"args": [
|
|
9
|
+
"-y",
|
|
10
|
+
"@modelcontextprotocol/server-filesystem",
|
|
11
|
+
"$PWD",
|
|
12
|
+
"$LLMS_HOME/.agent"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
import difflib
|
|
19
|
+
import fnmatch
|
|
20
|
+
import logging
|
|
21
|
+
import mimetypes
|
|
22
|
+
import os
|
|
23
|
+
import shutil
|
|
24
|
+
import time
|
|
25
|
+
from typing import Annotated, Any, Dict, List, Literal, Optional
|
|
26
|
+
|
|
27
|
+
# Configure logging
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
g_ctx = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def filesystem_init(ctx):
|
|
34
|
+
global g_ctx
|
|
35
|
+
g_ctx = ctx
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_app():
|
|
39
|
+
if g_ctx is None:
|
|
40
|
+
raise RuntimeError("Filesystem extension not initialized")
|
|
41
|
+
return g_ctx
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def set_allowed_directories(directories: List[str]) -> None:
|
|
45
|
+
"""Set the list of allowed directories.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
directories: List of absolute paths that are allowed to be accessed.
|
|
49
|
+
"""
|
|
50
|
+
get_app().set_allowed_directories(directories)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def add_allowed_directory(path: str) -> None:
|
|
54
|
+
"""Add a directory to the allowed list.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
path: Absolute path to add.
|
|
58
|
+
"""
|
|
59
|
+
get_app().add_allowed_directory(path)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_allowed_directories() -> List[str]:
|
|
63
|
+
"""
|
|
64
|
+
Returns the list of directories that this server is allowed to access.
|
|
65
|
+
"""
|
|
66
|
+
return get_app().get_allowed_directories()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def list_allowed_directories() -> str:
|
|
70
|
+
"""
|
|
71
|
+
Returns the list of directories that this server is allowed to access. Subdirectories within these allowed directories are also accessible.
|
|
72
|
+
Use this to understand which directories and their nested paths are available before trying to access files.
|
|
73
|
+
"""
|
|
74
|
+
return "Allowed directories:\n" + "\n".join(get_app().get_allowed_directories())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _validate_path(path_str: str) -> str:
|
|
78
|
+
"""Validate that the path is within one of the allowed directories.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
path_str: The path to validate.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
The absolute validated path.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ValueError: If path is invalid or not allowed.
|
|
88
|
+
"""
|
|
89
|
+
if not path_str:
|
|
90
|
+
raise ValueError("Path cannot be empty")
|
|
91
|
+
|
|
92
|
+
# Expand user (~)
|
|
93
|
+
path_str = os.path.expanduser(path_str)
|
|
94
|
+
|
|
95
|
+
# Get absolute path
|
|
96
|
+
try:
|
|
97
|
+
abs_path = os.path.abspath(path_str)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
raise ValueError(f"Invalid path: {e}") from e
|
|
100
|
+
|
|
101
|
+
# Check if path is within any allowed directory
|
|
102
|
+
is_allowed = False
|
|
103
|
+
for allowed_dir in get_allowed_directories():
|
|
104
|
+
# Check if abs_path starts with allowed_dir
|
|
105
|
+
# We add os.sep to ensure we don't match /app2 when allowed is /app
|
|
106
|
+
allowed_dir_str = str(allowed_dir)
|
|
107
|
+
if not allowed_dir_str.endswith(os.sep):
|
|
108
|
+
allowed_dir_str += os.sep
|
|
109
|
+
|
|
110
|
+
if abs_path.startswith(allowed_dir_str) or abs_path == allowed_dir:
|
|
111
|
+
is_allowed = True
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
if not is_allowed:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"Access denied: {abs_path} is not within allowed directories:\n{', '.join(get_allowed_directories())}"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return abs_path
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _format_size(size_bytes: int) -> str:
|
|
123
|
+
"""Format size in bytes to human readable string."""
|
|
124
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
|
125
|
+
if size_bytes < 1024.0:
|
|
126
|
+
return f"{size_bytes:.1f} {unit}"
|
|
127
|
+
size_bytes /= 1024.0
|
|
128
|
+
return f"{size_bytes:.1f} PB"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _is_binary(path_str: str) -> bool:
|
|
132
|
+
"""Check if file is binary (rudimentary check)."""
|
|
133
|
+
try:
|
|
134
|
+
with open(path_str) as check_file:
|
|
135
|
+
check_file.read(1024)
|
|
136
|
+
return False
|
|
137
|
+
except Exception:
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def read_text_file(
|
|
142
|
+
path: Annotated[str, "Path to the file."],
|
|
143
|
+
head: Annotated[Optional[int], "If provided, returns only the first N lines of the file"] = None,
|
|
144
|
+
tail: Annotated[Optional[int], "If provided, returns only the last N lines of the file"] = None,
|
|
145
|
+
) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Read the complete contents of a file from the file system as text. Handles various text encodings and provides detailed error messages if the file cannot be read.
|
|
148
|
+
Use this tool when you need to examine the contents of a single file. Use the 'head' parameter to read only the first N lines of a file, or the 'tail' parameter to read only the last N lines of a file.
|
|
149
|
+
Operates on the file as text regardless of extension. Only works within allowed directories.
|
|
150
|
+
Returns: The content of the file.
|
|
151
|
+
"""
|
|
152
|
+
valid_path = _validate_path(path)
|
|
153
|
+
|
|
154
|
+
if head is not None and tail is not None:
|
|
155
|
+
raise ValueError("Cannot specify both head and tail parameters simultaneously")
|
|
156
|
+
|
|
157
|
+
if not os.path.exists(valid_path):
|
|
158
|
+
raise FileNotFoundError(f"File not found: {valid_path}")
|
|
159
|
+
|
|
160
|
+
if not os.path.isfile(valid_path):
|
|
161
|
+
raise ValueError(f"Path is not a file: {valid_path}")
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
with open(valid_path, encoding="utf-8", errors="replace") as f:
|
|
165
|
+
if head is not None:
|
|
166
|
+
lines = []
|
|
167
|
+
for _ in range(head):
|
|
168
|
+
line = f.readline()
|
|
169
|
+
if not line:
|
|
170
|
+
break
|
|
171
|
+
lines.append(line)
|
|
172
|
+
return "".join(lines)
|
|
173
|
+
|
|
174
|
+
if tail is not None:
|
|
175
|
+
# This could be optimized for large files but reading all lines is safer for simple impl
|
|
176
|
+
lines = f.readlines()
|
|
177
|
+
return "".join(lines[-tail:])
|
|
178
|
+
|
|
179
|
+
return f.read()
|
|
180
|
+
except Exception as e:
|
|
181
|
+
raise RuntimeError(f"Error reading file {valid_path}: {e}") from e
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def read_media_file(path: Annotated[str, "Path to the file"]) -> Dict[str, Any]:
|
|
185
|
+
"""
|
|
186
|
+
Read an image or audio file.
|
|
187
|
+
Returns the base64 encoded data and MIME type. Only works within allowed directories.
|
|
188
|
+
"""
|
|
189
|
+
valid_path = _validate_path(path)
|
|
190
|
+
|
|
191
|
+
if not os.path.exists(valid_path):
|
|
192
|
+
raise FileNotFoundError(f"File not found: {valid_path}")
|
|
193
|
+
|
|
194
|
+
mime_type, _ = mimetypes.guess_type(valid_path)
|
|
195
|
+
if not mime_type:
|
|
196
|
+
mime_type = "application/octet-stream"
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
with open(valid_path, "rb") as f:
|
|
200
|
+
data = f.read()
|
|
201
|
+
b64_data = base64.b64encode(data).decode("utf-8")
|
|
202
|
+
|
|
203
|
+
file_type = "blob"
|
|
204
|
+
if mime_type.startswith("image/"):
|
|
205
|
+
file_type = "image"
|
|
206
|
+
elif mime_type.startswith("audio/"):
|
|
207
|
+
file_type = "audio"
|
|
208
|
+
|
|
209
|
+
return {"type": file_type, "data": b64_data, "mimeType": mime_type}
|
|
210
|
+
except Exception as e:
|
|
211
|
+
raise RuntimeError(f"Error reading media file {valid_path}: {e}") from e
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def read_multiple_files(paths: Annotated[List[str], "List of file paths to read"]) -> str:
|
|
215
|
+
"""
|
|
216
|
+
Read the contents of multiple files simultaneously. This is more efficient than reading files one by one when you need to analyze or compare multiple files.
|
|
217
|
+
Each file's content is returned with its path as a reference. Failed reads for individual files won't stop the entire operation. Only works within allowed directories.
|
|
218
|
+
Returns: Concatenated contents with file separators.
|
|
219
|
+
"""
|
|
220
|
+
results = []
|
|
221
|
+
for p in paths:
|
|
222
|
+
try:
|
|
223
|
+
content = read_text_file(p)
|
|
224
|
+
results.append(f"{p}:\n{content}\n")
|
|
225
|
+
except Exception as e:
|
|
226
|
+
results.append(f"{p}: Error - {e}")
|
|
227
|
+
|
|
228
|
+
return "\n---\n".join(results)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def write_file(path: Annotated[str, "Path to the file"], content: Annotated[str, "Content to write"]) -> str:
|
|
232
|
+
"""
|
|
233
|
+
Create a new file or completely overwrite an existing file with new content. Use with caution as it will overwrite existing files without warning. Handles text content with proper encoding. Only works within allowed directories.
|
|
234
|
+
Returns: Success message.
|
|
235
|
+
"""
|
|
236
|
+
valid_path = _validate_path(path)
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
# Ensure parent directory exists
|
|
240
|
+
os.makedirs(os.path.dirname(valid_path), exist_ok=True)
|
|
241
|
+
|
|
242
|
+
with open(valid_path, "w", encoding="utf-8") as f:
|
|
243
|
+
f.write(content)
|
|
244
|
+
|
|
245
|
+
return f"Successfully wrote to {path}"
|
|
246
|
+
except Exception as e:
|
|
247
|
+
raise RuntimeError(f"Error writing to file {valid_path}: {e}") from e
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def edit_file(
|
|
251
|
+
path: Annotated[str, "Path to the file"],
|
|
252
|
+
edits: Annotated[List[Dict[str, str]], "List of dicts with 'oldText' and 'newText'"],
|
|
253
|
+
dry_run: bool = False,
|
|
254
|
+
) -> str:
|
|
255
|
+
"""
|
|
256
|
+
Make line-based edits to a text file. Each edit replaces exact line sequences with new content.
|
|
257
|
+
Returns a git-style diff showing the changes made. Only works within allowed directories.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
# Example edits: [{"oldText":"boy","newText":"girl"}]
|
|
261
|
+
|
|
262
|
+
valid_path = _validate_path(path)
|
|
263
|
+
|
|
264
|
+
if not os.path.exists(valid_path):
|
|
265
|
+
raise FileNotFoundError(f"File not found: {valid_path}")
|
|
266
|
+
|
|
267
|
+
with open(valid_path, encoding="utf-8") as f:
|
|
268
|
+
original_content = f.read()
|
|
269
|
+
|
|
270
|
+
current_content = original_content
|
|
271
|
+
|
|
272
|
+
# Apply edits sequentially
|
|
273
|
+
for edit in edits:
|
|
274
|
+
old_text = edit.get("oldText", "")
|
|
275
|
+
new_text = edit.get("newText", "")
|
|
276
|
+
|
|
277
|
+
if old_text not in current_content:
|
|
278
|
+
raise ValueError(f"Could not find exact match for text to replace: {old_text[:50]}...")
|
|
279
|
+
|
|
280
|
+
# Replace only the first occurrence to be safe?
|
|
281
|
+
# The Node impl likely replaces one instance or all?
|
|
282
|
+
# Usually exact match replacement implies replacing the instance found.
|
|
283
|
+
# Python's replace replaces all by default, so we limit to 1.
|
|
284
|
+
current_content = current_content.replace(old_text, new_text, 1)
|
|
285
|
+
|
|
286
|
+
# Generate diff
|
|
287
|
+
original_lines = original_content.splitlines(keepends=True)
|
|
288
|
+
new_lines = current_content.splitlines(keepends=True)
|
|
289
|
+
|
|
290
|
+
diff = list(difflib.unified_diff(original_lines, new_lines, fromfile=f"a/{path}", tofile=f"b/{path}", lineterm=""))
|
|
291
|
+
|
|
292
|
+
diff_text = "".join(diff)
|
|
293
|
+
|
|
294
|
+
if not dry_run:
|
|
295
|
+
with open(valid_path, "w", encoding="utf-8") as f:
|
|
296
|
+
f.write(current_content)
|
|
297
|
+
|
|
298
|
+
return diff_text
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def create_directory(path: Annotated[str, "Path to the directory"]) -> str:
|
|
302
|
+
"""
|
|
303
|
+
Create a new directory or ensure a directory exists. Can create multiple nested directories in one operation. If the directory already exists, this operation will succeed silently.
|
|
304
|
+
Perfect for setting up directory structures for projects or ensuring required paths exist. Only works within allowed directories.
|
|
305
|
+
Returns: Success message.
|
|
306
|
+
"""
|
|
307
|
+
valid_path = _validate_path(path)
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
os.makedirs(valid_path, exist_ok=True)
|
|
311
|
+
return f"Successfully created directory {path}"
|
|
312
|
+
except Exception as e:
|
|
313
|
+
raise RuntimeError(f"Error creating directory {valid_path}: {e}") from e
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def list_directory(path: Annotated[str, "Path to the directory"]) -> str:
|
|
317
|
+
"""
|
|
318
|
+
Get a detailed listing of all files and directories in a specified path. Results clearly distinguish between files and directories with [FILE] and [DIR] prefixes.
|
|
319
|
+
This tool is essential for understanding directory structure and finding specific files within a directory. Only works within allowed directories.
|
|
320
|
+
"""
|
|
321
|
+
valid_path = _validate_path(path)
|
|
322
|
+
|
|
323
|
+
if not os.path.exists(valid_path):
|
|
324
|
+
raise FileNotFoundError(f"Directory not found: {valid_path}")
|
|
325
|
+
|
|
326
|
+
if not os.path.isdir(valid_path):
|
|
327
|
+
raise ValueError(f"Path is not a directory: {valid_path}")
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
entries = sorted(os.listdir(valid_path))
|
|
331
|
+
result = []
|
|
332
|
+
for entry in entries:
|
|
333
|
+
full_path = os.path.join(valid_path, entry)
|
|
334
|
+
if os.path.isdir(full_path):
|
|
335
|
+
result.append(f"[DIR] {entry}")
|
|
336
|
+
else:
|
|
337
|
+
result.append(f"[FILE] {entry}")
|
|
338
|
+
return "\n".join(result)
|
|
339
|
+
except Exception as e:
|
|
340
|
+
raise RuntimeError(f"Error listing directory {valid_path}: {e}") from e
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def list_directory_with_sizes(
|
|
344
|
+
path: Annotated[str, "Path to the directory"],
|
|
345
|
+
sort_by: Annotated[Literal["name", "size"], "Sort by name or size"] = "name",
|
|
346
|
+
) -> str:
|
|
347
|
+
"""
|
|
348
|
+
Get a detailed listing of all files and directories in a specified path, including sizes. Results clearly distinguish between files and directories with [FILE] and [DIR] prefixes.
|
|
349
|
+
This tool is useful for understanding directory structure and finding specific files within a directory. Only works within allowed directories.
|
|
350
|
+
"""
|
|
351
|
+
valid_path = _validate_path(path)
|
|
352
|
+
|
|
353
|
+
if not os.path.exists(valid_path):
|
|
354
|
+
raise FileNotFoundError(f"Directory not found: {valid_path}")
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
entries = []
|
|
358
|
+
with os.scandir(valid_path) as it:
|
|
359
|
+
for entry in it:
|
|
360
|
+
try:
|
|
361
|
+
stats = entry.stat()
|
|
362
|
+
entries.append(
|
|
363
|
+
{"name": entry.name, "is_dir": entry.is_dir(), "size": stats.st_size, "mtime": stats.st_mtime}
|
|
364
|
+
)
|
|
365
|
+
except OSError:
|
|
366
|
+
# Skip entries we can't stat
|
|
367
|
+
continue
|
|
368
|
+
|
|
369
|
+
# Sort
|
|
370
|
+
if sort_by == "size":
|
|
371
|
+
entries.sort(key=lambda x: x["size"], reverse=True)
|
|
372
|
+
else:
|
|
373
|
+
entries.sort(key=lambda x: x["name"])
|
|
374
|
+
|
|
375
|
+
# Format
|
|
376
|
+
lines = []
|
|
377
|
+
total_files = 0
|
|
378
|
+
total_dirs = 0
|
|
379
|
+
total_size = 0
|
|
380
|
+
|
|
381
|
+
for e in entries:
|
|
382
|
+
prefix = "[DIR] " if e["is_dir"] else "[FILE]"
|
|
383
|
+
name_padded = e["name"].ljust(30)
|
|
384
|
+
size_str = "" if e["is_dir"] else _format_size(e["size"]).rjust(10)
|
|
385
|
+
lines.append(f"{prefix} {name_padded} {size_str}")
|
|
386
|
+
|
|
387
|
+
if e["is_dir"]:
|
|
388
|
+
total_dirs += 1
|
|
389
|
+
else:
|
|
390
|
+
total_files += 1
|
|
391
|
+
total_size += e["size"]
|
|
392
|
+
|
|
393
|
+
# Summary
|
|
394
|
+
lines.append("")
|
|
395
|
+
lines.append(f"Total: {total_files} files, {total_dirs} directories")
|
|
396
|
+
lines.append(f"Combined size: {_format_size(total_size)}")
|
|
397
|
+
|
|
398
|
+
return "\n".join(lines)
|
|
399
|
+
except Exception as e:
|
|
400
|
+
raise RuntimeError(f"Error listing directory {valid_path}: {e}") from e
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def directory_tree(
|
|
404
|
+
path: Annotated[str, "Path to the root directory"],
|
|
405
|
+
exclude_patterns: Annotated[List[str], "Glob patterns to exclude"] = None,
|
|
406
|
+
) -> str:
|
|
407
|
+
"""
|
|
408
|
+
Get a recursive tree view of files and directories as a JSON structure. Each entry includes 'name', 'type' (file/directory), and 'children' for directories.
|
|
409
|
+
Files have no children array, while directories always have a children array (which may be empty).
|
|
410
|
+
The output is formatted with 2-space indentation for readability. Only works within allowed directories.
|
|
411
|
+
"""
|
|
412
|
+
import json
|
|
413
|
+
|
|
414
|
+
valid_path = _validate_path(path)
|
|
415
|
+
root_path_len = len(valid_path.rstrip(os.sep)) + 1
|
|
416
|
+
if exclude_patterns is None:
|
|
417
|
+
exclude_patterns = []
|
|
418
|
+
|
|
419
|
+
def _build_tree(current_path: str) -> List[Dict[str, Any]]:
|
|
420
|
+
entries = []
|
|
421
|
+
try:
|
|
422
|
+
with os.scandir(current_path) as it:
|
|
423
|
+
items = sorted(it, key=lambda x: x.name)
|
|
424
|
+
|
|
425
|
+
for entry in items:
|
|
426
|
+
# Check exclusion
|
|
427
|
+
rel_path = entry.path[root_path_len:]
|
|
428
|
+
# Check against patterns
|
|
429
|
+
should_exclude = False
|
|
430
|
+
for pattern in exclude_patterns:
|
|
431
|
+
if fnmatch.fnmatch(rel_path, pattern) or fnmatch.fnmatch(entry.name, pattern):
|
|
432
|
+
should_exclude = True
|
|
433
|
+
break
|
|
434
|
+
if should_exclude:
|
|
435
|
+
continue
|
|
436
|
+
|
|
437
|
+
entry_data = {"name": entry.name, "type": "directory" if entry.is_dir() else "file"}
|
|
438
|
+
|
|
439
|
+
if entry.is_dir():
|
|
440
|
+
entry_data["children"] = _build_tree(entry.path)
|
|
441
|
+
|
|
442
|
+
entries.append(entry_data)
|
|
443
|
+
except OSError as e:
|
|
444
|
+
logger.warning(f"Error scanning {current_path}: {e}")
|
|
445
|
+
|
|
446
|
+
return entries
|
|
447
|
+
|
|
448
|
+
tree_data = _build_tree(valid_path)
|
|
449
|
+
return json.dumps(tree_data, indent=2)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def move_file(source: Annotated[str, "Source path"], destination: Annotated[str, "Destination path"]) -> str:
|
|
453
|
+
"""
|
|
454
|
+
Move or rename files and directories. Can move files between directories and rename them in a single operation. If the destination exists, the operation will fail.
|
|
455
|
+
Works across different directories and can be used for simple renaming within the same directory. Both source and destination must be within allowed directories.
|
|
456
|
+
"""
|
|
457
|
+
valid_source = _validate_path(source)
|
|
458
|
+
valid_dest = _validate_path(destination)
|
|
459
|
+
|
|
460
|
+
try:
|
|
461
|
+
shutil.move(valid_source, valid_dest)
|
|
462
|
+
return f"Successfully moved {source} to {destination}"
|
|
463
|
+
except Exception as e:
|
|
464
|
+
raise RuntimeError(f"Error moving {source} to {destination}: {e}") from e
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def search_files(
|
|
468
|
+
path: Annotated[str, "Path to search in"],
|
|
469
|
+
pattern: Annotated[str, "Glob pattern to match"],
|
|
470
|
+
exclude_patterns: Annotated[List[str], "Glob patterns to exclude"] = None,
|
|
471
|
+
) -> str:
|
|
472
|
+
"""
|
|
473
|
+
Recursively search for files and directories matching a pattern. The patterns should be glob-style patterns that match paths relative to the working directory.
|
|
474
|
+
Use pattern like '.ext' to match files in current directory, and '**/.ext' to match files in all subdirectories.
|
|
475
|
+
Returns full paths to all matching items. Great for finding files when you don't know their exact location. Only searches within allowed directories.
|
|
476
|
+
"""
|
|
477
|
+
valid_path = _validate_path(path)
|
|
478
|
+
results = []
|
|
479
|
+
if exclude_patterns is None:
|
|
480
|
+
exclude_patterns = []
|
|
481
|
+
|
|
482
|
+
try:
|
|
483
|
+
for root, dirs, files in os.walk(valid_path):
|
|
484
|
+
# Check exclusions for directories to prune traversal
|
|
485
|
+
dirs[:] = [d for d in dirs if not any(fnmatch.fnmatch(d, pat) for pat in exclude_patterns)]
|
|
486
|
+
|
|
487
|
+
# Check all files and directories
|
|
488
|
+
all_entries = dirs + files
|
|
489
|
+
|
|
490
|
+
for entry in all_entries:
|
|
491
|
+
full_path = os.path.join(root, entry)
|
|
492
|
+
rel_path = os.path.relpath(full_path, valid_path)
|
|
493
|
+
|
|
494
|
+
# Check if matches search pattern
|
|
495
|
+
if fnmatch.fnmatch(entry, pattern) or fnmatch.fnmatch(rel_path, pattern): # noqa: SIM102
|
|
496
|
+
# Double check exclusions (redundant for dirs but safe)
|
|
497
|
+
if not any(
|
|
498
|
+
fnmatch.fnmatch(rel_path, pat) or fnmatch.fnmatch(entry, pat) for pat in exclude_patterns
|
|
499
|
+
):
|
|
500
|
+
results.append(full_path)
|
|
501
|
+
|
|
502
|
+
except Exception as e:
|
|
503
|
+
raise RuntimeError(f"Error searching files in {valid_path}: {e}") from e
|
|
504
|
+
|
|
505
|
+
if not results:
|
|
506
|
+
return "No matches found"
|
|
507
|
+
|
|
508
|
+
return "\n".join(results)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def get_file_info(path: Annotated[str, "Path to the file"]) -> str:
|
|
512
|
+
"""
|
|
513
|
+
Retrieve detailed metadata about a file or directory.
|
|
514
|
+
Returns comprehensive information including size, creation time, last modified time, permissions, and type. This tool is perfect for understanding file characteristics without reading the actual content. Only works within allowed directories.
|
|
515
|
+
"""
|
|
516
|
+
valid_path = _validate_path(path)
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
stats = os.stat(valid_path)
|
|
520
|
+
is_dir = os.path.isdir(valid_path)
|
|
521
|
+
is_file = os.path.isfile(valid_path)
|
|
522
|
+
|
|
523
|
+
def format_date(timestamp: float) -> str:
|
|
524
|
+
return time.strftime("%a %b %d %Y %H:%M:%S GMT%z (%Z)", time.localtime(timestamp))
|
|
525
|
+
|
|
526
|
+
# Try to get birthtime, fallback to ctime
|
|
527
|
+
created_time = getattr(stats, "st_birthtime", stats.st_ctime)
|
|
528
|
+
|
|
529
|
+
info = [
|
|
530
|
+
f"size: {stats.st_size}",
|
|
531
|
+
f"created: {format_date(created_time)}",
|
|
532
|
+
f"modified: {format_date(stats.st_mtime)}",
|
|
533
|
+
f"accessed: {format_date(stats.st_atime)}",
|
|
534
|
+
f"isDirectory: {'true' if is_dir else 'false'}",
|
|
535
|
+
f"isFile: {'true' if is_file else 'false'}",
|
|
536
|
+
f"permissions: {oct(stats.st_mode)[-3:]}",
|
|
537
|
+
]
|
|
538
|
+
|
|
539
|
+
return "\n".join(info)
|
|
540
|
+
|
|
541
|
+
except Exception as e:
|
|
542
|
+
raise RuntimeError(f"Error getting file info for {valid_path}: {e}") from e
|