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.
Files changed (113) hide show
  1. connectonion/__init__.py +78 -0
  2. connectonion/address.py +320 -0
  3. connectonion/agent.py +450 -0
  4. connectonion/announce.py +84 -0
  5. connectonion/asgi.py +287 -0
  6. connectonion/auto_debug_exception.py +181 -0
  7. connectonion/cli/__init__.py +3 -0
  8. connectonion/cli/browser_agent/__init__.py +5 -0
  9. connectonion/cli/browser_agent/browser.py +243 -0
  10. connectonion/cli/browser_agent/prompt.md +107 -0
  11. connectonion/cli/commands/__init__.py +1 -0
  12. connectonion/cli/commands/auth_commands.py +527 -0
  13. connectonion/cli/commands/browser_commands.py +27 -0
  14. connectonion/cli/commands/create.py +511 -0
  15. connectonion/cli/commands/deploy_commands.py +220 -0
  16. connectonion/cli/commands/doctor_commands.py +173 -0
  17. connectonion/cli/commands/init.py +469 -0
  18. connectonion/cli/commands/project_cmd_lib.py +828 -0
  19. connectonion/cli/commands/reset_commands.py +149 -0
  20. connectonion/cli/commands/status_commands.py +168 -0
  21. connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +2010 -0
  22. connectonion/cli/docs/connectonion.md +1256 -0
  23. connectonion/cli/docs.md +123 -0
  24. connectonion/cli/main.py +148 -0
  25. connectonion/cli/templates/meta-agent/README.md +287 -0
  26. connectonion/cli/templates/meta-agent/agent.py +196 -0
  27. connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +9 -0
  28. connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +15 -0
  29. connectonion/cli/templates/meta-agent/prompts/metagent.md +71 -0
  30. connectonion/cli/templates/meta-agent/prompts/think_prompt.md +18 -0
  31. connectonion/cli/templates/minimal/README.md +56 -0
  32. connectonion/cli/templates/minimal/agent.py +40 -0
  33. connectonion/cli/templates/playwright/README.md +118 -0
  34. connectonion/cli/templates/playwright/agent.py +336 -0
  35. connectonion/cli/templates/playwright/prompt.md +102 -0
  36. connectonion/cli/templates/playwright/requirements.txt +3 -0
  37. connectonion/cli/templates/web-research/agent.py +122 -0
  38. connectonion/connect.py +128 -0
  39. connectonion/console.py +539 -0
  40. connectonion/debug_agent/__init__.py +13 -0
  41. connectonion/debug_agent/agent.py +45 -0
  42. connectonion/debug_agent/prompts/debug_assistant.md +72 -0
  43. connectonion/debug_agent/runtime_inspector.py +406 -0
  44. connectonion/debug_explainer/__init__.py +10 -0
  45. connectonion/debug_explainer/explain_agent.py +114 -0
  46. connectonion/debug_explainer/explain_context.py +263 -0
  47. connectonion/debug_explainer/explainer_prompt.md +29 -0
  48. connectonion/debug_explainer/root_cause_analysis_prompt.md +43 -0
  49. connectonion/debugger_ui.py +1039 -0
  50. connectonion/decorators.py +208 -0
  51. connectonion/events.py +248 -0
  52. connectonion/execution_analyzer/__init__.py +9 -0
  53. connectonion/execution_analyzer/execution_analysis.py +93 -0
  54. connectonion/execution_analyzer/execution_analysis_prompt.md +47 -0
  55. connectonion/host.py +579 -0
  56. connectonion/interactive_debugger.py +342 -0
  57. connectonion/llm.py +801 -0
  58. connectonion/llm_do.py +307 -0
  59. connectonion/logger.py +300 -0
  60. connectonion/prompt_files/__init__.py +1 -0
  61. connectonion/prompt_files/analyze_contact.md +62 -0
  62. connectonion/prompt_files/eval_expected.md +12 -0
  63. connectonion/prompt_files/react_evaluate.md +11 -0
  64. connectonion/prompt_files/react_plan.md +16 -0
  65. connectonion/prompt_files/reflect.md +22 -0
  66. connectonion/prompts.py +144 -0
  67. connectonion/relay.py +200 -0
  68. connectonion/static/docs.html +688 -0
  69. connectonion/tool_executor.py +279 -0
  70. connectonion/tool_factory.py +186 -0
  71. connectonion/tool_registry.py +105 -0
  72. connectonion/trust.py +166 -0
  73. connectonion/trust_agents.py +71 -0
  74. connectonion/trust_functions.py +88 -0
  75. connectonion/tui/__init__.py +57 -0
  76. connectonion/tui/divider.py +39 -0
  77. connectonion/tui/dropdown.py +251 -0
  78. connectonion/tui/footer.py +31 -0
  79. connectonion/tui/fuzzy.py +56 -0
  80. connectonion/tui/input.py +278 -0
  81. connectonion/tui/keys.py +35 -0
  82. connectonion/tui/pick.py +130 -0
  83. connectonion/tui/providers.py +155 -0
  84. connectonion/tui/status_bar.py +163 -0
  85. connectonion/usage.py +161 -0
  86. connectonion/useful_events_handlers/__init__.py +16 -0
  87. connectonion/useful_events_handlers/reflect.py +116 -0
  88. connectonion/useful_plugins/__init__.py +20 -0
  89. connectonion/useful_plugins/calendar_plugin.py +163 -0
  90. connectonion/useful_plugins/eval.py +139 -0
  91. connectonion/useful_plugins/gmail_plugin.py +162 -0
  92. connectonion/useful_plugins/image_result_formatter.py +127 -0
  93. connectonion/useful_plugins/re_act.py +78 -0
  94. connectonion/useful_plugins/shell_approval.py +159 -0
  95. connectonion/useful_tools/__init__.py +44 -0
  96. connectonion/useful_tools/diff_writer.py +192 -0
  97. connectonion/useful_tools/get_emails.py +183 -0
  98. connectonion/useful_tools/gmail.py +1596 -0
  99. connectonion/useful_tools/google_calendar.py +613 -0
  100. connectonion/useful_tools/memory.py +380 -0
  101. connectonion/useful_tools/microsoft_calendar.py +604 -0
  102. connectonion/useful_tools/outlook.py +488 -0
  103. connectonion/useful_tools/send_email.py +205 -0
  104. connectonion/useful_tools/shell.py +97 -0
  105. connectonion/useful_tools/slash_command.py +201 -0
  106. connectonion/useful_tools/terminal.py +285 -0
  107. connectonion/useful_tools/todo_list.py +241 -0
  108. connectonion/useful_tools/web_fetch.py +216 -0
  109. connectonion/xray.py +467 -0
  110. connectonion-0.5.8.dist-info/METADATA +741 -0
  111. connectonion-0.5.8.dist-info/RECORD +113 -0
  112. connectonion-0.5.8.dist-info/WHEEL +4 -0
  113. 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