diff --git a/README.md b/README.md index 77c6eba..84bab4a 100644 --- a/README.md +++ b/README.md @@ -7,27 +7,27 @@

Repository's starts Issues License
Latest commit GitHub repository size

@@ -47,21 +47,34 @@ To use this plugin you need to get access to OpenAI's [Codex API](https://openai ## How do I install it? +### Manual Installation +1. Install the OpenAI package. +``` +pip3 install openai +``` -1. Download the ZSH plugin. +2. Download the ZSH plugin. ``` - $ git clone https://github.com/tom-doerr/zsh_codex.git ~/.oh-my-zsh/plugins/ +git clone https://github.com/tom-doerr/zsh_codex.git ~/.oh-my-zsh/custom/plugins/zsh_codex ``` -2. Add the following to your `.zshrc` file. +3. Add the following to your `.zshrc` file. +Using oh-my-zsh: ``` plugins=(zsh_codex) bindkey '^X' create_completion ``` +Without oh-my-zsh: +``` + # in your/custom/path you need to have a "plugins" folder and in there you clone the repository as zsh_codex + export ZSH_CUSTOM="your/custom/path" + source "$ZSH_CUSTOM/plugins/zsh_codex/zsh_codex.plugin.zsh" + bindkey '^X' create_completion +``` -3. Create a file called `openaiapirc` in `~/.config` with your ORGANIZATION_ID and SECRET_KEY. +4. Create a file called `openaiapirc` in `~/.config` with your ORGANIZATION_ID and SECRET_KEY. ``` [openai] @@ -69,4 +82,49 @@ organization_id = ... secret_key = ... ``` -4. Run `zsh`, start typing and complete it using `^X`! +5. Run `zsh`, start typing and complete it using `^X`! + +### Fig Installation + + + +## Troubleshooting + +### Unhandled ZLE widget 'create_completion' + +``` +zsh-syntax-highlighting: unhandled ZLE widget 'create_completion' +zsh-syntax-highlighting: (This is sometimes caused by doing `bindkey create_completion` without creating the 'create_completion' widget with `zle -N` or `zle -C`.) +``` + +Add the line +``` +zle -N create_completion +``` +before you call `bindkey` but after loading the plugin (`plugins=(zsh_codex)`). + +### Already exists and is not an empty directory +``` +fatal: destination path '~.oh-my-zsh/custom/plugins' +``` +Try to download the ZSH plugin again. +``` +git clone https://github.com/tom-doerr/zsh_codex.git ~/.oh-my-zsh/custom/plugins/zsh_codex +``` +--- +

+ Buy Me A Coffee +

+ +## More usage examples +

+ +

+

+

+ +------------------------------------------------------------------- + +[Fish Version](https://github.com/tom-doerr/codex.fish) + +[Traffic Statistics](https://tom-doerr.github.io/github_repo_stats_data/tom-doerr/zsh_codex/latest-report/report.html) diff --git a/create_completion.py b/create_completion.py index 246d29a..dd020d2 100755 --- a/create_completion.py +++ b/create_completion.py @@ -1,16 +1,50 @@ #!/usr/bin/env python3 +import asyncio import openai import sys import os +import configparser +import platform +import subprocess +import json +import time -STREAM = False # Get config dir from environment or default to ~/.config CONFIG_DIR = os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) API_KEYS_LOCATION = os.path.join(CONFIG_DIR, 'openaiapirc') + +CACHE_DIR = os.path.join(CONFIG_DIR, 'cache/zsh_codex') +os.makedirs(CACHE_DIR, exist_ok=True) +SYSTEM_INFO_CACHE_FILE = os.path.join(CACHE_DIR, 'system_info.json') +INSTALLED_PACKAGES_CACHE_FILE = os.path.join(CACHE_DIR, 'installed_packages.json') +CACHE_EXPIRATION_TIME = 60 * 60 * 24 # 24 hours + + + + + + + +def load_or_save_to_cache(filename, default_func): + if os.path.exists(filename): + with open(filename, 'r') as f: + data = json.load(f) + if time.time() - data['timestamp'] < CACHE_EXPIRATION_TIME: + return data['value'] + + data = { + 'value': default_func(), + 'timestamp': time.time() + } + with open(filename, 'w') as f: + json.dump(data, f) + + return data['value'] + # Read the organization_id and secret_key from the ini file ~/.config/openaiapirc # The format is: # [openai] @@ -20,8 +54,7 @@ # If you don't see your organization ID in the file you can get it from the # OpenAI web site: https://openai.com/organizations - -def create_template_ini_file(): +async def create_template_ini_file(): """ If the ini file does not exist create it and add the organization_id and secret_key @@ -31,61 +64,104 @@ def create_template_ini_file(): f.write('[openai]\n') f.write('organization_id=\n') f.write('secret_key=\n') + f.write('model=gpt-4-0314\n') print('OpenAI API config file created at {}'.format(API_KEYS_LOCATION)) print('Please edit it and add your organization ID and secret key') + print('If you do not yet have an organization ID and secret key, you\n' + 'need to register for OpenAI Codex: \n' + 'https://openai.com/blog/openai-codex/') sys.exit(1) -try: - with open(API_KEYS_LOCATION) as f: - config = f.read() +async def initialize_openai_api(): + """ + Initialize the OpenAI API + """ + # Check if file at API_KEYS_LOCATION exists + await create_template_ini_file() + config = configparser.ConfigParser() + config.read(API_KEYS_LOCATION) - config = '\n' + config - # Reading the values works even when there are spaces around the = sign. - organization_id = config.split('organization_id')[1].split('=')[1].split('\n')[0].strip() - secret_key = config.split('secret_key')[1].split('=')[1].split('\n')[0].strip() -except: - print("Unable to read openaiapirc at {}".format(API_KEYS_LOCATION)) - create_template_ini_file() + openai.organization_id = config['openai']['organization_id'].strip('"').strip("'") + openai.api_key = config['openai']['secret_key'].strip('"').strip("'") + if 'model' in config['openai']: + model = config['openai']['model'].strip('"').strip("'") + else: + model = 'gpt-4' -# Remove the quotes if there are any. -if organization_id[0] == '"' and organization_id[-1] == '"': - organization_id = organization_id[1:-1] + return model -if secret_key[0] == '"' and secret_key[-1] == '"': - secret_key = secret_key[1:-1] -openai.api_key = secret_key -openai.organization = organization_id +# model = initialize_openai_api() +model = asyncio.run(initialize_openai_api()) -# Read the input prompt from stdin. -input_prompt = '#!/bin/zsh\n\n' + sys.stdin.read() - - -response = openai.Completion.create(engine='davinci-codex', prompt=input_prompt, temperature=0.5, max_tokens=32, stream=STREAM) -# completion = response['choices'][0]['text'] -if STREAM: - while True: - next_response = next(response) - print("next_response:", next_response) - # next_response['choices'][0]['finish_reason'] - print(" next_response['choices'][0]['finish_reason']:", next_response['choices'][0]['finish_reason']) - completion = next_response['choices'][0]['text'] - print("completion:", completion) - # print(next(response)) -else: - completion_all = response['choices'][0]['text'] - completion_list = completion_all.split('\n') - if completion_all[:2] == '\n\n': - print(completion_all) - elif completion_list[0]: - print(completion_list[0]) - elif len(completion_list) == 1: - print('') - else: - print('\n' + completion_list[1]) +cursor_position_char = int(sys.argv[1]) + + +async def get_system_info(): + """ + Gather system information + """ + + def default_func(): + return { + "os": platform.system(), + "os_version": platform.version(), + "architecture": platform.architecture()[0], + "processor": platform.processor() + } + return load_or_save_to_cache(SYSTEM_INFO_CACHE_FILE, default_func) +system_info = asyncio.run(get_system_info()) + + +# last ten recent packages +async def get_installed_packages(): + # Get list of installed packages + def default_func(): + try: + if system_info['os'] == 'Linux': + command = "dpkg --get-selections | head -n 5" + return subprocess.check_output(command, shell=True).decode('utf-8') + elif system_info['os'] == 'Darwin': + command = "brew list -1t 2> /dev/null | head -n 5" + return subprocess.check_output(command, shell=True).decode('utf-8') + else: + return "Unsupported OS for package listing" + except subprocess.CalledProcessError as e: + return f"Error getting installed packages: {str(e)}" + + return load_or_save_to_cache(INSTALLED_PACKAGES_CACHE_FILE, default_func) + + + +installed_packages = asyncio.run(get_installed_packages()) + +# Get current working directory +current_directory = os.getcwd() +# Read the input prompt from stdin. +buffer = sys.stdin.read() +prompt_prefix = '#!/bin/zsh\n\n' + buffer[:cursor_position_char] +prompt_suffix = buffer[cursor_position_char:] +full_command = prompt_prefix + prompt_suffix +response = openai.ChatCompletion.create(model=model, messages=[ + { + "role": 'system', + "content": "You are a zsh shell expert, please help me complete the following command, you should only output the completed command, no need to include any other explanation", + }, + { + "role": 'user', + "content": f"I'm using a {system_info['os']} system with version {system_info['os_version']} on a {system_info['architecture']} architecture with a {system_info['processor']} processor. The current directory is {current_directory}. Here are some of my installed packages:\n{installed_packages}", + }, + { + "role": 'user', + "content": full_command, + } +]) +completed_command = response['choices'][0]['message']['content'] + +sys.stdout.write(f"\n{completed_command}") diff --git a/zsh_codex.plugin.zsh b/zsh_codex.plugin.zsh index 2c38c24..a503bc9 100644 --- a/zsh_codex.plugin.zsh +++ b/zsh_codex.plugin.zsh @@ -7,11 +7,16 @@ create_completion() { # Get the text typed until now. text=${BUFFER} - completion=$(echo -n "$text" | $ZSH_CUSTOM/plugins/zsh_codex/create_completion.py) + #echo $cursor_line $cursor_col + completion=$(echo -n "$text" | $ZSH_CUSTOM/plugins/zsh_codex/create_completion.py $CURSOR) + text_before_cursor=${text:0:$CURSOR} + text_after_cursor=${text:$CURSOR} # Add completion to the current buffer. - BUFFER="${text}${completion}" - # Put the cursor at the end of the line. - CURSOR=${#BUFFER} + #BUFFER="${text}${completion}" + BUFFER="${text_before_cursor}${completion}${text_after_cursor}" + prefix_and_completion="${text_before_cursor}${completion}" + # Put the cursor at the end of the completion + CURSOR=${#prefix_and_completion} } # Bind the create_completion function to a key.