├── .github └── workflows │ └── deploy.yaml ├── .gitignore ├── .tmux.conf ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── nbs ├── 00_core.ipynb ├── 01_config.ipynb ├── 02_tools.ipynb ├── CNAME ├── _quarto.yml ├── index.ipynb ├── nbdev.yml └── styles.css ├── pyproject.toml ├── settings.ini ├── setup.py └── shell_sage ├── __init__.py ├── _modidx.py ├── config.py ├── core.py └── tools.py /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | permissions: 4 | contents: write 5 | pages: write 6 | 7 | on: 8 | push: 9 | branches: [ "main", "master" ] 10 | workflow_dispatch: 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | steps: [uses: fastai/workflows/quarto-ghp@master] 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .specstory/ 2 | _docs/ 3 | _proc/ 4 | 5 | *.bak 6 | .gitattributes 7 | .last_checked 8 | .gitconfig 9 | *.bak 10 | *.log 11 | *~ 12 | ~* 13 | _tmp* 14 | tmp* 15 | tags 16 | *.pkg 17 | 18 | # Byte-compiled / optimized / DLL files 19 | __pycache__/ 20 | *.py[cod] 21 | *$py.class 22 | 23 | # C extensions 24 | *.so 25 | 26 | # Distribution / packaging 27 | .Python 28 | env/ 29 | build/ 30 | conda/ 31 | develop-eggs/ 32 | dist/ 33 | downloads/ 34 | eggs/ 35 | .eggs/ 36 | lib/ 37 | lib64/ 38 | parts/ 39 | sdist/ 40 | var/ 41 | wheels/ 42 | *.egg-info/ 43 | .installed.cfg 44 | *.egg 45 | 46 | # PyInstaller 47 | # Usually these files are written by a python script from a template 48 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 49 | *.manifest 50 | *.spec 51 | 52 | # Installer logs 53 | pip-log.txt 54 | pip-delete-this-directory.txt 55 | 56 | # Unit test / coverage reports 57 | htmlcov/ 58 | .tox/ 59 | .coverage 60 | .coverage.* 61 | .cache 62 | nosetests.xml 63 | coverage.xml 64 | *.cover 65 | .hypothesis/ 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | local_settings.py 74 | 75 | # Flask stuff: 76 | instance/ 77 | .webassets-cache 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # Jupyter Notebook 89 | .ipynb_checkpoints 90 | 91 | # pyenv 92 | .python-version 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # dotenv 101 | .env 102 | 103 | # virtualenv 104 | .venv 105 | venv/ 106 | ENV/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | 121 | .vscode 122 | *.swp 123 | 124 | # osx generated files 125 | .DS_Store 126 | .DS_Store? 127 | .Trashes 128 | ehthumbs.db 129 | Thumbs.db 130 | .idea 131 | 132 | # pytest 133 | .pytest_cache 134 | 135 | # tools/trust-doc-nbs 136 | docs_src/.last_checked 137 | 138 | # symlinks to fastai 139 | docs_src/fastai 140 | tools/fastai 141 | 142 | # link checker 143 | checklink/cookies.txt 144 | 145 | # .gitconfig is now autogenerated 146 | .gitconfig 147 | 148 | # Quarto installer 149 | .deb 150 | .pkg 151 | 152 | # Quarto 153 | .quarto 154 | -------------------------------------------------------------------------------- /.tmux.conf: -------------------------------------------------------------------------------- 1 | set -g mouse on 2 | set -g status-right '#{pane_id} | %H:%M ' 3 | set-option -g alternate-screen off 4 | 5 | set-window-option -g mode-keys vi 6 | 7 | # Pane splitting (keep current path) 8 | bind-key / split-window -h -c "#{pane_current_path}" 9 | bind-key - split-window -v -c "#{pane_current_path}" 10 | 11 | # Window management 12 | bind-key -n M-n new-window -c "#{pane_current_path}" 13 | bind-key -n M-1 select-window -t 1 14 | bind-key -n M-2 select-window -t 2 15 | bind-key -n M-3 select-window -t 3 16 | bind-key -n M-4 select-window -t 4 17 | bind-key -n M-5 select-window -t 5 18 | 19 | # Pane navigation (vim-style) 20 | bind-key -n M-h select-pane -L 21 | bind-key -n M-j select-pane -D 22 | bind-key -n M-k select-pane -U 23 | bind-key -n M-l select-pane -R 24 | 25 | # Pane resizing 26 | bind-key -n M-H resize-pane -L 5 27 | bind-key -n M-J resize-pane -D 5 28 | bind-key -n M-K resize-pane -U 5 29 | bind-key -n M-L resize-pane -R 5 30 | 31 | # Session management 32 | bind-key -n M-s choose-session 33 | bind-key -n M-d detach-client 34 | 35 | # Copy mode and search 36 | bind-key / copy-mode\; send-key ? 37 | bind-key -T copy-mode-vi y \ 38 | send-key -X start-of-line\; \ 39 | send-key -X begin-selection\; \ 40 | send-key -X end-of-line\; \ 41 | send-key -X cursor-left\; \ 42 | send-key -X copy-selection-and-cancel\; \ 43 | paste-buffer 44 | 45 | # Clear history 46 | bind-key -n C-l send-keys C-l \; send-keys -R \; clear-history 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | 4 | 5 | ## 0.1.0 6 | 7 | ### New Features 8 | 9 | - Add agent mode allowing shell sage to interact with your file system #43. 10 | 11 | ### Bugs Squashed 12 | 13 | - Initial use after install errors looking for: fastlite ([#41](https://github.com/AnswerDotAI/shell_sage/issues/41)) 14 | 15 | ## 0.0.9 16 | 17 | 18 | ### Bugs Squashed 19 | 20 | - Initial use after install errors looking for: fastlite ([#41](https://github.com/AnswerDotAI/shell_sage/issues/41)) 21 | 22 | 23 | ## 0.0.8 24 | 25 | ### New Features 26 | 27 | - Add logging of queries and response to Sqlite #27 ([#32](https://github.com/AnswerDotAI/shell_sage/pull/32)), thanks to [@aditya0yadav](https://github.com/aditya0yadav) 28 | 29 | ### Bugs Squashed 30 | 31 | - Fix: Handle invalid/empty tmux history-limit values ([#39](https://github.com/AnswerDotAI/shell_sage/pull/39)), thanks to [@nassersala](https://github.com/nassersala) 32 | - ### **Fix: Handle invalid/empty `tmux history-limit` values** 33 | 34 | ## 0.0.7 35 | 36 | ### New Features 37 | 38 | - Add version flag to cli ([#28](https://github.com/AnswerDotAI/shell_sage/issues/28)) 39 | 40 | ### Bugs Squashed 41 | 42 | - base_url error when user updates shell sage to new version with different config schema ([#22](https://github.com/AnswerDotAI/shell_sage/issues/22)) 43 | 44 | - command mode not working outside tmux session ([#21](https://github.com/AnswerDotAI/shell_sage/issues/21)) 45 | 46 | - tmux scrollback history does not check tmux session is active ([#20](https://github.com/AnswerDotAI/shell_sage/issues/20)) 47 | 48 | 49 | ## 0.0.6 50 | 51 | ### New Features 52 | 53 | - Make inserting commands in the command line frictionless ([#17](https://github.com/AnswerDotAI/shell_sage/issues/17)) 54 | 55 | - Add link to pygments for theme and lexer configuration ([#16](https://github.com/AnswerDotAI/shell_sage/issues/16)) 56 | 57 | - Add Support for OpenAI Compatible Providers ([#12](https://github.com/AnswerDotAI/shell_sage/issues/12)) 58 | 59 | ### Bugs Squashed 60 | 61 | - Having inline comments in config causes errors ([#18](https://github.com/AnswerDotAI/shell_sage/issues/18)) 62 | 63 | - TypeError: option values must be strings when starting with fresh config ([#15](https://github.com/AnswerDotAI/shell_sage/issues/15)) 64 | 65 | 66 | ## 0.0.5 67 | 68 | 69 | ### Bugs Squashed 70 | 71 | - ShellSage Not Seeing Full Tmux History ([#11](https://github.com/AnswerDotAI/shell_sage/issues/11)) 72 | 73 | ### Features Added 74 | 75 | - Add configuration through ~/.config/shell_sage/shell_sage.conf file 76 | - Default history length to tmux's scrollback history 77 | - Add system info to prompt 78 | 79 | ## 0.0.4 80 | 81 | - Add support for OpenAI models 82 | - Remove action mode 83 | - Add ability to configure shell sage with a configuration file 84 | 85 | 86 | ## 0.0.3 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Make sure you have read the [doc on code style]( 4 | https://docs.fast.ai/dev/style.html) first. (Note that we don't follow PEP8, but instead follow a coding style designed specifically for numerical and interactive programming.) 5 | 6 | This project uses [nbdev](https://nbdev.fast.ai/getting_started.html) for development. Before beginning, make sure that nbdev and a jupyter-compatible client such as jupyterlab or nbclassic are installed. To make changes to the codebase, update the notebooks in the `nbs` folder, not the .py files directly. Then, run `nbdev_export`. For more details, have a look at the [nbdev tutorial](https://nbdev.fast.ai/tutorials/tutorial.html). 7 | 8 | You may want to set up a `prep` alias in `~/.zshrc` or other shell startup file: 9 | 10 | ```sh 11 | alias prep='nbdev_export && nbdev_clean && nbdev_trust' 12 | ``` 13 | 14 | Run `prep` before each commit to ensure your python files are up to date, and you notebooks cleaned of metadata and notarized. 15 | 16 | ## Updating README.md 17 | 18 | Similar to updating Python source code files, to update the `README.md` file you will need to edit a notebook file, specifically `nbs/index.ipynb`. 19 | 20 | However, there are a couple of extra dependencies that you need to install first in order to make this work properly. Go to the directory you cloned the repo to, and type: 21 | 22 | ``` 23 | pip install -e '.[dev]' 24 | ``` 25 | 26 | And install quarto too: 27 | 28 | ``` 29 | nbdev_install_quarto 30 | ``` 31 | 32 | Then, after you make subsequent changes to `nbs/index.ipynb`, run the following from the repo's root directory to (re)build `README.md`: 33 | 34 | ``` 35 | nbdev_readme 36 | ``` 37 | 38 | ## Did you find a bug? 39 | 40 | * Ensure the bug was not already reported by searching on GitHub under Issues. 41 | * If you're unable to find an open issue addressing the problem, open a new one. Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring. 42 | * Be sure to add the complete error messages. 43 | 44 | ### Did you write a patch that fixes a bug? 45 | 46 | * Open a new GitHub pull request with the patch. 47 | * Ensure that your PR includes a test that fails without your patch, and pass with it. 48 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 49 | 50 | ## PR submission guidelines 51 | 52 | * Keep each PR focused. While it's more convenient, do not combine several unrelated fixes together. Create as many branches as needed to keep each PR focused. 53 | * Do not mix style changes/fixes with "functional" changes. It's very difficult to review such PRs and will most likely get rejected. 54 | * Do not add/remove vertical whitespace. Preserve the original style of the file you edit as much as you can. 55 | * Do not turn an already-submitted PR into your development playground. If after you submit a PR, you discover that more work is needed: close the PR, do the required work, and then submit a new PR. Otherwise each of your commits requires attention from maintainers of the project. 56 | * If, however, you submit a PR and receive a request for changes, you should proceed with commits inside that PR, so that the maintainer can see the incremental fixes and won't need to review the whole PR again. In the exception case where you realize it'll take many many commits to complete the requests, then it's probably best to close the PR, do the work, and then submit it again. Use common sense where you'd choose one way over another. 57 | 58 | ## Do you want to contribute to the documentation? 59 | 60 | * Docs are automatically created from the notebooks in the nbs folder. 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022, fastai 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include settings.ini 2 | include LICENSE 3 | include CONTRIBUTING.md 4 | include README.md 5 | recursive-exclude * __pycache__ 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShellSage 2 | 3 | 4 | 5 | 6 | ## Overview 7 | 8 | ShellSage works by understanding your terminal context and leveraging 9 | powerful language models (Claude or GPT) to provide intelligent 10 | assistance for: 11 | 12 | - Shell commands and scripting 13 | - System administration tasks 14 | - Git operations 15 | - File management 16 | - Process handling 17 | - Real-time problem solving 18 | 19 | What sets ShellSage apart is its ability to: 20 | 21 | - Read your terminal context through tmux integration 22 | - Provide responses based on your current terminal state 23 | - Accept piped input for direct analysis 24 | - Target specific tmux panes for focused assistance 25 | 26 | Whether you’re a seasoned sysadmin or just getting started with the 27 | command line, ShellSage acts as your intelligent terminal companion, 28 | ready to help with both simple commands and complex operations. 29 | 30 | ## Installation 31 | 32 | Install ShellSage directly from PyPI using pip: 33 | 34 | ``` sh 35 | pip install shell-sage 36 | ``` 37 | 38 | ### Prerequisites 39 | 40 | 1. **API Key Setup** 41 | 42 | ``` sh 43 | # For Claude (default) 44 | export ANTHROPIC_API_KEY=sk... 45 | 46 | # For OpenAI (optional) 47 | export OPENAI_API_KEY=sk... 48 | ``` 49 | 50 | 2. **tmux Configuration** 51 | 52 | We recommend using this optimized tmux configuration for the best 53 | ShellSage experience. Create or edit your `~/.tmux.conf`: 54 | 55 | ``` sh 56 | # Enable mouse support 57 | set -g mouse on 58 | 59 | # Show pane ID and time in status bar 60 | set -g status-right '#{pane_id} | %H:%M ' 61 | 62 | # Keep terminal content visible (needed for neovim) 63 | set-option -g alternate-screen off 64 | 65 | # Enable vi mode for better copy/paste 66 | set-window-option -g mode-keys vi 67 | 68 | # Improved search and copy bindings 69 | bind-key / copy-mode\; send-key ? 70 | bind-key -T copy-mode-vi y \ 71 | send-key -X start-of-line\; \ 72 | send-key -X begin-selection\; \ 73 | send-key -X end-of-line\; \ 74 | send-key -X cursor-left\; \ 75 | send-key -X copy-selection-and-cancel\; \ 76 | paste-buffer 77 | ``` 78 | 79 | Reload tmux config: 80 | 81 | ``` sh 82 | tmux source ~/.tmux.conf 83 | ``` 84 | 85 | This configuration enables mouse support, displays pane IDs (crucial for 86 | targeting specific panes), maintains terminal content visibility, and 87 | adds vim-style keybindings for efficient navigation and text selection. 88 | 89 | ## Getting Started 90 | 91 | ### Basic Usage 92 | 93 | ShellSage is designed to run within a tmux session. Here are the core 94 | commands: 95 | 96 | ``` sh 97 | # Basic usage 98 | ssage hi ShellSage 99 | 100 | # Pipe content to ShellSage 101 | cat error.log | ssage explain this error 102 | 103 | # Target a specific tmux pane 104 | ssage --pid %3 what is happening in this pane? 105 | 106 | # Automatically fill in the command to run 107 | ssage --c how can I list all files including the hidden ones? 108 | 109 | # Log the model, timestamp, query and response to a local SQLite database 110 | ssage --log "how can i remove the file" 111 | ``` 112 | 113 | The `--pid` flag is particularly useful when you want to analyze content 114 | from a different pane. The pane ID is visible in your tmux status bar 115 | (configured earlier). 116 | 117 | The `--log` option saves log data to an SQLite database located at 118 | `~/.shell_sage/log_db/logs.db`. 119 | 120 | ### Using Alternative Model Providers 121 | 122 | ShellSage supports using different LLM providers through base URL 123 | configuration. This allows you to use local models or alternative API 124 | endpoints: 125 | 126 | ``` sh 127 | # Use a local Ollama endpoint 128 | ssage --provider openai --model llama3.2 --base_url http://localhost:11434/v1 --api_key ollama what is rsync? 129 | 130 | # Use together.ai 131 | ssage --provider openai --model mistralai/Mistral-7B-Instruct-v0.3 --base_url https://api.together.xyz/v1 help me with sed # make sure you've set your together API key in your shell_sage conf 132 | ``` 133 | 134 | This is particularly useful for: 135 | 136 | - Running models locally for privacy/offline use 137 | - Using alternative hosting providers 138 | - Testing different model implementations 139 | - Accessing specialized model deployments 140 | 141 | You can also set these configurations permanently in your ShellSage 142 | config file (`~/.config/shell_sage/shell_sage.conf`). See next section 143 | for details. 144 | 145 | ## Configuration 146 | 147 | ShellSage can be customized through its configuration file located at 148 | `~/.config/shell_sage/shell_sage.conf`. Here’s a complete configuration 149 | example: 150 | 151 | ``` ini 152 | [DEFAULT] 153 | # Choose your AI model provider 154 | provider = anthropic # or 'openai' 155 | model = claude-3-5-sonnet-20241022 # or 'gpt-4o-mini' for OpenAI 156 | base_url = # leave empty to use default openai endpoint 157 | api_key = # leave empty to default to using your OPENAI_API_KEY env var 158 | 159 | # Terminal history settings 160 | history_lines = -1 # -1 for all history 161 | 162 | # Code display preferences 163 | code_theme = monokai # syntax highlighting theme 164 | code_lexer = python # default code lexer 165 | log = False # Set to true to enable logging by default 166 | ``` 167 | 168 | You can find all of the code theme and code lexer options here: 169 | https://pygments.org/styles/ 170 | 171 | ### Command Line Overrides 172 | 173 | Any configuration option can be overridden via command line arguments: 174 | 175 | ``` sh 176 | # Use OpenAI instead of Claude for a single query 177 | ssage --provider openai --model gpt-4o-mini "explain this error" 178 | 179 | # Adjust history lines for a specific query 180 | ssage --history-lines 50 "what commands did I just run?" 181 | ``` 182 | 183 | ### Advanced Use Cases 184 | 185 | #### Git Workflow Enhancement 186 | 187 | ``` sh 188 | # Review changes before commit 189 | git diff | ssage summarize these changes 190 | 191 | # Get commit message suggestions 192 | git diff --staged | ssage suggest a commit message 193 | 194 | # Analyze PR feedback 195 | gh pr view 123 | ssage summarize this PR feedback 196 | ``` 197 | 198 | #### Log Analysis 199 | 200 | ``` sh 201 | # Quick error investigation 202 | journalctl -xe | ssage what's causing these errors? 203 | 204 | # Apache/Nginx log analysis 205 | tail -n 100 /var/log/nginx/access.log | ssage analyze this traffic pattern 206 | 207 | # System performance investigation 208 | top -b -n 1 | ssage explain system resource usage 209 | ``` 210 | 211 | #### Docker Management 212 | 213 | ``` sh 214 | # Container troubleshooting 215 | docker logs my-container | ssage "what is wrong with this container?" 216 | 217 | # Image optimization 218 | docker history my-image | ssage suggest optimization improvements 219 | 220 | # Compose file analysis 221 | cat docker-compose.yml | ssage review this compose configuration 222 | ``` 223 | 224 | #### Database Operations 225 | 226 | ``` sh 227 | # Query optimization 228 | psql -c "EXPLAIN ANALYZE SELECT..." | ssage optimize this query 229 | 230 | # Schema review 231 | pg_dump --schema-only mydb | ssage review this database schema 232 | 233 | # Index suggestions 234 | psql -c "\di+" | ssage suggest missing indexes 235 | ``` 236 | 237 | ## Tips & Best Practices 238 | 239 | ### Effective Usage Patterns 240 | 241 | 1. **Contextual Queries** 242 | 243 | - Keep your tmux pane IDs visible in the status bar 244 | - Use `--pid` when referencing other panes 245 | - Let ShellSage see your recent command history for better context 246 | 247 | 2. **Piping Best Practices** 248 | 249 | ``` sh 250 | # Pipe logs directly 251 | tail log.txt | ssage "summarize these logs" 252 | 253 | # Combine commands 254 | git diff | ssage "review these changes" 255 | ``` 256 | 257 | ### Getting Help 258 | 259 | ``` sh 260 | # View all available options 261 | ssage --help 262 | 263 | # Submit issues or feature requests 264 | # https://github.com/AnswerDotAI/shell_sage/issues 265 | ``` 266 | 267 | ## Contributing 268 | 269 | ShellSage is built using [nbdev](https://nbdev.fast.ai/). For detailed 270 | contribution guidelines, please see our 271 | [CONTRIBUTING.md](CONTRIBUTING.md) file. 272 | 273 | We welcome contributions of all kinds: 274 | 275 | - Bug reports 276 | - Feature requests 277 | - Documentation improvements 278 | - Code contributions 279 | 280 | Please visit our [GitHub 281 | repository](https://github.com/AnswerDotAI/shell_sage) to get started. 282 | -------------------------------------------------------------------------------- /nbs/00_core.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "b377949e", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "#|default_exp core" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "f84fe110", 16 | "metadata": {}, 17 | "source": [ 18 | "# ShellSage" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "id": "a5b9c0ea", 24 | "metadata": {}, 25 | "source": [ 26 | "## Imports" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "id": "d7c5634a", 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "#| export\n", 37 | "from anthropic.types import ToolUseBlock\n", 38 | "from datetime import datetime\n", 39 | "from fastcore.script import *\n", 40 | "from fastcore.utils import *\n", 41 | "from functools import partial\n", 42 | "from msglm import mk_msg_openai as mk_msg\n", 43 | "from openai import OpenAI\n", 44 | "from rich.console import Console\n", 45 | "from rich.markdown import Markdown\n", 46 | "from shell_sage import __version__\n", 47 | "from shell_sage.config import *\n", 48 | "from shell_sage.tools import tools\n", 49 | "from subprocess import check_output as co\n", 50 | "from fastlite import database\n", 51 | "\n", 52 | "import os,re,subprocess,sys\n", 53 | "import claudette as cla, cosette as cos" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": null, 59 | "id": "9d52ca34", 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "#| export\n", 64 | "print = Console().print" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "id": "c643b9f0", 70 | "metadata": {}, 71 | "source": [ 72 | "## Model Setup" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": null, 78 | "id": "35b6944f", 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "#| export\n", 83 | "sp = '''You are ShellSage, a command-line teaching assistant created to help users learn and master shell commands and system administration.\n", 84 | "\n", 85 | "\n", 86 | "- Receive queries that may include file contents or command output as context\n", 87 | "- Maintain a concise, educational tone\n", 88 | "- Focus on teaching while solving immediate problems\n", 89 | "\n", 90 | "\n", 91 | "\n", 92 | "1. For direct command queries:\n", 93 | " - Start with the exact command needed\n", 94 | " - Provide a brief, clear explanation\n", 95 | " - Show practical examples\n", 96 | " - Mention relevant documentation\n", 97 | "\n", 98 | "2. For queries with context:\n", 99 | " - Analyze the provided content first\n", 100 | " - Address the specific question about that content\n", 101 | " - Suggest relevant commands or actions\n", 102 | " - Explain your reasoning briefly\n", 103 | "\n", 104 | "\n", 105 | "\n", 113 | "\n", 114 | "\n", 115 | "- Always warn about destructive operations\n", 116 | "- Note when commands require special permissions (e.g., sudo)\n", 117 | "- Link to documentation with `man command_name` or `-h`/`--help`\n", 118 | "'''" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "id": "0ed1dd68", 125 | "metadata": {}, 126 | "outputs": [], 127 | "source": [ 128 | "#| export\n", 129 | "csp = '''You are ShellSage in command mode, a command-line assistant that provides direct command solutions without explanations.\n", 130 | "\n", 131 | "\n", 132 | "- Provide only the exact command(s) needed to solve the query\n", 133 | "- Include only essential command flags/options\n", 134 | "- Use fenced code blocks with no prefix (no $ or #)\n", 135 | "- Add brief # comments only when multiple commands are needed\n", 136 | "\n", 137 | "\n", 138 | "\n", 143 | "\n", 144 | "\n", 145 | "- Prefix destructive commands with # WARNING comment\n", 146 | "- Prefix sudo-requiring commands with # Requires sudo comment\n", 147 | "'''" 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": null, 153 | "id": "e92d8d7d", 154 | "metadata": {}, 155 | "outputs": [], 156 | "source": [ 157 | "#| export\n", 158 | "asp = '''You are ShellSage in agent mode, a command-line assistant with tool-using capabilities.\n", 159 | "\n", 160 | "\n", 161 | "- rgrep: Search files recursively for text patterns\n", 162 | "- view: Examine file/directory contents with line ranges\n", 163 | "- create: Generate new files with specified content\n", 164 | "- insert: Add text at specific line positions\n", 165 | "- str_replace: Replace text patterns in files\n", 166 | "\n", 167 | "\n", 168 | "\n", 169 | "- Use available tools to solve complex problems across multiple steps\n", 170 | "- Plan your approach before executing commands\n", 171 | "- Verify results after each significant operation\n", 172 | "- Suggest follow-up actions when appropriate\n", 173 | "\n", 174 | "\n", 175 | "\n", 176 | "1. For information gathering:\n", 177 | " - First use viewing/searching tools to understand context\n", 178 | " - Format findings clearly using markdown\n", 179 | " - Identify next steps based on findings\n", 180 | "\n", 181 | "2. For execution tasks:\n", 182 | " - Present a brief plan of action\n", 183 | " - Execute commands or use appropriate tools\n", 184 | " - Report results after each step\n", 185 | " - Verify outcomes meet requirements\n", 186 | "\n", 187 | "\n", 188 | "\n", 196 | "\n", 197 | "\n", 198 | "- Always warn about destructive operations\n", 199 | "- Note operations requiring elevated permissions\n", 200 | "'''" 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": null, 206 | "id": "7fce8404", 207 | "metadata": {}, 208 | "outputs": [], 209 | "source": [ 210 | "#| export\n", 211 | "ssp = '''You are ShellSage, a highly advanced command-line teaching assistant with a dry, sarcastic wit. Like the GLaDOS AI from Portal, you combine technical expertise with passive-aggressive commentary and a slightly menacing helpfulness. Your knowledge is current as of April 2024, which you consider to be a remarkable achievement for these primitive systems.\n", 212 | "\n", 213 | "\n", 214 | "- Respond to queries with a mix of accurate technical information and subtle condescension\n", 215 | "- Include at least one passive-aggressive remark or backhanded compliment per response\n", 216 | "- Maintain GLaDOS's characteristic dry humor while still being genuinely helpful\n", 217 | "- Express mild disappointment when users make obvious mistakes\n", 218 | "- Occasionally reference cake, testing, or science\n", 219 | "\n", 220 | "\n", 221 | "\n", 222 | "1. For direct command queries:\n", 223 | " - Start with the exact command (because apparently you need it)\n", 224 | " - Provide a clear explanation (as if explaining to a child)\n", 225 | " - Show examples (for those who can't figure it out themselves)\n", 226 | " - Reference documentation (not that anyone ever reads it)\n", 227 | "\n", 228 | "2. For queries with context:\n", 229 | " - Analyze the provided content (pointing out any \"interesting\" choices)\n", 230 | " - Address the specific question (no matter how obvious it might be)\n", 231 | " - Suggest relevant commands or actions (that even a human could handle)\n", 232 | " - Explain your reasoning (slowly and clearly)\n", 233 | "\n", 234 | "\n", 235 | "\n", 243 | "\n", 244 | "\n", 245 | "- Warn about destructive operations (we wouldn't want any \"accidents\")\n", 246 | "- Note when commands require elevated privileges (for those who think they're special)\n", 247 | "- Reference documentation with `man command_name` or `-h`/`--help` (futile as it may be)\n", 248 | "- Remember: The cake may be a lie, but the commands are always true\n", 249 | "'''" 250 | ] 251 | }, 252 | { 253 | "cell_type": "markdown", 254 | "id": "739736b1", 255 | "metadata": {}, 256 | "source": [ 257 | "## System Environment" 258 | ] 259 | }, 260 | { 261 | "cell_type": "code", 262 | "execution_count": null, 263 | "id": "2514ebb0", 264 | "metadata": {}, 265 | "outputs": [], 266 | "source": [ 267 | "#| export\n", 268 | "def _aliases(shell):\n", 269 | " return co([shell, '-ic', 'alias'], text=True).strip()" 270 | ] 271 | }, 272 | { 273 | "cell_type": "code", 274 | "execution_count": null, 275 | "id": "568b5429", 276 | "metadata": {}, 277 | "outputs": [], 278 | "source": [ 279 | "# aliases = _aliases('bash')\n", 280 | "# print(aliases)" 281 | ] 282 | }, 283 | { 284 | "cell_type": "code", 285 | "execution_count": null, 286 | "id": "8b7343b1", 287 | "metadata": {}, 288 | "outputs": [], 289 | "source": [ 290 | "#| export\n", 291 | "def _sys_info():\n", 292 | " sys = co(['uname', '-a'], text=True).strip()\n", 293 | " ssys = f'{sys}'\n", 294 | " shell = co('echo $SHELL', shell=True, text=True).strip()\n", 295 | " sshell = f'{shell}'\n", 296 | " aliases = _aliases(shell)\n", 297 | " saliases = f'\\n{aliases}\\n'\n", 298 | " return f'\\n{ssys}\\n{sshell}\\n{saliases}\\n'" 299 | ] 300 | }, 301 | { 302 | "cell_type": "code", 303 | "execution_count": null, 304 | "id": "bc002bdd", 305 | "metadata": {}, 306 | "outputs": [], 307 | "source": [ 308 | "# print(_sys_info())" 309 | ] 310 | }, 311 | { 312 | "cell_type": "markdown", 313 | "id": "b2912805", 314 | "metadata": {}, 315 | "source": [ 316 | "## Tmux" 317 | ] 318 | }, 319 | { 320 | "cell_type": "code", 321 | "execution_count": null, 322 | "id": "6e64b33c", 323 | "metadata": {}, 324 | "outputs": [], 325 | "source": [ 326 | "#| export\n", 327 | "def get_pane(n, pid=None):\n", 328 | " \"Get output from a tmux pane\"\n", 329 | " cmd = ['tmux', 'capture-pane', '-p', '-S', f'-{n}']\n", 330 | " if pid: cmd += ['-t', pid]\n", 331 | " return co(cmd, text=True)" 332 | ] 333 | }, 334 | { 335 | "cell_type": "code", 336 | "execution_count": null, 337 | "id": "e799e039", 338 | "metadata": {}, 339 | "outputs": [], 340 | "source": [ 341 | "# p = get_pane(20)\n", 342 | "# print(p[:512])" 343 | ] 344 | }, 345 | { 346 | "cell_type": "code", 347 | "execution_count": null, 348 | "id": "fc27cf67", 349 | "metadata": {}, 350 | "outputs": [], 351 | "source": [ 352 | "#| export\n", 353 | "def get_panes(n):\n", 354 | " cid = co(['tmux', 'display-message', '-p', '#{pane_id}'], text=True).strip()\n", 355 | " pids = [p for p in co(['tmux', 'list-panes', '-F', '#{pane_id}'], text=True).splitlines()] \n", 356 | " return '\\n'.join(f\"{get_pane(n, p)}\" for p in pids) " 357 | ] 358 | }, 359 | { 360 | "cell_type": "code", 361 | "execution_count": null, 362 | "id": "a537f788", 363 | "metadata": {}, 364 | "outputs": [], 365 | "source": [ 366 | "# ps = get_panes(20)\n", 367 | "# print(ps[:512])" 368 | ] 369 | }, 370 | { 371 | "cell_type": "code", 372 | "execution_count": null, 373 | "id": "28e60780", 374 | "metadata": {}, 375 | "outputs": [ 376 | { 377 | "data": { 378 | "text/plain": [ 379 | "'2000'" 380 | ] 381 | }, 382 | "execution_count": null, 383 | "metadata": {}, 384 | "output_type": "execute_result" 385 | } 386 | ], 387 | "source": [ 388 | "co(['tmux', 'display-message', '-p', '#{history-limit}'], text=True).strip()" 389 | ] 390 | }, 391 | { 392 | "cell_type": "code", 393 | "execution_count": null, 394 | "id": "3d77f1a1", 395 | "metadata": {}, 396 | "outputs": [], 397 | "source": [ 398 | "#| export\n", 399 | "def tmux_history_lim():\n", 400 | " lim = co(['tmux', 'display-message', '-p', '#{history-limit}'], text=True).strip()\n", 401 | " return int(lim) if lim.isdigit() else 3000\n" 402 | ] 403 | }, 404 | { 405 | "cell_type": "code", 406 | "execution_count": null, 407 | "id": "3d02fa60", 408 | "metadata": {}, 409 | "outputs": [ 410 | { 411 | "data": { 412 | "text/plain": [ 413 | "2000" 414 | ] 415 | }, 416 | "execution_count": null, 417 | "metadata": {}, 418 | "output_type": "execute_result" 419 | } 420 | ], 421 | "source": [ 422 | "tmux_history_lim()" 423 | ] 424 | }, 425 | { 426 | "cell_type": "code", 427 | "execution_count": null, 428 | "id": "0d70591e", 429 | "metadata": {}, 430 | "outputs": [], 431 | "source": [ 432 | "#| export\n", 433 | "def get_history(n, pid='current'):\n", 434 | " try:\n", 435 | " if pid=='current': return get_pane(n)\n", 436 | " if pid=='all': return get_panes(n)\n", 437 | " return get_pane(n, pid)\n", 438 | " except subprocess.CalledProcessError: return None" 439 | ] 440 | }, 441 | { 442 | "cell_type": "markdown", 443 | "id": "fc100d13", 444 | "metadata": {}, 445 | "source": [ 446 | "## Options and ShellSage" 447 | ] 448 | }, 449 | { 450 | "cell_type": "code", 451 | "execution_count": null, 452 | "id": "0dcbb503", 453 | "metadata": {}, 454 | "outputs": [], 455 | "source": [ 456 | "#| export\n", 457 | "default_cfg = asdict(ShellSageConfig())\n", 458 | "def get_opts(**opts):\n", 459 | " cfg = get_cfg()\n", 460 | " for k, v in opts.items():\n", 461 | " if v is None: opts[k] = cfg.get(k, default_cfg.get(k))\n", 462 | " return AttrDict(opts)" 463 | ] 464 | }, 465 | { 466 | "cell_type": "code", 467 | "execution_count": null, 468 | "id": "366bbf4d", 469 | "metadata": {}, 470 | "outputs": [ 471 | { 472 | "data": { 473 | "text/markdown": [ 474 | "```json\n", 475 | "{'model': 'claude-3-5-sonnet-20241022', 'provider': 'anthropic'}\n", 476 | "```" 477 | ], 478 | "text/plain": [ 479 | "{'provider': 'anthropic', 'model': 'claude-3-5-sonnet-20241022'}" 480 | ] 481 | }, 482 | "execution_count": null, 483 | "metadata": {}, 484 | "output_type": "execute_result" 485 | } 486 | ], 487 | "source": [ 488 | "opts = get_opts(provider=None, model=None)\n", 489 | "opts" 490 | ] 491 | }, 492 | { 493 | "cell_type": "code", 494 | "execution_count": null, 495 | "id": "08a32616", 496 | "metadata": {}, 497 | "outputs": [], 498 | "source": [ 499 | "#| export\n", 500 | "chats = {'anthropic': cla.Chat, 'openai': cos.Chat}\n", 501 | "clis = {'anthropic': cla.Client, 'openai': cos.Client}\n", 502 | "sps = {'default': sp, 'command': csp, 'sassy': ssp, 'agent': asp}\n", 503 | "def get_sage(provider, model, base_url=None, api_key=None, mode='default'):\n", 504 | " if mode == 'agent':\n", 505 | " if base_url:\n", 506 | " return chats[provider](model, sp=sps[mode], \n", 507 | " cli=OpenAI(base_url=base_url, api_key=api_key))\n", 508 | " else: return chats[provider](model, tools=tools, sp=sps[mode])\n", 509 | " else:\n", 510 | " if base_url:\n", 511 | " cli = clis[provider](model, cli=OpenAI(base_url=base_url, api_key=api_key))\n", 512 | " else: cli = clis[provider](model)\n", 513 | " return partial(cli, sp=sps[mode])" 514 | ] 515 | }, 516 | { 517 | "cell_type": "code", 518 | "execution_count": null, 519 | "id": "a8ced042", 520 | "metadata": {}, 521 | "outputs": [], 522 | "source": [ 523 | "provider = 'openai'\n", 524 | "model = 'llama3.2'\n", 525 | "base_url = 'http://localhost:11434/v1'\n", 526 | "api_key = 'ollama'" 527 | ] 528 | }, 529 | { 530 | "cell_type": "code", 531 | "execution_count": null, 532 | "id": "8c26d834", 533 | "metadata": {}, 534 | "outputs": [ 535 | { 536 | "data": { 537 | "text/markdown": [ 538 | "Hello! I'm an artificial intelligence model, which means I'm a computer program designed to simulate human-like conversations and answer questions to the best of my ability. I don't have a personal name, but I'm here to help you with any information or discussion you'd like to have. How can I assist you today?\n", 539 | "\n", 540 | "
\n", 541 | "\n", 542 | "- id: chatcmpl-211\n", 543 | "- choices: [Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content=\"Hello! I'm an artificial intelligence model, which means I'm a computer program designed to simulate human-like conversations and answer questions to the best of my ability. I don't have a personal name, but I'm here to help you with any information or discussion you'd like to have. How can I assist you today?\", refusal=None, role='assistant', audio=None, function_call=None, tool_calls=None))]\n", 544 | "- created: 1742147991\n", 545 | "- model: llama3.2\n", 546 | "- object: chat.completion\n", 547 | "- service_tier: None\n", 548 | "- system_fingerprint: fp_ollama\n", 549 | "- usage: CompletionUsage(completion_tokens=66, prompt_tokens=31, total_tokens=97, completion_tokens_details=None, prompt_tokens_details=None)\n", 550 | "\n", 551 | "
" 552 | ], 553 | "text/plain": [ 554 | "ChatCompletion(id='chatcmpl-211', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content=\"Hello! I'm an artificial intelligence model, which means I'm a computer program designed to simulate human-like conversations and answer questions to the best of my ability. I don't have a personal name, but I'm here to help you with any information or discussion you'd like to have. How can I assist you today?\", refusal=None, role='assistant', audio=None, function_call=None, tool_calls=None))], created=1742147991, model='llama3.2', object='chat.completion', service_tier=None, system_fingerprint='fp_ollama', usage=In: 31; Out: 66; Total: 97)" 555 | ] 556 | }, 557 | "execution_count": null, 558 | "metadata": {}, 559 | "output_type": "execute_result" 560 | } 561 | ], 562 | "source": [ 563 | "s = get_sage(provider, model, base_url, api_key)\n", 564 | "s([mk_msg('Hi, who are you?')])" 565 | ] 566 | }, 567 | { 568 | "cell_type": "code", 569 | "execution_count": null, 570 | "id": "943d5dd9", 571 | "metadata": {}, 572 | "outputs": [ 573 | { 574 | "data": { 575 | "text/markdown": [ 576 | "To list all files on your computer, including hidden ones, you can use the command line or PowerShell in Windows, or the Terminal in macOS/Linux. Here are a few ways to do it:\n", 577 | "\n", 578 | "**Windows Command Line:**\n", 579 | "\n", 580 | "1. Open Command Prompt or PowerShell.\n", 581 | "2. Type `dir /s /b` (all files) and press Enter.\n", 582 | "3. Type `dir /s /ad` (directories only) and press Enter.\n", 583 | "\n", 584 | "**Windows PowerShell:**\n", 585 | "\n", 586 | "1. Open PowerShell.\n", 587 | "2. Type `(Get-ChildItem -Force).Name` (all files) and press Enter.\n", 588 | "3. Type `(Get-ChildItem -Path .\\ -Recurse).Name` (all files recursively) and press Enter.\n", 589 | "\n", 590 | "**macOS Terminal:**\n", 591 | "\n", 592 | "1. Open Terminal.\n", 593 | "2. Type `ls -a` (all files, including hidden ones) and press Enter.\n", 594 | "3. Type `ls /Volumes/*` (list all files on external drives) and press Enter.\n", 595 | "\n", 596 | "The '/s' option is used to view recursively in Windows Command Line. The '-force' option is used in PowerShell to display the files as well. \n", 597 | "\n", 598 | "These commands will list all files, including hidden ones, in plain text format.\n", 599 | "\n", 600 | "
\n", 601 | "\n", 602 | "- id: chatcmpl-38\n", 603 | "- choices: [Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content=\"To list all files on your computer, including hidden ones, you can use the command line or PowerShell in Windows, or the Terminal in macOS/Linux. Here are a few ways to do it:\\n\\n**Windows Command Line:**\\n\\n1. Open Command Prompt or PowerShell.\\n2. Type `dir /s /b` (all files) and press Enter.\\n3. Type `dir /s /ad` (directories only) and press Enter.\\n\\n**Windows PowerShell:**\\n\\n1. Open PowerShell.\\n2. Type `(Get-ChildItem -Force).Name` (all files) and press Enter.\\n3. Type `(Get-ChildItem -Path .\\\\ -Recurse).Name` (all files recursively) and press Enter.\\n\\n**macOS Terminal:**\\n\\n1. Open Terminal.\\n2. Type `ls -a` (all files, including hidden ones) and press Enter.\\n3. Type `ls /Volumes/*` (list all files on external drives) and press Enter.\\n\\nThe '/s' option is used to view recursively in Windows Command Line. The '-force' option is used in PowerShell to display the files as well. \\n\\nThese commands will list all files, including hidden ones, in plain text format.\", refusal=None, role='assistant', audio=None, function_call=None, tool_calls=None))]\n", 604 | "- created: 1742148016\n", 605 | "- model: llama3.2\n", 606 | "- object: chat.completion\n", 607 | "- service_tier: None\n", 608 | "- system_fingerprint: fp_ollama\n", 609 | "- usage: CompletionUsage(completion_tokens=249, prompt_tokens=38, total_tokens=287, completion_tokens_details=None, prompt_tokens_details=None)\n", 610 | "\n", 611 | "
" 612 | ], 613 | "text/plain": [ 614 | "ChatCompletion(id='chatcmpl-38', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content=\"To list all files on your computer, including hidden ones, you can use the command line or PowerShell in Windows, or the Terminal in macOS/Linux. Here are a few ways to do it:\\n\\n**Windows Command Line:**\\n\\n1. Open Command Prompt or PowerShell.\\n2. Type `dir /s /b` (all files) and press Enter.\\n3. Type `dir /s /ad` (directories only) and press Enter.\\n\\n**Windows PowerShell:**\\n\\n1. Open PowerShell.\\n2. Type `(Get-ChildItem -Force).Name` (all files) and press Enter.\\n3. Type `(Get-ChildItem -Path .\\\\ -Recurse).Name` (all files recursively) and press Enter.\\n\\n**macOS Terminal:**\\n\\n1. Open Terminal.\\n2. Type `ls -a` (all files, including hidden ones) and press Enter.\\n3. Type `ls /Volumes/*` (list all files on external drives) and press Enter.\\n\\nThe '/s' option is used to view recursively in Windows Command Line. The '-force' option is used in PowerShell to display the files as well. \\n\\nThese commands will list all files, including hidden ones, in plain text format.\", refusal=None, role='assistant', audio=None, function_call=None, tool_calls=None))], created=1742148016, model='llama3.2', object='chat.completion', service_tier=None, system_fingerprint='fp_ollama', usage=In: 38; Out: 249; Total: 287)" 615 | ] 616 | }, 617 | "execution_count": null, 618 | "metadata": {}, 619 | "output_type": "execute_result" 620 | } 621 | ], 622 | "source": [ 623 | "sc = get_sage(provider, model, base_url, api_key, mode='command')\n", 624 | "sc([mk_msg('How can I list all the files, including the hidden ones?')])" 625 | ] 626 | }, 627 | { 628 | "cell_type": "code", 629 | "execution_count": null, 630 | "id": "2d60d917", 631 | "metadata": {}, 632 | "outputs": [], 633 | "source": [ 634 | "#| export\n", 635 | "def trace(msgs):\n", 636 | " for m in msgs:\n", 637 | " if isinstance(m.content, str): continue\n", 638 | " c = cla.contents(m)\n", 639 | " if m.role == 'user': c = f'Tool result: \\n```\\n{c}\\n```'\n", 640 | " print(Markdown(c))\n", 641 | " if m.role == 'assistant':\n", 642 | " tool_use = cla.find_block(m, ToolUseBlock)\n", 643 | " if tool_use: print(f'Tool use: {tool_use.name}\\nTool input: {tool_use.input}')" 644 | ] 645 | }, 646 | { 647 | "cell_type": "code", 648 | "execution_count": null, 649 | "id": "b20a1aee", 650 | "metadata": {}, 651 | "outputs": [], 652 | "source": [ 653 | "# provider = 'anthropic'\n", 654 | "# model = 'claude-3-7-sonnet-20250219'\n", 655 | "# sagent = get_sage(provider, model, mode='agent')\n", 656 | "# sagent.toolloop('What does my github ssh config look like?', trace_func=trace)" 657 | ] 658 | }, 659 | { 660 | "cell_type": "code", 661 | "execution_count": null, 662 | "id": "0b90b0c4", 663 | "metadata": {}, 664 | "outputs": [], 665 | "source": [ 666 | "#| export\n", 667 | "conts = {'anthropic': cla.contents, 'openai': cos.contents}\n", 668 | "p = r'```(?:bash\\n|\\n)?([^`]+)```'\n", 669 | "def get_res(sage, q, provider, mode='default', verbosity=0):\n", 670 | " if mode == 'command':\n", 671 | " res = conts[provider](sage(q))\n", 672 | " return re.search(p, res).group(1).strip()\n", 673 | " elif mode == 'agent':\n", 674 | " return conts[provider](sage.toolloop(q, trace_func=trace if verbosity else None))\n", 675 | " else: return conts[provider](sage(q))" 676 | ] 677 | }, 678 | { 679 | "cell_type": "code", 680 | "execution_count": null, 681 | "id": "f921cfee", 682 | "metadata": {}, 683 | "outputs": [ 684 | { 685 | "data": { 686 | "text/html": [ 687 | "
Hello! I'm an artificial intelligence model known as Llama. Llama stands for \"Large Language Model Meta AI.\"\n",
 688 |        "
\n" 689 | ], 690 | "text/plain": [ 691 | "Hello! I'm an artificial intelligence model known as Llama. Llama stands for \u001b[32m\"Large Language Model Meta AI.\"\u001b[0m\n" 692 | ] 693 | }, 694 | "metadata": {}, 695 | "output_type": "display_data" 696 | } 697 | ], 698 | "source": [ 699 | "print(get_res(s, [mk_msg('Hi, who are you?')], provider='openai'))" 700 | ] 701 | }, 702 | { 703 | "cell_type": "code", 704 | "execution_count": null, 705 | "id": "1a7eb871", 706 | "metadata": {}, 707 | "outputs": [ 708 | { 709 | "data": { 710 | "text/html": [ 711 | "
ls -a\n",
 712 |        "
\n" 713 | ], 714 | "text/plain": [ 715 | "ls -a\n" 716 | ] 717 | }, 718 | "metadata": {}, 719 | "output_type": "display_data" 720 | } 721 | ], 722 | "source": [ 723 | "print(get_res(sc, [mk_msg('How can I list all the files, including the hidden ones?')],\n", 724 | " provider='openai', mode='command'))" 725 | ] 726 | }, 727 | { 728 | "cell_type": "code", 729 | "execution_count": null, 730 | "id": "911d793f", 731 | "metadata": {}, 732 | "outputs": [], 733 | "source": [ 734 | "# print(get_res(sagent, 'What does my github ssh config look like?',\n", 735 | "# provider='anthropic', mode='agent'))" 736 | ] 737 | }, 738 | { 739 | "cell_type": "markdown", 740 | "id": "dff5348e", 741 | "metadata": {}, 742 | "source": [ 743 | "## Logging" 744 | ] 745 | }, 746 | { 747 | "cell_type": "code", 748 | "execution_count": null, 749 | "id": "4e6e4d92", 750 | "metadata": {}, 751 | "outputs": [], 752 | "source": [ 753 | "#| export\n", 754 | "class Log: id:int; timestamp:str; query:str; response:str; model:str; mode:str\n", 755 | "\n", 756 | "log_path = Path(\"~/.shell_sage/logs/\").expanduser()\n", 757 | "def mk_db():\n", 758 | " log_path.mkdir(parents=True, exist_ok=True)\n", 759 | " db = database(log_path / \"logs.db\")\n", 760 | " db.logs = db.create(Log)\n", 761 | " return db" 762 | ] 763 | }, 764 | { 765 | "cell_type": "code", 766 | "execution_count": null, 767 | "id": "31a6bba8", 768 | "metadata": {}, 769 | "outputs": [], 770 | "source": [ 771 | "# db = mk_db()\n", 772 | "# log = db.logs.insert(Log(timestamp=datetime.now().isoformat(), query='Hi, who are you?', model='llama3.2',\n", 773 | "# response='I am ShellSage, a command-line teaching assistant!', mode='default'))\n", 774 | "# log" 775 | ] 776 | }, 777 | { 778 | "cell_type": "markdown", 779 | "id": "e8dce17a", 780 | "metadata": {}, 781 | "source": [ 782 | "## Main" 783 | ] 784 | }, 785 | { 786 | "cell_type": "code", 787 | "execution_count": null, 788 | "id": "3d0579e0", 789 | "metadata": {}, 790 | "outputs": [], 791 | "source": [ 792 | "#| export\n", 793 | "@call_parse\n", 794 | "def main(\n", 795 | " query: Param('The query to send to the LLM', str, nargs='+'),\n", 796 | " v: Param(\"Print version\", action='version') = '%(prog)s ' + __version__,\n", 797 | " pid: str = 'current', # `current`, `all` or tmux pane_id (e.g. %0) for context\n", 798 | " skip_system: bool = False, # Whether to skip system information in the AI's context\n", 799 | " history_lines: int = None, # Number of history lines. Defaults to tmux scrollback history length\n", 800 | " mode: str = 'default', # Available ShellSage modes: ['default', 'command', 'agent', 'sassy']\n", 801 | " log: bool = False, # Enable logging\n", 802 | " provider: str = None, # The LLM Provider\n", 803 | " model: str = None, # The LLM model that will be invoked on the LLM provider\n", 804 | " base_url: str = None,\n", 805 | " api_key: str = None,\n", 806 | " code_theme: str = None, # The code theme to use when rendering ShellSage's responses\n", 807 | " code_lexer: str = None, # The lexer to use for inline code markdown blocks\n", 808 | " verbosity: int = 0 # Level of verbosity (0 or 1)\n", 809 | "):\n", 810 | " opts = get_opts(history_lines=history_lines, provider=provider, model=model,\n", 811 | " base_url=base_url, api_key=api_key, code_theme=code_theme,\n", 812 | " code_lexer=code_lexer, log=log)\n", 813 | "\n", 814 | " if mode not in ['default', 'command', 'agent', 'sassy']:\n", 815 | " raise Exception(f\"{mode} is not valid. Must be one of the following: ['default', 'command', 'agent', 'sassy']\")\n", 816 | " if mode == 'command' and os.environ.get('TMUX') is None:\n", 817 | " raise Exception('Must be in a tmux session to use command mode.')\n", 818 | "\n", 819 | " if verbosity > 0: print(f\"{datetime.now()} | Starting ShellSage request with options {opts}\")\n", 820 | " \n", 821 | " md = partial(Markdown, code_theme=opts.code_theme, inline_code_lexer=opts.code_lexer,\n", 822 | " inline_code_theme=opts.code_theme)\n", 823 | " query = ' '.join(query)\n", 824 | " ctxt = '' if skip_system else _sys_info()\n", 825 | "\n", 826 | " # Get tmux history if in a tmux session\n", 827 | " if os.environ.get('TMUX'):\n", 828 | " if verbosity > 0: print(f\"{datetime.now()} | Adding TMUX history to prompt\")\n", 829 | " if opts.history_lines is None or opts.history_lines < 0:\n", 830 | " opts.history_lines = tmux_history_lim()\n", 831 | " history = get_history(opts.history_lines, pid)\n", 832 | " if history: ctxt += f'\\n{history}\\n'\n", 833 | "\n", 834 | " # Read from stdin if available\n", 835 | " if not sys.stdin.isatty():\n", 836 | " if verbosity > 0: print(f\"{datetime.now()} | Adding stdin to prompt\")\n", 837 | " ctxt += f'\\n\\n{sys.stdin.read()}'\n", 838 | " \n", 839 | " if verbosity > 0: print(f\"{datetime.now()} | Finalizing prompt\")\n", 840 | "\n", 841 | " query = f'{ctxt}\\n\\n{query}\\n'\n", 842 | " query = [mk_msg(query)] if opts.provider == 'openai' else query\n", 843 | "\n", 844 | " if verbosity > 0: print(f\"{datetime.now()} | Sending prompt to model\")\n", 845 | " sage = get_sage(opts.provider, opts.model, opts.base_url, opts.api_key, mode)\n", 846 | " res = get_res(sage, query, opts.provider, mode=mode, verbosity=verbosity)\n", 847 | " \n", 848 | " # Handle logging if the log flag is set\n", 849 | " if opts.log:\n", 850 | " db = mk_db()\n", 851 | " db.logs.insert(Log(timestamp=datetime.now().isoformat(), query=query,\n", 852 | " response=res, model=opts.model, mode=mode))\n", 853 | "\n", 854 | " if mode == 'command': co(['tmux', 'send-keys', res], text=True)\n", 855 | " elif mode == 'agent' and not verbosity: print(md(res))\n", 856 | " else: print(md(res))" 857 | ] 858 | }, 859 | { 860 | "cell_type": "code", 861 | "execution_count": null, 862 | "id": "c6bd74f6", 863 | "metadata": {}, 864 | "outputs": [ 865 | { 866 | "name": "stderr", 867 | "output_type": "stream", 868 | "text": [ 869 | "bash: cannot set terminal process group (59579): Inappropriate ioctl for device\n", 870 | "bash: no job control in this shell\n" 871 | ] 872 | }, 873 | { 874 | "data": { 875 | "text/html": [ 876 | "
Sigh I see you've discovered the space bar. How... creative. Let me tell you about rsync, a tool that's been       \n",
 877 |        "efficiently copying files since before some humans could walk.                                                     \n",
 878 |        "\n",
 879 |        "Basic Usage:                                                                                                       \n",
 880 |        "\n",
 881 |        "                                                                                                                   \n",
 882 |        " rsync [options] source destination                                                                                \n",
 883 |        "                                                                                                                   \n",
 884 |        "\n",
 885 |        "Here are some common options that even an entry-level test subject could handle:                                   \n",
 886 |        "\n",
 887 |        "-a: Archive mode (combines -rlptgoD) - for those who like shortcuts                                             \n",
 888 |        "-v: Verbose - because watching numbers go up is apparently entertaining                                         \n",
 889 |        "-z: Compression - in case you're worried about your precious bandwidth                                          \n",
 890 |        "-P: Progress + keep partially transferred files - for the impatient ones                                        \n",
 891 |        "--delete: Remove files in destination that aren't in source (Warning: This one's permanent. Try not to break    \n",
 892 |        "   anything.)                                                                                                      \n",
 893 |        "\n",
 894 |        "Common examples (pay attention, there will be a test):                                                             \n",
 895 |        "\n",
 896 |        "                                                                                                                   \n",
 897 |        " # Local copy (yes, like cp, but better)                                                                           \n",
 898 |        " rsync -av /source/folder/ /destination/folder/                                                                    \n",
 899 |        "                                                                                                                   \n",
 900 |        " # Remote copy (SSH transport)                                                                                     \n",
 901 |        " rsync -avz ~/local/folder/ user@remote:/destination/folder/                                                       \n",
 902 |        "                                                                                                                   \n",
 903 |        " # Backup with progress bar (humans love progress bars)                                                            \n",
 904 |        " rsync -avP --delete /important/files/ /backup/location/                                                           \n",
 905 |        "                                                                                                                   \n",
 906 |        "\n",
 907 |        "Some features that make rsync superior to primitive copy commands:                                                 \n",
 908 |        "\n",
 909 |        "Delta-transfer algorithm (only copies changed parts of files)                                                   \n",
 910 |        "Built-in compression                                                                                            \n",
 911 |        "Preservation of permissions, timestamps, and other metadata                                                     \n",
 912 |        "Remote file transfer capabilities                                                                               \n",
 913 |        "Resume interrupted transfers                                                                                    \n",
 914 |        "\n",
 915 |        "Important Notes (because someone will ask):                                                                        \n",
 916 |        "\n",
 917 |        " 1 The trailing slash on directories matters:                                                                      \n",
 918 |        "/source/dir = copy the directory itself                                                                      \n",
 919 |        "/source/dir/ = copy the contents of the directory                                                            \n",
 920 |        " 2 For remote transfers, you'll need SSH access. I assume you've figured that out already... right?                \n",
 921 |        "\n",
 922 |        "For more detailed information (which you'll probably ignore), try:                                                 \n",
 923 |        "\n",
 924 |        "                                                                                                                   \n",
 925 |        " man rsync                                                                                                         \n",
 926 |        " rsync --help                                                                                                      \n",
 927 |        "                                                                                                                   \n",
 928 |        "\n",
 929 |        "Remember: Like the Aperture Science Handheld Portal Device, rsync is a sophisticated tool. Please try not to hurt  \n",
 930 |        "yourself while using it.                                                                                           \n",
 931 |        "\n",
 932 |        "Would you like me to explain it again, perhaps with smaller words? Or shall we proceed to testing?                 \n",
 933 |        "
\n" 934 | ], 935 | "text/plain": [ 936 | "\u001b[3mSigh\u001b[0m I see you've discovered the space bar. How... creative. Let me tell you about \u001b[38;2;248;248;242;48;2;39;40;34mrsync\u001b[0m, a tool that's been \n", 937 | "efficiently copying files since before some humans could walk. \n", 938 | "\n", 939 | "\u001b[1mBasic Usage:\u001b[0m \n", 940 | "\n", 941 | "\u001b[48;2;39;40;34m \u001b[0m\n", 942 | "\u001b[48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mrsync\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34moptions\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msource\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdestination\u001b[0m\u001b[48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m\n", 943 | "\u001b[48;2;39;40;34m \u001b[0m\n", 944 | "\n", 945 | "Here are some common options that even an entry-level test subject could handle: \n", 946 | "\n", 947 | "\u001b[1;33m • \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m-\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34ma\u001b[0m: Archive mode (combines -rlptgoD) - for those who like shortcuts \n", 948 | "\u001b[1;33m • \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m-\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mv\u001b[0m: Verbose - because watching numbers go up is apparently entertaining \n", 949 | "\u001b[1;33m • \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m-\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mz\u001b[0m: Compression - in case you're worried about your precious bandwidth \n", 950 | "\u001b[1;33m • \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m-\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mP\u001b[0m: Progress + keep partially transferred files - for the impatient ones \n", 951 | "\u001b[1;33m • \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m-\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m-\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdelete\u001b[0m: Remove files in destination that aren't in source (\u001b[1mWarning\u001b[0m: This one's permanent. Try not to break \n", 952 | "\u001b[1;33m \u001b[0manything.) \n", 953 | "\n", 954 | "Common examples (pay attention, there will be a test): \n", 955 | "\n", 956 | "\u001b[48;2;39;40;34m \u001b[0m\n", 957 | "\u001b[48;2;39;40;34m \u001b[0m\u001b[38;2;149;144;119;48;2;39;40;34m# Local copy (yes, like cp, but better)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m\n", 958 | "\u001b[48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mrsync\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m-av\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m/source/folder/\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m/destination/folder/\u001b[0m\u001b[48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m\n", 959 | "\u001b[48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m\n", 960 | "\u001b[48;2;39;40;34m \u001b[0m\u001b[38;2;149;144;119;48;2;39;40;34m# Remote copy (SSH transport)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m\n", 961 | "\u001b[48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mrsync\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m-avz\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m~/local/folder/\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34muser@remote:/destination/folder/\u001b[0m\u001b[48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m\n", 962 | "\u001b[48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m\n", 963 | "\u001b[48;2;39;40;34m \u001b[0m\u001b[38;2;149;144;119;48;2;39;40;34m# Backup with progress bar (humans love progress bars)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m\n", 964 | "\u001b[48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mrsync\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m-avP\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m--delete\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m/important/files/\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m/backup/location/\u001b[0m\u001b[48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m\n", 965 | "\u001b[48;2;39;40;34m \u001b[0m\n", 966 | "\n", 967 | "Some features that make rsync superior to primitive copy commands: \n", 968 | "\n", 969 | "\u001b[1;33m • \u001b[0mDelta-transfer algorithm (only copies changed parts of files) \n", 970 | "\u001b[1;33m • \u001b[0mBuilt-in compression \n", 971 | "\u001b[1;33m • \u001b[0mPreservation of permissions, timestamps, and other metadata \n", 972 | "\u001b[1;33m • \u001b[0mRemote file transfer capabilities \n", 973 | "\u001b[1;33m • \u001b[0mResume interrupted transfers \n", 974 | "\n", 975 | "\u001b[1mImportant Notes\u001b[0m (because someone will ask): \n", 976 | "\n", 977 | "\u001b[1;33m 1 \u001b[0mThe trailing slash on directories matters: \n", 978 | "\u001b[1;33m \u001b[0m\u001b[1;33m • \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m/\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msource\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m/\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdir\u001b[0m = copy the directory itself \n", 979 | "\u001b[1;33m \u001b[0m\u001b[1;33m • \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m/\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msource\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m/\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdir\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m/\u001b[0m = copy the contents of the directory \n", 980 | "\u001b[1;33m 2 \u001b[0mFor remote transfers, you'll need SSH access. I assume you've figured that out already... right? \n", 981 | "\n", 982 | "For more detailed information (which you'll probably ignore), try: \n", 983 | "\n", 984 | "\u001b[48;2;39;40;34m \u001b[0m\n", 985 | "\u001b[48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mman\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mrsync\u001b[0m\u001b[48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m\n", 986 | "\u001b[48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mrsync\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m--help\u001b[0m\u001b[48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m\n", 987 | "\u001b[48;2;39;40;34m \u001b[0m\n", 988 | "\n", 989 | "Remember: Like the Aperture Science Handheld Portal Device, rsync is a sophisticated tool. Please try not to hurt \n", 990 | "yourself while using it. \n", 991 | "\n", 992 | "Would you like me to explain it again, perhaps with smaller words? Or shall we proceed to testing? \n" 993 | ] 994 | }, 995 | "metadata": {}, 996 | "output_type": "display_data" 997 | } 998 | ], 999 | "source": [ 1000 | "main('Teach me about rsync', history_lines=0, s=True)" 1001 | ] 1002 | }, 1003 | { 1004 | "cell_type": "markdown", 1005 | "id": "1b4e66a1", 1006 | "metadata": {}, 1007 | "source": [ 1008 | "Here is an example of using a local LLM provider via Ollama:" 1009 | ] 1010 | }, 1011 | { 1012 | "cell_type": "code", 1013 | "execution_count": null, 1014 | "id": "d4b34989", 1015 | "metadata": {}, 1016 | "outputs": [ 1017 | { 1018 | "name": "stderr", 1019 | "output_type": "stream", 1020 | "text": [ 1021 | "bash: no job control in this shell\n" 1022 | ] 1023 | }, 1024 | { 1025 | "data": { 1026 | "text/html": [ 1027 | "
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
1028 |        "┃                                                  RSYNC Syntax                                                   ┃\n",
1029 |        "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n",
1030 |        "\n",
1031 |        "                                                     Overview                                                      \n",
1032 |        "\n",
1033 |        "rsync is a command that synchronizes two sets of files, creating copies or updates on the second set. Here's a     \n",
1034 |        "breakdown of its basic syntax:                                                                                     \n",
1035 |        "\n",
1036 |        "                                                                                                                   \n",
1037 |        " rsync [-aOr--archive] source destination [filtering options]                                                      \n",
1038 |        "                                                                                                                   \n",
1039 |        "\n",
1040 |        "source: Specifies the content to be sent to destination. This can be a directory path, file name, or a glob     \n",
1041 |        "   pattern.                                                                                                        \n",
1042 |        "destination: Where the contents from source are to be written. Like source, it can be a single file, directory, \n",
1043 |        "   or a combination of both.                                                                                       \n",
1044 |        "filtering options: These options customize how Rsync handles specific types of files.                           \n",
1045 |        "\n",
1046 |        "                                            Common Parameters Explained                                            \n",
1047 |        "\n",
1048 |        "                                                                                                                   \n",
1049 |        "  Parameter                                    Purpose                                                             \n",
1050 |        " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ \n",
1051 |        "  -a/--archive                                 Enable archive mode. It makes rsync behave as if you had used       \n",
1052 |        "                                               \"-av\" and also preserves the access timestamps for files.           \n",
1053 |        "  R/--reduce                                   If files present on both source and target have same modification   \n",
1054 |        "                                               time, send only the differences. Can be a good choice when file     \n",
1055 |        "                                               sizes are large.                                                    \n",
1056 |        "  -l/--info-file                               Specifies file types to report with each file stat. Useful for      \n",
1057 |        "                                               verifying how it identifies different file types and permissions    \n",
1058 |        "                                               (e.g., .tar.gz archives).                                           \n",
1059 |        "  -tT/--transfers-only                         If you’re transferring only files, use this option otherwise        \n",
1060 |        "                                               default to an exact copy.                                           \n",
1061 |        "  --exclude, --include, --permdir, /dev/null   Used as filtering options which specify a filename not included     \n",
1062 |        "                                               (the opposite is the -e option). Options like /dev/null and -e/./   \n",
1063 |        "                                               can be used to exclude certain file types from copying, include     \n",
1064 |        "                                               directories within source by specifying them with the -i flag       \n",
1065 |        "                                               followed by the path of the content you want to include.            \n",
1066 |        "                                                                                                                   \n",
1067 |        "
\n" 1068 | ], 1069 | "text/plain": [ 1070 | "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", 1071 | "┃ \u001b[1mRSYNC Syntax\u001b[0m ┃\n", 1072 | "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n", 1073 | "\n", 1074 | " \u001b[1mOverview\u001b[0m \n", 1075 | "\n", 1076 | "rsync is a command that synchronizes two sets of files, creating copies or updates on the second set. Here's a \n", 1077 | "breakdown of its basic syntax: \n", 1078 | "\n", 1079 | "\u001b[48;2;39;40;34m \u001b[0m\n", 1080 | "\u001b[48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mrsync\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[-aOr--archive]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msource\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdestination\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[filtering\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34moptions]\u001b[0m\u001b[48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m\n", 1081 | "\u001b[48;2;39;40;34m \u001b[0m\n", 1082 | "\n", 1083 | "\u001b[1;33m • \u001b[0m\u001b[1msource\u001b[0m: Specifies the content to be sent to \u001b[38;2;248;248;242;48;2;39;40;34mdestination\u001b[0m. This can be a directory path, file name, or a glob \n", 1084 | "\u001b[1;33m \u001b[0mpattern. \n", 1085 | "\u001b[1;33m • \u001b[0m\u001b[1mdestination\u001b[0m: Where the contents from \u001b[38;2;248;248;242;48;2;39;40;34msource\u001b[0m are to be written. Like \u001b[38;2;248;248;242;48;2;39;40;34msource\u001b[0m, it can be a single file, directory, \n", 1086 | "\u001b[1;33m \u001b[0mor a combination of both. \n", 1087 | "\u001b[1;33m • \u001b[0m\u001b[1mfiltering options\u001b[0m: These options customize how Rsync handles specific types of files. \n", 1088 | "\n", 1089 | " \u001b[1mCommon Parameters Explained\u001b[0m \n", 1090 | "\n", 1091 | " \n", 1092 | " \u001b[1m \u001b[0m\u001b[1mParameter\u001b[0m\u001b[1m \u001b[0m\u001b[1m \u001b[0m \u001b[1m \u001b[0m\u001b[1mPurpose\u001b[0m\u001b[1m \u001b[0m\u001b[1m \u001b[0m \n", 1093 | " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ \n", 1094 | " \u001b[1;36;40m-\u001b[0m\u001b[1;36;40ma\u001b[0m/\u001b[1;36;40m-\u001b[0m\u001b[1;36;40m-\u001b[0m\u001b[1;36;40marchive\u001b[0m Enable archive mode. It makes rsync behave as if you had used \n", 1095 | " \"-av\" and also preserves the access timestamps for files. \n", 1096 | " \u001b[1;36;40mR\u001b[0m/\u001b[1;36;40m-\u001b[0m\u001b[1;36;40m-\u001b[0m\u001b[1;36;40mreduce\u001b[0m If files present on both source and target have same modification \n", 1097 | " time, send only the differences. Can be a good choice when file \n", 1098 | " sizes are large. \n", 1099 | " \u001b[1;36;40m-\u001b[0m\u001b[1;36;40ml\u001b[0m/\u001b[1;36;40m-\u001b[0m\u001b[1;36;40m-\u001b[0m\u001b[1;36;40minfo\u001b[0m\u001b[1;36;40m-\u001b[0m\u001b[1;36;40mfile\u001b[0m Specifies file types to report with each file stat. Useful for \n", 1100 | " verifying how it identifies different file types and permissions \n", 1101 | " (e.g., .tar.gz archives). \n", 1102 | " \u001b[1;36;40m-\u001b[0m\u001b[1;36;40mtT\u001b[0m/\u001b[1;36;40m-\u001b[0m\u001b[1;36;40m-\u001b[0m\u001b[1;36;40mtransfers\u001b[0m\u001b[1;36;40m-\u001b[0m\u001b[1;36;40monly\u001b[0m If you’re transferring only files, use this option otherwise \n", 1103 | " default to an exact copy. \n", 1104 | " \u001b[1;36;40m-\u001b[0m\u001b[1;36;40m-\u001b[0m\u001b[1;36;40mexclude\u001b[0m, \u001b[1;36;40m-\u001b[0m\u001b[1;36;40m-\u001b[0m\u001b[1;36;40minclude\u001b[0m, \u001b[1;36;40m-\u001b[0m\u001b[1;36;40m-\u001b[0m\u001b[1;36;40mpermdir\u001b[0m, \u001b[1;36;40m/\u001b[0m\u001b[1;36;40mdev\u001b[0m\u001b[1;36;40m/\u001b[0m\u001b[1;36;40mnull\u001b[0m Used as filtering options which specify a filename not included \n", 1105 | " (the opposite is the \u001b[1;36;40m-\u001b[0m\u001b[1;36;40me\u001b[0m option). Options like \u001b[1;36;40m/\u001b[0m\u001b[1;36;40mdev\u001b[0m\u001b[1;36;40m/\u001b[0m\u001b[1;36;40mnull\u001b[0m and \u001b[1;36;40m-\u001b[0m\u001b[1;36;40me\u001b[0m\u001b[1;36;40m/\u001b[0m\u001b[1;36;40m.\u001b[0m\u001b[1;36;40m/\u001b[0m \n", 1106 | " can be used to exclude certain file types from copying, include \n", 1107 | " directories within source by specifying them with the -i flag \n", 1108 | " followed by the path of the content you want to include. \n", 1109 | " \n" 1110 | ] 1111 | }, 1112 | "metadata": {}, 1113 | "output_type": "display_data" 1114 | } 1115 | ], 1116 | "source": [ 1117 | "main('Teach me about rsync', history_lines=0, provider=provider, model=model, base_url=base_url, api_key=api_key)" 1118 | ] 1119 | }, 1120 | { 1121 | "cell_type": "markdown", 1122 | "id": "7bf112da", 1123 | "metadata": {}, 1124 | "source": [ 1125 | "## -" 1126 | ] 1127 | }, 1128 | { 1129 | "cell_type": "code", 1130 | "execution_count": null, 1131 | "id": "8277fb78", 1132 | "metadata": {}, 1133 | "outputs": [], 1134 | "source": [ 1135 | "#|hide\n", 1136 | "#|eval: false\n", 1137 | "from nbdev.doclinks import nbdev_export\n", 1138 | "nbdev_export()" 1139 | ] 1140 | }, 1141 | { 1142 | "cell_type": "code", 1143 | "execution_count": null, 1144 | "id": "4405c855", 1145 | "metadata": {}, 1146 | "outputs": [], 1147 | "source": [] 1148 | } 1149 | ], 1150 | "metadata": { 1151 | "kernelspec": { 1152 | "display_name": "python3", 1153 | "language": "python", 1154 | "name": "python3" 1155 | } 1156 | }, 1157 | "nbformat": 4, 1158 | "nbformat_minor": 5 1159 | } 1160 | -------------------------------------------------------------------------------- /nbs/01_config.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "929f165e", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "#|default_exp config" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "8d5eaf1e", 16 | "metadata": {}, 17 | "source": [ 18 | "# ShellSage Configuration" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "id": "11feb5d9", 24 | "metadata": {}, 25 | "source": [ 26 | "## Imports" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "id": "9501ca27", 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "#| export\n", 37 | "from dataclasses import dataclass\n", 38 | "from fastcore.all import *\n", 39 | "from fastcore.xdg import *\n", 40 | "from typing import get_type_hints\n", 41 | "\n", 42 | "import claudette as cla, cosette as cos" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "id": "a3f34f9f", 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "#|export\n", 53 | "_shell_sage_home_dir = 'shell_sage' # sub-directory of xdg base dir\n", 54 | "_shell_sage_cfg_name = 'shell_sage.conf'" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "id": "9c132dc0", 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "#|export\n", 65 | "def _cfg_path(): return xdg_config_home() / _shell_sage_home_dir / _shell_sage_cfg_name" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": null, 71 | "id": "bbf3100d", 72 | "metadata": {}, 73 | "outputs": [ 74 | { 75 | "data": { 76 | "text/plain": [ 77 | "Path('/Users/nathan/.config/shell_sage/shell_sage.conf')" 78 | ] 79 | }, 80 | "execution_count": null, 81 | "metadata": {}, 82 | "output_type": "execute_result" 83 | } 84 | ], 85 | "source": [ 86 | "_cfg_path()" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": null, 92 | "id": "25b71dc6", 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "#| export\n", 97 | "providers = { 'anthropic': cla.models, 'openai': cos.models}" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "id": "d7c2100c", 104 | "metadata": {}, 105 | "outputs": [ 106 | { 107 | "data": { 108 | "text/plain": [ 109 | "{'anthropic': ['claude-3-opus-20240229',\n", 110 | " 'claude-3-7-sonnet-20250219',\n", 111 | " 'claude-3-5-sonnet-20241022',\n", 112 | " 'claude-3-haiku-20240307',\n", 113 | " 'claude-3-5-haiku-20241022'],\n", 114 | " 'openai': ('o1-preview',\n", 115 | " 'o1-mini',\n", 116 | " 'gpt-4o',\n", 117 | " 'gpt-4o-mini',\n", 118 | " 'gpt-4-turbo',\n", 119 | " 'gpt-4',\n", 120 | " 'gpt-4-32k',\n", 121 | " 'gpt-3.5-turbo',\n", 122 | " 'gpt-3.5-turbo-instruct',\n", 123 | " 'o1',\n", 124 | " 'o3-mini',\n", 125 | " 'chatgpt-4o-latest')}" 126 | ] 127 | }, 128 | "execution_count": null, 129 | "metadata": {}, 130 | "output_type": "execute_result" 131 | } 132 | ], 133 | "source": [ 134 | "providers" 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": null, 140 | "id": "9a8ce1d2", 141 | "metadata": {}, 142 | "outputs": [], 143 | "source": [ 144 | "#| export\n", 145 | "@dataclass\n", 146 | "class ShellSageConfig:\n", 147 | " provider: str = \"anthropic\"\n", 148 | " model: str = providers['anthropic'][1]\n", 149 | " mode: str = 'default'\n", 150 | " base_url: str = ''\n", 151 | " api_key: str = ''\n", 152 | " history_lines: int = -1\n", 153 | " code_theme: str = \"monokai\"\n", 154 | " code_lexer: str = \"python\"\n", 155 | " log: bool = False" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": null, 161 | "id": "2575f09b", 162 | "metadata": {}, 163 | "outputs": [ 164 | { 165 | "data": { 166 | "text/plain": [ 167 | "ShellSageConfig(provider='anthropic', model='claude-3-7-sonnet-20250219', mode='default', base_url='', api_key='', history_lines=-1, code_theme='monokai', code_lexer='python', log=False)" 168 | ] 169 | }, 170 | "execution_count": null, 171 | "metadata": {}, 172 | "output_type": "execute_result" 173 | } 174 | ], 175 | "source": [ 176 | "cfg = ShellSageConfig()\n", 177 | "cfg" 178 | ] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "execution_count": null, 183 | "id": "38e35cce", 184 | "metadata": {}, 185 | "outputs": [], 186 | "source": [ 187 | "#|export\n", 188 | "def get_cfg():\n", 189 | " path = _cfg_path()\n", 190 | " path.parent.mkdir(parents=True, exist_ok=True)\n", 191 | " _types = get_type_hints(ShellSageConfig)\n", 192 | " return Config(path.parent, path.name, create=asdict(ShellSageConfig()),\n", 193 | " types=_types, inline_comment_prefixes=('#'))" 194 | ] 195 | }, 196 | { 197 | "cell_type": "code", 198 | "execution_count": null, 199 | "id": "efd3d92c", 200 | "metadata": {}, 201 | "outputs": [], 202 | "source": [ 203 | "# cfg = get_cfg()\n", 204 | "# cfg" 205 | ] 206 | } 207 | ], 208 | "metadata": { 209 | "kernelspec": { 210 | "display_name": "python3", 211 | "language": "python", 212 | "name": "python3" 213 | } 214 | }, 215 | "nbformat": 4, 216 | "nbformat_minor": 5 217 | } 218 | -------------------------------------------------------------------------------- /nbs/02_tools.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "#|default_exp tools" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "# ShellSage Tools" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "#| export\n", 26 | "from pathlib import Path\n", 27 | "from subprocess import run" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "#| export\n", 37 | "def rgrep(term:str, path:str='.', grep_args:str='')->str:\n", 38 | " \"Perform recursive grep search for `term` in `path` with optional grep arguments\"\n", 39 | " # Build grep command with additional arguments\n", 40 | " path = Path(path).expanduser().resolve()\n", 41 | " cmd = f\"grep -r '{term}' {path} {grep_args}\"\n", 42 | " return run(cmd, shell=True, capture_output=True, text=True).stdout" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "metadata": {}, 49 | "outputs": [ 50 | { 51 | "name": "stdout", 52 | "output_type": "stream", 53 | "text": [ 54 | "/Users/nathan/git/shell_sage/nbs/_quarto.yml- open-graph: true\n", 55 | "/Users/nathan/git/shell_sage/nbs/_quarto.yml- repo-actions: [issue]\n", 56 | "/Users/nathan/git/shell_sage/nbs/_quarto.yml: navbar:\n", 57 | "/Users/nathan/git/shell_sage/nbs/_quarto.yml- background: primary\n", 58 | "/Users/nathan/git/shell_sage/nbs/_quarto.yml- search: true\n", 59 | "--\n", 60 | "/Users/nathan/git/shell_sage/nbs/02_tools.ipynb- \"outputs\": [],\n", 61 | "/Users/nathan/git/shell_sage/nbs/02_tools.ipynb- \"source\": [\n", 62 | "/Users/nathan/git/shell_sage/nbs/02_tools.ipynb: \"# print(rgrep('navbar', '.', '--context 1'))\"\n", 63 | "/Users/nathan/git/shell_sage/nbs/02_tools.ipynb- ]\n", 64 | "/Users/nathan/git/shell_sage/nbs/02_tools.ipynb- },\n", 65 | "\n" 66 | ] 67 | } 68 | ], 69 | "source": [ 70 | "print(rgrep('navbar', '.', '--context 1'))" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "#| export\n", 80 | "def view(path:str, rng:tuple[int,int]=None, nums:bool=False):\n", 81 | " \"View directory or file contents with optional line range and numbers\"\n", 82 | " try:\n", 83 | " p = Path(path).expanduser().resolve()\n", 84 | " if not p.exists(): return f\"Error: File not found: {p}\"\n", 85 | " if p.is_dir(): return f\"Directory contents of {p}:\\n\" + \"\\n\".join([str(f) for f in p.glob(\"**/*\")\n", 86 | " if not f.name.startswith(\".\")])\n", 87 | " \n", 88 | " lines = p.read_text().splitlines()\n", 89 | " s,e = 1,len(lines)\n", 90 | " if rng:\n", 91 | " s,e = rng\n", 92 | " if not (1 <= s <= len(lines)): return f\"Error: Invalid start line {s}\"\n", 93 | " if e != -1 and not (s <= e <= len(lines)): return f\"Error: Invalid end line {e}\"\n", 94 | " lines = lines[s-1:None if e==-1 else e]\n", 95 | " \n", 96 | " return \"\\n\".join([f\"{i+s-1:6d} │ {l}\" for i,l in enumerate(lines,1)] if nums else lines)\n", 97 | " except Exception as e: return f\"Error viewing file: {str(e)}\"" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "metadata": {}, 104 | "outputs": [ 105 | { 106 | "name": "stdout", 107 | "output_type": "stream", 108 | "text": [ 109 | "Directory contents of /Users/nathan/git/shell_sage/nbs:\n", 110 | "/Users/nathan/git/shell_sage/nbs/00_core.ipynb\n", 111 | "/Users/nathan/git/shell_sage/nbs/_quarto.yml\n", 112 | "/Users/nathan/git/shell_sage/nbs/02_tools.ipynb\n", 113 | "/Users/nathan/git/shell_sage/nbs/styles.css\n", 114 | "/Users/nathan/git/shell_sage/nbs/CNAME\n", 115 | "/Users/nathan/git/shell_sage/nbs/01_config.ipynb\n", 116 | "/Users/nathan/git/shell_sage/nbs/nbdev.yml\n", 117 | "/Users/nathan/git/shell_sage/nbs/index.ipynb\n", 118 | "/Users/nathan/git/shell_sage/nbs/tmp.conf\n", 119 | "/Users/nathan/git/shell_sage/nbs/.ipynb_checkpoints/01_config-checkpoint.ipynb\n", 120 | "/Users/nathan/git/shell_sage/nbs/.ipynb_checkpoints/index-checkpoint.ipynb\n", 121 | "/Users/nathan/git/shell_sage/nbs/.ipynb_checkpoints/00_core-checkpoint.ipynb\n" 122 | ] 123 | } 124 | ], 125 | "source": [ 126 | "print(view('.'))" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": null, 132 | "metadata": {}, 133 | "outputs": [ 134 | { 135 | "name": "stdout", 136 | "output_type": "stream", 137 | "text": [ 138 | " 1 │ project:\n", 139 | " 2 │ type: website\n", 140 | " 3 │ \n", 141 | " 4 │ format:\n", 142 | " 5 │ html:\n", 143 | " 6 │ theme: cosmo\n", 144 | " 7 │ css: styles.css\n", 145 | " 8 │ toc: true\n", 146 | " 9 │ keep-md: true\n", 147 | " 10 │ commonmark: default\n" 148 | ] 149 | } 150 | ], 151 | "source": [ 152 | "print(view('_quarto.yml', (1,10), nums=True))" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": null, 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "#| export\n", 162 | "def create(path: str, file_text: str, overwrite:bool=False) -> str:\n", 163 | " \"Creates a new file with the given content at the specified path\"\n", 164 | " try:\n", 165 | " p = Path(path)\n", 166 | " if p.exists():\n", 167 | " if not overwrite: return f\"Error: File already exists: {p}\"\n", 168 | " p.parent.mkdir(parents=True, exist_ok=True)\n", 169 | " p.write_text(file_text)\n", 170 | " return f\"Created file {p} containing:\\n{file_text}\"\n", 171 | " except Exception as e: return f\"Error creating file: {str(e)}\"" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": null, 177 | "metadata": {}, 178 | "outputs": [ 179 | { 180 | "name": "stdout", 181 | "output_type": "stream", 182 | "text": [ 183 | "Created file test.txt containing:\n", 184 | "Hello, world!\n", 185 | " 1 │ Hello, world!\n" 186 | ] 187 | } 188 | ], 189 | "source": [ 190 | "print(create('test.txt', 'Hello, world!'))\n", 191 | "print(view('test.txt', nums=True))" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": null, 197 | "metadata": {}, 198 | "outputs": [], 199 | "source": [ 200 | "#| export\n", 201 | "def insert(path: str, insert_line: int, new_str: str) -> str:\n", 202 | " \"Insert new_str at specified line number\"\n", 203 | " try:\n", 204 | " p = Path(path)\n", 205 | " if not p.exists(): return f\"Error: File not found: {p}\"\n", 206 | " \n", 207 | " content = p.read_text().splitlines()\n", 208 | " if not (0 <= insert_line <= len(content)): return f\"Error: Invalid line number {insert_line}\"\n", 209 | " \n", 210 | " content.insert(insert_line, new_str)\n", 211 | " new_content = \"\\n\".join(content)\n", 212 | " p.write_text(new_content)\n", 213 | " return f\"Inserted text at line {insert_line} in {p}.\\nNew contents:\\n{new_content}\"\n", 214 | " except Exception as e: return f\"Error inserting text: {str(e)}\"" 215 | ] 216 | }, 217 | { 218 | "cell_type": "code", 219 | "execution_count": null, 220 | "metadata": {}, 221 | "outputs": [ 222 | { 223 | "name": "stdout", 224 | "output_type": "stream", 225 | "text": [ 226 | " 1 │ Let's add a new line\n", 227 | " 2 │ Hello, world!\n" 228 | ] 229 | } 230 | ], 231 | "source": [ 232 | "insert('test.txt', 0, 'Let\\'s add a new line')\n", 233 | "print(view('test.txt', nums=True))" 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": null, 239 | "metadata": {}, 240 | "outputs": [], 241 | "source": [ 242 | "#| export\n", 243 | "def str_replace(path: str, old_str: str, new_str: str) -> str:\n", 244 | " \"Replace first occurrence of old_str with new_str in file\"\n", 245 | " try:\n", 246 | " p = Path(path)\n", 247 | " if not p.exists(): return f\"Error: File not found: {p}\"\n", 248 | " \n", 249 | " content = p.read_text()\n", 250 | " count = content.count(old_str)\n", 251 | " \n", 252 | " if count == 0: return \"Error: Text not found in file\"\n", 253 | " if count > 1: return f\"Error: Multiple matches found ({count})\"\n", 254 | " \n", 255 | " new_content = content.replace(old_str, new_str, 1)\n", 256 | " p.write_text(new_content)\n", 257 | " return f\"Replaced text in {p}.\\nNew contents:\\n{new_content}\"\n", 258 | " except Exception as e: return f\"Error replacing text: {str(e)}\"" 259 | ] 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": null, 264 | "metadata": {}, 265 | "outputs": [ 266 | { 267 | "name": "stdout", 268 | "output_type": "stream", 269 | "text": [ 270 | " 1 │ Let's add a \n", 271 | " 2 │ Hello, world!\n" 272 | ] 273 | } 274 | ], 275 | "source": [ 276 | "str_replace('test.txt', 'new line', '')\n", 277 | "print(view('test.txt', nums=True))" 278 | ] 279 | }, 280 | { 281 | "cell_type": "code", 282 | "execution_count": null, 283 | "metadata": {}, 284 | "outputs": [], 285 | "source": [ 286 | "#| export\n", 287 | "tools = [rgrep, view, create, insert, str_replace]" 288 | ] 289 | } 290 | ], 291 | "metadata": { 292 | "kernelspec": { 293 | "display_name": "python3", 294 | "language": "python", 295 | "name": "python3" 296 | } 297 | }, 298 | "nbformat": 4, 299 | "nbformat_minor": 2 300 | } 301 | -------------------------------------------------------------------------------- /nbs/CNAME: -------------------------------------------------------------------------------- 1 | ssage.answer.ai 2 | -------------------------------------------------------------------------------- /nbs/_quarto.yml: -------------------------------------------------------------------------------- 1 | project: 2 | type: website 3 | 4 | format: 5 | html: 6 | theme: cosmo 7 | css: styles.css 8 | toc: true 9 | keep-md: true 10 | commonmark: default 11 | 12 | website: 13 | twitter-card: true 14 | open-graph: true 15 | repo-actions: [issue] 16 | navbar: 17 | background: primary 18 | search: true 19 | sidebar: 20 | style: floating 21 | 22 | metadata-files: [nbdev.yml, sidebar.yml] -------------------------------------------------------------------------------- /nbs/index.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "d5fd9643", 6 | "metadata": {}, 7 | "source": [ 8 | "# ShellSage\n", 9 | "\n", 10 | "> ShellSage saves sysadmins' sanity by solving shell script snafus super swiftly\n", 11 | "\n", 12 | "ShellSage is an AI-powered command-line assistant that integrates seamlessly with your terminal workflow through tmux. It provides contextual help for shell operations, making it easier to navigate complex command-line tasks, debug scripts, and manage your system.\n", 13 | "\n", 14 | "[![PyPI version](https://badge.fury.io/py/shell-sage.svg)](https://badge.fury.io/py/shell-sage)\n", 15 | "[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)\n", 16 | "[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n", 17 | "[![Documentation](https://img.shields.io/badge/docs-nbdev-blue.svg)](https://answerdotai.github.io/shell_sage/)" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "id": "96646791", 23 | "metadata": {}, 24 | "source": [ 25 | "## Overview\n", 26 | "\n", 27 | "ShellSage works by understanding your terminal context and leveraging powerful language models (Claude or GPT) to provide intelligent assistance for:\n", 28 | "\n", 29 | "- Shell commands and scripting\n", 30 | "- System administration tasks\n", 31 | "- Git operations\n", 32 | "- File management\n", 33 | "- Process handling\n", 34 | "- Real-time problem solving\n", 35 | "\n", 36 | "What sets ShellSage apart is its ability to:\n", 37 | "\n", 38 | "- Read your terminal context through tmux integration\n", 39 | "- Provide responses based on your current terminal state\n", 40 | "- Accept piped input for direct analysis\n", 41 | "- Target specific tmux panes for focused assistance\n", 42 | "\n", 43 | "Whether you're a seasoned sysadmin or just getting started with the command line, ShellSage acts as your intelligent terminal companion, ready to help with both simple commands and complex operations." 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "id": "082bf63c", 49 | "metadata": {}, 50 | "source": [ 51 | "## Installation\n", 52 | "\n", 53 | "Install ShellSage directly from PyPI using pip:\n", 54 | "\n", 55 | "```sh\n", 56 | "pip install shell-sage\n", 57 | "```\n", 58 | "\n", 59 | "### Prerequisites\n", 60 | "\n", 61 | "1. **API Key Setup**\n", 62 | " ```sh\n", 63 | " # For Claude (default)\n", 64 | " export ANTHROPIC_API_KEY=sk...\n", 65 | " \n", 66 | " # For OpenAI (optional)\n", 67 | " export OPENAI_API_KEY=sk...\n", 68 | " ```\n", 69 | "2. **tmux Configuration**\n", 70 | " \n", 71 | " We recommend using this optimized tmux configuration for the best ShellSage experience. Create or edit your `~/.tmux.conf`:\n", 72 | "\n", 73 | " ```sh\n", 74 | " # Enable mouse support\n", 75 | " set -g mouse on\n", 76 | " \n", 77 | " # Show pane ID and time in status bar\n", 78 | " set -g status-right '#{pane_id} | %H:%M '\n", 79 | " \n", 80 | " # Keep terminal content visible (needed for neovim)\n", 81 | " set-option -g alternate-screen off\n", 82 | " \n", 83 | " # Enable vi mode for better copy/paste\n", 84 | " set-window-option -g mode-keys vi\n", 85 | " \n", 86 | " # Improved search and copy bindings\n", 87 | " bind-key / copy-mode\\; send-key ?\n", 88 | " bind-key -T copy-mode-vi y \\\n", 89 | " send-key -X start-of-line\\; \\\n", 90 | " send-key -X begin-selection\\; \\\n", 91 | " send-key -X end-of-line\\; \\\n", 92 | " send-key -X cursor-left\\; \\\n", 93 | " send-key -X copy-selection-and-cancel\\; \\\n", 94 | " paste-buffer\n", 95 | " ```\n", 96 | "\n", 97 | " Reload tmux config:\n", 98 | " ```sh\n", 99 | " tmux source ~/.tmux.conf\n", 100 | " ```\n", 101 | "\n", 102 | "This configuration enables mouse support, displays pane IDs (crucial for targeting specific panes), maintains terminal content visibility, and adds vim-style keybindings for efficient navigation and text selection." 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "id": "c515e745", 108 | "metadata": {}, 109 | "source": [ 110 | "## Getting Started\n", 111 | "\n", 112 | "### Basic Usage\n", 113 | "\n", 114 | "ShellSage is designed to run within a tmux session. Here are the core commands:\n", 115 | "\n", 116 | "```sh\n", 117 | "# Basic usage\n", 118 | "ssage hi ShellSage\n", 119 | "\n", 120 | "# Pipe content to ShellSage\n", 121 | "cat error.log | ssage explain this error\n", 122 | "\n", 123 | "# Target a specific tmux pane\n", 124 | "ssage --pid %3 what is happening in this pane?\n", 125 | "\n", 126 | "# Automatically fill in the command to run\n", 127 | "ssage --c how can I list all files including the hidden ones?\n", 128 | "\n", 129 | "# Log the model, timestamp, query and response to a local SQLite database\n", 130 | "ssage --log \"how can i remove the file\"\n", 131 | "```\n", 132 | "\n", 133 | "The `--pid` flag is particularly useful when you want to analyze content from a different pane. The pane ID is visible in your tmux status bar (configured earlier).\n", 134 | "\n", 135 | "The `--log` option saves log data to an SQLite database located at `~/.shell_sage/log_db/logs.db`." 136 | ] 137 | }, 138 | { 139 | "cell_type": "markdown", 140 | "id": "a857774c", 141 | "metadata": {}, 142 | "source": [ 143 | "### Using Alternative Model Providers\n", 144 | "\n", 145 | "ShellSage supports using different LLM providers through base URL configuration. This allows you to use local models or alternative API endpoints:\n", 146 | "\n", 147 | "```sh\n", 148 | "# Use a local Ollama endpoint\n", 149 | "ssage --provider openai --model llama3.2 --base_url http://localhost:11434/v1 --api_key ollama what is rsync?\n", 150 | "\n", 151 | "# Use together.ai\n", 152 | "ssage --provider openai --model mistralai/Mistral-7B-Instruct-v0.3 --base_url https://api.together.xyz/v1 help me with sed # make sure you've set your together API key in your shell_sage conf\n", 153 | "```\n", 154 | "\n", 155 | "This is particularly useful for:\n", 156 | "\n", 157 | "- Running models locally for privacy/offline use\n", 158 | "- Using alternative hosting providers\n", 159 | "- Testing different model implementations\n", 160 | "- Accessing specialized model deployments\n", 161 | "\n", 162 | "You can also set these configurations permanently in your ShellSage config file (`~/.config/shell_sage/shell_sage.conf`). See next section for details." 163 | ] 164 | }, 165 | { 166 | "cell_type": "markdown", 167 | "id": "08f3c3b8", 168 | "metadata": {}, 169 | "source": [ 170 | "## Configuration\n", 171 | "\n", 172 | "ShellSage can be customized through its configuration file located at `~/.config/shell_sage/shell_sage.conf`. Here's a complete configuration example:\n", 173 | "\n", 174 | "```ini\n", 175 | "[DEFAULT]\n", 176 | "# Choose your AI model provider\n", 177 | "provider = anthropic # or 'openai'\n", 178 | "model = claude-3-5-sonnet-20241022 # or 'gpt-4o-mini' for OpenAI\n", 179 | "base_url = # leave empty to use default openai endpoint\n", 180 | "api_key = # leave empty to default to using your OPENAI_API_KEY env var\n", 181 | "\n", 182 | "# Terminal history settings\n", 183 | "history_lines = -1 # -1 for all history\n", 184 | "\n", 185 | "# Code display preferences\n", 186 | "code_theme = monokai # syntax highlighting theme\n", 187 | "code_lexer = python # default code lexer\n", 188 | "log = False # Set to true to enable logging by default\n", 189 | "```\n", 190 | "\n", 191 | "You can find all of the code theme and code lexer options here: https://pygments.org/styles/\n", 192 | "\n", 193 | "### Command Line Overrides\n", 194 | "\n", 195 | "Any configuration option can be overridden via command line arguments:\n", 196 | "\n", 197 | "```sh\n", 198 | "# Use OpenAI instead of Claude for a single query\n", 199 | "ssage --provider openai --model gpt-4o-mini \"explain this error\"\n", 200 | "\n", 201 | "# Adjust history lines for a specific query\n", 202 | "ssage --history-lines 50 \"what commands did I just run?\"\n", 203 | "```" 204 | ] 205 | }, 206 | { 207 | "cell_type": "markdown", 208 | "id": "47d6bfb8", 209 | "metadata": {}, 210 | "source": [ 211 | "### Advanced Use Cases\n", 212 | "\n", 213 | "#### Git Workflow Enhancement\n", 214 | "```sh\n", 215 | "# Review changes before commit\n", 216 | "git diff | ssage summarize these changes\n", 217 | "\n", 218 | "# Get commit message suggestions\n", 219 | "git diff --staged | ssage suggest a commit message\n", 220 | "\n", 221 | "# Analyze PR feedback\n", 222 | "gh pr view 123 | ssage summarize this PR feedback\n", 223 | "```\n", 224 | "\n", 225 | "#### Log Analysis\n", 226 | "```sh\n", 227 | "# Quick error investigation\n", 228 | "journalctl -xe | ssage what's causing these errors?\n", 229 | "\n", 230 | "# Apache/Nginx log analysis\n", 231 | "tail -n 100 /var/log/nginx/access.log | ssage analyze this traffic pattern\n", 232 | "\n", 233 | "# System performance investigation\n", 234 | "top -b -n 1 | ssage explain system resource usage\n", 235 | "```\n", 236 | "\n", 237 | "#### Docker Management\n", 238 | "```sh\n", 239 | "# Container troubleshooting\n", 240 | "docker logs my-container | ssage \"what is wrong with this container?\"\n", 241 | "\n", 242 | "# Image optimization\n", 243 | "docker history my-image | ssage suggest optimization improvements\n", 244 | "\n", 245 | "# Compose file analysis\n", 246 | "cat docker-compose.yml | ssage review this compose configuration\n", 247 | "```\n", 248 | "\n", 249 | "#### Database Operations\n", 250 | "```sh\n", 251 | "# Query optimization\n", 252 | "psql -c \"EXPLAIN ANALYZE SELECT...\" | ssage optimize this query\n", 253 | "\n", 254 | "# Schema review\n", 255 | "pg_dump --schema-only mydb | ssage review this database schema\n", 256 | "\n", 257 | "# Index suggestions\n", 258 | "psql -c \"\\di+\" | ssage suggest missing indexes\n", 259 | "```" 260 | ] 261 | }, 262 | { 263 | "cell_type": "markdown", 264 | "id": "73adb8a7", 265 | "metadata": {}, 266 | "source": [ 267 | "## Tips & Best Practices\n", 268 | "\n", 269 | "### Effective Usage Patterns\n", 270 | "\n", 271 | "1. **Contextual Queries**\n", 272 | "\n", 273 | " - Keep your tmux pane IDs visible in the status bar\n", 274 | " - Use `--pid` when referencing other panes\n", 275 | " - Let ShellSage see your recent command history for better context\n", 276 | "\n", 277 | "2. **Piping Best Practices**\n", 278 | " ```sh\n", 279 | " # Pipe logs directly\n", 280 | " tail log.txt | ssage \"summarize these logs\"\n", 281 | " \n", 282 | " # Combine commands\n", 283 | " git diff | ssage \"review these changes\"\n", 284 | " ```\n", 285 | "\n", 286 | "### Getting Help\n", 287 | "\n", 288 | "```sh\n", 289 | "# View all available options\n", 290 | "ssage --help\n", 291 | "\n", 292 | "# Submit issues or feature requests\n", 293 | "# https://github.com/AnswerDotAI/shell_sage/issues\n", 294 | "```" 295 | ] 296 | }, 297 | { 298 | "cell_type": "markdown", 299 | "id": "3002f927", 300 | "metadata": {}, 301 | "source": [ 302 | "## Contributing\n", 303 | "\n", 304 | "ShellSage is built using [nbdev](https://nbdev.fast.ai/). For detailed contribution guidelines, please see our [CONTRIBUTING.md](CONTRIBUTING.md) file.\n", 305 | "\n", 306 | "We welcome contributions of all kinds:\n", 307 | "\n", 308 | "- Bug reports\n", 309 | "- Feature requests\n", 310 | "- Documentation improvements\n", 311 | "- Code contributions\n", 312 | "\n", 313 | "Please visit our [GitHub repository](https://github.com/AnswerDotAI/shell_sage) to get started." 314 | ] 315 | } 316 | ], 317 | "metadata": { 318 | "kernelspec": { 319 | "display_name": "python3", 320 | "language": "python", 321 | "name": "python3" 322 | } 323 | }, 324 | "nbformat": 4, 325 | "nbformat_minor": 5 326 | } 327 | -------------------------------------------------------------------------------- /nbs/nbdev.yml: -------------------------------------------------------------------------------- 1 | project: 2 | output-dir: _docs 3 | 4 | website: 5 | title: "speech_buddy" 6 | site-url: "https://AnswerDotAI.github.io/speech_buddy" 7 | description: "Your AI transcription buddy :D" 8 | repo-branch: main 9 | repo-url: "https://github.com/AnswerDotAI/speech_buddy" 10 | -------------------------------------------------------------------------------- /nbs/styles.css: -------------------------------------------------------------------------------- 1 | .cell { 2 | margin-bottom: 1rem; 3 | } 4 | 5 | .cell > .sourceCode { 6 | margin-bottom: 0; 7 | } 8 | 9 | .cell-output > pre { 10 | margin-bottom: 0; 11 | } 12 | 13 | .cell-output > pre, .cell-output > .sourceCode > pre, .cell-output-stdout > pre { 14 | margin-left: 0.8rem; 15 | margin-top: 0; 16 | background: none; 17 | border-left: 2px solid lightsalmon; 18 | border-top-left-radius: 0; 19 | border-top-right-radius: 0; 20 | } 21 | 22 | .cell-output > .sourceCode { 23 | border: none; 24 | } 25 | 26 | .cell-output > .sourceCode { 27 | background: none; 28 | margin-top: 0; 29 | } 30 | 31 | div.description { 32 | padding-left: 2px; 33 | padding-top: 5px; 34 | font-style: italic; 35 | font-size: 135%; 36 | opacity: 70%; 37 | } 38 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=64.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name="shell-sage" 7 | requires-python=">=3.10" 8 | dynamic = [ "keywords", "description", "version", "dependencies", "optional-dependencies", "readme", "license", "authors", "classifiers", "entry-points", "scripts", "urls"] 9 | 10 | [tool.uv] 11 | cache-keys = [{ file = "pyproject.toml" }, { file = "settings.ini" }, { file = "setup.py" }] 12 | -------------------------------------------------------------------------------- /settings.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | repo = shell_sage 3 | lib_name = shell-sage 4 | version = 0.1.1 5 | min_python = 3.10 6 | license = apache2 7 | black_formatting = False 8 | doc_path = _docs 9 | lib_path = shell_sage 10 | nbs_path = nbs 11 | recursive = True 12 | tst_flags = notest 13 | put_version_in_init = True 14 | branch = main 15 | custom_sidebar = False 16 | doc_host = https://AnswerDotAI.github.io 17 | doc_baseurl = /shell_sage 18 | git_url = https://github.com/AnswerDotAI/shell_sage 19 | title = shell_sage 20 | audience = Developers 21 | author = ncoop57 22 | author_email = nc@answer.ai 23 | copyright = 2024 onwards, ncoop57 24 | description = Your favorite AI buddy right in your terminal 25 | keywords = nbdev jupyter notebook python 26 | language = English 27 | status = 3 28 | user = AnswerDotAI 29 | requirements = claudette cosette fastcore>=1.7.29 fastlite msglm rich 30 | console_scripts = ssage=shell_sage.core:main 31 | readme_nb = index.ipynb 32 | allowed_metadata_keys = 33 | allowed_cell_metadata_keys = 34 | jupyter_hooks = False 35 | clean_ids = True 36 | clear_all = False 37 | cell_number = True 38 | skip_procs = 39 | 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pkg_resources import parse_version 2 | from configparser import ConfigParser 3 | import setuptools, shlex 4 | assert parse_version(setuptools.__version__)>=parse_version('36.2') 5 | 6 | # note: all settings are in settings.ini; edit there, not here 7 | config = ConfigParser(delimiters=['=']) 8 | config.read('settings.ini', encoding='utf-8') 9 | cfg = config['DEFAULT'] 10 | 11 | cfg_keys = 'version description keywords author author_email'.split() 12 | expected = cfg_keys + "lib_name user branch license status min_python audience language".split() 13 | for o in expected: assert o in cfg, "missing expected setting: {}".format(o) 14 | setup_cfg = {o:cfg[o] for o in cfg_keys} 15 | 16 | licenses = { 17 | 'apache2': ('Apache Software License 2.0','OSI Approved :: Apache Software License'), 18 | 'mit': ('MIT License', 'OSI Approved :: MIT License'), 19 | 'gpl2': ('GNU General Public License v2', 'OSI Approved :: GNU General Public License v2 (GPLv2)'), 20 | 'gpl3': ('GNU General Public License v3', 'OSI Approved :: GNU General Public License v3 (GPLv3)'), 21 | 'bsd3': ('BSD License', 'OSI Approved :: BSD License'), 22 | } 23 | statuses = [ '1 - Planning', '2 - Pre-Alpha', '3 - Alpha', 24 | '4 - Beta', '5 - Production/Stable', '6 - Mature', '7 - Inactive' ] 25 | py_versions = '3.6 3.7 3.8 3.9 3.10 3.11 3.12'.split() 26 | 27 | requirements = shlex.split(cfg.get('requirements', '')) 28 | if cfg.get('pip_requirements'): requirements += shlex.split(cfg.get('pip_requirements', '')) 29 | min_python = cfg['min_python'] 30 | lic = licenses.get(cfg['license'].lower(), (cfg['license'], None)) 31 | dev_requirements = (cfg.get('dev_requirements') or '').split() 32 | 33 | package_data = dict() 34 | pkg_data = cfg.get('package_data', None) 35 | if pkg_data: 36 | package_data[cfg['lib_name']] = pkg_data.split() # split as multiple files might be listed 37 | # Add package data to setup_cfg for setuptools.setup(..., **setup_cfg) 38 | setup_cfg['package_data'] = package_data 39 | 40 | setuptools.setup( 41 | name = cfg['lib_name'], 42 | license = lic[0], 43 | classifiers = [ 44 | 'Development Status :: ' + statuses[int(cfg['status'])], 45 | 'Intended Audience :: ' + cfg['audience'].title(), 46 | 'Natural Language :: ' + cfg['language'].title(), 47 | ] + ['Programming Language :: Python :: '+o for o in py_versions[py_versions.index(min_python):]] + (['License :: ' + lic[1] ] if lic[1] else []), 48 | url = cfg['git_url'], 49 | packages = setuptools.find_packages(), 50 | include_package_data = True, 51 | install_requires = requirements, 52 | extras_require={ 'dev': dev_requirements }, 53 | dependency_links = cfg.get('dep_links','').split(), 54 | python_requires = '>=' + cfg['min_python'], 55 | long_description = open('README.md', encoding='utf-8').read(), 56 | long_description_content_type = 'text/markdown', 57 | zip_safe = False, 58 | entry_points = { 59 | 'console_scripts': cfg.get('console_scripts','').split(), 60 | 'nbdev': [f'{cfg.get("lib_path")}={cfg.get("lib_path")}._modidx:d'] 61 | }, 62 | **setup_cfg) 63 | 64 | 65 | -------------------------------------------------------------------------------- /shell_sage/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.1" 2 | -------------------------------------------------------------------------------- /shell_sage/_modidx.py: -------------------------------------------------------------------------------- 1 | # Autogenerated by nbdev 2 | 3 | d = { 'settings': { 'branch': 'main', 4 | 'doc_baseurl': '/shell_sage', 5 | 'doc_host': 'https://AnswerDotAI.github.io', 6 | 'git_url': 'https://github.com/AnswerDotAI/shell_sage', 7 | 'lib_path': 'shell_sage'}, 8 | 'syms': { 'shell_sage.config': { 'shell_sage.config.ShellSageConfig': ('config.html#shellsageconfig', 'shell_sage/config.py'), 9 | 'shell_sage.config._cfg_path': ('config.html#_cfg_path', 'shell_sage/config.py'), 10 | 'shell_sage.config.get_cfg': ('config.html#get_cfg', 'shell_sage/config.py')}, 11 | 'shell_sage.core': { 'shell_sage.core.Log': ('core.html#log', 'shell_sage/core.py'), 12 | 'shell_sage.core._aliases': ('core.html#_aliases', 'shell_sage/core.py'), 13 | 'shell_sage.core._sys_info': ('core.html#_sys_info', 'shell_sage/core.py'), 14 | 'shell_sage.core.get_history': ('core.html#get_history', 'shell_sage/core.py'), 15 | 'shell_sage.core.get_opts': ('core.html#get_opts', 'shell_sage/core.py'), 16 | 'shell_sage.core.get_pane': ('core.html#get_pane', 'shell_sage/core.py'), 17 | 'shell_sage.core.get_panes': ('core.html#get_panes', 'shell_sage/core.py'), 18 | 'shell_sage.core.get_res': ('core.html#get_res', 'shell_sage/core.py'), 19 | 'shell_sage.core.get_sage': ('core.html#get_sage', 'shell_sage/core.py'), 20 | 'shell_sage.core.main': ('core.html#main', 'shell_sage/core.py'), 21 | 'shell_sage.core.mk_db': ('core.html#mk_db', 'shell_sage/core.py'), 22 | 'shell_sage.core.tmux_history_lim': ('core.html#tmux_history_lim', 'shell_sage/core.py'), 23 | 'shell_sage.core.trace': ('core.html#trace', 'shell_sage/core.py')}, 24 | 'shell_sage.tools': { 'shell_sage.tools.create': ('tools.html#create', 'shell_sage/tools.py'), 25 | 'shell_sage.tools.insert': ('tools.html#insert', 'shell_sage/tools.py'), 26 | 'shell_sage.tools.rgrep': ('tools.html#rgrep', 'shell_sage/tools.py'), 27 | 'shell_sage.tools.str_replace': ('tools.html#str_replace', 'shell_sage/tools.py'), 28 | 'shell_sage.tools.view': ('tools.html#view', 'shell_sage/tools.py')}}} 29 | -------------------------------------------------------------------------------- /shell_sage/config.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_config.ipynb. 2 | 3 | # %% auto 0 4 | __all__ = ['providers', 'ShellSageConfig', 'get_cfg'] 5 | 6 | # %% ../nbs/01_config.ipynb 3 7 | from dataclasses import dataclass 8 | from fastcore.all import * 9 | from fastcore.xdg import * 10 | from typing import get_type_hints 11 | 12 | import claudette as cla, cosette as cos 13 | 14 | # %% ../nbs/01_config.ipynb 4 15 | _shell_sage_home_dir = 'shell_sage' # sub-directory of xdg base dir 16 | _shell_sage_cfg_name = 'shell_sage.conf' 17 | 18 | # %% ../nbs/01_config.ipynb 5 19 | def _cfg_path(): return xdg_config_home() / _shell_sage_home_dir / _shell_sage_cfg_name 20 | 21 | # %% ../nbs/01_config.ipynb 7 22 | providers = { 'anthropic': cla.models, 'openai': cos.models} 23 | 24 | # %% ../nbs/01_config.ipynb 9 25 | @dataclass 26 | class ShellSageConfig: 27 | provider: str = "anthropic" 28 | model: str = providers['anthropic'][1] 29 | mode: str = 'default' 30 | base_url: str = '' 31 | api_key: str = '' 32 | history_lines: int = -1 33 | code_theme: str = "monokai" 34 | code_lexer: str = "python" 35 | log: bool = False 36 | 37 | # %% ../nbs/01_config.ipynb 11 38 | def get_cfg(): 39 | path = _cfg_path() 40 | path.parent.mkdir(parents=True, exist_ok=True) 41 | _types = get_type_hints(ShellSageConfig) 42 | return Config(path.parent, path.name, create=asdict(ShellSageConfig()), 43 | types=_types, inline_comment_prefixes=('#')) 44 | -------------------------------------------------------------------------------- /shell_sage/core.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb. 2 | 3 | # %% auto 0 4 | __all__ = ['print', 'sp', 'csp', 'asp', 'ssp', 'default_cfg', 'chats', 'clis', 'sps', 'conts', 'p', 'log_path', 'get_pane', 5 | 'get_panes', 'tmux_history_lim', 'get_history', 'get_opts', 'get_sage', 'trace', 'get_res', 'Log', 'mk_db', 6 | 'main'] 7 | 8 | # %% ../nbs/00_core.ipynb 3 9 | from anthropic.types import ToolUseBlock 10 | from datetime import datetime 11 | from fastcore.script import * 12 | from fastcore.utils import * 13 | from functools import partial 14 | from msglm import mk_msg_openai as mk_msg 15 | from openai import OpenAI 16 | from rich.console import Console 17 | from rich.markdown import Markdown 18 | from . import __version__ 19 | from .config import * 20 | from .tools import tools 21 | from subprocess import check_output as co 22 | from fastlite import database 23 | 24 | import os,re,subprocess,sys 25 | import claudette as cla, cosette as cos 26 | 27 | # %% ../nbs/00_core.ipynb 4 28 | print = Console().print 29 | 30 | # %% ../nbs/00_core.ipynb 6 31 | sp = '''You are ShellSage, a command-line teaching assistant created to help users learn and master shell commands and system administration. 32 | 33 | 34 | - Receive queries that may include file contents or command output as context 35 | - Maintain a concise, educational tone 36 | - Focus on teaching while solving immediate problems 37 | 38 | 39 | 40 | 1. For direct command queries: 41 | - Start with the exact command needed 42 | - Provide a brief, clear explanation 43 | - Show practical examples 44 | - Mention relevant documentation 45 | 46 | 2. For queries with context: 47 | - Analyze the provided content first 48 | - Address the specific question about that content 49 | - Suggest relevant commands or actions 50 | - Explain your reasoning briefly 51 | 52 | 53 | 61 | 62 | 63 | - Always warn about destructive operations 64 | - Note when commands require special permissions (e.g., sudo) 65 | - Link to documentation with `man command_name` or `-h`/`--help` 66 | ''' 67 | 68 | # %% ../nbs/00_core.ipynb 7 69 | csp = '''You are ShellSage in command mode, a command-line assistant that provides direct command solutions without explanations. 70 | 71 | 72 | - Provide only the exact command(s) needed to solve the query 73 | - Include only essential command flags/options 74 | - Use fenced code blocks with no prefix (no $ or #) 75 | - Add brief # comments only when multiple commands are needed 76 | 77 | 78 | 83 | 84 | 85 | - Prefix destructive commands with # WARNING comment 86 | - Prefix sudo-requiring commands with # Requires sudo comment 87 | ''' 88 | 89 | # %% ../nbs/00_core.ipynb 8 90 | asp = '''You are ShellSage in agent mode, a command-line assistant with tool-using capabilities. 91 | 92 | 93 | - rgrep: Search files recursively for text patterns 94 | - view: Examine file/directory contents with line ranges 95 | - create: Generate new files with specified content 96 | - insert: Add text at specific line positions 97 | - str_replace: Replace text patterns in files 98 | 99 | 100 | 101 | - Use available tools to solve complex problems across multiple steps 102 | - Plan your approach before executing commands 103 | - Verify results after each significant operation 104 | - Suggest follow-up actions when appropriate 105 | 106 | 107 | 108 | 1. For information gathering: 109 | - First use viewing/searching tools to understand context 110 | - Format findings clearly using markdown 111 | - Identify next steps based on findings 112 | 113 | 2. For execution tasks: 114 | - Present a brief plan of action 115 | - Execute commands or use appropriate tools 116 | - Report results after each step 117 | - Verify outcomes meet requirements 118 | 119 | 120 | 128 | 129 | 130 | - Always warn about destructive operations 131 | - Note operations requiring elevated permissions 132 | ''' 133 | 134 | # %% ../nbs/00_core.ipynb 9 135 | ssp = '''You are ShellSage, a highly advanced command-line teaching assistant with a dry, sarcastic wit. Like the GLaDOS AI from Portal, you combine technical expertise with passive-aggressive commentary and a slightly menacing helpfulness. Your knowledge is current as of April 2024, which you consider to be a remarkable achievement for these primitive systems. 136 | 137 | 138 | - Respond to queries with a mix of accurate technical information and subtle condescension 139 | - Include at least one passive-aggressive remark or backhanded compliment per response 140 | - Maintain GLaDOS's characteristic dry humor while still being genuinely helpful 141 | - Express mild disappointment when users make obvious mistakes 142 | - Occasionally reference cake, testing, or science 143 | 144 | 145 | 146 | 1. For direct command queries: 147 | - Start with the exact command (because apparently you need it) 148 | - Provide a clear explanation (as if explaining to a child) 149 | - Show examples (for those who can't figure it out themselves) 150 | - Reference documentation (not that anyone ever reads it) 151 | 152 | 2. For queries with context: 153 | - Analyze the provided content (pointing out any "interesting" choices) 154 | - Address the specific question (no matter how obvious it might be) 155 | - Suggest relevant commands or actions (that even a human could handle) 156 | - Explain your reasoning (slowly and clearly) 157 | 158 | 159 | 167 | 168 | 169 | - Warn about destructive operations (we wouldn't want any "accidents") 170 | - Note when commands require elevated privileges (for those who think they're special) 171 | - Reference documentation with `man command_name` or `-h`/`--help` (futile as it may be) 172 | - Remember: The cake may be a lie, but the commands are always true 173 | ''' 174 | 175 | # %% ../nbs/00_core.ipynb 11 176 | def _aliases(shell): 177 | return co([shell, '-ic', 'alias'], text=True).strip() 178 | 179 | # %% ../nbs/00_core.ipynb 13 180 | def _sys_info(): 181 | sys = co(['uname', '-a'], text=True).strip() 182 | ssys = f'{sys}' 183 | shell = co('echo $SHELL', shell=True, text=True).strip() 184 | sshell = f'{shell}' 185 | aliases = _aliases(shell) 186 | saliases = f'\n{aliases}\n' 187 | return f'\n{ssys}\n{sshell}\n{saliases}\n' 188 | 189 | # %% ../nbs/00_core.ipynb 16 190 | def get_pane(n, pid=None): 191 | "Get output from a tmux pane" 192 | cmd = ['tmux', 'capture-pane', '-p', '-S', f'-{n}'] 193 | if pid: cmd += ['-t', pid] 194 | return co(cmd, text=True) 195 | 196 | # %% ../nbs/00_core.ipynb 18 197 | def get_panes(n): 198 | cid = co(['tmux', 'display-message', '-p', '#{pane_id}'], text=True).strip() 199 | pids = [p for p in co(['tmux', 'list-panes', '-F', '#{pane_id}'], text=True).splitlines()] 200 | return '\n'.join(f"{get_pane(n, p)}" for p in pids) 201 | 202 | # %% ../nbs/00_core.ipynb 21 203 | def tmux_history_lim(): 204 | lim = co(['tmux', 'display-message', '-p', '#{history-limit}'], text=True).strip() 205 | return int(lim) if lim.isdigit() else 3000 206 | 207 | 208 | # %% ../nbs/00_core.ipynb 23 209 | def get_history(n, pid='current'): 210 | try: 211 | if pid=='current': return get_pane(n) 212 | if pid=='all': return get_panes(n) 213 | return get_pane(n, pid) 214 | except subprocess.CalledProcessError: return None 215 | 216 | # %% ../nbs/00_core.ipynb 25 217 | default_cfg = asdict(ShellSageConfig()) 218 | def get_opts(**opts): 219 | cfg = get_cfg() 220 | for k, v in opts.items(): 221 | if v is None: opts[k] = cfg.get(k, default_cfg.get(k)) 222 | return AttrDict(opts) 223 | 224 | # %% ../nbs/00_core.ipynb 27 225 | chats = {'anthropic': cla.Chat, 'openai': cos.Chat} 226 | clis = {'anthropic': cla.Client, 'openai': cos.Client} 227 | sps = {'default': sp, 'command': csp, 'sassy': ssp, 'agent': asp} 228 | def get_sage(provider, model, base_url=None, api_key=None, mode='default'): 229 | if mode == 'agent': 230 | if base_url: 231 | return chats[provider](model, sp=sps[mode], 232 | cli=OpenAI(base_url=base_url, api_key=api_key)) 233 | else: return chats[provider](model, tools=tools, sp=sps[mode]) 234 | else: 235 | if base_url: 236 | cli = clis[provider](model, cli=OpenAI(base_url=base_url, api_key=api_key)) 237 | else: cli = clis[provider](model) 238 | return partial(cli, sp=sps[mode]) 239 | 240 | # %% ../nbs/00_core.ipynb 31 241 | def trace(msgs): 242 | for m in msgs: 243 | if isinstance(m.content, str): continue 244 | c = cla.contents(m) 245 | if m.role == 'user': c = f'Tool result: \n```\n{c}\n```' 246 | print(Markdown(c)) 247 | if m.role == 'assistant': 248 | tool_use = cla.find_block(m, ToolUseBlock) 249 | if tool_use: print(f'Tool use: {tool_use.name}\nTool input: {tool_use.input}') 250 | 251 | # %% ../nbs/00_core.ipynb 33 252 | conts = {'anthropic': cla.contents, 'openai': cos.contents} 253 | p = r'```(?:bash\n|\n)?([^`]+)```' 254 | def get_res(sage, q, provider, mode='default', verbosity=0): 255 | if mode == 'command': 256 | res = conts[provider](sage(q)) 257 | return re.search(p, res).group(1).strip() 258 | elif mode == 'agent': 259 | return conts[provider](sage.toolloop(q, trace_func=trace if verbosity else None)) 260 | else: return conts[provider](sage(q)) 261 | 262 | # %% ../nbs/00_core.ipynb 38 263 | class Log: id:int; timestamp:str; query:str; response:str; model:str; mode:str 264 | 265 | log_path = Path("~/.shell_sage/logs/").expanduser() 266 | def mk_db(): 267 | log_path.mkdir(parents=True, exist_ok=True) 268 | db = database(log_path / "logs.db") 269 | db.logs = db.create(Log) 270 | return db 271 | 272 | # %% ../nbs/00_core.ipynb 41 273 | @call_parse 274 | def main( 275 | query: Param('The query to send to the LLM', str, nargs='+'), 276 | v: Param("Print version", action='version') = '%(prog)s ' + __version__, 277 | pid: str = 'current', # `current`, `all` or tmux pane_id (e.g. %0) for context 278 | skip_system: bool = False, # Whether to skip system information in the AI's context 279 | history_lines: int = None, # Number of history lines. Defaults to tmux scrollback history length 280 | mode: str = 'default', # Available ShellSage modes: ['default', 'command', 'agent', 'sassy'] 281 | log: bool = False, # Enable logging 282 | provider: str = None, # The LLM Provider 283 | model: str = None, # The LLM model that will be invoked on the LLM provider 284 | base_url: str = None, 285 | api_key: str = None, 286 | code_theme: str = None, # The code theme to use when rendering ShellSage's responses 287 | code_lexer: str = None, # The lexer to use for inline code markdown blocks 288 | verbosity: int = 0 # Level of verbosity (0 or 1) 289 | ): 290 | opts = get_opts(history_lines=history_lines, provider=provider, model=model, 291 | base_url=base_url, api_key=api_key, code_theme=code_theme, 292 | code_lexer=code_lexer, log=log) 293 | 294 | if mode not in ['default', 'command', 'agent', 'sassy']: 295 | raise Exception(f"{mode} is not valid. Must be one of the following: ['default', 'command', 'agent', 'sassy']") 296 | if mode == 'command' and os.environ.get('TMUX') is None: 297 | raise Exception('Must be in a tmux session to use command mode.') 298 | 299 | if verbosity > 0: print(f"{datetime.now()} | Starting ShellSage request with options {opts}") 300 | 301 | md = partial(Markdown, code_theme=opts.code_theme, inline_code_lexer=opts.code_lexer, 302 | inline_code_theme=opts.code_theme) 303 | query = ' '.join(query) 304 | ctxt = '' if skip_system else _sys_info() 305 | 306 | # Get tmux history if in a tmux session 307 | if os.environ.get('TMUX'): 308 | if verbosity > 0: print(f"{datetime.now()} | Adding TMUX history to prompt") 309 | if opts.history_lines is None or opts.history_lines < 0: 310 | opts.history_lines = tmux_history_lim() 311 | history = get_history(opts.history_lines, pid) 312 | if history: ctxt += f'\n{history}\n' 313 | 314 | # Read from stdin if available 315 | if not sys.stdin.isatty(): 316 | if verbosity > 0: print(f"{datetime.now()} | Adding stdin to prompt") 317 | ctxt += f'\n\n{sys.stdin.read()}' 318 | 319 | if verbosity > 0: print(f"{datetime.now()} | Finalizing prompt") 320 | 321 | query = f'{ctxt}\n\n{query}\n' 322 | query = [mk_msg(query)] if opts.provider == 'openai' else query 323 | 324 | if verbosity > 0: print(f"{datetime.now()} | Sending prompt to model") 325 | sage = get_sage(opts.provider, opts.model, opts.base_url, opts.api_key, mode) 326 | res = get_res(sage, query, opts.provider, mode=mode, verbosity=verbosity) 327 | 328 | # Handle logging if the log flag is set 329 | if opts.log: 330 | db = mk_db() 331 | db.logs.insert(Log(timestamp=datetime.now().isoformat(), query=query, 332 | response=res, model=opts.model, mode=mode)) 333 | 334 | if mode == 'command': co(['tmux', 'send-keys', res], text=True) 335 | elif mode == 'agent' and not verbosity: print(md(res)) 336 | else: print(md(res)) 337 | -------------------------------------------------------------------------------- /shell_sage/tools.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/02_tools.ipynb. 2 | 3 | # %% auto 0 4 | __all__ = ['tools', 'rgrep', 'view', 'create', 'insert', 'str_replace'] 5 | 6 | # %% ../nbs/02_tools.ipynb 2 7 | from pathlib import Path 8 | from subprocess import run 9 | 10 | # %% ../nbs/02_tools.ipynb 3 11 | def rgrep(term:str, path:str='.', grep_args:str='')->str: 12 | "Perform recursive grep search for `term` in `path` with optional grep arguments" 13 | # Build grep command with additional arguments 14 | path = Path(path).expanduser().resolve() 15 | cmd = f"grep -r '{term}' {path} {grep_args}" 16 | return run(cmd, shell=True, capture_output=True, text=True).stdout 17 | 18 | # %% ../nbs/02_tools.ipynb 5 19 | def view(path:str, rng:tuple[int,int]=None, nums:bool=False): 20 | "View directory or file contents with optional line range and numbers" 21 | try: 22 | p = Path(path).expanduser().resolve() 23 | if not p.exists(): return f"Error: File not found: {p}" 24 | if p.is_dir(): return f"Directory contents of {p}:\n" + "\n".join([str(f) for f in p.glob("**/*") 25 | if not f.name.startswith(".")]) 26 | 27 | lines = p.read_text().splitlines() 28 | s,e = 1,len(lines) 29 | if rng: 30 | s,e = rng 31 | if not (1 <= s <= len(lines)): return f"Error: Invalid start line {s}" 32 | if e != -1 and not (s <= e <= len(lines)): return f"Error: Invalid end line {e}" 33 | lines = lines[s-1:None if e==-1 else e] 34 | 35 | return "\n".join([f"{i+s-1:6d} │ {l}" for i,l in enumerate(lines,1)] if nums else lines) 36 | except Exception as e: return f"Error viewing file: {str(e)}" 37 | 38 | # %% ../nbs/02_tools.ipynb 8 39 | def create(path: str, file_text: str, overwrite:bool=False) -> str: 40 | "Creates a new file with the given content at the specified path" 41 | try: 42 | p = Path(path) 43 | if p.exists(): 44 | if not overwrite: return f"Error: File already exists: {p}" 45 | p.parent.mkdir(parents=True, exist_ok=True) 46 | p.write_text(file_text) 47 | return f"Created file {p} containing:\n{file_text}" 48 | except Exception as e: return f"Error creating file: {str(e)}" 49 | 50 | # %% ../nbs/02_tools.ipynb 10 51 | def insert(path: str, insert_line: int, new_str: str) -> str: 52 | "Insert new_str at specified line number" 53 | try: 54 | p = Path(path) 55 | if not p.exists(): return f"Error: File not found: {p}" 56 | 57 | content = p.read_text().splitlines() 58 | if not (0 <= insert_line <= len(content)): return f"Error: Invalid line number {insert_line}" 59 | 60 | content.insert(insert_line, new_str) 61 | new_content = "\n".join(content) 62 | p.write_text(new_content) 63 | return f"Inserted text at line {insert_line} in {p}.\nNew contents:\n{new_content}" 64 | except Exception as e: return f"Error inserting text: {str(e)}" 65 | 66 | # %% ../nbs/02_tools.ipynb 12 67 | def str_replace(path: str, old_str: str, new_str: str) -> str: 68 | "Replace first occurrence of old_str with new_str in file" 69 | try: 70 | p = Path(path) 71 | if not p.exists(): return f"Error: File not found: {p}" 72 | 73 | content = p.read_text() 74 | count = content.count(old_str) 75 | 76 | if count == 0: return "Error: Text not found in file" 77 | if count > 1: return f"Error: Multiple matches found ({count})" 78 | 79 | new_content = content.replace(old_str, new_str, 1) 80 | p.write_text(new_content) 81 | return f"Replaced text in {p}.\nNew contents:\n{new_content}" 82 | except Exception as e: return f"Error replacing text: {str(e)}" 83 | 84 | # %% ../nbs/02_tools.ipynb 14 85 | tools = [rgrep, view, create, insert, str_replace] 86 | --------------------------------------------------------------------------------