q-bot 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- q.py +403 -0
- q_bot-1.3.0.dist-info/METADATA +254 -0
- q_bot-1.3.0.dist-info/RECORD +6 -0
- q_bot-1.3.0.dist-info/WHEEL +5 -0
- q_bot-1.3.0.dist-info/entry_points.txt +2 -0
- q_bot-1.3.0.dist-info/top_level.txt +1 -0
q.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# standard library imports
|
|
4
|
+
import base64
|
|
5
|
+
import getpass
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import string
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Any, Dict, List, Tuple
|
|
12
|
+
|
|
13
|
+
# third-party imports
|
|
14
|
+
import openai
|
|
15
|
+
import pyperclip
|
|
16
|
+
from colorama import just_fix_windows_console
|
|
17
|
+
from termcolor import colored, cprint
|
|
18
|
+
|
|
19
|
+
# module metadata
|
|
20
|
+
__version__ = '1.3.0'
|
|
21
|
+
DESCRIPTION = 'An LLM-powered programming copilot from the comfort of your command line'
|
|
22
|
+
|
|
23
|
+
# command parameters
|
|
24
|
+
DEFAULT_CODE = 'python' # default language for code generation
|
|
25
|
+
DEFAULT_SHELL = 'debian+bash' # default system for shell command generation
|
|
26
|
+
|
|
27
|
+
# model variants
|
|
28
|
+
MINI_LLM = 'gpt-4.1-mini' # cheap and fast
|
|
29
|
+
FULL_LLM = 'gpt-4.1' # expensive and more powerful
|
|
30
|
+
|
|
31
|
+
# model parameters
|
|
32
|
+
DEFAULT_MODEL_ARGS = {
|
|
33
|
+
'model': MINI_LLM,
|
|
34
|
+
'max_output_tokens': 1024,
|
|
35
|
+
'temperature': 0.0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# program resources
|
|
39
|
+
RESOURCE_PATH = os.path.join(os.path.expanduser('~'), '.q', 'resources.json')
|
|
40
|
+
os.makedirs(os.path.dirname(RESOURCE_PATH), exist_ok=True)
|
|
41
|
+
|
|
42
|
+
def _load_resource(name: str, default: Any) -> Any:
|
|
43
|
+
try:
|
|
44
|
+
with open(RESOURCE_PATH) as f:
|
|
45
|
+
return json.load(f)[name]
|
|
46
|
+
except:
|
|
47
|
+
return default
|
|
48
|
+
|
|
49
|
+
def _save_resource(name: str, value: Any):
|
|
50
|
+
try:
|
|
51
|
+
with open(RESOURCE_PATH) as f:
|
|
52
|
+
resources = json.load(f)
|
|
53
|
+
except:
|
|
54
|
+
resources = {}
|
|
55
|
+
resources[name] = value
|
|
56
|
+
with open(RESOURCE_PATH, 'w') as f:
|
|
57
|
+
json.dump(resources, f, indent=4)
|
|
58
|
+
|
|
59
|
+
COMMANDS = [
|
|
60
|
+
{
|
|
61
|
+
'flags': [],
|
|
62
|
+
'description': 'follow-up on the previous response',
|
|
63
|
+
'clip_output': _load_resource('clip_output', False),
|
|
64
|
+
'model_args': _load_resource('model_args', {}),
|
|
65
|
+
'messages': _load_resource('messages', []) + [
|
|
66
|
+
{
|
|
67
|
+
'role': 'user',
|
|
68
|
+
'content': '{text}'
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
'flags': ['-e', '--explain'],
|
|
74
|
+
'description': 'explain code, commands, or a technical concept',
|
|
75
|
+
'model_args' : {
|
|
76
|
+
'model': MINI_LLM,
|
|
77
|
+
},
|
|
78
|
+
'messages': [
|
|
79
|
+
{
|
|
80
|
+
'role': 'developer',
|
|
81
|
+
'content': 'You are a programming assistant. Given a shell command, code snippet, or technical concept, provide a concise and technical explanation. Assume the reader is an experienced developer. Avoid restating the code or command. Avoid explaining obvious syntax. Avoid breaking the answer into bullet points unless necessary. The response should be a single short paragraph optimized for clarity.',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
'role': 'user',
|
|
85
|
+
'content': 'Explain: {text}'
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
'flags': ['-c', '--code'],
|
|
91
|
+
'description': f'generate a code snippet (default: {DEFAULT_CODE})',
|
|
92
|
+
'clip_output': True,
|
|
93
|
+
'model_args': {
|
|
94
|
+
'model': FULL_LLM,
|
|
95
|
+
},
|
|
96
|
+
'messages': [
|
|
97
|
+
{
|
|
98
|
+
'role': 'developer',
|
|
99
|
+
'content': f'You are a coding assistant. Given a natural language description, generate a code snippet that accomplishes the requested task. The code should be correct, efficient, concise, and idiomatic. Respond with only the code snippet, without explanations, additional text, or formatting. Assume the programming language is {DEFAULT_CODE} unless otherwise specified.'
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
'role': 'user',
|
|
103
|
+
'content': 'Generate a code snippet to accomplish the following task: {text}. Respond only with the code, without explanation or additional text.'
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
'flags': ['-s', '--shell'],
|
|
109
|
+
'description': f'generate a shell command (default: {DEFAULT_SHELL})',
|
|
110
|
+
'clip_output': True,
|
|
111
|
+
'messages': [
|
|
112
|
+
{
|
|
113
|
+
'role': 'developer',
|
|
114
|
+
'content': f'You are a command-line assistant. Given a natural language task description, generate the simplest single shell command that accomplishes the task. Favor minimal, commonly available commands with no extra formatting or piping. Avoid commands that could delete, overwrite, or modify important files or system settings (e.g., rm -rf, dd, mkfs, chmod -R, chown, kill -9). Respond with only the command, without explanations, additional text, or formatting. Assume a {DEFAULT_SHELL} shell unless otherwise specified.'
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
'role': 'user',
|
|
118
|
+
'content': 'Generate a single shell command to accomplish the following task: {text}. Respond with only the command, without explanation or additional text.'
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
'flags': ['-i', '--image'],
|
|
124
|
+
'description': 'generate an image (very expensive)',
|
|
125
|
+
'model_args': {
|
|
126
|
+
'model': MINI_LLM,
|
|
127
|
+
'tools': [{
|
|
128
|
+
'type': 'image_generation',
|
|
129
|
+
'size': '1024x1024',
|
|
130
|
+
'quality': 'auto' # low, medium, high
|
|
131
|
+
}],
|
|
132
|
+
},
|
|
133
|
+
'messages': [
|
|
134
|
+
{
|
|
135
|
+
'role': 'user',
|
|
136
|
+
'content': 'Generate an image of the following: {text}.'
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
'flags': ['-w', '--web'],
|
|
142
|
+
'description': 'search the internet (expensive)',
|
|
143
|
+
'model_args' : {
|
|
144
|
+
'model': MINI_LLM,
|
|
145
|
+
'tools': [{
|
|
146
|
+
'type': 'web_search_preview',
|
|
147
|
+
'search_context_size': 'low'
|
|
148
|
+
}],
|
|
149
|
+
},
|
|
150
|
+
'messages': [
|
|
151
|
+
{
|
|
152
|
+
'role': 'developer',
|
|
153
|
+
'content': 'You fetch real-time data from the internet. Always respond with only the data requested. Do not provide additional information in the form of context, background, or links. The response should be less than a single sentence.'
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
'role': 'user',
|
|
157
|
+
'content': 'Fetch the following information: {text}.'
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
OPTIONS = [
|
|
164
|
+
{
|
|
165
|
+
'name': 'overwrite',
|
|
166
|
+
'flags': ['-o', '--overwrite'],
|
|
167
|
+
'description': 'overwrite the previous command',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
'name': 'no-clip',
|
|
171
|
+
'flags': ['-n', '--no-clip'],
|
|
172
|
+
'description': 'do not copy the output to the clipboard',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
'name': 'verbose',
|
|
176
|
+
'flags': ['-v', '--verbose'],
|
|
177
|
+
'description': 'print the model parameters and message history',
|
|
178
|
+
},
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
def get_client() -> openai.OpenAI:
|
|
182
|
+
api_key =_load_resource('openai_key', None)
|
|
183
|
+
|
|
184
|
+
if api_key is None:
|
|
185
|
+
cprint(f'Error: OpenAI API key not found. Please paste your API key: ', 'red', end='', flush=True, file=sys.stderr)
|
|
186
|
+
api_key = getpass.getpass(prompt='')
|
|
187
|
+
_save_resource('openai_key', api_key)
|
|
188
|
+
|
|
189
|
+
while True:
|
|
190
|
+
try:
|
|
191
|
+
client = openai.OpenAI(api_key=api_key)
|
|
192
|
+
client.models.list() # test the API key
|
|
193
|
+
return client
|
|
194
|
+
|
|
195
|
+
except openai.APIError:
|
|
196
|
+
cprint(f'Error: OpenAI API key not valid. Please paste your API key: ', 'red', end='', flush=True, file=sys.stderr)
|
|
197
|
+
api_key = getpass.getpass(prompt='')
|
|
198
|
+
_save_resource('openai_key', api_key)
|
|
199
|
+
|
|
200
|
+
def process_text_response(text_response: str) -> str:
|
|
201
|
+
# remove markdown formatting from code responses
|
|
202
|
+
text_response = re.sub(r'^```.*?\n(.*)\n```$', r'\1', text_response, flags=re.DOTALL)
|
|
203
|
+
|
|
204
|
+
# shorten links from web search responses
|
|
205
|
+
text_response = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text_response).strip()
|
|
206
|
+
|
|
207
|
+
# convert bash code blocks into colored text with $ prefix
|
|
208
|
+
text_response = re.sub(r'```bash\n?(.*?)```', lambda m: colored('\n'.join('$ ' + line for line in m.group(1).strip().split('\n')), 'cyan'), text_response, flags=re.DOTALL)
|
|
209
|
+
|
|
210
|
+
# convert non-bash code blocks into colored text
|
|
211
|
+
text_response = re.sub(r'```(?:\w+\n?)?(.*?)```', lambda m: colored(m.group(1).strip(), 'cyan'), text_response, flags=re.DOTALL)
|
|
212
|
+
|
|
213
|
+
# convert inline-code into colored text
|
|
214
|
+
text_response = re.sub(r'`([^`]+)`', lambda m: colored(m.group(1), 'cyan'), text_response)
|
|
215
|
+
|
|
216
|
+
# convert two-plus newlines into only two
|
|
217
|
+
text_response = re.sub(r'\n{2,}', '\n\n', text_response)
|
|
218
|
+
|
|
219
|
+
return text_response
|
|
220
|
+
|
|
221
|
+
def prompt_model(model_args: Dict, messages: List[Dict]) -> Tuple[str, str]:
|
|
222
|
+
response = get_client().responses.create(
|
|
223
|
+
input=messages,
|
|
224
|
+
**model_args
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# extract and process text response
|
|
228
|
+
text_response = process_text_response(response.output_text)
|
|
229
|
+
|
|
230
|
+
# extract image response
|
|
231
|
+
image_response = None
|
|
232
|
+
for output in response.output:
|
|
233
|
+
if output.type == 'image_generation_call':
|
|
234
|
+
image_response = output
|
|
235
|
+
break
|
|
236
|
+
|
|
237
|
+
return text_response, image_response
|
|
238
|
+
|
|
239
|
+
def run_command(cmd: Dict, text: str, **opt_args):
|
|
240
|
+
# load model and messages from command
|
|
241
|
+
model_args = {**DEFAULT_MODEL_ARGS, **cmd.get('model_args', {})}
|
|
242
|
+
messages = [ { role : content.replace('{text}', text) for role, content in msg.items() } for msg in cmd.get('messages', []) ]
|
|
243
|
+
|
|
244
|
+
# save model args for follow-up commands
|
|
245
|
+
_save_resource('model_args', model_args)
|
|
246
|
+
# save command args for follow-up commands
|
|
247
|
+
_save_resource('clip_output', cmd.get('clip_output', False))
|
|
248
|
+
|
|
249
|
+
# overwrite previous follow-up command
|
|
250
|
+
if opt_args.get('overwrite', False):
|
|
251
|
+
# remove messages from second-to-last user message to last user message
|
|
252
|
+
user_msg_indices = [i for i, msg in enumerate(messages) if msg.get('role') == 'user']
|
|
253
|
+
if len(user_msg_indices) > 1:
|
|
254
|
+
messages = messages[:user_msg_indices[-2]] + messages[user_msg_indices[-1]:]
|
|
255
|
+
else:
|
|
256
|
+
cprint(f'Error: No previous command to overwrite.', 'red', file=sys.stderr)
|
|
257
|
+
sys.exit(1)
|
|
258
|
+
|
|
259
|
+
# prompt the model
|
|
260
|
+
text_response, image_response = prompt_model(model_args, messages)
|
|
261
|
+
|
|
262
|
+
# save messages for follow-up commands
|
|
263
|
+
if image_response:
|
|
264
|
+
messages.append({'type': 'image_generation_call', 'id': image_response.id})
|
|
265
|
+
else:
|
|
266
|
+
messages.append({'role': 'assistant', 'content': text_response})
|
|
267
|
+
_save_resource('messages', messages)
|
|
268
|
+
|
|
269
|
+
# print output
|
|
270
|
+
if opt_args.get('verbose', False):
|
|
271
|
+
# model parameters
|
|
272
|
+
cprint('MODEL PARAMETERS:', 'red', file=sys.stderr)
|
|
273
|
+
for arg in model_args:
|
|
274
|
+
print(colored(f'{arg}:', 'green'), model_args[arg], file=sys.stderr)
|
|
275
|
+
# message history
|
|
276
|
+
cprint('\n'+'MESSAGES:', 'red', file=sys.stderr)
|
|
277
|
+
for message in messages:
|
|
278
|
+
if message.get('role'):
|
|
279
|
+
print(colored(f'{message["role"].capitalize()}:', 'green'), message['content'], file=sys.stderr)
|
|
280
|
+
elif message.get('type'):
|
|
281
|
+
print(colored(f'{message["type"]}:', 'green'), message['id'], file=sys.stderr)
|
|
282
|
+
elif not image_response:
|
|
283
|
+
print(text_response)
|
|
284
|
+
|
|
285
|
+
# copy text response to clipboard
|
|
286
|
+
if not image_response and not opt_args.get('no-clip', False) and cmd.get('clip_output', False):
|
|
287
|
+
try:
|
|
288
|
+
pyperclip.copy(text_response)
|
|
289
|
+
cprint(f'Output copied to clipboard.', 'yellow', file=sys.stderr)
|
|
290
|
+
except pyperclip.PyperclipException:
|
|
291
|
+
pass # ignore clipboard errors
|
|
292
|
+
|
|
293
|
+
# save image to file
|
|
294
|
+
if image_response:
|
|
295
|
+
image_file = 'q_' + ''.join(c for c in text if c not in string.punctuation).replace(' ', '_') + '.png'
|
|
296
|
+
with open(image_file, 'wb') as f:
|
|
297
|
+
f.write(base64.b64decode(image_response.result))
|
|
298
|
+
cprint(f'Image saved to {image_file}.', 'yellow', file=sys.stderr)
|
|
299
|
+
|
|
300
|
+
def validate_commands():
|
|
301
|
+
# check if there is a default command
|
|
302
|
+
if len([cmd for cmd in COMMANDS if not cmd['flags']]) == 0:
|
|
303
|
+
cprint(f'Error: No default command found.', 'red', file=sys.stderr)
|
|
304
|
+
sys.exit(1)
|
|
305
|
+
|
|
306
|
+
# check if there is more than one default command
|
|
307
|
+
if len([cmd for cmd in COMMANDS if not cmd['flags']]) > 1:
|
|
308
|
+
cprint(f'Error: More than one default command found. If a custom command was added, it is missing a flag.', 'red', file=sys.stderr)
|
|
309
|
+
sys.exit(1)
|
|
310
|
+
|
|
311
|
+
# check if there are duplicate commands
|
|
312
|
+
cmd_flags = [flag for cmd in COMMANDS for flag in cmd['flags']]
|
|
313
|
+
dup_flags = set(flag for flag in cmd_flags if cmd_flags.count(flag) > 1)
|
|
314
|
+
if dup_flags:
|
|
315
|
+
cprint(f'Error: Duplicate commands found: {", ".join(dup_flags)}.', 'red', file=sys.stderr)
|
|
316
|
+
sys.exit(1)
|
|
317
|
+
|
|
318
|
+
def print_help():
|
|
319
|
+
tab_spaces, flag_len = 4, max(len(', '.join(cmd['flags'])) for cmd in COMMANDS + OPTIONS) + 2
|
|
320
|
+
help_text = f'q {__version__} - {DESCRIPTION}'
|
|
321
|
+
help_text += '\n\nUsage: ' + colored('q [command] TEXT [options]', 'green')
|
|
322
|
+
help_text += '\n\nCommands (one required):\n'
|
|
323
|
+
help_text += '\n'.join([' '*tab_spaces + colored(f'{", ".join(cmd["flags"]) if cmd["flags"] else "TEXT":<{flag_len}}', 'green') + f'{cmd["description"]}' for cmd in COMMANDS])
|
|
324
|
+
help_text += '\n\nOptions:\n'
|
|
325
|
+
help_text += '\n'.join([' '*tab_spaces + colored(f'{", ".join(opt["flags"]):<{flag_len}}', 'green') + f'{opt["description"]}' for opt in OPTIONS])
|
|
326
|
+
|
|
327
|
+
print(help_text)
|
|
328
|
+
|
|
329
|
+
def main():
|
|
330
|
+
# fix ANSI escape codes on Windows
|
|
331
|
+
just_fix_windows_console()
|
|
332
|
+
|
|
333
|
+
# validate custom commands
|
|
334
|
+
validate_commands()
|
|
335
|
+
|
|
336
|
+
# get command line arguments
|
|
337
|
+
args = sys.argv
|
|
338
|
+
|
|
339
|
+
# print help text if no arguments or -h/--help flag is provided
|
|
340
|
+
if len(args) == 1 or args[1] in ['-h', '--help']:
|
|
341
|
+
print_help()
|
|
342
|
+
sys.exit(0)
|
|
343
|
+
|
|
344
|
+
# check if there is more than one command
|
|
345
|
+
cmd_flags = [flag for cmd in COMMANDS for flag in cmd['flags']]
|
|
346
|
+
if len([arg for arg in args[1:] if arg in cmd_flags]) > 1:
|
|
347
|
+
cprint(f'Error: Only one command may be provided.', 'red', file=sys.stderr)
|
|
348
|
+
sys.exit(1)
|
|
349
|
+
|
|
350
|
+
# check if there is a command that is not the first argument
|
|
351
|
+
if len([arg for arg in args[1:] if arg in cmd_flags]) == 1 and args[1] not in cmd_flags:
|
|
352
|
+
cprint(f'Error: Command must be the first argument.', 'red', file=sys.stderr)
|
|
353
|
+
sys.exit(1)
|
|
354
|
+
|
|
355
|
+
# check if the first argument is an invalid command
|
|
356
|
+
if args[1].startswith('-') and args[1] not in cmd_flags:
|
|
357
|
+
cprint(f'Error: Invalid command "{args[1]}".', 'red', file=sys.stderr)
|
|
358
|
+
sys.exit(1)
|
|
359
|
+
|
|
360
|
+
# check if there is no text provided for a command
|
|
361
|
+
if args[1] in cmd_flags and len(args) < 3:
|
|
362
|
+
cprint(f'Error: No text provided.', 'red', file=sys.stderr)
|
|
363
|
+
sys.exit(1)
|
|
364
|
+
|
|
365
|
+
# extract options and remove them from the text
|
|
366
|
+
opt_args = {opt['name']: False for opt in OPTIONS}
|
|
367
|
+
opt_flags = [flag for opt in OPTIONS for flag in opt['flags']]
|
|
368
|
+
while args[-1].startswith('-') and args[-1] != '-':
|
|
369
|
+
# individual flags (e.g. -v -n)
|
|
370
|
+
if args[-1] in opt_flags:
|
|
371
|
+
flag = args.pop()
|
|
372
|
+
for opt in OPTIONS:
|
|
373
|
+
if flag in opt['flags']:
|
|
374
|
+
opt_args[opt['name']] = True
|
|
375
|
+
# combined flags (e.g. -vn)
|
|
376
|
+
else:
|
|
377
|
+
flags = args.pop()[1:]
|
|
378
|
+
for flag in flags:
|
|
379
|
+
for opt in OPTIONS:
|
|
380
|
+
if f'-{flag}' in opt['flags']:
|
|
381
|
+
opt_args[opt['name']] = True
|
|
382
|
+
break
|
|
383
|
+
else:
|
|
384
|
+
cprint(f'Error: Invalid option "-{flags}".', 'red', file=sys.stderr)
|
|
385
|
+
sys.exit(1)
|
|
386
|
+
|
|
387
|
+
# mask stderr if stdout is being piped
|
|
388
|
+
if not sys.stdout.isatty():
|
|
389
|
+
sys.stderr = open(os.devnull, 'w')
|
|
390
|
+
|
|
391
|
+
# run command
|
|
392
|
+
for cmd in COMMANDS:
|
|
393
|
+
if args[1] in cmd['flags']:
|
|
394
|
+
run_command(cmd, ' '.join(args[2:]), **opt_args)
|
|
395
|
+
sys.exit(0)
|
|
396
|
+
# run default command
|
|
397
|
+
else:
|
|
398
|
+
# already validated there is exactly one default command
|
|
399
|
+
cmd = [cmd for cmd in COMMANDS if not cmd['flags']][0]
|
|
400
|
+
run_command(cmd, ' '.join(args[1:]), **opt_args)
|
|
401
|
+
|
|
402
|
+
if __name__ == '__main__':
|
|
403
|
+
main()
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: q-bot
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: An LLM-powered programming copilot from the comfort of your command line
|
|
5
|
+
Author-email: Tushar Khan <tushar.54k@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/tk755/q
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: colorama==0.4.6
|
|
11
|
+
Requires-Dist: openai==1.82.0
|
|
12
|
+
Requires-Dist: pyperclip==1.9.0
|
|
13
|
+
Requires-Dist: termcolor==2.5.0
|
|
14
|
+
|
|
15
|
+
# Overview
|
|
16
|
+
`q` is an LLM-powered programming copilot from the comfort of your command line. It can generate code snippets, shell commands, technical explanations, web searches, and even images. Through multi-turn conversations, it can build, debug, and refine complex solutions iteratively. It even saves generated code and commands to your clipboard, so you can paste it wherever you need it.
|
|
17
|
+
|
|
18
|
+
Currently `q` uses the OpenAI API, but support for other LLM providers is planned in the future.
|
|
19
|
+
|
|
20
|
+
# Installation
|
|
21
|
+
|
|
22
|
+
Install from PyPI using pipx (recommended for CLI tools):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pipx install q-cli
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or using pip:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install q-cli
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires Python 3.8+.
|
|
35
|
+
|
|
36
|
+
# Usage
|
|
37
|
+
|
|
38
|
+
The basic syntax is `q [command] TEXT [options]`. `q` accepts at most one command and any number of options. Any arguments between the command and options is treated as the input text.
|
|
39
|
+
|
|
40
|
+
`q` saves generated code snippets and shell commands to your clipboard so you can paste it wherever you need it. This only works on non-headless environments (no VMs and Docker containers); check [here](https://pyperclip.readthedocs.io/en/latest/index.html#not-implemented-error) if it doesn't work.
|
|
41
|
+
|
|
42
|
+
For a full list of commands and options, run `q -h`.
|
|
43
|
+
|
|
44
|
+
> The first time `q` is prompted, you will be asked for an OpenAI API key which you can create [here](https://platform.openai.com/api-keys).
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
Each command runs a tailored LLM prompt using the OpenAI API and returns the parsed response.
|
|
49
|
+
|
|
50
|
+
### Generate Code
|
|
51
|
+
|
|
52
|
+
Use the `-c` or `--code` command to generate code snippets:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
$ q -c copy user input to clipboard
|
|
56
|
+
import pyperclip
|
|
57
|
+
|
|
58
|
+
user_input = input("Enter text to copy to clipboard: ")
|
|
59
|
+
pyperclip.copy(user_input)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
By default this generates Python code, but you can specify a different programming language in the prompt itself (or [modify the default value](#modifying-default-values)):
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
$ q -c copy user input to clipboard in rust
|
|
66
|
+
use std::io::{self, Write};
|
|
67
|
+
use clipboard::{ClipboardContext, ClipboardProvider};
|
|
68
|
+
|
|
69
|
+
fn main() {
|
|
70
|
+
let mut input = String::new();
|
|
71
|
+
print!("Enter text to copy to clipboard: ");
|
|
72
|
+
io::stdout().flush().unwrap();
|
|
73
|
+
io::stdin().read_line(&mut input).unwrap();
|
|
74
|
+
|
|
75
|
+
let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
|
|
76
|
+
ctx.set_contents(input.trim().to_string()).unwrap();
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
You can redirect the output to a file and run it instantly:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
$ q -c fib function w main function > fib.py && python3 fib.py
|
|
84
|
+
Enter a number: 10
|
|
85
|
+
Fibonacci number at position 10 is 55
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Generate Shell Commands
|
|
89
|
+
|
|
90
|
+
Use the `-s` or `--shell` command to generate shell commands:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
$ q -s add README.md to prev commit and push changes
|
|
94
|
+
git add README.md && git commit --amend --no-edit && git push --force-with-lease
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
By default this generates Bash commands for a Linux system, but you can specify a different shell or OS in the prompt itself (or [modify the default value](#modifying-default-values)):
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
$ q -s auto hide the dock on macos
|
|
101
|
+
defaults write com.apple.dock autohide -bool true; killall Dock
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
You can pipe the output to the shell interpreter to run it instantly:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
$ q -s count line numbers in fib.py | bash
|
|
108
|
+
12 fib.py
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Explain
|
|
112
|
+
|
|
113
|
+
Use the `-e` or `--explain` command to generate a concise explanation for code or shell commands:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
$ q -e 'print(os.getcwd())'
|
|
117
|
+
`print(os.getcwd())` outputs the current working directory of the Python process by calling `os.getcwd()`, which returns the absolute path as a string, and then prints it to the standard output.
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This is particularly useful for understanding complex code or shell commands you may not be familiar with:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
$ q -e 'find . -type d -name .git -exec dirname {} \; | sort'
|
|
124
|
+
This command searches recursively from the current directory for directories named `.git`, then uses `dirname` to output their parent directory paths, effectively listing all Git repository root directories. The results are then sorted alphabetically.
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
It can even generate explanations about technical concepts:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
$ q -e neuroevolution
|
|
131
|
+
Neuroevolution is a technique that applies evolutionary algorithms to optimize artificial neural networks, typically evolving their weights, architectures, or learning rules. Instead of using gradient-based methods like backpropagation, neuroevolution treats network parameters as genomes and iteratively improves them through selection, mutation, and crossover, enabling the discovery of novel network topologies and solutions, especially useful in reinforcement learning and problems with non-differentiable objectives.
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Search the Web
|
|
135
|
+
|
|
136
|
+
Use the `-w` or `--web` command to search the web for up-to-date information (note: this is slightly expensive):
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
$ q -w highest qbit computer
|
|
140
|
+
As of July 2025, the quantum computer with the highest number of qubits is Atom Computing's processor, which has 1,180 qubits. (spinquanta.com)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Generate Images
|
|
144
|
+
|
|
145
|
+
Use the `-i` or `--image` command to generate 1024x1024 images (note: this is very expensive):
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
$ q -i george washington riding a harley through the american civil war
|
|
149
|
+
Image saved to q_george_washington_riding_a_harley_through_the_american_civil_war.png.
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Multi-Turn Conversations
|
|
153
|
+
|
|
154
|
+
Use `q` without specifying a command to build on the previous response conversationally:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
$ q -w nba champions
|
|
158
|
+
The Oklahoma City Thunder won the 2025 NBA Finals, defeating the Indiana Pacers 4-3. (nba.com)
|
|
159
|
+
$ q final game score
|
|
160
|
+
The Oklahoma City Thunder defeated the Indiana Pacers 103-91 in Game 7 of the 2025 NBA Finals on June 22, 2025. (espn.com)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
This is very useful for iteratively refining generated code or shell commands:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
$ q -s get cuda version
|
|
167
|
+
nvcc --version
|
|
168
|
+
$ nvcc --version
|
|
169
|
+
bash: nvcc: command not found
|
|
170
|
+
$ q command not found
|
|
171
|
+
cat /usr/local/cuda/version.txt
|
|
172
|
+
$ cat /usr/local/cuda/version.txt
|
|
173
|
+
cat: /usr/local/cuda/version.txt: No such file or directory
|
|
174
|
+
$ q file not found
|
|
175
|
+
nvidia-smi | grep "CUDA Version"
|
|
176
|
+
$ nvidia-smi | grep "CUDA Version"
|
|
177
|
+
| NVIDIA-SMI 560.35.02 Driver Version: 560.94 CUDA Version: 12.6 |
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
And ask follow-up questions about the previous response:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
$ q -c function to merge two dictionaries x and y
|
|
184
|
+
def merge_dictionaries(x, y):
|
|
185
|
+
return {**x, **y}
|
|
186
|
+
$ q what is the time complexity
|
|
187
|
+
The time complexity of merging two dictionaries using `{**x, **y}` is O(n + m), where n is the number of elements in dictionary `x` and m is the number of elements in dictionary `y`.
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
You can even refine generated images:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
$ q -i low poly rubber duck
|
|
194
|
+
Image saved to q_low_poly_rubber_duck.png.
|
|
195
|
+
$ q make it float in a high res bathtub
|
|
196
|
+
Image saved to q_make_it_float_in_a_high_res_bathtub.png.
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Options
|
|
200
|
+
|
|
201
|
+
Options are boolean flags that modify the behavior of `q`:
|
|
202
|
+
|
|
203
|
+
- Use the `-o` or `--overwrite` option to overwrite the previous command.
|
|
204
|
+
- Use the `-n` or `--no-clip` option to disable automatically storing responses to the clipboard.
|
|
205
|
+
- Use the `-v` or `--verbose` option to print the model parameters and message history.
|
|
206
|
+
|
|
207
|
+
You can combine multiple options using their abbreviated forms following a single hyphen:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
$ q -p knock knock
|
|
211
|
+
Who's there?
|
|
212
|
+
$ q apple
|
|
213
|
+
Apple who?
|
|
214
|
+
$ q orange -vo
|
|
215
|
+
|
|
216
|
+
MODEL PARAMETERS:
|
|
217
|
+
model: gpt-4o
|
|
218
|
+
max_tokens: 256
|
|
219
|
+
temperature: 0.25
|
|
220
|
+
frequency_penalty: 0
|
|
221
|
+
presence_penalty: 0
|
|
222
|
+
top_p: 1
|
|
223
|
+
|
|
224
|
+
MESSAGES:
|
|
225
|
+
System: You are a helpful and knowledgeable AI assistant.
|
|
226
|
+
User: knock knock
|
|
227
|
+
Assistant: Who's there?
|
|
228
|
+
User: orange
|
|
229
|
+
Assistant: Orange who?
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
# Customization
|
|
233
|
+
|
|
234
|
+
`q` was designed to be easily customizable to suit any programmer's needs.
|
|
235
|
+
|
|
236
|
+
## Modifying Default Values
|
|
237
|
+
|
|
238
|
+
The following constants can be modified in the script to change the default behavior of `q`:
|
|
239
|
+
- `MINI_LLM`: the default model for fast and cheap responses.
|
|
240
|
+
- `FULL_LLM`: the default model for long and detailed responses.
|
|
241
|
+
- `DEFAULT_MODEL_ARGS`: the default model arguments used by all commands if not overrided.
|
|
242
|
+
- `DEFAULT_CODE`: the default language for code generation used by the `-c` command.
|
|
243
|
+
- `DEFAULT_SHELL`: the default system for shell command generation used by the `-s` command.
|
|
244
|
+
|
|
245
|
+
## Adding New Commands
|
|
246
|
+
|
|
247
|
+
Add a new command to `q` by inserting a new dictionary in the `COMMANDS` list with the following keys:
|
|
248
|
+
- `flags` *(required)*: a list of flags to invoke the command.
|
|
249
|
+
- `description` *(required)*: a brief description of the command shown in the help message.
|
|
250
|
+
- `messages` *(required)*: the instructions sent to the LLM, using `{text}` as a placeholder for the input text.
|
|
251
|
+
- `model_args` *(optional)*: override default model arguments set in `DEFAULT_MODEL_ARGS` or set new arguments.
|
|
252
|
+
- `clip_output` *(optional)*: set to `True` to copy the output to the clipboard; `False` by default.
|
|
253
|
+
|
|
254
|
+
Refer to the existing commands in the script for examples.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
q.py,sha256=nyISrxngnIN9jhNlxzIhF9EhCiMkT2g2V97b6df1PRU,15776
|
|
2
|
+
q_bot-1.3.0.dist-info/METADATA,sha256=ZHYLH8uNXcS5_Q5-P9AYB23bjBdbymr-EWA8RQvoPKk,9119
|
|
3
|
+
q_bot-1.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
4
|
+
q_bot-1.3.0.dist-info/entry_points.txt,sha256=bbaDFCUb5znyZkqgvJPyP8NUgaW181O8W4qCitPgzn8,29
|
|
5
|
+
q_bot-1.3.0.dist-info/top_level.txt,sha256=StwzvZ_nQwPDRL5G5ZFtZRgvshjiSP6ARSqz8CWwbGQ,2
|
|
6
|
+
q_bot-1.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
q
|