q-bot 1.4.0__tar.gz → 2.0.0.dev1__tar.gz

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.
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: q-bot
3
+ Version: 2.0.0.dev1
4
+ Summary: An LLM agent from the comfort of your command line
5
+ Author-email: Tushar Khan <dev@tusharkhan.com>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/tk755/q
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: anthropic==0.75.0
11
+ Requires-Dist: colorama==0.4.6
12
+ Requires-Dist: distro==1.9.0
13
+ Requires-Dist: humanize==4.14.0
14
+ Requires-Dist: openai==2.9.0
15
+ Requires-Dist: pydantic==2.12.5
16
+ Requires-Dist: pyperclip==1.11.0
17
+ Requires-Dist: python-dotenv==1.2.1
18
+ Requires-Dist: termcolor==3.2.0
19
+
20
+ # Overview
21
+
22
+ `q` is a lightweight, flexible, multi-provider LLM-agent framework for the terminal.
23
+
24
+ I built this before Claude Code ever existed, and it remains useful to me for quick CLI interactions or prototyping multi-agent experiments. However, for most complex coding tasks, Claude Code is unquestionably superior.
25
+
26
+ # Installation
27
+
28
+ Install using any pip-compatible package manager (e.g. `pip`, `pipx`, `uv`, etc.):
29
+
30
+ ```bash
31
+ pipx install q-bot
32
+ ```
33
+
34
+ Requires Python 3.12+.
35
+
36
+ # CLI Usage
37
+
38
+ `q` uses a simple paradigm where each character from a-z is mapped to a single flag representing a command or option. This enables concise combinations of flags to achieve complex behavior.
39
+
40
+ ## Flag Reference
41
+
42
+ | Flag | Name | Arg | Description | Type |
43
+ | ---- | ------------ | ------- | ------------------------------ | ------: |
44
+ | `-a` | agent | | *[reserved for future use]* | Command |
45
+ | `-b` | batch | | *[reserved for future use]* | |
46
+ | `-c` | code | str | generate code | Command |
47
+ | `-d` | directory | - / str | add a directory to context | Option |
48
+ | `-e` | explain | - / str | explain code or text | Command |
49
+ | `-f` | file | str | read input from file | Option |
50
+ | `-g` | | | | |
51
+ | `-h` | help | - / str | help message / help agent | Command |
52
+ | `-i` | image | str | generate/edit an image | Command |
53
+ | `-j` | json | - | output as JSON | Option |
54
+ | `-k` | api key | str | *[reserved for future use]* | Option |
55
+ | `-l` | load | - / int | list all / load session by id | Command |
56
+ | `-m` | model | str | set model and/or provider | Option |
57
+ | `-n` | | | | |
58
+ | `-o` | output | str | output file | Option |
59
+ | `-p` | | | | |
60
+ | `-q` | | | | |
61
+ | `-r` | rag | - / str | *[reserved for future use]* | Command |
62
+ | `-s` | shell | - / str | generate a shell command | Command |
63
+ | `-t` | text | str | generate text | Command |
64
+ | `-u` | user command | str | *[reserved for future use]* | Command |
65
+ | `-v` | verbose | - | debug logging | Option |
66
+ | `-w` | web search | str | search the web | Command |
67
+ | `-x` | execute | - | execute a shell command | Option |
68
+ | `-y` | | | | |
69
+ | `-z` | undo | - / int | undo exchanges (default 1) | Option |
70
+
71
+ <!-- [TODO]
72
+
73
+ # Library Usage
74
+
75
+ `q` follows a highly modular and provider-agnostic capability-driven design. -->
@@ -0,0 +1,56 @@
1
+ # Overview
2
+
3
+ `q` is a lightweight, flexible, multi-provider LLM-agent framework for the terminal.
4
+
5
+ I built this before Claude Code ever existed, and it remains useful to me for quick CLI interactions or prototyping multi-agent experiments. However, for most complex coding tasks, Claude Code is unquestionably superior.
6
+
7
+ # Installation
8
+
9
+ Install using any pip-compatible package manager (e.g. `pip`, `pipx`, `uv`, etc.):
10
+
11
+ ```bash
12
+ pipx install q-bot
13
+ ```
14
+
15
+ Requires Python 3.12+.
16
+
17
+ # CLI Usage
18
+
19
+ `q` uses a simple paradigm where each character from a-z is mapped to a single flag representing a command or option. This enables concise combinations of flags to achieve complex behavior.
20
+
21
+ ## Flag Reference
22
+
23
+ | Flag | Name | Arg | Description | Type |
24
+ | ---- | ------------ | ------- | ------------------------------ | ------: |
25
+ | `-a` | agent | | *[reserved for future use]* | Command |
26
+ | `-b` | batch | | *[reserved for future use]* | |
27
+ | `-c` | code | str | generate code | Command |
28
+ | `-d` | directory | - / str | add a directory to context | Option |
29
+ | `-e` | explain | - / str | explain code or text | Command |
30
+ | `-f` | file | str | read input from file | Option |
31
+ | `-g` | | | | |
32
+ | `-h` | help | - / str | help message / help agent | Command |
33
+ | `-i` | image | str | generate/edit an image | Command |
34
+ | `-j` | json | - | output as JSON | Option |
35
+ | `-k` | api key | str | *[reserved for future use]* | Option |
36
+ | `-l` | load | - / int | list all / load session by id | Command |
37
+ | `-m` | model | str | set model and/or provider | Option |
38
+ | `-n` | | | | |
39
+ | `-o` | output | str | output file | Option |
40
+ | `-p` | | | | |
41
+ | `-q` | | | | |
42
+ | `-r` | rag | - / str | *[reserved for future use]* | Command |
43
+ | `-s` | shell | - / str | generate a shell command | Command |
44
+ | `-t` | text | str | generate text | Command |
45
+ | `-u` | user command | str | *[reserved for future use]* | Command |
46
+ | `-v` | verbose | - | debug logging | Option |
47
+ | `-w` | web search | str | search the web | Command |
48
+ | `-x` | execute | - | execute a shell command | Option |
49
+ | `-y` | | | | |
50
+ | `-z` | undo | - / int | undo exchanges (default 1) | Option |
51
+
52
+ <!-- [TODO]
53
+
54
+ # Library Usage
55
+
56
+ `q` follows a highly modular and provider-agnostic capability-driven design. -->
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "q-bot"
7
+ dynamic = ["version"]
8
+ description = "An LLM agent from the comfort of your command line"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "anthropic==0.75.0",
12
+ "colorama==0.4.6",
13
+ "distro==1.9.0",
14
+ "humanize==4.14.0",
15
+ "openai==2.9.0",
16
+ "pydantic==2.12.5",
17
+ "pyperclip==1.11.0",
18
+ "python-dotenv==1.2.1",
19
+ "termcolor==3.2.0",
20
+ ]
21
+ authors = [
22
+ {name = "Tushar Khan", email = "dev@tusharkhan.com"}
23
+ ]
24
+ readme = "README.md"
25
+ license = "MIT"
26
+
27
+ [project.scripts]
28
+ q = "q.cli.main:main"
29
+
30
+ [project.urls]
31
+ Repository = "https://github.com/tk755/q"
32
+
33
+ [tool.setuptools.packages.find]
34
+ include = ["q*"]
35
+
36
+ [tool.setuptools.dynamic]
37
+ version = {attr = "q.__version__"}
@@ -0,0 +1 @@
1
+ __version__ = "2.0.0.dev1"
@@ -0,0 +1,63 @@
1
+ import asyncio
2
+
3
+ from .client import Client
4
+ from .message import Message, Role
5
+
6
+
7
+ class ChatAgent[T]:
8
+ """Conversational agent with persistent message history."""
9
+
10
+ def __init__(self, client: Client[T], system: str | None = None, messages: list[Message] | None = None):
11
+ self.client = client
12
+ self.system = system
13
+ self.messages: list[Message] = messages.copy() if messages else []
14
+
15
+ async def prompt(self, text: str) -> T:
16
+ """Generate response and update conversation history."""
17
+ self.messages.append(Message(role=Role.USER, content=text))
18
+
19
+ messages = self.messages
20
+ if self.system:
21
+ messages = [Message(role=Role.SYSTEM, content=self.system), *self.messages]
22
+ response = await self.client.generate(messages)
23
+
24
+ if isinstance(response, str):
25
+ self.messages.append(Message(role=Role.ASSISTANT, content=response))
26
+
27
+ return response
28
+
29
+ def drop_exchanges(self, n: int = 1) -> None:
30
+ """Drop the last N conversation exchanges (user message + responses)."""
31
+ if n <= 0:
32
+ return
33
+
34
+ user_messages_found = 0
35
+ for i in range(len(self.messages) - 1, -1, -1):
36
+ if self.messages[i].role == Role.USER:
37
+ user_messages_found += 1
38
+ if user_messages_found == n:
39
+ self.messages = self.messages[:i]
40
+ return
41
+
42
+
43
+ class BatchAgent[T]:
44
+ """Batch agent for applying a single prompt to multiple inputs concurrently."""
45
+
46
+ def __init__(self, client: Client[T], system: str | None = None):
47
+ self.client = client
48
+ self.system = system
49
+
50
+ async def batch_prompt(self, text_list: list[str], n_threads: int = 8) -> list[T]:
51
+ """Process multiple inputs concurrently and return the outputs in order."""
52
+ semaphore = asyncio.Semaphore(n_threads)
53
+
54
+ async def process(text: str) -> T:
55
+ async with semaphore:
56
+ messages: list[Message] = []
57
+ if self.system:
58
+ messages.append(Message(role=Role.SYSTEM, content=self.system))
59
+ messages.append(Message(role=Role.USER, content=text))
60
+ return await self.client.generate(messages)
61
+
62
+ tasks = [process(text) for text in text_list]
63
+ return await asyncio.gather(*tasks)
@@ -0,0 +1,434 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import os
6
+ import platform
7
+ import shutil
8
+ import string
9
+ import subprocess
10
+ import sys
11
+ from abc import ABC, abstractmethod
12
+ from enum import Enum
13
+ from pathlib import Path
14
+
15
+ import distro
16
+ import humanize
17
+ import pyperclip
18
+ from termcolor import colored
19
+
20
+ from q import __version__
21
+ from q.providers import load_client_class
22
+
23
+ from ..agents import ChatAgent
24
+ from ..message import Role
25
+ from .models import Tier, resolve_model_arg
26
+ from .session import SessionManager
27
+ from .terminal import UserError, format_response, qprint
28
+
29
+ # region Registry
30
+
31
+ COMMANDS: list[type[Command]] = []
32
+ OPTIONS: list[type[Flag]] = []
33
+
34
+
35
+ def get_default_command() -> type[Command]:
36
+ char = SessionManager.load_command_char()
37
+ if char:
38
+ for cmd in COMMANDS:
39
+ if cmd.char == char:
40
+ return cmd
41
+ return TextCommand
42
+
43
+
44
+ # region Types
45
+
46
+ type Value = str | int | None
47
+ type ArgMap = dict[str, Value]
48
+
49
+
50
+ class ValueType(Enum):
51
+ NONE = None
52
+ TEXT = "text"
53
+ STR = "str"
54
+ INT = "N"
55
+
56
+
57
+ # region Base Classes
58
+
59
+
60
+ class Flag(ABC):
61
+ """Base class for CLI flags. Subclasses auto-register to OPTIONS."""
62
+
63
+ char: str
64
+ desc: str
65
+ value_type: ValueType = ValueType.NONE
66
+ required: bool = False
67
+ default: Value = None
68
+
69
+ def __init_subclass__(cls, **kwargs):
70
+ """Auto-register subclass to OPTIONS if it defines a char."""
71
+ super().__init_subclass__(**kwargs)
72
+ if hasattr(cls, "char"):
73
+ OPTIONS.append(cls)
74
+
75
+
76
+ class Command(Flag):
77
+ """Base class for CLI commands. Subclasses auto-register to COMMANDS."""
78
+
79
+ def __init__(self, args: ArgMap):
80
+ self.args = args
81
+
82
+ def __init_subclass__(cls, **kwargs):
83
+ """Move subclass from OPTIONS to COMMANDS if it defines a char."""
84
+ super().__init_subclass__(**kwargs)
85
+ if hasattr(cls, "char"):
86
+ OPTIONS.remove(cls)
87
+ COMMANDS.append(cls)
88
+
89
+ @abstractmethod
90
+ async def execute(self) -> None: ...
91
+
92
+
93
+ class AgentCommand(Command):
94
+ """Base class for commands that prompt an agent."""
95
+
96
+ client_str: str = "TextClient"
97
+ tier: Tier
98
+ system: str | None = None
99
+ clip: bool = False
100
+
101
+ async def execute(self) -> None:
102
+ if "n" in self.args:
103
+ SessionManager.new_session()
104
+
105
+ # resolve provider, model, and model args
106
+ default_provider = SessionManager.load_default_provider()
107
+ provider, model, model_args = resolve_model_arg(self.args.get("m"), self.tier, default_provider)
108
+
109
+ # create client dynamically
110
+ client_class = load_client_class(provider, self.client_str)
111
+ api_key = SessionManager.load_api_key(provider)
112
+ client = client_class(api_key, model, **model_args)
113
+
114
+ if "v" in self.args:
115
+ qprint("MODEL PARAMETERS:", color="cyan", file=sys.stderr)
116
+ qprint("model: ", color="green", file=sys.stderr, end="")
117
+ qprint(f"{client.model} ({provider})", file=sys.stderr)
118
+ if client.model_args:
119
+ for k, v in client.model_args.items():
120
+ qprint(f"{k}: ", color="green", file=sys.stderr, end="")
121
+ qprint(f"{v}", file=sys.stderr)
122
+
123
+ # resolve system prompt (command's system overrides saved system)
124
+ system = self.system if self.system is not None else SessionManager.load_system()
125
+
126
+ # create agent
127
+ agent = ChatAgent(client, system, SessionManager.load_messages())
128
+ if "z" in self.args:
129
+ agent.drop_exchanges(self.args["z"])
130
+
131
+ # prompt agent and save session
132
+ response = await agent.prompt(self.args[self.char])
133
+ SessionManager.save_session(agent.system, agent.messages, self.char)
134
+
135
+ if "v" in self.args:
136
+ qprint("\nMESSAGES:", color="cyan", file=sys.stderr)
137
+ if agent.system:
138
+ qprint("system: ", color="green", file=sys.stderr, end="")
139
+ qprint(agent.system, file=sys.stderr)
140
+ for msg in agent.messages:
141
+ qprint(f"{msg.role.value}: ", color="green", file=sys.stderr, end="")
142
+ qprint(msg.content, file=sys.stderr)
143
+
144
+ # process response
145
+ self.process_response(response)
146
+
147
+ def process_response(self, response: str) -> None:
148
+ """Format response and route output."""
149
+ if "j" not in self.args:
150
+ response = format_response(response)
151
+
152
+ if "o" in self.args:
153
+ Path(self.args["o"]).write_text(response)
154
+ qprint(f"Response saved to {self.args['o']}", color="yellow", file=sys.stderr)
155
+ else:
156
+ if "v" not in self.args:
157
+ qprint(response)
158
+
159
+ # copy output to clipboard
160
+ if self.clip:
161
+ with contextlib.suppress(pyperclip.PyperclipException):
162
+ pyperclip.copy(response)
163
+ qprint("Copied to clipboard.", color="yellow", file=sys.stderr)
164
+
165
+
166
+ # region Commands
167
+
168
+
169
+ class TextCommand(AgentCommand):
170
+ char = "t"
171
+ desc = "text"
172
+ value_type = ValueType.TEXT
173
+ required = True
174
+ tier = Tier.MED
175
+ system = ""
176
+
177
+
178
+ class ExplainCommand(AgentCommand):
179
+ char = "e"
180
+ desc = "explain"
181
+ value_type = ValueType.TEXT
182
+ tier = Tier.HIGH
183
+ system = "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."
184
+
185
+
186
+ class CodeCommand(AgentCommand):
187
+ char = "c"
188
+ desc = "code"
189
+ value_type = ValueType.TEXT
190
+ required = True
191
+ tier = Tier.HIGH
192
+ clip = True
193
+
194
+ @property
195
+ def system(self) -> str:
196
+ return 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 {SessionManager.load_code_lang()} unless otherwise specified."
197
+
198
+
199
+ class ShellCommand(AgentCommand):
200
+ char = "s"
201
+ desc = "shell"
202
+ value_type = ValueType.TEXT
203
+ required = False
204
+ tier = Tier.MED
205
+ clip = True
206
+
207
+ @property
208
+ def system(self) -> str:
209
+ return f"You are a command-line assistant. Given a 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. System is running {self._get_system_info()}."
210
+
211
+ def _get_system_info(self) -> str:
212
+ shell = os.environ.get("SHELL") or os.environ.get("COMSPEC")
213
+ shell = Path(shell).name if shell else ""
214
+
215
+ sys_name = platform.system()
216
+ if sys_name == "Linux":
217
+ with contextlib.suppress(ImportError):
218
+ sys_name = distro.name(pretty=True)
219
+
220
+ if shell:
221
+ return f"{shell} on {sys_name}"
222
+ return sys_name
223
+
224
+ async def execute(self) -> None:
225
+ # rerun and fix last command (requires shell integration)
226
+ if self.args[self.char] is None:
227
+ cmd = os.environ.get("Q_CMD", None)
228
+ exit_code = os.environ.get("Q_EXIT", None)
229
+ if cmd is None or exit_code is None:
230
+ raise UserError(
231
+ "q -s without a prompt requires shell integration. Add to ~/.bashrc:\n"
232
+ ' q() { Q_EXIT=$? Q_CMD=$(fc -ln -1) command q "$@"; }'
233
+ )
234
+ cmd, exit_code = cmd.strip(), exit_code.strip()
235
+
236
+ try:
237
+ # run command and capture output
238
+ proc = await asyncio.create_subprocess_shell(
239
+ cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
240
+ )
241
+ stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=10)
242
+ text = f"The command `{cmd}` failed with exit code {proc.returncode}. Fix it."
243
+ if stderr:
244
+ text += f"\nSTDERR:\n{stderr.decode().strip()}"
245
+ if stdout:
246
+ text += f"\nSTDOUT:\n{stdout.decode().strip()}"
247
+ except TimeoutError:
248
+ # kill long-running command
249
+ proc.kill()
250
+ text = f"The command `{cmd}` failed with exit code {exit_code}. Fix it."
251
+ self.args[self.char] = text
252
+
253
+ await super().execute()
254
+
255
+ def process_response(self, response: str) -> None:
256
+ # execute command
257
+ if "x" in self.args:
258
+ qprint(f"> {response}", color="green", file=sys.stderr)
259
+ subprocess.run(response, shell=True)
260
+ else:
261
+ super().process_response(response)
262
+
263
+
264
+ class WebCommand(AgentCommand):
265
+ char = "w"
266
+ desc = "web"
267
+ value_type = ValueType.TEXT
268
+ required = True
269
+ tier = Tier.LOW
270
+ client_str = "WebClient"
271
+ system = "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. Always search the internet."
272
+
273
+
274
+ class ImageCommand(AgentCommand):
275
+ char = "i"
276
+ desc = "image"
277
+ value_type = ValueType.TEXT
278
+ required = True
279
+ tier = Tier.MED
280
+ client_str = "ImageClient"
281
+ system = "Generate an image of the following description."
282
+
283
+ def process_response(self, response: bytes) -> None:
284
+ text = self.args[self.char].translate(str.maketrans("", "", string.punctuation)).replace(" ", "_")
285
+ path = self.args.get("o") or f"q_{text}"
286
+ path = path if path.lower().endswith(".png") else f"{path}.png"
287
+ Path(path).write_bytes(response)
288
+ qprint(f"Image saved to {path}", color="yellow", file=sys.stderr)
289
+
290
+
291
+ class HelpCommand(AgentCommand):
292
+ char = "h"
293
+ desc = "help"
294
+ value_type = ValueType.TEXT
295
+ required = False
296
+ tier = Tier.LOW
297
+
298
+ @property
299
+ def system(self) -> str:
300
+ cli_dir = Path(__file__).parent
301
+ source_code = "\n\n".join((cli_dir / name).read_text() for name in Path(cli_dir).glob("*.py"))
302
+ return (
303
+ "You are `q`, a command-line LLM tool. Answer questions about usage based on the source code."
304
+ f"\n\n{source_code}\n\n"
305
+ "Be extremely concise. Answer in one line. Focus on usage."
306
+ "Always surround code snippets, commands, flags, and paths with backticks."
307
+ )
308
+
309
+ async def execute(self) -> None:
310
+ if self.args.get(self.char):
311
+ await super().execute()
312
+ else:
313
+ qprint(self._help_text())
314
+
315
+ def _help_text(self) -> str:
316
+ command_color = "cyan"
317
+ flags = []
318
+ for f in sorted(COMMANDS + OPTIONS, key=lambda f: f.char):
319
+ flag_arg = f.value_type.value or ""
320
+ if flag_arg:
321
+ flag_arg = f"<{flag_arg}>" if f.required else f"[{flag_arg}]"
322
+ flag_str = f" -{f.char} {f.desc} {flag_arg}"
323
+ flags.append(colored(flag_str, command_color if f in COMMANDS else "dark_grey"))
324
+
325
+ lines = [
326
+ f"q {__version__} - a command line programming agent",
327
+ "",
328
+ "Usage: q [-flag [value]] ...",
329
+ "",
330
+ " Flags can be combined: -sx = -s -x",
331
+ " Use -- to disable remaining flag parsing.",
332
+ f" One {colored('command', command_color)} is required.",
333
+ "",
334
+ "Flags:",
335
+ *flags,
336
+ ]
337
+ return "\n".join(lines)
338
+
339
+
340
+ class LoadCommand(Command):
341
+ char = "l"
342
+ desc = "load session"
343
+ value_type = ValueType.INT
344
+
345
+ async def execute(self) -> None:
346
+ session_id = self.args.get(self.char)
347
+ if session_id is None:
348
+ self._print_session_list()
349
+ elif not SessionManager.switch_session(session_id):
350
+ raise UserError(f"invalid session: {session_id}")
351
+ else:
352
+ qprint(f"Loaded session {session_id}", color="yellow", file=sys.stderr)
353
+
354
+ def _print_session_list(self) -> None:
355
+ sessions = SessionManager.list_sessions()
356
+ if not sessions:
357
+ qprint("No sessions found.")
358
+ return
359
+
360
+ current_id = SessionManager.load_session_id()
361
+ term_width = shutil.get_terminal_size().columns
362
+
363
+ for s in sessions:
364
+ age = humanize.naturaltime(s.updated) if s.updated else "unknown"
365
+ prefix_len = len(f" {s.id}. ")
366
+ suffix_len = len(f" ({age})")
367
+ max_len = max(20, term_width - prefix_len - suffix_len - 5)
368
+
369
+ preview = "(empty)"
370
+ for msg in reversed(s.messages):
371
+ if msg.role == Role.USER:
372
+ preview = msg.content[:max_len] + "..." if len(msg.content) > max_len else msg.content
373
+ break
374
+
375
+ line = f" {s.id}. {preview} ({age})"
376
+ color = None if s.id == current_id else "dark_grey"
377
+ qprint(line, color=color)
378
+
379
+
380
+ # region Options
381
+
382
+
383
+ class DirectoryOption(Flag):
384
+ char = "d"
385
+ desc = "directory"
386
+ value_type = ValueType.STR
387
+
388
+
389
+ class FileOption(Flag):
390
+ char = "f"
391
+ desc = "file"
392
+ value_type = ValueType.STR
393
+ required = True
394
+
395
+
396
+ class JsonOption(Flag):
397
+ char = "j"
398
+ desc = "json"
399
+
400
+
401
+ class ModelOption(Flag):
402
+ char = "m"
403
+ desc = "model"
404
+ value_type = ValueType.STR
405
+ required = True
406
+
407
+
408
+ class NewSessionOption(Flag):
409
+ char = "n"
410
+ desc = "new session"
411
+
412
+
413
+ class OutputOption(Flag):
414
+ char = "o"
415
+ desc = "output"
416
+ value_type = ValueType.STR
417
+ required = True
418
+
419
+
420
+ class VerboseOption(Flag):
421
+ char = "v"
422
+ desc = "verbose"
423
+
424
+
425
+ class ExecuteOption(Flag):
426
+ char = "x"
427
+ desc = "execute"
428
+
429
+
430
+ class UndoOption(Flag):
431
+ char = "z"
432
+ desc = "undo"
433
+ value_type = ValueType.INT
434
+ default = 1
@@ -0,0 +1,26 @@
1
+ import asyncio
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from .parser import parse
7
+ from .terminal import UserError, is_terminal, qprint
8
+
9
+
10
+ def main():
11
+ # suppress stderr when piped
12
+ if not is_terminal():
13
+ sys.stderr = Path(os.devnull).open("w") # noqa: SIM115
14
+
15
+ try:
16
+ command = parse(sys.argv[1:])
17
+ asyncio.run(command.execute())
18
+ except (UserError, ImportError) as e:
19
+ qprint(str(e), color="red", file=sys.stderr)
20
+ sys.exit(1)
21
+ except KeyboardInterrupt:
22
+ sys.exit(130)
23
+
24
+
25
+ if __name__ == "__main__":
26
+ main()