emdash-cli 0.1.35__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 (30) hide show
  1. emdash_cli/client.py +35 -0
  2. emdash_cli/clipboard.py +30 -61
  3. emdash_cli/commands/agent/__init__.py +14 -0
  4. emdash_cli/commands/agent/cli.py +100 -0
  5. emdash_cli/commands/agent/constants.py +53 -0
  6. emdash_cli/commands/agent/file_utils.py +178 -0
  7. emdash_cli/commands/agent/handlers/__init__.py +41 -0
  8. emdash_cli/commands/agent/handlers/agents.py +421 -0
  9. emdash_cli/commands/agent/handlers/auth.py +69 -0
  10. emdash_cli/commands/agent/handlers/doctor.py +319 -0
  11. emdash_cli/commands/agent/handlers/hooks.py +121 -0
  12. emdash_cli/commands/agent/handlers/mcp.py +183 -0
  13. emdash_cli/commands/agent/handlers/misc.py +200 -0
  14. emdash_cli/commands/agent/handlers/rules.py +394 -0
  15. emdash_cli/commands/agent/handlers/sessions.py +168 -0
  16. emdash_cli/commands/agent/handlers/setup.py +582 -0
  17. emdash_cli/commands/agent/handlers/skills.py +440 -0
  18. emdash_cli/commands/agent/handlers/todos.py +98 -0
  19. emdash_cli/commands/agent/handlers/verify.py +648 -0
  20. emdash_cli/commands/agent/interactive.py +657 -0
  21. emdash_cli/commands/agent/menus.py +728 -0
  22. emdash_cli/commands/agent.py +7 -1321
  23. emdash_cli/commands/server.py +99 -40
  24. emdash_cli/server_manager.py +70 -10
  25. emdash_cli/sse_renderer.py +36 -5
  26. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.46.dist-info}/METADATA +2 -4
  27. emdash_cli-0.1.46.dist-info/RECORD +49 -0
  28. emdash_cli-0.1.35.dist-info/RECORD +0 -30
  29. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.46.dist-info}/WHEEL +0 -0
  30. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.46.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,200 @@
1
+ """Handlers for miscellaneous slash commands."""
2
+
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+
6
+ from rich.console import Console
7
+
8
+ console = Console()
9
+
10
+
11
+ def handle_status(client) -> None:
12
+ """Handle /status command.
13
+
14
+ Args:
15
+ client: EmdashClient instance
16
+ """
17
+ console.print("\n[bold cyan]Status[/bold cyan]\n")
18
+
19
+ # Index status
20
+ console.print("[bold]Index Status[/bold]")
21
+ try:
22
+ status = client.index_status(str(Path.cwd()))
23
+ is_indexed = status.get("is_indexed", False)
24
+ console.print(f" Indexed: {'[green]Yes[/green]' if is_indexed else '[yellow]No[/yellow]'}")
25
+
26
+ if is_indexed:
27
+ console.print(f" Files: {status.get('file_count', 0)}")
28
+ console.print(f" Functions: {status.get('function_count', 0)}")
29
+ console.print(f" Classes: {status.get('class_count', 0)}")
30
+ console.print(f" Communities: {status.get('community_count', 0)}")
31
+ if status.get("last_indexed"):
32
+ console.print(f" Last indexed: {status.get('last_indexed')}")
33
+ if status.get("last_commit"):
34
+ console.print(f" Last commit: {status.get('last_commit')}")
35
+ except Exception as e:
36
+ console.print(f" [red]Error fetching index status: {e}[/red]")
37
+
38
+ console.print()
39
+
40
+ # PROJECT.md status
41
+ console.print("[bold]PROJECT.md Status[/bold]")
42
+ projectmd_path = Path.cwd() / "PROJECT.md"
43
+ if projectmd_path.exists():
44
+ stat = projectmd_path.stat()
45
+ modified_time = datetime.fromtimestamp(stat.st_mtime)
46
+ size_kb = stat.st_size / 1024
47
+ console.print(f" Exists: [green]Yes[/green]")
48
+ console.print(f" Path: {projectmd_path}")
49
+ console.print(f" Size: {size_kb:.1f} KB")
50
+ console.print(f" Last modified: {modified_time.strftime('%Y-%m-%d %H:%M:%S')}")
51
+ else:
52
+ console.print(f" Exists: [yellow]No[/yellow]")
53
+ console.print("[dim] Run /projectmd to generate it[/dim]")
54
+
55
+ console.print()
56
+
57
+
58
+ def handle_pr(args: str, run_slash_command_task, client, renderer, model, max_iterations) -> None:
59
+ """Handle /pr command.
60
+
61
+ Args:
62
+ args: PR URL or number
63
+ run_slash_command_task: Function to run slash command tasks
64
+ client: EmdashClient instance
65
+ renderer: SSERenderer instance
66
+ model: Current model
67
+ max_iterations: Max iterations
68
+ """
69
+ if not args:
70
+ console.print("[yellow]Usage: /pr <pr-url-or-number>[/yellow]")
71
+ console.print("[dim]Example: /pr 123 or /pr https://github.com/org/repo/pull/123[/dim]")
72
+ else:
73
+ console.print(f"[cyan]Reviewing PR: {args}[/cyan]")
74
+ run_slash_command_task(
75
+ client, renderer, model, max_iterations,
76
+ f"Review this pull request and provide feedback: {args}",
77
+ {"mode": "code"}
78
+ )
79
+
80
+
81
+ def handle_projectmd(run_slash_command_task, client, renderer, model, max_iterations) -> None:
82
+ """Handle /projectmd command.
83
+
84
+ Args:
85
+ run_slash_command_task: Function to run slash command tasks
86
+ client: EmdashClient instance
87
+ renderer: SSERenderer instance
88
+ model: Current model
89
+ max_iterations: Max iterations
90
+ """
91
+ console.print("[cyan]Generating PROJECT.md...[/cyan]")
92
+ run_slash_command_task(
93
+ client, renderer, model, max_iterations,
94
+ "Analyze this codebase and generate a comprehensive PROJECT.md file that describes the architecture, main components, how to get started, and key design decisions.",
95
+ {"mode": "code"}
96
+ )
97
+
98
+
99
+ def handle_research(args: str, run_slash_command_task, client, renderer, model) -> None:
100
+ """Handle /research command.
101
+
102
+ Args:
103
+ args: Research goal
104
+ run_slash_command_task: Function to run slash command tasks
105
+ client: EmdashClient instance
106
+ renderer: SSERenderer instance
107
+ model: Current model
108
+ """
109
+ if not args:
110
+ console.print("[yellow]Usage: /research <goal>[/yellow]")
111
+ console.print("[dim]Example: /research How does authentication work in this codebase?[/dim]")
112
+ else:
113
+ console.print(f"[cyan]Researching: {args}[/cyan]")
114
+ run_slash_command_task(
115
+ client, renderer, model, 50, # More iterations for research
116
+ f"Conduct deep research on: {args}\n\nExplore the codebase thoroughly, analyze relevant code, and provide a comprehensive answer with references to specific files and functions.",
117
+ {"mode": "plan"} # Use plan mode for research
118
+ )
119
+
120
+
121
+ def handle_context(renderer) -> None:
122
+ """Handle /context command.
123
+
124
+ Args:
125
+ renderer: SSERenderer instance with _last_context_frame attribute
126
+ """
127
+ context_data = getattr(renderer, '_last_context_frame', None)
128
+ if not context_data:
129
+ console.print("\n[dim]No context frame available yet. Run a query first.[/dim]\n")
130
+ else:
131
+ adding = context_data.get("adding") or {}
132
+ reading = context_data.get("reading") or {}
133
+
134
+ # Get stats
135
+ step_count = adding.get("step_count", 0)
136
+ entities_found = adding.get("entities_found", 0)
137
+ context_tokens = adding.get("context_tokens", 0)
138
+ context_breakdown = adding.get("context_breakdown", {})
139
+
140
+ console.print()
141
+ console.print("[bold cyan]Context Frame[/bold cyan]")
142
+ console.print()
143
+
144
+ # Show total context
145
+ if context_tokens > 0:
146
+ console.print(f"[bold]Total:[/bold] {context_tokens:,} tokens")
147
+
148
+ # Show breakdown
149
+ if context_breakdown:
150
+ console.print(f"\n[bold]Breakdown:[/bold]")
151
+ for key, tokens in context_breakdown.items():
152
+ if tokens > 0:
153
+ console.print(f" {key}: {tokens:,}")
154
+
155
+ # Show stats
156
+ if step_count > 0 or entities_found > 0:
157
+ console.print(f"\n[bold]Stats:[/bold]")
158
+ if step_count > 0:
159
+ console.print(f" Steps: {step_count}")
160
+ if entities_found > 0:
161
+ console.print(f" Entities: {entities_found}")
162
+
163
+ # Show reranking query
164
+ query = reading.get("query")
165
+ if query:
166
+ console.print(f"\n[bold]Reranking Query:[/bold]")
167
+ console.print(f" [yellow]{query}[/yellow]")
168
+
169
+ # Show reranked items
170
+ items = reading.get("items", [])
171
+ if items:
172
+ console.print(f"\n[bold]Reranked Items ({len(items)}):[/bold]")
173
+ for i, item in enumerate(items, 1):
174
+ name = item.get("name", "?")
175
+ item_type = item.get("type", "?")
176
+ score = item.get("score")
177
+ file_path = item.get("file", "")
178
+ description = item.get("description", "")
179
+ touch_count = item.get("touch_count", 0)
180
+ neighbors = item.get("neighbors", [])
181
+
182
+ score_str = f"[cyan]{score:.3f}[/cyan]" if score is not None else "[dim]n/a[/dim]"
183
+ touch_str = f"[magenta]×{touch_count}[/magenta]" if touch_count > 1 else ""
184
+
185
+ console.print(f"\n [bold white]{i}.[/bold white] [dim]{item_type}[/dim] [bold]{name}[/bold]")
186
+ console.print(f" Score: {score_str} {touch_str}")
187
+ if file_path:
188
+ console.print(f" File: [dim]{file_path}[/dim]")
189
+ if description:
190
+ desc_preview = description[:100] + "..." if len(description) > 100 else description
191
+ console.print(f" Desc: [dim]{desc_preview}[/dim]")
192
+ if neighbors:
193
+ console.print(f" Neighbors: [dim]{', '.join(neighbors)}[/dim]")
194
+ else:
195
+ debug_info = reading.get("debug", "")
196
+ if debug_info:
197
+ console.print(f"\n[dim]No reranked items: {debug_info}[/dim]")
198
+ else:
199
+ console.print(f"\n[dim]No reranked items yet. Items appear after exploration (file reads, searches).[/dim]")
200
+ console.print()
@@ -0,0 +1,394 @@
1
+ """Handler for /rules command."""
2
+
3
+ from pathlib import Path
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+
8
+ console = Console()
9
+
10
+
11
+ def get_rules_dir() -> Path:
12
+ """Get the rules directory path."""
13
+ return Path.cwd() / ".emdash" / "rules"
14
+
15
+
16
+ def list_rules() -> list[dict]:
17
+ """List all rules files.
18
+
19
+ Returns:
20
+ List of dicts with name, file_path, and preview
21
+ """
22
+ rules_dir = get_rules_dir()
23
+ rules = []
24
+
25
+ if not rules_dir.exists():
26
+ return rules
27
+
28
+ for md_file in sorted(rules_dir.glob("*.md")):
29
+ try:
30
+ content = md_file.read_text().strip()
31
+ # Get first non-empty line as preview
32
+ lines = [l for l in content.split("\n") if l.strip()]
33
+ preview = lines[0][:60] + "..." if lines and len(lines[0]) > 60 else (lines[0] if lines else "")
34
+ # Remove markdown heading prefix
35
+ if preview.startswith("#"):
36
+ preview = preview.lstrip("#").strip()
37
+
38
+ rules.append({
39
+ "name": md_file.stem,
40
+ "file_path": str(md_file),
41
+ "preview": preview,
42
+ })
43
+ except Exception:
44
+ rules.append({
45
+ "name": md_file.stem,
46
+ "file_path": str(md_file),
47
+ "preview": "(error reading file)",
48
+ })
49
+
50
+ return rules
51
+
52
+
53
+ def show_rules_interactive_menu() -> tuple[str, str]:
54
+ """Show interactive rules menu.
55
+
56
+ Returns:
57
+ Tuple of (action, rule_name) where action is one of:
58
+ - 'view': View rule details
59
+ - 'create': Create new rule
60
+ - 'delete': Delete rule
61
+ - 'cancel': User cancelled
62
+ """
63
+ from prompt_toolkit import Application
64
+ from prompt_toolkit.key_binding import KeyBindings
65
+ from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
66
+ from prompt_toolkit.styles import Style
67
+
68
+ rules = list_rules()
69
+
70
+ # Build menu items: (name, preview, is_action)
71
+ menu_items = []
72
+
73
+ for rule in rules:
74
+ menu_items.append((rule["name"], rule["preview"], False))
75
+
76
+ # Add action items at the bottom
77
+ menu_items.append(("+ Create New Rule", "Create a new rule with AI assistance", True))
78
+
79
+ if not menu_items:
80
+ menu_items.append(("+ Create New Rule", "Create a new rule with AI assistance", True))
81
+
82
+ selected_index = [0]
83
+ result = [("cancel", "")]
84
+
85
+ kb = KeyBindings()
86
+
87
+ @kb.add("up")
88
+ @kb.add("k")
89
+ def move_up(event):
90
+ selected_index[0] = (selected_index[0] - 1) % len(menu_items)
91
+
92
+ @kb.add("down")
93
+ @kb.add("j")
94
+ def move_down(event):
95
+ selected_index[0] = (selected_index[0] + 1) % len(menu_items)
96
+
97
+ @kb.add("enter")
98
+ def select(event):
99
+ item = menu_items[selected_index[0]]
100
+ name, preview, is_action = item
101
+ if is_action:
102
+ if "Create" in name:
103
+ result[0] = ("create", "")
104
+ else:
105
+ result[0] = ("view", name)
106
+ event.app.exit()
107
+
108
+ @kb.add("d")
109
+ def delete_rule(event):
110
+ item = menu_items[selected_index[0]]
111
+ name, preview, is_action = item
112
+ if not is_action:
113
+ result[0] = ("delete", name)
114
+ event.app.exit()
115
+
116
+ @kb.add("n")
117
+ def new_rule(event):
118
+ result[0] = ("create", "")
119
+ event.app.exit()
120
+
121
+ @kb.add("c-c")
122
+ @kb.add("escape")
123
+ @kb.add("q")
124
+ def cancel(event):
125
+ result[0] = ("cancel", "")
126
+ event.app.exit()
127
+
128
+ def get_formatted_menu():
129
+ lines = [("class:title", "Rules\n\n")]
130
+
131
+ if not rules:
132
+ lines.append(("class:dim", "No rules defined yet.\n\n"))
133
+
134
+ for i, (name, preview, is_action) in enumerate(menu_items):
135
+ is_selected = i == selected_index[0]
136
+ prefix = "❯ " if is_selected else " "
137
+
138
+ if is_action:
139
+ if is_selected:
140
+ lines.append(("class:action-selected", f"{prefix}{name}\n"))
141
+ else:
142
+ lines.append(("class:action", f"{prefix}{name}\n"))
143
+ else:
144
+ if is_selected:
145
+ lines.append(("class:rule-selected", f"{prefix}{name}"))
146
+ lines.append(("class:preview-selected", f" - {preview}\n"))
147
+ else:
148
+ lines.append(("class:rule", f"{prefix}{name}"))
149
+ lines.append(("class:preview", f" - {preview}\n"))
150
+
151
+ lines.append(("class:hint", "\n↑/↓ navigate • Enter view • n new • d delete • q quit"))
152
+ return lines
153
+
154
+ style = Style.from_dict({
155
+ "title": "#00ccff bold",
156
+ "dim": "#666666",
157
+ "rule": "#00ccff",
158
+ "rule-selected": "#00cc66 bold",
159
+ "action": "#ffcc00",
160
+ "action-selected": "#ffcc00 bold",
161
+ "preview": "#666666",
162
+ "preview-selected": "#00cc66",
163
+ "hint": "#888888 italic",
164
+ })
165
+
166
+ height = len(menu_items) + 5 # items + title + hint + padding
167
+
168
+ layout = Layout(
169
+ HSplit([
170
+ Window(
171
+ FormattedTextControl(get_formatted_menu),
172
+ height=height,
173
+ ),
174
+ ])
175
+ )
176
+
177
+ app = Application(
178
+ layout=layout,
179
+ key_bindings=kb,
180
+ style=style,
181
+ full_screen=False,
182
+ )
183
+
184
+ console.print()
185
+
186
+ try:
187
+ app.run()
188
+ except (KeyboardInterrupt, EOFError):
189
+ result[0] = ("cancel", "")
190
+
191
+ # Clear menu visually with separator
192
+ console.print()
193
+
194
+ return result[0]
195
+
196
+
197
+ def show_rule_details(name: str) -> None:
198
+ """Show detailed view of a rule."""
199
+ rules_dir = get_rules_dir()
200
+ rule_file = rules_dir / f"{name}.md"
201
+
202
+ console.print()
203
+ console.print("[dim]─" * 50 + "[/dim]")
204
+ console.print()
205
+
206
+ if rule_file.exists():
207
+ try:
208
+ content = rule_file.read_text()
209
+ console.print(f"[bold cyan]{name}[/bold cyan]\n")
210
+ console.print(f"[bold]File:[/bold] {rule_file}\n")
211
+ console.print(Panel(content, border_style="dim"))
212
+ except Exception as e:
213
+ console.print(f"[red]Error reading rule: {e}[/red]")
214
+ else:
215
+ console.print(f"[yellow]Rule '{name}' not found[/yellow]")
216
+
217
+ console.print()
218
+ console.print("[dim]─" * 50 + "[/dim]")
219
+
220
+
221
+ def confirm_delete(rule_name: str) -> bool:
222
+ """Confirm rule deletion."""
223
+ from prompt_toolkit import PromptSession
224
+
225
+ console.print()
226
+ console.print(f"[yellow]Delete rule '{rule_name}'?[/yellow]")
227
+ console.print("[dim]This will remove the rule file. Type 'yes' to confirm.[/dim]")
228
+
229
+ try:
230
+ session = PromptSession()
231
+ response = session.prompt("Confirm > ").strip().lower()
232
+ return response in ("yes", "y")
233
+ except (KeyboardInterrupt, EOFError):
234
+ return False
235
+
236
+
237
+ def delete_rule(name: str) -> bool:
238
+ """Delete a rule file."""
239
+ rules_dir = get_rules_dir()
240
+ rule_file = rules_dir / f"{name}.md"
241
+
242
+ if not rule_file.exists():
243
+ console.print(f"[yellow]Rule file not found: {rule_file}[/yellow]")
244
+ return False
245
+
246
+ if confirm_delete(name):
247
+ rule_file.unlink()
248
+ console.print(f"[green]Deleted rule: {name}[/green]")
249
+ return True
250
+ else:
251
+ console.print("[dim]Cancelled[/dim]")
252
+ return False
253
+
254
+
255
+ def chat_create_rule(client, renderer, model, max_iterations, render_with_interrupt) -> None:
256
+ """Start a chat session to create a new rule with AI assistance."""
257
+ from prompt_toolkit import PromptSession
258
+ from prompt_toolkit.styles import Style
259
+
260
+ rules_dir = get_rules_dir()
261
+
262
+ console.print()
263
+ console.print("[bold cyan]Create New Rule[/bold cyan]")
264
+ console.print("[dim]Describe what rule you want to create. The AI will help you write it.[/dim]")
265
+ console.print("[dim]Type 'done' to finish, Ctrl+C to cancel[/dim]")
266
+ console.print()
267
+
268
+ chat_style = Style.from_dict({
269
+ "prompt": "#00cc66 bold",
270
+ })
271
+
272
+ ps = PromptSession(style=chat_style)
273
+ chat_session_id = None
274
+ first_message = True
275
+
276
+ # Ensure rules directory exists
277
+ rules_dir.mkdir(parents=True, exist_ok=True)
278
+
279
+ # Chat loop
280
+ while True:
281
+ try:
282
+ user_input = ps.prompt([("class:prompt", "› ")]).strip()
283
+
284
+ if not user_input:
285
+ continue
286
+
287
+ if user_input.lower() in ("done", "quit", "exit", "q"):
288
+ console.print("[dim]Finished[/dim]")
289
+ break
290
+
291
+ # First message includes context about rules
292
+ if first_message:
293
+ message_with_context = f"""I want to create a new rule file for my project.
294
+
295
+ **Rules directory:** `{rules_dir}`
296
+
297
+ Rules are markdown files that define guidelines for the AI agent. They are stored in `.emdash/rules/` and get injected into the agent's system prompt.
298
+
299
+ Example rule file:
300
+ ```markdown
301
+ # Code Style Guidelines
302
+
303
+ - Use meaningful variable names
304
+ - Keep functions small and focused
305
+ - Add comments for complex logic
306
+ ```
307
+
308
+ **My request:** {user_input}
309
+
310
+ Please help me create a rule file. Ask me questions if needed to understand what rules I want, then use the Write tool to create the file at `{rules_dir}/<rule-name>.md`."""
311
+ stream = client.agent_chat_stream(
312
+ message=message_with_context,
313
+ model=model,
314
+ max_iterations=max_iterations,
315
+ options={"mode": "code"},
316
+ )
317
+ first_message = False
318
+ elif chat_session_id:
319
+ stream = client.agent_continue_stream(
320
+ chat_session_id, user_input
321
+ )
322
+ else:
323
+ stream = client.agent_chat_stream(
324
+ message=user_input,
325
+ model=model,
326
+ max_iterations=max_iterations,
327
+ options={"mode": "code"},
328
+ )
329
+
330
+ result = render_with_interrupt(renderer, stream)
331
+ if result and result.get("session_id"):
332
+ chat_session_id = result["session_id"]
333
+
334
+ except (KeyboardInterrupt, EOFError):
335
+ console.print()
336
+ console.print("[dim]Cancelled[/dim]")
337
+ break
338
+ except Exception as e:
339
+ console.print(f"[red]Error: {e}[/red]")
340
+
341
+
342
+ def handle_rules(args: str, client, renderer, model, max_iterations, render_with_interrupt) -> None:
343
+ """Handle /rules command."""
344
+ from prompt_toolkit import PromptSession
345
+
346
+ # Handle subcommands
347
+ if args:
348
+ subparts = args.split(maxsplit=1)
349
+ subcommand = subparts[0].lower()
350
+ subargs = subparts[1] if len(subparts) > 1 else ""
351
+
352
+ if subcommand == "list":
353
+ rules = list_rules()
354
+ if rules:
355
+ console.print("\n[bold cyan]Rules[/bold cyan]\n")
356
+ for rule in rules:
357
+ console.print(f" [cyan]{rule['name']}[/cyan] - {rule['preview']}")
358
+ console.print()
359
+ else:
360
+ console.print("\n[dim]No rules defined yet.[/dim]")
361
+ console.print(f"[dim]Rules directory: {get_rules_dir()}[/dim]\n")
362
+ elif subcommand == "show" and subargs:
363
+ show_rule_details(subargs.strip())
364
+ elif subcommand == "delete" and subargs:
365
+ delete_rule(subargs.strip())
366
+ elif subcommand == "add" or subcommand == "create" or subcommand == "new":
367
+ chat_create_rule(client, renderer, model, max_iterations, render_with_interrupt)
368
+ else:
369
+ console.print("[yellow]Usage: /rules [list|show|add|delete] [name][/yellow]")
370
+ console.print("[dim]Or just /rules for interactive menu[/dim]")
371
+ else:
372
+ # Interactive menu
373
+ while True:
374
+ action, rule_name = show_rules_interactive_menu()
375
+
376
+ if action == "cancel":
377
+ break
378
+ elif action == "view":
379
+ show_rule_details(rule_name)
380
+ # After viewing, show options
381
+ try:
382
+ console.print("[dim]'d' delete • Enter back[/dim]", end="")
383
+ ps = PromptSession()
384
+ resp = ps.prompt(" ").strip().lower()
385
+ if resp == 'd':
386
+ delete_rule(rule_name)
387
+ console.print()
388
+ except (KeyboardInterrupt, EOFError):
389
+ break
390
+ elif action == "create":
391
+ chat_create_rule(client, renderer, model, max_iterations, render_with_interrupt)
392
+ # Refresh menu after creating
393
+ elif action == "delete":
394
+ delete_rule(rule_name)