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.
- sudosu/__init__.py +3 -0
- sudosu/cli.py +561 -0
- sudosu/commands/__init__.py +15 -0
- sudosu/commands/agent.py +318 -0
- sudosu/commands/config.py +96 -0
- sudosu/commands/init.py +73 -0
- sudosu/commands/integrations.py +563 -0
- sudosu/commands/memory.py +170 -0
- sudosu/commands/onboarding.py +319 -0
- sudosu/commands/tasks.py +635 -0
- sudosu/core/__init__.py +238 -0
- sudosu/core/agent_loader.py +263 -0
- sudosu/core/connection.py +196 -0
- sudosu/core/default_agent.py +541 -0
- sudosu/core/prompt_refiner.py +0 -0
- sudosu/core/safety.py +75 -0
- sudosu/core/session.py +205 -0
- sudosu/tools/__init__.py +373 -0
- sudosu/ui/__init__.py +451 -0
- sudosu-0.1.5.dist-info/METADATA +172 -0
- sudosu-0.1.5.dist-info/RECORD +25 -0
- sudosu-0.1.5.dist-info/WHEEL +5 -0
- sudosu-0.1.5.dist-info/entry_points.txt +2 -0
- sudosu-0.1.5.dist-info/licenses/LICENSE +21 -0
- sudosu-0.1.5.dist-info/top_level.txt +1 -0
sudosu/__init__.py
ADDED
sudosu/cli.py
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
"""Sudosu CLI - Your AI Coworker Platform."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from sudosu.commands.agent import get_agent_config, get_available_agents, handle_agent_command
|
|
13
|
+
from sudosu.commands.config import handle_config_command
|
|
14
|
+
from sudosu.commands.init import init_command, init_project_command
|
|
15
|
+
from sudosu.commands.integrations import (
|
|
16
|
+
get_user_id,
|
|
17
|
+
handle_connect_command,
|
|
18
|
+
handle_disconnect_command,
|
|
19
|
+
handle_integrations_command,
|
|
20
|
+
)
|
|
21
|
+
from sudosu.commands.memory import handle_memory_command
|
|
22
|
+
from sudosu.commands.onboarding import (
|
|
23
|
+
ensure_onboarding,
|
|
24
|
+
get_user_profile,
|
|
25
|
+
handle_profile_command,
|
|
26
|
+
)
|
|
27
|
+
from sudosu.commands.tasks import handle_tasks_command, app as tasks_app
|
|
28
|
+
from sudosu.core import ensure_config_structure, ensure_project_structure, get_backend_url, get_global_config_dir
|
|
29
|
+
from sudosu.core.connection import ConnectionManager
|
|
30
|
+
from sudosu.core.default_agent import get_default_agent_config, load_default_agent_from_file
|
|
31
|
+
from sudosu.core.safety import is_safe_directory, get_safety_warning
|
|
32
|
+
from sudosu.core.session import get_session_manager
|
|
33
|
+
from sudosu.tools import execute_tool
|
|
34
|
+
from sudosu.tools import ROUTING_MARKER
|
|
35
|
+
from sudosu.ui import (
|
|
36
|
+
clear_screen,
|
|
37
|
+
console,
|
|
38
|
+
get_user_input,
|
|
39
|
+
get_user_input_async,
|
|
40
|
+
LiveStreamPrinter,
|
|
41
|
+
print_agent_thinking,
|
|
42
|
+
print_consultation_route,
|
|
43
|
+
print_error,
|
|
44
|
+
print_help,
|
|
45
|
+
print_info,
|
|
46
|
+
print_routing_to_agent,
|
|
47
|
+
print_success,
|
|
48
|
+
print_tool_execution,
|
|
49
|
+
print_tool_result,
|
|
50
|
+
print_welcome,
|
|
51
|
+
StreamPrinter,
|
|
52
|
+
COLOR_PRIMARY,
|
|
53
|
+
COLOR_ACCENT,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
app = typer.Typer(
|
|
57
|
+
name="sudosu",
|
|
58
|
+
help="Your AI Coworker Platform — AI teammates that actually get work done",
|
|
59
|
+
add_completion=False,
|
|
60
|
+
invoke_without_command=True,
|
|
61
|
+
no_args_is_help=False,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Add tasks as a subcommand group
|
|
65
|
+
app.add_typer(tasks_app, name="tasks", help="Manage background tasks")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.callback(invoke_without_command=True)
|
|
69
|
+
def main_callback(
|
|
70
|
+
ctx: typer.Context,
|
|
71
|
+
init: bool = typer.Option(False, "--init", "-i", help="Initialize Sudosu configuration"),
|
|
72
|
+
version: bool = typer.Option(False, "--version", "-v", help="Show version"),
|
|
73
|
+
):
|
|
74
|
+
"""
|
|
75
|
+
Sudosu - Your AI Coworker Platform
|
|
76
|
+
|
|
77
|
+
Get AI coworkers that can read your files, write code, connect to your tools
|
|
78
|
+
(Gmail, Calendar, GitHub, Linear, Slack), and actually get work done.
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
sudosu # Start interactive session
|
|
82
|
+
sudosu --init # Initialize configuration
|
|
83
|
+
sudosu tasks list # List background tasks
|
|
84
|
+
"""
|
|
85
|
+
# If a subcommand was invoked (like 'tasks'), don't run main
|
|
86
|
+
if ctx.invoked_subcommand is not None:
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
if version:
|
|
90
|
+
from sudosu import __version__
|
|
91
|
+
console.print(f"sudosu version {__version__}")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
if init:
|
|
95
|
+
asyncio.run(init_command(silent=False))
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
# Interactive mode (default)
|
|
99
|
+
_run_main_logic(None)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def parse_agent_prompt(text: str) -> tuple[str, str]:
|
|
103
|
+
"""
|
|
104
|
+
Parse @agent_name from the prompt.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Tuple of (agent_name, message)
|
|
108
|
+
"""
|
|
109
|
+
match = re.match(r"@(\w+)\s*(.*)", text, re.DOTALL)
|
|
110
|
+
if match:
|
|
111
|
+
return match.group(1), match.group(2).strip()
|
|
112
|
+
return "", text
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def stream_agent_response(agent_config: dict, message: str, cwd: str, agent_name: str = "agent") -> dict | None:
|
|
116
|
+
"""
|
|
117
|
+
Common function to stream agent response from backend with SHARED thread memory.
|
|
118
|
+
|
|
119
|
+
All agents use the same thread_id to share conversation context.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Routing info dict if agent called route_to_agent or consultation triggered routing, None otherwise
|
|
123
|
+
"""
|
|
124
|
+
backend_url = get_backend_url()
|
|
125
|
+
routing_info = None
|
|
126
|
+
consultation_routing = None
|
|
127
|
+
|
|
128
|
+
# Get session manager for SHARED thread memory
|
|
129
|
+
session_mgr = get_session_manager()
|
|
130
|
+
|
|
131
|
+
# Use shared thread_id, not per-agent thread
|
|
132
|
+
thread_id = session_mgr.get_thread_id()
|
|
133
|
+
session_id = session_mgr.session_id
|
|
134
|
+
|
|
135
|
+
# Get user_id for integration tools (Gmail, etc.)
|
|
136
|
+
user_id = get_user_id()
|
|
137
|
+
|
|
138
|
+
# Increment message count
|
|
139
|
+
session_mgr.increment_message_count()
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
manager = ConnectionManager(backend_url)
|
|
143
|
+
await manager.connect()
|
|
144
|
+
except Exception as e:
|
|
145
|
+
print_error(f"Failed to connect to backend: {e}")
|
|
146
|
+
print_info("Make sure the backend is running")
|
|
147
|
+
print_info(f"Backend URL: {backend_url}")
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
stream_printer = LiveStreamPrinter()
|
|
152
|
+
stream_printer.start()
|
|
153
|
+
|
|
154
|
+
# Define callbacks
|
|
155
|
+
def on_text(content: str):
|
|
156
|
+
stream_printer.print_chunk(content)
|
|
157
|
+
|
|
158
|
+
async def on_tool_call(tool_name: str, args: dict):
|
|
159
|
+
nonlocal routing_info
|
|
160
|
+
|
|
161
|
+
# Special handling for route_to_agent
|
|
162
|
+
if tool_name == "route_to_agent":
|
|
163
|
+
result = await execute_tool(tool_name, args, cwd)
|
|
164
|
+
# Capture routing info for later
|
|
165
|
+
if result.get(ROUTING_MARKER):
|
|
166
|
+
routing_info = {
|
|
167
|
+
"agent_name": result["agent_name"],
|
|
168
|
+
"message": result["message"],
|
|
169
|
+
}
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
# consult_orchestrator is handled by backend - shouldn't reach here
|
|
173
|
+
if tool_name == "consult_orchestrator":
|
|
174
|
+
# Backend handles this, but we need to return something
|
|
175
|
+
return {"output": "Consultation in progress..."}
|
|
176
|
+
|
|
177
|
+
# Normal tool execution
|
|
178
|
+
print_tool_execution(tool_name, args)
|
|
179
|
+
result = await execute_tool(tool_name, args, cwd)
|
|
180
|
+
print_tool_result(tool_name, result)
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
def on_status(status: str):
|
|
184
|
+
console.print(f"[dim]{status}[/dim]")
|
|
185
|
+
|
|
186
|
+
async def on_special_message(msg: dict):
|
|
187
|
+
"""Handle special messages from backend."""
|
|
188
|
+
nonlocal consultation_routing
|
|
189
|
+
|
|
190
|
+
if msg.get("type") == "get_available_agents":
|
|
191
|
+
# Backend is asking for available agents for consultation
|
|
192
|
+
agents = get_available_agents()
|
|
193
|
+
return {
|
|
194
|
+
"type": "available_agents",
|
|
195
|
+
"agents": agents,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
elif msg.get("type") == "consultation_route":
|
|
199
|
+
# Sub-agent consulted sudosu and got a routing decision
|
|
200
|
+
consultation_routing = {
|
|
201
|
+
"from_agent": msg["from_agent"],
|
|
202
|
+
"to_agent": msg["to_agent"],
|
|
203
|
+
"reason": msg["reason"],
|
|
204
|
+
"user_request": msg["user_request"],
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
elif msg.get("type") == "background_queued":
|
|
208
|
+
# Task has been queued for background execution
|
|
209
|
+
task_id = msg.get("task_id", "unknown")
|
|
210
|
+
reason = msg.get("reason", "Complex task")
|
|
211
|
+
stream_printer.flush()
|
|
212
|
+
console.print()
|
|
213
|
+
console.print(f"[bold cyan]📋 Task Queued for Background Execution[/bold cyan]")
|
|
214
|
+
console.print(f"[dim]Task ID:[/dim] [yellow]{task_id}[/yellow]")
|
|
215
|
+
console.print(f"[dim]Reason:[/dim] {reason}")
|
|
216
|
+
console.print()
|
|
217
|
+
console.print("[dim]Use [bold]/tasks status {task_id}[/bold] to check progress[/dim]")
|
|
218
|
+
console.print("[dim]Use [bold]/tasks list[/bold] to see all your background tasks[/dim]")
|
|
219
|
+
console.print()
|
|
220
|
+
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
# Stream response with SHARED thread context for memory
|
|
224
|
+
async for msg in manager.invoke_agent(
|
|
225
|
+
agent_config=agent_config,
|
|
226
|
+
message=message,
|
|
227
|
+
cwd=cwd,
|
|
228
|
+
session_id=session_id,
|
|
229
|
+
thread_id=thread_id,
|
|
230
|
+
user_id=user_id,
|
|
231
|
+
on_text=on_text,
|
|
232
|
+
on_tool_call=on_tool_call,
|
|
233
|
+
on_status=on_status,
|
|
234
|
+
on_special_message=on_special_message,
|
|
235
|
+
):
|
|
236
|
+
if msg.get("type") == "error":
|
|
237
|
+
stream_printer.flush()
|
|
238
|
+
print_error(msg.get("message", "Unknown error"))
|
|
239
|
+
break
|
|
240
|
+
elif msg.get("type") == "done":
|
|
241
|
+
stream_printer.flush()
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
finally:
|
|
245
|
+
await manager.disconnect()
|
|
246
|
+
|
|
247
|
+
# Check for consultation-triggered routing first
|
|
248
|
+
if consultation_routing:
|
|
249
|
+
return {
|
|
250
|
+
"type": "consultation_route",
|
|
251
|
+
**consultation_routing,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return routing_info
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
async def invoke_agent(prompt: str, cwd: str):
|
|
258
|
+
"""Send prompt to backend and stream response for @agent invocation."""
|
|
259
|
+
# Parse @agent_name from prompt
|
|
260
|
+
agent_name, message = parse_agent_prompt(prompt)
|
|
261
|
+
|
|
262
|
+
if not agent_name:
|
|
263
|
+
print_error("Please specify an agent: @agent_name <message>")
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
if not message:
|
|
267
|
+
print_error("Please provide a message")
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
# Load agent config
|
|
271
|
+
agent_config = get_agent_config(agent_name)
|
|
272
|
+
|
|
273
|
+
if not agent_config:
|
|
274
|
+
print_error(f"Agent '{agent_name}' not found")
|
|
275
|
+
print_info("Use /agent to list available agents, or /agent create <name> to create one")
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
# Explicit @agent switches the active agent
|
|
279
|
+
session_mgr = get_session_manager()
|
|
280
|
+
session_mgr.set_active_agent(agent_name, via_routing=False)
|
|
281
|
+
|
|
282
|
+
print_agent_thinking(agent_name)
|
|
283
|
+
await stream_agent_response(agent_config, message, cwd, agent_name)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
async def invoke_active_agent(message: str, cwd: str):
|
|
287
|
+
"""Continue conversation with the currently active agent."""
|
|
288
|
+
session_mgr = get_session_manager()
|
|
289
|
+
agent_name = session_mgr.get_active_agent()
|
|
290
|
+
|
|
291
|
+
agent_config = get_agent_config(agent_name)
|
|
292
|
+
if not agent_config:
|
|
293
|
+
# Fallback to orchestrator if agent not found
|
|
294
|
+
session_mgr.reset_to_orchestrator()
|
|
295
|
+
await invoke_default_agent(message, cwd)
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
print_agent_thinking(agent_name)
|
|
299
|
+
result = await stream_agent_response(agent_config, message, cwd, agent_name)
|
|
300
|
+
|
|
301
|
+
# Handle consultation-triggered routing
|
|
302
|
+
if result and result.get("type") == "consultation_route":
|
|
303
|
+
target_agent = result["to_agent"]
|
|
304
|
+
user_request = result["user_request"]
|
|
305
|
+
reason = result["reason"]
|
|
306
|
+
from_agent = result["from_agent"]
|
|
307
|
+
|
|
308
|
+
# Show the consultation decision
|
|
309
|
+
print_consultation_route(
|
|
310
|
+
from_agent=from_agent,
|
|
311
|
+
to_agent=target_agent,
|
|
312
|
+
reason=reason,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Load target agent and continue
|
|
316
|
+
target_config = get_agent_config(target_agent)
|
|
317
|
+
if target_config:
|
|
318
|
+
session_mgr.set_active_agent(target_agent, via_routing=True)
|
|
319
|
+
print_agent_thinking(target_agent)
|
|
320
|
+
await stream_agent_response(target_config, user_request, cwd, target_agent)
|
|
321
|
+
else:
|
|
322
|
+
print_error(f"Agent '{target_agent}' not found")
|
|
323
|
+
print_info("Available agents:")
|
|
324
|
+
for agent in get_available_agents():
|
|
325
|
+
console.print(f" - @{agent['name']}")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
async def invoke_default_agent(message: str, cwd: str):
|
|
329
|
+
"""Invoke the default Sudosu assistant with intelligent routing.
|
|
330
|
+
|
|
331
|
+
First tries to load from .sudosu/AGENT.md (user-editable).
|
|
332
|
+
Falls back to built-in default if file doesn't exist.
|
|
333
|
+
"""
|
|
334
|
+
# Get available agents to provide context
|
|
335
|
+
available_agents = get_available_agents()
|
|
336
|
+
|
|
337
|
+
# Get user profile for personalization
|
|
338
|
+
user_profile = get_user_profile()
|
|
339
|
+
|
|
340
|
+
# Try to load from .sudosu/AGENT.md first (user-customizable)
|
|
341
|
+
file_config = load_default_agent_from_file(cwd)
|
|
342
|
+
|
|
343
|
+
if file_config:
|
|
344
|
+
# User has customized their AGENT.md - use it
|
|
345
|
+
# But still inject dynamic context (available agents, user profile)
|
|
346
|
+
agent_config = file_config.copy()
|
|
347
|
+
|
|
348
|
+
# Inject available agents into system prompt if placeholder exists
|
|
349
|
+
# or append at the end
|
|
350
|
+
system_prompt = agent_config.get("system_prompt", "")
|
|
351
|
+
|
|
352
|
+
# Add available agents context
|
|
353
|
+
if available_agents:
|
|
354
|
+
agents_text = "\n## Available Agents in This Project\n\n"
|
|
355
|
+
for a in available_agents:
|
|
356
|
+
agents_text += f"- **@{a.get('name')}**: {a.get('description', 'No description')}\n"
|
|
357
|
+
system_prompt = system_prompt + "\n" + agents_text
|
|
358
|
+
|
|
359
|
+
# Add user context
|
|
360
|
+
if user_profile:
|
|
361
|
+
from sudosu.core.default_agent import format_user_context_for_prompt
|
|
362
|
+
user_context = format_user_context_for_prompt(user_profile)
|
|
363
|
+
if user_context:
|
|
364
|
+
system_prompt = user_context + "\n\n" + system_prompt
|
|
365
|
+
|
|
366
|
+
agent_config["system_prompt"] = system_prompt
|
|
367
|
+
else:
|
|
368
|
+
# Fall back to built-in default agent config
|
|
369
|
+
agent_config = get_default_agent_config(available_agents, cwd, user_profile)
|
|
370
|
+
|
|
371
|
+
print_agent_thinking("sudosu")
|
|
372
|
+
routing_info = await stream_agent_response(agent_config, message, cwd, "sudosu")
|
|
373
|
+
|
|
374
|
+
# Check if the default agent decided to route to another agent
|
|
375
|
+
if routing_info:
|
|
376
|
+
target_agent_name = routing_info["agent_name"]
|
|
377
|
+
routed_message = routing_info["message"]
|
|
378
|
+
|
|
379
|
+
print_routing_to_agent(target_agent_name)
|
|
380
|
+
|
|
381
|
+
# Load the target agent config
|
|
382
|
+
target_config = get_agent_config(target_agent_name)
|
|
383
|
+
|
|
384
|
+
if target_config:
|
|
385
|
+
# Mark this agent as active for follow-ups
|
|
386
|
+
session_mgr = get_session_manager()
|
|
387
|
+
session_mgr.set_active_agent(target_agent_name, via_routing=True)
|
|
388
|
+
|
|
389
|
+
print_agent_thinking(target_agent_name)
|
|
390
|
+
await stream_agent_response(target_config, routed_message, cwd, target_agent_name)
|
|
391
|
+
else:
|
|
392
|
+
print_error(f"Agent '{target_agent_name}' not found")
|
|
393
|
+
print_info("Available agents:")
|
|
394
|
+
for agent in available_agents:
|
|
395
|
+
console.print(f" - @{agent['name']}")
|
|
396
|
+
print_info("Create a new agent with /agent create <name>")
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
async def handle_command(command: str):
|
|
400
|
+
"""Handle slash commands."""
|
|
401
|
+
parts = command.split()
|
|
402
|
+
cmd = parts[0].lower()
|
|
403
|
+
args = parts[1:]
|
|
404
|
+
|
|
405
|
+
if cmd == "/help":
|
|
406
|
+
print_help()
|
|
407
|
+
|
|
408
|
+
elif cmd == "/agent":
|
|
409
|
+
await handle_agent_command(args)
|
|
410
|
+
|
|
411
|
+
elif cmd == "/config":
|
|
412
|
+
await handle_config_command(args)
|
|
413
|
+
|
|
414
|
+
elif cmd == "/memory":
|
|
415
|
+
await handle_memory_command(args)
|
|
416
|
+
|
|
417
|
+
elif cmd == "/connect":
|
|
418
|
+
await handle_connect_command(" ".join(args))
|
|
419
|
+
|
|
420
|
+
elif cmd == "/disconnect":
|
|
421
|
+
await handle_disconnect_command(" ".join(args))
|
|
422
|
+
|
|
423
|
+
elif cmd == "/integrations":
|
|
424
|
+
await handle_integrations_command(" ".join(args))
|
|
425
|
+
|
|
426
|
+
elif cmd == "/profile":
|
|
427
|
+
await handle_profile_command(" ".join(args))
|
|
428
|
+
|
|
429
|
+
elif cmd == "/tasks":
|
|
430
|
+
handle_tasks_command(args)
|
|
431
|
+
|
|
432
|
+
elif cmd == "/init":
|
|
433
|
+
if args and args[0] == "project":
|
|
434
|
+
init_project_command()
|
|
435
|
+
else:
|
|
436
|
+
await init_command()
|
|
437
|
+
|
|
438
|
+
elif cmd == "/back":
|
|
439
|
+
# Return from sub-agent to sudosu orchestrator
|
|
440
|
+
session_mgr = get_session_manager()
|
|
441
|
+
old_agent = session_mgr.get_active_agent()
|
|
442
|
+
session_mgr.reset_to_orchestrator()
|
|
443
|
+
if old_agent != "sudosu":
|
|
444
|
+
print_info(f"Returned from @{old_agent} to sudosu")
|
|
445
|
+
else:
|
|
446
|
+
print_info("Already talking to sudosu")
|
|
447
|
+
|
|
448
|
+
elif cmd == "/clear":
|
|
449
|
+
clear_screen()
|
|
450
|
+
|
|
451
|
+
elif cmd == "/quit" or cmd == "/exit":
|
|
452
|
+
raise KeyboardInterrupt
|
|
453
|
+
|
|
454
|
+
else:
|
|
455
|
+
print_error(f"Unknown command: {cmd}")
|
|
456
|
+
print_info("Type /help for available commands")
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
async def interactive_session():
|
|
460
|
+
"""Main interactive loop with active agent routing."""
|
|
461
|
+
# Ensure global config exists (just config.yaml, not project structure)
|
|
462
|
+
config_dir = get_global_config_dir()
|
|
463
|
+
if not (config_dir / "config.yaml").exists():
|
|
464
|
+
# Silent auto-init - just create config with defaults, no prompts
|
|
465
|
+
await init_command(silent=True)
|
|
466
|
+
|
|
467
|
+
# Run onboarding for first-time users (or sync profile from backend)
|
|
468
|
+
user_profile = await ensure_onboarding()
|
|
469
|
+
|
|
470
|
+
cwd = os.getcwd()
|
|
471
|
+
|
|
472
|
+
# Safety check - warn if running from unsafe directory
|
|
473
|
+
is_safe, reason = is_safe_directory(Path(cwd))
|
|
474
|
+
|
|
475
|
+
if not is_safe:
|
|
476
|
+
console.print(get_safety_warning(reason))
|
|
477
|
+
console.print("[dim]Press Enter to exit, or type 'continue' to proceed in read-only mode...[/dim]")
|
|
478
|
+
response = (await get_user_input_async("")).strip().lower()
|
|
479
|
+
if response != "continue":
|
|
480
|
+
console.print(f"[{COLOR_ACCENT}]Exiting. Navigate to a project folder and try again.[/{COLOR_ACCENT}]")
|
|
481
|
+
return
|
|
482
|
+
console.print(f"[{COLOR_ACCENT}]Running in restricted mode. Agent creation and file writes are disabled.[/{COLOR_ACCENT}]\n")
|
|
483
|
+
else:
|
|
484
|
+
# Safe directory - ensure project .sudosu/ structure exists with default AGENT.md
|
|
485
|
+
project_config = Path(cwd) / ".sudosu"
|
|
486
|
+
is_new_project = not project_config.exists()
|
|
487
|
+
ensure_project_structure(Path(cwd))
|
|
488
|
+
|
|
489
|
+
if is_new_project:
|
|
490
|
+
console.print(f"[{COLOR_PRIMARY}]✓[/{COLOR_PRIMARY}] Created .sudosu/ with default AGENT.md")
|
|
491
|
+
console.print("[dim] Edit .sudosu/AGENT.md to customize your AI assistant[/dim]")
|
|
492
|
+
console.print()
|
|
493
|
+
|
|
494
|
+
# Get username for welcome message (prefer profile name, fallback to system user)
|
|
495
|
+
if user_profile and user_profile.get("name"):
|
|
496
|
+
username = user_profile["name"]
|
|
497
|
+
else:
|
|
498
|
+
import getpass
|
|
499
|
+
username = getpass.getuser().capitalize()
|
|
500
|
+
print_welcome(username)
|
|
501
|
+
|
|
502
|
+
# Get session manager for active agent tracking
|
|
503
|
+
session_mgr = get_session_manager()
|
|
504
|
+
|
|
505
|
+
while True:
|
|
506
|
+
try:
|
|
507
|
+
# Show active agent in prompt
|
|
508
|
+
active = session_mgr.get_active_agent()
|
|
509
|
+
if active != "sudosu":
|
|
510
|
+
prompt = f"[@{active}] > "
|
|
511
|
+
else:
|
|
512
|
+
prompt = "> "
|
|
513
|
+
|
|
514
|
+
user_input = (await get_user_input_async(prompt)).strip()
|
|
515
|
+
|
|
516
|
+
if not user_input:
|
|
517
|
+
continue
|
|
518
|
+
|
|
519
|
+
if user_input.startswith("/"):
|
|
520
|
+
await handle_command(user_input)
|
|
521
|
+
|
|
522
|
+
elif user_input.startswith("@"):
|
|
523
|
+
# Check safety for @agent invocation
|
|
524
|
+
if not is_safe:
|
|
525
|
+
print_error("Agent invocation disabled in unsafe directory")
|
|
526
|
+
print_info("Navigate to a project folder to use agents")
|
|
527
|
+
continue
|
|
528
|
+
# Explicit agent switch - user is taking control
|
|
529
|
+
await invoke_agent(user_input, cwd)
|
|
530
|
+
|
|
531
|
+
else:
|
|
532
|
+
# Plain text - route to active agent
|
|
533
|
+
active_agent = session_mgr.get_active_agent()
|
|
534
|
+
|
|
535
|
+
if active_agent == "sudosu":
|
|
536
|
+
# Use orchestrator - it may route to another agent
|
|
537
|
+
await invoke_default_agent(user_input, cwd)
|
|
538
|
+
else:
|
|
539
|
+
# Continue with the active sub-agent
|
|
540
|
+
await invoke_active_agent(user_input, cwd)
|
|
541
|
+
|
|
542
|
+
except KeyboardInterrupt:
|
|
543
|
+
console.print(f"\n[{COLOR_PRIMARY}]Goodbye! 👋[/{COLOR_PRIMARY}]")
|
|
544
|
+
break
|
|
545
|
+
except EOFError:
|
|
546
|
+
break
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _run_main_logic(prompt: Optional[str]):
|
|
550
|
+
"""Execute the main logic for sudosu CLI."""
|
|
551
|
+
if prompt:
|
|
552
|
+
# Direct invocation
|
|
553
|
+
cwd = os.getcwd()
|
|
554
|
+
asyncio.run(invoke_agent(prompt, cwd))
|
|
555
|
+
else:
|
|
556
|
+
# Interactive mode
|
|
557
|
+
asyncio.run(interactive_session())
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
if __name__ == "__main__":
|
|
561
|
+
app()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Command handlers for Sudosu CLI."""
|
|
2
|
+
|
|
3
|
+
from sudosu.commands.integrations import (
|
|
4
|
+
get_user_id,
|
|
5
|
+
handle_connect_command,
|
|
6
|
+
handle_disconnect_command,
|
|
7
|
+
handle_integrations_command,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"get_user_id",
|
|
12
|
+
"handle_connect_command",
|
|
13
|
+
"handle_disconnect_command",
|
|
14
|
+
"handle_integrations_command",
|
|
15
|
+
]
|