├── .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 = '''
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'
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