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
@@ -1,1324 +1,10 @@
1
- """Agent CLI commands."""
1
+ """Agent CLI commands - backward compatibility module.
2
2
 
3
- import os
4
- import threading
3
+ This file re-exports from the refactored agent/ package for backward compatibility.
4
+ The actual implementation is now in agent/ subdirectory.
5
+ """
5
6
 
6
- import click
7
- from enum import Enum
8
- from rich.console import Console
9
- from rich.panel import Panel
10
- from rich.markdown import Markdown
7
+ # Re-export the agent click group and commands
8
+ from .agent import agent, agent_code
11
9
 
12
- from ..client import EmdashClient
13
- from ..keyboard import KeyListener
14
- from ..server_manager import get_server_manager
15
- from ..sse_renderer import SSERenderer
16
-
17
- console = Console()
18
-
19
-
20
- class AgentMode(Enum):
21
- """Agent operation modes."""
22
- PLAN = "plan"
23
- CODE = "code"
24
-
25
-
26
- # Slash commands available in interactive mode
27
- SLASH_COMMANDS = {
28
- # Mode switching
29
- "/plan": "Switch to plan mode (explore codebase, create plans)",
30
- "/code": "Switch to code mode (execute file changes)",
31
- "/mode": "Show current mode",
32
- # Generation commands
33
- "/pr [url]": "Review a pull request",
34
- "/projectmd": "Generate PROJECT.md for the codebase",
35
- "/research [goal]": "Deep research on a topic",
36
- # Status commands
37
- "/status": "Show index and PROJECT.md status",
38
- "/agents": "List, create, or show agents (e.g., /agents create my-agent)",
39
- # Session management
40
- "/session": "Save, load, or list sessions (e.g., /session save my-task)",
41
- "/spec": "Show current specification",
42
- "/reset": "Reset session state",
43
- "/save": "Save current spec to disk",
44
- "/help": "Show available commands",
45
- "/quit": "Exit the agent",
46
- }
47
-
48
-
49
- @click.group()
50
- def agent():
51
- """AI agent commands."""
52
- pass
53
-
54
-
55
- @agent.command("code")
56
- @click.argument("task", required=False)
57
- @click.option("--model", "-m", default=None, help="Model to use")
58
- @click.option("--mode", type=click.Choice(["plan", "code"]), default="code",
59
- help="Starting mode")
60
- @click.option("--quiet", "-q", is_flag=True, help="Less verbose output")
61
- @click.option("--max-iterations", default=int(os.getenv("EMDASH_MAX_ITERATIONS", "100")), help="Max agent iterations")
62
- @click.option("--no-graph-tools", is_flag=True, help="Skip graph exploration tools")
63
- @click.option("--save", is_flag=True, help="Save specs to specs/<feature>/")
64
- def agent_code(
65
- task: str | None,
66
- model: str | None,
67
- mode: str,
68
- quiet: bool,
69
- max_iterations: int,
70
- no_graph_tools: bool,
71
- save: bool,
72
- ):
73
- """Start the coding agent.
74
-
75
- With TASK: Run single task and exit
76
- Without TASK: Start interactive REPL mode
77
-
78
- MODES:
79
- plan - Explore codebase and create plans (read-only)
80
- code - Execute code changes (default)
81
-
82
- SLASH COMMANDS (in interactive mode):
83
- /plan - Switch to plan mode
84
- /code - Switch to code mode
85
- /help - Show available commands
86
- /reset - Reset session
87
-
88
- Examples:
89
- emdash # Interactive code mode
90
- emdash agent code # Same as above
91
- emdash agent code --mode plan # Start in plan mode
92
- emdash agent code "Fix the login bug" # Single task
93
- """
94
- # Get server URL (starts server if needed)
95
- server = get_server_manager()
96
- base_url = server.get_server_url()
97
-
98
- client = EmdashClient(base_url)
99
- renderer = SSERenderer(console=console, verbose=not quiet)
100
-
101
- options = {
102
- "mode": mode,
103
- "no_graph_tools": no_graph_tools,
104
- "save": save,
105
- }
106
-
107
- if task:
108
- # Single task mode
109
- _run_single_task(client, renderer, task, model, max_iterations, options)
110
- else:
111
- # Interactive REPL mode
112
- _run_interactive(client, renderer, model, max_iterations, options)
113
-
114
-
115
- def _get_clarification_response(clarification: dict) -> str | None:
116
- """Get user response for clarification with interactive selection.
117
-
118
- Args:
119
- clarification: Dict with question, context, and options
120
-
121
- Returns:
122
- User's selected option or typed response, or None if cancelled
123
- """
124
- from prompt_toolkit import Application, PromptSession
125
- from prompt_toolkit.key_binding import KeyBindings
126
- from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
127
- from prompt_toolkit.styles import Style
128
-
129
- options = clarification.get("options", [])
130
-
131
- if not options:
132
- # No options, just get free-form input
133
- session = PromptSession()
134
- try:
135
- return session.prompt("response > ").strip() or None
136
- except (KeyboardInterrupt, EOFError):
137
- return None
138
-
139
- selected_index = [0]
140
- result = [None]
141
-
142
- # Key bindings
143
- kb = KeyBindings()
144
-
145
- @kb.add("up")
146
- @kb.add("k")
147
- def move_up(event):
148
- selected_index[0] = (selected_index[0] - 1) % len(options)
149
-
150
- @kb.add("down")
151
- @kb.add("j")
152
- def move_down(event):
153
- selected_index[0] = (selected_index[0] + 1) % len(options)
154
-
155
- @kb.add("enter")
156
- def select(event):
157
- result[0] = options[selected_index[0]]
158
- event.app.exit()
159
-
160
- # Number key shortcuts (1-9)
161
- for i in range(min(9, len(options))):
162
- @kb.add(str(i + 1))
163
- def select_by_number(event, idx=i):
164
- result[0] = options[idx]
165
- event.app.exit()
166
-
167
- @kb.add("c-c")
168
- @kb.add("escape")
169
- def cancel(event):
170
- result[0] = None
171
- event.app.exit()
172
-
173
- @kb.add("o") # 'o' for Other - custom input
174
- def other_input(event):
175
- result[0] = "OTHER_INPUT"
176
- event.app.exit()
177
-
178
- def get_formatted_options():
179
- lines = []
180
- for i, opt in enumerate(options):
181
- if i == selected_index[0]:
182
- lines.append(("class:selected", f" ❯ [{i+1}] {opt}\n"))
183
- else:
184
- lines.append(("class:option", f" [{i+1}] {opt}\n"))
185
- lines.append(("class:hint", "\n↑/↓ to move, Enter to select, 1-9 for quick select, o for other"))
186
- return lines
187
-
188
- # Style
189
- style = Style.from_dict({
190
- "selected": "#00cc66 bold",
191
- "option": "#888888",
192
- "hint": "#444444 italic",
193
- })
194
-
195
- # Calculate height based on options
196
- height = len(options) + 2 # options + hint line + padding
197
-
198
- # Layout
199
- layout = Layout(
200
- HSplit([
201
- Window(
202
- FormattedTextControl(get_formatted_options),
203
- height=height,
204
- ),
205
- ])
206
- )
207
-
208
- # Application
209
- app = Application(
210
- layout=layout,
211
- key_bindings=kb,
212
- style=style,
213
- full_screen=False,
214
- )
215
-
216
- console.print()
217
-
218
- try:
219
- app.run()
220
- except (KeyboardInterrupt, EOFError):
221
- return None
222
-
223
- # Handle "other" option - get custom input
224
- if result[0] == "OTHER_INPUT":
225
- session = PromptSession()
226
- console.print()
227
- try:
228
- return session.prompt("response > ").strip() or None
229
- except (KeyboardInterrupt, EOFError):
230
- return None
231
-
232
- # Check if selected option is an "other/explain" type that needs text input
233
- if result[0]:
234
- lower_result = result[0].lower()
235
- needs_input = any(phrase in lower_result for phrase in [
236
- "something else",
237
- "other",
238
- "i'll explain",
239
- "i will explain",
240
- "let me explain",
241
- "custom",
242
- "none of the above",
243
- ])
244
- if needs_input:
245
- session = PromptSession()
246
- console.print()
247
- console.print("[dim]Please explain:[/dim]")
248
- try:
249
- custom_input = session.prompt("response > ").strip()
250
- if custom_input:
251
- return custom_input
252
- except (KeyboardInterrupt, EOFError):
253
- return None
254
-
255
- return result[0]
256
-
257
-
258
- def _show_plan_approval_menu() -> tuple[str, str]:
259
- """Show plan approval menu with simple approve/reject options.
260
-
261
- Returns:
262
- Tuple of (choice, feedback) where feedback is only set for 'reject'
263
- """
264
- from prompt_toolkit import Application
265
- from prompt_toolkit.key_binding import KeyBindings
266
- from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
267
- from prompt_toolkit.styles import Style
268
-
269
- options = [
270
- ("approve", "Approve and start implementation"),
271
- ("reject", "Reject and provide feedback"),
272
- ]
273
-
274
- selected_index = [0] # Use list to allow mutation in closure
275
- result = [None]
276
-
277
- # Key bindings
278
- kb = KeyBindings()
279
-
280
- @kb.add("up")
281
- @kb.add("k")
282
- def move_up(event):
283
- selected_index[0] = (selected_index[0] - 1) % len(options)
284
-
285
- @kb.add("down")
286
- @kb.add("j")
287
- def move_down(event):
288
- selected_index[0] = (selected_index[0] + 1) % len(options)
289
-
290
- @kb.add("enter")
291
- def select(event):
292
- result[0] = options[selected_index[0]][0]
293
- event.app.exit()
294
-
295
- @kb.add("1")
296
- @kb.add("y")
297
- def select_approve(event):
298
- result[0] = "approve"
299
- event.app.exit()
300
-
301
- @kb.add("2")
302
- @kb.add("n")
303
- def select_reject(event):
304
- result[0] = "reject"
305
- event.app.exit()
306
-
307
- @kb.add("c-c")
308
- @kb.add("q")
309
- @kb.add("escape")
310
- def cancel(event):
311
- result[0] = "reject"
312
- event.app.exit()
313
-
314
- def get_formatted_options():
315
- lines = [("class:title", "Approve this plan?\n\n")]
316
- for i, (key, desc) in enumerate(options):
317
- if i == selected_index[0]:
318
- lines.append(("class:selected", f" ❯ {key:8} "))
319
- lines.append(("class:selected-desc", f"- {desc}\n"))
320
- else:
321
- lines.append(("class:option", f" {key:8} "))
322
- lines.append(("class:desc", f"- {desc}\n"))
323
- lines.append(("class:hint", "\n↑/↓ to move, Enter to select, y/n for quick select"))
324
- return lines
325
-
326
- # Style
327
- style = Style.from_dict({
328
- "title": "#00ccff bold",
329
- "selected": "#00cc66 bold",
330
- "selected-desc": "#00cc66",
331
- "option": "#888888",
332
- "desc": "#666666",
333
- "hint": "#444444 italic",
334
- })
335
-
336
- # Layout
337
- layout = Layout(
338
- HSplit([
339
- Window(
340
- FormattedTextControl(get_formatted_options),
341
- height=6,
342
- ),
343
- ])
344
- )
345
-
346
- # Application
347
- app = Application(
348
- layout=layout,
349
- key_bindings=kb,
350
- style=style,
351
- full_screen=False,
352
- )
353
-
354
- console.print()
355
-
356
- try:
357
- app.run()
358
- except (KeyboardInterrupt, EOFError):
359
- result[0] = "reject"
360
-
361
- choice = result[0] or "reject"
362
-
363
- # Get feedback if reject was chosen
364
- feedback = ""
365
- if choice == "reject":
366
- from prompt_toolkit import PromptSession
367
- console.print()
368
- console.print("[dim]What changes would you like?[/dim]")
369
- try:
370
- session = PromptSession()
371
- feedback = session.prompt("feedback > ").strip()
372
- except (KeyboardInterrupt, EOFError):
373
- return "reject", ""
374
-
375
- return choice, feedback
376
-
377
-
378
- def _show_plan_mode_approval_menu() -> tuple[str, str]:
379
- """Show plan mode entry approval menu.
380
-
381
- Returns:
382
- Tuple of (choice, feedback) where feedback is only set for 'reject'
383
- """
384
- from prompt_toolkit import Application
385
- from prompt_toolkit.key_binding import KeyBindings
386
- from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
387
- from prompt_toolkit.styles import Style
388
-
389
- options = [
390
- ("approve", "Enter plan mode and explore"),
391
- ("reject", "Skip planning, proceed directly"),
392
- ]
393
-
394
- selected_index = [0]
395
- result = [None]
396
-
397
- kb = KeyBindings()
398
-
399
- @kb.add("up")
400
- @kb.add("k")
401
- def move_up(event):
402
- selected_index[0] = (selected_index[0] - 1) % len(options)
403
-
404
- @kb.add("down")
405
- @kb.add("j")
406
- def move_down(event):
407
- selected_index[0] = (selected_index[0] + 1) % len(options)
408
-
409
- @kb.add("enter")
410
- def select(event):
411
- result[0] = options[selected_index[0]][0]
412
- event.app.exit()
413
-
414
- @kb.add("1")
415
- @kb.add("y")
416
- def select_approve(event):
417
- result[0] = "approve"
418
- event.app.exit()
419
-
420
- @kb.add("2")
421
- @kb.add("n")
422
- def select_reject(event):
423
- result[0] = "reject"
424
- event.app.exit()
425
-
426
- @kb.add("c-c")
427
- @kb.add("q")
428
- @kb.add("escape")
429
- def cancel(event):
430
- result[0] = "reject"
431
- event.app.exit()
432
-
433
- def get_formatted_options():
434
- lines = [("class:title", "Enter plan mode?\n\n")]
435
- for i, (key, desc) in enumerate(options):
436
- if i == selected_index[0]:
437
- lines.append(("class:selected", f" ❯ {key:8} "))
438
- lines.append(("class:selected-desc", f"- {desc}\n"))
439
- else:
440
- lines.append(("class:option", f" {key:8} "))
441
- lines.append(("class:desc", f"- {desc}\n"))
442
- lines.append(("class:hint", "\n↑/↓ to move, Enter to select, y/n for quick select"))
443
- return lines
444
-
445
- style = Style.from_dict({
446
- "title": "#ffcc00 bold",
447
- "selected": "#00cc66 bold",
448
- "selected-desc": "#00cc66",
449
- "option": "#888888",
450
- "desc": "#666666",
451
- "hint": "#444444 italic",
452
- })
453
-
454
- layout = Layout(
455
- HSplit([
456
- Window(
457
- FormattedTextControl(get_formatted_options),
458
- height=6,
459
- ),
460
- ])
461
- )
462
-
463
- app = Application(
464
- layout=layout,
465
- key_bindings=kb,
466
- style=style,
467
- full_screen=False,
468
- )
469
-
470
- console.print()
471
-
472
- try:
473
- app.run()
474
- except (KeyboardInterrupt, EOFError):
475
- result[0] = "reject"
476
-
477
- choice = result[0] or "reject"
478
-
479
- feedback = ""
480
- if choice == "reject":
481
- from prompt_toolkit import PromptSession
482
- console.print()
483
- console.print("[dim]Reason for skipping plan mode (optional):[/dim]")
484
- try:
485
- session = PromptSession()
486
- feedback = session.prompt("feedback > ").strip()
487
- except (KeyboardInterrupt, EOFError):
488
- return "reject", ""
489
-
490
- return choice, feedback
491
-
492
-
493
- def _render_with_interrupt(renderer: SSERenderer, stream) -> dict:
494
- """Render stream with ESC key interrupt support.
495
-
496
- Args:
497
- renderer: SSE renderer instance
498
- stream: SSE stream iterator
499
-
500
- Returns:
501
- Result dict from renderer, with 'interrupted' flag
502
- """
503
- interrupt_event = threading.Event()
504
-
505
- def on_escape():
506
- interrupt_event.set()
507
-
508
- listener = KeyListener(on_escape)
509
-
510
- try:
511
- listener.start()
512
- result = renderer.render_stream(stream, interrupt_event=interrupt_event)
513
- return result
514
- finally:
515
- listener.stop()
516
-
517
-
518
- def _run_single_task(
519
- client: EmdashClient,
520
- renderer: SSERenderer,
521
- task: str,
522
- model: str | None,
523
- max_iterations: int,
524
- options: dict,
525
- ):
526
- """Run a single agent task."""
527
- try:
528
- stream = client.agent_chat_stream(
529
- message=task,
530
- model=model,
531
- max_iterations=max_iterations,
532
- options=options,
533
- )
534
- result = _render_with_interrupt(renderer, stream)
535
- if result.get("interrupted"):
536
- console.print("[dim]Task interrupted. You can continue or start a new task.[/dim]")
537
- except Exception as e:
538
- console.print(f"[red]Error: {e}[/red]")
539
- raise click.Abort()
540
-
541
-
542
- def _run_slash_command_task(
543
- client: EmdashClient,
544
- renderer: SSERenderer,
545
- model: str | None,
546
- max_iterations: int,
547
- task: str,
548
- options: dict,
549
- ):
550
- """Run a task from a slash command."""
551
- try:
552
- stream = client.agent_chat_stream(
553
- message=task,
554
- model=model,
555
- max_iterations=max_iterations,
556
- options=options,
557
- )
558
- result = _render_with_interrupt(renderer, stream)
559
- if result.get("interrupted"):
560
- console.print("[dim]Task interrupted.[/dim]")
561
- console.print()
562
- except Exception as e:
563
- console.print(f"[red]Error: {e}[/red]")
564
-
565
-
566
- def _run_interactive(
567
- client: EmdashClient,
568
- renderer: SSERenderer,
569
- model: str | None,
570
- max_iterations: int,
571
- options: dict,
572
- ):
573
- """Run interactive REPL mode with slash commands."""
574
- from prompt_toolkit import PromptSession
575
- from prompt_toolkit.history import FileHistory
576
- from prompt_toolkit.completion import Completer, Completion
577
- from prompt_toolkit.styles import Style
578
- from prompt_toolkit.key_binding import KeyBindings
579
- from pathlib import Path
580
-
581
- # Current mode
582
- current_mode = AgentMode(options.get("mode", "code"))
583
- session_id = None
584
- current_spec = None
585
- # Attached images for next message
586
- attached_images: list[dict] = []
587
- # Loaded messages from saved session (for restoration)
588
- loaded_messages: list[dict] = []
589
-
590
- # Style for prompt
591
- PROMPT_STYLE = Style.from_dict({
592
- "prompt.mode.plan": "#ffcc00 bold",
593
- "prompt.mode.code": "#00cc66 bold",
594
- "prompt.prefix": "#888888",
595
- "prompt.image": "#00ccff",
596
- "completion-menu": "bg:#1a1a2e #ffffff",
597
- "completion-menu.completion": "bg:#1a1a2e #ffffff",
598
- "completion-menu.completion.current": "bg:#4a4a6e #ffffff bold",
599
- "completion-menu.meta.completion": "bg:#1a1a2e #888888",
600
- "completion-menu.meta.completion.current": "bg:#4a4a6e #aaaaaa",
601
- "command": "#00ccff bold",
602
- })
603
-
604
- class SlashCommandCompleter(Completer):
605
- """Completer for slash commands."""
606
-
607
- def get_completions(self, document, complete_event):
608
- text = document.text_before_cursor
609
- if not text.startswith("/"):
610
- return
611
- for cmd, description in SLASH_COMMANDS.items():
612
- # Extract base command (e.g., "/pr" from "/pr [url]")
613
- base_cmd = cmd.split()[0]
614
- if base_cmd.startswith(text):
615
- yield Completion(
616
- base_cmd,
617
- start_position=-len(text),
618
- display=cmd,
619
- display_meta=description,
620
- )
621
-
622
- # Setup history file
623
- history_file = Path.home() / ".emdash" / "cli_history"
624
- history_file.parent.mkdir(parents=True, exist_ok=True)
625
- history = FileHistory(str(history_file))
626
-
627
- # Key bindings: Enter submits, Alt+Enter inserts newline
628
- # Note: Shift+Enter is indistinguishable from Enter in most terminals
629
- kb = KeyBindings()
630
-
631
- @kb.add("enter")
632
- def submit_on_enter(event):
633
- """Submit on Enter."""
634
- event.current_buffer.validate_and_handle()
635
-
636
- @kb.add("escape", "enter") # Alt+Enter (Escape then Enter)
637
- @kb.add("c-j") # Ctrl+J as alternative for newline
638
- def insert_newline_alt(event):
639
- """Insert a newline character with Alt+Enter or Ctrl+J."""
640
- event.current_buffer.insert_text("\n")
641
-
642
- @kb.add("c-v") # Ctrl+V to paste (check for images)
643
- def paste_with_image_check(event):
644
- """Paste text or attach image from clipboard."""
645
- nonlocal attached_images
646
- from ..clipboard import get_clipboard_image, get_image_from_path
647
-
648
- # Try to get image from clipboard
649
- image_data = get_clipboard_image()
650
- if image_data:
651
- base64_data, img_format = image_data
652
- attached_images.append({"data": base64_data, "format": img_format})
653
- # Refresh prompt to show updated image list
654
- event.app.invalidate()
655
- return
656
-
657
- # Check if clipboard contains an image file path
658
- clipboard_data = event.app.clipboard.get_data()
659
- if clipboard_data and clipboard_data.text:
660
- text = clipboard_data.text.strip()
661
- # Remove escape characters from dragged paths (e.g., "path\ with\ spaces")
662
- clean_path = text.replace("\\ ", " ")
663
- # Check if it looks like an image file path
664
- if clean_path.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp')):
665
- image_data = get_image_from_path(clean_path)
666
- if image_data:
667
- base64_data, img_format = image_data
668
- attached_images.append({"data": base64_data, "format": img_format})
669
- event.app.invalidate()
670
- return
671
-
672
- # No image, do normal paste
673
- event.current_buffer.paste_clipboard_data(clipboard_data)
674
-
675
- def check_for_image_path(buff):
676
- """Check if buffer contains an image path and attach it."""
677
- nonlocal attached_images
678
- text = buff.text.strip()
679
- if not text:
680
- return
681
- # Clean escaped spaces from dragged paths
682
- clean_text = text.replace("\\ ", " ")
683
- if clean_text.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp')):
684
- from ..clipboard import get_image_from_path
685
- from prompt_toolkit.application import get_app
686
- image_data = get_image_from_path(clean_text)
687
- if image_data:
688
- base64_data, img_format = image_data
689
- attached_images.append({"data": base64_data, "format": img_format})
690
- # Clear the buffer
691
- buff.text = ""
692
- buff.cursor_position = 0
693
- # Refresh prompt to show image indicator
694
- try:
695
- get_app().invalidate()
696
- except Exception:
697
- pass
698
-
699
- session = PromptSession(
700
- history=history,
701
- completer=SlashCommandCompleter(),
702
- style=PROMPT_STYLE,
703
- complete_while_typing=True,
704
- multiline=True,
705
- prompt_continuation="... ",
706
- key_bindings=kb,
707
- )
708
-
709
- # Watch for image paths being pasted/dropped
710
- session.default_buffer.on_text_changed += check_for_image_path
711
-
712
- def get_prompt():
713
- """Get formatted prompt."""
714
- nonlocal attached_images
715
- parts = []
716
- # Show attached images above prompt
717
- if attached_images:
718
- image_tags = " ".join(f"[Image #{i+1}]" for i in range(len(attached_images)))
719
- parts.append(("class:prompt.image", f" {image_tags}\n"))
720
- parts.append(("class:prompt.prefix", "> "))
721
- return parts
722
-
723
- def show_help():
724
- """Show available commands."""
725
- console.print()
726
- console.print("[bold cyan]Available Commands[/bold cyan]")
727
- console.print()
728
- for cmd, desc in SLASH_COMMANDS.items():
729
- console.print(f" [cyan]{cmd:12}[/cyan] {desc}")
730
- console.print()
731
- console.print("[dim]Type your task or question to interact with the agent.[/dim]")
732
- console.print()
733
-
734
- def handle_slash_command(cmd: str) -> bool:
735
- """Handle a slash command. Returns True if should continue, False to exit."""
736
- nonlocal current_mode, session_id, current_spec
737
-
738
- cmd_parts = cmd.strip().split(maxsplit=1)
739
- command = cmd_parts[0].lower()
740
- args = cmd_parts[1] if len(cmd_parts) > 1 else ""
741
-
742
- if command == "/quit" or command == "/exit" or command == "/q":
743
- return False
744
-
745
- elif command == "/help":
746
- show_help()
747
-
748
- elif command == "/plan":
749
- current_mode = AgentMode.PLAN
750
- # Reset session so next chat creates a new session with plan mode
751
- if session_id:
752
- session_id = None
753
- console.print("[bold green]✓ Plan mode activated[/bold green] [dim](session reset)[/dim]")
754
- else:
755
- console.print("[bold green]✓ Plan mode activated[/bold green]")
756
-
757
- elif command == "/code":
758
- current_mode = AgentMode.CODE
759
- # Reset session so next chat creates a new session with code mode
760
- if session_id:
761
- session_id = None
762
- console.print("[green]Switched to code mode (session reset)[/green]")
763
- else:
764
- console.print("[green]Switched to code mode[/green]")
765
-
766
- elif command == "/mode":
767
- console.print(f"Current mode: [bold]{current_mode.value}[/bold]")
768
-
769
- elif command == "/reset":
770
- session_id = None
771
- current_spec = None
772
- console.print("[dim]Session reset[/dim]")
773
-
774
- elif command == "/spec":
775
- if current_spec:
776
- console.print(Panel(Markdown(current_spec), title="Current Spec"))
777
- else:
778
- console.print("[dim]No spec available. Use plan mode to create one.[/dim]")
779
-
780
- elif command == "/save":
781
- if current_spec:
782
- # TODO: Save spec via API
783
- console.print("[yellow]Save not implemented yet[/yellow]")
784
- else:
785
- console.print("[dim]No spec to save[/dim]")
786
-
787
- elif command == "/pr":
788
- # PR review
789
- if not args:
790
- console.print("[yellow]Usage: /pr <pr-url-or-number>[/yellow]")
791
- console.print("[dim]Example: /pr 123 or /pr https://github.com/org/repo/pull/123[/dim]")
792
- else:
793
- console.print(f"[cyan]Reviewing PR: {args}[/cyan]")
794
- _run_slash_command_task(
795
- client, renderer, model, max_iterations,
796
- f"Review this pull request and provide feedback: {args}",
797
- {"mode": "code"}
798
- )
799
-
800
- elif command == "/projectmd":
801
- # Generate PROJECT.md
802
- console.print("[cyan]Generating PROJECT.md...[/cyan]")
803
- _run_slash_command_task(
804
- client, renderer, model, max_iterations,
805
- "Analyze this codebase and generate a comprehensive PROJECT.md file that describes the architecture, main components, how to get started, and key design decisions.",
806
- {"mode": "code"}
807
- )
808
-
809
- elif command == "/research":
810
- # Deep research
811
- if not args:
812
- console.print("[yellow]Usage: /research <goal>[/yellow]")
813
- console.print("[dim]Example: /research How does authentication work in this codebase?[/dim]")
814
- else:
815
- console.print(f"[cyan]Researching: {args}[/cyan]")
816
- _run_slash_command_task(
817
- client, renderer, model, 50, # More iterations for research
818
- 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.",
819
- {"mode": "plan"} # Use plan mode for research
820
- )
821
-
822
- elif command == "/status":
823
- # Show index and PROJECT.md status
824
- from datetime import datetime
825
-
826
- console.print("\n[bold cyan]Status[/bold cyan]\n")
827
-
828
- # Index status
829
- console.print("[bold]Index Status[/bold]")
830
- try:
831
- status = client.index_status(str(Path.cwd()))
832
- is_indexed = status.get("is_indexed", False)
833
- console.print(f" Indexed: {'[green]Yes[/green]' if is_indexed else '[yellow]No[/yellow]'}")
834
-
835
- if is_indexed:
836
- console.print(f" Files: {status.get('file_count', 0)}")
837
- console.print(f" Functions: {status.get('function_count', 0)}")
838
- console.print(f" Classes: {status.get('class_count', 0)}")
839
- console.print(f" Communities: {status.get('community_count', 0)}")
840
- if status.get("last_indexed"):
841
- console.print(f" Last indexed: {status.get('last_indexed')}")
842
- if status.get("last_commit"):
843
- console.print(f" Last commit: {status.get('last_commit')}")
844
- except Exception as e:
845
- console.print(f" [red]Error fetching index status: {e}[/red]")
846
-
847
- console.print()
848
-
849
- # PROJECT.md status
850
- console.print("[bold]PROJECT.md Status[/bold]")
851
- projectmd_path = Path.cwd() / "PROJECT.md"
852
- if projectmd_path.exists():
853
- stat = projectmd_path.stat()
854
- modified_time = datetime.fromtimestamp(stat.st_mtime)
855
- size_kb = stat.st_size / 1024
856
- console.print(f" Exists: [green]Yes[/green]")
857
- console.print(f" Path: {projectmd_path}")
858
- console.print(f" Size: {size_kb:.1f} KB")
859
- console.print(f" Last modified: {modified_time.strftime('%Y-%m-%d %H:%M:%S')}")
860
- else:
861
- console.print(f" Exists: [yellow]No[/yellow]")
862
- console.print("[dim] Run /projectmd to generate it[/dim]")
863
-
864
- console.print()
865
-
866
- elif command == "/agents":
867
- # Agents management: list, create, show
868
- from emdash_core.agent.toolkits import list_agent_types, get_custom_agent
869
-
870
- # Parse subcommand
871
- subparts = args.split(maxsplit=1) if args else []
872
- subcommand = subparts[0].lower() if subparts else "list"
873
- subargs = subparts[1] if len(subparts) > 1 else ""
874
-
875
- if subcommand in ("list", ""):
876
- # List all agents
877
- console.print("\n[bold cyan]Available Agents[/bold cyan]\n")
878
-
879
- all_agents = list_agent_types(Path.cwd())
880
- builtin = ["Explore", "Plan"]
881
-
882
- console.print("[bold]Built-in Agents[/bold]")
883
- for agent_name in builtin:
884
- if agent_name == "Explore":
885
- console.print(" [green]Explore[/green] - Fast codebase exploration (read-only)")
886
- elif agent_name == "Plan":
887
- console.print(" [green]Plan[/green] - Design implementation plans")
888
-
889
- # Custom agents
890
- custom = [a for a in all_agents if a not in builtin]
891
- if custom:
892
- console.print("\n[bold]Custom Agents[/bold] [dim](.emdash/agents/)[/dim]")
893
- for name in custom:
894
- agent = get_custom_agent(name, Path.cwd())
895
- desc = agent.description if agent else ""
896
- if desc:
897
- console.print(f" [cyan]{name}[/cyan] - {desc}")
898
- else:
899
- console.print(f" [cyan]{name}[/cyan]")
900
- else:
901
- console.print("\n[dim]No custom agents found.[/dim]")
902
- console.print("[dim]Create with: /agents create <name>[/dim]")
903
-
904
- console.print()
905
-
906
- elif subcommand == "create":
907
- # Create a new custom agent
908
- if not subargs:
909
- console.print("[yellow]Usage: /agents create <name>[/yellow]")
910
- console.print("[dim]Example: /agents create code-reviewer[/dim]")
911
- else:
912
- agent_name = subargs.strip().lower().replace(" ", "-")
913
- agents_dir = Path.cwd() / ".emdash" / "agents"
914
- agent_file = agents_dir / f"{agent_name}.md"
915
-
916
- if agent_file.exists():
917
- console.print(f"[yellow]Agent '{agent_name}' already exists[/yellow]")
918
- console.print(f"[dim]Edit: {agent_file}[/dim]")
919
- else:
920
- # Create directory if needed
921
- agents_dir.mkdir(parents=True, exist_ok=True)
922
-
923
- # Create template
924
- template = f'''---
925
- description: Custom agent for specific tasks
926
- tools: [grep, glob, read_file, semantic_search]
927
- ---
928
-
929
- # System Prompt
930
-
931
- You are a specialized assistant for {agent_name.replace("-", " ")} tasks.
932
-
933
- ## Your Mission
934
-
935
- Describe what this agent should accomplish:
936
- - Task 1
937
- - Task 2
938
- - Task 3
939
-
940
- ## Approach
941
-
942
- 1. **Step One**
943
- - Details about the first step
944
-
945
- 2. **Step Two**
946
- - Details about the second step
947
-
948
- ## Output Format
949
-
950
- Describe how the agent should format its responses.
951
-
952
- # Examples
953
-
954
- ## Example 1
955
- User: Example user request
956
- Agent: Example agent response describing what it would do
957
- '''
958
- agent_file.write_text(template)
959
- console.print(f"[green]Created agent: {agent_name}[/green]")
960
- console.print(f"[dim]Edit to customize: {agent_file}[/dim]")
961
- console.print(f"\n[dim]Spawn with Task tool: subagent_type='{agent_name}'[/dim]")
962
-
963
- elif subcommand == "show":
964
- # Show details of an agent
965
- if not subargs:
966
- console.print("[yellow]Usage: /agents show <name>[/yellow]")
967
- else:
968
- agent_name = subargs.strip()
969
- builtin = ["Explore", "Plan"]
970
-
971
- if agent_name in builtin:
972
- console.print(f"\n[bold cyan]{agent_name}[/bold cyan] [dim](built-in)[/dim]\n")
973
- if agent_name == "Explore":
974
- console.print("Fast codebase exploration agent (read-only)")
975
- console.print("\n[bold]Tools:[/bold] glob, grep, read_file, list_files, semantic_search")
976
- elif agent_name == "Plan":
977
- console.print("Implementation planning agent")
978
- console.print("\n[bold]Tools:[/bold] glob, grep, read_file, list_files, semantic_search")
979
- console.print()
980
- else:
981
- agent = get_custom_agent(agent_name, Path.cwd())
982
- if agent:
983
- console.print(f"\n[bold cyan]{agent.name}[/bold cyan] [dim](custom)[/dim]\n")
984
- if agent.description:
985
- console.print(f"[bold]Description:[/bold] {agent.description}")
986
- if agent.tools:
987
- console.print(f"[bold]Tools:[/bold] {', '.join(agent.tools)}")
988
- if agent.file_path:
989
- console.print(f"[bold]File:[/bold] {agent.file_path}")
990
- if agent.system_prompt:
991
- console.print(f"\n[bold]System Prompt:[/bold]")
992
- # Show first 500 chars of system prompt
993
- preview = agent.system_prompt[:500]
994
- if len(agent.system_prompt) > 500:
995
- preview += "..."
996
- console.print(Panel(preview, border_style="dim"))
997
- console.print()
998
- else:
999
- console.print(f"[yellow]Agent '{agent_name}' not found[/yellow]")
1000
- console.print("[dim]Use /agents to list available agents[/dim]")
1001
-
1002
- else:
1003
- console.print(f"[yellow]Unknown subcommand: {subcommand}[/yellow]")
1004
- console.print("[dim]Usage: /agents [list|create|show] [name][/dim]")
1005
-
1006
- elif command == "/session":
1007
- # Session management: list, save, load, delete, clear
1008
- from ..session_store import SessionStore
1009
-
1010
- store = SessionStore(Path.cwd())
1011
-
1012
- # Parse subcommand
1013
- subparts = args.split(maxsplit=1) if args else []
1014
- subcommand = subparts[0].lower() if subparts else "list"
1015
- subargs = subparts[1].strip() if len(subparts) > 1 else ""
1016
-
1017
- if subcommand == "list" or subcommand == "":
1018
- # List all sessions
1019
- sessions = store.list_sessions()
1020
- if sessions:
1021
- console.print("\n[bold cyan]Saved Sessions[/bold cyan]\n")
1022
- for s in sessions:
1023
- mode_color = "green" if s.mode == "code" else "yellow"
1024
- active_marker = " [bold green]*[/bold green]" if store.get_active_session() == s.name else ""
1025
- console.print(f" [cyan]{s.name}[/cyan]{active_marker} [{mode_color}]{s.mode}[/{mode_color}]")
1026
- console.print(f" [dim]{s.message_count} messages | {s.updated_at[:10]}[/dim]")
1027
- if s.summary:
1028
- summary = s.summary[:60] + "..." if len(s.summary) > 60 else s.summary
1029
- console.print(f" [dim]{summary}[/dim]")
1030
- console.print()
1031
- else:
1032
- console.print("\n[dim]No saved sessions.[/dim]")
1033
- console.print("[dim]Save with: /session save <name>[/dim]\n")
1034
-
1035
- elif subcommand == "save":
1036
- if not subargs:
1037
- console.print("[yellow]Usage: /session save <name>[/yellow]")
1038
- console.print("[dim]Example: /session save auth-feature[/dim]")
1039
- else:
1040
- # Get current messages from the API session
1041
- if session_id:
1042
- try:
1043
- # Export messages from server
1044
- export_resp = client.get(f"/api/agent/chat/{session_id}/export")
1045
- if export_resp.status_code == 200:
1046
- data = export_resp.json()
1047
- messages = data.get("messages", [])
1048
- else:
1049
- messages = []
1050
- except Exception:
1051
- messages = []
1052
- else:
1053
- messages = []
1054
-
1055
- success, msg = store.save_session(
1056
- name=subargs,
1057
- messages=messages,
1058
- mode=current_mode.value,
1059
- spec=current_spec,
1060
- model=model,
1061
- )
1062
- if success:
1063
- store.set_active_session(subargs)
1064
- console.print(f"[green]{msg}[/green]")
1065
- else:
1066
- console.print(f"[yellow]{msg}[/yellow]")
1067
-
1068
- elif subcommand == "load":
1069
- if not subargs:
1070
- console.print("[yellow]Usage: /session load <name>[/yellow]")
1071
- else:
1072
- session_data = store.load_session(subargs)
1073
- if session_data:
1074
- # Reset current session
1075
- session_id = None
1076
- current_spec = session_data.spec
1077
- if session_data.mode == "plan":
1078
- current_mode = AgentMode.PLAN
1079
- else:
1080
- current_mode = AgentMode.CODE
1081
-
1082
- # Store loaded messages for replay
1083
- nonlocal loaded_messages
1084
- loaded_messages = session_data.messages
1085
-
1086
- store.set_active_session(subargs)
1087
- console.print(f"[green]Loaded session '{subargs}'[/green]")
1088
- console.print(f"[dim]{len(session_data.messages)} messages restored, mode: {current_mode.value}[/dim]")
1089
- if current_spec:
1090
- console.print("[dim]Spec restored[/dim]")
1091
- else:
1092
- console.print(f"[yellow]Session '{subargs}' not found[/yellow]")
1093
-
1094
- elif subcommand == "delete":
1095
- if not subargs:
1096
- console.print("[yellow]Usage: /session delete <name>[/yellow]")
1097
- else:
1098
- success, msg = store.delete_session(subargs)
1099
- if success:
1100
- console.print(f"[green]{msg}[/green]")
1101
- else:
1102
- console.print(f"[yellow]{msg}[/yellow]")
1103
-
1104
- elif subcommand == "clear":
1105
- # Clear current session state
1106
- session_id = None
1107
- current_spec = None
1108
- loaded_messages = []
1109
- store.set_active_session(None)
1110
- console.print("[green]Session cleared[/green]")
1111
-
1112
- else:
1113
- console.print(f"[yellow]Unknown subcommand: {subcommand}[/yellow]")
1114
- console.print("[dim]Usage: /session [list|save|load|delete|clear] [name][/dim]")
1115
-
1116
- else:
1117
- console.print(f"[yellow]Unknown command: {command}[/yellow]")
1118
- console.print("[dim]Type /help for available commands[/dim]")
1119
-
1120
- return True
1121
-
1122
- # Show welcome message
1123
- from .. import __version__
1124
- import subprocess
1125
-
1126
- # Get current working directory
1127
- cwd = Path.cwd()
1128
-
1129
- # Get git repo name (if in a git repo)
1130
- git_repo = None
1131
- try:
1132
- result = subprocess.run(
1133
- ["git", "rev-parse", "--show-toplevel"],
1134
- capture_output=True, text=True, cwd=cwd
1135
- )
1136
- if result.returncode == 0:
1137
- git_repo = Path(result.stdout.strip()).name
1138
- except Exception:
1139
- pass
1140
-
1141
- # Welcome banner
1142
- console.print()
1143
- console.print(f"[bold cyan]Mendy10 Emdash Code[/bold cyan] [dim]v{__version__}[/dim]")
1144
- if git_repo:
1145
- console.print(f"[dim]Repo:[/dim] [bold green]{git_repo}[/bold green] [dim]| Mode:[/dim] [bold]{current_mode.value}[/bold] [dim]| Model:[/dim] {model or 'default'}")
1146
- else:
1147
- console.print(f"[dim]Mode:[/dim] [bold]{current_mode.value}[/bold] [dim]| Model:[/dim] {model or 'default'}")
1148
- console.print()
1149
-
1150
- while True:
1151
- try:
1152
- # Get user input
1153
- user_input = session.prompt(get_prompt()).strip()
1154
-
1155
- if not user_input:
1156
- continue
1157
-
1158
- # Check if input is an image file path (dragged file)
1159
- clean_input = user_input.replace("\\ ", " ")
1160
- if clean_input.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp')):
1161
- from ..clipboard import get_image_from_path
1162
- image_data = get_image_from_path(clean_input)
1163
- if image_data:
1164
- base64_data, img_format = image_data
1165
- attached_images.append({"data": base64_data, "format": img_format})
1166
- continue # Prompt again for actual message
1167
-
1168
- # Handle slash commands
1169
- if user_input.startswith("/"):
1170
- if not handle_slash_command(user_input):
1171
- break
1172
- continue
1173
-
1174
- # Handle quit shortcuts
1175
- if user_input.lower() in ("quit", "exit", "q"):
1176
- break
1177
-
1178
- # Build options with current mode
1179
- request_options = {
1180
- **options,
1181
- "mode": current_mode.value,
1182
- }
1183
-
1184
- # Run agent with current mode
1185
- try:
1186
- # Prepare images for API call
1187
- images_to_send = attached_images if attached_images else None
1188
-
1189
- if session_id:
1190
- stream = client.agent_continue_stream(
1191
- session_id, user_input, images=images_to_send
1192
- )
1193
- else:
1194
- # Pass loaded_messages from saved session if available
1195
- stream = client.agent_chat_stream(
1196
- message=user_input,
1197
- model=model,
1198
- max_iterations=max_iterations,
1199
- options=request_options,
1200
- images=images_to_send,
1201
- history=loaded_messages if loaded_messages else None,
1202
- )
1203
- # Clear loaded_messages after first use
1204
- loaded_messages = []
1205
-
1206
- # Clear attached images after sending
1207
- attached_images = []
1208
-
1209
- # Render the stream and capture any spec output
1210
- result = _render_with_interrupt(renderer, stream)
1211
-
1212
- # Check if we got a session ID back
1213
- if result and result.get("session_id"):
1214
- session_id = result["session_id"]
1215
-
1216
- # Check for spec output
1217
- if result and result.get("spec"):
1218
- current_spec = result["spec"]
1219
-
1220
- # Handle clarifications (may be chained - loop until no more)
1221
- while True:
1222
- clarification = result.get("clarification")
1223
- if not (clarification and session_id):
1224
- break
1225
-
1226
- response = _get_clarification_response(clarification)
1227
- if not response:
1228
- break
1229
-
1230
- # Show the user's selection in the chat
1231
- console.print()
1232
- console.print(f"[dim]Selected:[/dim] [bold]{response}[/bold]")
1233
- console.print()
1234
-
1235
- # Use dedicated clarification answer endpoint
1236
- try:
1237
- stream = client.clarification_answer_stream(session_id, response)
1238
- result = _render_with_interrupt(renderer, stream)
1239
-
1240
- # Update mode if user chose code
1241
- if "code" in response.lower():
1242
- current_mode = AgentMode.CODE
1243
- except Exception as e:
1244
- console.print(f"[red]Error continuing session: {e}[/red]")
1245
- break
1246
-
1247
- # Handle plan mode entry request (show approval menu)
1248
- plan_mode_requested = result.get("plan_mode_requested")
1249
- if plan_mode_requested is not None and session_id:
1250
- choice, feedback = _show_plan_mode_approval_menu()
1251
-
1252
- if choice == "approve":
1253
- current_mode = AgentMode.PLAN
1254
- console.print()
1255
- console.print("[bold green]✓ Plan mode activated[/bold green]")
1256
- console.print()
1257
- # Use the planmode approve endpoint
1258
- stream = client.planmode_approve_stream(session_id)
1259
- result = _render_with_interrupt(renderer, stream)
1260
- # After approval, check if there's now a plan submitted
1261
- if result.get("plan_submitted"):
1262
- plan_submitted = result.get("plan_submitted")
1263
- elif choice == "reject":
1264
- # Use the planmode reject endpoint - stay in code mode
1265
- stream = client.planmode_reject_stream(session_id, feedback)
1266
- _render_with_interrupt(renderer, stream)
1267
-
1268
- # Handle plan mode completion (show approval menu)
1269
- # Only show menu when agent explicitly submits a plan via exit_plan tool
1270
- plan_submitted = result.get("plan_submitted")
1271
- should_show_plan_menu = (
1272
- current_mode == AgentMode.PLAN and
1273
- session_id and
1274
- plan_submitted is not None # Agent called exit_plan tool
1275
- )
1276
- if should_show_plan_menu:
1277
- choice, feedback = _show_plan_approval_menu()
1278
-
1279
- if choice == "approve":
1280
- current_mode = AgentMode.CODE
1281
- # Use the plan approve endpoint which properly resets mode on server
1282
- stream = client.plan_approve_stream(session_id)
1283
- _render_with_interrupt(renderer, stream)
1284
- elif choice == "reject":
1285
- if feedback:
1286
- # Use the plan reject endpoint which keeps mode as PLAN on server
1287
- stream = client.plan_reject_stream(session_id, feedback)
1288
- _render_with_interrupt(renderer, stream)
1289
- else:
1290
- console.print("[dim]Plan rejected[/dim]")
1291
- session_id = None
1292
- current_spec = None
1293
-
1294
- console.print()
1295
-
1296
- except Exception as e:
1297
- console.print(f"[red]Error: {e}[/red]")
1298
-
1299
- except KeyboardInterrupt:
1300
- console.print("\n[dim]Interrupted[/dim]")
1301
- break
1302
- except EOFError:
1303
- break
1304
-
1305
-
1306
- @agent.command("sessions")
1307
- def list_sessions():
1308
- """List active agent sessions."""
1309
- server = get_server_manager()
1310
- base_url = server.get_server_url()
1311
-
1312
- client = EmdashClient(base_url)
1313
- sessions = client.list_sessions()
1314
-
1315
- if not sessions:
1316
- console.print("[dim]No active sessions[/dim]")
1317
- return
1318
-
1319
- for s in sessions:
1320
- console.print(
1321
- f" {s['session_id'][:8]}... "
1322
- f"[dim]({s.get('model', 'unknown')}, "
1323
- f"{s.get('message_count', 0)} messages)[/dim]"
1324
- )
10
+ __all__ = ["agent", "agent_code"]