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,527 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Authenticate with OpenOnion backend using Ed25519 signature-based authentication to obtain JWT for managed keys
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [sys, time, toml, requests, pathlib, rich.console, rich.progress, rich.panel, address] | imported by [cli/main.py via handle_auth(), cli/commands/init.py, cli/commands/create.py] | calls backend at [https://api.openonion.ai/api/auth/login] | tested by [tests/cli/test_cli_auth.py]
|
|
5
|
+
Data flow: receives co_dir: Path from caller ā address.load(co_dir) reads Ed25519 keypair from .co/keys/ ā creates auth message with timestamp ā address.sign() creates signature ā POST to /api/auth/login with {public_key, message, signature, timestamp} ā backend verifies signature ā receives JWT token ā saves to ~/.co/keys.env as OPENONION_API_KEY ā optionally saves to project .env if save_to_project=True ā displays balance and email status ā returns success bool
|
|
6
|
+
State/Effects: modifies ~/.co/keys.env (adds/updates OPENONION_API_KEY and AGENT_EMAIL) | optionally modifies project .env if save_to_project=True | makes network POST request to api.openonion.ai | chmod 0o600 on .env files (Unix/Mac) | writes to stdout via rich.Console with progress spinner | updates ~/.co/config.toml with email_active status
|
|
7
|
+
Integration: exposes handle_auth() for CLI and authenticate(co_dir, save_to_project) for programmatic use | called by init.py and create.py during project setup | relies on address module for Ed25519 keypair operations | uses requests for HTTP calls | displays Rich progress spinner during network call | backend creates account on first auth (no separate registration)
|
|
8
|
+
Performance: network call to backend (2-5s) | signature generation is fast (<10ms) | file I/O for .env and config.toml | retries on network errors (up to 3 attempts with exponential backoff)
|
|
9
|
+
Errors: fails if ~/.co/keys/ missing (no keypair) | fails if backend unreachable (network error) | fails if signature invalid (backend 401) | fails if timestamp expired (5min window) | prints error messages to console and returns False | backend 500 errors bubble up with error details
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
import toml
|
|
15
|
+
import requests
|
|
16
|
+
import json
|
|
17
|
+
import webbrowser
|
|
18
|
+
import os
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
22
|
+
from rich.panel import Panel
|
|
23
|
+
from dotenv import load_dotenv
|
|
24
|
+
|
|
25
|
+
from ... import address
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _save_api_key_to_env(co_dir: Path, api_key: str, agent_email: str = None, agent_address: str = None) -> None:
|
|
31
|
+
"""Save OPENONION_API_KEY, AGENT_EMAIL, and AGENT_ADDRESS to .env file.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
co_dir: Path to .co directory
|
|
35
|
+
api_key: The API key/token to save
|
|
36
|
+
agent_email: The agent email address to save (optional)
|
|
37
|
+
agent_address: The full agent address to save (optional)
|
|
38
|
+
"""
|
|
39
|
+
env_file = co_dir.parent / ".env"
|
|
40
|
+
env_lines = []
|
|
41
|
+
key_found = False
|
|
42
|
+
email_found = False
|
|
43
|
+
address_found = False
|
|
44
|
+
|
|
45
|
+
# Read existing .env if it exists
|
|
46
|
+
if env_file.exists():
|
|
47
|
+
with open(env_file, "r", encoding='utf-8') as f:
|
|
48
|
+
for line in f:
|
|
49
|
+
if line.strip().startswith("OPENONION_API_KEY="):
|
|
50
|
+
env_lines.append(f"OPENONION_API_KEY={api_key}\n")
|
|
51
|
+
key_found = True
|
|
52
|
+
elif line.strip().startswith("AGENT_EMAIL=") and agent_email:
|
|
53
|
+
env_lines.append(f"AGENT_EMAIL={agent_email}\n")
|
|
54
|
+
email_found = True
|
|
55
|
+
elif line.strip().startswith("AGENT_ADDRESS=") and agent_address:
|
|
56
|
+
env_lines.append(f"AGENT_ADDRESS={agent_address}\n")
|
|
57
|
+
address_found = True
|
|
58
|
+
else:
|
|
59
|
+
env_lines.append(line)
|
|
60
|
+
|
|
61
|
+
# Add key if not found
|
|
62
|
+
if not key_found:
|
|
63
|
+
if env_lines and not env_lines[-1].endswith("\n"):
|
|
64
|
+
env_lines.append("\n")
|
|
65
|
+
env_lines.append(f"OPENONION_API_KEY={api_key}\n")
|
|
66
|
+
|
|
67
|
+
# Add email if not found and provided
|
|
68
|
+
if agent_email and not email_found:
|
|
69
|
+
env_lines.append(f"AGENT_EMAIL={agent_email}\n")
|
|
70
|
+
|
|
71
|
+
# Add address if not found and provided
|
|
72
|
+
if agent_address and not address_found:
|
|
73
|
+
env_lines.append(f"AGENT_ADDRESS={agent_address}\n")
|
|
74
|
+
|
|
75
|
+
# Write .env file
|
|
76
|
+
with open(env_file, "w", encoding='utf-8') as f:
|
|
77
|
+
f.writelines(env_lines)
|
|
78
|
+
|
|
79
|
+
# Make sure file permissions are restrictive (Unix/Mac only)
|
|
80
|
+
if sys.platform != 'win32':
|
|
81
|
+
env_file.chmod(0o600)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def authenticate(co_dir: Path, save_to_project: bool = True, quiet: bool = False) -> bool:
|
|
85
|
+
"""Authenticate with OpenOnion API directly.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
co_dir: Path to .co directory with keys
|
|
89
|
+
save_to_project: Whether to also save token to current directory's .env
|
|
90
|
+
quiet: If True, suppress verbose output (only show errors and minimal success)
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if authentication successful, False otherwise
|
|
94
|
+
"""
|
|
95
|
+
# Load agent keys - let it fail naturally if there's a problem
|
|
96
|
+
addr_data = address.load(co_dir)
|
|
97
|
+
if not addr_data:
|
|
98
|
+
console.print("ā No agent keys found!", style="red")
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
public_key = addr_data["address"]
|
|
102
|
+
|
|
103
|
+
# Create signed authentication message
|
|
104
|
+
timestamp = int(time.time())
|
|
105
|
+
message = f"ConnectOnion-Auth-{public_key}-{timestamp}"
|
|
106
|
+
signature = address.sign(addr_data, message.encode()).hex()
|
|
107
|
+
|
|
108
|
+
# Call the new unified auth endpoint
|
|
109
|
+
auth_url = "https://oo.openonion.ai/api/v1/auth"
|
|
110
|
+
|
|
111
|
+
response = requests.post(auth_url, json={
|
|
112
|
+
"public_key": public_key,
|
|
113
|
+
"signature": signature,
|
|
114
|
+
"message": message
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
if response.status_code == 200:
|
|
118
|
+
data = response.json()
|
|
119
|
+
token = data.get("token")
|
|
120
|
+
|
|
121
|
+
# Extract agent email from server response FIRST (before saving to .env)
|
|
122
|
+
user = data.get("user", {})
|
|
123
|
+
email_info = user.get("email") if user else None
|
|
124
|
+
|
|
125
|
+
# Get the agent email from the server response
|
|
126
|
+
if email_info:
|
|
127
|
+
agent_email = email_info.get("address", f"{public_key[:10]}@mail.openonion.ai")
|
|
128
|
+
else:
|
|
129
|
+
agent_email = f"{public_key[:10]}@mail.openonion.ai"
|
|
130
|
+
|
|
131
|
+
# Save token to appropriate .env file(s)
|
|
132
|
+
is_global = co_dir.resolve() == (Path.home() / ".co").resolve()
|
|
133
|
+
|
|
134
|
+
if is_global:
|
|
135
|
+
# Save to global keys.env
|
|
136
|
+
global_keys_env = co_dir / "keys.env"
|
|
137
|
+
env_lines = []
|
|
138
|
+
key_found = False
|
|
139
|
+
email_found = False
|
|
140
|
+
address_found = False
|
|
141
|
+
|
|
142
|
+
# Read existing keys.env if it exists (preserve AGENT_ADDRESS)
|
|
143
|
+
config_path_found = False
|
|
144
|
+
if global_keys_env.exists():
|
|
145
|
+
with open(global_keys_env, "r", encoding='utf-8') as f:
|
|
146
|
+
for line in f:
|
|
147
|
+
if line.strip().startswith("OPENONION_API_KEY="):
|
|
148
|
+
env_lines.append(f"OPENONION_API_KEY={token}\n")
|
|
149
|
+
key_found = True
|
|
150
|
+
elif line.strip().startswith("AGENT_EMAIL="):
|
|
151
|
+
env_lines.append(f"AGENT_EMAIL={agent_email}\n")
|
|
152
|
+
email_found = True
|
|
153
|
+
elif line.strip().startswith("AGENT_ADDRESS="):
|
|
154
|
+
address_found = True
|
|
155
|
+
env_lines.append(line) # Preserve existing address
|
|
156
|
+
elif line.strip().startswith("AGENT_CONFIG_PATH="):
|
|
157
|
+
config_path_found = True
|
|
158
|
+
env_lines.append(line) # Preserve existing config path
|
|
159
|
+
else:
|
|
160
|
+
env_lines.append(line)
|
|
161
|
+
|
|
162
|
+
# Add config path if not found (at the beginning)
|
|
163
|
+
if not config_path_found:
|
|
164
|
+
env_lines.insert(0, f"AGENT_CONFIG_PATH={co_dir}\n")
|
|
165
|
+
|
|
166
|
+
# Add key if not found
|
|
167
|
+
if not key_found:
|
|
168
|
+
if env_lines and not env_lines[-1].endswith("\n"):
|
|
169
|
+
env_lines.append("\n")
|
|
170
|
+
env_lines.append(f"OPENONION_API_KEY={token}\n")
|
|
171
|
+
|
|
172
|
+
# Add email if not found
|
|
173
|
+
if not email_found:
|
|
174
|
+
env_lines.append(f"AGENT_EMAIL={agent_email}\n")
|
|
175
|
+
|
|
176
|
+
# Add address if not found (ensure AGENT_ADDRESS is always in global keys.env)
|
|
177
|
+
if not address_found:
|
|
178
|
+
env_lines.append(f"AGENT_ADDRESS={public_key}\n")
|
|
179
|
+
|
|
180
|
+
# Write global keys.env file
|
|
181
|
+
with open(global_keys_env, "w", encoding='utf-8') as f:
|
|
182
|
+
f.writelines(env_lines)
|
|
183
|
+
if sys.platform != 'win32':
|
|
184
|
+
global_keys_env.chmod(0o600)
|
|
185
|
+
|
|
186
|
+
console.print(f"ā Saved to {global_keys_env}", style="green")
|
|
187
|
+
|
|
188
|
+
# Also save to current directory's .env (always create if using global keys and save_to_project=True)
|
|
189
|
+
if save_to_project:
|
|
190
|
+
local_env_path = Path(".co") if Path(".co").exists() else co_dir
|
|
191
|
+
_save_api_key_to_env(local_env_path, token, agent_email, public_key)
|
|
192
|
+
# Show relative path for local .env
|
|
193
|
+
local_env_file = Path.cwd() / ".env"
|
|
194
|
+
console.print(f"ā Saved to {local_env_file}", style="green")
|
|
195
|
+
else:
|
|
196
|
+
# Save to local project .env
|
|
197
|
+
_save_api_key_to_env(co_dir, token, agent_email, public_key)
|
|
198
|
+
|
|
199
|
+
# Simple success message with balance
|
|
200
|
+
balance = user.get('balance_usd', 0.0) if user else 0.0
|
|
201
|
+
console.print(f"ā Authenticated (Balance: ${balance:.2f})", style="green")
|
|
202
|
+
|
|
203
|
+
return True
|
|
204
|
+
else:
|
|
205
|
+
error_msg = response.json().get("detail", "Registration failed")
|
|
206
|
+
console.print(f"ā Registration failed: {error_msg}", style="red")
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def handle_auth():
|
|
213
|
+
"""Authenticate with OpenOnion for managed keys (co/ models).
|
|
214
|
+
|
|
215
|
+
This command will:
|
|
216
|
+
1. Load your agent's keys from .co/keys/ (or ~/.co/keys/ as fallback)
|
|
217
|
+
2. Sign an authentication message
|
|
218
|
+
3. Authenticate with the backend API
|
|
219
|
+
4. Display comprehensive account information
|
|
220
|
+
5. Save the token for future use
|
|
221
|
+
"""
|
|
222
|
+
# Check if we have local keys first
|
|
223
|
+
co_dir = Path(".co")
|
|
224
|
+
use_global = False
|
|
225
|
+
|
|
226
|
+
# Check if local .co/keys/agent.key exists
|
|
227
|
+
if co_dir.exists() and (co_dir / "keys" / "agent.key").exists():
|
|
228
|
+
# Use local keys
|
|
229
|
+
console.print("š Using local project keys (.co)", style="cyan")
|
|
230
|
+
else:
|
|
231
|
+
# No local keys, try global
|
|
232
|
+
co_dir = Path.home() / ".co"
|
|
233
|
+
use_global = True
|
|
234
|
+
|
|
235
|
+
if not co_dir.exists() or not (co_dir / "keys" / "agent.key").exists():
|
|
236
|
+
console.print("\nā [bold red]No agent keys found[/bold red]")
|
|
237
|
+
console.print("\n[cyan]Initialize ConnectOnion first:[/cyan]")
|
|
238
|
+
console.print(" [bold]co init[/bold] Add to current directory")
|
|
239
|
+
console.print(" [bold]co create[/bold] Create new project folder")
|
|
240
|
+
console.print("\n[dim]Both set up ~/.co/ with your keys[/dim]\n")
|
|
241
|
+
return
|
|
242
|
+
else:
|
|
243
|
+
console.print("š Using global ConnectOnion keys (~/.co)", style="cyan")
|
|
244
|
+
|
|
245
|
+
# Use the unified authenticate function
|
|
246
|
+
success = authenticate(co_dir)
|
|
247
|
+
|
|
248
|
+
if not success:
|
|
249
|
+
console.print("\n[yellow]Need help?[/yellow]")
|
|
250
|
+
console.print(" ⢠Check your internet connection")
|
|
251
|
+
console.print(" ⢠Try 'co init' to reinitialize your keys")
|
|
252
|
+
console.print(" ⢠Visit https://discord.gg/4xfD9k8AUF for support")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _load_api_key() -> str:
|
|
256
|
+
"""Load OPENONION_API_KEY from environment.
|
|
257
|
+
|
|
258
|
+
Checks in order:
|
|
259
|
+
1. Environment variable
|
|
260
|
+
2. Local .env file
|
|
261
|
+
3. Global ~/.co/keys.env file
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
API key if found, None otherwise
|
|
265
|
+
"""
|
|
266
|
+
# Check environment variable first
|
|
267
|
+
api_key = os.getenv("OPENONION_API_KEY")
|
|
268
|
+
if api_key:
|
|
269
|
+
return api_key
|
|
270
|
+
|
|
271
|
+
# Check local .env
|
|
272
|
+
local_env = Path(".env")
|
|
273
|
+
if local_env.exists():
|
|
274
|
+
load_dotenv(local_env)
|
|
275
|
+
api_key = os.getenv("OPENONION_API_KEY")
|
|
276
|
+
if api_key:
|
|
277
|
+
return api_key
|
|
278
|
+
|
|
279
|
+
# Check global ~/.co/keys.env
|
|
280
|
+
global_env = Path.home() / ".co" / "keys.env"
|
|
281
|
+
if global_env.exists():
|
|
282
|
+
load_dotenv(global_env)
|
|
283
|
+
api_key = os.getenv("OPENONION_API_KEY")
|
|
284
|
+
if api_key:
|
|
285
|
+
return api_key
|
|
286
|
+
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _save_google_to_env(env_file: Path, credentials: dict) -> None:
|
|
291
|
+
"""Save Google OAuth credentials to .env file.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
env_file: Path to .env file
|
|
295
|
+
credentials: Dict with access_token, refresh_token, expires_at, google_email, scopes
|
|
296
|
+
"""
|
|
297
|
+
env_lines = []
|
|
298
|
+
|
|
299
|
+
# Read existing .env
|
|
300
|
+
if env_file.exists():
|
|
301
|
+
with open(env_file, 'r', encoding='utf-8') as f:
|
|
302
|
+
for line in f:
|
|
303
|
+
# Skip existing Google credentials
|
|
304
|
+
if not line.strip().startswith('GOOGLE_'):
|
|
305
|
+
env_lines.append(line)
|
|
306
|
+
|
|
307
|
+
# Add Google credentials
|
|
308
|
+
if not env_lines or not env_lines[-1].endswith('\n'):
|
|
309
|
+
env_lines.append('\n')
|
|
310
|
+
|
|
311
|
+
env_lines.append('# Google OAuth Credentials\n')
|
|
312
|
+
env_lines.append(f"GOOGLE_ACCESS_TOKEN={credentials['access_token']}\n")
|
|
313
|
+
env_lines.append(f"GOOGLE_REFRESH_TOKEN={credentials['refresh_token']}\n")
|
|
314
|
+
env_lines.append(f"GOOGLE_TOKEN_EXPIRES_AT={credentials['expires_at']}\n")
|
|
315
|
+
env_lines.append(f"GOOGLE_SCOPES={credentials['scopes']}\n")
|
|
316
|
+
env_lines.append(f"GOOGLE_EMAIL={credentials['google_email']}\n")
|
|
317
|
+
|
|
318
|
+
# Write .env
|
|
319
|
+
with open(env_file, 'w', encoding='utf-8') as f:
|
|
320
|
+
f.writelines(env_lines)
|
|
321
|
+
|
|
322
|
+
# Set permissions (Unix/Mac only)
|
|
323
|
+
if sys.platform != 'win32':
|
|
324
|
+
env_file.chmod(0o600)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def handle_google_auth():
|
|
328
|
+
"""Authenticate with Google OAuth for Gmail/Calendar access."""
|
|
329
|
+
|
|
330
|
+
# Check if user is authenticated with OpenOnion first
|
|
331
|
+
api_key = _load_api_key()
|
|
332
|
+
if not api_key:
|
|
333
|
+
console.print("\nā [bold red]Not authenticated with OpenOnion[/bold red]")
|
|
334
|
+
console.print("\n[cyan]Authenticate first:[/cyan]")
|
|
335
|
+
console.print(" [bold]co auth[/bold] Get your OpenOnion API key\n")
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
api_url = "https://oo.openonion.ai/api/v1/oauth"
|
|
339
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
340
|
+
|
|
341
|
+
# Clear any existing connection first - this ensures we wait for NEW OAuth to complete
|
|
342
|
+
# (otherwise /google/status returns connected=true immediately from old credentials)
|
|
343
|
+
requests.delete(f"{api_url}/google/revoke", headers=headers)
|
|
344
|
+
|
|
345
|
+
# Get OAuth URL
|
|
346
|
+
console.print("š Initializing Google OAuth...", style="cyan")
|
|
347
|
+
|
|
348
|
+
response = requests.get(f"{api_url}/google/init", headers=headers)
|
|
349
|
+
if response.status_code != 200:
|
|
350
|
+
console.print(f"\nā Failed to initialize OAuth: {response.text}", style="red")
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
auth_url = response.json()['auth_url']
|
|
354
|
+
|
|
355
|
+
# Open browser
|
|
356
|
+
console.print(f"\nš Opening browser for Google authentication...")
|
|
357
|
+
console.print(f" URL: {auth_url}\n", style="dim")
|
|
358
|
+
|
|
359
|
+
webbrowser.open(auth_url)
|
|
360
|
+
|
|
361
|
+
# Poll for completion
|
|
362
|
+
console.print("ā³ Waiting for authorization...", style="yellow")
|
|
363
|
+
console.print(" (Complete the authorization in your browser)\n", style="dim")
|
|
364
|
+
|
|
365
|
+
max_attempts = 60 # 5 minutes (5 second intervals)
|
|
366
|
+
for attempt in range(max_attempts):
|
|
367
|
+
time.sleep(5)
|
|
368
|
+
|
|
369
|
+
status_response = requests.get(f"{api_url}/google/status", headers=headers)
|
|
370
|
+
if status_response.status_code == 200:
|
|
371
|
+
status = status_response.json()
|
|
372
|
+
if status.get('connected'):
|
|
373
|
+
console.print("ā Authorization successful!", style="green")
|
|
374
|
+
break
|
|
375
|
+
else:
|
|
376
|
+
console.print("\nā Authorization timed out", style="red")
|
|
377
|
+
console.print("Please try again with: [bold]co auth google[/bold]\n")
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
# Get credentials
|
|
381
|
+
creds_response = requests.get(f"{api_url}/google/credentials", headers=headers)
|
|
382
|
+
if creds_response.status_code != 200:
|
|
383
|
+
console.print(f"\nā Failed to get credentials: {creds_response.text}", style="red")
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
credentials = creds_response.json()
|
|
387
|
+
|
|
388
|
+
# Save credentials
|
|
389
|
+
console.print("\nš¾ Saving credentials...", style="cyan")
|
|
390
|
+
|
|
391
|
+
# Save to global ~/.co/keys.env
|
|
392
|
+
global_keys_env = Path.home() / ".co" / "keys.env"
|
|
393
|
+
if global_keys_env.exists():
|
|
394
|
+
_save_google_to_env(global_keys_env, credentials)
|
|
395
|
+
console.print(f" ā Saved to {global_keys_env}", style="green")
|
|
396
|
+
|
|
397
|
+
# Save to local .env
|
|
398
|
+
local_env = Path(".env")
|
|
399
|
+
_save_google_to_env(local_env, credentials)
|
|
400
|
+
console.print(f" ā Saved to {local_env.absolute()}", style="green")
|
|
401
|
+
|
|
402
|
+
# Success message
|
|
403
|
+
console.print(f"\nā
[bold green]Google account connected![/bold green]")
|
|
404
|
+
console.print(f" Email: {credentials['google_email']}", style="green")
|
|
405
|
+
console.print(f"\nš§ You can now use Google tools in your agents:")
|
|
406
|
+
console.print(f" [dim]from connectonion.tools import gmail_send[/dim]")
|
|
407
|
+
console.print(f" [dim]agent = Agent('assistant', tools=[gmail_send])[/dim]\n")
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _save_microsoft_to_env(env_file: Path, credentials: dict) -> None:
|
|
411
|
+
"""Save Microsoft OAuth credentials to .env file.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
env_file: Path to .env file
|
|
415
|
+
credentials: Dict with access_token, refresh_token, expires_at, microsoft_email, scopes
|
|
416
|
+
"""
|
|
417
|
+
env_lines = []
|
|
418
|
+
|
|
419
|
+
# Read existing .env
|
|
420
|
+
if env_file.exists():
|
|
421
|
+
with open(env_file, 'r', encoding='utf-8') as f:
|
|
422
|
+
for line in f:
|
|
423
|
+
# Skip existing Microsoft credentials
|
|
424
|
+
if not line.strip().startswith('MICROSOFT_'):
|
|
425
|
+
env_lines.append(line)
|
|
426
|
+
|
|
427
|
+
# Add Microsoft credentials
|
|
428
|
+
if not env_lines or not env_lines[-1].endswith('\n'):
|
|
429
|
+
env_lines.append('\n')
|
|
430
|
+
|
|
431
|
+
env_lines.append('# Microsoft OAuth Credentials\n')
|
|
432
|
+
env_lines.append(f"MICROSOFT_ACCESS_TOKEN={credentials['access_token']}\n")
|
|
433
|
+
env_lines.append(f"MICROSOFT_REFRESH_TOKEN={credentials['refresh_token']}\n")
|
|
434
|
+
env_lines.append(f"MICROSOFT_TOKEN_EXPIRES_AT={credentials['expires_at']}\n")
|
|
435
|
+
env_lines.append(f"MICROSOFT_SCOPES={credentials['scopes']}\n")
|
|
436
|
+
env_lines.append(f"MICROSOFT_EMAIL={credentials['microsoft_email']}\n")
|
|
437
|
+
|
|
438
|
+
# Write .env
|
|
439
|
+
with open(env_file, 'w', encoding='utf-8') as f:
|
|
440
|
+
f.writelines(env_lines)
|
|
441
|
+
|
|
442
|
+
# Set permissions (Unix/Mac only)
|
|
443
|
+
if sys.platform != 'win32':
|
|
444
|
+
env_file.chmod(0o600)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def handle_microsoft_auth():
|
|
448
|
+
"""Authenticate with Microsoft OAuth for Outlook/Calendar access."""
|
|
449
|
+
|
|
450
|
+
# Check if user is authenticated with OpenOnion first
|
|
451
|
+
api_key = _load_api_key()
|
|
452
|
+
if not api_key:
|
|
453
|
+
console.print("\nā [bold red]Not authenticated with OpenOnion[/bold red]")
|
|
454
|
+
console.print("\n[cyan]Authenticate first:[/cyan]")
|
|
455
|
+
console.print(" [bold]co auth[/bold] Get your OpenOnion API key\n")
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
api_url = "https://oo.openonion.ai/api/v1/oauth"
|
|
459
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
460
|
+
|
|
461
|
+
# Clear any existing connection first
|
|
462
|
+
requests.delete(f"{api_url}/microsoft/revoke", headers=headers)
|
|
463
|
+
|
|
464
|
+
# Get OAuth URL
|
|
465
|
+
console.print("š Initializing Microsoft OAuth...", style="cyan")
|
|
466
|
+
|
|
467
|
+
response = requests.get(f"{api_url}/microsoft/init", headers=headers)
|
|
468
|
+
if response.status_code != 200:
|
|
469
|
+
console.print(f"\nā Failed to initialize OAuth: {response.text}", style="red")
|
|
470
|
+
return
|
|
471
|
+
|
|
472
|
+
auth_url = response.json()['auth_url']
|
|
473
|
+
|
|
474
|
+
# Open browser
|
|
475
|
+
console.print(f"\nš Opening browser for Microsoft authentication...")
|
|
476
|
+
console.print(f" URL: {auth_url}\n", style="dim")
|
|
477
|
+
|
|
478
|
+
webbrowser.open(auth_url)
|
|
479
|
+
|
|
480
|
+
# Poll for completion
|
|
481
|
+
console.print("ā³ Waiting for authorization...", style="yellow")
|
|
482
|
+
console.print(" (Complete the authorization in your browser)\n", style="dim")
|
|
483
|
+
|
|
484
|
+
max_attempts = 60 # 5 minutes (5 second intervals)
|
|
485
|
+
for attempt in range(max_attempts):
|
|
486
|
+
time.sleep(5)
|
|
487
|
+
|
|
488
|
+
status_response = requests.get(f"{api_url}/microsoft/status", headers=headers)
|
|
489
|
+
if status_response.status_code == 200:
|
|
490
|
+
status = status_response.json()
|
|
491
|
+
if status.get('connected'):
|
|
492
|
+
console.print("ā Authorization successful!", style="green")
|
|
493
|
+
break
|
|
494
|
+
else:
|
|
495
|
+
console.print("\nā Authorization timed out", style="red")
|
|
496
|
+
console.print("Please try again with: [bold]co auth microsoft[/bold]\n")
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
# Get credentials
|
|
500
|
+
creds_response = requests.get(f"{api_url}/microsoft/credentials", headers=headers)
|
|
501
|
+
if creds_response.status_code != 200:
|
|
502
|
+
console.print(f"\nā Failed to get credentials: {creds_response.text}", style="red")
|
|
503
|
+
return
|
|
504
|
+
|
|
505
|
+
credentials = creds_response.json()
|
|
506
|
+
|
|
507
|
+
# Save credentials
|
|
508
|
+
console.print("\nš¾ Saving credentials...", style="cyan")
|
|
509
|
+
|
|
510
|
+
# Save to global ~/.co/keys.env
|
|
511
|
+
global_keys_env = Path.home() / ".co" / "keys.env"
|
|
512
|
+
if global_keys_env.exists():
|
|
513
|
+
_save_microsoft_to_env(global_keys_env, credentials)
|
|
514
|
+
console.print(f" ā Saved to {global_keys_env}", style="green")
|
|
515
|
+
|
|
516
|
+
# Save to local .env
|
|
517
|
+
local_env = Path(".env")
|
|
518
|
+
_save_microsoft_to_env(local_env, credentials)
|
|
519
|
+
console.print(f" ā Saved to {local_env.absolute()}", style="green")
|
|
520
|
+
|
|
521
|
+
# Success message
|
|
522
|
+
console.print(f"\nā
[bold green]Microsoft account connected![/bold green]")
|
|
523
|
+
console.print(f" Email: {credentials['microsoft_email']}", style="green")
|
|
524
|
+
console.print(f"\nš§ You can now use Microsoft tools in your agents:")
|
|
525
|
+
console.print(f" [dim]from connectonion import Outlook, MicrosoftCalendar[/dim]")
|
|
526
|
+
console.print(f" [dim]agent = Agent('assistant', tools=[Outlook()])[/dim]\n")
|
|
527
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Execute browser automation commands using Playwright-based browser agent
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [rich.console, cli/browser_agent/browser.execute_browser_command] | imported by [cli/main.py via handle_browser()] | requires Playwright installation | tested by [tests/cli/test_cli_browser.py]
|
|
5
|
+
Data flow: receives command: str from CLI parser (e.g., "screenshot localhost:3000") ā calls execute_browser_command(command) ā browser agent parses command and executes via Playwright ā returns result string ā prints to console via rich.Console
|
|
6
|
+
State/Effects: no persistent state | launches headless browser via Playwright | may create screenshot files in current directory | writes to stdout via rich.Console | browser process lifecycle managed by browser_agent module
|
|
7
|
+
Integration: exposes handle_browser(command) | called from main.py via --browser flag or 'browser' subcommand | delegates to browser_agent/browser.execute_browser_command() | supports commands like "screenshot URL", "navigate URL", "click selector"
|
|
8
|
+
Performance: browser launch overhead (1-3s) | Playwright operations vary by command | screenshot generation is fast (<1s)
|
|
9
|
+
Errors: fails if Playwright not installed | fails if browser launch fails | fails if invalid command syntax | prints error to console but doesn't raise exception
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def handle_browser(command: str):
|
|
18
|
+
"""Execute browser automation commands - guide browser to do something.
|
|
19
|
+
|
|
20
|
+
This is an alternative to the -b flag. Both 'co -b' and 'co browser' are supported.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
command: The browser command to execute
|
|
24
|
+
"""
|
|
25
|
+
from ..browser_agent.browser import execute_browser_command
|
|
26
|
+
result = execute_browser_command(command)
|
|
27
|
+
console.print(result)
|