emdash-cli 0.1.30__py3-none-any.whl → 0.1.46__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.
- emdash_cli/__init__.py +15 -0
- emdash_cli/client.py +156 -0
- emdash_cli/clipboard.py +30 -61
- emdash_cli/commands/agent/__init__.py +14 -0
- emdash_cli/commands/agent/cli.py +100 -0
- emdash_cli/commands/agent/constants.py +53 -0
- emdash_cli/commands/agent/file_utils.py +178 -0
- emdash_cli/commands/agent/handlers/__init__.py +41 -0
- emdash_cli/commands/agent/handlers/agents.py +421 -0
- emdash_cli/commands/agent/handlers/auth.py +69 -0
- emdash_cli/commands/agent/handlers/doctor.py +319 -0
- emdash_cli/commands/agent/handlers/hooks.py +121 -0
- emdash_cli/commands/agent/handlers/mcp.py +183 -0
- emdash_cli/commands/agent/handlers/misc.py +200 -0
- emdash_cli/commands/agent/handlers/rules.py +394 -0
- emdash_cli/commands/agent/handlers/sessions.py +168 -0
- emdash_cli/commands/agent/handlers/setup.py +582 -0
- emdash_cli/commands/agent/handlers/skills.py +440 -0
- emdash_cli/commands/agent/handlers/todos.py +98 -0
- emdash_cli/commands/agent/handlers/verify.py +648 -0
- emdash_cli/commands/agent/interactive.py +657 -0
- emdash_cli/commands/agent/menus.py +728 -0
- emdash_cli/commands/agent.py +7 -856
- emdash_cli/commands/server.py +99 -40
- emdash_cli/server_manager.py +70 -10
- emdash_cli/session_store.py +321 -0
- emdash_cli/sse_renderer.py +256 -110
- {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.46.dist-info}/METADATA +2 -4
- emdash_cli-0.1.46.dist-info/RECORD +49 -0
- emdash_cli-0.1.30.dist-info/RECORD +0 -29
- {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.46.dist-info}/WHEEL +0 -0
- {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.46.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""File utilities for the agent CLI.
|
|
2
|
+
|
|
3
|
+
Handles @file reference expansion and fuzzy file finding.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import fnmatch
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def fuzzy_find_files(query: str, limit: int = 10) -> list[Path]:
|
|
17
|
+
"""Find files matching a fuzzy query.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
query: File name or partial path to search for
|
|
21
|
+
limit: Maximum number of results
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
List of matching file paths
|
|
25
|
+
"""
|
|
26
|
+
cwd = Path.cwd()
|
|
27
|
+
matches = []
|
|
28
|
+
|
|
29
|
+
# Common directories to skip
|
|
30
|
+
skip_dirs = {'.git', 'node_modules', '__pycache__', '.venv', 'venv', 'dist', 'build', '.emdash'}
|
|
31
|
+
|
|
32
|
+
# Walk the directory tree (more control than glob for skipping dirs)
|
|
33
|
+
for root, dirs, files in os.walk(cwd):
|
|
34
|
+
# Skip unwanted directories
|
|
35
|
+
dirs[:] = [d for d in dirs if d not in skip_dirs and not d.startswith('.')]
|
|
36
|
+
|
|
37
|
+
rel_root = Path(root).relative_to(cwd)
|
|
38
|
+
|
|
39
|
+
for file in files:
|
|
40
|
+
# Skip hidden files
|
|
41
|
+
if file.startswith('.'):
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
rel_path = rel_root / file if str(rel_root) != '.' else Path(file)
|
|
45
|
+
full_path = cwd / rel_path
|
|
46
|
+
|
|
47
|
+
# Check if query matches (case-insensitive)
|
|
48
|
+
path_str = str(rel_path).lower()
|
|
49
|
+
query_lower = query.lower()
|
|
50
|
+
|
|
51
|
+
if query_lower in path_str or fnmatch.fnmatch(path_str, f"*{query_lower}*"):
|
|
52
|
+
matches.append(full_path)
|
|
53
|
+
|
|
54
|
+
if len(matches) >= limit:
|
|
55
|
+
return matches
|
|
56
|
+
|
|
57
|
+
return matches
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def select_file_interactive(matches: list[Path], query: str) -> Path | None:
|
|
61
|
+
"""Show interactive file selection menu.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
matches: List of matching file paths
|
|
65
|
+
query: Original query string
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Selected file path or None if cancelled
|
|
69
|
+
"""
|
|
70
|
+
if not matches:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
if len(matches) == 1:
|
|
74
|
+
return matches[0]
|
|
75
|
+
|
|
76
|
+
cwd = Path.cwd()
|
|
77
|
+
|
|
78
|
+
# Print numbered list
|
|
79
|
+
console.print(f"\n[bold cyan]Select file for @{query}:[/bold cyan]\n")
|
|
80
|
+
|
|
81
|
+
for i, path in enumerate(matches):
|
|
82
|
+
try:
|
|
83
|
+
rel_path = path.relative_to(cwd)
|
|
84
|
+
except ValueError:
|
|
85
|
+
rel_path = path
|
|
86
|
+
console.print(f" [bold]{i + 1}[/bold]) {rel_path}")
|
|
87
|
+
|
|
88
|
+
console.print(f"\n[dim]Enter number (1-{len(matches)}) or press Enter to cancel:[/dim]")
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
from prompt_toolkit import PromptSession
|
|
92
|
+
selection_session = PromptSession()
|
|
93
|
+
choice = selection_session.prompt("").strip()
|
|
94
|
+
if choice.isdigit():
|
|
95
|
+
idx = int(choice) - 1
|
|
96
|
+
if 0 <= idx < len(matches):
|
|
97
|
+
return matches[idx]
|
|
98
|
+
except (KeyboardInterrupt, EOFError):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def expand_file_references(message: str) -> tuple[str, list[str]]:
|
|
105
|
+
"""Expand @file references in a message to include file contents.
|
|
106
|
+
|
|
107
|
+
Supports:
|
|
108
|
+
- @file.txt - exact path (relative or absolute)
|
|
109
|
+
- @utils - fuzzy search for files containing "utils"
|
|
110
|
+
- @~/path/file.txt - home directory paths
|
|
111
|
+
|
|
112
|
+
Shows interactive selection if multiple files match.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
message: User message potentially containing @file references
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Tuple of (expanded_message, list_of_included_files)
|
|
119
|
+
"""
|
|
120
|
+
# Pattern to match @word (not followed by space immediately, at least 2 chars)
|
|
121
|
+
pattern = r'@([^\s@]{2,})'
|
|
122
|
+
|
|
123
|
+
files_included = []
|
|
124
|
+
file_contents = []
|
|
125
|
+
replacements = {} # Store replacements to apply after iteration
|
|
126
|
+
|
|
127
|
+
# Find all @references
|
|
128
|
+
for match in re.finditer(pattern, message):
|
|
129
|
+
file_query = match.group(1)
|
|
130
|
+
original = match.group(0)
|
|
131
|
+
|
|
132
|
+
# Skip if already processed
|
|
133
|
+
if original in replacements:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
# Expand ~ to home directory
|
|
137
|
+
if file_query.startswith("~"):
|
|
138
|
+
file_query_expanded = os.path.expanduser(file_query)
|
|
139
|
+
else:
|
|
140
|
+
file_query_expanded = file_query
|
|
141
|
+
|
|
142
|
+
# Check if it's an exact path first
|
|
143
|
+
path = Path(file_query_expanded)
|
|
144
|
+
if not path.is_absolute():
|
|
145
|
+
path = Path.cwd() / path
|
|
146
|
+
|
|
147
|
+
resolved_path = None
|
|
148
|
+
|
|
149
|
+
if path.exists() and path.is_file():
|
|
150
|
+
# Exact match
|
|
151
|
+
resolved_path = path
|
|
152
|
+
else:
|
|
153
|
+
# Fuzzy search
|
|
154
|
+
matches = fuzzy_find_files(file_query)
|
|
155
|
+
if matches:
|
|
156
|
+
resolved_path = select_file_interactive(matches, file_query)
|
|
157
|
+
|
|
158
|
+
if resolved_path:
|
|
159
|
+
try:
|
|
160
|
+
content = resolved_path.read_text()
|
|
161
|
+
files_included.append(str(resolved_path))
|
|
162
|
+
file_contents.append(f"\n\n**File: {resolved_path.name}**\n```\n{content}\n```")
|
|
163
|
+
replacements[original] = "" # Remove the @reference
|
|
164
|
+
except Exception:
|
|
165
|
+
pass # Can't read file, leave as-is
|
|
166
|
+
|
|
167
|
+
# Apply replacements
|
|
168
|
+
expanded_message = message
|
|
169
|
+
for original, replacement in replacements.items():
|
|
170
|
+
expanded_message = expanded_message.replace(original, replacement)
|
|
171
|
+
|
|
172
|
+
expanded_message = expanded_message.strip()
|
|
173
|
+
|
|
174
|
+
# Append file contents to the message
|
|
175
|
+
if file_contents:
|
|
176
|
+
expanded_message = expanded_message + "\n" + "\n".join(file_contents)
|
|
177
|
+
|
|
178
|
+
return expanded_message, files_included
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Slash command handlers for the agent CLI."""
|
|
2
|
+
|
|
3
|
+
from .agents import handle_agents
|
|
4
|
+
from .sessions import handle_session
|
|
5
|
+
from .todos import handle_todos, handle_todo_add
|
|
6
|
+
from .hooks import handle_hooks
|
|
7
|
+
from .rules import handle_rules
|
|
8
|
+
from .skills import handle_skills
|
|
9
|
+
from .mcp import handle_mcp
|
|
10
|
+
from .auth import handle_auth
|
|
11
|
+
from .doctor import handle_doctor
|
|
12
|
+
from .verify import handle_verify, handle_verify_loop
|
|
13
|
+
from .setup import handle_setup
|
|
14
|
+
from .misc import (
|
|
15
|
+
handle_status,
|
|
16
|
+
handle_pr,
|
|
17
|
+
handle_projectmd,
|
|
18
|
+
handle_research,
|
|
19
|
+
handle_context,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"handle_agents",
|
|
24
|
+
"handle_session",
|
|
25
|
+
"handle_todos",
|
|
26
|
+
"handle_todo_add",
|
|
27
|
+
"handle_hooks",
|
|
28
|
+
"handle_rules",
|
|
29
|
+
"handle_skills",
|
|
30
|
+
"handle_mcp",
|
|
31
|
+
"handle_auth",
|
|
32
|
+
"handle_doctor",
|
|
33
|
+
"handle_verify",
|
|
34
|
+
"handle_verify_loop",
|
|
35
|
+
"handle_setup",
|
|
36
|
+
"handle_status",
|
|
37
|
+
"handle_pr",
|
|
38
|
+
"handle_projectmd",
|
|
39
|
+
"handle_research",
|
|
40
|
+
"handle_context",
|
|
41
|
+
]
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Handler for /agents command."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
|
|
10
|
+
from ..menus import show_agents_interactive_menu, prompt_agent_name, confirm_delete
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_agent(name: str) -> bool:
|
|
16
|
+
"""Create a new agent with the given name."""
|
|
17
|
+
agents_dir = Path.cwd() / ".emdash" / "agents"
|
|
18
|
+
agent_file = agents_dir / f"{name}.md"
|
|
19
|
+
|
|
20
|
+
if agent_file.exists():
|
|
21
|
+
console.print(f"[yellow]Agent '{name}' already exists[/yellow]")
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
|
|
26
|
+
template = f'''---
|
|
27
|
+
description: Custom agent for specific tasks
|
|
28
|
+
tools: [grep, glob, read_file, semantic_search]
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# System Prompt
|
|
32
|
+
|
|
33
|
+
You are a specialized assistant for {name.replace("-", " ")} tasks.
|
|
34
|
+
|
|
35
|
+
## Your Mission
|
|
36
|
+
|
|
37
|
+
Describe what this agent should accomplish:
|
|
38
|
+
- Task 1
|
|
39
|
+
- Task 2
|
|
40
|
+
- Task 3
|
|
41
|
+
|
|
42
|
+
## Approach
|
|
43
|
+
|
|
44
|
+
1. **Step One**
|
|
45
|
+
- Details about the first step
|
|
46
|
+
|
|
47
|
+
2. **Step Two**
|
|
48
|
+
- Details about the second step
|
|
49
|
+
|
|
50
|
+
## Output Format
|
|
51
|
+
|
|
52
|
+
Describe how the agent should format its responses.
|
|
53
|
+
'''
|
|
54
|
+
agent_file.write_text(template)
|
|
55
|
+
console.print(f"[green]Created agent: {name}[/green]")
|
|
56
|
+
console.print(f"[dim]File: {agent_file}[/dim]")
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def show_agent_details(name: str) -> None:
|
|
61
|
+
"""Show detailed view of an agent."""
|
|
62
|
+
from emdash_core.agent.toolkits import get_custom_agent
|
|
63
|
+
|
|
64
|
+
builtin_agents = ["Explore", "Plan"]
|
|
65
|
+
|
|
66
|
+
console.print()
|
|
67
|
+
console.print("[dim]─" * 50 + "[/dim]")
|
|
68
|
+
console.print()
|
|
69
|
+
if name in builtin_agents:
|
|
70
|
+
console.print(f"[bold cyan]{name}[/bold cyan] [dim](built-in)[/dim]\n")
|
|
71
|
+
if name == "Explore":
|
|
72
|
+
console.print("[bold]Description:[/bold] Fast codebase exploration (read-only)")
|
|
73
|
+
console.print("[bold]Tools:[/bold] glob, grep, read_file, list_files, semantic_search")
|
|
74
|
+
elif name == "Plan":
|
|
75
|
+
console.print("[bold]Description:[/bold] Design implementation plans")
|
|
76
|
+
console.print("[bold]Tools:[/bold] glob, grep, read_file, list_files, semantic_search")
|
|
77
|
+
console.print("\n[dim]Built-in agents cannot be edited or deleted.[/dim]")
|
|
78
|
+
else:
|
|
79
|
+
agent = get_custom_agent(name, Path.cwd())
|
|
80
|
+
if agent:
|
|
81
|
+
console.print(f"[bold cyan]{agent.name}[/bold cyan] [dim](custom)[/dim]\n")
|
|
82
|
+
|
|
83
|
+
# Show description
|
|
84
|
+
if agent.description:
|
|
85
|
+
console.print(f"[bold]Description:[/bold] {agent.description}")
|
|
86
|
+
|
|
87
|
+
# Show model
|
|
88
|
+
if agent.model:
|
|
89
|
+
console.print(f"[bold]Model:[/bold] {agent.model}")
|
|
90
|
+
|
|
91
|
+
# Show tools
|
|
92
|
+
if agent.tools:
|
|
93
|
+
console.print(f"[bold]Tools:[/bold] {', '.join(agent.tools)}")
|
|
94
|
+
|
|
95
|
+
# Show MCP servers
|
|
96
|
+
if agent.mcp_servers:
|
|
97
|
+
console.print(f"\n[bold]MCP Servers:[/bold]")
|
|
98
|
+
for server in agent.mcp_servers:
|
|
99
|
+
status = "[green]enabled[/green]" if server.enabled else "[dim]disabled[/dim]"
|
|
100
|
+
console.print(f" [cyan]{server.name}[/cyan] ({status})")
|
|
101
|
+
console.print(f" [dim]{server.command} {' '.join(server.args)}[/dim]")
|
|
102
|
+
|
|
103
|
+
# Show file path
|
|
104
|
+
if agent.file_path:
|
|
105
|
+
console.print(f"\n[bold]File:[/bold] {agent.file_path}")
|
|
106
|
+
|
|
107
|
+
# Show system prompt preview
|
|
108
|
+
if agent.system_prompt:
|
|
109
|
+
console.print(f"\n[bold]System Prompt Preview:[/bold]")
|
|
110
|
+
preview = agent.system_prompt[:300]
|
|
111
|
+
if len(agent.system_prompt) > 300:
|
|
112
|
+
preview += "..."
|
|
113
|
+
console.print(Panel(preview, border_style="dim"))
|
|
114
|
+
else:
|
|
115
|
+
console.print(f"[yellow]Agent '{name}' not found[/yellow]")
|
|
116
|
+
console.print()
|
|
117
|
+
console.print("[dim]─" * 50 + "[/dim]")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def delete_agent(name: str) -> bool:
|
|
121
|
+
"""Delete a custom agent."""
|
|
122
|
+
agents_dir = Path.cwd() / ".emdash" / "agents"
|
|
123
|
+
agent_file = agents_dir / f"{name}.md"
|
|
124
|
+
|
|
125
|
+
if not agent_file.exists():
|
|
126
|
+
console.print(f"[yellow]Agent file not found: {agent_file}[/yellow]")
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
if confirm_delete(name):
|
|
130
|
+
agent_file.unlink()
|
|
131
|
+
console.print(f"[green]Deleted agent: {name}[/green]")
|
|
132
|
+
return True
|
|
133
|
+
else:
|
|
134
|
+
console.print("[dim]Cancelled[/dim]")
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def edit_agent(name: str) -> None:
|
|
139
|
+
"""Open agent file in editor."""
|
|
140
|
+
agents_dir = Path.cwd() / ".emdash" / "agents"
|
|
141
|
+
agent_file = agents_dir / f"{name}.md"
|
|
142
|
+
|
|
143
|
+
if not agent_file.exists():
|
|
144
|
+
console.print(f"[yellow]Agent file not found: {agent_file}[/yellow]")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
# Try to open in editor
|
|
148
|
+
editor = os.environ.get("EDITOR", "")
|
|
149
|
+
if not editor:
|
|
150
|
+
# Try common editors
|
|
151
|
+
for ed in ["code", "vim", "nano", "vi"]:
|
|
152
|
+
try:
|
|
153
|
+
subprocess.run(["which", ed], capture_output=True, check=True)
|
|
154
|
+
editor = ed
|
|
155
|
+
break
|
|
156
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
if editor:
|
|
160
|
+
console.print(f"[dim]Opening {agent_file} in {editor}...[/dim]")
|
|
161
|
+
try:
|
|
162
|
+
subprocess.run([editor, str(agent_file)])
|
|
163
|
+
except Exception as e:
|
|
164
|
+
console.print(f"[red]Failed to open editor: {e}[/red]")
|
|
165
|
+
console.print(f"[dim]Edit manually: {agent_file}[/dim]")
|
|
166
|
+
else:
|
|
167
|
+
console.print(f"[yellow]No editor found. Edit manually:[/yellow]")
|
|
168
|
+
console.print(f" {agent_file}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def chat_edit_agent(name: str, client, renderer, model, max_iterations, render_with_interrupt) -> None:
|
|
172
|
+
"""Start a chat session to edit an agent with AI assistance."""
|
|
173
|
+
from prompt_toolkit import PromptSession
|
|
174
|
+
from prompt_toolkit.styles import Style
|
|
175
|
+
|
|
176
|
+
agents_dir = Path.cwd() / ".emdash" / "agents"
|
|
177
|
+
agent_file = agents_dir / f"{name}.md"
|
|
178
|
+
|
|
179
|
+
if not agent_file.exists():
|
|
180
|
+
console.print(f"[yellow]Agent file not found: {agent_file}[/yellow]")
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
# Read current content
|
|
184
|
+
content = agent_file.read_text()
|
|
185
|
+
|
|
186
|
+
console.print()
|
|
187
|
+
console.print(f"[bold cyan]Chat: Editing agent '{name}'[/bold cyan]")
|
|
188
|
+
console.print("[dim]What would you like to change? Type 'done' to finish, Ctrl+C to cancel[/dim]")
|
|
189
|
+
console.print()
|
|
190
|
+
|
|
191
|
+
chat_style = Style.from_dict({
|
|
192
|
+
"prompt": "#00cc66 bold",
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
ps = PromptSession(style=chat_style)
|
|
196
|
+
chat_session_id = None
|
|
197
|
+
first_message = True
|
|
198
|
+
|
|
199
|
+
# Chat loop
|
|
200
|
+
while True:
|
|
201
|
+
try:
|
|
202
|
+
user_input = ps.prompt([("class:prompt", "› ")]).strip()
|
|
203
|
+
|
|
204
|
+
if not user_input:
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
if user_input.lower() in ("done", "quit", "exit", "q"):
|
|
208
|
+
console.print("[dim]Finished editing agent[/dim]")
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
# First message includes agent context
|
|
212
|
+
if first_message:
|
|
213
|
+
message_with_context = f"""I want to edit my custom agent "{name}".
|
|
214
|
+
|
|
215
|
+
**File:** `{agent_file}`
|
|
216
|
+
|
|
217
|
+
**Current content:**
|
|
218
|
+
```markdown
|
|
219
|
+
{content}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**My request:** {user_input}
|
|
223
|
+
|
|
224
|
+
Please make the requested changes using the Edit tool."""
|
|
225
|
+
stream = client.agent_chat_stream(
|
|
226
|
+
message=message_with_context,
|
|
227
|
+
model=model,
|
|
228
|
+
max_iterations=max_iterations,
|
|
229
|
+
options={"mode": "code"},
|
|
230
|
+
)
|
|
231
|
+
first_message = False
|
|
232
|
+
elif chat_session_id:
|
|
233
|
+
stream = client.agent_continue_stream(
|
|
234
|
+
chat_session_id, user_input
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
stream = client.agent_chat_stream(
|
|
238
|
+
message=user_input,
|
|
239
|
+
model=model,
|
|
240
|
+
max_iterations=max_iterations,
|
|
241
|
+
options={"mode": "code"},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
result = render_with_interrupt(renderer, stream)
|
|
245
|
+
if result and result.get("session_id"):
|
|
246
|
+
chat_session_id = result["session_id"]
|
|
247
|
+
|
|
248
|
+
except (KeyboardInterrupt, EOFError):
|
|
249
|
+
console.print()
|
|
250
|
+
console.print("[dim]Finished editing agent[/dim]")
|
|
251
|
+
break
|
|
252
|
+
except Exception as e:
|
|
253
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def chat_create_agent(client, renderer, model, max_iterations, render_with_interrupt) -> str | None:
|
|
257
|
+
"""Start a chat session to create a new agent with AI assistance.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
The name of the created agent, or None if cancelled.
|
|
261
|
+
"""
|
|
262
|
+
from prompt_toolkit import PromptSession
|
|
263
|
+
from prompt_toolkit.styles import Style
|
|
264
|
+
|
|
265
|
+
agents_dir = Path.cwd() / ".emdash" / "agents"
|
|
266
|
+
|
|
267
|
+
console.print()
|
|
268
|
+
console.print("[bold cyan]Create New Agent[/bold cyan]")
|
|
269
|
+
console.print("[dim]Describe what agent you want to create. The AI will help you design it.[/dim]")
|
|
270
|
+
console.print("[dim]Type 'done' to finish, Ctrl+C to cancel[/dim]")
|
|
271
|
+
console.print()
|
|
272
|
+
|
|
273
|
+
chat_style = Style.from_dict({
|
|
274
|
+
"prompt": "#00cc66 bold",
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
ps = PromptSession(style=chat_style)
|
|
278
|
+
chat_session_id = None
|
|
279
|
+
first_message = True
|
|
280
|
+
|
|
281
|
+
# Ensure agents directory exists
|
|
282
|
+
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
283
|
+
|
|
284
|
+
# Chat loop
|
|
285
|
+
while True:
|
|
286
|
+
try:
|
|
287
|
+
user_input = ps.prompt([("class:prompt", "› ")]).strip()
|
|
288
|
+
|
|
289
|
+
if not user_input:
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
if user_input.lower() in ("done", "quit", "exit", "q"):
|
|
293
|
+
console.print("[dim]Finished[/dim]")
|
|
294
|
+
break
|
|
295
|
+
|
|
296
|
+
# First message includes context about agents
|
|
297
|
+
if first_message:
|
|
298
|
+
message_with_context = f"""I want to create a new custom agent for my project.
|
|
299
|
+
|
|
300
|
+
**Agents directory:** `{agents_dir}`
|
|
301
|
+
|
|
302
|
+
Agents are markdown files with YAML frontmatter that define specialized assistants with custom system prompts and tools.
|
|
303
|
+
|
|
304
|
+
**Agent file format:**
|
|
305
|
+
```markdown
|
|
306
|
+
---
|
|
307
|
+
description: Brief description of what this agent does
|
|
308
|
+
model: claude-sonnet # optional, defaults to main model
|
|
309
|
+
tools: [grep, glob, read_file, edit_file, bash] # tools this agent can use
|
|
310
|
+
mcp_servers: # optional, MCP servers for this agent
|
|
311
|
+
- name: server-name
|
|
312
|
+
command: npx
|
|
313
|
+
args: ["-y", "@modelcontextprotocol/server-name"]
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
# System Prompt
|
|
317
|
+
|
|
318
|
+
You are a specialized assistant for [purpose].
|
|
319
|
+
|
|
320
|
+
## Your Mission
|
|
321
|
+
[What this agent should accomplish]
|
|
322
|
+
|
|
323
|
+
## Approach
|
|
324
|
+
[How this agent should work]
|
|
325
|
+
|
|
326
|
+
## Output Format
|
|
327
|
+
[How the agent should format responses]
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Available tools:** grep, glob, read_file, edit_file, write_file, bash, semantic_search, list_files, etc.
|
|
331
|
+
|
|
332
|
+
**My request:** {user_input}
|
|
333
|
+
|
|
334
|
+
Please help me design and create an agent. Ask me questions about what I need, then use the Write tool to create the file at `{agents_dir}/<agent-name>.md`."""
|
|
335
|
+
stream = client.agent_chat_stream(
|
|
336
|
+
message=message_with_context,
|
|
337
|
+
model=model,
|
|
338
|
+
max_iterations=max_iterations,
|
|
339
|
+
options={"mode": "code"},
|
|
340
|
+
)
|
|
341
|
+
first_message = False
|
|
342
|
+
elif chat_session_id:
|
|
343
|
+
stream = client.agent_continue_stream(
|
|
344
|
+
chat_session_id, user_input
|
|
345
|
+
)
|
|
346
|
+
else:
|
|
347
|
+
stream = client.agent_chat_stream(
|
|
348
|
+
message=user_input,
|
|
349
|
+
model=model,
|
|
350
|
+
max_iterations=max_iterations,
|
|
351
|
+
options={"mode": "code"},
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
result = render_with_interrupt(renderer, stream)
|
|
355
|
+
if result and result.get("session_id"):
|
|
356
|
+
chat_session_id = result["session_id"]
|
|
357
|
+
|
|
358
|
+
except (KeyboardInterrupt, EOFError):
|
|
359
|
+
console.print()
|
|
360
|
+
console.print("[dim]Cancelled[/dim]")
|
|
361
|
+
break
|
|
362
|
+
except Exception as e:
|
|
363
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
364
|
+
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def handle_agents(args: str, client, renderer, model, max_iterations, render_with_interrupt) -> None:
|
|
369
|
+
"""Handle /agents command."""
|
|
370
|
+
from prompt_toolkit import PromptSession
|
|
371
|
+
|
|
372
|
+
# Handle subcommands for backward compatibility
|
|
373
|
+
if args:
|
|
374
|
+
subparts = args.split(maxsplit=1)
|
|
375
|
+
subcommand = subparts[0].lower()
|
|
376
|
+
subargs = subparts[1] if len(subparts) > 1 else ""
|
|
377
|
+
|
|
378
|
+
if subcommand == "create" and subargs:
|
|
379
|
+
create_agent(subargs.strip().lower().replace(" ", "-"))
|
|
380
|
+
elif subcommand == "show" and subargs:
|
|
381
|
+
show_agent_details(subargs.strip())
|
|
382
|
+
elif subcommand == "delete" and subargs:
|
|
383
|
+
delete_agent(subargs.strip())
|
|
384
|
+
elif subcommand == "edit" and subargs:
|
|
385
|
+
edit_agent(subargs.strip())
|
|
386
|
+
else:
|
|
387
|
+
console.print("[yellow]Usage: /agents [create|show|delete|edit] <name>[/yellow]")
|
|
388
|
+
console.print("[dim]Or just /agents for interactive menu[/dim]")
|
|
389
|
+
else:
|
|
390
|
+
# Interactive menu
|
|
391
|
+
while True:
|
|
392
|
+
action, agent_name = show_agents_interactive_menu()
|
|
393
|
+
|
|
394
|
+
if action == "cancel":
|
|
395
|
+
break
|
|
396
|
+
elif action == "view":
|
|
397
|
+
show_agent_details(agent_name)
|
|
398
|
+
# After viewing, show options based on agent type
|
|
399
|
+
is_custom = agent_name not in ("Explore", "Plan")
|
|
400
|
+
try:
|
|
401
|
+
if is_custom:
|
|
402
|
+
console.print("[dim]'c' chat • 'e' edit • Enter back[/dim]", end="")
|
|
403
|
+
else:
|
|
404
|
+
console.print("[dim]Press Enter to go back...[/dim]", end="")
|
|
405
|
+
ps = PromptSession()
|
|
406
|
+
resp = ps.prompt(" ").strip().lower()
|
|
407
|
+
if is_custom and resp == 'c':
|
|
408
|
+
chat_edit_agent(agent_name, client, renderer, model, max_iterations, render_with_interrupt)
|
|
409
|
+
elif is_custom and resp == 'e':
|
|
410
|
+
edit_agent(agent_name)
|
|
411
|
+
console.print() # Add spacing before menu reappears
|
|
412
|
+
except (KeyboardInterrupt, EOFError):
|
|
413
|
+
break
|
|
414
|
+
elif action == "create":
|
|
415
|
+
# Use AI-assisted creation
|
|
416
|
+
chat_create_agent(client, renderer, model, max_iterations, render_with_interrupt)
|
|
417
|
+
elif action == "delete":
|
|
418
|
+
delete_agent(agent_name)
|
|
419
|
+
elif action == "edit":
|
|
420
|
+
edit_agent(agent_name)
|
|
421
|
+
break # Exit menu after editing
|