sudosu 0.1.5__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.
@@ -0,0 +1,318 @@
1
+ """Agent command handlers."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import httpx
8
+
9
+ from sudosu.core import get_project_config_dir
10
+ from sudosu.core.agent_loader import (
11
+ AGENT_TEMPLATES,
12
+ create_agent_template,
13
+ discover_agents,
14
+ load_agent_config,
15
+ )
16
+ from sudosu.core.safety import is_safe_directory
17
+ from sudosu.commands.integrations import get_available_integrations
18
+ from sudosu.ui import (
19
+ console,
20
+ get_user_confirmation,
21
+ get_user_input,
22
+ print_agents,
23
+ print_error,
24
+ print_info,
25
+ print_success,
26
+ print_warning,
27
+ COLOR_PRIMARY,
28
+ COLOR_SECONDARY,
29
+ COLOR_ACCENT,
30
+ COLOR_INTERACTIVE,
31
+ )
32
+
33
+
34
+ # Backend URL for prompt refinement
35
+ BACKEND_URL = os.environ.get("SUDOSU_BACKEND_URL", "http://localhost:8000")
36
+
37
+
38
+ async def refine_prompt_via_backend(
39
+ name: str,
40
+ description: str,
41
+ tools: list[str],
42
+ integrations: list[str] = None,
43
+ ) -> tuple[str, str]:
44
+ """Call the backend to refine an agent's system prompt.
45
+
46
+ Args:
47
+ name: Agent name
48
+ description: Brief description from user
49
+ tools: List of available tools
50
+ integrations: List of connected integrations (gmail, github, etc.)
51
+
52
+ Returns:
53
+ Tuple of (refined_system_prompt, refined_description)
54
+ Returns fallback values if backend is unavailable.
55
+ """
56
+ if integrations is None:
57
+ integrations = []
58
+
59
+ try:
60
+ async with httpx.AsyncClient(timeout=30.0) as client:
61
+ response = await client.post(
62
+ f"{BACKEND_URL}/refine-prompt",
63
+ json={
64
+ "name": name,
65
+ "description": description,
66
+ "tools": tools,
67
+ "integrations": integrations,
68
+ },
69
+ )
70
+
71
+ if response.status_code == 200:
72
+ data = response.json()
73
+ return data["system_prompt"], data["refined_description"]
74
+ else:
75
+ # Backend returned an error, use fallback
76
+ return None, None
77
+
78
+ except Exception:
79
+ # Backend unavailable, return None to signal fallback
80
+ return None, None
81
+
82
+
83
+ def list_agents_command():
84
+ """List all available agents (project-local only)."""
85
+ agents = []
86
+
87
+ # Get agents from project config only (no global agents)
88
+ project_dir = get_project_config_dir()
89
+ if project_dir:
90
+ project_agents_dir = project_dir / "agents"
91
+ if project_agents_dir.exists():
92
+ agents = discover_agents(project_agents_dir)
93
+ # Mark as project-specific with location
94
+ for agent in agents:
95
+ agent["_project"] = True
96
+ agent["_location"] = ".sudosu"
97
+
98
+ print_agents(agents)
99
+
100
+
101
+ def get_available_agents() -> list[dict]:
102
+ """Get list of all available agents in current project."""
103
+ agents = []
104
+ project_dir = get_project_config_dir()
105
+ if project_dir:
106
+ project_agents_dir = project_dir / "agents"
107
+ if project_agents_dir.exists():
108
+ agents = discover_agents(project_agents_dir)
109
+ return agents
110
+
111
+
112
+ async def create_agent_command(name: Optional[str] = None):
113
+ """Create a new agent."""
114
+ # Safety check - block home directory
115
+ cwd = Path.cwd()
116
+ is_safe, reason = is_safe_directory(cwd)
117
+
118
+ if not is_safe:
119
+ print_error(f"Cannot create agents from {reason}")
120
+ print_info("Navigate to a project folder first:")
121
+ console.print("[dim] mkdir ~/my-project && cd ~/my-project[/dim]")
122
+ return
123
+
124
+ # Get agent name
125
+ if not name:
126
+ name = get_user_input("Agent name: ").strip().lower()
127
+
128
+ if not name:
129
+ print_error("Agent name cannot be empty")
130
+ return
131
+
132
+ # Validate name
133
+ if not name.replace("-", "").replace("_", "").isalnum():
134
+ print_error("Agent name can only contain letters, numbers, hyphens, and underscores")
135
+ return
136
+
137
+ # Always use current directory's .sudosu/agents/
138
+ project_sudosu_dir = cwd / ".sudosu"
139
+ agents_dir = project_sudosu_dir / "agents"
140
+
141
+ # Auto-create .sudosu/ if it doesn't exist
142
+ if not project_sudosu_dir.exists():
143
+ project_sudosu_dir.mkdir()
144
+ console.print(f"[{COLOR_INTERACTIVE}]✓[/{COLOR_INTERACTIVE}] Created .sudosu/ in current directory")
145
+
146
+ agents_dir.mkdir(parents=True, exist_ok=True)
147
+
148
+ # Check if agent already exists in this project
149
+ if (agents_dir / name).exists():
150
+ print_error(f"Agent '{name}' already exists in this project")
151
+ return
152
+
153
+ # Fetch connected integrations
154
+ connected_integrations = []
155
+ try:
156
+ integrations_info = await get_available_integrations()
157
+ connected_integrations = integrations_info.get("connected", [])
158
+ if connected_integrations:
159
+ console.print(f"[dim]Found {len(connected_integrations)} connected integration(s): {', '.join(connected_integrations)}[/dim]")
160
+ except Exception:
161
+ pass # Silently continue without integrations
162
+
163
+ # Check if we have a template
164
+ if name in AGENT_TEMPLATES:
165
+ template = AGENT_TEMPLATES[name]
166
+ print_info(f"Using built-in template for '{name}'")
167
+ description = template["description"]
168
+ system_prompt = template["system_prompt"]
169
+ else:
170
+ # Ask for description
171
+ print_info("What should this agent do?")
172
+ description = get_user_input("Description: ").strip()
173
+
174
+ if not description:
175
+ description = f"A helpful assistant named {name}"
176
+
177
+ # Default tools for the agent
178
+ default_tools = ["read_file", "write_file", "list_directory"]
179
+
180
+ # Refine the prompt via backend (include connected integrations)
181
+ console.print("[dim]Crafting agent prompt...[/dim]")
182
+ refined_prompt, refined_description = await refine_prompt_via_backend(
183
+ name=name,
184
+ description=description,
185
+ tools=default_tools,
186
+ integrations=connected_integrations,
187
+ )
188
+
189
+ if refined_prompt:
190
+ # Use the refined prompt from backend
191
+ system_prompt = refined_prompt
192
+ if refined_description:
193
+ description = refined_description
194
+ console.print(f"[{COLOR_INTERACTIVE}]✓[/{COLOR_INTERACTIVE}] Agent prompt refined")
195
+ else:
196
+ # Fallback to basic prompt if backend unavailable
197
+ console.print(f"[{COLOR_ACCENT}]![/{COLOR_ACCENT}] [dim]Using basic prompt (backend unavailable)[/dim]")
198
+
199
+ # Include integrations in fallback prompt if available
200
+ integrations_section = ""
201
+ if connected_integrations:
202
+ integrations_section = f"""
203
+ ## Connected Integrations
204
+
205
+ You have access to these integrations - USE THEM for relevant tasks:
206
+ {chr(10).join(f'- **{i}**: Use for {i}-related tasks' for i in connected_integrations)}
207
+
208
+ **IMPORTANT**: Always use available integrations instead of asking the user to do things manually.
209
+ """
210
+
211
+ system_prompt = f"""# {name.replace('-', ' ').replace('_', ' ').title()} Agent
212
+
213
+ You are {description.lower()}.
214
+
215
+ ## Guidelines
216
+
217
+ 1. Be helpful and accurate
218
+ 2. Ask clarifying questions when needed
219
+ 3. Use markdown formatting for better readability
220
+ 4. Save files when appropriate
221
+
222
+ ## Tools Available
223
+
224
+ - **read_file**: Read content from files
225
+ - **write_file**: Write content to files
226
+ - **list_directory**: List directory contents
227
+ {integrations_section}"""
228
+
229
+ # Create the agent
230
+ try:
231
+ agent_path = create_agent_template(
232
+ agent_dir=agents_dir,
233
+ name=name,
234
+ description=description,
235
+ system_prompt=system_prompt,
236
+ integrations=connected_integrations,
237
+ )
238
+ console.print(f"[{COLOR_INTERACTIVE}]✓[/{COLOR_INTERACTIVE}] Agent [{COLOR_PRIMARY}]'{name}'[/{COLOR_PRIMARY}] created at {agent_path}", highlight=False)
239
+ console.print(f"[{COLOR_INTERACTIVE}]ℹ[/{COLOR_INTERACTIVE}] Use [{COLOR_PRIMARY}]@{name}[/{COLOR_PRIMARY}] to start chatting", highlight=False)
240
+ if connected_integrations:
241
+ console.print(f"[dim]Agent configured with integrations: {', '.join(connected_integrations)}[/dim]")
242
+ except Exception as e:
243
+ print_error(f"Failed to create agent: {e}")
244
+
245
+
246
+ async def delete_agent_command(name: Optional[str] = None):
247
+ """Delete an agent (project-local only)."""
248
+ if not name:
249
+ name = get_user_input("Agent name to delete: ").strip().lower()
250
+
251
+ if not name:
252
+ print_error("Agent name cannot be empty")
253
+ return
254
+
255
+ # Find the agent in project directory only
256
+ project_dir = get_project_config_dir()
257
+ if not project_dir:
258
+ print_error("No .sudosu/ folder found in current directory")
259
+ print_info("You can only delete agents from a project with .sudosu/ folder")
260
+ return
261
+
262
+ agent_path = project_dir / "agents" / name
263
+
264
+ if not agent_path.exists():
265
+ print_error(f"Agent '{name}' not found in this project")
266
+ return
267
+
268
+ # Confirm deletion
269
+ if not get_user_confirmation(f"Delete agent '{name}'?"):
270
+ print_info("Cancelled")
271
+ return
272
+
273
+ # Delete
274
+ import shutil
275
+ try:
276
+ shutil.rmtree(agent_path)
277
+ console.print(f"[{COLOR_INTERACTIVE}]✓[/{COLOR_INTERACTIVE}] Agent [{COLOR_PRIMARY}]'{name}'[/{COLOR_PRIMARY}] deleted", highlight=False)
278
+ except Exception as e:
279
+ print_error(f"Failed to delete agent: {e}")
280
+
281
+
282
+ def get_agent_config(agent_name: str) -> Optional[dict]:
283
+ """Load agent configuration by name (project-local only)."""
284
+ agents_dirs = []
285
+
286
+ # Only check project directory - no global agents
287
+ project_dir = get_project_config_dir()
288
+ if project_dir:
289
+ agents_dirs.append(project_dir / "agents")
290
+
291
+ if not agents_dirs:
292
+ return None
293
+
294
+ return load_agent_config(agent_name, agents_dirs)
295
+
296
+
297
+ async def handle_agent_command(args: list[str]):
298
+ """Handle /agent command with subcommands."""
299
+ if not args:
300
+ list_agents_command()
301
+ return
302
+
303
+ subcommand = args[0].lower()
304
+
305
+ if subcommand == "create":
306
+ name = args[1] if len(args) > 1 else None
307
+ await create_agent_command(name)
308
+
309
+ elif subcommand == "delete":
310
+ name = args[1] if len(args) > 1 else None
311
+ await delete_agent_command(name)
312
+
313
+ elif subcommand == "list":
314
+ list_agents_command()
315
+
316
+ else:
317
+ print_error(f"Unknown subcommand: {subcommand}")
318
+ print_info("Available: create, delete, list")
@@ -0,0 +1,96 @@
1
+ """Config command handler."""
2
+
3
+ from sudosu.core import load_config, set_config_value, get_mode, set_mode, get_backend_url
4
+ from sudosu.ui import (
5
+ console,
6
+ print_error,
7
+ print_info,
8
+ print_success,
9
+ COLOR_PRIMARY,
10
+ COLOR_SECONDARY,
11
+ COLOR_INTERACTIVE,
12
+ )
13
+
14
+
15
+ def show_config():
16
+ """Show current configuration."""
17
+ config = load_config()
18
+ current_mode = get_mode()
19
+ current_url = get_backend_url()
20
+
21
+ console.print("\n[bold]Current Configuration:[/bold]\n")
22
+
23
+ # Show mode first
24
+ console.print(f" [{COLOR_INTERACTIVE}]mode[/{COLOR_INTERACTIVE}]: [bold]{current_mode}[/bold] (active)")
25
+ console.print(f" [{COLOR_INTERACTIVE}]active_backend_url[/{COLOR_INTERACTIVE}]: {current_url}\n")
26
+
27
+ for key, value in config.items():
28
+ # Mask API key
29
+ if "key" in key.lower() and value:
30
+ display_value = value[:4] + "..." + value[-4:] if len(value) > 8 else "****"
31
+ else:
32
+ display_value = value
33
+
34
+ console.print(f" [{COLOR_INTERACTIVE}]{key}[/{COLOR_INTERACTIVE}]: {display_value}")
35
+
36
+ console.print()
37
+ console.print("[dim]Tip: Use '/config mode dev' or '/config mode prod' to switch environments[/dim]\n")
38
+
39
+
40
+ def set_config(key: str, value: str):
41
+ """Set a configuration value."""
42
+ valid_keys = ["backend_url", "dev_backend_url", "prod_backend_url", "api_key", "default_model", "theme"]
43
+
44
+ if key not in valid_keys:
45
+ print_error(f"Invalid key: {key}")
46
+ print_info(f"Valid keys: {', '.join(valid_keys)}")
47
+ return
48
+
49
+ set_config_value(key, value)
50
+
51
+ # Mask display for sensitive values
52
+ if "key" in key.lower():
53
+ display_value = value[:4] + "..." + value[-4:] if len(value) > 8 else "****"
54
+ else:
55
+ display_value = value
56
+
57
+ print_success(f"Set {key} = {display_value}")
58
+
59
+
60
+ def switch_mode(mode: str):
61
+ """Switch between dev and prod modes."""
62
+ mode = mode.lower()
63
+ if mode not in ["dev", "prod"]:
64
+ print_error("Mode must be 'dev' or 'prod'")
65
+ return
66
+
67
+ try:
68
+ set_mode(mode)
69
+ new_url = get_backend_url()
70
+ print_success(f"Switched to {mode.upper()} mode")
71
+ print_info(f"Backend URL: {new_url}")
72
+ except Exception as e:
73
+ print_error(f"Failed to switch mode: {e}")
74
+
75
+
76
+ async def handle_config_command(args: list[str]):
77
+ """Handle /config command with subcommands."""
78
+ if not args:
79
+ show_config()
80
+ return
81
+
82
+ if args[0] == "set":
83
+ if len(args) < 3:
84
+ print_error("Usage: /config set <key> <value>")
85
+ return
86
+ set_config(args[1], " ".join(args[2:]))
87
+ elif args[0] == "mode":
88
+ if len(args) < 2:
89
+ current_mode = get_mode()
90
+ print_info(f"Current mode: {current_mode.upper()}")
91
+ print_info("Usage: /config mode <dev|prod>")
92
+ return
93
+ switch_mode(args[1])
94
+ else:
95
+ print_error(f"Unknown subcommand: {args[0]}")
96
+ print_info("Usage: /config or /config set <key> <value> or /config mode <dev|prod>")
@@ -0,0 +1,73 @@
1
+ """Init command handler."""
2
+
3
+ from pathlib import Path
4
+
5
+ from sudosu.core import ensure_config_structure, ensure_project_structure, get_global_config_dir
6
+ from sudosu.ui import console, print_info, print_success
7
+
8
+
9
+ async def init_command(silent: bool = False):
10
+ """Initialize Sudosu configuration.
11
+
12
+ Args:
13
+ silent: If True, skip all prompts and use defaults (for auto-init)
14
+ """
15
+ config_dir = get_global_config_dir()
16
+
17
+ # Ensure global config exists (just config.yaml)
18
+ ensure_config_structure()
19
+
20
+ if not silent:
21
+ console.print()
22
+ console.print("[bold blue]🚀 Welcome to Sudosu![/bold blue]")
23
+ console.print()
24
+ print_success(f"Global config created at {config_dir}/config.yaml")
25
+ console.print()
26
+ print_info("Run `sudosu` in any project folder to get started!")
27
+ print_info("A .sudosu/ folder with your customizable AGENT.md will be created there.")
28
+ console.print()
29
+
30
+
31
+ def init_project_command():
32
+ """Initialize project-specific Sudosu configuration with AGENT.md."""
33
+ cwd = Path.cwd()
34
+ project_config = cwd / ".sudosu"
35
+
36
+ if project_config.exists():
37
+ print_info(f"Project configuration already exists at {project_config}")
38
+ if not (project_config / "AGENT.md").exists():
39
+ # Create AGENT.md if missing
40
+ ensure_project_structure(cwd)
41
+ print_success("Created .sudosu/AGENT.md")
42
+ return
43
+
44
+ # Create full project structure with AGENT.md
45
+ ensure_project_structure(cwd)
46
+
47
+ # Create context.md template
48
+ context_file = project_config / "context.md"
49
+ if not context_file.exists():
50
+ context_file.write_text("""# Project Context
51
+
52
+ This file provides context about the project to all agents.
53
+
54
+ ## Project Overview
55
+
56
+ Describe your project here...
57
+
58
+ ## Key Files
59
+
60
+ - `src/` - Source code
61
+ - `docs/` - Documentation
62
+
63
+ ## Guidelines
64
+
65
+ Any specific guidelines for agents working in this project...
66
+ """)
67
+
68
+ print_success(f"Created {project_config}/")
69
+ print_success(f"Created {project_config}/AGENT.md (your customizable AI assistant)")
70
+ print_success(f"Created {project_config}/agents/")
71
+ print_success(f"Created {project_config}/context.md")
72
+ print_info("Edit AGENT.md to customize your default AI assistant")
73
+ print_info("Edit context.md to provide project context to all agents")