connectonion 0.5.8__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.
- connectonion/__init__.py +78 -0
- connectonion/address.py +320 -0
- connectonion/agent.py +450 -0
- connectonion/announce.py +84 -0
- connectonion/asgi.py +287 -0
- connectonion/auto_debug_exception.py +181 -0
- connectonion/cli/__init__.py +3 -0
- connectonion/cli/browser_agent/__init__.py +5 -0
- connectonion/cli/browser_agent/browser.py +243 -0
- connectonion/cli/browser_agent/prompt.md +107 -0
- connectonion/cli/commands/__init__.py +1 -0
- connectonion/cli/commands/auth_commands.py +527 -0
- connectonion/cli/commands/browser_commands.py +27 -0
- connectonion/cli/commands/create.py +511 -0
- connectonion/cli/commands/deploy_commands.py +220 -0
- connectonion/cli/commands/doctor_commands.py +173 -0
- connectonion/cli/commands/init.py +469 -0
- connectonion/cli/commands/project_cmd_lib.py +828 -0
- connectonion/cli/commands/reset_commands.py +149 -0
- connectonion/cli/commands/status_commands.py +168 -0
- connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +2010 -0
- connectonion/cli/docs/connectonion.md +1256 -0
- connectonion/cli/docs.md +123 -0
- connectonion/cli/main.py +148 -0
- connectonion/cli/templates/meta-agent/README.md +287 -0
- connectonion/cli/templates/meta-agent/agent.py +196 -0
- connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +9 -0
- connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +15 -0
- connectonion/cli/templates/meta-agent/prompts/metagent.md +71 -0
- connectonion/cli/templates/meta-agent/prompts/think_prompt.md +18 -0
- connectonion/cli/templates/minimal/README.md +56 -0
- connectonion/cli/templates/minimal/agent.py +40 -0
- connectonion/cli/templates/playwright/README.md +118 -0
- connectonion/cli/templates/playwright/agent.py +336 -0
- connectonion/cli/templates/playwright/prompt.md +102 -0
- connectonion/cli/templates/playwright/requirements.txt +3 -0
- connectonion/cli/templates/web-research/agent.py +122 -0
- connectonion/connect.py +128 -0
- connectonion/console.py +539 -0
- connectonion/debug_agent/__init__.py +13 -0
- connectonion/debug_agent/agent.py +45 -0
- connectonion/debug_agent/prompts/debug_assistant.md +72 -0
- connectonion/debug_agent/runtime_inspector.py +406 -0
- connectonion/debug_explainer/__init__.py +10 -0
- connectonion/debug_explainer/explain_agent.py +114 -0
- connectonion/debug_explainer/explain_context.py +263 -0
- connectonion/debug_explainer/explainer_prompt.md +29 -0
- connectonion/debug_explainer/root_cause_analysis_prompt.md +43 -0
- connectonion/debugger_ui.py +1039 -0
- connectonion/decorators.py +208 -0
- connectonion/events.py +248 -0
- connectonion/execution_analyzer/__init__.py +9 -0
- connectonion/execution_analyzer/execution_analysis.py +93 -0
- connectonion/execution_analyzer/execution_analysis_prompt.md +47 -0
- connectonion/host.py +579 -0
- connectonion/interactive_debugger.py +342 -0
- connectonion/llm.py +801 -0
- connectonion/llm_do.py +307 -0
- connectonion/logger.py +300 -0
- connectonion/prompt_files/__init__.py +1 -0
- connectonion/prompt_files/analyze_contact.md +62 -0
- connectonion/prompt_files/eval_expected.md +12 -0
- connectonion/prompt_files/react_evaluate.md +11 -0
- connectonion/prompt_files/react_plan.md +16 -0
- connectonion/prompt_files/reflect.md +22 -0
- connectonion/prompts.py +144 -0
- connectonion/relay.py +200 -0
- connectonion/static/docs.html +688 -0
- connectonion/tool_executor.py +279 -0
- connectonion/tool_factory.py +186 -0
- connectonion/tool_registry.py +105 -0
- connectonion/trust.py +166 -0
- connectonion/trust_agents.py +71 -0
- connectonion/trust_functions.py +88 -0
- connectonion/tui/__init__.py +57 -0
- connectonion/tui/divider.py +39 -0
- connectonion/tui/dropdown.py +251 -0
- connectonion/tui/footer.py +31 -0
- connectonion/tui/fuzzy.py +56 -0
- connectonion/tui/input.py +278 -0
- connectonion/tui/keys.py +35 -0
- connectonion/tui/pick.py +130 -0
- connectonion/tui/providers.py +155 -0
- connectonion/tui/status_bar.py +163 -0
- connectonion/usage.py +161 -0
- connectonion/useful_events_handlers/__init__.py +16 -0
- connectonion/useful_events_handlers/reflect.py +116 -0
- connectonion/useful_plugins/__init__.py +20 -0
- connectonion/useful_plugins/calendar_plugin.py +163 -0
- connectonion/useful_plugins/eval.py +139 -0
- connectonion/useful_plugins/gmail_plugin.py +162 -0
- connectonion/useful_plugins/image_result_formatter.py +127 -0
- connectonion/useful_plugins/re_act.py +78 -0
- connectonion/useful_plugins/shell_approval.py +159 -0
- connectonion/useful_tools/__init__.py +44 -0
- connectonion/useful_tools/diff_writer.py +192 -0
- connectonion/useful_tools/get_emails.py +183 -0
- connectonion/useful_tools/gmail.py +1596 -0
- connectonion/useful_tools/google_calendar.py +613 -0
- connectonion/useful_tools/memory.py +380 -0
- connectonion/useful_tools/microsoft_calendar.py +604 -0
- connectonion/useful_tools/outlook.py +488 -0
- connectonion/useful_tools/send_email.py +205 -0
- connectonion/useful_tools/shell.py +97 -0
- connectonion/useful_tools/slash_command.py +201 -0
- connectonion/useful_tools/terminal.py +285 -0
- connectonion/useful_tools/todo_list.py +241 -0
- connectonion/useful_tools/web_fetch.py +216 -0
- connectonion/xray.py +467 -0
- connectonion-0.5.8.dist-info/METADATA +741 -0
- connectonion-0.5.8.dist-info/RECORD +113 -0
- connectonion-0.5.8.dist-info/WHEEL +4 -0
- connectonion-0.5.8.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Shared utility functions for CLI project commands including validation, API key detection, template generation, and Rich UI helpers
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [os, re, sys, time, shutil, toml, rich.console, rich.prompt, rich.progress, rich.table, rich.panel, datetime, pathlib, __version__, address] | imported by [cli/commands/init.py, cli/commands/create.py] | calls LLM APIs for custom template generation | tested indirectly via test_cli_init.py and test_cli_create.py
|
|
5
|
+
Data flow: provides utility functions called by init.py and create.py → validate_project_name() checks regex patterns → check_environment_for_api_keys() scans env vars for OpenAI/Anthropic/Google keys → detect_api_provider() inspects key format to identify provider → api_key_setup_menu() displays interactive menu for key selection → generate_custom_template_with_name() calls LLM API with custom prompt to generate agent.py code → show_progress() displays Rich spinner → LoadingAnimation context manager for long operations → get_special_directory_warning() warns about home/root dirs
|
|
6
|
+
State/Effects: no persistent state | reads from environment variables | writes to stdout via rich.Console | calls LLM APIs (OpenAI/Anthropic/Google) when generating custom templates | creates Rich UI elements (tables, panels, progress bars, prompts) | does NOT write files (caller handles that)
|
|
7
|
+
Integration: exposes 16+ utility functions and 1 class (LoadingAnimation) | used by init.py and create.py for shared logic | validate_project_name() enforces naming conventions (starts with letter, no spaces, max 50 chars) | check_environment_for_api_keys() scans OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, GOOGLE_API_KEY | detect_api_provider() identifies provider by key prefix (sk- for OpenAI, sk-ant- for Anthropic, AIzaSy for Google, gsk- for Groq) | generate_custom_template_with_name() uses LLM to create agent.py from natural language description
|
|
8
|
+
Performance: environment scanning is O(n) env vars | regex validation is fast (<1ms) | LLM API calls for custom templates (5-15s) | Rich UI rendering is lightweight | LoadingAnimation runs in main thread (non-blocking spinner)
|
|
9
|
+
Errors: validate_project_name() returns (False, error_msg) for invalid names | detect_api_provider() returns ("unknown", "unknown") for unrecognized keys | generate_custom_template_with_name() may fail if LLM API unreachable | api_key_setup_menu() catches KeyboardInterrupt and returns ("", "", None) | no try-except blocks (follows fail-fast principle)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
import shutil
|
|
17
|
+
import toml
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.prompt import Prompt, Confirm, IntPrompt
|
|
20
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
from rich.panel import Panel
|
|
23
|
+
from rich import box
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Optional, Tuple, List
|
|
27
|
+
|
|
28
|
+
from ... import __version__
|
|
29
|
+
from ... import address
|
|
30
|
+
|
|
31
|
+
console = Console()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def validate_project_name(name: str) -> Tuple[bool, str]:
|
|
37
|
+
"""Validate project name for common issues.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Tuple of (is_valid, error_message)
|
|
41
|
+
"""
|
|
42
|
+
if not name:
|
|
43
|
+
return False, "Project name cannot be empty"
|
|
44
|
+
|
|
45
|
+
if ' ' in name:
|
|
46
|
+
return False, "Project name cannot contain spaces. Try using hyphens instead (e.g., 'my-agent')"
|
|
47
|
+
|
|
48
|
+
if not re.match(r'^[a-zA-Z][a-zA-Z0-9-_]*$', name):
|
|
49
|
+
return False, "Project name must start with a letter and contain only letters, numbers, hyphens, and underscores"
|
|
50
|
+
|
|
51
|
+
if len(name) > 50:
|
|
52
|
+
return False, "Project name is too long (max 50 characters)"
|
|
53
|
+
|
|
54
|
+
return True, ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def show_progress(message: str, duration: float = 0.5):
|
|
58
|
+
"""Show a brief progress spinner using Rich."""
|
|
59
|
+
with Progress(
|
|
60
|
+
SpinnerColumn(style="cyan"),
|
|
61
|
+
TextColumn("[cyan]{task.description}"),
|
|
62
|
+
transient=True,
|
|
63
|
+
console=console,
|
|
64
|
+
) as progress:
|
|
65
|
+
task_id = progress.add_task(message, total=None)
|
|
66
|
+
end_time = time.time() + duration
|
|
67
|
+
while time.time() < end_time:
|
|
68
|
+
time.sleep(0.05)
|
|
69
|
+
progress.remove_task(task_id)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class LoadingAnimation:
|
|
73
|
+
"""Context manager for showing loading animation during long operations."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, message: str):
|
|
76
|
+
self.message = message
|
|
77
|
+
self.progress = None
|
|
78
|
+
self.task_id = None
|
|
79
|
+
|
|
80
|
+
def __enter__(self):
|
|
81
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
82
|
+
self.progress = Progress(
|
|
83
|
+
SpinnerColumn(style="cyan"),
|
|
84
|
+
TextColumn("[cyan]{task.description}"),
|
|
85
|
+
transient=False,
|
|
86
|
+
console=console,
|
|
87
|
+
)
|
|
88
|
+
self.progress.start()
|
|
89
|
+
self.task_id = self.progress.add_task(self.message, total=None)
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def update(self, new_message: str):
|
|
93
|
+
"""Update the loading message."""
|
|
94
|
+
if self.progress and self.task_id is not None:
|
|
95
|
+
self.progress.update(self.task_id, description=new_message)
|
|
96
|
+
|
|
97
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
98
|
+
if self.progress:
|
|
99
|
+
self.progress.stop()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_template_info() -> list:
|
|
103
|
+
"""Get template information for display."""
|
|
104
|
+
return [
|
|
105
|
+
('minimal', '📦 Minimal', 'Basic agent structure'),
|
|
106
|
+
('playwright', '🎭 Playwright', 'Browser automation agent'),
|
|
107
|
+
('custom', '✨ Custom', 'AI-generated agent'),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_template_suggested_name(template: str) -> str:
|
|
112
|
+
"""Get suggested project name for a template."""
|
|
113
|
+
suggestions = {
|
|
114
|
+
'minimal': 'my-agent',
|
|
115
|
+
'playwright': 'browser-agent',
|
|
116
|
+
'custom': None # Will be generated by AI
|
|
117
|
+
}
|
|
118
|
+
return suggestions.get(template, 'my-agent')
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def api_key_setup_menu(temp_project_dir: Optional[Path] = None) -> Tuple[str, str, Path]:
|
|
122
|
+
"""Show API key setup options to the user.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
temp_project_dir: Optional temporary project directory to use for auth
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Tuple of (api_key, provider, temp_dir) where temp_dir is the temporary project created for auth
|
|
129
|
+
"""
|
|
130
|
+
from ... import address # Import address module for key generation
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
import questionary
|
|
134
|
+
from questionary import Style
|
|
135
|
+
|
|
136
|
+
custom_style = Style([
|
|
137
|
+
('question', 'fg:#00ffff bold'),
|
|
138
|
+
('pointer', 'fg:#00ff00 bold'),
|
|
139
|
+
('highlighted', 'fg:#00ff00 bold'),
|
|
140
|
+
('selected', 'fg:#00ffff'),
|
|
141
|
+
('separator', 'fg:#808080'),
|
|
142
|
+
('instruction', 'fg:#808080'),
|
|
143
|
+
])
|
|
144
|
+
|
|
145
|
+
choices = [
|
|
146
|
+
questionary.Choice(
|
|
147
|
+
title="🔑 BYO API key (OpenAI, Anthropic, Gemini)",
|
|
148
|
+
value="own_key"
|
|
149
|
+
),
|
|
150
|
+
questionary.Choice(
|
|
151
|
+
title="⭐ Star for $1 OpenOnion credit (100k free tokens)",
|
|
152
|
+
value="star"
|
|
153
|
+
),
|
|
154
|
+
questionary.Choice(
|
|
155
|
+
title="⏭️ Skip (get $0.1 OpenOnion credit 10k free tokens)",
|
|
156
|
+
value="skip"
|
|
157
|
+
),
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
result = questionary.select(
|
|
161
|
+
"How would you like to set up API access?",
|
|
162
|
+
choices=choices,
|
|
163
|
+
style=custom_style,
|
|
164
|
+
instruction="(Use ↑/↓ arrows, press Enter to confirm)",
|
|
165
|
+
).ask()
|
|
166
|
+
|
|
167
|
+
if result == "own_key":
|
|
168
|
+
# Ask for their API key
|
|
169
|
+
console.print("\n[cyan]Paste your API key (we'll detect the provider)[/cyan]")
|
|
170
|
+
api_key = questionary.password("API key:").ask()
|
|
171
|
+
if api_key:
|
|
172
|
+
provider, key_type = detect_api_provider(api_key)
|
|
173
|
+
console.print(f"[green]✓ {provider.title()} API key configured[/green]")
|
|
174
|
+
return api_key, provider, None # No temp dir for own keys
|
|
175
|
+
return "", "", None
|
|
176
|
+
|
|
177
|
+
elif result == "star":
|
|
178
|
+
# Star for free credits - create temp project and authenticate immediately
|
|
179
|
+
import webbrowser
|
|
180
|
+
import shutil
|
|
181
|
+
from pathlib import Path
|
|
182
|
+
|
|
183
|
+
console.print("\n[cyan]⭐ Get 100k Free Tokens[/cyan]")
|
|
184
|
+
console.print("\nOpening GitHub in your browser...")
|
|
185
|
+
|
|
186
|
+
# Try to open the GitHub repo for starring
|
|
187
|
+
github_url = "https://github.com/openonion/connectonion"
|
|
188
|
+
try:
|
|
189
|
+
webbrowser.open(github_url)
|
|
190
|
+
except:
|
|
191
|
+
pass # Browser opening might fail in some environments
|
|
192
|
+
|
|
193
|
+
# Keep asking until they confirm they've starred
|
|
194
|
+
while True:
|
|
195
|
+
already_starred = questionary.confirm(
|
|
196
|
+
"\nHave you starred our repository?",
|
|
197
|
+
default=False
|
|
198
|
+
).ask()
|
|
199
|
+
|
|
200
|
+
if already_starred:
|
|
201
|
+
console.print("[green]✓ Thank you for your support![/green]")
|
|
202
|
+
|
|
203
|
+
# Create temporary project directory
|
|
204
|
+
temp_name = "connectonion-temp-project"
|
|
205
|
+
temp_dir = Path(temp_name)
|
|
206
|
+
counter = 1
|
|
207
|
+
while temp_dir.exists():
|
|
208
|
+
temp_dir = Path(f"{temp_name}-{counter}")
|
|
209
|
+
counter += 1
|
|
210
|
+
|
|
211
|
+
console.print(f"\n[yellow]Setting up temporary project for authentication...[/yellow]")
|
|
212
|
+
temp_dir.mkdir(parents=True)
|
|
213
|
+
|
|
214
|
+
# Create .co directory and generate keys
|
|
215
|
+
co_dir = temp_dir / ".co"
|
|
216
|
+
co_dir.mkdir()
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
# Generate keys for this project
|
|
220
|
+
addr_data = address.generate()
|
|
221
|
+
address.save(addr_data, co_dir)
|
|
222
|
+
|
|
223
|
+
# Run direct registration with the project keys (no browser)
|
|
224
|
+
console.print("\n[yellow]Activating your free credits...[/yellow]\n")
|
|
225
|
+
|
|
226
|
+
from .auth_commands import authenticate
|
|
227
|
+
if authenticate(co_dir):
|
|
228
|
+
console.print("\n[green]✓ We verified your star. Thanks for supporting us![/green]")
|
|
229
|
+
console.print("[green]You now have 100k free tokens![/green]")
|
|
230
|
+
console.print("\n[cyan]You can use ConnectOnion models with the 'co/' prefix:[/cyan]")
|
|
231
|
+
console.print(" • co/gemini-2.5-pro")
|
|
232
|
+
console.print(" • co/gpt-4o")
|
|
233
|
+
console.print(" • co/gemini-2.5-pro")
|
|
234
|
+
console.print(" • co/gpt-5")
|
|
235
|
+
console.print(" • co/claude-3-haiku")
|
|
236
|
+
console.print(" • co/claude-3-sonnet")
|
|
237
|
+
|
|
238
|
+
return "star", "connectonion", temp_dir # Return the temp directory
|
|
239
|
+
else:
|
|
240
|
+
# Auth failed, clean up
|
|
241
|
+
shutil.rmtree(temp_dir)
|
|
242
|
+
console.print("[yellow]Authentication failed. Please try again.[/yellow]")
|
|
243
|
+
return "", "", None
|
|
244
|
+
except Exception as e:
|
|
245
|
+
# Clean up on error
|
|
246
|
+
if temp_dir.exists():
|
|
247
|
+
shutil.rmtree(temp_dir)
|
|
248
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
249
|
+
return "", "", None
|
|
250
|
+
|
|
251
|
+
break # Exit the loop
|
|
252
|
+
else:
|
|
253
|
+
console.print("\n[yellow]Please star the repository to get your free tokens![/yellow]")
|
|
254
|
+
console.print(f"\nIf the browser didn't open, visit: [cyan]{github_url}[/cyan]")
|
|
255
|
+
console.print("You can copy and paste this URL into your browser.")
|
|
256
|
+
console.print("\n[dim]We'll wait for you to star the repository...[/dim]")
|
|
257
|
+
# Loop will continue to ask again
|
|
258
|
+
|
|
259
|
+
return "star", "connectonion", None # Should not reach here
|
|
260
|
+
|
|
261
|
+
elif result == "skip":
|
|
262
|
+
# User chose to skip API setup
|
|
263
|
+
console.print("\n[yellow]⏭️ Skipping API setup[/yellow]")
|
|
264
|
+
console.print("[dim]You can add your API key later in the .env file[/dim]")
|
|
265
|
+
return "skip", "", None # Return "skip" as api_key to indicate skip choice
|
|
266
|
+
|
|
267
|
+
else:
|
|
268
|
+
raise KeyboardInterrupt()
|
|
269
|
+
|
|
270
|
+
except ImportError:
|
|
271
|
+
# Fallback to simple menu
|
|
272
|
+
console.print("\n[cyan]🔑 API Key Setup[/cyan]")
|
|
273
|
+
console.print("1. BYO API key (OpenAI, Anthropic, Gemini)")
|
|
274
|
+
console.print("2. Star for $1 OpenOnion credit (100k free tokens)")
|
|
275
|
+
console.print("3. Skip (get $0.1 OpenOnion credit 10k free tokens)")
|
|
276
|
+
|
|
277
|
+
choice = IntPrompt.ask("Select option", choices=["1", "2", "3"], default="3")
|
|
278
|
+
choice = int(choice)
|
|
279
|
+
|
|
280
|
+
if choice == 1:
|
|
281
|
+
api_key = Prompt.ask("API key", password=True, default="")
|
|
282
|
+
if api_key:
|
|
283
|
+
provider, key_type = detect_api_provider(api_key)
|
|
284
|
+
console.print(f"[green]✓ {provider.title()} API key configured[/green]")
|
|
285
|
+
return api_key, provider, False # No auth needed for own keys
|
|
286
|
+
return "", "", False
|
|
287
|
+
elif choice == 2:
|
|
288
|
+
import webbrowser
|
|
289
|
+
|
|
290
|
+
console.print("\n[cyan]⭐ Get 100k Free Tokens[/cyan]")
|
|
291
|
+
console.print("\nOpening GitHub in your browser...")
|
|
292
|
+
|
|
293
|
+
# Try to open the GitHub repo for starring
|
|
294
|
+
github_url = "https://github.com/openonion/connectonion"
|
|
295
|
+
try:
|
|
296
|
+
webbrowser.open(github_url)
|
|
297
|
+
except:
|
|
298
|
+
pass # Browser opening might fail in some environments
|
|
299
|
+
|
|
300
|
+
# Keep asking until they confirm they've starred
|
|
301
|
+
while True:
|
|
302
|
+
already_starred = Confirm.ask("\nHave you starred our repository?", default=False)
|
|
303
|
+
|
|
304
|
+
if already_starred:
|
|
305
|
+
console.print("[green]✓ Thank you for your support![/green]")
|
|
306
|
+
console.print("\n[yellow]Authenticating to activate your free credits...[/yellow]\n")
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
from .auth_commands import authenticate
|
|
310
|
+
authenticate(Path(".co"))
|
|
311
|
+
console.print("\n[green]✓ We verified your star. Thanks for supporting us![/green]")
|
|
312
|
+
console.print("[green]You now have 100k free tokens![/green]")
|
|
313
|
+
console.print("\n[cyan]You can use ConnectOnion models with the 'co/' prefix:[/cyan]")
|
|
314
|
+
console.print(" • co/gemini-2.5-pro")
|
|
315
|
+
console.print(" • co/gpt-4o")
|
|
316
|
+
console.print(" • co/gemini-2.5-pro")
|
|
317
|
+
console.print(" • co/gpt-5")
|
|
318
|
+
console.print(" • co/claude-3-haiku")
|
|
319
|
+
console.print(" • co/claude-3-sonnet")
|
|
320
|
+
break # Success, exit the loop
|
|
321
|
+
except Exception as e:
|
|
322
|
+
console.print(f"\n[red]Authentication failed: {e}[/red]")
|
|
323
|
+
console.print("[yellow]Please try running: [bold]co auth[/bold][/yellow]")
|
|
324
|
+
break # Exit on auth failure
|
|
325
|
+
else:
|
|
326
|
+
console.print("\n[yellow]Please star the repository to get your free tokens![/yellow]")
|
|
327
|
+
console.print(f"\nIf the browser didn't open, visit: [cyan]{github_url}[/cyan]")
|
|
328
|
+
console.print("You can copy and paste this URL into your browser.")
|
|
329
|
+
console.print("\n[dim]We'll wait for you to star the repository...[/dim]")
|
|
330
|
+
# Loop will continue to ask again
|
|
331
|
+
|
|
332
|
+
return "star", "connectonion", None
|
|
333
|
+
|
|
334
|
+
elif choice == 3:
|
|
335
|
+
# Skip - user will get free tokens
|
|
336
|
+
console.print("\n[yellow]⏭️ Skipping API setup[/yellow]")
|
|
337
|
+
console.print("[dim]You'll get $0.1 OpenOnion credit (10k tokens) to get started[/dim]")
|
|
338
|
+
console.print("[dim]Add your own API key to .env later for unlimited usage[/dim]")
|
|
339
|
+
return "skip", "", None
|
|
340
|
+
|
|
341
|
+
else:
|
|
342
|
+
return "", "", None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def interactive_menu(options: List[Tuple[str, str, str]], prompt: str = "Choose an option:") -> str:
|
|
346
|
+
"""Interactive menu with arrow key navigation using questionary.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
options: List of (key, emoji+name, description) tuples
|
|
350
|
+
prompt: Menu prompt text
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Selected option key
|
|
354
|
+
"""
|
|
355
|
+
try:
|
|
356
|
+
import questionary
|
|
357
|
+
from questionary import Style
|
|
358
|
+
|
|
359
|
+
# Custom style using questionary's styling
|
|
360
|
+
custom_style = Style([
|
|
361
|
+
('question', 'fg:#00ffff bold'),
|
|
362
|
+
('pointer', 'fg:#00ff00 bold'), # The > pointer
|
|
363
|
+
('highlighted', 'fg:#00ff00 bold'), # Currently selected item
|
|
364
|
+
('selected', 'fg:#00ffff'), # Selected item after pressing enter
|
|
365
|
+
('separator', 'fg:#808080'),
|
|
366
|
+
('instruction', 'fg:#808080'), # (Use arrow keys)
|
|
367
|
+
])
|
|
368
|
+
|
|
369
|
+
# Create choices with formatted strings
|
|
370
|
+
choices = []
|
|
371
|
+
for key, name, desc in options:
|
|
372
|
+
# Format: "📦 Minimal - Basic agent"
|
|
373
|
+
choice_text = f"{name} - {desc}"
|
|
374
|
+
choices.append(questionary.Choice(title=choice_text, value=key))
|
|
375
|
+
|
|
376
|
+
# Show the selection menu
|
|
377
|
+
result = questionary.select(
|
|
378
|
+
prompt,
|
|
379
|
+
choices=choices,
|
|
380
|
+
style=custom_style,
|
|
381
|
+
instruction="(Use ↑/↓ arrows, press Enter to confirm)",
|
|
382
|
+
).ask()
|
|
383
|
+
|
|
384
|
+
if result:
|
|
385
|
+
# Find the selected option name for confirmation
|
|
386
|
+
for key, name, _ in options:
|
|
387
|
+
if key == result:
|
|
388
|
+
console.print(f"[green]✓ Selected:[/green] {name}")
|
|
389
|
+
break
|
|
390
|
+
return result
|
|
391
|
+
else:
|
|
392
|
+
# User cancelled (pressed Ctrl+C or Escape)
|
|
393
|
+
raise KeyboardInterrupt()
|
|
394
|
+
|
|
395
|
+
except ImportError:
|
|
396
|
+
# Fallback to the original Rich + Click implementation
|
|
397
|
+
console.print()
|
|
398
|
+
console.print(Panel.fit(prompt, style="cyan", border_style="cyan", title="Templates"))
|
|
399
|
+
|
|
400
|
+
table = Table(box=box.SIMPLE_HEAVY)
|
|
401
|
+
table.add_column("No.", justify="right", style="bold")
|
|
402
|
+
table.add_column("Template", style="white")
|
|
403
|
+
table.add_column("Description", style="dim")
|
|
404
|
+
|
|
405
|
+
for i, (_, name, desc) in enumerate(options, 1):
|
|
406
|
+
table.add_row(str(i), name, desc)
|
|
407
|
+
|
|
408
|
+
console.print(table)
|
|
409
|
+
|
|
410
|
+
choices = [str(i) for i in range(1, len(options) + 1)]
|
|
411
|
+
selected = IntPrompt.ask(
|
|
412
|
+
"Select [number]",
|
|
413
|
+
choices=choices,
|
|
414
|
+
default="1"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
idx = int(selected) - 1
|
|
418
|
+
selected_option = options[idx]
|
|
419
|
+
console.print(f"[green]✓ Selected:[/green] {selected_option[1]}")
|
|
420
|
+
return selected_option[0]
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def get_template_preview(template: str) -> str:
|
|
424
|
+
"""Get a preview of what the template includes."""
|
|
425
|
+
previews = {
|
|
426
|
+
'minimal': """ 📦 Minimal - Simple starting point
|
|
427
|
+
├── agent.py (50 lines) - Basic agent with example tool
|
|
428
|
+
├── .env - API key configuration
|
|
429
|
+
├── README.md - Quick start guide
|
|
430
|
+
└── .co/ - Agent identity & metadata""",
|
|
431
|
+
|
|
432
|
+
'web-research': """ 🔍 Web Research - Data analysis & web scraping
|
|
433
|
+
├── agent.py (100+ lines) - Agent with web tools
|
|
434
|
+
├── tools/ - Web scraping & data extraction
|
|
435
|
+
├── .env - API key configuration
|
|
436
|
+
├── README.md - Usage examples
|
|
437
|
+
└── .co/ - Agent identity & metadata""",
|
|
438
|
+
|
|
439
|
+
'email-agent': """ 📧 Email Agent - Professional email assistant
|
|
440
|
+
├── agent.py (400+ lines) - Full email management
|
|
441
|
+
├── README.md - Comprehensive guide
|
|
442
|
+
├── .env.example - Configuration options
|
|
443
|
+
└── .co/ - Agent identity & metadata
|
|
444
|
+
Features: inbox management, auto-respond, search, statistics""",
|
|
445
|
+
|
|
446
|
+
'custom': """ ✨ Custom - AI generates based on your needs
|
|
447
|
+
├── agent.py - Tailored to your description
|
|
448
|
+
├── tools/ - Custom tools for your use case
|
|
449
|
+
├── .env - API key configuration
|
|
450
|
+
├── README.md - Custom documentation
|
|
451
|
+
└── .co/ - Agent identity & metadata""",
|
|
452
|
+
|
|
453
|
+
'meta-agent': """ 🤖 Meta-Agent - ConnectOnion development assistant
|
|
454
|
+
├── agent.py - Advanced agent with llm_do
|
|
455
|
+
├── prompts/ - System prompts (4 files)
|
|
456
|
+
├── .env - API key configuration
|
|
457
|
+
├── README.md - Comprehensive guide
|
|
458
|
+
└── .co/ - Agent identity & metadata""",
|
|
459
|
+
|
|
460
|
+
'playwright': """ 🎭 Playwright - Browser automation
|
|
461
|
+
├── agent.py - Browser control agent
|
|
462
|
+
├── prompt.md - System prompt
|
|
463
|
+
├── .env - API key configuration
|
|
464
|
+
├── README.md - Setup instructions
|
|
465
|
+
└── .co/ - Agent identity & metadata"""
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return previews.get(template, f" 📄 {template.title()} template")
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def check_environment_for_api_keys() -> Optional[Tuple[str, str]]:
|
|
472
|
+
"""Check environment variables for API keys.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
Tuple of (provider, api_key) if found, None otherwise
|
|
476
|
+
"""
|
|
477
|
+
import os
|
|
478
|
+
|
|
479
|
+
# Check for various API key environment variables
|
|
480
|
+
checks = [
|
|
481
|
+
('OPENAI_API_KEY', 'openai'),
|
|
482
|
+
('ANTHROPIC_API_KEY', 'anthropic'),
|
|
483
|
+
('GEMINI_API_KEY', 'google'),
|
|
484
|
+
('GOOGLE_API_KEY', 'google'),
|
|
485
|
+
('GROQ_API_KEY', 'groq'),
|
|
486
|
+
]
|
|
487
|
+
|
|
488
|
+
for env_var, provider in checks:
|
|
489
|
+
api_key = os.environ.get(env_var)
|
|
490
|
+
if api_key and api_key != 'your-api-key-here' and not api_key.startswith('sk-your'):
|
|
491
|
+
return provider, api_key
|
|
492
|
+
|
|
493
|
+
return None
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def detect_api_provider(api_key: str) -> Tuple[str, str]:
|
|
497
|
+
"""Detect API provider from key format.
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
Tuple of (provider, key_type)
|
|
501
|
+
"""
|
|
502
|
+
# Check Anthropic first (more specific prefix)
|
|
503
|
+
if api_key.startswith('sk-ant-'):
|
|
504
|
+
return 'anthropic', 'claude'
|
|
505
|
+
|
|
506
|
+
# OpenAI formats
|
|
507
|
+
if api_key.startswith('sk-proj-'):
|
|
508
|
+
return 'openai', 'project'
|
|
509
|
+
elif api_key.startswith('sk-'):
|
|
510
|
+
return 'openai', 'user'
|
|
511
|
+
|
|
512
|
+
# Google (Gemini)
|
|
513
|
+
if api_key.startswith('AIza'):
|
|
514
|
+
return 'google', 'gemini'
|
|
515
|
+
|
|
516
|
+
# Groq
|
|
517
|
+
if api_key.startswith('gsk_'):
|
|
518
|
+
return 'groq', 'groq'
|
|
519
|
+
|
|
520
|
+
# Default to OpenAI if unsure
|
|
521
|
+
return 'openai', 'unknown'
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def configure_env_for_provider(provider: str, api_key: str) -> str:
|
|
525
|
+
"""Generate .env content based on provider.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
provider: API provider name
|
|
529
|
+
api_key: The API key
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
.env file content
|
|
533
|
+
"""
|
|
534
|
+
configs = {
|
|
535
|
+
'openai': {
|
|
536
|
+
'var': 'OPENAI_API_KEY',
|
|
537
|
+
'model': 'gpt-4o-mini'
|
|
538
|
+
},
|
|
539
|
+
'anthropic': {
|
|
540
|
+
'var': 'ANTHROPIC_API_KEY',
|
|
541
|
+
'model': 'claude-3-5-haiku-latest'
|
|
542
|
+
},
|
|
543
|
+
'google': {
|
|
544
|
+
'var': 'GEMINI_API_KEY',
|
|
545
|
+
'model': 'gemini-2.5-flash'
|
|
546
|
+
},
|
|
547
|
+
'groq': {
|
|
548
|
+
'var': 'GROQ_API_KEY',
|
|
549
|
+
'model': 'llama3-70b-8192'
|
|
550
|
+
},
|
|
551
|
+
'connectonion': {
|
|
552
|
+
'var': 'CONNECTONION_API_KEY',
|
|
553
|
+
'model': 'co/gemini-2.5-pro' # Prefixed models for managed keys
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
config = configs.get(provider, configs['openai'])
|
|
558
|
+
|
|
559
|
+
# Special handling for ConnectOnion managed keys
|
|
560
|
+
if provider == 'connectonion':
|
|
561
|
+
if api_key == 'managed':
|
|
562
|
+
return f"""# ConnectOnion Managed Keys Configuration
|
|
563
|
+
# Authenticate with: co auth
|
|
564
|
+
# Purchase credits at: https://o.openonion.ai
|
|
565
|
+
# Same pricing as OpenAI/Anthropic
|
|
566
|
+
|
|
567
|
+
# Model Configuration (use co/ prefix for managed models)
|
|
568
|
+
MODEL=co/gemini-2.5-pro
|
|
569
|
+
# Available models: co/gemini-2.5-pro, co/gpt-4o, co/claude-3-haiku, co/claude-3-sonnet
|
|
570
|
+
|
|
571
|
+
# No API key needed - authentication handled via JWT token from 'co auth'
|
|
572
|
+
|
|
573
|
+
# Optional: Override default settings
|
|
574
|
+
# MAX_TOKENS=2000
|
|
575
|
+
# TEMPERATURE=0.7
|
|
576
|
+
"""
|
|
577
|
+
elif api_key == 'star':
|
|
578
|
+
return f"""# ConnectOnion Free Credits (100k tokens)
|
|
579
|
+
# 1. Star us: https://github.com/openonion/connectonion
|
|
580
|
+
# 2. Authenticate with: co auth
|
|
581
|
+
# 3. Your GitHub star will be verified automatically
|
|
582
|
+
|
|
583
|
+
# Model Configuration (use co/ prefix for managed models)
|
|
584
|
+
MODEL=co/gemini-2.5-pro
|
|
585
|
+
|
|
586
|
+
# No API key needed - authentication handled via JWT token from 'co auth'
|
|
587
|
+
|
|
588
|
+
# Optional: Override default settings
|
|
589
|
+
# MAX_TOKENS=2000
|
|
590
|
+
# TEMPERATURE=0.7
|
|
591
|
+
"""
|
|
592
|
+
|
|
593
|
+
return f"""# {provider.title()} API Configuration
|
|
594
|
+
{config['var']}={api_key}
|
|
595
|
+
|
|
596
|
+
# Model Configuration
|
|
597
|
+
MODEL={config['model']}
|
|
598
|
+
|
|
599
|
+
# Optional: Override default settings
|
|
600
|
+
# MAX_TOKENS=2000
|
|
601
|
+
# TEMPERATURE=0.7
|
|
602
|
+
"""
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def generate_custom_template_with_name(description: str, api_key: str, model: str = None, loading_animation=None) -> Tuple[str, str]:
|
|
606
|
+
"""Generate custom agent template and suggested name using AI.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
description: What the agent should do
|
|
610
|
+
api_key: API key or token for LLM
|
|
611
|
+
model: Optional model to use (e.g., "co/gpt-4o-mini")
|
|
612
|
+
loading_animation: Optional LoadingAnimation instance to update
|
|
613
|
+
|
|
614
|
+
Returns:
|
|
615
|
+
Tuple of (agent_code, suggested_name)
|
|
616
|
+
"""
|
|
617
|
+
import re
|
|
618
|
+
|
|
619
|
+
# Default fallback values
|
|
620
|
+
suggested_name = "custom-agent"
|
|
621
|
+
|
|
622
|
+
# Try to use AI to generate name and code
|
|
623
|
+
if model or api_key:
|
|
624
|
+
try:
|
|
625
|
+
from ...llm import create_llm
|
|
626
|
+
|
|
627
|
+
# Use the model specified or default to co/gemini-2.5-pro
|
|
628
|
+
llm_model = model if model else "co/gemini-2.5-pro"
|
|
629
|
+
|
|
630
|
+
if loading_animation:
|
|
631
|
+
loading_animation.update(f"Connecting to {llm_model}...")
|
|
632
|
+
|
|
633
|
+
# Create LLM instance
|
|
634
|
+
if model and model.startswith("co/"):
|
|
635
|
+
# Using ConnectOnion managed keys - api_key is actually the JWT token
|
|
636
|
+
llm = create_llm(model=llm_model, api_key=api_key)
|
|
637
|
+
else:
|
|
638
|
+
# Using user's API key
|
|
639
|
+
llm = create_llm(model=llm_model, api_key=api_key if api_key else None)
|
|
640
|
+
|
|
641
|
+
# Generate project name and code with AI
|
|
642
|
+
prompt = f"""Based on this description: "{description}"
|
|
643
|
+
|
|
644
|
+
Generate:
|
|
645
|
+
1. A short, descriptive project name (lowercase, hyphenated, max 30 chars, no spaces)
|
|
646
|
+
2. Python code for a ConnectOnion agent that implements this functionality
|
|
647
|
+
|
|
648
|
+
Respond in this exact format:
|
|
649
|
+
PROJECT_NAME: your-suggested-name
|
|
650
|
+
CODE:
|
|
651
|
+
```python
|
|
652
|
+
# Your generated code here
|
|
653
|
+
```"""
|
|
654
|
+
|
|
655
|
+
messages = [
|
|
656
|
+
{"role": "system", "content": "You are an AI assistant that generates ConnectOnion agent code and project names."},
|
|
657
|
+
{"role": "user", "content": prompt}
|
|
658
|
+
]
|
|
659
|
+
|
|
660
|
+
if loading_animation:
|
|
661
|
+
loading_animation.update(f"Generating agent code...")
|
|
662
|
+
|
|
663
|
+
response = llm.complete(messages)
|
|
664
|
+
|
|
665
|
+
if response.content:
|
|
666
|
+
# Parse the response
|
|
667
|
+
lines = response.content.split('\n')
|
|
668
|
+
for line in lines:
|
|
669
|
+
if line.startswith("PROJECT_NAME:"):
|
|
670
|
+
suggested_name = line.replace("PROJECT_NAME:", "").strip()
|
|
671
|
+
# Validate name format
|
|
672
|
+
suggested_name = re.sub(r'[^a-z0-9-]', '', suggested_name.lower())
|
|
673
|
+
if len(suggested_name) > 30:
|
|
674
|
+
suggested_name = suggested_name[:30]
|
|
675
|
+
break
|
|
676
|
+
|
|
677
|
+
# Extract code between ```python and ```
|
|
678
|
+
if "```python" in response.content and "```" in response.content:
|
|
679
|
+
code_start = response.content.find("```python") + 9
|
|
680
|
+
code_end = response.content.find("```", code_start)
|
|
681
|
+
if code_end > code_start:
|
|
682
|
+
agent_code = response.content[code_start:code_end].strip()
|
|
683
|
+
return agent_code, suggested_name
|
|
684
|
+
|
|
685
|
+
except Exception as e:
|
|
686
|
+
# If AI generation fails, fall back to simple generation
|
|
687
|
+
print(f"AI generation failed: {e}, using fallback")
|
|
688
|
+
|
|
689
|
+
# Fallback: Simple name generation from description
|
|
690
|
+
words = description.lower().split()[:3]
|
|
691
|
+
suggested_name = "-".join(re.sub(r'[^a-z0-9]', '', word) for word in words if word)
|
|
692
|
+
if not suggested_name:
|
|
693
|
+
suggested_name = "custom-agent"
|
|
694
|
+
else:
|
|
695
|
+
suggested_name = suggested_name + "-agent"
|
|
696
|
+
|
|
697
|
+
if len(suggested_name) > 30:
|
|
698
|
+
suggested_name = suggested_name[:30]
|
|
699
|
+
|
|
700
|
+
# Fallback agent code
|
|
701
|
+
agent_code = f"""# {description}
|
|
702
|
+
# Generated with ConnectOnion
|
|
703
|
+
|
|
704
|
+
from connectonion import Agent
|
|
705
|
+
|
|
706
|
+
def process_request(query: str) -> str:
|
|
707
|
+
'''Process user queries for: {description}'''
|
|
708
|
+
return f"Processing: {{query}}"
|
|
709
|
+
|
|
710
|
+
# Create agent
|
|
711
|
+
agent = Agent(
|
|
712
|
+
name="{suggested_name.replace('-', '_')}",
|
|
713
|
+
model="{'co/gemini-2.5-pro' if model and model.startswith('co/') else 'co/gemini-2.5-pro'}",
|
|
714
|
+
system_prompt=\"\"\"You are an AI agent designed to: {description}
|
|
715
|
+
|
|
716
|
+
Provide helpful, accurate, and concise responses.\"\"\",
|
|
717
|
+
tools=[process_request]
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
if __name__ == "__main__":
|
|
721
|
+
print(f"🤖 {suggested_name.replace('-', ' ').title()} Ready!")
|
|
722
|
+
print("Type 'exit' to quit\\n")
|
|
723
|
+
|
|
724
|
+
while True:
|
|
725
|
+
user_input = input("You: ")
|
|
726
|
+
if user_input.lower() in ['exit', 'quit']:
|
|
727
|
+
break
|
|
728
|
+
|
|
729
|
+
response = agent.input(user_input)
|
|
730
|
+
print(f"Agent: {{response}}\\n")
|
|
731
|
+
"""
|
|
732
|
+
|
|
733
|
+
return agent_code, suggested_name
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def generate_custom_template(description: str, api_key: str) -> str:
|
|
737
|
+
"""Generate custom agent template using AI.
|
|
738
|
+
|
|
739
|
+
This is a placeholder - actual implementation would call AI API.
|
|
740
|
+
"""
|
|
741
|
+
# TODO: Implement actual AI generation
|
|
742
|
+
return f"""# Custom Agent Generated from: {description}
|
|
743
|
+
|
|
744
|
+
from connectonion import Agent
|
|
745
|
+
|
|
746
|
+
def custom_tool(param: str) -> str:
|
|
747
|
+
'''Custom tool for: {description}'''
|
|
748
|
+
return f"Processing: {{param}}"
|
|
749
|
+
|
|
750
|
+
agent = Agent(
|
|
751
|
+
name="custom_agent",
|
|
752
|
+
system_prompt="You are a custom agent designed for: {description}",
|
|
753
|
+
tools=[custom_tool]
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
if __name__ == "__main__":
|
|
757
|
+
while True:
|
|
758
|
+
user_input = input("You: ")
|
|
759
|
+
if user_input.lower() == 'quit':
|
|
760
|
+
break
|
|
761
|
+
response = agent.input(user_input)
|
|
762
|
+
print(f"Agent: {{response}}")
|
|
763
|
+
"""
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def is_directory_empty(directory: str) -> bool:
|
|
767
|
+
"""Check if a directory is empty (ignoring .git directory)."""
|
|
768
|
+
contents = os.listdir(directory)
|
|
769
|
+
# Ignore '.', '..', and '.git' directory
|
|
770
|
+
meaningful_contents = [item for item in contents if item not in ['.', '..', '.git']]
|
|
771
|
+
return len(meaningful_contents) == 0
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def is_special_directory(directory: str) -> bool:
|
|
775
|
+
"""Check if directory is a special system directory."""
|
|
776
|
+
abs_path = os.path.abspath(directory)
|
|
777
|
+
|
|
778
|
+
if abs_path == os.path.expanduser("~"):
|
|
779
|
+
return True
|
|
780
|
+
if abs_path == "/":
|
|
781
|
+
return True
|
|
782
|
+
if "/tmp" in abs_path or "temp" in abs_path.lower():
|
|
783
|
+
return False
|
|
784
|
+
|
|
785
|
+
system_dirs = ["/usr", "/etc", "/bin", "/sbin", "/lib", "/opt"]
|
|
786
|
+
for sys_dir in system_dirs:
|
|
787
|
+
if abs_path.startswith(sys_dir + "/") or abs_path == sys_dir:
|
|
788
|
+
return True
|
|
789
|
+
|
|
790
|
+
return False
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def get_special_directory_warning(directory: str) -> str:
|
|
794
|
+
"""Get warning message for special directories."""
|
|
795
|
+
abs_path = os.path.abspath(directory)
|
|
796
|
+
|
|
797
|
+
if abs_path == os.path.expanduser("~"):
|
|
798
|
+
return "⚠️ You're in your HOME directory. Consider creating a project folder first."
|
|
799
|
+
elif abs_path == "/":
|
|
800
|
+
return "⚠️ You're in the ROOT directory. This is not recommended!"
|
|
801
|
+
elif any(abs_path.startswith(d) for d in ["/usr", "/etc", "/bin", "/sbin", "/lib", "/opt"]):
|
|
802
|
+
return "⚠️ You're in a SYSTEM directory. This could affect system files!"
|
|
803
|
+
|
|
804
|
+
return ""
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
# Export shared utilities for use by init.py and create.py
|
|
808
|
+
__all__ = [
|
|
809
|
+
'LoadingAnimation',
|
|
810
|
+
'validate_project_name',
|
|
811
|
+
'get_special_directory_warning',
|
|
812
|
+
'is_special_directory',
|
|
813
|
+
'is_directory_empty',
|
|
814
|
+
'check_environment_for_api_keys',
|
|
815
|
+
'detect_api_provider',
|
|
816
|
+
'configure_env_for_provider',
|
|
817
|
+
'api_key_setup_menu',
|
|
818
|
+
'get_template_info',
|
|
819
|
+
'get_template_suggested_name',
|
|
820
|
+
'get_template_preview',
|
|
821
|
+
'interactive_menu',
|
|
822
|
+
'show_progress',
|
|
823
|
+
'generate_custom_template',
|
|
824
|
+
'generate_custom_template_with_name',
|
|
825
|
+
]
|
|
826
|
+
|
|
827
|
+
# All the handle_init and handle_create code has been moved to init.py and create.py
|
|
828
|
+
# This file now only contains shared utilities
|