termask-ai 1.0.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.
- termai_pkg/__init__.py +1244 -0
- termask_ai-1.0.0.dist-info/METADATA +246 -0
- termask_ai-1.0.0.dist-info/RECORD +7 -0
- termask_ai-1.0.0.dist-info/WHEEL +5 -0
- termask_ai-1.0.0.dist-info/entry_points.txt +3 -0
- termask_ai-1.0.0.dist-info/licenses/LICENSE +21 -0
- termask_ai-1.0.0.dist-info/top_level.txt +1 -0
termai_pkg/__init__.py
ADDED
|
@@ -0,0 +1,1244 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import requests
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import copy # Import copy for deepcopy
|
|
8
|
+
import shutil # Import shutil to check for editor availability
|
|
9
|
+
|
|
10
|
+
# --- Configuration Paths (XDG Base Directory Specification) ---
|
|
11
|
+
# https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
|
12
|
+
APP_NAME = "termai"
|
|
13
|
+
|
|
14
|
+
# XDG_CONFIG_HOME: user-specific config files (default: ~/.config)
|
|
15
|
+
# This is where credentials and settings live.
|
|
16
|
+
_xdg_config_home = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
17
|
+
CONFIG_DIR = _xdg_config_home / APP_NAME
|
|
18
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
19
|
+
|
|
20
|
+
# XDG_DATA_HOME: user-specific data files (default: ~/.local/share)
|
|
21
|
+
# Reserved for future use (e.g. chat history, caches).
|
|
22
|
+
_xdg_data_home = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
|
|
23
|
+
DATA_DIR = _xdg_data_home / APP_NAME
|
|
24
|
+
|
|
25
|
+
# Legacy paths — used only for one-time migration
|
|
26
|
+
_LEGACY_DATA_DIR = Path.home() / ".local" / "share" / APP_NAME
|
|
27
|
+
_LEGACY_CONFIG_FILE = _LEGACY_DATA_DIR / "config.json"
|
|
28
|
+
OLD_KEY_FILE = _LEGACY_DATA_DIR / "key"
|
|
29
|
+
|
|
30
|
+
# --- Colors ---
|
|
31
|
+
if sys.stdout.isatty():
|
|
32
|
+
GREEN = "\033[92m"
|
|
33
|
+
CYAN = "\033[96m"
|
|
34
|
+
YELLOW = "\033[93m"
|
|
35
|
+
RED = "\033[91m"
|
|
36
|
+
BLUE = "\033[94m"
|
|
37
|
+
RESET = "\033[0m"
|
|
38
|
+
BG_USER = "\033[48;5;99m\033[38;5;255m"
|
|
39
|
+
BG_HEADER = "\033[48;5;24m\033[38;5;255m"
|
|
40
|
+
else:
|
|
41
|
+
GREEN = ""
|
|
42
|
+
CYAN = ""
|
|
43
|
+
YELLOW = ""
|
|
44
|
+
RED = ""
|
|
45
|
+
BLUE = ""
|
|
46
|
+
RESET = ""
|
|
47
|
+
BG_USER = ""
|
|
48
|
+
BG_HEADER = ""
|
|
49
|
+
|
|
50
|
+
# --- Default Settings ---
|
|
51
|
+
# If the config file is deleted/missing, these values are used to recreate it.
|
|
52
|
+
DEFAULT_CONFIG = {
|
|
53
|
+
"active_profile": "gemini-default",
|
|
54
|
+
"profiles": {
|
|
55
|
+
"gemini-default": {
|
|
56
|
+
"provider": "gemini",
|
|
57
|
+
"api_key": "",
|
|
58
|
+
"model_name": "gemini-2.5-flash",
|
|
59
|
+
"system_instruction": "You are a CLI assistant for command-line users. Answer concisely and use clear formatting. Use standard Markdown for headers, bolding, bullet points, and code blocks.",
|
|
60
|
+
"generation_config": {
|
|
61
|
+
"temperature": 0.7,
|
|
62
|
+
"top_p": 0.9,
|
|
63
|
+
"top_k": 40,
|
|
64
|
+
"maxOutputTokens": 1024
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"openai-default": {
|
|
68
|
+
"provider": "openai",
|
|
69
|
+
"base_url": "https://api.openai.com/v1",
|
|
70
|
+
"api_key": "",
|
|
71
|
+
"model_name": "gpt-4o",
|
|
72
|
+
"system_instruction": "You are a CLI assistant for command-line users. Answer concisely and use clear formatting. Use standard Markdown for headers, bolding, bullet points, and code blocks.",
|
|
73
|
+
"temperature": 0.7,
|
|
74
|
+
"max_tokens": 1024
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"proxy": ""
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def load_config():
|
|
81
|
+
"""
|
|
82
|
+
Loads config.json from ~/.config/termai/ (XDG_CONFIG_HOME).
|
|
83
|
+
Auto-migrates config from old ~/.local/share/termai/ location on first run.
|
|
84
|
+
Handles migration from old nested structure and creates default file if missing.
|
|
85
|
+
"""
|
|
86
|
+
# Ensure config directory exists
|
|
87
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
|
|
89
|
+
# --- One-time migration from old ~/.local/share/termai/config.json ---
|
|
90
|
+
if not CONFIG_FILE.exists() and _LEGACY_CONFIG_FILE.exists():
|
|
91
|
+
print(f"[{APP_NAME}] Migrating config to ~/.config/termai/config.json (XDG standard)...")
|
|
92
|
+
import shutil as _shutil
|
|
93
|
+
_shutil.copy2(_LEGACY_CONFIG_FILE, CONFIG_FILE)
|
|
94
|
+
# Rename the old file so migration doesn't re-trigger
|
|
95
|
+
_LEGACY_CONFIG_FILE.rename(_LEGACY_CONFIG_FILE.with_suffix(".json.bak"))
|
|
96
|
+
print(f"[{APP_NAME}] Migration complete. Old file backed up as config.json.bak")
|
|
97
|
+
|
|
98
|
+
config = {}
|
|
99
|
+
# 1. Check for Config File
|
|
100
|
+
if CONFIG_FILE.exists():
|
|
101
|
+
try:
|
|
102
|
+
with open(CONFIG_FILE, "r") as f:
|
|
103
|
+
config = json.load(f)
|
|
104
|
+
except json.JSONDecodeError:
|
|
105
|
+
print(f"[Error] Your config file ({CONFIG_FILE}) is invalid JSON.")
|
|
106
|
+
print("Please fix it or delete it to reset defaults.")
|
|
107
|
+
sys.exit(1)
|
|
108
|
+
|
|
109
|
+
# Migration from old static structure to profile-based structure
|
|
110
|
+
if "profiles" not in config:
|
|
111
|
+
print(f"[{APP_NAME}] Migrating config to profile-based structure...")
|
|
112
|
+
old_provider = config.get("provider", "gemini")
|
|
113
|
+
old_proxy = config.get("proxy", "")
|
|
114
|
+
|
|
115
|
+
new_config = copy.deepcopy(DEFAULT_CONFIG)
|
|
116
|
+
new_config["proxy"] = old_proxy
|
|
117
|
+
|
|
118
|
+
if "gemini_config" in config:
|
|
119
|
+
new_config["profiles"]["gemini-default"] = {
|
|
120
|
+
"provider": "gemini",
|
|
121
|
+
"api_key": config["gemini_config"].get("api_key", ""),
|
|
122
|
+
"model_name": config["gemini_config"].get("model_name", "gemini-2.5-flash"),
|
|
123
|
+
"system_instruction": config["gemini_config"].get("system_instruction", DEFAULT_CONFIG["profiles"]["gemini-default"]["system_instruction"]),
|
|
124
|
+
"generation_config": config["gemini_config"].get("generation_config", DEFAULT_CONFIG["profiles"]["gemini-default"]["generation_config"])
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if "openai_config" in config:
|
|
128
|
+
new_config["profiles"]["openai-default"] = {
|
|
129
|
+
"provider": "openai",
|
|
130
|
+
"base_url": config["openai_config"].get("base_url", "https://api.openai.com/v1"),
|
|
131
|
+
"api_key": config["openai_config"].get("api_key", ""),
|
|
132
|
+
"model_name": config["openai_config"].get("model_name", "gpt-4o"),
|
|
133
|
+
"system_instruction": config["openai_config"].get("system_instruction", DEFAULT_CONFIG["profiles"]["openai-default"]["system_instruction"]),
|
|
134
|
+
"temperature": config["openai_config"].get("temperature", 0.7),
|
|
135
|
+
"max_tokens": config["openai_config"].get("max_tokens", 1024)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if old_provider == "openai":
|
|
139
|
+
new_config["active_profile"] = "openai-default"
|
|
140
|
+
else:
|
|
141
|
+
new_config["active_profile"] = "gemini-default"
|
|
142
|
+
|
|
143
|
+
config = new_config
|
|
144
|
+
with open(CONFIG_FILE, "w") as f:
|
|
145
|
+
json.dump(config, f, indent=4)
|
|
146
|
+
print("Migration complete.")
|
|
147
|
+
return new_config
|
|
148
|
+
|
|
149
|
+
# Modernize profiles to have base_url if they are openai provider and missing base_url
|
|
150
|
+
updated = False
|
|
151
|
+
if "profiles" in config:
|
|
152
|
+
for p_name, p_config in config["profiles"].items():
|
|
153
|
+
if p_config.get("provider") == "openai" and "base_url" not in p_config:
|
|
154
|
+
p_config["base_url"] = "https://api.openai.com/v1"
|
|
155
|
+
updated = True
|
|
156
|
+
|
|
157
|
+
# Check for restrictive legacy system instruction
|
|
158
|
+
sys_instr = p_config.get("system_instruction", "")
|
|
159
|
+
if "Do NOT use Markdown" in sys_instr or "Do NOT use backticks" in sys_instr:
|
|
160
|
+
if p_config.get("provider") == "gemini":
|
|
161
|
+
p_config["system_instruction"] = DEFAULT_CONFIG["profiles"]["gemini-default"]["system_instruction"]
|
|
162
|
+
else:
|
|
163
|
+
p_config["system_instruction"] = DEFAULT_CONFIG["profiles"]["openai-default"]["system_instruction"]
|
|
164
|
+
updated = True
|
|
165
|
+
|
|
166
|
+
if updated:
|
|
167
|
+
with open(CONFIG_FILE, "w") as f:
|
|
168
|
+
json.dump(config, f, indent=4)
|
|
169
|
+
|
|
170
|
+
return config
|
|
171
|
+
|
|
172
|
+
# If no config file exists, proceed with first run setup
|
|
173
|
+
# 2. Migration: If no config, check for old key file
|
|
174
|
+
gemini_api_key = ""
|
|
175
|
+
backup_file = DATA_DIR / "key.bak"
|
|
176
|
+
if OLD_KEY_FILE.exists():
|
|
177
|
+
print(f"[{APP_NAME}] Migrating legacy key file to new config format...")
|
|
178
|
+
with open(OLD_KEY_FILE, "r") as f:
|
|
179
|
+
gemini_api_key = f.read().strip()
|
|
180
|
+
OLD_KEY_FILE.rename(backup_file)
|
|
181
|
+
|
|
182
|
+
# 3. First Run Setup
|
|
183
|
+
new_config = copy.deepcopy(DEFAULT_CONFIG)
|
|
184
|
+
if sys.stdin.isatty() and "--complete" not in sys.argv:
|
|
185
|
+
print(f"[{APP_NAME}] First run! Choose your primary AI provider.")
|
|
186
|
+
provider = ""
|
|
187
|
+
while provider not in ["1", "2"]:
|
|
188
|
+
provider = input("Enter 1 for Gemini or 2 for OpenAI: ").strip()
|
|
189
|
+
|
|
190
|
+
if provider == "1":
|
|
191
|
+
new_config["active_profile"] = "gemini-default"
|
|
192
|
+
if not gemini_api_key:
|
|
193
|
+
print(f"[{APP_NAME}] Enter your Gemini API Key. Get it from aistudio.google.com")
|
|
194
|
+
gemini_api_key = input("Gemini API Key: ").strip()
|
|
195
|
+
if not gemini_api_key:
|
|
196
|
+
print("Error: Gemini key cannot be empty.")
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
new_config["profiles"]["gemini-default"]["api_key"] = gemini_api_key
|
|
199
|
+
|
|
200
|
+
elif provider == "2":
|
|
201
|
+
new_config["active_profile"] = "openai-default"
|
|
202
|
+
print(f"[{APP_NAME}] Enter OpenAI Base URL (Press Enter for default: https://api.openai.com/v1)")
|
|
203
|
+
base_url = input("Base URL: ").strip()
|
|
204
|
+
if base_url:
|
|
205
|
+
new_config["profiles"]["openai-default"]["base_url"] = base_url
|
|
206
|
+
|
|
207
|
+
print(f"[{APP_NAME}] Enter your OpenAI or custom API Key.")
|
|
208
|
+
openai_api_key = input("API Key: ").strip()
|
|
209
|
+
if not openai_api_key:
|
|
210
|
+
print("Error: API Key cannot be empty.")
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
new_config["profiles"]["openai-default"]["api_key"] = openai_api_key
|
|
213
|
+
|
|
214
|
+
print(f"[{APP_NAME}] Enter Model Name (Press Enter for default: gpt-4o)")
|
|
215
|
+
model_name = input("Model Name: ").strip()
|
|
216
|
+
if model_name:
|
|
217
|
+
new_config["profiles"]["openai-default"]["model_name"] = model_name
|
|
218
|
+
else:
|
|
219
|
+
# Default to Gemini if non-interactive and no config exists
|
|
220
|
+
if not gemini_api_key:
|
|
221
|
+
return None # Cannot proceed without an API key
|
|
222
|
+
new_config["profiles"]["gemini-default"]["api_key"] = gemini_api_key
|
|
223
|
+
|
|
224
|
+
# Save the new configuration
|
|
225
|
+
with open(CONFIG_FILE, "w") as f:
|
|
226
|
+
json.dump(new_config, f, indent=4)
|
|
227
|
+
|
|
228
|
+
print(f"Configuration saved to {CONFIG_FILE}\n")
|
|
229
|
+
|
|
230
|
+
# Clean up the legacy key backup file if it exists after migration
|
|
231
|
+
if backup_file.exists():
|
|
232
|
+
backup_file.unlink()
|
|
233
|
+
|
|
234
|
+
return new_config
|
|
235
|
+
|
|
236
|
+
def open_editor():
|
|
237
|
+
"""Opens config.json in the user's terminal editor."""
|
|
238
|
+
# 1. Prioritize the user's explicit choice
|
|
239
|
+
editor = os.getenv('EDITOR')
|
|
240
|
+
|
|
241
|
+
# 2. If no $EDITOR, try to find 'vim'
|
|
242
|
+
if not editor and shutil.which('vim'):
|
|
243
|
+
editor = 'vim'
|
|
244
|
+
|
|
245
|
+
# 3. If still no editor, fall back to 'nano'
|
|
246
|
+
if not editor:
|
|
247
|
+
editor = 'nano'
|
|
248
|
+
|
|
249
|
+
print(f"Opening config in {editor}...")
|
|
250
|
+
try:
|
|
251
|
+
subprocess.call([editor, str(CONFIG_FILE)])
|
|
252
|
+
except FileNotFoundError:
|
|
253
|
+
print(f"[Error] Editor '{editor}' not found. Please install it or set the $EDITOR environment variable.")
|
|
254
|
+
return 1
|
|
255
|
+
return 0 # Return 0 for success
|
|
256
|
+
|
|
257
|
+
def print_help():
|
|
258
|
+
"""Prints the help menu with available commands."""
|
|
259
|
+
help_markdown = """
|
|
260
|
+
# Termai - A CLI AI Assistant
|
|
261
|
+
A lightweight CLI tool for AI integration in your terminal.
|
|
262
|
+
|
|
263
|
+
## Usage
|
|
264
|
+
* `ai [OPTIONS] "YOUR QUERY"`
|
|
265
|
+
* `cat file.txt | ai [OPTIONS] "OPTIONAL PROMPT"`
|
|
266
|
+
|
|
267
|
+
## Options
|
|
268
|
+
* `-i`, `--chat`, `chat` : Start an interactive chat session
|
|
269
|
+
* `-p`, `--profile [name]` : Run query using or switching temporarily to a profile
|
|
270
|
+
* `-m`, `--model [name]` : List available Gemini models or set a specific one
|
|
271
|
+
* `profile [action]` : Profile management: `list`, `use`, `add`, `remove` (or `rm`)
|
|
272
|
+
* `completion [shell]` : Generate shell auto-completion script (`bash` or `zsh`)
|
|
273
|
+
* `-o`, `--save <file>` : Save the response or chat session to a file
|
|
274
|
+
* `--config` : Open configuration file
|
|
275
|
+
* `--debug` : Enable debug mode
|
|
276
|
+
* `--debug-config` : Print the loaded configuration (redacts keys)
|
|
277
|
+
* `--help`, `-h` : Show this help message
|
|
278
|
+
* `--reinstall` : Re-run the first-time setup
|
|
279
|
+
|
|
280
|
+
## Legacy Profile Flags (deprecated)
|
|
281
|
+
* `--profiles` : List all configured profiles
|
|
282
|
+
* `--use [name]` : Set active profile default
|
|
283
|
+
* `--profile-add <name>` : Add a new custom profile
|
|
284
|
+
* `--profile-remove <n>` : Remove a profile
|
|
285
|
+
|
|
286
|
+
## Examples
|
|
287
|
+
* `ai "How do I unzip a tar file?"`
|
|
288
|
+
* `ai chat`
|
|
289
|
+
* `ai chat -o session.md`
|
|
290
|
+
* `ai profile use`
|
|
291
|
+
* `ai -p local-ollama "What is Python?"`
|
|
292
|
+
* `ai --model`
|
|
293
|
+
* `cat error.log | ai "Explain this error briefly"`
|
|
294
|
+
"""
|
|
295
|
+
print(render_markdown(help_markdown.strip()))
|
|
296
|
+
return 0 # Return 0 for success
|
|
297
|
+
|
|
298
|
+
def handle_completion(config):
|
|
299
|
+
"""Generates autocomplete suggestions for bash/zsh tab completion."""
|
|
300
|
+
try:
|
|
301
|
+
complete_idx = sys.argv.index("--complete")
|
|
302
|
+
cword_idx = sys.argv.index("--cword")
|
|
303
|
+
# Extract command line words
|
|
304
|
+
words = sys.argv[complete_idx + 1:cword_idx]
|
|
305
|
+
cword = int(sys.argv[cword_idx + 1])
|
|
306
|
+
except (ValueError, IndexError):
|
|
307
|
+
return 0
|
|
308
|
+
|
|
309
|
+
if cword < 0 or cword >= len(words):
|
|
310
|
+
return 0
|
|
311
|
+
|
|
312
|
+
cur = words[cword]
|
|
313
|
+
suggestions = []
|
|
314
|
+
|
|
315
|
+
# Case 1: First argument completion (ai [tab] or ai ch[tab])
|
|
316
|
+
if cword == 1:
|
|
317
|
+
suggestions = [
|
|
318
|
+
"chat", "profile", "completion", "help",
|
|
319
|
+
"-i", "--chat", "-p", "--profile", "-m", "--model",
|
|
320
|
+
"--profiles", "--use", "--profile-add", "--profile-remove",
|
|
321
|
+
"--config", "--debug", "--debug-config", "--help", "-h", "--reinstall"
|
|
322
|
+
]
|
|
323
|
+
|
|
324
|
+
# Case 2: Subcommands/Options under 'profile'
|
|
325
|
+
elif cword == 2 and words[1] == "profile":
|
|
326
|
+
suggestions = ["list", "use", "set", "add", "remove", "rm", "help", "--help", "-h"]
|
|
327
|
+
|
|
328
|
+
# Case 3: Profile names for 'profile use/set/remove/rm'
|
|
329
|
+
elif cword == 3 and words[1] == "profile" and words[2] in ["use", "set", "remove", "rm"]:
|
|
330
|
+
if config:
|
|
331
|
+
suggestions = list(config.get("profiles", {}).keys())
|
|
332
|
+
|
|
333
|
+
# Case 4: Profile names for legacy/temporary profile flags
|
|
334
|
+
elif cword >= 2 and words[cword - 1] in ["--use", "--profile-remove", "--profile", "-p"]:
|
|
335
|
+
if config:
|
|
336
|
+
suggestions = list(config.get("profiles", {}).keys())
|
|
337
|
+
|
|
338
|
+
# Case 5: Model names for --model/-m
|
|
339
|
+
elif cword >= 2 and words[cword - 1] in ["--model", "-m"]:
|
|
340
|
+
suggestions = ["gemini-2.5-flash", "gemini-2.5-pro", "gpt-4o", "gpt-4o-mini"]
|
|
341
|
+
|
|
342
|
+
# Case 6: Shell options for 'completion'
|
|
343
|
+
elif cword == 2 and words[1] == "completion":
|
|
344
|
+
suggestions = ["bash", "zsh"]
|
|
345
|
+
|
|
346
|
+
# Filter and print matching suggestions
|
|
347
|
+
matches = [s for s in suggestions if s.startswith(cur)]
|
|
348
|
+
for m in matches:
|
|
349
|
+
print(m)
|
|
350
|
+
return 0
|
|
351
|
+
|
|
352
|
+
def visual_len(s):
|
|
353
|
+
"""Calculates the visual column width of a string in the terminal, accounting for double-width wide characters/emojis."""
|
|
354
|
+
length = 0
|
|
355
|
+
for char in s:
|
|
356
|
+
if ord(char) > 0x2000:
|
|
357
|
+
length += 2
|
|
358
|
+
else:
|
|
359
|
+
length += 1
|
|
360
|
+
return length
|
|
361
|
+
|
|
362
|
+
def visual_ljust(s, width):
|
|
363
|
+
"""Pads a string with spaces to a visual width, rather than character count width."""
|
|
364
|
+
v_len = visual_len(s)
|
|
365
|
+
needed = width - v_len
|
|
366
|
+
if needed > 0:
|
|
367
|
+
return s + (" " * needed)
|
|
368
|
+
return s
|
|
369
|
+
|
|
370
|
+
def save_chat_history(history, filename, provider, target_profile, model_name):
|
|
371
|
+
"""Saves the chat history to a file in a clean Markdown format."""
|
|
372
|
+
from datetime import datetime
|
|
373
|
+
try:
|
|
374
|
+
filepath = Path(filename).resolve()
|
|
375
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
376
|
+
|
|
377
|
+
with open(filepath, "w") as f:
|
|
378
|
+
f.write(f"# Termai Chat Session\n")
|
|
379
|
+
f.write(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
|
380
|
+
f.write(f"Profile: {target_profile} | Provider: {provider.capitalize()} | Model: {model_name}\n\n")
|
|
381
|
+
f.write(f"---\n\n")
|
|
382
|
+
|
|
383
|
+
for msg in history:
|
|
384
|
+
if provider == "gemini":
|
|
385
|
+
role = "You" if msg.get("role") == "user" else "AI"
|
|
386
|
+
text = msg.get("parts", [{}])[0].get("text", "")
|
|
387
|
+
else: # openai
|
|
388
|
+
role = "You" if msg.get("role") == "user" else "AI"
|
|
389
|
+
text = msg.get("content", "")
|
|
390
|
+
|
|
391
|
+
f.write(f"### {role}\n{text}\n\n")
|
|
392
|
+
|
|
393
|
+
print(f"{GREEN}[✓] Chat history saved successfully to: {filepath}{RESET}")
|
|
394
|
+
return True
|
|
395
|
+
except Exception as e:
|
|
396
|
+
print(f"{RED}[Error] Failed to save chat history: {e}{RESET}")
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
def save_single_response(text, filename):
|
|
400
|
+
"""Saves a single AI response to a file."""
|
|
401
|
+
try:
|
|
402
|
+
filepath = Path(filename).resolve()
|
|
403
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
404
|
+
with open(filepath, "w") as f:
|
|
405
|
+
f.write(text.strip() + "\n")
|
|
406
|
+
print(f"\n{GREEN}[✓] Response saved to: {filepath}{RESET}")
|
|
407
|
+
except Exception as e:
|
|
408
|
+
print(f"\n{RED}[Error] Failed to save response: {e}{RESET}")
|
|
409
|
+
|
|
410
|
+
def print_user_message(prompt_text, message_text):
|
|
411
|
+
"""Prints a styled user message block with full-width background color and clean word wrapping."""
|
|
412
|
+
if not BG_USER:
|
|
413
|
+
print(f"{prompt_text}{message_text}")
|
|
414
|
+
return
|
|
415
|
+
|
|
416
|
+
import shutil
|
|
417
|
+
import textwrap
|
|
418
|
+
|
|
419
|
+
try:
|
|
420
|
+
cols = shutil.get_terminal_size().columns
|
|
421
|
+
except Exception:
|
|
422
|
+
cols = 80
|
|
423
|
+
|
|
424
|
+
width = cols - 1 if cols > 1 else 1
|
|
425
|
+
|
|
426
|
+
lines = textwrap.wrap(message_text, width=width, initial_indent=prompt_text, subsequent_indent=" " * len(prompt_text))
|
|
427
|
+
for line in lines:
|
|
428
|
+
padded = visual_ljust(line, width)
|
|
429
|
+
print(f"{BG_USER}{padded}{RESET}")
|
|
430
|
+
|
|
431
|
+
def print_header_block(target_profile, provider, model_name):
|
|
432
|
+
"""Prints the chat session header block with full-width background color."""
|
|
433
|
+
if not BG_HEADER:
|
|
434
|
+
print(f"\n💬 Termai Interactive Chat Session")
|
|
435
|
+
print(f"Using Profile: {target_profile} | Provider: {provider.capitalize()} | Model: {model_name}")
|
|
436
|
+
print(f"Type exit or quit (or Ctrl+D) to end the chat.\n")
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
import shutil
|
|
440
|
+
import textwrap
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
cols = shutil.get_terminal_size().columns
|
|
444
|
+
except Exception:
|
|
445
|
+
cols = 80
|
|
446
|
+
|
|
447
|
+
width = cols - 1 if cols > 1 else 1
|
|
448
|
+
|
|
449
|
+
title = "💬 Termai Interactive Chat Session"
|
|
450
|
+
details = f"Using Profile: {target_profile} | Provider: {provider.capitalize()} | Model: {model_name}"
|
|
451
|
+
info = "Type exit or quit (or Ctrl+D) to end the chat."
|
|
452
|
+
|
|
453
|
+
lines = []
|
|
454
|
+
for text in [title, details, info]:
|
|
455
|
+
wrapped = textwrap.wrap(text, width=width - 4)
|
|
456
|
+
for w in wrapped:
|
|
457
|
+
lines.append(f" {w}")
|
|
458
|
+
|
|
459
|
+
pad = " " * width
|
|
460
|
+
print()
|
|
461
|
+
print(f"{BG_HEADER}{pad}{RESET}")
|
|
462
|
+
for line in lines:
|
|
463
|
+
padded = visual_ljust(line, width)
|
|
464
|
+
print(f"{BG_HEADER}{padded}{RESET}")
|
|
465
|
+
print(f"{BG_HEADER}{pad}{RESET}")
|
|
466
|
+
|
|
467
|
+
def list_profiles(config):
|
|
468
|
+
"""Displays a formatted list of all configured profiles and indicates which is currently active."""
|
|
469
|
+
profiles = config.get("profiles", {})
|
|
470
|
+
active = config.get("active_profile", "")
|
|
471
|
+
|
|
472
|
+
print(f"\n{BLUE}💬 Configured Profiles:{RESET}")
|
|
473
|
+
for idx, p_name in enumerate(profiles.keys(), 1):
|
|
474
|
+
is_active = f" {GREEN}(active){RESET}" if p_name == active else ""
|
|
475
|
+
p_config = profiles[p_name]
|
|
476
|
+
prov = p_config.get("provider", "gemini")
|
|
477
|
+
model = p_config.get("model_name", "")
|
|
478
|
+
extra = f" ({p_config['base_url']})" if prov == "openai" and "base_url" in p_config else ""
|
|
479
|
+
print(f" {CYAN}{idx}. {p_name}{RESET} [{YELLOW}{prov}{RESET}] -> {model}{extra}{is_active}")
|
|
480
|
+
print()
|
|
481
|
+
return 0
|
|
482
|
+
|
|
483
|
+
def switch_profile(config, profile_name=None):
|
|
484
|
+
"""Changes the active profile globally, either directly or via an interactive selection list."""
|
|
485
|
+
profiles = config.get("profiles", {})
|
|
486
|
+
|
|
487
|
+
if not profile_name:
|
|
488
|
+
# Interactive Selection list
|
|
489
|
+
print(f"\n{BLUE}💬 Select a profile to set as default active profile:{RESET}")
|
|
490
|
+
profile_list = list(profiles.keys())
|
|
491
|
+
active = config.get("active_profile", "")
|
|
492
|
+
|
|
493
|
+
for idx, p_name in enumerate(profile_list, 1):
|
|
494
|
+
is_active = f" {GREEN}(active){RESET}" if p_name == active else ""
|
|
495
|
+
p_config = profiles[p_name]
|
|
496
|
+
prov = p_config.get("provider", "gemini")
|
|
497
|
+
model = p_config.get("model_name", "")
|
|
498
|
+
print(f" {CYAN}{idx}. {p_name}{RESET} [{YELLOW}{prov}{RESET}] -> {model}{is_active}")
|
|
499
|
+
|
|
500
|
+
try:
|
|
501
|
+
choice = input(f"\nSelect a profile number to set as active (or press Enter to cancel): ").strip()
|
|
502
|
+
if not choice:
|
|
503
|
+
print("Cancelled.")
|
|
504
|
+
return 0
|
|
505
|
+
choice_idx = int(choice) - 1
|
|
506
|
+
if 0 <= choice_idx < len(profile_list):
|
|
507
|
+
profile_name = profile_list[choice_idx]
|
|
508
|
+
else:
|
|
509
|
+
print(f"{RED}[!] Invalid choice.{RESET}")
|
|
510
|
+
return 1
|
|
511
|
+
except ValueError:
|
|
512
|
+
print(f"{RED}[!] Invalid number entered.{RESET}")
|
|
513
|
+
return 1
|
|
514
|
+
except (KeyboardInterrupt, EOFError):
|
|
515
|
+
print("\nCancelled.")
|
|
516
|
+
return 0
|
|
517
|
+
|
|
518
|
+
if profile_name in profiles:
|
|
519
|
+
config["active_profile"] = profile_name
|
|
520
|
+
with open(CONFIG_FILE, "w") as f:
|
|
521
|
+
json.dump(config, f, indent=4)
|
|
522
|
+
print(f"{GREEN}[✓] Default active profile switched successfully to: {profile_name}{RESET}")
|
|
523
|
+
return 0
|
|
524
|
+
else:
|
|
525
|
+
print(f"{RED}[Error] Profile '{profile_name}' not found.{RESET}")
|
|
526
|
+
return 1
|
|
527
|
+
|
|
528
|
+
def add_profile(config, profile_name):
|
|
529
|
+
"""Adds a new profile to config.json interactively."""
|
|
530
|
+
profiles = config.get("profiles", {})
|
|
531
|
+
if profile_name in profiles:
|
|
532
|
+
print(f"{RED}[Error] Profile '{profile_name}' already exists.{RESET}")
|
|
533
|
+
return 1
|
|
534
|
+
|
|
535
|
+
print(f"\n{BLUE}🚀 Adding new profile: {profile_name}{RESET}")
|
|
536
|
+
print("Choose profile provider type:")
|
|
537
|
+
print(" 1. Gemini")
|
|
538
|
+
print(" 2. OpenAI (or OpenAI-compatible custom endpoint)")
|
|
539
|
+
|
|
540
|
+
provider_type = ""
|
|
541
|
+
while provider_type not in ["1", "2"]:
|
|
542
|
+
try:
|
|
543
|
+
provider_type = input("Choice [1-2]: ").strip()
|
|
544
|
+
except (KeyboardInterrupt, EOFError):
|
|
545
|
+
print("\nCancelled.")
|
|
546
|
+
return 0
|
|
547
|
+
|
|
548
|
+
new_profile = {}
|
|
549
|
+
if provider_type == "1":
|
|
550
|
+
new_profile["provider"] = "gemini"
|
|
551
|
+
print(f"[{APP_NAME}] Enter your Gemini API Key. Get it from aistudio.google.com")
|
|
552
|
+
api_key = input("Gemini API Key: ").strip()
|
|
553
|
+
if not api_key:
|
|
554
|
+
print("Error: Key cannot be empty.")
|
|
555
|
+
return 1
|
|
556
|
+
new_profile["api_key"] = api_key
|
|
557
|
+
new_profile["model_name"] = "gemini-2.5-flash"
|
|
558
|
+
new_profile["system_instruction"] = DEFAULT_CONFIG["profiles"]["gemini-default"]["system_instruction"]
|
|
559
|
+
new_profile["generation_config"] = DEFAULT_CONFIG["profiles"]["gemini-default"]["generation_config"]
|
|
560
|
+
else:
|
|
561
|
+
new_profile["provider"] = "openai"
|
|
562
|
+
print(f"[{APP_NAME}] Enter OpenAI/Custom Base URL (Press Enter for default: https://api.openai.com/v1)")
|
|
563
|
+
base_url = input("Base URL: ").strip()
|
|
564
|
+
new_profile["base_url"] = base_url if base_url else "https://api.openai.com/v1"
|
|
565
|
+
|
|
566
|
+
print(f"[{APP_NAME}] Enter your OpenAI/Custom API Key.")
|
|
567
|
+
api_key = input("API Key: ").strip()
|
|
568
|
+
if not api_key:
|
|
569
|
+
print("Error: Key cannot be empty.")
|
|
570
|
+
return 1
|
|
571
|
+
new_profile["api_key"] = api_key
|
|
572
|
+
|
|
573
|
+
print(f"[{APP_NAME}] Enter Model Name (Press Enter for default: gpt-4o)")
|
|
574
|
+
model_name = input("Model Name: ").strip()
|
|
575
|
+
new_profile["model_name"] = model_name if model_name else "gpt-4o"
|
|
576
|
+
new_profile["system_instruction"] = DEFAULT_CONFIG["profiles"]["openai-default"]["system_instruction"]
|
|
577
|
+
new_profile["temperature"] = 0.7
|
|
578
|
+
new_profile["max_tokens"] = 1024
|
|
579
|
+
|
|
580
|
+
config["profiles"][profile_name] = new_profile
|
|
581
|
+
with open(CONFIG_FILE, "w") as f:
|
|
582
|
+
json.dump(config, f, indent=4)
|
|
583
|
+
print(f"{GREEN}[✓] Profile '{profile_name}' created successfully!{RESET}")
|
|
584
|
+
return 0
|
|
585
|
+
|
|
586
|
+
def remove_profile(config, profile_name):
|
|
587
|
+
"""Deletes a profile from config.json."""
|
|
588
|
+
profiles = config.get("profiles", {})
|
|
589
|
+
active = config.get("active_profile", "")
|
|
590
|
+
|
|
591
|
+
if profile_name not in profiles:
|
|
592
|
+
print(f"{RED}[Error] Profile '{profile_name}' not found.{RESET}")
|
|
593
|
+
return 1
|
|
594
|
+
|
|
595
|
+
if profile_name == active:
|
|
596
|
+
print(f"{RED}[Error] Cannot remove currently active profile '{profile_name}'. Please switch to another profile first.{RESET}")
|
|
597
|
+
return 1
|
|
598
|
+
|
|
599
|
+
del config["profiles"][profile_name]
|
|
600
|
+
with open(CONFIG_FILE, "w") as f:
|
|
601
|
+
json.dump(config, f, indent=4)
|
|
602
|
+
print(f"{GREEN}[✓] Profile '{profile_name}' deleted successfully.{RESET}")
|
|
603
|
+
return 0
|
|
604
|
+
|
|
605
|
+
def handle_model_option(config):
|
|
606
|
+
"""Fetches and displays available Gemini models interactively, or directly sets the model if specified."""
|
|
607
|
+
active_profile = config.get("active_profile", "")
|
|
608
|
+
profiles = config.get("profiles", {})
|
|
609
|
+
profile_config = profiles.get(active_profile, {})
|
|
610
|
+
|
|
611
|
+
provider = profile_config.get("provider", "gemini")
|
|
612
|
+
if provider != "gemini":
|
|
613
|
+
print(f"{YELLOW}[*] Model listing/switching is currently supported for profiles using the Gemini provider.{RESET}")
|
|
614
|
+
return 0
|
|
615
|
+
|
|
616
|
+
api_key = profile_config.get("api_key")
|
|
617
|
+
current_model = profile_config.get("model_name", "gemini-2.5-flash")
|
|
618
|
+
|
|
619
|
+
# Check if a model argument is provided after the flag
|
|
620
|
+
model_arg = ""
|
|
621
|
+
model_flags = ["--model", "-m"]
|
|
622
|
+
for i, arg in enumerate(sys.argv):
|
|
623
|
+
if arg in model_flags and i + 1 < len(sys.argv):
|
|
624
|
+
# Ensure it is not another flag
|
|
625
|
+
if not sys.argv[i + 1].startswith("-"):
|
|
626
|
+
model_arg = sys.argv[i + 1].strip()
|
|
627
|
+
break
|
|
628
|
+
|
|
629
|
+
if model_arg:
|
|
630
|
+
clean_model = model_arg.replace("models/", "")
|
|
631
|
+
config["profiles"][active_profile]["model_name"] = clean_model
|
|
632
|
+
with open(CONFIG_FILE, "w") as f:
|
|
633
|
+
json.dump(config, f, indent=4)
|
|
634
|
+
print(f"{GREEN}[✓] Model for profile '{active_profile}' updated successfully to: {clean_model}{RESET}")
|
|
635
|
+
return 0
|
|
636
|
+
|
|
637
|
+
if not api_key:
|
|
638
|
+
print(f"{RED}[Error] Gemini API key not found for active profile '{active_profile}'. Please configure it first.{RESET}")
|
|
639
|
+
return 1
|
|
640
|
+
|
|
641
|
+
print(f"{BLUE}[*] Fetching available models from Gemini API...{RESET}")
|
|
642
|
+
api_url = f"https://generativelanguage.googleapis.com/v1beta/models?key={api_key}"
|
|
643
|
+
try:
|
|
644
|
+
response = requests.get(api_url)
|
|
645
|
+
if response.status_code != 200:
|
|
646
|
+
print(f"{RED}[Error {response.status_code}] Failed to fetch models: {response.text}{RESET}")
|
|
647
|
+
return 1
|
|
648
|
+
data = response.json()
|
|
649
|
+
models = data.get("models", [])
|
|
650
|
+
|
|
651
|
+
generation_models = []
|
|
652
|
+
for m in models:
|
|
653
|
+
name = m.get("name", "")
|
|
654
|
+
if "generateContent" in m.get("supportedGenerationMethods", []):
|
|
655
|
+
short_name = name.replace("models/", "")
|
|
656
|
+
display_name = m.get("displayName", short_name)
|
|
657
|
+
desc = m.get("description", "No description available.")
|
|
658
|
+
generation_models.append({
|
|
659
|
+
"name": short_name,
|
|
660
|
+
"displayName": display_name,
|
|
661
|
+
"description": desc
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
if not generation_models:
|
|
665
|
+
print(f"{YELLOW}[!] No text generation models returned from Gemini API.{RESET}")
|
|
666
|
+
return 0
|
|
667
|
+
|
|
668
|
+
print(f"\n{BLUE}🔍 Available Gemini Text Models:{RESET}")
|
|
669
|
+
for idx, m in enumerate(generation_models, 1):
|
|
670
|
+
is_current = f" {GREEN}(active){RESET}" if m["name"] == current_model else ""
|
|
671
|
+
print(f" {CYAN}{idx}. {m['displayName']}{RESET} [{YELLOW}{m['name']}{RESET}]{is_current}")
|
|
672
|
+
print(f" {m['description']}")
|
|
673
|
+
print()
|
|
674
|
+
|
|
675
|
+
try:
|
|
676
|
+
choice = input(f"Select a model number to set as active (or press Enter to cancel): ").strip()
|
|
677
|
+
if not choice:
|
|
678
|
+
print("Cancelled.")
|
|
679
|
+
return 0
|
|
680
|
+
choice_idx = int(choice) - 1
|
|
681
|
+
if 0 <= choice_idx < len(generation_models):
|
|
682
|
+
selected_model = generation_models[choice_idx]["name"]
|
|
683
|
+
config["profiles"][active_profile]["model_name"] = selected_model
|
|
684
|
+
with open(CONFIG_FILE, "w") as f:
|
|
685
|
+
json.dump(config, f, indent=4)
|
|
686
|
+
print(f"{GREEN}[✓] Gemini model for profile '{active_profile}' successfully updated to: {selected_model}{RESET}")
|
|
687
|
+
else:
|
|
688
|
+
print(f"{RED}[!] Invalid choice.{RESET}")
|
|
689
|
+
except ValueError:
|
|
690
|
+
print(f"{RED}[!] Invalid number entered.{RESET}")
|
|
691
|
+
except (KeyboardInterrupt, EOFError):
|
|
692
|
+
print("\nCancelled.")
|
|
693
|
+
|
|
694
|
+
return 0
|
|
695
|
+
except Exception as e:
|
|
696
|
+
print(f"{RED}[Connection Error] Failed to contact Gemini API: {e}{RESET}")
|
|
697
|
+
return 1
|
|
698
|
+
|
|
699
|
+
def render_markdown(text):
|
|
700
|
+
"""Renders basic Markdown beautifully in terminal using ANSI escape codes, with an optional rich-library fallback."""
|
|
701
|
+
if not sys.stdout.isatty():
|
|
702
|
+
return text
|
|
703
|
+
|
|
704
|
+
# Try to use rich library if available
|
|
705
|
+
try:
|
|
706
|
+
from rich.console import Console
|
|
707
|
+
from rich.markdown import Markdown
|
|
708
|
+
import io
|
|
709
|
+
string_io = io.StringIO()
|
|
710
|
+
console = Console(file=string_io, force_terminal=True)
|
|
711
|
+
console.print(Markdown(text))
|
|
712
|
+
return string_io.getvalue().strip()
|
|
713
|
+
except ImportError:
|
|
714
|
+
pass
|
|
715
|
+
|
|
716
|
+
# High-fidelity custom ANSI renderer fallback
|
|
717
|
+
lines = text.split("\n")
|
|
718
|
+
rendered_lines = []
|
|
719
|
+
in_code_block = False
|
|
720
|
+
code_lang = ""
|
|
721
|
+
|
|
722
|
+
for line in lines:
|
|
723
|
+
# 1. Handle Code Block boundaries
|
|
724
|
+
if line.strip().startswith("```"):
|
|
725
|
+
if not in_code_block:
|
|
726
|
+
in_code_block = True
|
|
727
|
+
code_lang = line.strip()[3:].strip()
|
|
728
|
+
lang_str = f" {code_lang.upper()} " if code_lang else " CODE "
|
|
729
|
+
border = f"\033[96m┌──────────────────{lang_str}──────────────────\033[0m"
|
|
730
|
+
rendered_lines.append(border)
|
|
731
|
+
else:
|
|
732
|
+
in_code_block = False
|
|
733
|
+
border = "\033[96m└───────────────────────────────────────────────\033[0m"
|
|
734
|
+
rendered_lines.append(border)
|
|
735
|
+
continue
|
|
736
|
+
|
|
737
|
+
# 2. Inside Code Block
|
|
738
|
+
if in_code_block:
|
|
739
|
+
rendered_lines.append(f"\033[93m{line}\033[0m")
|
|
740
|
+
continue
|
|
741
|
+
|
|
742
|
+
# 3. Headers (# Header, ## Header, etc.)
|
|
743
|
+
if line.strip().startswith("#"):
|
|
744
|
+
stripped = line.strip()
|
|
745
|
+
header_text = stripped.lstrip("#").strip()
|
|
746
|
+
rendered_lines.append(f"\033[1;96m✦ {header_text}\033[0m")
|
|
747
|
+
continue
|
|
748
|
+
|
|
749
|
+
# 4. Bullet Points (* item, - item, etc.)
|
|
750
|
+
stripped_line = line.lstrip()
|
|
751
|
+
indent = line[:len(line) - len(stripped_line)]
|
|
752
|
+
if stripped_line.startswith("* ") or stripped_line.startswith("- ") or stripped_line.startswith("+ "):
|
|
753
|
+
bullet_text = stripped_line[2:]
|
|
754
|
+
rendered_lines.append(f"{indent}\033[92m•\033[0m {bullet_text}")
|
|
755
|
+
continue
|
|
756
|
+
|
|
757
|
+
# 5. Inline formatting (Bold, Italic, Inline Code)
|
|
758
|
+
formatted_line = line
|
|
759
|
+
import re
|
|
760
|
+
formatted_line = re.sub(r'`([^`]+)`', r'\033[96m\1\033[0m', formatted_line)
|
|
761
|
+
formatted_line = re.sub(r'\*\*([^*]+)\*\*', r'\033[1m\1\033[22m', formatted_line)
|
|
762
|
+
formatted_line = re.sub(r'\*([^*]+)\*', r'\033[3m\1\033[23m', formatted_line)
|
|
763
|
+
|
|
764
|
+
rendered_lines.append(formatted_line)
|
|
765
|
+
|
|
766
|
+
return "\n".join(rendered_lines)
|
|
767
|
+
|
|
768
|
+
def send_gemini_request(profile_config, user_input, debug_mode, proxy="", history=None, output_file=None):
|
|
769
|
+
api_key = profile_config.get("api_key")
|
|
770
|
+
model_name = profile_config.get("model_name", "gemini-2.5-flash")
|
|
771
|
+
system_instr = profile_config.get("system_instruction", "")
|
|
772
|
+
gen_config = profile_config.get("generation_config", {})
|
|
773
|
+
api_url = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent?key={api_key}"
|
|
774
|
+
|
|
775
|
+
payload_contents = history if history is not None else [{"parts": [{"text": user_input}]}]
|
|
776
|
+
payload = {
|
|
777
|
+
"contents": payload_contents,
|
|
778
|
+
"systemInstruction": {"parts": [{"text": system_instr}]},
|
|
779
|
+
"generationConfig": gen_config
|
|
780
|
+
}
|
|
781
|
+
if debug_mode: print(f"[Debug] Provider: Gemini | Model: {model_name} | Temp: {gen_config.get('temperature')} | Proxy: {proxy if proxy else 'None'}")
|
|
782
|
+
try:
|
|
783
|
+
proxies = {"http": proxy, "https": proxy} if proxy else None
|
|
784
|
+
response = requests.post(api_url, json=payload, proxies=proxies)
|
|
785
|
+
if debug_mode:
|
|
786
|
+
print(f"[Debug] Status: {response.status_code}")
|
|
787
|
+
if response.status_code != 200:
|
|
788
|
+
if response.status_code == 429:
|
|
789
|
+
print(f"\n[Error 429] You have exceeded your Gemini API quota.")
|
|
790
|
+
print("Please check your usage and billing details at aistudio.google.com.")
|
|
791
|
+
else:
|
|
792
|
+
print(f"\n[Error {response.status_code}]")
|
|
793
|
+
print(response.text)
|
|
794
|
+
return 1
|
|
795
|
+
data = response.json()
|
|
796
|
+
if "promptFeedback" in data and "blockReason" in data["promptFeedback"]:
|
|
797
|
+
print(f"[Blocked] Reason: {data['promptFeedback']['blockReason']}")
|
|
798
|
+
return 0
|
|
799
|
+
if "candidates" in data and data["candidates"]:
|
|
800
|
+
cand = data["candidates"][0]
|
|
801
|
+
if "content" in cand and "parts" in cand["content"] and cand["content"]["parts"]:
|
|
802
|
+
response_text = cand['content']['parts'][0]['text']
|
|
803
|
+
rendered_text = render_markdown(response_text)
|
|
804
|
+
print(rendered_text.strip())
|
|
805
|
+
if history is not None:
|
|
806
|
+
history.append({"role": "model", "parts": [{"text": response_text}]})
|
|
807
|
+
if output_file and history is None:
|
|
808
|
+
save_single_response(response_text, output_file)
|
|
809
|
+
else:
|
|
810
|
+
print("[No content returned]")
|
|
811
|
+
if debug_mode: print(data)
|
|
812
|
+
else:
|
|
813
|
+
print("[Error] Invalid response format from Gemini")
|
|
814
|
+
if debug_mode: print(data)
|
|
815
|
+
return 0
|
|
816
|
+
except Exception as e:
|
|
817
|
+
print(f"\n[Connection Error] {e}")
|
|
818
|
+
return 1
|
|
819
|
+
|
|
820
|
+
def send_openai_request(profile_config, user_input, debug_mode, proxy="", history=None, output_file=None):
|
|
821
|
+
api_key = profile_config.get("api_key")
|
|
822
|
+
model_name = profile_config.get("model_name", "gpt-4o")
|
|
823
|
+
system_instr = profile_config.get("system_instruction", "")
|
|
824
|
+
temperature = profile_config.get("temperature", 0.7)
|
|
825
|
+
max_tokens = profile_config.get("max_tokens", 1024)
|
|
826
|
+
base_url = profile_config.get("base_url", "https://api.openai.com/v1")
|
|
827
|
+
# Form the completions endpoint URL robustly
|
|
828
|
+
if base_url.endswith("/"):
|
|
829
|
+
base_url = base_url[:-1]
|
|
830
|
+
|
|
831
|
+
if base_url.endswith("/chat/completions"):
|
|
832
|
+
api_url = base_url
|
|
833
|
+
else:
|
|
834
|
+
api_url = f"{base_url}/chat/completions"
|
|
835
|
+
|
|
836
|
+
headers = {
|
|
837
|
+
"Content-Type": "application/json",
|
|
838
|
+
"Authorization": f"Bearer {api_key}"
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if history is not None:
|
|
842
|
+
payload_messages = [{"role": "system", "content": system_instr}] + history
|
|
843
|
+
else:
|
|
844
|
+
payload_messages = [
|
|
845
|
+
{"role": "system", "content": system_instr},
|
|
846
|
+
{"role": "user", "content": user_input}
|
|
847
|
+
]
|
|
848
|
+
|
|
849
|
+
payload = {
|
|
850
|
+
"model": model_name,
|
|
851
|
+
"messages": payload_messages,
|
|
852
|
+
"temperature": temperature,
|
|
853
|
+
"max_tokens": max_tokens
|
|
854
|
+
}
|
|
855
|
+
if debug_mode: print(f"[Debug] Provider: OpenAI | Model: {model_name} | Temp: {temperature} | Proxy: {proxy if proxy else 'None'}")
|
|
856
|
+
try:
|
|
857
|
+
proxies = {"http": proxy, "https": proxy} if proxy else None
|
|
858
|
+
response = requests.post(api_url, headers=headers, json=payload, proxies=proxies)
|
|
859
|
+
if debug_mode:
|
|
860
|
+
print(f"[Debug] Status: {response.status_code}")
|
|
861
|
+
if response.status_code != 200:
|
|
862
|
+
if response.status_code == 429:
|
|
863
|
+
print(f"\n[Error 429] You have exceeded your OpenAI API quota.")
|
|
864
|
+
print("Please check your usage and billing details at platform.openai.com.")
|
|
865
|
+
else:
|
|
866
|
+
print(f"\n[Error {response.status_code}]")
|
|
867
|
+
print(response.text)
|
|
868
|
+
return 1
|
|
869
|
+
data = response.json()
|
|
870
|
+
if "choices" in data and data["choices"]:
|
|
871
|
+
message = data["choices"][0].get("message", {})
|
|
872
|
+
content = message.get("content", "")
|
|
873
|
+
if content:
|
|
874
|
+
rendered_text = render_markdown(content)
|
|
875
|
+
print(rendered_text.strip())
|
|
876
|
+
if history is not None:
|
|
877
|
+
history.append({"role": "assistant", "content": content})
|
|
878
|
+
if output_file and history is None:
|
|
879
|
+
save_single_response(content, output_file)
|
|
880
|
+
else:
|
|
881
|
+
print("[No content returned]")
|
|
882
|
+
if debug_mode: print(data)
|
|
883
|
+
else:
|
|
884
|
+
print("[Error] Invalid response format from OpenAI")
|
|
885
|
+
if debug_mode: print(data)
|
|
886
|
+
return 0
|
|
887
|
+
except Exception as e:
|
|
888
|
+
print(f"\n[Connection Error] {e}")
|
|
889
|
+
return 1
|
|
890
|
+
|
|
891
|
+
def cli_entry_point():
|
|
892
|
+
# Handle --reinstall flag first
|
|
893
|
+
if "--reinstall" in sys.argv:
|
|
894
|
+
if CONFIG_FILE.exists():
|
|
895
|
+
print(f"[{APP_NAME}] Deleting existing config for reinstall...")
|
|
896
|
+
CONFIG_FILE.unlink()
|
|
897
|
+
else:
|
|
898
|
+
print(f"[{APP_NAME}] No existing config found. Starting first-time setup...")
|
|
899
|
+
|
|
900
|
+
config = load_config()
|
|
901
|
+
|
|
902
|
+
if "--complete" in sys.argv:
|
|
903
|
+
return handle_completion(config)
|
|
904
|
+
|
|
905
|
+
if "--reinstall" in sys.argv:
|
|
906
|
+
print(f"[{APP_NAME}] Reinstall complete.")
|
|
907
|
+
return 0
|
|
908
|
+
|
|
909
|
+
# Handle --debug-config flag
|
|
910
|
+
if "--debug-config" in sys.argv:
|
|
911
|
+
if not config:
|
|
912
|
+
print("[Error] No configuration file found. Run `ai --reinstall` to create one.")
|
|
913
|
+
return 1
|
|
914
|
+
|
|
915
|
+
debug_config = copy.deepcopy(config)
|
|
916
|
+
if "profiles" in debug_config:
|
|
917
|
+
for p_name in debug_config["profiles"]:
|
|
918
|
+
p_cfg = debug_config["profiles"][p_name]
|
|
919
|
+
if "api_key" in p_cfg:
|
|
920
|
+
key = p_cfg["api_key"]
|
|
921
|
+
p_cfg["api_key"] = f"***{key[-4:]}" if key else ""
|
|
922
|
+
|
|
923
|
+
print(json.dumps(debug_config, indent=4))
|
|
924
|
+
return 0
|
|
925
|
+
|
|
926
|
+
if config is None and not sys.stdin.isatty():
|
|
927
|
+
return 1
|
|
928
|
+
|
|
929
|
+
# Handle 'profile' subcommand
|
|
930
|
+
if len(sys.argv) > 1 and sys.argv[1] == "profile" and sys.stdin.isatty():
|
|
931
|
+
subcommand = sys.argv[2] if len(sys.argv) > 2 else "list"
|
|
932
|
+
|
|
933
|
+
if subcommand in ["--help", "-h", "help"]:
|
|
934
|
+
print(f"\n{GREEN}Termai Profile Management{RESET}")
|
|
935
|
+
print(f"Manage different AI configuration profiles.\n")
|
|
936
|
+
print(f"{YELLOW}Usage:{RESET}")
|
|
937
|
+
print(f" ai profile list List all configured profiles (alias: ai profile)")
|
|
938
|
+
print(f" ai profile use [name] Set a profile as the active default (interactive if no name)")
|
|
939
|
+
print(f" ai profile add <name> Add a new custom profile")
|
|
940
|
+
print(f" ai profile remove <name> Remove a profile (alias: rm)")
|
|
941
|
+
return 0
|
|
942
|
+
|
|
943
|
+
elif subcommand == "list":
|
|
944
|
+
return list_profiles(config)
|
|
945
|
+
|
|
946
|
+
elif subcommand in ["use", "set"]:
|
|
947
|
+
profile_name = sys.argv[3] if len(sys.argv) > 3 else None
|
|
948
|
+
return switch_profile(config, profile_name)
|
|
949
|
+
|
|
950
|
+
elif subcommand == "add":
|
|
951
|
+
if len(sys.argv) > 3:
|
|
952
|
+
return add_profile(config, sys.argv[3])
|
|
953
|
+
else:
|
|
954
|
+
print(f"{RED}[Error] Please provide a name for the new profile: ai profile add <name>{RESET}")
|
|
955
|
+
return 1
|
|
956
|
+
|
|
957
|
+
elif subcommand in ["remove", "rm"]:
|
|
958
|
+
if len(sys.argv) > 3:
|
|
959
|
+
return remove_profile(config, sys.argv[3])
|
|
960
|
+
else:
|
|
961
|
+
print(f"{RED}[Error] Please provide a profile name to remove: ai profile remove <name>{RESET}")
|
|
962
|
+
return 1
|
|
963
|
+
else:
|
|
964
|
+
print(f"{RED}[Error] Unknown profile subcommand '{subcommand}'.")
|
|
965
|
+
print(f"Run 'ai profile --help' to see available commands.{RESET}")
|
|
966
|
+
return 1
|
|
967
|
+
|
|
968
|
+
# Handle 'completion' subcommand
|
|
969
|
+
if len(sys.argv) > 1 and sys.argv[1] == "completion" and sys.stdin.isatty():
|
|
970
|
+
shell = sys.argv[2] if len(sys.argv) > 2 else None
|
|
971
|
+
if not shell:
|
|
972
|
+
print(f"{RED}[Error] Please specify a shell: ai completion [bash|zsh]{RESET}")
|
|
973
|
+
return 1
|
|
974
|
+
|
|
975
|
+
if shell == "bash":
|
|
976
|
+
print(f"""# Bash completion for termai (ai)
|
|
977
|
+
_ai_completion() {{
|
|
978
|
+
local cur
|
|
979
|
+
COMPREPLY=()
|
|
980
|
+
cur="${{COMP_WORDS[COMP_CWORD]}}"
|
|
981
|
+
local IFS=$'\\n'
|
|
982
|
+
COMPREPLY=( $(ai --complete "${{COMP_WORDS[@]}}" --cword "$COMP_CWORD") )
|
|
983
|
+
return 0
|
|
984
|
+
}}
|
|
985
|
+
complete -F _ai_completion ai""")
|
|
986
|
+
return 0
|
|
987
|
+
elif shell == "zsh":
|
|
988
|
+
print(f"""# Zsh completion for termai (ai)
|
|
989
|
+
(( $+functions[compdef] )) || {{ autoload -Uz compinit && compinit; }}
|
|
990
|
+
_ai_completion() {{
|
|
991
|
+
local -a replies
|
|
992
|
+
local IFS=$'\\n'
|
|
993
|
+
replies=($(ai --complete "${{words[@]}}" --cword $((CURRENT-1))))
|
|
994
|
+
compadd -a replies
|
|
995
|
+
}}
|
|
996
|
+
compdef _ai_completion ai""")
|
|
997
|
+
return 0
|
|
998
|
+
else:
|
|
999
|
+
print(f"{RED}[Error] Unsupported shell '{shell}'. Supported: bash, zsh.{RESET}")
|
|
1000
|
+
return 1
|
|
1001
|
+
|
|
1002
|
+
if "--help" in sys.argv or "-h" in sys.argv:
|
|
1003
|
+
return print_help()
|
|
1004
|
+
|
|
1005
|
+
if "--config" in sys.argv:
|
|
1006
|
+
return open_editor()
|
|
1007
|
+
|
|
1008
|
+
# Legacy Profile management options
|
|
1009
|
+
if "--profiles" in sys.argv:
|
|
1010
|
+
return list_profiles(config)
|
|
1011
|
+
|
|
1012
|
+
if "--use" in sys.argv:
|
|
1013
|
+
idx = sys.argv.index("--use")
|
|
1014
|
+
profile_name = sys.argv[idx + 1] if idx + 1 < len(sys.argv) else None
|
|
1015
|
+
return switch_profile(config, profile_name)
|
|
1016
|
+
|
|
1017
|
+
if "--profile-add" in sys.argv:
|
|
1018
|
+
idx = sys.argv.index("--profile-add")
|
|
1019
|
+
if idx + 1 < len(sys.argv):
|
|
1020
|
+
return add_profile(config, sys.argv[idx + 1])
|
|
1021
|
+
else:
|
|
1022
|
+
print(f"{RED}[Error] Please provide a name for the new profile: ai --profile-add <name>{RESET}")
|
|
1023
|
+
return 1
|
|
1024
|
+
|
|
1025
|
+
if "--profile-remove" in sys.argv:
|
|
1026
|
+
idx = sys.argv.index("--profile-remove")
|
|
1027
|
+
if idx + 1 < len(sys.argv):
|
|
1028
|
+
return remove_profile(config, sys.argv[idx + 1])
|
|
1029
|
+
else:
|
|
1030
|
+
print(f"{RED}[Error] Please provide a profile name to remove: ai --profile-remove <name>{RESET}")
|
|
1031
|
+
return 1
|
|
1032
|
+
|
|
1033
|
+
if "--model" in sys.argv or "-m" in sys.argv:
|
|
1034
|
+
return handle_model_option(config)
|
|
1035
|
+
|
|
1036
|
+
debug_mode = "--debug" in sys.argv
|
|
1037
|
+
chat_mode = any(x in sys.argv for x in ["--chat", "-i", "chat"])
|
|
1038
|
+
|
|
1039
|
+
chat_flags = ["--chat", "-i", "chat"]
|
|
1040
|
+
profile_flags = ["--profile", "-p"]
|
|
1041
|
+
model_flags = ["--model", "-m"]
|
|
1042
|
+
save_flags = ["--save", "-o"]
|
|
1043
|
+
|
|
1044
|
+
output_file = None
|
|
1045
|
+
for flag in save_flags:
|
|
1046
|
+
if flag in sys.argv:
|
|
1047
|
+
idx = sys.argv.index(flag)
|
|
1048
|
+
if idx + 1 < len(sys.argv) and not sys.argv[idx + 1].startswith("-"):
|
|
1049
|
+
output_file = sys.argv[idx + 1].strip()
|
|
1050
|
+
break
|
|
1051
|
+
|
|
1052
|
+
# Check if a custom profile is temporarily chosen
|
|
1053
|
+
target_profile = config.get("active_profile", "")
|
|
1054
|
+
temp_profile = None
|
|
1055
|
+
for flag in profile_flags:
|
|
1056
|
+
if flag in sys.argv:
|
|
1057
|
+
idx = sys.argv.index(flag)
|
|
1058
|
+
if idx + 1 < len(sys.argv) and not sys.argv[idx + 1].startswith("-"):
|
|
1059
|
+
temp_profile = sys.argv[idx + 1].strip()
|
|
1060
|
+
break
|
|
1061
|
+
else:
|
|
1062
|
+
# Interactive Run - no direct profile argument provided, prompt user to select a profile
|
|
1063
|
+
profiles = config.get("profiles", {})
|
|
1064
|
+
profile_list = list(profiles.keys())
|
|
1065
|
+
print(f"\n{BLUE}💬 Select a profile to run this query:{RESET}")
|
|
1066
|
+
for p_idx, p_name in enumerate(profile_list, 1):
|
|
1067
|
+
p_config = profiles[p_name]
|
|
1068
|
+
prov = p_config.get("provider", "gemini")
|
|
1069
|
+
print(f" {CYAN}{p_idx}. {p_name}{RESET} [{YELLOW}{prov}{RESET}]")
|
|
1070
|
+
try:
|
|
1071
|
+
choice = input(f"\nSelect a profile number: ").strip()
|
|
1072
|
+
if not choice:
|
|
1073
|
+
print("Cancelled.")
|
|
1074
|
+
return 0
|
|
1075
|
+
choice_idx = int(choice) - 1
|
|
1076
|
+
if 0 <= choice_idx < len(profile_list):
|
|
1077
|
+
temp_profile = profile_list[choice_idx]
|
|
1078
|
+
else:
|
|
1079
|
+
print(f"{RED}[!] Invalid choice.{RESET}")
|
|
1080
|
+
return 1
|
|
1081
|
+
except (ValueError, IndexError):
|
|
1082
|
+
print(f"{RED}[!] Invalid choice.{RESET}")
|
|
1083
|
+
return 1
|
|
1084
|
+
except (KeyboardInterrupt, EOFError):
|
|
1085
|
+
print("\nCancelled.")
|
|
1086
|
+
return 0
|
|
1087
|
+
|
|
1088
|
+
if temp_profile:
|
|
1089
|
+
if temp_profile in config.get("profiles", {}):
|
|
1090
|
+
target_profile = temp_profile
|
|
1091
|
+
else:
|
|
1092
|
+
print(f"{RED}[Error] Profile '{temp_profile}' not found in configuration.{RESET}")
|
|
1093
|
+
return 1
|
|
1094
|
+
|
|
1095
|
+
# Filter out configuration, help, reinstall, chat, model, profile, and save flags/arguments from prompt text
|
|
1096
|
+
filtered_args = []
|
|
1097
|
+
skip = False
|
|
1098
|
+
for idx, arg in enumerate(sys.argv[1:]):
|
|
1099
|
+
if skip:
|
|
1100
|
+
skip = False
|
|
1101
|
+
continue
|
|
1102
|
+
if arg in model_flags + profile_flags + save_flags:
|
|
1103
|
+
if idx + 2 < len(sys.argv) and not sys.argv[idx + 2].startswith("-"):
|
|
1104
|
+
skip = True
|
|
1105
|
+
continue
|
|
1106
|
+
if arg in ["--debug", "--config", "--help", "-h", "--reinstall", "--debug-config", "--profiles", "--use", "--profile-add", "--profile-remove"] + chat_flags:
|
|
1107
|
+
continue
|
|
1108
|
+
filtered_args.append(arg)
|
|
1109
|
+
args = filtered_args
|
|
1110
|
+
|
|
1111
|
+
# Handle interactive chat session mode
|
|
1112
|
+
if chat_mode:
|
|
1113
|
+
active_config = config["profiles"][target_profile]
|
|
1114
|
+
provider = active_config.get("provider", "gemini")
|
|
1115
|
+
proxy = config.get("proxy", "")
|
|
1116
|
+
if provider == "gemini":
|
|
1117
|
+
model_name = active_config.get("model_name", "gemini-2.5-flash")
|
|
1118
|
+
else:
|
|
1119
|
+
model_name = active_config.get("model_name", "gpt-4o")
|
|
1120
|
+
|
|
1121
|
+
# Read piped content if stdin is not a TTY (before we redirect it)
|
|
1122
|
+
piped_content = ""
|
|
1123
|
+
if not sys.stdin.isatty():
|
|
1124
|
+
piped_content = sys.stdin.read().strip()
|
|
1125
|
+
# Redirect stdin back to the interactive terminal (/dev/tty) so input() works
|
|
1126
|
+
try:
|
|
1127
|
+
sys.stdin = open('/dev/tty')
|
|
1128
|
+
except OSError:
|
|
1129
|
+
pass
|
|
1130
|
+
|
|
1131
|
+
print_header_block(target_profile, provider, model_name)
|
|
1132
|
+
|
|
1133
|
+
history = []
|
|
1134
|
+
initial_prompt = ""
|
|
1135
|
+
display_prompt = ""
|
|
1136
|
+
|
|
1137
|
+
if piped_content:
|
|
1138
|
+
if args:
|
|
1139
|
+
user_question = " ".join(args)
|
|
1140
|
+
initial_prompt = f"Context:\n```\n{piped_content}\n```\n\nQuestion: {user_question}"
|
|
1141
|
+
display_prompt = f"[Piped Context] + {user_question}"
|
|
1142
|
+
else:
|
|
1143
|
+
initial_prompt = f"I have provided some context below. Please acknowledge receipt of this context, briefly summarize it, and wait for my questions about it.\n\nContext:\n```\n{piped_content}\n```"
|
|
1144
|
+
display_prompt = "[Piped Context] (Awaiting your questions)"
|
|
1145
|
+
elif args:
|
|
1146
|
+
initial_prompt = " ".join(args)
|
|
1147
|
+
display_prompt = initial_prompt
|
|
1148
|
+
|
|
1149
|
+
if initial_prompt:
|
|
1150
|
+
print_user_message(" You >>> ", display_prompt)
|
|
1151
|
+
if provider == "gemini":
|
|
1152
|
+
history.append({"role": "user", "parts": [{"text": initial_prompt}]})
|
|
1153
|
+
status = send_gemini_request(active_config, "", debug_mode, proxy=proxy, history=history)
|
|
1154
|
+
else:
|
|
1155
|
+
history.append({"role": "user", "content": initial_prompt})
|
|
1156
|
+
status = send_openai_request(active_config, "", debug_mode, proxy=proxy, history=history)
|
|
1157
|
+
|
|
1158
|
+
if status != 0:
|
|
1159
|
+
print(f"{RED}[Error] Failed to get response. Continuing session...{RESET}")
|
|
1160
|
+
|
|
1161
|
+
while True:
|
|
1162
|
+
try:
|
|
1163
|
+
prompt = f"\n You >>> "
|
|
1164
|
+
user_input = input(prompt)
|
|
1165
|
+
user_input = user_input.strip()
|
|
1166
|
+
if not user_input:
|
|
1167
|
+
continue
|
|
1168
|
+
|
|
1169
|
+
# Rewrite typed text with beautiful full-width purple background block
|
|
1170
|
+
if BG_USER:
|
|
1171
|
+
import shutil
|
|
1172
|
+
import math
|
|
1173
|
+
try:
|
|
1174
|
+
cols = shutil.get_terminal_size().columns
|
|
1175
|
+
except Exception:
|
|
1176
|
+
cols = 80
|
|
1177
|
+
total_len = len(prompt) + len(user_input)
|
|
1178
|
+
n_lines = math.ceil(total_len / cols) if cols else 1
|
|
1179
|
+
sys.stdout.write(f"\033[{n_lines}A\r\033[J")
|
|
1180
|
+
sys.stdout.flush()
|
|
1181
|
+
|
|
1182
|
+
print_user_message(" You >>> ", user_input)
|
|
1183
|
+
|
|
1184
|
+
if user_input.lower() in ["exit", "quit"]:
|
|
1185
|
+
print(f"\n{YELLOW}Goodbye!{RESET}")
|
|
1186
|
+
break
|
|
1187
|
+
|
|
1188
|
+
if user_input.lower().startswith("save ") or user_input.lower().startswith("/save "):
|
|
1189
|
+
parts = user_input.split(None, 1)
|
|
1190
|
+
if len(parts) > 1:
|
|
1191
|
+
filename = parts[1].strip()
|
|
1192
|
+
save_chat_history(history, filename, provider, target_profile, model_name)
|
|
1193
|
+
else:
|
|
1194
|
+
print(f"{RED}[Error] Please provide a filename: save <filename>{RESET}")
|
|
1195
|
+
continue
|
|
1196
|
+
|
|
1197
|
+
if provider == "gemini":
|
|
1198
|
+
history.append({"role": "user", "parts": [{"text": user_input}]})
|
|
1199
|
+
status = send_gemini_request(active_config, "", debug_mode, proxy=proxy, history=history)
|
|
1200
|
+
else:
|
|
1201
|
+
history.append({"role": "user", "content": user_input})
|
|
1202
|
+
status = send_openai_request(active_config, "", debug_mode, proxy=proxy, history=history)
|
|
1203
|
+
|
|
1204
|
+
if status != 0:
|
|
1205
|
+
print(f"{RED}[Error] Failed to get response. Continuing session...{RESET}")
|
|
1206
|
+
except (KeyboardInterrupt, EOFError):
|
|
1207
|
+
print(f"\n{YELLOW}Goodbye!{RESET}")
|
|
1208
|
+
break
|
|
1209
|
+
|
|
1210
|
+
# Auto-save history on exit if output_file is set
|
|
1211
|
+
if output_file and history:
|
|
1212
|
+
save_chat_history(history, output_file, provider, target_profile, model_name)
|
|
1213
|
+
return 0
|
|
1214
|
+
|
|
1215
|
+
user_input = ""
|
|
1216
|
+
if not sys.stdin.isatty():
|
|
1217
|
+
user_input = sys.stdin.read().strip()
|
|
1218
|
+
if args: user_input += "\n" + " ".join(args)
|
|
1219
|
+
elif args:
|
|
1220
|
+
user_input = " ".join(args)
|
|
1221
|
+
else:
|
|
1222
|
+
return print_help()
|
|
1223
|
+
|
|
1224
|
+
active_config = config["profiles"][target_profile]
|
|
1225
|
+
provider = active_config.get("provider", "gemini")
|
|
1226
|
+
proxy = config.get("proxy", "")
|
|
1227
|
+
|
|
1228
|
+
if provider == "gemini":
|
|
1229
|
+
return send_gemini_request(active_config, user_input, debug_mode, proxy=proxy, output_file=output_file)
|
|
1230
|
+
elif provider == "openai":
|
|
1231
|
+
return send_openai_request(active_config, user_input, debug_mode, proxy=proxy, output_file=output_file)
|
|
1232
|
+
else:
|
|
1233
|
+
print(f"[Error] Invalid provider '{provider}' in profile '{target_profile}'. Use 'gemini' or 'openai'.")
|
|
1234
|
+
return 1
|
|
1235
|
+
|
|
1236
|
+
def main():
|
|
1237
|
+
try:
|
|
1238
|
+
sys.exit(cli_entry_point())
|
|
1239
|
+
except KeyboardInterrupt:
|
|
1240
|
+
print("\nCancelled.")
|
|
1241
|
+
sys.exit(130)
|
|
1242
|
+
|
|
1243
|
+
if __name__ == "__main__":
|
|
1244
|
+
main()
|