q-bot 2.0.0.dev1__tar.gz → 2.0.0.dev3__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.
Files changed (28) hide show
  1. q_bot-2.0.0.dev3/PKG-INFO +188 -0
  2. q_bot-2.0.0.dev3/README.md +169 -0
  3. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/pyproject.toml +2 -2
  4. q_bot-2.0.0.dev3/q/__init__.py +1 -0
  5. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/cli/commands.py +29 -60
  6. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/cli/main.py +4 -8
  7. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/cli/models.py +3 -3
  8. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/cli/parser.py +14 -12
  9. q_bot-2.0.0.dev3/q/cli/session.py +135 -0
  10. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/cli/terminal.py +2 -2
  11. q_bot-2.0.0.dev3/q_bot.egg-info/PKG-INFO +188 -0
  12. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q_bot.egg-info/requires.txt +1 -1
  13. q_bot-2.0.0.dev1/PKG-INFO +0 -75
  14. q_bot-2.0.0.dev1/README.md +0 -56
  15. q_bot-2.0.0.dev1/q/__init__.py +0 -1
  16. q_bot-2.0.0.dev1/q/cli/session.py +0 -207
  17. q_bot-2.0.0.dev1/q_bot.egg-info/PKG-INFO +0 -75
  18. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/agents.py +0 -0
  19. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/client.py +0 -0
  20. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/message.py +0 -0
  21. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/providers/__init__.py +0 -0
  22. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/providers/anthropic.py +0 -0
  23. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/providers/openai.py +0 -0
  24. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q_bot.egg-info/SOURCES.txt +0 -0
  25. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q_bot.egg-info/dependency_links.txt +0 -0
  26. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q_bot.egg-info/entry_points.txt +0 -0
  27. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q_bot.egg-info/top_level.txt +0 -0
  28. {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/setup.cfg +0 -0
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: q-bot
3
+ Version: 2.0.0.dev3
4
+ Summary: A provider-agnostic command-line agent and LLM framework.
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: openai==2.9.0
14
+ Requires-Dist: psutil==7.2.1
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 provider-agnostic command-line agent and LLM framework.
23
+
24
+ > I originally built this as a personal CLI tool before Claude Code existed. I still find it more useful for quick shell interactions and running multi-model experiments.
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]* | Command |
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 / agent | Command |
52
+ | `-i` | image | str | generate/edit an image | Command |
53
+ | `-j` | json | - | output as JSON | Option |
54
+ | `-k` | api key | str | override API key | Option |
55
+ | `-l` | code lang | str | override code generation language | Option |
56
+ | `-m` | model | str | override model/provider | Option |
57
+ | `-n` | new session | - | clear the session history | Option |
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 | 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
+ ## Sessions
72
+
73
+ Each terminal or script that runs `q` maintains an isolated **session** that persists conversation history across calls. Use `-n` to clear the session history for a new conversation or one-shot prompt. Sessions are automatically deleted when the parent shell process exits.
74
+
75
+ # Library Usage
76
+
77
+ The `q` library is built on two principles:
78
+
79
+ **Clients are single-capability.** Each client does one thing (e.g. text generation, image generation, web search, etc.) and has a static return type. No mode switching or tool selection logic is necessary.
80
+
81
+ **Agents are provider- and capability-agnostic.** Every agent accepts any client and inherits its return type, regardless of what the underlying client does or which provider it calls.
82
+
83
+ ## Clients
84
+
85
+ A **client** wraps a provider's API for one capability.
86
+
87
+ Clients extend `Client[T]` and are instantiated with an API key, model name, and optionally provider- and model-specific argument overrides. All clients expose the same `generate` method which returns a value of type `T`:
88
+
89
+ ```python
90
+ Client[T](api_key: str, model: str, **model_args)
91
+ Client[T].generate(messages: list[Message]) -> T
92
+ ```
93
+
94
+ A number of built-in clients with sensible defaults are provided for the following providers and capabilities:
95
+
96
+ | Client | T | Description | `openai` | `anthropic` |
97
+ | ------------- | ------- | ---------------------------- | :------: | :---------: |
98
+ | `TextClient` | `str` | text generation | ✓ | ✓ |
99
+ | `WebClient` | `str` | web-grounded text generation | ✓ | ✗ |
100
+ | `ImageClient` | `bytes` | image generation | ✓ | ✗ |
101
+
102
+ ### Dynamic Loading
103
+
104
+ Client classes are typically imported from their provider module:
105
+
106
+ ```python
107
+ from q.providers.openai import ImageClient
108
+
109
+ client = ImageClient(api_key, model, **model_args)
110
+ ```
111
+
112
+ They can also be dynamically loaded at runtime by specifying a provider and capability using the `load_client_class` utility:
113
+
114
+ ```python
115
+ from q.providers import load_client_class
116
+
117
+ client_class = load_client_class('openai', 'ImageClient')
118
+ client = client_class(api_key, model, **model_args)
119
+ ```
120
+
121
+ ## Agents
122
+
123
+ An **agent** manages conversation state and delegates generation to a client.
124
+
125
+ `ChatAgent[T]` maintains a message history and prepends an optional system prompt:
126
+
127
+ ```python
128
+ ChatAgent[T](client: Client[T], system: str | None = None)
129
+ ChatAgent[T].prompt(text: str) -> T
130
+ ```
131
+
132
+ `BatchAgent[T]` processes multiple inputs concurrently using a shared system prompt, with no conversation history:
133
+
134
+ ```python
135
+ BatchAgent[T](client: Client[T], system: str | None = None)
136
+ BatchAgent[T].batch_prompt(text_list: list[str], n_threads: int = 8) -> list[T]
137
+ ```
138
+
139
+ <!-- ## Examples
140
+
141
+ This design enables full LLM functionality with minimal code.
142
+
143
+ **Example 1:** Generate an image via OpenAI
144
+
145
+ ```python
146
+ from q.providers.openai import ImageClient
147
+ from q.agents import ChatAgent
148
+
149
+ client = ImageClient(api_key, "gpt-image-2", quality="high")
150
+ agent = ChatAgent(client)
151
+ image_bytes = await agent.prompt("a cat in space")
152
+ Path("cat.png").write_bytes(image_bytes)
153
+ ```
154
+
155
+ **Example 2:** Generate batch text via Anthropic
156
+
157
+ ```python
158
+ from q.providers.anthropic import TextClient
159
+ from q.agents import BatchAgent
160
+
161
+ client = TextClient(api_key, "claude-opus-4-8")
162
+ agent = BatchAgent(client, system="Identify the language of the text.")
163
+ inputs = ["How are you?", "¿Cómo estás?", "Comment ça va?"]
164
+ langs = await agent.batch_prompt(inputs)
165
+ ```
166
+
167
+ **Example 3:** Multi-model orchestration
168
+
169
+ ```python
170
+ from q.providers import load_client_class
171
+ from q.agents import ChatAgent
172
+
173
+ client1 = load_client_class('openai', 'TextClient')(oai_key, "gpt-5-5")
174
+ client2 = load_client_class('anthropic', 'TextClient')(anthropic_key, "claude-opus-4-8")
175
+
176
+ system = "You are an AI speaking with another AI. Engage in a discussion about the future of AI."
177
+
178
+ agent1 = ChatAgent(client1, system=system)
179
+ agent2 = ChatAgent(client2, system=system)
180
+
181
+ text = "What are your thoughts on the future of AI?"
182
+
183
+ for _ in range(5):
184
+ text = await agent1.prompt(text)
185
+ print("agent1:", text)
186
+ text = await agent2.prompt(text)
187
+ print("agent2:", text)
188
+ ``` -->
@@ -0,0 +1,169 @@
1
+ # Overview
2
+
3
+ `q` is a provider-agnostic command-line agent and LLM framework.
4
+
5
+ > I originally built this as a personal CLI tool before Claude Code existed. I still find it more useful for quick shell interactions and running multi-model experiments.
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]* | Command |
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 / agent | Command |
33
+ | `-i` | image | str | generate/edit an image | Command |
34
+ | `-j` | json | - | output as JSON | Option |
35
+ | `-k` | api key | str | override API key | Option |
36
+ | `-l` | code lang | str | override code generation language | Option |
37
+ | `-m` | model | str | override model/provider | Option |
38
+ | `-n` | new session | - | clear the session history | Option |
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 | 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
+ ## Sessions
53
+
54
+ Each terminal or script that runs `q` maintains an isolated **session** that persists conversation history across calls. Use `-n` to clear the session history for a new conversation or one-shot prompt. Sessions are automatically deleted when the parent shell process exits.
55
+
56
+ # Library Usage
57
+
58
+ The `q` library is built on two principles:
59
+
60
+ **Clients are single-capability.** Each client does one thing (e.g. text generation, image generation, web search, etc.) and has a static return type. No mode switching or tool selection logic is necessary.
61
+
62
+ **Agents are provider- and capability-agnostic.** Every agent accepts any client and inherits its return type, regardless of what the underlying client does or which provider it calls.
63
+
64
+ ## Clients
65
+
66
+ A **client** wraps a provider's API for one capability.
67
+
68
+ Clients extend `Client[T]` and are instantiated with an API key, model name, and optionally provider- and model-specific argument overrides. All clients expose the same `generate` method which returns a value of type `T`:
69
+
70
+ ```python
71
+ Client[T](api_key: str, model: str, **model_args)
72
+ Client[T].generate(messages: list[Message]) -> T
73
+ ```
74
+
75
+ A number of built-in clients with sensible defaults are provided for the following providers and capabilities:
76
+
77
+ | Client | T | Description | `openai` | `anthropic` |
78
+ | ------------- | ------- | ---------------------------- | :------: | :---------: |
79
+ | `TextClient` | `str` | text generation | ✓ | ✓ |
80
+ | `WebClient` | `str` | web-grounded text generation | ✓ | ✗ |
81
+ | `ImageClient` | `bytes` | image generation | ✓ | ✗ |
82
+
83
+ ### Dynamic Loading
84
+
85
+ Client classes are typically imported from their provider module:
86
+
87
+ ```python
88
+ from q.providers.openai import ImageClient
89
+
90
+ client = ImageClient(api_key, model, **model_args)
91
+ ```
92
+
93
+ They can also be dynamically loaded at runtime by specifying a provider and capability using the `load_client_class` utility:
94
+
95
+ ```python
96
+ from q.providers import load_client_class
97
+
98
+ client_class = load_client_class('openai', 'ImageClient')
99
+ client = client_class(api_key, model, **model_args)
100
+ ```
101
+
102
+ ## Agents
103
+
104
+ An **agent** manages conversation state and delegates generation to a client.
105
+
106
+ `ChatAgent[T]` maintains a message history and prepends an optional system prompt:
107
+
108
+ ```python
109
+ ChatAgent[T](client: Client[T], system: str | None = None)
110
+ ChatAgent[T].prompt(text: str) -> T
111
+ ```
112
+
113
+ `BatchAgent[T]` processes multiple inputs concurrently using a shared system prompt, with no conversation history:
114
+
115
+ ```python
116
+ BatchAgent[T](client: Client[T], system: str | None = None)
117
+ BatchAgent[T].batch_prompt(text_list: list[str], n_threads: int = 8) -> list[T]
118
+ ```
119
+
120
+ <!-- ## Examples
121
+
122
+ This design enables full LLM functionality with minimal code.
123
+
124
+ **Example 1:** Generate an image via OpenAI
125
+
126
+ ```python
127
+ from q.providers.openai import ImageClient
128
+ from q.agents import ChatAgent
129
+
130
+ client = ImageClient(api_key, "gpt-image-2", quality="high")
131
+ agent = ChatAgent(client)
132
+ image_bytes = await agent.prompt("a cat in space")
133
+ Path("cat.png").write_bytes(image_bytes)
134
+ ```
135
+
136
+ **Example 2:** Generate batch text via Anthropic
137
+
138
+ ```python
139
+ from q.providers.anthropic import TextClient
140
+ from q.agents import BatchAgent
141
+
142
+ client = TextClient(api_key, "claude-opus-4-8")
143
+ agent = BatchAgent(client, system="Identify the language of the text.")
144
+ inputs = ["How are you?", "¿Cómo estás?", "Comment ça va?"]
145
+ langs = await agent.batch_prompt(inputs)
146
+ ```
147
+
148
+ **Example 3:** Multi-model orchestration
149
+
150
+ ```python
151
+ from q.providers import load_client_class
152
+ from q.agents import ChatAgent
153
+
154
+ client1 = load_client_class('openai', 'TextClient')(oai_key, "gpt-5-5")
155
+ client2 = load_client_class('anthropic', 'TextClient')(anthropic_key, "claude-opus-4-8")
156
+
157
+ system = "You are an AI speaking with another AI. Engage in a discussion about the future of AI."
158
+
159
+ agent1 = ChatAgent(client1, system=system)
160
+ agent2 = ChatAgent(client2, system=system)
161
+
162
+ text = "What are your thoughts on the future of AI?"
163
+
164
+ for _ in range(5):
165
+ text = await agent1.prompt(text)
166
+ print("agent1:", text)
167
+ text = await agent2.prompt(text)
168
+ print("agent2:", text)
169
+ ``` -->
@@ -5,14 +5,14 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "q-bot"
7
7
  dynamic = ["version"]
8
- description = "An LLM agent from the comfort of your command line"
8
+ description = "A provider-agnostic command-line agent and LLM framework."
9
9
  requires-python = ">=3.12"
10
10
  dependencies = [
11
11
  "anthropic==0.75.0",
12
12
  "colorama==0.4.6",
13
13
  "distro==1.9.0",
14
- "humanize==4.14.0",
15
14
  "openai==2.9.0",
15
+ "psutil==7.2.1",
16
16
  "pydantic==2.12.5",
17
17
  "pyperclip==1.11.0",
18
18
  "python-dotenv==1.2.1",
@@ -0,0 +1 @@
1
+ __version__ = "2.0.0.dev3"
@@ -4,7 +4,6 @@ import asyncio
4
4
  import contextlib
5
5
  import os
6
6
  import platform
7
- import shutil
8
7
  import string
9
8
  import subprocess
10
9
  import sys
@@ -13,7 +12,6 @@ from enum import Enum
13
12
  from pathlib import Path
14
13
 
15
14
  import distro
16
- import humanize
17
15
  import pyperclip
18
16
  from termcolor import colored
19
17
 
@@ -21,10 +19,9 @@ from q import __version__
21
19
  from q.providers import load_client_class
22
20
 
23
21
  from ..agents import ChatAgent
24
- from ..message import Role
25
22
  from .models import Tier, resolve_model_arg
26
- from .session import SessionManager
27
- from .terminal import UserError, format_response, qprint
23
+ from .session import StateManager
24
+ from .terminal import InputError, format_response, qprint
28
25
 
29
26
  # region Registry
30
27
 
@@ -33,7 +30,7 @@ OPTIONS: list[type[Flag]] = []
33
30
 
34
31
 
35
32
  def get_default_command() -> type[Command]:
36
- char = SessionManager.load_command_char()
33
+ char = StateManager.load_command_char()
37
34
  if char:
38
35
  for cmd in COMMANDS:
39
36
  if cmd.char == char:
@@ -93,22 +90,19 @@ class Command(Flag):
93
90
  class AgentCommand(Command):
94
91
  """Base class for commands that prompt an agent."""
95
92
 
96
- client_str: str = "TextClient"
97
93
  tier: Tier
94
+ client_str: str = "TextClient"
98
95
  system: str | None = None
99
96
  clip: bool = False
100
97
 
101
98
  async def execute(self) -> None:
102
- if "n" in self.args:
103
- SessionManager.new_session()
104
-
105
99
  # resolve provider, model, and model args
106
- default_provider = SessionManager.load_default_provider()
100
+ default_provider = StateManager.load_default_provider()
107
101
  provider, model, model_args = resolve_model_arg(self.args.get("m"), self.tier, default_provider)
108
102
 
109
103
  # create client dynamically
110
104
  client_class = load_client_class(provider, self.client_str)
111
- api_key = SessionManager.load_api_key(provider)
105
+ api_key = self.args.get("k") or StateManager.load_api_key(provider)
112
106
  client = client_class(api_key, model, **model_args)
113
107
 
114
108
  if "v" in self.args:
@@ -120,17 +114,17 @@ class AgentCommand(Command):
120
114
  qprint(f"{k}: ", color="green", file=sys.stderr, end="")
121
115
  qprint(f"{v}", file=sys.stderr)
122
116
 
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
117
  # create agent
127
- agent = ChatAgent(client, system, SessionManager.load_messages())
118
+ messages = [] if "n" in self.args else StateManager.load_messages()
119
+ agent = ChatAgent(client, self.system, messages)
128
120
  if "z" in self.args:
129
121
  agent.drop_exchanges(self.args["z"])
130
122
 
131
- # prompt agent and save session
123
+ # prompt agent
132
124
  response = await agent.prompt(self.args[self.char])
133
- SessionManager.save_session(agent.system, agent.messages, self.char)
125
+
126
+ # save session
127
+ StateManager.save_session(self.char, agent.messages)
134
128
 
135
129
  if "v" in self.args:
136
130
  qprint("\nMESSAGES:", color="cyan", file=sys.stderr)
@@ -179,6 +173,7 @@ class ExplainCommand(AgentCommand):
179
173
  char = "e"
180
174
  desc = "explain"
181
175
  value_type = ValueType.TEXT
176
+ required = True
182
177
  tier = Tier.HIGH
183
178
  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
179
 
@@ -193,7 +188,8 @@ class CodeCommand(AgentCommand):
193
188
 
194
189
  @property
195
190
  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."
191
+ code_lang = self.args.get("l") or StateManager.load_code_lang()
192
+ 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. Use the {code_lang} programming language."
197
193
 
198
194
 
199
195
  class ShellCommand(AgentCommand):
@@ -227,7 +223,7 @@ class ShellCommand(AgentCommand):
227
223
  cmd = os.environ.get("Q_CMD", None)
228
224
  exit_code = os.environ.get("Q_EXIT", None)
229
225
  if cmd is None or exit_code is None:
230
- raise UserError(
226
+ raise InputError(
231
227
  "q -s without a prompt requires shell integration. Add to ~/.bashrc:\n"
232
228
  ' q() { Q_EXIT=$? Q_CMD=$(fc -ln -1) command q "$@"; }'
233
229
  )
@@ -337,46 +333,6 @@ class HelpCommand(AgentCommand):
337
333
  return "\n".join(lines)
338
334
 
339
335
 
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
336
  # region Options
381
337
 
382
338
 
@@ -398,6 +354,19 @@ class JsonOption(Flag):
398
354
  desc = "json"
399
355
 
400
356
 
357
+ class KeyOption(Flag):
358
+ char = "k"
359
+ desc = "api key"
360
+ value_type = ValueType.STR
361
+ required = True
362
+
363
+
364
+ class CodeLangOption(Flag):
365
+ char = "l"
366
+ desc = "code lang"
367
+ value_type = ValueType.STR
368
+
369
+
401
370
  class ModelOption(Flag):
402
371
  char = "m"
403
372
  desc = "model"
@@ -1,21 +1,17 @@
1
1
  import asyncio
2
- import os
3
2
  import sys
4
- from pathlib import Path
5
3
 
6
4
  from .parser import parse
7
- from .terminal import UserError, is_terminal, qprint
5
+ from .session import StateManager
6
+ from .terminal import InputError, qprint
8
7
 
9
8
 
10
9
  def main():
11
- # suppress stderr when piped
12
- if not is_terminal():
13
- sys.stderr = Path(os.devnull).open("w") # noqa: SIM115
14
-
15
10
  try:
16
11
  command = parse(sys.argv[1:])
12
+ StateManager.reap_sessions()
17
13
  asyncio.run(command.execute())
18
- except (UserError, ImportError) as e:
14
+ except (InputError, ImportError) as e:
19
15
  qprint(str(e), color="red", file=sys.stderr)
20
16
  sys.exit(1)
21
17
  except KeyboardInterrupt:
@@ -1,7 +1,7 @@
1
1
  import contextlib
2
2
  from enum import Enum
3
3
 
4
- from .terminal import UserError
4
+ from .terminal import InputError
5
5
 
6
6
 
7
7
  class Tier(Enum):
@@ -57,7 +57,7 @@ MODELS = {
57
57
 
58
58
  def _lookup(provider: str, tier: Tier) -> tuple[str, str, dict]:
59
59
  if provider not in MODELS:
60
- raise UserError(f"unknown provider: {provider}")
60
+ raise InputError(f"unknown provider: {provider}")
61
61
 
62
62
  config = MODELS[provider][tier]
63
63
  model = config["model"]
@@ -87,4 +87,4 @@ def resolve_model_arg(arg: str | None, default_tier: Tier, default_provider: str
87
87
  if arg in MODELS:
88
88
  return _lookup(arg, default_tier)
89
89
 
90
- raise UserError(f"invalid model: {arg}")
90
+ raise InputError(f"invalid model: {arg}")