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.
Files changed (32) hide show
  1. emdash_cli/__init__.py +15 -0
  2. emdash_cli/client.py +156 -0
  3. emdash_cli/clipboard.py +30 -61
  4. emdash_cli/commands/agent/__init__.py +14 -0
  5. emdash_cli/commands/agent/cli.py +100 -0
  6. emdash_cli/commands/agent/constants.py +53 -0
  7. emdash_cli/commands/agent/file_utils.py +178 -0
  8. emdash_cli/commands/agent/handlers/__init__.py +41 -0
  9. emdash_cli/commands/agent/handlers/agents.py +421 -0
  10. emdash_cli/commands/agent/handlers/auth.py +69 -0
  11. emdash_cli/commands/agent/handlers/doctor.py +319 -0
  12. emdash_cli/commands/agent/handlers/hooks.py +121 -0
  13. emdash_cli/commands/agent/handlers/mcp.py +183 -0
  14. emdash_cli/commands/agent/handlers/misc.py +200 -0
  15. emdash_cli/commands/agent/handlers/rules.py +394 -0
  16. emdash_cli/commands/agent/handlers/sessions.py +168 -0
  17. emdash_cli/commands/agent/handlers/setup.py +582 -0
  18. emdash_cli/commands/agent/handlers/skills.py +440 -0
  19. emdash_cli/commands/agent/handlers/todos.py +98 -0
  20. emdash_cli/commands/agent/handlers/verify.py +648 -0
  21. emdash_cli/commands/agent/interactive.py +657 -0
  22. emdash_cli/commands/agent/menus.py +728 -0
  23. emdash_cli/commands/agent.py +7 -856
  24. emdash_cli/commands/server.py +99 -40
  25. emdash_cli/server_manager.py +70 -10
  26. emdash_cli/session_store.py +321 -0
  27. emdash_cli/sse_renderer.py +256 -110
  28. {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.46.dist-info}/METADATA +2 -4
  29. emdash_cli-0.1.46.dist-info/RECORD +49 -0
  30. emdash_cli-0.1.30.dist-info/RECORD +0 -29
  31. {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.46.dist-info}/WHEEL +0 -0
  32. {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