grucli 3.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- grucli/__init__.py +1 -0
- grucli/api.py +725 -0
- grucli/auth.py +115 -0
- grucli/chat_manager.py +190 -0
- grucli/commands.py +318 -0
- grucli/config.py +262 -0
- grucli/handlers.py +75 -0
- grucli/interrupt.py +179 -0
- grucli/main.py +617 -0
- grucli/permissions.py +181 -0
- grucli/stats.py +100 -0
- grucli/sysprompts/main_sysprompt.txt +65 -0
- grucli/theme.py +144 -0
- grucli/tools.py +368 -0
- grucli/ui.py +496 -0
- grucli-3.3.0.dist-info/METADATA +145 -0
- grucli-3.3.0.dist-info/RECORD +21 -0
- grucli-3.3.0.dist-info/WHEEL +5 -0
- grucli-3.3.0.dist-info/entry_points.txt +2 -0
- grucli-3.3.0.dist-info/licenses/LICENSE +21 -0
- grucli-3.3.0.dist-info/top_level.txt +1 -0
grucli/auth.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import webbrowser
|
|
4
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
5
|
+
from urllib.parse import urlparse, parse_qs
|
|
6
|
+
import threading
|
|
7
|
+
import requests
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from google.oauth2.credentials import Credentials
|
|
10
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
11
|
+
from google.auth.transport.requests import Request
|
|
12
|
+
|
|
13
|
+
from . import config
|
|
14
|
+
|
|
15
|
+
OAUTH_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com'
|
|
16
|
+
OAUTH_CLIENT_SECRET = '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'
|
|
17
|
+
OAUTH_SCOPES = [
|
|
18
|
+
'https://www.googleapis.com/auth/cloud-platform',
|
|
19
|
+
'https://www.googleapis.com/auth/userinfo.email',
|
|
20
|
+
'https://www.googleapis.com/auth/userinfo.profile',
|
|
21
|
+
'openid',
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
|
|
25
|
+
|
|
26
|
+
def get_oauth_file_path():
|
|
27
|
+
return os.path.join(config.get_config_dir(), 'google_auth.json')
|
|
28
|
+
|
|
29
|
+
def get_oauth_credentials():
|
|
30
|
+
creds = None
|
|
31
|
+
oauth_file = get_oauth_file_path()
|
|
32
|
+
if os.path.exists(oauth_file):
|
|
33
|
+
try:
|
|
34
|
+
with open(oauth_file, 'r') as f:
|
|
35
|
+
creds_data = json.load(f)
|
|
36
|
+
|
|
37
|
+
expiry = None
|
|
38
|
+
if creds_data.get('expiry'):
|
|
39
|
+
expiry_str = creds_data.get('expiry')
|
|
40
|
+
if expiry_str.endswith('Z'):
|
|
41
|
+
expiry_str = expiry_str[:-1] + '+00:00'
|
|
42
|
+
expiry = datetime.fromisoformat(expiry_str)
|
|
43
|
+
|
|
44
|
+
creds = Credentials(
|
|
45
|
+
token=creds_data.get('access_token'),
|
|
46
|
+
refresh_token=creds_data.get('refresh_token'),
|
|
47
|
+
token_uri='https://oauth2.googleapis.com/token',
|
|
48
|
+
client_id=OAUTH_CLIENT_ID,
|
|
49
|
+
client_secret=OAUTH_CLIENT_SECRET,
|
|
50
|
+
scopes=creds_data.get('scope').split() if creds_data.get('scope') else OAUTH_SCOPES,
|
|
51
|
+
expiry=expiry
|
|
52
|
+
)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print(f"Error loading credentials: {e}")
|
|
55
|
+
|
|
56
|
+
if not creds or not creds.valid:
|
|
57
|
+
if creds and creds.expired and creds.refresh_token:
|
|
58
|
+
try:
|
|
59
|
+
creds.refresh(Request())
|
|
60
|
+
save_oauth_credentials(creds)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print(f"Error refreshing credentials: {e}")
|
|
63
|
+
creds = None
|
|
64
|
+
else:
|
|
65
|
+
creds = None
|
|
66
|
+
|
|
67
|
+
return creds
|
|
68
|
+
|
|
69
|
+
def save_oauth_credentials(creds):
|
|
70
|
+
oauth_file = get_oauth_file_path()
|
|
71
|
+
os.makedirs(os.path.dirname(oauth_file), exist_ok=True)
|
|
72
|
+
|
|
73
|
+
creds_data = {
|
|
74
|
+
'access_token': creds.token,
|
|
75
|
+
'refresh_token': creds.refresh_token,
|
|
76
|
+
'scope': ' '.join(creds.scopes) if creds.scopes else None,
|
|
77
|
+
'token_type': 'Bearer',
|
|
78
|
+
'expiry': creds.expiry.isoformat() if creds.expiry else None
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
with open(oauth_file, 'w') as f:
|
|
82
|
+
json.dump(creds_data, f, indent=2)
|
|
83
|
+
|
|
84
|
+
def perform_oauth_login():
|
|
85
|
+
client_config = {
|
|
86
|
+
"installed": {
|
|
87
|
+
"client_id": OAUTH_CLIENT_ID,
|
|
88
|
+
"client_secret": OAUTH_CLIENT_SECRET,
|
|
89
|
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
90
|
+
"token_uri": "https://oauth2.googleapis.com/token",
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
flow = InstalledAppFlow.from_client_config(client_config, OAUTH_SCOPES)
|
|
95
|
+
|
|
96
|
+
print("\n\033[94m--- Google OAuth Login ---\033[0m")
|
|
97
|
+
print("1. A browser window should open automatically.")
|
|
98
|
+
print("2. If it doesn't, look for a URL in the console below.")
|
|
99
|
+
print("3. Follow the instructions to authorize the application.")
|
|
100
|
+
print("--------------------------\n")
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
creds = flow.run_local_server(port=0)
|
|
104
|
+
save_oauth_credentials(creds)
|
|
105
|
+
print("\n\033[92mLogin successful! Credentials saved.\033[0m")
|
|
106
|
+
return creds
|
|
107
|
+
except Exception as e:
|
|
108
|
+
print(f"\n\033[91mOAuth login failed: {e}\033[0m")
|
|
109
|
+
raise e
|
|
110
|
+
|
|
111
|
+
def get_auth_token():
|
|
112
|
+
creds = get_oauth_credentials()
|
|
113
|
+
if creds:
|
|
114
|
+
return creds.token
|
|
115
|
+
return None
|
grucli/chat_manager.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chat management for grucli - handles saving, loading and managing chat histories.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from .theme import Colors, Icons, Borders
|
|
9
|
+
from . import interrupt
|
|
10
|
+
from . import ui
|
|
11
|
+
|
|
12
|
+
CHATS_DIR = os.path.expanduser("~/.grucli/chats/")
|
|
13
|
+
|
|
14
|
+
def ensure_chats_dir():
|
|
15
|
+
"""Ensure the chats directory exists."""
|
|
16
|
+
if not os.path.exists(CHATS_DIR):
|
|
17
|
+
os.makedirs(CHATS_DIR)
|
|
18
|
+
|
|
19
|
+
def get_chat_path(name):
|
|
20
|
+
"""Get the full path for a chat file."""
|
|
21
|
+
# Simple slugify to avoid issues with filenames
|
|
22
|
+
safe_name = "".join(c for c in name if c.isalnum() or c in (' ', '-', '_')).strip()
|
|
23
|
+
return os.path.join(CHATS_DIR, f"{safe_name}.json")
|
|
24
|
+
|
|
25
|
+
def save_chat(name, messages, model, provider):
|
|
26
|
+
"""Save the current chat history."""
|
|
27
|
+
ensure_chats_dir()
|
|
28
|
+
path = get_chat_path(name)
|
|
29
|
+
|
|
30
|
+
chat_data = {
|
|
31
|
+
"name": name,
|
|
32
|
+
"messages": messages,
|
|
33
|
+
"model": model,
|
|
34
|
+
"provider": provider,
|
|
35
|
+
"last_opened": datetime.now().isoformat(),
|
|
36
|
+
"version": 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
40
|
+
json.dump(chat_data, f, indent=2, ensure_ascii=False)
|
|
41
|
+
|
|
42
|
+
return path
|
|
43
|
+
|
|
44
|
+
def load_chat(name):
|
|
45
|
+
"""Load a chat history by name."""
|
|
46
|
+
path = get_chat_path(name)
|
|
47
|
+
if not os.path.exists(path):
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
51
|
+
chat_data = json.load(f)
|
|
52
|
+
|
|
53
|
+
# Update last opened time
|
|
54
|
+
chat_data["last_opened"] = datetime.now().isoformat()
|
|
55
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
56
|
+
json.dump(chat_data, f, indent=2, ensure_ascii=False)
|
|
57
|
+
|
|
58
|
+
return chat_data
|
|
59
|
+
|
|
60
|
+
def list_chats():
|
|
61
|
+
"""List all saved chats with metadata."""
|
|
62
|
+
ensure_chats_dir()
|
|
63
|
+
chats = []
|
|
64
|
+
for filename in os.listdir(CHATS_DIR):
|
|
65
|
+
if filename.endswith(".json"):
|
|
66
|
+
try:
|
|
67
|
+
with open(os.path.join(CHATS_DIR, filename), 'r', encoding='utf-8') as f:
|
|
68
|
+
data = json.load(f)
|
|
69
|
+
chats.append({
|
|
70
|
+
"name": data.get("name", filename[:-5]),
|
|
71
|
+
"filename": filename,
|
|
72
|
+
"last_opened": data.get("last_opened", "Unknown"),
|
|
73
|
+
"model": data.get("model", "unknown")
|
|
74
|
+
})
|
|
75
|
+
except Exception:
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
# Sort by last opened (newest first)
|
|
79
|
+
chats.sort(key=lambda x: x["last_opened"], reverse=True)
|
|
80
|
+
return chats
|
|
81
|
+
|
|
82
|
+
def delete_chat(name):
|
|
83
|
+
"""Delete a saved chat."""
|
|
84
|
+
path = get_chat_path(name)
|
|
85
|
+
if os.path.exists(path):
|
|
86
|
+
os.remove(path)
|
|
87
|
+
return True
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
def rename_chat(old_name, new_name):
|
|
91
|
+
"""Rename a saved chat."""
|
|
92
|
+
old_path = get_chat_path(old_name)
|
|
93
|
+
new_path = get_chat_path(new_name)
|
|
94
|
+
|
|
95
|
+
if os.path.exists(old_path):
|
|
96
|
+
if os.path.exists(new_path):
|
|
97
|
+
return False, "A chat with that name already exists."
|
|
98
|
+
|
|
99
|
+
with open(old_path, 'r', encoding='utf-8') as f:
|
|
100
|
+
data = json.load(f)
|
|
101
|
+
|
|
102
|
+
data["name"] = new_name
|
|
103
|
+
|
|
104
|
+
with open(new_path, 'w', encoding='utf-8') as f:
|
|
105
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
106
|
+
|
|
107
|
+
os.remove(old_path)
|
|
108
|
+
return True, None
|
|
109
|
+
return False, "Original chat not found."
|
|
110
|
+
|
|
111
|
+
def manage_chats_ui():
|
|
112
|
+
"""Interactive UI to manage chats."""
|
|
113
|
+
while True:
|
|
114
|
+
chats = list_chats()
|
|
115
|
+
if not chats:
|
|
116
|
+
print(f"\n{Colors.WARNING}No saved chats found.{Colors.RESET}\n")
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
title = "Manage Saved Chats"
|
|
120
|
+
options = []
|
|
121
|
+
for chat in chats:
|
|
122
|
+
# Format last opened time for display
|
|
123
|
+
try:
|
|
124
|
+
dt = datetime.fromisoformat(chat["last_opened"])
|
|
125
|
+
last_opened_str = dt.strftime("%Y-%m-%d %H:%M")
|
|
126
|
+
except Exception:
|
|
127
|
+
last_opened_str = chat["last_opened"]
|
|
128
|
+
|
|
129
|
+
label = f"{chat['name']} ({Colors.MUTED}{chat['model']}, {last_opened_str}{Colors.RESET})"
|
|
130
|
+
options.append(label)
|
|
131
|
+
|
|
132
|
+
options.append(f"{Colors.MUTED}Back to chat{Colors.RESET}")
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
result = ui.select_option(options, title)
|
|
136
|
+
if not result:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
_, selected_index = result
|
|
140
|
+
|
|
141
|
+
# Check if 'Back to chat' was selected (it's the last option)
|
|
142
|
+
if selected_index == len(chats):
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
selected_chat = chats[selected_index]
|
|
146
|
+
selected_chat_name = selected_chat['name']
|
|
147
|
+
|
|
148
|
+
# Action menu for the selected chat
|
|
149
|
+
action_title = f"Chat: {selected_chat_name}"
|
|
150
|
+
actions = [
|
|
151
|
+
f"{Colors.SUCCESS}{Icons.CHECK} Load Chat{Colors.RESET}",
|
|
152
|
+
f"{Colors.WARNING} Rename Chat{Colors.RESET}",
|
|
153
|
+
f"{Colors.ERROR}{Icons.CROSS} Delete Chat{Colors.RESET}",
|
|
154
|
+
f"{Colors.MUTED}Cancel{Colors.RESET}"
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
action_result = ui.select_option(actions, action_title)
|
|
158
|
+
if not action_result:
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
_, action_index = action_result
|
|
162
|
+
|
|
163
|
+
if action_index == 0: # Load
|
|
164
|
+
return selected_chat_name
|
|
165
|
+
elif action_index == 1: # Rename
|
|
166
|
+
print(f"\n{Colors.MUTED}Enter new name for '{selected_chat_name}':{Colors.RESET}")
|
|
167
|
+
new_name = interrupt.safe_input(f"{Colors.INPUT_ACTIVE}> {Colors.RESET}").strip()
|
|
168
|
+
if new_name:
|
|
169
|
+
success, err = rename_chat(selected_chat_name, new_name)
|
|
170
|
+
if success:
|
|
171
|
+
print(f"{Colors.SUCCESS}{Icons.CHECK} Chat renamed to '{new_name}'.{Colors.RESET}")
|
|
172
|
+
time.sleep(1)
|
|
173
|
+
else:
|
|
174
|
+
print(f"{Colors.ERROR}{Icons.CROSS} {err}{Colors.RESET}")
|
|
175
|
+
time.sleep(2)
|
|
176
|
+
elif action_index == 2: # Delete
|
|
177
|
+
confirm = interrupt.safe_input(f"{Colors.WARNING}Delete chat '{selected_chat_name}'? (y/n): {Colors.RESET}").strip().lower()
|
|
178
|
+
if confirm == 'y':
|
|
179
|
+
if delete_chat(selected_chat_name):
|
|
180
|
+
print(f"{Colors.SUCCESS}{Icons.CHECK} Chat deleted.{Colors.RESET}")
|
|
181
|
+
time.sleep(1)
|
|
182
|
+
else:
|
|
183
|
+
print(f"{Colors.ERROR}{Icons.CROSS} Failed to delete chat.{Colors.RESET}")
|
|
184
|
+
time.sleep(2)
|
|
185
|
+
# action_index == 3 is Cancel, which just loops back
|
|
186
|
+
|
|
187
|
+
except interrupt.BackSignal:
|
|
188
|
+
return None
|
|
189
|
+
except KeyboardInterrupt:
|
|
190
|
+
return None
|
grucli/commands.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command handling for grucli slash commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
6
|
+
from prompt_toolkit.formatted_text import HTML
|
|
7
|
+
from . import api
|
|
8
|
+
from . import config
|
|
9
|
+
from . import tools
|
|
10
|
+
from . import auth
|
|
11
|
+
from . import interrupt
|
|
12
|
+
from . import chat_manager
|
|
13
|
+
from . import ui
|
|
14
|
+
from .theme import Colors, Icons, Borders
|
|
15
|
+
import os
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
CMD_LIST = ['/exit', '/clear', '/help', '/model', '/manage-api-keys', '/gemini-login', '/gemini-auth-mode', '/show-reasoning', '/save', '/load', '/manage-chats']
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ChatCompleter(Completer):
|
|
22
|
+
"""Completer for slash commands and @file mentions."""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self._file_cache = []
|
|
26
|
+
self._last_cache_time = 0
|
|
27
|
+
|
|
28
|
+
def get_completions(self, document, complete_event):
|
|
29
|
+
text_before_cursor = document.text_before_cursor
|
|
30
|
+
if not text_before_cursor:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
last_space = text_before_cursor.rfind(' ')
|
|
34
|
+
current_word = text_before_cursor[last_space+1:]
|
|
35
|
+
|
|
36
|
+
if text_before_cursor.startswith('/') and ' ' not in text_before_cursor:
|
|
37
|
+
for cmd in CMD_LIST:
|
|
38
|
+
if cmd.startswith(text_before_cursor):
|
|
39
|
+
yield Completion(
|
|
40
|
+
cmd,
|
|
41
|
+
start_position=-len(text_before_cursor),
|
|
42
|
+
display=HTML(f'<style color="#af5fff">{cmd}</style>')
|
|
43
|
+
)
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
if text_before_cursor.startswith('/load ') and ' ' not in text_before_cursor[6:]:
|
|
47
|
+
chat_prefix = text_before_cursor[6:].lower()
|
|
48
|
+
chats = chat_manager.list_chats()
|
|
49
|
+
for chat in chats:
|
|
50
|
+
name = chat['name']
|
|
51
|
+
if name.lower().startswith(chat_prefix):
|
|
52
|
+
yield Completion(
|
|
53
|
+
name,
|
|
54
|
+
start_position=-len(chat_prefix),
|
|
55
|
+
display=HTML(f'<style color="#af5fff">{name}</style>')
|
|
56
|
+
)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
if '@' in current_word:
|
|
60
|
+
at_idx = current_word.rfind('@')
|
|
61
|
+
mention = current_word[at_idx+1:]
|
|
62
|
+
mention_lower = mention.lower()
|
|
63
|
+
|
|
64
|
+
# Simple cache to avoid walking disk on every character
|
|
65
|
+
now = time.time()
|
|
66
|
+
if now - self._last_cache_time > 5:
|
|
67
|
+
self._file_cache = tools.list_files_recursive()
|
|
68
|
+
self._last_cache_time = now
|
|
69
|
+
|
|
70
|
+
for file_path in self._file_cache:
|
|
71
|
+
if mention_lower in file_path.lower():
|
|
72
|
+
yield Completion(
|
|
73
|
+
file_path,
|
|
74
|
+
start_position=-len(mention),
|
|
75
|
+
display=HTML(f'<style color="#af5fff">@{file_path}</style>')
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
completer = ChatCompleter()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def manage_api_key(api_type='gemini'):
|
|
83
|
+
"""Manage saved API keys."""
|
|
84
|
+
api_name = api_type.capitalize()
|
|
85
|
+
|
|
86
|
+
print(f"\n{Colors.PRIMARY}{Colors.BOLD}{api_name} API Key Management{Colors.RESET}")
|
|
87
|
+
print(f"{Colors.MUTED}{Borders.HORIZONTAL * 30}{Colors.RESET}\n")
|
|
88
|
+
|
|
89
|
+
print(f" {Colors.SUCCESS}1{Colors.RESET}) Change stored API key")
|
|
90
|
+
print(f" {Colors.WARNING}2{Colors.RESET}) Change password")
|
|
91
|
+
print(f" {Colors.ERROR}3{Colors.RESET}) Remove stored API key")
|
|
92
|
+
print(f" {Colors.MUTED}4{Colors.RESET}) Cancel")
|
|
93
|
+
print()
|
|
94
|
+
|
|
95
|
+
choice = interrupt.safe_input(f"{Colors.INPUT_ACTIVE}Choose (1-4): {Colors.RESET}").strip()
|
|
96
|
+
|
|
97
|
+
if choice == '1':
|
|
98
|
+
if config.has_saved_api_key(api_type):
|
|
99
|
+
print(f"\n{Colors.MUTED}Enter new {api_name} API key:{Colors.RESET}")
|
|
100
|
+
new_api_key = interrupt.safe_input(f"{Colors.INPUT_ACTIVE}> {Colors.RESET}").strip()
|
|
101
|
+
if new_api_key:
|
|
102
|
+
config.save_encrypted_api_key(new_api_key, api_type)
|
|
103
|
+
print(f"{Colors.SUCCESS}{Icons.CHECK} API key updated.{Colors.RESET}")
|
|
104
|
+
else:
|
|
105
|
+
print(f"{Colors.WARNING}No API key provided.{Colors.RESET}")
|
|
106
|
+
else:
|
|
107
|
+
print(f"{Colors.WARNING}No saved API key found. Use the main interface to save a new API key.{Colors.RESET}")
|
|
108
|
+
|
|
109
|
+
elif choice == '2':
|
|
110
|
+
config.change_encrypted_api_key_password(api_type)
|
|
111
|
+
|
|
112
|
+
elif choice == '3':
|
|
113
|
+
config.remove_saved_api_key(api_type)
|
|
114
|
+
|
|
115
|
+
elif choice == '4':
|
|
116
|
+
print(f"{Colors.MUTED}Cancelled.{Colors.RESET}")
|
|
117
|
+
else:
|
|
118
|
+
print(f"{Colors.ERROR}Invalid option. Please choose 1-4.{Colors.RESET}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def handle_command(cmd, state):
|
|
122
|
+
"""Handle slash commands."""
|
|
123
|
+
c = cmd.lower().strip()
|
|
124
|
+
|
|
125
|
+
if c == '/exit':
|
|
126
|
+
return "exit"
|
|
127
|
+
|
|
128
|
+
elif c == '/clear':
|
|
129
|
+
# Reload custom prompt on clear
|
|
130
|
+
sys_prompt = api.get_system_prompt()
|
|
131
|
+
state['messages'] = [{"role": "system", "content": sys_prompt}]
|
|
132
|
+
print(f"{Colors.SUCCESS}{Icons.CHECK} Chat cleared.{Colors.RESET}\n")
|
|
133
|
+
return "continue"
|
|
134
|
+
|
|
135
|
+
elif c == '/help':
|
|
136
|
+
print(f"\n{Colors.PRIMARY}{Colors.BOLD}Available Commands{Colors.RESET}")
|
|
137
|
+
print(f"{Colors.MUTED}{Borders.HORIZONTAL * 40}{Colors.RESET}\n")
|
|
138
|
+
|
|
139
|
+
commands_help = [
|
|
140
|
+
('/help', 'Show this help message'),
|
|
141
|
+
('/exit', 'Exit the application'),
|
|
142
|
+
('/clear', 'Clear the current chat history'),
|
|
143
|
+
('/model', 'Switch between different models'),
|
|
144
|
+
('/manage-api-keys', 'Manage your saved API keys'),
|
|
145
|
+
('/gemini-login', 'Login with your Google account'),
|
|
146
|
+
('/gemini-auth-mode', 'Toggle Google Auth vs API Key mode'),
|
|
147
|
+
('/show-reasoning', 'Toggle showing reasoning tokens [true/false]'),
|
|
148
|
+
('/save [name]', 'Save current chat history'),
|
|
149
|
+
('/load [name]', 'Load a saved chat history'),
|
|
150
|
+
('/manage-chats', 'Manage (load/rename/delete) saved chats'),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
for cmd_name, desc in commands_help:
|
|
154
|
+
print(f" {Colors.SECONDARY}{cmd_name:20}{Colors.RESET} {Colors.MUTED}{desc}{Colors.RESET}")
|
|
155
|
+
|
|
156
|
+
print(f"\n{Colors.MUTED}Tip: Use @filename to attach file contents to your message.{Colors.RESET}\n")
|
|
157
|
+
return "continue"
|
|
158
|
+
|
|
159
|
+
elif c == '/model':
|
|
160
|
+
return "select_model"
|
|
161
|
+
|
|
162
|
+
elif c == '/manage-api-keys':
|
|
163
|
+
print(f"\n{Colors.PRIMARY}{Colors.BOLD}Select API to Manage{Colors.RESET}")
|
|
164
|
+
print(f"{Colors.MUTED}{Borders.HORIZONTAL * 25}{Colors.RESET}\n")
|
|
165
|
+
|
|
166
|
+
print(f" {Colors.SUCCESS}1{Colors.RESET}) OpenAI API")
|
|
167
|
+
print(f" {Colors.SUCCESS}2{Colors.RESET}) Anthropic API")
|
|
168
|
+
print(f" {Colors.SUCCESS}3{Colors.RESET}) Gemini API")
|
|
169
|
+
print(f" {Colors.SUCCESS}4{Colors.RESET}) Cerebras API")
|
|
170
|
+
print(f" {Colors.MUTED}5{Colors.RESET}) Cancel")
|
|
171
|
+
print()
|
|
172
|
+
|
|
173
|
+
choice = interrupt.safe_input(f"{Colors.INPUT_ACTIVE}Choose (1-5): {Colors.RESET}").strip()
|
|
174
|
+
if choice == '1':
|
|
175
|
+
manage_api_key('openai')
|
|
176
|
+
elif choice == '2':
|
|
177
|
+
manage_api_key('anthropic')
|
|
178
|
+
elif choice == '3':
|
|
179
|
+
manage_api_key('gemini')
|
|
180
|
+
elif choice == '4':
|
|
181
|
+
manage_api_key('cerebras')
|
|
182
|
+
elif choice == '5':
|
|
183
|
+
print(f"{Colors.MUTED}Cancelled.{Colors.RESET}")
|
|
184
|
+
else:
|
|
185
|
+
print(f"{Colors.ERROR}Invalid option.{Colors.RESET}")
|
|
186
|
+
|
|
187
|
+
return "continue"
|
|
188
|
+
|
|
189
|
+
elif c == '/gemini-login':
|
|
190
|
+
print(f"\n{Colors.INFO}Starting Google OAuth login flow...{Colors.RESET}")
|
|
191
|
+
try:
|
|
192
|
+
auth.perform_oauth_login()
|
|
193
|
+
print(f"{Colors.SUCCESS}{Icons.CHECK} Successfully logged in with Google!{Colors.RESET}")
|
|
194
|
+
config.set_use_google_auth(True)
|
|
195
|
+
print(f"{Colors.MUTED}Switched to Google Auth mode.{Colors.RESET}")
|
|
196
|
+
except Exception as e:
|
|
197
|
+
print(f"{Colors.ERROR}{Icons.CROSS} Login failed: {e}{Colors.RESET}")
|
|
198
|
+
return "continue"
|
|
199
|
+
|
|
200
|
+
elif c == '/gemini-auth-mode':
|
|
201
|
+
current_mode = "Google Auth" if config.is_using_google_auth() else "Gemini API Key"
|
|
202
|
+
print(f"\n{Colors.PRIMARY}{Colors.BOLD}Gemini Authentication Mode{Colors.RESET}")
|
|
203
|
+
print(f"{Colors.MUTED}{Borders.HORIZONTAL * 30}{Colors.RESET}")
|
|
204
|
+
print(f"\n{Colors.MUTED}Current mode:{Colors.RESET} {Colors.SECONDARY}{current_mode}{Colors.RESET}")
|
|
205
|
+
|
|
206
|
+
if config.is_using_google_auth():
|
|
207
|
+
project_id = os.environ.get('GOOGLE_CLOUD_PROJECT') or os.environ.get('GOOGLE_CLOUD_PROJECT_ID') or config.get_google_cloud_project()
|
|
208
|
+
print(f"{Colors.MUTED}Project ID:{Colors.RESET} {Colors.SECONDARY}{project_id or 'Not set'}{Colors.RESET}")
|
|
209
|
+
|
|
210
|
+
print(f"\n {Colors.SUCCESS}1{Colors.RESET}) Use Google Auth (Login with Google)")
|
|
211
|
+
print(f" {Colors.SUCCESS}2{Colors.RESET}) Use Gemini API Key")
|
|
212
|
+
print(f" {Colors.WARNING}3{Colors.RESET}) Set Google Cloud Project ID")
|
|
213
|
+
print(f" {Colors.MUTED}4{Colors.RESET}) Cancel")
|
|
214
|
+
print()
|
|
215
|
+
|
|
216
|
+
choice = interrupt.safe_input(f"{Colors.INPUT_ACTIVE}Choose (1-4): {Colors.RESET}").strip()
|
|
217
|
+
if choice == '1':
|
|
218
|
+
config.set_use_google_auth(True)
|
|
219
|
+
print(f"{Colors.SUCCESS}{Icons.CHECK} Switched to Google Auth mode.{Colors.RESET}")
|
|
220
|
+
if not auth.get_auth_token():
|
|
221
|
+
print(f"{Colors.MUTED}No active session found. Starting login...{Colors.RESET}")
|
|
222
|
+
auth.perform_oauth_login()
|
|
223
|
+
elif choice == '2':
|
|
224
|
+
config.set_use_google_auth(False)
|
|
225
|
+
print(f"{Colors.SUCCESS}{Icons.CHECK} Switched to Gemini API Key mode.{Colors.RESET}")
|
|
226
|
+
elif choice == '3':
|
|
227
|
+
new_project_id = interrupt.safe_input(f"{Colors.INPUT_ACTIVE}Enter Project ID: {Colors.RESET}").strip()
|
|
228
|
+
if new_project_id:
|
|
229
|
+
config.set_google_cloud_project(new_project_id)
|
|
230
|
+
print(f"{Colors.SUCCESS}{Icons.CHECK} Project ID set to: {new_project_id}{Colors.RESET}")
|
|
231
|
+
elif choice == '4':
|
|
232
|
+
print(f"{Colors.MUTED}Cancelled.{Colors.RESET}")
|
|
233
|
+
else:
|
|
234
|
+
print(f"{Colors.ERROR}Invalid option.{Colors.RESET}")
|
|
235
|
+
return "continue"
|
|
236
|
+
|
|
237
|
+
elif c.startswith('/show-reasoning'):
|
|
238
|
+
parts = c.split()
|
|
239
|
+
if len(parts) > 1:
|
|
240
|
+
val = parts[1]
|
|
241
|
+
if val in ['true', 'on', '1', 'yes']:
|
|
242
|
+
api.SHOW_REASONING = True
|
|
243
|
+
print(f"{Colors.SUCCESS}{Icons.CHECK} Showing reasoning tokens enabled (session only).{Colors.RESET}")
|
|
244
|
+
elif val in ['false', 'off', '0', 'no']:
|
|
245
|
+
api.SHOW_REASONING = False
|
|
246
|
+
print(f"{Colors.SUCCESS}{Icons.CHECK} Showing reasoning tokens disabled.{Colors.RESET}")
|
|
247
|
+
else:
|
|
248
|
+
print(f"{Colors.ERROR}Invalid value: {val}. Use true/false.{Colors.RESET}")
|
|
249
|
+
else:
|
|
250
|
+
# Toggle if no value provided
|
|
251
|
+
api.SHOW_REASONING = not api.SHOW_REASONING
|
|
252
|
+
status = "enabled" if api.SHOW_REASONING else "disabled"
|
|
253
|
+
print(f"{Colors.SUCCESS}{Icons.CHECK} Showing reasoning tokens {status}.{Colors.RESET}")
|
|
254
|
+
return "continue"
|
|
255
|
+
|
|
256
|
+
elif c.startswith('/save'):
|
|
257
|
+
parts = cmd.split(None, 1)
|
|
258
|
+
if len(parts) > 1:
|
|
259
|
+
name = parts[1].strip()
|
|
260
|
+
else:
|
|
261
|
+
print(f"\n{Colors.MUTED}Enter a name for this chat:{Colors.RESET}")
|
|
262
|
+
name = interrupt.safe_input(f"{Colors.INPUT_ACTIVE}> {Colors.RESET}").strip()
|
|
263
|
+
|
|
264
|
+
if not name:
|
|
265
|
+
print(f"{Colors.ERROR}Chat name required.{Colors.RESET}")
|
|
266
|
+
return "continue"
|
|
267
|
+
|
|
268
|
+
path = chat_manager.save_chat(name, state['messages'], state['current_model'], api.CURRENT_API)
|
|
269
|
+
print(f"{Colors.SUCCESS}{Icons.CHECK} Chat saved to {Colors.SECONDARY}{path}{Colors.RESET}\n")
|
|
270
|
+
return "continue"
|
|
271
|
+
|
|
272
|
+
elif c.startswith('/load'):
|
|
273
|
+
parts = cmd.split(None, 1)
|
|
274
|
+
if len(parts) > 1:
|
|
275
|
+
name = parts[1].strip()
|
|
276
|
+
else:
|
|
277
|
+
# If no name provided, list chats and let user pick
|
|
278
|
+
chats = chat_manager.list_chats()
|
|
279
|
+
if not chats:
|
|
280
|
+
print(f"{Colors.WARNING}No saved chats found.{Colors.RESET}")
|
|
281
|
+
return "continue"
|
|
282
|
+
|
|
283
|
+
title = "Select Chat to Load"
|
|
284
|
+
options = [f"{c['name']} ({Colors.MUTED}{c['model']}{Colors.RESET})" for c in chats]
|
|
285
|
+
result = ui.select_option(options, title)
|
|
286
|
+
if not result:
|
|
287
|
+
return "continue"
|
|
288
|
+
_, idx = result
|
|
289
|
+
name = chats[idx]['name']
|
|
290
|
+
|
|
291
|
+
chat_data = chat_manager.load_chat(name)
|
|
292
|
+
if chat_data:
|
|
293
|
+
state['messages'] = chat_data['messages']
|
|
294
|
+
|
|
295
|
+
print(f"{Colors.SUCCESS}{Icons.CHECK} Chat '{name}' loaded.{Colors.RESET}\n")
|
|
296
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
297
|
+
ui.print_messages(state['messages'])
|
|
298
|
+
return "continue"
|
|
299
|
+
else:
|
|
300
|
+
print(f"{Colors.ERROR}Chat '{name}' not found.{Colors.RESET}")
|
|
301
|
+
return "continue"
|
|
302
|
+
|
|
303
|
+
elif c == '/manage-chats':
|
|
304
|
+
chat_to_load = chat_manager.manage_chats_ui()
|
|
305
|
+
if chat_to_load:
|
|
306
|
+
chat_data = chat_manager.load_chat(chat_to_load)
|
|
307
|
+
if chat_data:
|
|
308
|
+
state['messages'] = chat_data['messages']
|
|
309
|
+
|
|
310
|
+
print(f"{Colors.SUCCESS}{Icons.CHECK} Chat '{chat_to_load}' loaded.{Colors.RESET}\n")
|
|
311
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
312
|
+
ui.print_messages(state['messages'])
|
|
313
|
+
return "continue"
|
|
314
|
+
|
|
315
|
+
else:
|
|
316
|
+
print(f"{Colors.ERROR}Unknown command: {cmd}{Colors.RESET}")
|
|
317
|
+
print(f"{Colors.MUTED}Type /help for available commands.{Colors.RESET}")
|
|
318
|
+
return "continue"
|