├── senpai ├── data │ ├── __init__.py │ ├── config.py │ └── history.py ├── lib │ ├── __init__.py │ ├── color.py │ └── user_input.py ├── __init__.py ├── api.py ├── terminal.py ├── main.py └── senpai.py ├── packages ├── debian │ ├── compat │ ├── rules │ ├── control │ ├── changelog │ └── copyright ├── win │ ├── requirements.txt │ ├── run.py │ └── install.iss ├── arch │ ├── .SRCINFO │ └── PKGBUILD ├── rpm │ └── senpai-cli.spec └── brew │ └── senpai-cli.rb ├── media ├── app.ico └── screenshot.png ├── requirements.txt ├── .gitignore ├── run.sh ├── setup.py ├── README.md └── LICENSE /senpai/data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/debian/compat: -------------------------------------------------------------------------------- 1 | 11 2 | -------------------------------------------------------------------------------- /packages/win/requirements.txt: -------------------------------------------------------------------------------- 1 | colorama==0.4.6 2 | pywin32==306 3 | -------------------------------------------------------------------------------- /media/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BashSenpai/cli/HEAD/media/app.ico -------------------------------------------------------------------------------- /media/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BashSenpai/cli/HEAD/media/screenshot.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2023.5.7 2 | charset-normalizer==3.1.0 3 | idna==3.4 4 | requests==2.31.0 5 | toml==0.10.2 6 | urllib3==2.0.2 7 | -------------------------------------------------------------------------------- /packages/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | export PYBILD_NAME = senpai-cli 4 | 5 | %: 6 | dh $@ --with python3 --buildsystem=pybuild 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # python bytecode 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # build 7 | build/ 8 | dist/ 9 | 10 | # app specific 11 | .venv/ 12 | -------------------------------------------------------------------------------- /packages/arch/.SRCINFO: -------------------------------------------------------------------------------- 1 | pkgbase = senpai-cli 2 | pkgdesc = BashSenpai is a terminal assistant powered by ChatGPT. 3 | pkgver = 1.0 4 | pkgrel = 1 5 | url = https://github.com/BashSenpai 6 | arch = x86_64 7 | license = Apache 8 | makedepends = python-setuptools 9 | depends = python-requests 10 | depends = python-toml 11 | provides = senpai 12 | source = https://github.com/BashSenpai/cli/archive/refs/tags/v1.0.tar.gz 13 | sha256sums = 374dbf636aa7219f71bb7711a329e2bc8824efab99f39855c8c6b76cca40206b 14 | 15 | pkgname = senpai-cli 16 | -------------------------------------------------------------------------------- /packages/debian/control: -------------------------------------------------------------------------------- 1 | Source: senpai-cli 2 | Maintainer: Bogdan Tatarov 3 | Build-Depends: debhelper,dh-python,python3-all,python3-setuptools,python3-requests,python3-toml 4 | Section: utils 5 | Priority: optional 6 | Standards-Version: 3.9.6 7 | X-Python3-Version: >= 3.10 8 | 9 | Package: senpai-cli 10 | Architecture: all 11 | Depends: ${python3:Depends},python3-requests,python3-toml 12 | Description: BashSenpai command-line interface. 13 | BashSenpai is a terminal assistant powered by ChatGPT that transforms instructions into ready-to-use commands. 14 | -------------------------------------------------------------------------------- /senpai/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Bogdan Tatarov 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /senpai/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Bogdan Tatarov 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __version__ = '1.0' 16 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2023 Bogdan Tatarov 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | python -m senpai.main $@ 17 | -------------------------------------------------------------------------------- /packages/win/run.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Bogdan Tatarov 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import colorama 16 | from senpai.main import main 17 | 18 | if __name__ == '__main__': 19 | colorama.just_fix_windows_console() 20 | main() 21 | -------------------------------------------------------------------------------- /packages/arch/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Bogdan Tatarov 2 | pkgname=senpai-cli 3 | _appname=senpai 4 | _reponame=cli 5 | pkgver=1.0 6 | pkgrel=1 7 | pkgdesc='BashSenpai is a terminal assistant powered by ChatGPT.' 8 | arch=('x86_64') 9 | url='https://github.com/BashSenpai' 10 | license=('Apache') 11 | groups=() 12 | depends=('python-requests' 'python-toml') 13 | makedepends=('python-setuptools') 14 | optdepends=() 15 | provides=('senpai') 16 | source=("https://github.com/BashSenpai/${_reponame}/archive/refs/tags/v${pkgver}.tar.gz") 17 | sha256sums=('374dbf636aa7219f71bb7711a329e2bc8824efab99f39855c8c6b76cca40206b') 18 | 19 | build() { 20 | cd "${_reponame}-${pkgver}" 21 | python setup.py build 22 | } 23 | 24 | package() { 25 | cd "${_reponame}-${pkgver}" 26 | python setup.py install --root="$pkgdir" 27 | } 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Bogdan Tatarov 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from setuptools import setup, find_packages 16 | 17 | from senpai.main import get_version 18 | 19 | 20 | def readme(): 21 | with open('README.md') as f: 22 | return f.read() 23 | 24 | setup( 25 | name='senpai-cli', 26 | version=get_version(), 27 | description='BashSenpai command-line interface', 28 | long_description=readme(), 29 | long_description_content_type='text/markdown', 30 | url='https://BashSenpai.com/', 31 | author='Bogdan Tatarov', 32 | author_email='bogdan@tatarov.me', 33 | license='Apache-2.0', 34 | install_requires=('requests', 'toml'), 35 | packages=find_packages(), 36 | entry_points=dict( 37 | console_scripts=['senpai=senpai.main:main'] 38 | ) 39 | ) 40 | -------------------------------------------------------------------------------- /packages/debian/changelog: -------------------------------------------------------------------------------- 1 | senpai-cli (1.0) lunar; urgency=medium 2 | 3 | - Improvement: send history only if logs are not older than 7 hours 4 | - Fix: new version check works again 5 | 6 | -- Bogdan Tatarov Wed, 13 Sep 2023 09:54:00 +0800 7 | 8 | senpai-cli (0.82b) lunar; urgency=medium 9 | 10 | - Feature: new prompt `explain ` 11 | - Feature: animate the loading message 12 | - Improvement: better `--help` output 13 | 14 | -- Bogdan Tatarov Mon, 12 Jun 2023 22:32:00 +0300 15 | 16 | senpai-cli (0.81b) lunar; urgency=medium 17 | 18 | * Update: stream prompt responses 19 | * Update: simplify API calls 20 | 21 | -- Bogdan Tatarov Wed, 07 Jun 2023 18:02:00 +0300 22 | 23 | senpai-cli (0.80b) lunar; urgency=medium 24 | 25 | * Feature: prettier output style 26 | * Feature: check if there is a new version available 27 | * Various small bug fixes and improvements 28 | 29 | -- Bogdan Tatarov Sat, 27 May 2023 21:36:00 +0300 30 | 31 | senpai-cli (0.79b) lunar; urgency=medium 32 | 33 | * Feature: send optional metadata about the user's environment 34 | 35 | -- Bogdan Tatarov Sat, 27 May 2023 21:36:00 +0300 36 | 37 | senpai-cli (0.78b-1) lunar; urgency=medium 38 | 39 | * Fix: handle internal server errors on API calls 40 | * Fix: handle multi-line inputs in the command execution menu 41 | * Fix: proper color escaping for readline 42 | 43 | -- Bogdan Tatarov Sat, 27 May 2023 17:27:00 +0300 44 | 45 | senpai-cli (0.77b) lunar; urgency=medium 46 | 47 | * Feature: handle various response errors from the API 48 | 49 | -- Bogdan Tatarov Sat, 27 May 2023 07:47:00 +0300 50 | 51 | senpai-cli (0.75b) lunar; urgency=medium 52 | 53 | * Feature: menu to execute any provided command directly in the terminal 54 | 55 | -- Bogdan Tatarov Sat, 27 May 2023 01:24:00 +0300 56 | 57 | senpai-cli (0.71b) lunar; urgency=medium 58 | 59 | * Various fixes in metadata 60 | * Other small bug fixes 61 | 62 | -- Bogdan Tatarov Mon, 22 May 2023 05:10:58 +0300 63 | 64 | senpai-cli (0.70b-3ubuntu3) lunar; urgency=medium 65 | 66 | * First public release 67 | 68 | -- Bogdan Tatarov Mon, 22 May 2023 02:10:58 +0300 69 | -------------------------------------------------------------------------------- /senpai/lib/color.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Bogdan Tatarov 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import sys 16 | 17 | # Dictionary of all 4-bit ANSI colors 18 | COLOR = { 19 | 'black': ('30', '30'), 20 | 'white': ('97', '97'), 21 | 'gray': ('90', '37'), 22 | 'red': ('31', '91'), 23 | 'green': ('32', '92'), 24 | 'yellow': ('33', '93'), 25 | 'blue': ('34', '94'), 26 | 'magenta': ('35', '95'), 27 | 'cyan': ('36', '96'), 28 | } 29 | 30 | def parse_color(color: str) -> str: 31 | """ 32 | Convert a color name to an ANSI-formatted string. 33 | 34 | Args: 35 | color (str): The name of the color to be parsed. Valid color names 36 | include 'black', 'white', 'gray', 'red', 'green', 'yellow', 'blue', 37 | 'magenta', and 'cyan'. The color name can be optionally preceded 38 | by 'bright' or 'bold' to create bright or bold color codes. 39 | 40 | Returns: 41 | str: ANSI-formatted string representing the color. The string includes 42 | escape sequences for changing the color of text on ANSI-compliant 43 | terminals. The returned string follows the format 44 | '\x1B[m', where is an ANSI color code. 45 | 46 | Note: 47 | If the system platform is either 'win32' or 'cygwin', the returned 48 | string does not include the initial '\1' and trailing '\2' characters. 49 | These are included on other platforms to enable changing the color of a 50 | portion of text, with the color change ending at the '\2' character. 51 | """ 52 | pos = 1 if 'bright' in color else 0 53 | col_prefix = '\1\x1B[;1m\2' if 'bold' in color else '' 54 | for col_name, col_values in COLOR.items(): 55 | if col_name in color: 56 | color = f'{col_prefix}\1\x1B[{col_values[pos]}m\2%s\1\x1B[0m\2' 57 | 58 | if sys.platform in ('win32', 'cygwin'): 59 | color = color.replace('\1', '').replace('\2', '') 60 | 61 | return color 62 | -------------------------------------------------------------------------------- /packages/rpm/senpai-cli.spec: -------------------------------------------------------------------------------- 1 | # 2 | # spec file for package senpai-cli 3 | # 4 | # Copyright 2023 Bogdan Tatarov 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | Name: senpai-cli 19 | Version: 1.0 20 | Release: 1 21 | Summary: BashSenpai command-line interface 22 | 23 | License: Apache-2.0 24 | Source0: https://github.com/BashSenpai/cli/archive/refs/tags/v%{version}.tar.gz 25 | 26 | BuildArch: noarch 27 | 28 | BuildRequires: python3-devel 29 | BuildRequires: python3-setuptools 30 | BuildRequires: python3-requests 31 | BuildRequires: python3-toml 32 | 33 | Requires: python3-requests 34 | Requires: python3-toml 35 | 36 | %description 37 | BashSenpai is a terminal assistant powered by ChatGPT that transforms instructions into ready-to-use commands. 38 | 39 | 40 | %prep 41 | %autosetup -n cli-%{version} 42 | 43 | 44 | %build 45 | %py3_build 46 | 47 | 48 | %install 49 | %py3_install 50 | 51 | 52 | %files 53 | %doc README.md 54 | %license LICENSE 55 | %{_bindir}/senpai 56 | %{python3_sitelib}/senpai_cli-*.egg-info/ 57 | %{python3_sitelib}/senpai/ 58 | 59 | 60 | %changelog 61 | * Wed Sep 13 2023 Bogdan Tatarov 1.0-1 62 | - Improvement: send history only if logs are not older than 7 hours (bogdan@tatarov.me) 63 | - Fix: new version check works again (bogdan@tatarov.me) 64 | * Mon Jun 12 2023 Bogdan Tatarov 0.82b-1 65 | - Feature: new prompt `explain ` (bogdan@tatarov.me) 66 | - Feature: animate the loading message (bogdan@tatarov.me) 67 | - Improvement: better `--help` output (bogdan@tatarov.me) 68 | * Wed Jun 07 2023 Bogdan Tatarov 0.81b-1 69 | - Update: stream prompt responses (bogdan@tatarov.me) 70 | - Update: simplify API calls (bogdan@tatarov.me) 71 | * Wed May 31 2023 Bogdan Tatarov 0.80b-1 72 | - Feature: prettier output style (bogdan@tatarov.me) 73 | - Feature: check if there is a new version available (bogdan@tatarov.me) 74 | - Various small bug fixes and improvements (bogdan@tatarov.me) 75 | * Sat May 27 2023 Bogdan Tatarov 0.79b-2 76 | - Fix: --version shows wrong version number (bogdan@tatarov.me) 77 | * Sat May 27 2023 Bogdan Tatarov 0.79b-1 78 | - Feature: send optional metadata about the user's environment (bogdan@tatarov.me) 79 | * Sat May 27 2023 Bogdan Tatarov 0.78b-1 80 | - Fix: handle internal server errors on API calls (bogdan@tatarov.me) 81 | - Fix: handle multi-line inputs in the command execution menu (bogdan@tatarov.me) 82 | - Fix: proper color escaping for readline (bogdan@tatarov.me) 83 | * Sat May 27 2023 Bogdan Tatarov 0.77b-1 84 | - Feature: better error handling on API error (bogdan@tatarov.me) 85 | * Sat May 27 2023 Bogdan Tatarov 0.75b-1 86 | - Feature: menu to execute any provided command directly in the terminal 87 | * Mon May 22 2023 Bogdan Tatarov 0.71b-1 88 | - various changes in metadata (bogdan@tatarov.me) 89 | - small bug fixes (bogdan@tatarov.me) 90 | * Mon May 22 2023 Bogdan Tatarov 0.70b-1 91 | - First public release (bogdan@tatarov.me) 92 | -------------------------------------------------------------------------------- /packages/brew/senpai-cli.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Bogdan Tatarov 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | class SenpaiCli < Formula 16 | include Language::Python::Virtualenv 17 | 18 | desc "BashSenpai command-line interface" 19 | homepage "https://bashsenpai.com/" 20 | url "https://github.com/BashSenpai/cli/archive/refs/tags/v1.0.tar.gz" 21 | sha256 "374dbf636aa7219f71bb7711a329e2bc8824efab99f39855c8c6b76cca40206b" 22 | license "Apache-2.0" 23 | 24 | depends_on "python@3.11" 25 | 26 | resource "certifi" do 27 | url "https://files.pythonhosted.org/packages/93/71/752f7a4dd4c20d6b12341ed1732368546bc0ca9866139fe812f6009d9ac7/certifi-2023.5.7.tar.gz" 28 | sha256 "0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7" 29 | end 30 | 31 | resource "charset-nomalizer" do 32 | url "https://files.pythonhosted.org/packages/ff/d7/8d757f8bd45be079d76309248845a04f09619a7b17d6dfc8c9ff6433cac2/charset-normalizer-3.1.0.tar.gz" 33 | sha256 "34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5" 34 | end 35 | 36 | resource "gnureadline" do 37 | if Hardware::CPU.arm? 38 | url "https://files.pythonhosted.org/packages/83/03/65d82e9290ae8a2a3b2285dc8aebd304437a6ba7ad03823438730525ab45/gnureadline-8.1.2-cp311-cp311-macosx_11_0_arm64.whl", :using => :nounzip 39 | sha256 "74f2538ac15ff4ef9534823abdef077bb34c7dd343e204a36d978f09e168462f" 40 | else 41 | url "https://files.pythonhosted.org/packages/a7/f2/77195ef94f56b61ad881685e3a87cc39a9972e01ccacd555acaa001a92a0/gnureadline-8.1.2-cp311-cp311-macosx_10_9_x86_64.whl", :using => :nounzip 42 | sha256 "c1bcb32e3b63442570d6425055aa6d5c3b6e8b09b9c7d1f8333e70203166a5a3" 43 | end 44 | end 45 | 46 | resource "idna" do 47 | url "https://files.pythonhosted.org/packages/8b/e1/43beb3d38dba6cb420cefa297822eac205a277ab43e5ba5d5c46faf96438/idna-3.4.tar.gz" 48 | sha256 "814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4" 49 | end 50 | 51 | resource "requests" do 52 | url "https://files.pythonhosted.org/packages/9d/be/10918a2eac4ae9f02f6cfe6414b7a155ccd8f7f9d4380d62fd5b955065c3/requests-2.31.0.tar.gz" 53 | sha256 "942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" 54 | end 55 | 56 | resource "toml" do 57 | url "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz" 58 | sha256 "b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 59 | end 60 | 61 | resource "urllib3" do 62 | url "https://files.pythonhosted.org/packages/fb/c0/1abba1a1233b81cf2e36f56e05194f5e8a0cec8c03c244cab56cc9dfb5bd/urllib3-2.0.2.tar.gz" 63 | sha256 "61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc" 64 | end 65 | 66 | def install 67 | python = "python3.11" 68 | venv = virtualenv_create(libexec, python) 69 | 70 | resources.each do |r| 71 | if r.name.eql? "gnureadline" 72 | r.stage do 73 | if Hardware::CPU.arm? 74 | venv.pip_install "gnureadline-8.1.2-cp311-cp311-macosx_11_0_arm64.whl" 75 | else 76 | venv.pip_install "gnureadline-8.1.2-cp311-cp311-macosx_10_9_x86_64.whl" 77 | end 78 | end 79 | else 80 | venv.pip_install r 81 | end 82 | end 83 | 84 | system libexec/"bin/python", "setup.py", "build" 85 | system libexec/"bin/python", *Language::Python.setup_install_args(prefix, python) 86 | end 87 | 88 | test do 89 | assert_equal "New persona confirmed.", shell_output("#{bin}/senpai become default").strip 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /packages/win/install.iss: -------------------------------------------------------------------------------- 1 | ; Copyright 2023 Bogdan Tatarov, Nikolay Dyankov 2 | ; 3 | ; Licensed under the Apache License, Version 2.0 (the "License"); 4 | ; you may not use this file except in compliance with the License. 5 | ; You may obtain a copy of the License at 6 | ; 7 | ; http://www.apache.org/licenses/LICENSE-2.0 8 | ; 9 | ; Unless required by applicable law or agreed to in writing, software 10 | ; distributed under the License is distributed on an "AS IS" BASIS, 11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ; See the License for the specific language governing permissions and 13 | ; limitations under the License. 14 | 15 | #define MyAppName "BashSenpai CLI" 16 | #define MyAppVersion "1.0" 17 | #define MyAppPublisher "Bash Senpai" 18 | #define MyAppURL "https://bashsenpai.com/" 19 | #define MyAppExeName "senpai.exe" 20 | 21 | [Setup] 22 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 23 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 24 | AppId={{9C7EC14D-71A8-4479-8790-7257B68B403D} 25 | AppName={#MyAppName} 26 | AppVersion={#MyAppVersion} 27 | ;AppVerName={#MyAppName} {#MyAppVersion} 28 | AppPublisher={#MyAppPublisher} 29 | AppPublisherURL={#MyAppURL} 30 | AppSupportURL={#MyAppURL} 31 | AppUpdatesURL={#MyAppURL} 32 | DefaultDirName={autopf}\BashSenpai 33 | DefaultGroupName={#MyAppName} 34 | DisableProgramGroupPage=yes 35 | LicenseFile=..\..\LICENSE 36 | ; Remove the following line to run in administrative install mode (install for all users.) 37 | ; PrivilegesRequired=lowest 38 | ; PrivilegesRequiredOverridesAllowed=dialog 39 | ChangesEnvironment=yes 40 | OutputDir=. 41 | OutputBaseFilename=BashSenpaiSetup-v{#MyAppVersion} 42 | SetupIconFile=..\..\media\app.ico 43 | Compression=lzma 44 | SolidCompression=yes 45 | WizardStyle=modern 46 | WizardSizePercent=100,120 47 | 48 | [Languages] 49 | Name: "english"; MessagesFile: "compiler:Default.isl" 50 | 51 | [Files] 52 | Source: ".\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion 53 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 54 | 55 | [Icons] 56 | Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 57 | 58 | [Tasks] 59 | Name: envPath; Description: "Add BashSenpai CLI to the PATH environment variable" 60 | 61 | [Code] 62 | const EnvironmentKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'; 63 | 64 | procedure EnvAddPath(Path: string); 65 | var 66 | Paths: string; 67 | begin 68 | { Retrieve current path (use empty string if entry not exists) } 69 | if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) 70 | then Paths := ''; 71 | 72 | { Skip if string already found in path } 73 | if Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';') > 0 then exit; 74 | 75 | { App string to the end of the path variable } 76 | Paths := Paths + ';'+ Path +';' 77 | 78 | { Overwrite (or create if missing) path environment variable } 79 | if RegWriteStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) 80 | then Log(Format('The [%s] added to PATH: [%s]', [Path, Paths])) 81 | else Log(Format('Error while adding the [%s] to PATH: [%s]', [Path, Paths])); 82 | end; 83 | 84 | procedure EnvRemovePath(Path: string); 85 | var 86 | Paths: string; 87 | P: Integer; 88 | begin 89 | { Skip if registry entry not exists } 90 | if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) then 91 | exit; 92 | 93 | { Skip if string not found in path } 94 | P := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';'); 95 | if P = 0 then exit; 96 | 97 | { Update path variable } 98 | Delete(Paths, P - 1, Length(Path) + 1); 99 | 100 | { Overwrite path environment variable } 101 | if RegWriteStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) 102 | then Log(Format('The [%s] removed from PATH: [%s]', [Path, Paths])) 103 | else Log(Format('Error while removing the [%s] from PATH: [%s]', [Path, Paths])); 104 | end; 105 | 106 | procedure CurStepChanged(CurStep: TSetupStep); 107 | begin 108 | if (CurStep = ssPostInstall) and WizardIsTaskSelected('envPath') 109 | then EnvAddPath(ExpandConstant('{app}')); 110 | end; 111 | 112 | procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); 113 | begin 114 | if CurUninstallStep = usPostUninstall 115 | then EnvRemovePath(ExpandConstant('{app}')); 116 | end; 117 | -------------------------------------------------------------------------------- /senpai/data/config.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Bogdan Tatarov 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from pathlib import Path 16 | import toml 17 | from typing import Union 18 | 19 | 20 | class Config: 21 | """ 22 | Config handles the user configuration settings for the BashSenpai tool. 23 | 24 | It loads the user's config file in TOML format, allows getting and setting 25 | specific configuration values, and writing the updated configuration file. 26 | 27 | Attributes: 28 | path (Path): The path to the user configuration file. 29 | _config (dict): The dictionary holding the current configuration values. 30 | 31 | Usage: 32 | >>> config = Config(path=Path('/path/to/config')) 33 | >>> token = config.get_value('token') 34 | >>> config.set_value('token', '') 35 | >>> config.write() 36 | """ 37 | 38 | def __init__(self, path: Path) -> None: 39 | """ 40 | Initialize the Config object with the provided configuration file path. 41 | 42 | Args: 43 | path (Path): The path to the directory where the config file is 44 | located. 45 | """ 46 | self.path = path / 'config.toml' 47 | self._load() 48 | 49 | def _load(self) -> None: 50 | """ 51 | Load the configuration file and set the configuration values. 52 | 53 | If the file doesn't exist, creates a new dictionary with default values. 54 | """ 55 | try: 56 | with open(self.path, 'r') as f: 57 | config = toml.load(f) 58 | except FileNotFoundError: 59 | config = {'main': {}} 60 | 61 | self._config = { 62 | 'TOKEN': config['main'].get('token', None), 63 | 'PERSONA': config['main'].get('persona', 'default'), 64 | 'PROG': config['main'].get('prog', 'senpai'), 65 | 'VERSION': config['main'].get('version', '0'), 66 | 'COMMAND_COLOR': config['main'].get('command_color', 'bold bright blue'), 67 | 'COMMENT_COLOR': config['main'].get('comment_color', 'bright gray'), 68 | 'EXECUTE': config['main'].get('execute', True), 69 | 'METADATA': config['main'].get('metadata', True), 70 | } 71 | 72 | def get_value(self, setting: str) -> Union[str, None]: 73 | """ 74 | Get the value of a specific configuration setting. 75 | 76 | Args: 77 | setting (str): The name of the configuration setting. 78 | 79 | Returns: 80 | str or None: The value of the configuration setting, or None if 81 | not found. 82 | """ 83 | return self._config.get(setting.upper(), None) 84 | 85 | def set_value(self, setting: str, value: Union[str, bool]) -> None: 86 | """ 87 | Set the value of a specific configuration setting. 88 | 89 | Args: 90 | setting (str): The name of the configuration setting. 91 | value (str | bool): The new value for the configuration setting. 92 | """ 93 | self._config[setting.upper()] = value 94 | 95 | def write(self) -> None: 96 | """ 97 | Write the current configuration values to the user config file. 98 | 99 | The config file is stored in TOML format. If a configuration file does 100 | not exist, a new one is created. 101 | """ 102 | with open(self.path, 'w') as f: 103 | config_data = { 104 | 'main': { 105 | 'token': self._config['TOKEN'], 106 | 'persona': self._config['PERSONA'], 107 | 'prog': self._config['PROG'], 108 | 'version': self._config['VERSION'], 109 | 'command_color': self._config['COMMAND_COLOR'], 110 | 'comment_color': self._config['COMMENT_COLOR'], 111 | 'execute': self._config['EXECUTE'], 112 | 'metadata': self._config['METADATA'], 113 | } 114 | } 115 | toml.dump(config_data, f) 116 | -------------------------------------------------------------------------------- /senpai/data/history.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Bogdan Tatarov 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from datetime import datetime 16 | import json 17 | from pathlib import Path 18 | from typing import Any, Dict, List, Union 19 | 20 | 21 | # time delta in seconds after which the history is skipped 22 | DELTA_HISTORY = 7 * 60 * 60 23 | 24 | 25 | class History: 26 | """ 27 | This class is responsible for managing the user's interaction history with 28 | the BashSenpai tool. 29 | 30 | The class loads the previous history from a JSON file upon initialization. 31 | It allows adding new prompts to the history, clearing the history, 32 | retrieving the current history, and writing the history to the file. 33 | 34 | Attributes: 35 | path (Path): Path to the JSON file storing the history. 36 | _history (List[Dict[str, Union[str, List[Any]]]]): Loaded history data. 37 | 38 | Usage: 39 | >>> history = History(path=Path('/path/to/history')) 40 | >>> history.add({ 41 | >>> 'question': 'how to list files', 'answer': 'ls -l', 'persona': '' 42 | >>> }) 43 | >>> history.write() 44 | >>> prompts = history.get_history() 45 | """ 46 | 47 | def __init__(self, path: Path) -> None: 48 | """ 49 | Initializes the History object, sets the path to the history file, and 50 | loads the history. 51 | 52 | Args: 53 | path (Path): The path to the directory where the history file is 54 | located. 55 | """ 56 | self.path = path / 'history.json' 57 | self._load() 58 | 59 | def _load(self) -> None: 60 | """ 61 | Private method to load user history from the history file. 62 | If the history file does not exist, initialize an empty history. 63 | """ 64 | self._history = list() 65 | if self.path.exists(): 66 | with open(self.path, 'r') as f: 67 | self._history = json.load(f) 68 | 69 | # convert old json history 70 | for idx, history_message in enumerate(self._history): 71 | if isinstance(history_message.get('answer'), list): 72 | answer = '' 73 | for line in history_message['answer']: 74 | line_type = line.get('type') 75 | if line_type == 'comment': 76 | answer += '# ' + line['data'] + '\n' 77 | elif line_type == 'command': 78 | answer += '$ ' + line['data'] + '\n' 79 | self._history[idx]['answer'] = answer.strip() 80 | if isinstance(history_message.get('persona'), list): 81 | answer = '' 82 | for line in history_message['persona']: 83 | line_type = line.get('type') 84 | if line_type == 'comment': 85 | answer += '# ' + line['data'] + '\n' 86 | elif line_type == 'command': 87 | answer += '$ ' + line['data'] + '\n' 88 | self._history[idx]['persona'] = answer.strip() or None 89 | self.write() 90 | 91 | 92 | def add(self, prompt: dict[str, Union[str, list[Any]]]) -> None: 93 | """ 94 | Adds a new prompt to the user history. 95 | 96 | Args: 97 | prompt (Dict[str, Union[str, List[Any]]]): The prompt to be added to 98 | the history. 99 | """ 100 | self._history.append(prompt) 101 | 102 | def clear(self) -> None: 103 | """Clear the previous user history.""" 104 | self._history = list() 105 | 106 | def get_history(self) -> List[Union[Dict[str, str], Any]]: 107 | """ 108 | Returns the current user interaction history if not older than 7 hours. 109 | 110 | Returns: 111 | List[Dict[str, Union[str, List[Any]]]]: The current user's 112 | interaction history. 113 | """ 114 | now = datetime.now().timestamp() 115 | return list(filter( 116 | lambda x: now - x.get('timestamp', 0) < DELTA_HISTORY, 117 | self._history, 118 | )) 119 | 120 | def write(self) -> None: 121 | """ 122 | Writes the current user history to the history log file. 123 | Only the latest 5 prompts are kept. 124 | """ 125 | with open(self.path, 'w') as f: 126 | # limit to latest 4 prompts only 127 | json.dump(self._history[-4:], f) 128 | -------------------------------------------------------------------------------- /senpai/lib/user_input.py: -------------------------------------------------------------------------------- 1 | # This code is part of the BashSenpai project, which is licensed under the 2 | # Apache License, Version 2.0. Portions of this code are derived from the 3 | # python-readchar project, which is licensed under the MIT License. 4 | # Please see the LICENSE file in the root of the BashSenpai project for the 5 | # full license terms. 6 | 7 | # Portions of the code are borrowed from python-readchar: 8 | # https://github.com/magmax/python-readchar 9 | # License: MIT (see below) 10 | 11 | # MIT Licence 12 | # 13 | # Copyright (c) 2022 Miguel Angel Garcia 14 | # 15 | # Permission is hereby granted, free of charge, to any person obtaining a copy 16 | # of this software and associated documentation files (the "Software"), to deal 17 | # in the Software without restriction, including without limitation the rights 18 | # to use, copy, modify, merge, publish, distribute, sublicence, and/or sell 19 | # copies of the Software, and to permit persons to whom the Software is 20 | # furnished to do so, subject to the following conditions: 21 | # 22 | # The above copyright notice and this permission notice shall be included in all 23 | # copies or substantial portions of the Software. 24 | # 25 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | # SOFTWARE. 32 | 33 | import sys 34 | 35 | if sys.platform in ('win32', 'cygwin'): 36 | import msvcrt 37 | import win32console 38 | else: 39 | try: 40 | import gnureadline as readline # macos 41 | except: 42 | import readline # linux 43 | import termios 44 | 45 | 46 | class BASE_KEYS: 47 | """Base key codes.""" 48 | CTRL_C = '\x03' 49 | CTRL_D = '\x04' 50 | SPACE = '\x20' 51 | 52 | 53 | if sys.platform in ('win32', 'cygwin'): 54 | class OS_KEYS: 55 | """Windows OS-specific key codes.""" 56 | UP = '\x00\x48' 57 | DOWN = '\x00\x50' 58 | ENTER = '\x0D' 59 | 60 | else: # linux, macos 61 | class OS_KEYS: 62 | """Linux and MacOS OS-specific key codes.""" 63 | UP = '\x1B\x5B\x41' 64 | DOWN = '\x1B\x5B\x42' 65 | ENTER = '\x0A' 66 | 67 | 68 | def readchar() -> str: 69 | """ 70 | Reads a single character from the standard input. 71 | 72 | Returns: 73 | str: The character read from the standard input. 74 | """ 75 | # handle for windows 76 | if sys.platform in ('win32', 'cygwin'): 77 | # manual byte decoding as some bytes in windows are not utf-8 encodable 78 | return chr(int.from_bytes(msvcrt.getch(), 'big')) 79 | 80 | # handle for linux and macos 81 | fd = sys.stdin.fileno() 82 | old_settings = termios.tcgetattr(fd) 83 | term = termios.tcgetattr(fd) 84 | try: 85 | term[3] &= ~(termios.ICANON | termios.ECHO | termios.IGNBRK | termios.BRKINT) 86 | termios.tcsetattr(fd, termios.TCSAFLUSH, term) 87 | ch = sys.stdin.read(1) 88 | finally: 89 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 90 | return ch 91 | 92 | 93 | def readinput(prompt: str, default: str) -> str: 94 | """ 95 | Reads user input with an extra default value provided and returns the 96 | result. 97 | 98 | Args: 99 | prompt (str): The default prompt to show when reading the input. 100 | default (str): The default value to set for editing. 101 | 102 | Returns: 103 | str: The value read from the user input. 104 | """ 105 | # handle windows 106 | if sys.platform in ('win32', 'cygwin'): 107 | _stdin = win32console.GetStdHandle(win32console.STD_INPUT_HANDLE) 108 | 109 | keys = [] 110 | for c in str(default): 111 | evt = win32console.PyINPUT_RECORDType(win32console.KEY_EVENT) 112 | evt.Char = c 113 | evt.RepeatCount = 1 114 | evt.KeyDown = True 115 | keys.append(evt) 116 | 117 | _stdin.WriteConsoleInput(keys) 118 | return input(prompt) 119 | 120 | # handle linux and macos 121 | readline.set_startup_hook( 122 | lambda: readline.insert_text(default) 123 | ) 124 | try: 125 | result = input(prompt) 126 | finally: 127 | readline.set_startup_hook() 128 | return result 129 | 130 | 131 | def readkey() -> str: 132 | """ 133 | Reads the next keypress. If an escaped key is pressed, the full sequence is 134 | read and returned. 135 | 136 | Returns: 137 | str: The key sequence read from the standard input. 138 | """ 139 | c1 = readchar() 140 | 141 | if c1 == BASE_KEYS.CTRL_C: 142 | raise KeyboardInterrupt 143 | 144 | # handle for windows 145 | if sys.platform in ('win32', 'cygwin'): 146 | # if it is a normal character: 147 | if c1 not in '\x00\xE0': 148 | return c1 149 | 150 | # if it is a scpeal key, read second half: 151 | ch2 = readchar() 152 | return '\x00' + ch2 153 | 154 | # handle for linux and macos 155 | if c1 != '\x1B': 156 | return c1 157 | 158 | c2 = readchar() 159 | if c2 not in '\x4F\x5B': 160 | return c1 + c2 161 | 162 | c3 = readchar() 163 | if c3 not in '\x31\x32\x33\x35\x36': 164 | return c1 + c2 + c3 165 | 166 | c4 = readchar() 167 | if c4 not in '\x30\x31\x33\x34\x35\x37\x38\x39': 168 | return c1 + c2 + c3 + c4 169 | 170 | c5 = readchar() 171 | return c1 + c2 + c3 + c4 + c5 172 | -------------------------------------------------------------------------------- /senpai/api.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Bogdan Tatarov 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import asyncio 16 | from requests import post as POST, Response 17 | from typing import Optional, Union 18 | 19 | from .data.config import Config 20 | from .data.history import History 21 | 22 | 23 | class API: 24 | """ 25 | API is a class responsible for interacting with the BashSenpai API server. 26 | 27 | It encapsulates methods for user authentication and sending/receiving 28 | queries to/from the API server. 29 | 30 | Attributes: 31 | HOST: An URL to the BashSenpai API server. 32 | _config: A Config object storing the user settings. 33 | _history: A History object to maintain the log of user interactions. 34 | 35 | Usage: 36 | >>> api = API(config=config, history=history) 37 | >>> api.login('') 38 | >>> response = api.question('how do I create a new directory') 39 | >>> response = api.explain('ffmpeg') 40 | """ 41 | 42 | # HOST = 'http://localhost:8000/v1' 43 | HOST = 'https://api.bashsenpai.com/v1' 44 | 45 | def __init__(self, config: Config, history: History) -> None: 46 | """ 47 | Initializes the API object with user config and history. 48 | 49 | Args: 50 | config (Config): An instance of the Config class containing the user 51 | settings. 52 | history (History): An instance of the History class for storing the 53 | log of user interactions. 54 | """ 55 | self._config = config 56 | self._history = history 57 | 58 | async def explain(self, command: str) -> Union[Response, dict[str, str]]: 59 | """ 60 | Explains the given command by querying the BashSenpai API. 61 | 62 | Args: 63 | command (str): The command to be explained. 64 | 65 | Returns: 66 | Resonse | dict: The API response or an error message if the user is 67 | not authenticated or if an unknown server error occurs. 68 | 69 | Raises: 70 | Exception: In case of server communication issues or other errors. 71 | """ 72 | # check if the user is authenticated first 73 | token = self._config.get_value('token') 74 | if not token: 75 | return { 76 | 'error': True, 77 | 'type': 'auth', 78 | 'message': 'You are not authenticated', 79 | } 80 | 81 | # send the question to our API 82 | try: 83 | data = { 84 | 'token': token, 85 | 'version': self._config.get_value('version'), 86 | 'persona': self._config.get_value('persona'), 87 | 'question': command, 88 | } 89 | return await asyncio.to_thread( 90 | POST, f'{self.HOST}/explain/', json=data, stream=True, 91 | ) 92 | except Exception as e: 93 | return { 94 | 'error': True, 95 | 'type': 'server', 96 | 'message': f'Unknown server error occured: {str(e)}', 97 | } 98 | 99 | async def login(self, token: str) -> dict[str, str]: 100 | """ 101 | Authenticates the user with the BashSenpai API server using the provided 102 | token. 103 | 104 | Args: 105 | token (str): The authentication token provided by the user. 106 | 107 | Returns: 108 | dict: JSON response received from the API server indicating the 109 | result of the authentication process. 110 | """ 111 | data = { 112 | 'token': token, 113 | } 114 | response = await asyncio.to_thread(POST, f'{self.HOST}/auth/', json=data) 115 | return response.json() 116 | 117 | async def question( 118 | self, 119 | question: str, 120 | metadata: Optional[dict[str, str]] = None, 121 | ) -> Union[Response, dict[str, str]]: 122 | """ 123 | Sends a question to the BashSenpai API server and returns the response. 124 | 125 | Args: 126 | question (str): The question to send to the API server. 127 | metadata Optional(dict[str, str]): Optional dictionary containing 128 | user environment metadata. 129 | 130 | Returns: 131 | Resonse | dict: Response received from the API server containing the 132 | answer to the question or an error message. 133 | 134 | Raises: 135 | Exception: In case of server communication issues or other errors. 136 | """ 137 | # check if the user is authenticated first 138 | token = self._config.get_value('token') 139 | if not token: 140 | return { 141 | 'error': True, 142 | 'type': 'auth', 143 | 'message': 'You are not authenticated', 144 | } 145 | 146 | # send the question to our API 147 | try: 148 | data = { 149 | 'token': token, 150 | 'version': self._config.get_value('version'), 151 | 'persona': self._config.get_value('persona'), 152 | 'question': question, 153 | 'history': self._history.get_history(), 154 | 'metadata': metadata, 155 | } 156 | return await asyncio.to_thread( 157 | POST, f'{self.HOST}/prompt/', json=data, stream=True, 158 | ) 159 | except Exception as e: 160 | return { 161 | 'error': True, 162 | 'type': 'server', 163 | 'message': f'Unknown server error occured: {str(e)}', 164 | } 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BashSenpai 2 | 3 | BashSenpai is a command-line tool that utilizes the power of ChatGPT, bringing it straight to your terminal. You can ask questions and receive insightful responses related to shell scripting, making it an indispensable asset for both beginners and seasoned users alike. 4 | 5 |
6 |
7 | Example usage of the BashSenpai command-line interface 8 |
9 |

10 | BashSenpai.com 11 |

12 | 13 | 14 | ## 🎯 Features 15 | 16 | - Powered by ChatGPT. 17 | - Refined prompts employing a multi-step self-reflection process for the most optimal results. 18 | - Receive answers executable directly in the terminal, complemented with instructive comments. 19 | - Ask follow-up questions without providing any context; our API will include your log history. 20 | - Menu prompt after each response, enabling you to run any provided commands, or modify if needed. 21 | - Share optional information about the OS you are using to enhance the responses for general queries. 22 | - Nicely formatted answers with customizable colors for improved readability. 23 | - Modify the persona of BashSenpai, adding a touch of fun and personality to your interactions. 24 | 25 | ## 📖 Table of Contents 26 | 27 | - [Installation](#-installation) 28 | - [Usage](#️-usage) 29 | - [Options](#️-options) 30 | - [Contributing](#-contributing) 31 | - [License](#-license) 32 | - [Roadmap](#️-roadmap) 33 | - [Maintainers](#-maintainers) 34 | 35 | ## 💻 Installation 36 | 37 | We ensure up-to-date packages for the following Operating Systems and Linux distributions: 38 | 39 | ### Ubuntu-based 40 | 41 | Install from PPA: 42 | 43 | ```shell 44 | sudo add-apt-repository ppa:bashsenpai/cli 45 | sudo apt update 46 | sudo apt install senpai-cli 47 | ``` 48 | 49 | Supported distributions: **Ubuntu 22.04 LTS** or later, **Linux Mint 21** or later, **Pop!_OS 22.04**, **KDE neon 5.27**, **elementary OS 7**. 50 | 51 | ### RPM-based 52 | 53 | Install from Copr: 54 | 55 | ```shell 56 | sudo dnf copr enable bashsenpai/cli 57 | sudo dnf install senpai-cli 58 | ``` 59 | 60 | Supported distributions: **Fedora 38**, **RHEL 9**, **CentOS Stream 9**. 61 | 62 | ### Arch Linux-based 63 | 64 | Install from the AUR: 65 | 66 | ```shell 67 | yay -S senpai-cli 68 | ``` 69 | 70 | Supported distributions: any Arch-based rolling-release distribution that supports installing packages from the AUR. **Manjaro** should also work, but it's untested. 71 | 72 | ### openSUSE Tumbleweed 73 | 74 | Install from OBS: 75 | 76 | ```shell 77 | sudo zypper addrepo https://download.opensuse.org/repositories/home:bashsenpai/openSUSE_Tumbleweed/home:bashsenpai.repo 78 | sudo zypper refresh 79 | sudo zypper install senpai-cli 80 | ``` 81 | 82 | ### MacOS 83 | 84 | Install with Homebrew: 85 | 86 | ```shell 87 | brew tap BashSenpai/core 88 | brew install senpai-cli 89 | ``` 90 | 91 | ### Windows 92 | 93 | Download: **[Installer](https://bashsenpai.com/download/latest)**. 94 | 95 | ## ⌨️ Usage 96 | 97 | To use BashSenpai, run the following command: 98 | 99 | ```shell 100 | senpai [options] prompt 101 | ``` 102 | 103 | The `prompt` argument represents the question you want to ask or a special command you wish to execute. BashSenpai sends the prompt to the BashSenpai API and displays the response in your terminal. 104 | 105 | ### Examples 106 | 107 | * Login to BashSenpai: 108 | 109 | ```shell 110 | senpai login 111 | ``` 112 | 113 | This prompt asks you to enter an authentication token, storing it in the configuration file. 114 | 115 | * Change the persona of BashSenpai: 116 | 117 | ```shell 118 | senpai become angry pirate 119 | ``` 120 | 121 | This prompt alters BashSenpai's persona to an angry pirate, infusing a fun twist into the responses. You are not confined to a specific list of characters and can input anything you desire. Our ingeniously designed backend API, equipped with multi-level prompts and instructions, ensures all eccentric ideas are nicely integrated with the core functionality of the tool. 122 | 123 | * Ask a question: 124 | 125 | ```shell 126 | senpai how to disable SSH connections 127 | ``` 128 | 129 | This prompt sends the question to the BashSenpai API and displays an informative and well-formatted response. 130 | 131 | * Ask for a command explanation: 132 | 133 | ```shell 134 | senpai explain tar 135 | ``` 136 | 137 | This prompt sends a question about a specific command to the BashSenpai API and displays a list of most common use cases along with informative comments and remarks of what each one does. 138 | 139 | ## ⚙️ Options 140 | 141 | * `--command-color`: sets the color of the commands in the responses. Valid options are: black, white, gray, red, green, yellow, blue, magenta and cyan. Brighter versions of each color are available, for example: "bright blue". You can also bold colors, for example: "bold red" or "bold bright cyan". 142 | 143 | * `--comment-color`: sets the color of the comments in the responses. 144 | 145 | * `--meta`, `--no-meta`: determines whether to send OS metadata to refine the responses. This includes OS type and version (all OSes), shell type (macOS and Linux), and architecture (macOS). Users may choose to disable this feature for privacy reasons, or in cases where it yields undesired results (for example if the tool is operating on a Windows machine, but the user expects answers about Linux). 146 | 147 | * `-n, --new`: excludes any preceding history when asking a question. Use this option to initiate a new context. 148 | 149 | * `--run`, `--no-run`: controls whether to display the menu prompt to execute each returned command. 150 | 151 | * `-v`, `--version`: shows current version number. 152 | 153 | Check our [Roadmap](#roadmap) section for new and upcoming features as we develop the configuration options. We are committed to making BashSenpai user-friendly, flexible, and easily-configurable, and we value your initial feedback. If you have any suggestions or feature requests, don't hesitate to use the appropriate tools provided by GitHub to share your ideas with us. 154 | 155 | ## 👥 Contributing 156 | 157 | We warmly welcome contributions to enrich and advance the BashSenpai tool. If you have any interesting ideas, bug reports, or feature requests, please create an issue on the GitHub repository. Feel free to fork the repository, make changes, and submit pull requests. 158 | 159 | To contribute to BashSenpai, please follow these steps: 160 | 161 | 1. Fork the repository. 162 | 2. Create a new branch for your feature or bug fix. 163 | 3. Make your changes, ensuring they do not disrupt the core functionality of the tool. 164 | 4. Commit your changes and push them to your forked repository. 165 | 5. Submit a pull request, providing a clear explanation of the changes you've made. 166 | 167 | We appreciate your interest in BashSenpai and will do our best to review any pull requests promptly and maintain an open discussion of our feature goals to facilitate a smooth and easy contribution process for everyone involved. 168 | 169 | ## 📜 License 170 | 171 | This project is licensed under the Apache 2.0 License. For more information, see the [LICENSE](LICENSE) file. 172 | 173 | ## 🗺️ Roadmap 174 | 175 | ### Version 0.90 (planned) 176 | - [ ] Print current user configuration 177 | - [ ] Text-only mode 178 | - [ ] Provide instructions of how to update when a new version is available (based on OS) 179 | - [ ] Generate docker compose configuration files 180 | - [x] Generate common examples for a specific command 181 | 182 | ### Version 0.80 (finished) 183 | 184 | - [x] Better error handling: print better output on receiving an error from the API 185 | - [x] Additional context: provide optional information about your own environment to improve the results 186 | - [x] Improve the formatting of the output of the CLI 187 | - [x] Check if a new version is available 188 | 189 | ### Version 0.75 (finished) 190 | 191 | - [x] MacOS build: proper MacOS integration 192 | - [x] Windows build: native build script with an installer 193 | - [x] Command execution: execute any provided list of commands directly in the terminal 194 | - [x] Configurable color schemes: change the default colors so they fit better with your terminal configuration 195 | 196 | ### Other planned features 197 | 198 | - [ ] Multi-language support: delivered by ChatGPT 199 | - [ ] Alpine Linux packages 200 | - [x] openSUSE packages 201 | - [ ] BSD-based distros packages 202 | - [ ] Windows code-signing 203 | 204 | ## 👨‍💻 Maintainers 205 | 206 | This project is maintained by: 207 | 208 | - [Bogdan Tatarov](https://github.com/btatarov) 209 | - [Nikolay Dyankov](https://github.com/nikolaydyankov) 210 | 211 | We welcome contributions from the community. If you have any queries, suggestions, or bug reports, please feel free to connect with any of the maintainers. 212 | -------------------------------------------------------------------------------- /senpai/terminal.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Bogdan Tatarov 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import asyncio 16 | import os 17 | import subprocess 18 | import sys 19 | 20 | from .lib.user_input import readkey, readinput, BASE_KEYS, OS_KEYS 21 | 22 | 23 | class Terminal: 24 | """ 25 | An object that handles all terminal manipulations and the command execution 26 | menu. 27 | 28 | This class provides an interactive menu for executing commands provided as a 29 | list of strings. The menu runs in an endless loop until the user 30 | cancels the execution or there are no commands left to run. 31 | 32 | Attributes: 33 | commands (list[str]): A list of commands provided to the menu to be run. 34 | index (int): The index of the currently selected command in the list. 35 | command_color (str): The ANSI color code used for printing for commands. 36 | comment_color (str): The ANSI color code for for printing comments. 37 | terminal_size (int): The width of the terminal in characters. 38 | extra_lines (int): The extra lines that need to be cleared on command 39 | wrapping. 40 | 41 | Usage: 42 | >>> terminal = Terminal(command_color, comment_color) 43 | >>> terminal.show_menu() 44 | """ 45 | 46 | def __init__(self, command_color: str, comment_color: str) -> None: 47 | """ 48 | Initialize the menu with a list of commands to run. 49 | 50 | Args: 51 | colors (tuple[str, str]): A tuple containing the command and comment 52 | color patterns. 53 | """ 54 | self.command_color = command_color 55 | self.comment_color = comment_color 56 | 57 | self.terminal_size = os.get_terminal_size().columns 58 | 59 | def clear_line(self) -> None: 60 | """Clears any text from the last line in the console.""" 61 | print(f'\x1B[1A\x1B[2K\r', end='') 62 | 63 | def hide_loading(self) -> None: 64 | """Hide the loading message.""" 65 | print('\x1B[?25h', end='') # show the cursor 66 | self.clear_line() 67 | 68 | async def show_loading(self) -> None: 69 | """ 70 | Show a loading message while waiting for the response from the API. 71 | """ 72 | # hide the cursor 73 | print('\x1B[?25l', end='') 74 | 75 | # separate the output from the shell command with a single line 76 | print('') 77 | 78 | # show the loading prompt 79 | terminal_height = os.get_terminal_size().lines 80 | print('\n' * (terminal_height - 1), end='') 81 | for _ in range(terminal_height - 1): 82 | self.clear_line() 83 | 84 | # animate the dots... 85 | i = 0 86 | backwards = True 87 | while True: 88 | print( 89 | self.comment_color % '⌛️ Your request is being processed', 90 | end='', 91 | ) 92 | if backwards: 93 | print('.' * (2 - i)) 94 | else: 95 | print('.' * (i + 1)) 96 | sys.stdout.flush() 97 | 98 | i += 1 99 | if i == 3: 100 | backwards = not backwards 101 | i = 0 102 | 103 | await asyncio.sleep(0.55) 104 | self.clear_line() 105 | 106 | def show_menu(self, commands: list[str]) -> None: 107 | """ 108 | Displays the menu and handles user input. 109 | 110 | In the menu, users can edit or run any command from the remaining 111 | commands in the list. Once a command is run, it's removed from the list. 112 | The execution ends once no commands are left in the list or the user 113 | aborts the execution either with the 'Q' key, or by using the regular 114 | interrupts. 115 | 116 | Args: 117 | commands (list[str]): The commands to select from. 118 | """ 119 | self.commands = commands 120 | self.index = 0 121 | 122 | self._print_header() 123 | self._print_newlines() 124 | 125 | self.extra_lines = 0 126 | while True: 127 | # get the terminal width on each step 128 | self.terminal_size = os.get_terminal_size().columns 129 | 130 | for _ in range(len(self.commands) + 2 + self.extra_lines): 131 | self.clear_line() 132 | 133 | for index, command in enumerate(self.commands): 134 | # truncate longer commands 135 | if len(command) > self.terminal_size - 5: 136 | command = f'{command[:self.terminal_size - 8]}...' 137 | 138 | if index == self.index: 139 | print(self.command_color % f'👉 {command}') 140 | else: 141 | print(self.comment_color % f' {command}') 142 | 143 | self._print_separator() 144 | 145 | prompt_prefix = '🚀 Run: ' 146 | print( 147 | self.command_color % prompt_prefix + \ 148 | self.comment_color % self.commands[self.index], 149 | ) 150 | 151 | # extra lines to clear if the prompt goes on more lines 152 | prompt_len = len(self.commands[self.index]) + len(prompt_prefix) 153 | self.extra_lines = prompt_len // self.terminal_size 154 | 155 | # handle user input 156 | key = readkey() 157 | if key in [OS_KEYS.UP, 'k', 'K']: 158 | if self.index > 0: 159 | self.index -= 1 160 | elif key in [OS_KEYS.DOWN, 'j', 'J']: 161 | if self.index < len(self.commands) - 1: 162 | self.index += 1 163 | elif key in ['e', 'E']: 164 | self._edit_command() 165 | elif key in [BASE_KEYS.SPACE, OS_KEYS.ENTER]: 166 | self._execute_command() 167 | if self.commands: 168 | self._print_header() 169 | self._print_newlines() 170 | elif key in ['q', 'Q', BASE_KEYS.CTRL_D]: 171 | break 172 | 173 | if not self.commands: 174 | break 175 | 176 | def _edit_command(self) -> None: 177 | """Allows the user to edit the currently selected command.""" 178 | for _ in range(1 + self.extra_lines): 179 | self.clear_line() 180 | 181 | prompt_prefix = '📝 Edit: ' 182 | self.commands[self.index] = readinput( 183 | self.command_color % prompt_prefix, 184 | self.commands[self.index], 185 | ) 186 | 187 | # extra lines to clear if the prompt goes on more lines 188 | prompt_len = len(self.commands[self.index]) + len(prompt_prefix) 189 | self.extra_lines = prompt_len // self.terminal_size 190 | 191 | def _execute_command(self) -> None: 192 | """ 193 | Executes the currently selected command and removes it from the command 194 | list. 195 | """ 196 | 197 | # get current command 198 | command = self.commands.pop(self.index) 199 | 200 | # execute the command and print the result 201 | print('') 202 | command_result = subprocess.run( 203 | command, 204 | shell=True, capture_output=True, text=True, 205 | ) 206 | if len(command_result.stdout.strip()): 207 | print(command_result.stdout) 208 | elif len(command_result.stderr.strip()): 209 | print(command_result.stderr) 210 | 211 | if self.index > 0: 212 | self.index -= 1 213 | 214 | def _print_header(self) -> None: 215 | """Prints the header of the menu.""" 216 | self._print_separator() 217 | print( 218 | self.command_color % '💬 ' + \ 219 | self.comment_color % 'Press ' + \ 220 | self.command_color % '[Enter]' + \ 221 | self.comment_color % ' to execute, ' + \ 222 | self.command_color % '[E]' + \ 223 | self.comment_color % ' to edit, or ' + \ 224 | self.command_color % '[Q]' + \ 225 | self.comment_color % ' to exit.' 226 | ) 227 | self._print_separator() 228 | 229 | def _print_newlines(self) -> None: 230 | """Prints the new lines for a new menu prompt.""" 231 | print('\n' * (len(self.commands) + 1)) 232 | 233 | def _print_separator(self) -> None: 234 | """Prints a line separator.""" 235 | print(self.comment_color % '—' * self.terminal_size) 236 | -------------------------------------------------------------------------------- /senpai/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | BashSenpai - A terminal assistant powered by ChatGPT. 3 | 4 | This is the main entry point for the BashSenpai command line application. 5 | It parses any command-line arguments and provides a get_version() function. 6 | 7 | Copyright 2023 Bogdan Tatarov 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | 22 | import asyncio 23 | import argparse 24 | import sys 25 | 26 | from .senpai import BashSenpai 27 | 28 | 29 | class SimpleNargsFormatter(argparse.RawDescriptionHelpFormatter): 30 | """ 31 | Custom argparse Formatter that skips metavar text formatting. 32 | 33 | This formatter is designed to avoid repetition of metavar text in the help 34 | output when nargs is used. The _format_args method is overridden to achieve 35 | this. 36 | """ 37 | 38 | def _format_args(self, action: argparse.Action, default_metavar: str) -> str: 39 | """ 40 | Returns a string representing the argument(s), replacing the default 41 | metavar. 42 | 43 | Args: 44 | action (argparse.Action): The action object containing information 45 | about the argument. 46 | default_metavar (str): The default metavar for the argument. 47 | 48 | Returns: 49 | str: A string representing the formatted argument(s). 50 | """ 51 | get_metavar = self._metavar_formatter(action, default_metavar) 52 | return '%s' % get_metavar(1) 53 | 54 | 55 | def get_version() -> str: 56 | """ 57 | Get the current version of the application from __init__.py. 58 | 59 | The version is retrieved by importing the __version__ attribute from the 60 | __init__.py file. 61 | 62 | Returns: 63 | str: The version of the application as a string. 64 | """ 65 | from . import __version__ 66 | return __version__ 67 | 68 | 69 | async def run(): 70 | """ 71 | Entry point of the BashSenpai command-line interface. 72 | 73 | This function initializes the BashSenpai object, parses the provided 74 | command-line arguments, validates and sets the appropriate configurations 75 | based on the arguments, and handles any provided prompt. 76 | 77 | Raises: 78 | SystemExit: If an error occurs while parsing the command line arguments 79 | or setting configurations. 80 | """ 81 | 82 | # initialize bash senpai 83 | senpai = BashSenpai() 84 | 85 | # parse any command-line arguments 86 | parser = argparse.ArgumentParser( 87 | prog='senpai', 88 | usage='%(prog)s [options] prompt', 89 | description='BashSenpai command-line interface.', 90 | epilog='\n'.join([ 91 | 'colors:', 92 | ' black, white, gray, red, greeen, yellow, blue, magenta and cyan', 93 | ' There are also brighter versions of each color, for example: "bright blue"', 94 | ' You can also make colors bold, for example: "bold red" or "bold bright cyan"', 95 | '', 96 | 'prompts:', 97 | ' login authenticate using your auth token', 98 | ' ask any shell-related question using common language', 99 | ' explain show most common use cases for a specific command', 100 | ' become change the persona of BashSenpai, use "default" to reset', 101 | '', 102 | 'example usage:', 103 | ' %(prog)s login', 104 | ' %(prog)s become angry pirate', 105 | ' %(prog)s explain tar', 106 | ' %(prog)s how to disable ssh connections', 107 | '', 108 | 'For more information, visit: https://bashsenpai.com/docs' 109 | ]), 110 | formatter_class=SimpleNargsFormatter, 111 | ) 112 | 113 | action = parser.add_argument( 114 | '-n', '--new', 115 | action=argparse.BooleanOptionalAction, 116 | help='ignore previous history when sending a question', 117 | ) 118 | action.option_strings.remove('--no-new') 119 | 120 | action = parser.add_argument( 121 | '--command-color', 122 | type=str, 123 | metavar='col', 124 | nargs='+', 125 | help='set color for the commands, check the "available colors" ' + \ 126 | 'section for a list of all available options', 127 | ) 128 | 129 | action = parser.add_argument( 130 | '--comment-color', 131 | type=str, 132 | metavar='col', 133 | nargs='+', 134 | help='set color for the comments', 135 | ) 136 | 137 | action = parser.add_argument( 138 | '--meta', 139 | action=argparse.BooleanOptionalAction, 140 | default=senpai.config.get_value('metadata'), 141 | help='send information about your OS to imporove the responses', 142 | ) 143 | 144 | action = parser.add_argument( 145 | '--run', 146 | action=argparse.BooleanOptionalAction, 147 | default=senpai.config.get_value('execute'), 148 | help='show menu prompt to execute each returned command', 149 | ) 150 | 151 | parser.add_argument( 152 | '-v', '--version', 153 | action='version', 154 | help='show current version', 155 | version='%(prog)s ' + get_version(), 156 | ) 157 | 158 | parser.add_argument( 159 | 'prompt', 160 | action='store', 161 | type=str, 162 | nargs='*', 163 | metavar='', 164 | help='ask a question or execute a special command', 165 | ) 166 | 167 | # check for empty arguments first 168 | if len(sys.argv) < 2: 169 | print('Error! No arguments provided. For list of available options, run:') 170 | print(f'{parser.prog} --help') 171 | raise SystemExit(1) 172 | 173 | # parse the arguments 174 | args = parser.parse_args() 175 | 176 | # store the app name and version in the config 177 | senpai.config.set_value('prog', parser.prog) 178 | senpai.config.set_value('version', get_version()) 179 | senpai.config.write() 180 | 181 | # set colors 182 | color_chunks = ( 183 | 'bold', 'bright', 'black', 'white', 'gray', 'red', 184 | 'green', 'yellow', 'blue', 'magenta', 'cyan', 185 | ) 186 | 187 | if args.command_color: 188 | command_color = ' '.join(args.command_color) 189 | command_color = command_color.lower().replace('grey', 'gray') 190 | for chunk in command_color.split(): 191 | if not chunk in color_chunks: 192 | print(f'Error! Can\'t parse "{chunk}".') 193 | raise SystemExit(1) 194 | senpai.config.set_value('command_color', command_color) 195 | senpai.config.write() 196 | 197 | if args.comment_color: 198 | comment_color = ' '.join(args.comment_color) 199 | comment_color = comment_color.lower().replace('grey', 'gray') 200 | for chunk in comment_color.split(): 201 | if not chunk in color_chunks: 202 | print(f'Error! Can\'t parse "{chunk}".') 203 | raise SystemExit(1) 204 | senpai.config.set_value('comment_color', comment_color) 205 | senpai.config.write() 206 | 207 | # whether to send OS metadata 208 | if args.meta: 209 | senpai.config.set_value('metadata', True) 210 | else: 211 | senpai.config.set_value('metadata', False) 212 | senpai.config.write() 213 | 214 | # whether to execute the provided commands 215 | if args.run: 216 | senpai.config.set_value('execute', True) 217 | else: 218 | senpai.config.set_value('execute', False) 219 | senpai.config.write() 220 | 221 | # clear the previous user history 222 | if args.new: 223 | senpai.history.clear() 224 | 225 | # parse the prompt 226 | if not args.prompt: 227 | raise SystemExit(0) 228 | 229 | prompt = args.prompt[0] 230 | if prompt == 'login': 231 | if len(args.prompt) > 1: 232 | print('Error! The "login" prompt takes no extra arguments.') 233 | raise SystemExit(1) 234 | 235 | # read the auth token from the stdin and send a login request 236 | token = input('Auth token: ') 237 | await senpai.login(token) 238 | 239 | elif prompt == 'become': 240 | if len(args.prompt) == 1: 241 | print('Error! Please provide the persona you wish BashSenpai to use.') 242 | raise SystemExit(1) 243 | 244 | persona = ' '.join(args.prompt[1:]) 245 | senpai.config.set_value('persona', persona) 246 | senpai.config.write() 247 | print('New persona confirmed.') 248 | 249 | elif prompt == 'explain' and len(args.prompt) < 3: 250 | if len(args.prompt) == 1: 251 | print('Error! The "explain" prompt takes one extra argument in the form of a command name.') 252 | raise SystemExit(1) 253 | await senpai.explain(args.prompt[1]) 254 | 255 | else: 256 | question = ' '.join(args.prompt) 257 | await senpai.ask_question(question) 258 | 259 | 260 | def main(): 261 | """Runs the CLI in async mode.""" 262 | asyncio.run(run()) 263 | 264 | 265 | if __name__ == '__main__': 266 | main() 267 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright (c) 2023 Bogdan Tatarov, Nikolay Dyankov 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /packages/debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: senpai-cli 3 | Source: https://github.com/BashSenpai/cli 4 | 5 | Files: * 6 | Copyright: Copyright (C) 2023 Bogdan Tatarov 7 | License: Apache-2.0 8 | 9 | Files: lib/user_input.py 10 | Copyright: Copyright (c) 2022 Miguel Angel Garcia 11 | License: MIT 12 | On the MIT License 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicence, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in all 21 | copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | SOFTWARE. 30 | 31 | License: Apache-2.0 32 | On the Apache License 2.0 33 | Apache License 34 | Version 2.0, January 2004 35 | http://www.apache.org/licenses/ 36 | 37 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 38 | 39 | 1. Definitions. 40 | 41 | "License" shall mean the terms and conditions for use, reproduction, 42 | and distribution as defined by Sections 1 through 9 of this document. 43 | 44 | "Licensor" shall mean the copyright owner or entity authorized by 45 | the copyright owner that is granting the License. 46 | 47 | "Legal Entity" shall mean the union of the acting entity and all 48 | other entities that control, are controlled by, or are under common 49 | control with that entity. For the purposes of this definition, 50 | "control" means (i) the power, direct or indirect, to cause the 51 | direction or management of such entity, whether by contract or 52 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 53 | outstanding shares, or (iii) beneficial ownership of such entity. 54 | 55 | "You" (or "Your") shall mean an individual or Legal Entity 56 | exercising permissions granted by this License. 57 | 58 | "Source" form shall mean the preferred form for making modifications, 59 | including but not limited to software source code, documentation 60 | source, and configuration files. 61 | 62 | "Object" form shall mean any form resulting from mechanical 63 | transformation or translation of a Source form, including but 64 | not limited to compiled object code, generated documentation, 65 | and conversions to other media types. 66 | 67 | "Work" shall mean the work of authorship, whether in Source or 68 | Object form, made available under the License, as indicated by a 69 | copyright notice that is included in or attached to the work 70 | (an example is provided in the Appendix below). 71 | 72 | "Derivative Works" shall mean any work, whether in Source or Object 73 | form, that is based on (or derived from) the Work and for which the 74 | editorial revisions, annotations, elaborations, or other modifications 75 | represent, as a whole, an original work of authorship. For the purposes 76 | of this License, Derivative Works shall not include works that remain 77 | separable from, or merely link (or bind by name) to the interfaces of, 78 | the Work and Derivative Works thereof. 79 | 80 | "Contribution" shall mean any work of authorship, including 81 | the original version of the Work and any modifications or additions 82 | to that Work or Derivative Works thereof, that is intentionally 83 | submitted to Licensor for inclusion in the Work by the copyright owner 84 | or by an individual or Legal Entity authorized to submit on behalf of 85 | the copyright owner. For the purposes of this definition, "submitted" 86 | means any form of electronic, verbal, or written communication sent 87 | to the Licensor or its representatives, including but not limited to 88 | communication on electronic mailing lists, source code control systems, 89 | and issue tracking systems that are managed by, or on behalf of, the 90 | Licensor for the purpose of discussing and improving the Work, but 91 | excluding communication that is conspicuously marked or otherwise 92 | designated in writing by the copyright owner as "Not a Contribution." 93 | 94 | "Contributor" shall mean Licensor and any individual or Legal Entity 95 | on behalf of whom a Contribution has been received by Licensor and 96 | subsequently incorporated within the Work. 97 | 98 | 2. Grant of Copyright License. Subject to the terms and conditions of 99 | this License, each Contributor hereby grants to You a perpetual, 100 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 101 | copyright license to reproduce, prepare Derivative Works of, 102 | publicly display, publicly perform, sublicense, and distribute the 103 | Work and such Derivative Works in Source or Object form. 104 | 105 | 3. Grant of Patent License. Subject to the terms and conditions of 106 | this License, each Contributor hereby grants to You a perpetual, 107 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 108 | (except as stated in this section) patent license to make, have made, 109 | use, offer to sell, sell, import, and otherwise transfer the Work, 110 | where such license applies only to those patent claims licensable 111 | by such Contributor that are necessarily infringed by their 112 | Contribution(s) alone or by combination of their Contribution(s) 113 | with the Work to which such Contribution(s) was submitted. If You 114 | institute patent litigation against any entity (including a 115 | cross-claim or counterclaim in a lawsuit) alleging that the Work 116 | or a Contribution incorporated within the Work constitutes direct 117 | or contributory patent infringement, then any patent licenses 118 | granted to You under this License for that Work shall terminate 119 | as of the date such litigation is filed. 120 | 121 | 4. Redistribution. You may reproduce and distribute copies of the 122 | Work or Derivative Works thereof in any medium, with or without 123 | modifications, and in Source or Object form, provided that You 124 | meet the following conditions: 125 | 126 | (a) You must give any other recipients of the Work or 127 | Derivative Works a copy of this License; and 128 | 129 | (b) You must cause any modified files to carry prominent notices 130 | stating that You changed the files; and 131 | 132 | (c) You must retain, in the Source form of any Derivative Works 133 | that You distribute, all copyright, patent, trademark, and 134 | attribution notices from the Source form of the Work, 135 | excluding those notices that do not pertain to any part of 136 | the Derivative Works; and 137 | 138 | (d) If the Work includes a "NOTICE" text file as part of its 139 | distribution, then any Derivative Works that You distribute must 140 | include a readable copy of the attribution notices contained 141 | within such NOTICE file, excluding those notices that do not 142 | pertain to any part of the Derivative Works, in at least one 143 | of the following places: within a NOTICE text file distributed 144 | as part of the Derivative Works; within the Source form or 145 | documentation, if provided along with the Derivative Works; or, 146 | within a display generated by the Derivative Works, if and 147 | wherever such third-party notices normally appear. The contents 148 | of the NOTICE file are for informational purposes only and 149 | do not modify the License. You may add Your own attribution 150 | notices within Derivative Works that You distribute, alongside 151 | or as an addendum to the NOTICE text from the Work, provided 152 | that such additional attribution notices cannot be construed 153 | as modifying the License. 154 | 155 | You may add Your own copyright statement to Your modifications and 156 | may provide additional or different license terms and conditions 157 | for use, reproduction, or distribution of Your modifications, or 158 | for any such Derivative Works as a whole, provided Your use, 159 | reproduction, and distribution of the Work otherwise complies with 160 | the conditions stated in this License. 161 | 162 | 5. Submission of Contributions. Unless You explicitly state otherwise, 163 | any Contribution intentionally submitted for inclusion in the Work 164 | by You to the Licensor shall be under the terms and conditions of 165 | this License, without any additional terms or conditions. 166 | Notwithstanding the above, nothing herein shall supersede or modify 167 | the terms of any separate license agreement you may have executed 168 | with Licensor regarding such Contributions. 169 | 170 | 6. Trademarks. This License does not grant permission to use the trade 171 | names, trademarks, service marks, or product names of the Licensor, 172 | except as required for reasonable and customary use in describing the 173 | origin of the Work and reproducing the content of the NOTICE file. 174 | 175 | 7. Disclaimer of Warranty. Unless required by applicable law or 176 | agreed to in writing, Licensor provides the Work (and each 177 | Contributor provides its Contributions) on an "AS IS" BASIS, 178 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 179 | implied, including, without limitation, any warranties or conditions 180 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 181 | PARTICULAR PURPOSE. You are solely responsible for determining the 182 | appropriateness of using or redistributing the Work and assume any 183 | risks associated with Your exercise of permissions under this License. 184 | 185 | 8. Limitation of Liability. In no event and under no legal theory, 186 | whether in tort (including negligence), contract, or otherwise, 187 | unless required by applicable law (such as deliberate and grossly 188 | negligent acts) or agreed to in writing, shall any Contributor be 189 | liable to You for damages, including any direct, indirect, special, 190 | incidental, or consequential damages of any character arising as a 191 | result of this License or out of the use or inability to use the 192 | Work (including but not limited to damages for loss of goodwill, 193 | work stoppage, computer failure or malfunction, or any and all 194 | other commercial damages or losses), even if such Contributor 195 | has been advised of the possibility of such damages. 196 | 197 | 9. Accepting Warranty or Additional Liability. While redistributing 198 | the Work or Derivative Works thereof, You may choose to offer, 199 | and charge a fee for, acceptance of support, warranty, indemnity, 200 | or other liability obligations and/or rights consistent with this 201 | License. However, in accepting such obligations, You may act only 202 | on Your own behalf and on Your sole responsibility, not on behalf 203 | of any other Contributor, and only if You agree to indemnify, 204 | defend, and hold each Contributor harmless for any liability 205 | incurred by, or claims asserted against, such Contributor by reason 206 | of your accepting any such warranty or additional liability. 207 | 208 | END OF TERMS AND CONDITIONS 209 | 210 | APPENDIX: How to apply the Apache License to your work. 211 | 212 | To apply the Apache License to your work, attach the following 213 | boilerplate notice, with the fields enclosed by brackets "[]" 214 | replaced with your own identifying information. (Don't include 215 | the brackets!) The text should be enclosed in the appropriate 216 | comment syntax for the file format. We also recommend that a 217 | file or class name and description of purpose be included on the 218 | same "printed page" as the copyright notice for easier 219 | identification within third-party archives. 220 | 221 | Copyright (c) 2023 Bogdan Tatarov, Nikolay Dyankov 222 | 223 | Licensed under the Apache License, Version 2.0 (the "License"); 224 | you may not use this file except in compliance with the License. 225 | You may obtain a copy of the License at 226 | 227 | http://www.apache.org/licenses/LICENSE-2.0 228 | 229 | Unless required by applicable law or agreed to in writing, software 230 | distributed under the License is distributed on an "AS IS" BASIS, 231 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 232 | See the License for the specific language governing permissions and 233 | limitations under the License. 234 | -------------------------------------------------------------------------------- /senpai/senpai.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Bogdan Tatarov 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import asyncio 16 | from datetime import datetime 17 | import json 18 | import os 19 | from pathlib import Path 20 | import platform 21 | from requests import Response 22 | import sys 23 | from typing import Callable, Union 24 | 25 | from .api import API 26 | from .data.config import Config 27 | from .data.history import History 28 | from .lib.color import parse_color 29 | from .terminal import Terminal 30 | 31 | 32 | # default config storage based on OS type 33 | if sys.platform in ('win32', 'cygwin'): 34 | CONFIG_BASE = Path(os.path.normpath(os.getenv('LOCALAPPDATA'))) 35 | elif sys.platform in ('darwin',): 36 | CONFIG_BASE = Path.home() / 'Library' / 'Application Support' 37 | else: # linux, freebsd, etc. 38 | CONFIG_BASE = Path.home() / '.config' 39 | 40 | 41 | class BashSenpai: 42 | """ 43 | BashSenpai is a tool that provides assistance to new Linux users working in 44 | the terminal. It allows users to ask questions about commands or features 45 | directly from the terminal. 46 | 47 | BashSenpai interacts with an API to send questions and receive formatted 48 | responses. The API provides concise explanations and commands for Linux 49 | shell environments using ChatGPT as a backend. It maintains a configuration 50 | file, user history, shows the menu for executing returned commands, and 51 | formats response output with configurable colors converted to ANSI-escaped 52 | sequences. 53 | 54 | Attributes: 55 | CONFIG_DIR (Path): The directory where configuration files are stored. 56 | DASHBOARD_URL (str): The URL for the dashboard of the application. 57 | config (Config): The configuration object managing the settings. 58 | history (History): The history object managing the user's interactions. 59 | terminal (Terminal): The object manipulating the content on the screen. 60 | api (API): The API object managing the communication with the backend. 61 | command_color (str): The ANSI color code for commands. 62 | comment_color (str): The ANSI color code for comments. 63 | endline_color (str): THe ANSI color code to end the line. 64 | 65 | Usage: 66 | >>> senpai = BashSenpai() 67 | >>> senpai.ask_question('how do I list files in a directory') 68 | >>> senpai.explain('tar') 69 | """ 70 | 71 | CONFIG_DIR = CONFIG_BASE / 'senpai' 72 | DASHBOARD_URL = 'https://bashsenpai.com/dashboard' 73 | 74 | def __init__(self) -> None: 75 | """ 76 | Initialize the BashSenpai object. 77 | 78 | Creates the configuration directory if it doesn't exist and initializes 79 | the `config`, `history`, and `api` objects. 80 | """ 81 | # create config dir if it doesn't exist 82 | path = Path(self.CONFIG_DIR) 83 | path.mkdir(parents=True, exist_ok=True) 84 | 85 | self.config = Config(path=self.CONFIG_DIR) 86 | self.history = History(path=self.CONFIG_DIR) 87 | 88 | self.api = API(config=self.config, history=self.history) 89 | 90 | # parse colors 91 | command_color = parse_color(self.config.get_value('command_color')) 92 | comment_color = parse_color(self.config.get_value('comment_color')) 93 | 94 | self.terminal = Terminal(command_color, comment_color) 95 | 96 | self.command_color, self.endline_color = command_color.split('%s') 97 | self.comment_color, _ = comment_color.split('%s') 98 | 99 | 100 | async def ask_question(self, question: str) -> None: 101 | """ 102 | Send a question to the BashSenpai API and print a formatted response. 103 | 104 | If the user has command execution enabled, shows the menu for executing 105 | each command. 106 | 107 | Args: 108 | question (str): The question to send to the API. 109 | 110 | Raises: 111 | SystemExit: If an error occurs while parsing the response received 112 | from the API. 113 | """ 114 | # send an API call with a question and get the response 115 | metadata = self._get_metadata() 116 | task = self.api.question 117 | response = await self._run_async_prompt(task, question, metadata) 118 | 119 | # if the response is a dict, it already contains an error 120 | if not isinstance(response, dict): 121 | response = self._parse_response(response) 122 | 123 | # check the response for errors 124 | self._handle_response_errors(response) 125 | 126 | # update the history 127 | self.history.add({ 128 | 'question': question, 129 | 'answer': response.get('response'), 130 | 'persona': response.get('persona'), 131 | 'timestamp': datetime.now().timestamp(), 132 | }) 133 | self.history.write() 134 | 135 | # if command execution is enabled, generate the menu and run it 136 | commands = response.get('commands') 137 | if self.config.get_value('execute') and commands: 138 | self.terminal.show_menu(commands=commands) 139 | 140 | # check if the tool is on the latest version, otherwise show a message 141 | self._check_version(response.get('version')) 142 | 143 | async def explain(self, command: str) -> None: 144 | """ 145 | Send a request to the BashSenpai API to explain the usage of a shell 146 | command or a tool and print a formatted response. 147 | 148 | Args: 149 | command (str): The command to send to the API. 150 | 151 | Raises: 152 | SystemExit: If an error occurs while parsing the response received 153 | from the API. 154 | """ 155 | # send an async API call with the command 156 | response = await self._run_async_prompt(self.api.explain, command) 157 | 158 | # if the response is a dict, it already contains an error 159 | if not isinstance(response, dict): 160 | response = self._parse_response(response) 161 | 162 | # check the response for errors 163 | self._handle_response_errors(response) 164 | 165 | # TODO: different interactive menu 166 | 167 | # check if the tool is on the latest version, otherwise show a message 168 | self._check_version(response.get('version')) 169 | 170 | async def login(self, token: str) -> None: 171 | """ 172 | Validate the auth token and store it in the config file. 173 | 174 | Args: 175 | token (str): The auth token provided by the user. 176 | 177 | Raises: 178 | SystemExit: If there is an error with the authentication process. 179 | """ 180 | response = await self.api.login(token) 181 | 182 | if not response['success']: 183 | if response['error']['code'] == 1: 184 | print('Error! No token provided.') 185 | elif response['error']['code'] == 2: 186 | print('Error! Invalid auth token provided.') 187 | print(f'Visit {self.DASHBOARD_URL} to retreive a valid token.') 188 | elif response['error']['code'] == 3: 189 | print('Error! Your user doesn\'t have a valid subscription.') 190 | print(f'Visit {self.DASHBOARD_URL} to subscribe.') 191 | raise SystemExit(2) 192 | 193 | # store the auth token in the config file 194 | self.config.set_value('token', token) 195 | self.config.write() 196 | 197 | print('Authentication successful.') 198 | 199 | async def _run_async_prompt( 200 | self, 201 | prompt_fn: Callable, 202 | *args, 203 | ) -> Union[Response, dict[str, str]]: 204 | """ 205 | Animates the loading dots while waiting for the response. 206 | 207 | Args: 208 | prompt_fn (Callable): The API method to call 209 | *args: optional arguments to pass to the call 210 | 211 | Returns: 212 | Response | dict[str, str]: An API call response or an error. 213 | """ 214 | async def run_prompt(): 215 | global response 216 | response = await prompt_fn(*args) 217 | raise asyncio.CancelledError 218 | 219 | tasks = (run_prompt(), self.terminal.show_loading(),) 220 | try: 221 | await asyncio.gather(*tasks) 222 | except asyncio.CancelledError: 223 | self.terminal.hide_loading() 224 | return response 225 | 226 | def _check_version(self, latest_version: str) -> None: 227 | """ 228 | Check and notify if there's a new version of the CLI available. 229 | 230 | Args: 231 | latest_version (str): The latest version as a string. 232 | """ 233 | if latest_version and latest_version > self.config.get_value('version'): 234 | print('') 235 | print('There is a new version available, please consider updating.') 236 | 237 | def _get_metadata(self) -> Union[dict[str, str], None]: 238 | """ 239 | Gets user system information to include with the prompt. 240 | 241 | The user may disable this functionality for privacy or other reasons. 242 | 243 | Returns: 244 | dict or None: Dictionary containing the user metadata. 245 | """ 246 | if not self.config.get_value('metadata'): 247 | return None 248 | 249 | metadata = dict() 250 | 251 | if sys.platform in ('win32', 'cygwin'): # windows 252 | metadata['os'] = 'Windows' 253 | metadata['version'] = platform.win32_ver()[0] 254 | 255 | elif sys.platform in ('darwin',): # macos 256 | mac_ver = platform.mac_ver() 257 | metadata['os'] = 'macOS' 258 | metadata['version'] = mac_ver[0] 259 | metadata['arch'] = mac_ver[-1] 260 | metadata['shell'] = os.environ.get('SHELL', None) 261 | 262 | else: # linux, freebsd, etc. 263 | metadata['os'] = 'Linux' 264 | metadata['version'] = None 265 | 266 | # raw-parse os-release as python 3.9 lacks freedesktop_os_release 267 | os_release_path = Path('/etc/os-release') 268 | if not os_release_path.exists(): 269 | os_release_path = Path('/usr/lib/os-release') 270 | if not os_release_path.exists(): 271 | os_release_path = None 272 | 273 | if os_release_path: 274 | with open(os_release_path) as f: 275 | os_release = f.read() 276 | 277 | for line in os_release.splitlines(): 278 | line = line.strip() 279 | if line.upper().startswith('PRETTY_NAME'): 280 | metadata['version'] = line.split('=')[1].strip('"\'') 281 | 282 | metadata['shell'] = os.environ.get('SHELL', None) 283 | 284 | return metadata 285 | 286 | def _handle_response_errors(self, response_data: dict[str, str]) -> None: 287 | """ 288 | Validate the API response and handle errors. 289 | 290 | Args: 291 | response_data (dict): The response data from the API. 292 | 293 | Raises: 294 | SystemExit: If an error is found in the response data. 295 | """ 296 | if response_data.get('error', False): 297 | print('Error! %s.' % response_data.get('message')) 298 | 299 | prog = self.config.get_value('prog') 300 | error_type = response_data.get('type', None) 301 | if error_type == 'auth': 302 | print(f'Run: {prog} login') 303 | raise SystemExit(2) 304 | elif error_type in ['timeout', 'server']: 305 | print('Try running the same command again a little later.') 306 | raise SystemExit(3) 307 | elif error_type == 'history': 308 | print(f'Try running: {prog} -n ') 309 | raise SystemExit(3) 310 | raise SystemExit(3) # Unknown error 311 | 312 | def _parse_response(self, response: Response) -> dict[str, str]: 313 | """ 314 | Parses the response received from the API. 315 | 316 | If there are no errors, prints the streamed response and returns a 317 | dictionary with all parsed data. Otherwise returns the error. 318 | 319 | Args: 320 | response (Response): The response object received from the API. 321 | 322 | Returns: 323 | dict: JSON data wtih all the parsed data from the response. 324 | """ 325 | latest_version = None 326 | original_response = None 327 | printed_response = '' 328 | 329 | new_line = None 330 | new_line_text = '' 331 | new_line_type = None 332 | commands = list() 333 | for chunk in response.iter_lines(chunk_size=None): 334 | chunk = json.loads(chunk) 335 | 336 | # check for errors 337 | if 'error' in chunk: 338 | return chunk 339 | 340 | # parse the version 341 | if 'latest_version' in chunk: 342 | latest_version = chunk['latest_version'] 343 | if 'original_response' in chunk: 344 | original_response = chunk['original_response'] 345 | continue 346 | 347 | if 'end' in chunk and chunk['end']: 348 | # append last command and stop 349 | if new_line_type == 'command': 350 | commands.append(new_line_text) 351 | break 352 | 353 | if 'content' in chunk: 354 | printed_response += chunk['content'] 355 | chunk = chunk['content'] 356 | if chunk == '\n': 357 | new_line = True 358 | continue 359 | 360 | if new_line: 361 | if new_line_text: 362 | print(self.endline_color) 363 | if new_line_type == 'command': 364 | commands.append(new_line_text) 365 | new_line_text = '' 366 | 367 | new_line_text += chunk 368 | # determine line type and separate commands 369 | if new_line or chunk.startswith('>'): 370 | if chunk.startswith('$'): 371 | new_line_type = 'command' 372 | chunk = chunk.lstrip('$ ') 373 | print(self.command_color, end='') 374 | elif chunk.startswith('>'): 375 | new_line_type = 'command' 376 | print(self.comment_color, end='') 377 | else: 378 | if new_line_type == 'command': 379 | print('') 380 | print(self.comment_color, end='') 381 | new_line_type = 'comment' 382 | 383 | # strip command indicator from new line and chunk 384 | new_line_text = new_line_text.lstrip('$') 385 | if new_line_text.startswith(' '): 386 | new_line_text = new_line_text.lstrip() 387 | chunk = chunk.lstrip() 388 | 389 | if new_line_text and chunk: 390 | print(chunk, end='') 391 | sys.stdout.flush() 392 | 393 | new_line = False 394 | 395 | print('\n') 396 | 397 | if original_response: 398 | return { 399 | 'version': latest_version, 400 | 'response': original_response, 401 | 'persona': printed_response, 402 | 'commands': commands, 403 | } 404 | 405 | else: 406 | return { 407 | 'version': latest_version, 408 | 'response': printed_response, 409 | 'persona': None, 410 | 'commands': commands, 411 | } 412 | --------------------------------------------------------------------------------