emdash-cli 0.1.4__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 +3 -0
- emdash_cli/client.py +556 -0
- emdash_cli/commands/__init__.py +37 -0
- emdash_cli/commands/agent.py +883 -0
- emdash_cli/commands/analyze.py +137 -0
- emdash_cli/commands/auth.py +121 -0
- emdash_cli/commands/db.py +95 -0
- emdash_cli/commands/embed.py +103 -0
- emdash_cli/commands/index.py +134 -0
- emdash_cli/commands/plan.py +77 -0
- emdash_cli/commands/projectmd.py +51 -0
- emdash_cli/commands/research.py +47 -0
- emdash_cli/commands/rules.py +93 -0
- emdash_cli/commands/search.py +56 -0
- emdash_cli/commands/server.py +117 -0
- emdash_cli/commands/spec.py +49 -0
- emdash_cli/commands/swarm.py +86 -0
- emdash_cli/commands/tasks.py +52 -0
- emdash_cli/commands/team.py +51 -0
- emdash_cli/main.py +104 -0
- emdash_cli/server_manager.py +231 -0
- emdash_cli/sse_renderer.py +442 -0
- emdash_cli-0.1.4.dist-info/METADATA +17 -0
- emdash_cli-0.1.4.dist-info/RECORD +26 -0
- emdash_cli-0.1.4.dist-info/WHEEL +4 -0
- emdash_cli-0.1.4.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
"""Agent CLI commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.markdown import Markdown
|
|
8
|
+
|
|
9
|
+
from ..client import EmdashClient
|
|
10
|
+
from ..server_manager import get_server_manager
|
|
11
|
+
from ..sse_renderer import SSERenderer
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentMode(Enum):
|
|
17
|
+
"""Agent operation modes."""
|
|
18
|
+
PLAN = "plan"
|
|
19
|
+
TASKS = "tasks"
|
|
20
|
+
CODE = "code"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Slash commands available in interactive mode
|
|
24
|
+
SLASH_COMMANDS = {
|
|
25
|
+
# Mode switching
|
|
26
|
+
"/plan": "Switch to plan mode (explore codebase, create specs)",
|
|
27
|
+
"/tasks": "Switch to tasks mode (generate task lists)",
|
|
28
|
+
"/code": "Switch to code mode (execute file changes)",
|
|
29
|
+
"/mode": "Show current mode",
|
|
30
|
+
# Generation commands
|
|
31
|
+
"/pr [url]": "Review a pull request",
|
|
32
|
+
"/projectmd": "Generate PROJECT.md for the codebase",
|
|
33
|
+
"/research [goal]": "Deep research on a topic",
|
|
34
|
+
# Status commands
|
|
35
|
+
"/status": "Show index and PROJECT.md status",
|
|
36
|
+
# Session management
|
|
37
|
+
"/spec": "Show current specification",
|
|
38
|
+
"/reset": "Reset session state",
|
|
39
|
+
"/save": "Save current spec to disk",
|
|
40
|
+
"/help": "Show available commands",
|
|
41
|
+
"/quit": "Exit the agent",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@click.group()
|
|
46
|
+
def agent():
|
|
47
|
+
"""AI agent commands."""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@agent.command("code")
|
|
52
|
+
@click.argument("task", required=False)
|
|
53
|
+
@click.option("--model", "-m", default=None, help="Model to use")
|
|
54
|
+
@click.option("--mode", type=click.Choice(["plan", "tasks", "code"]), default="code",
|
|
55
|
+
help="Starting mode")
|
|
56
|
+
@click.option("--quiet", "-q", is_flag=True, help="Less verbose output")
|
|
57
|
+
@click.option("--max-iterations", default=20, help="Max agent iterations")
|
|
58
|
+
@click.option("--no-graph-tools", is_flag=True, help="Skip graph exploration tools")
|
|
59
|
+
@click.option("--save", is_flag=True, help="Save specs to specs/<feature>/")
|
|
60
|
+
def agent_code(
|
|
61
|
+
task: str | None,
|
|
62
|
+
model: str | None,
|
|
63
|
+
mode: str,
|
|
64
|
+
quiet: bool,
|
|
65
|
+
max_iterations: int,
|
|
66
|
+
no_graph_tools: bool,
|
|
67
|
+
save: bool,
|
|
68
|
+
):
|
|
69
|
+
"""Start the coding agent.
|
|
70
|
+
|
|
71
|
+
With TASK: Run single task and exit
|
|
72
|
+
Without TASK: Start interactive REPL mode
|
|
73
|
+
|
|
74
|
+
MODES:
|
|
75
|
+
plan - Explore codebase and create specifications
|
|
76
|
+
tasks - Generate implementation task lists
|
|
77
|
+
code - Execute code changes (default)
|
|
78
|
+
|
|
79
|
+
SLASH COMMANDS (in interactive mode):
|
|
80
|
+
/plan - Switch to plan mode
|
|
81
|
+
/tasks - Switch to tasks mode
|
|
82
|
+
/code - Switch to code mode
|
|
83
|
+
/help - Show available commands
|
|
84
|
+
/reset - Reset session
|
|
85
|
+
|
|
86
|
+
Examples:
|
|
87
|
+
emdash # Interactive code mode
|
|
88
|
+
emdash agent code # Same as above
|
|
89
|
+
emdash agent code --mode plan # Start in plan mode
|
|
90
|
+
emdash agent code "Fix the login bug" # Single task
|
|
91
|
+
"""
|
|
92
|
+
# Get server URL (starts server if needed)
|
|
93
|
+
server = get_server_manager()
|
|
94
|
+
base_url = server.get_server_url()
|
|
95
|
+
|
|
96
|
+
client = EmdashClient(base_url)
|
|
97
|
+
renderer = SSERenderer(console=console, verbose=not quiet)
|
|
98
|
+
|
|
99
|
+
options = {
|
|
100
|
+
"mode": mode,
|
|
101
|
+
"no_graph_tools": no_graph_tools,
|
|
102
|
+
"save": save,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if task:
|
|
106
|
+
# Single task mode
|
|
107
|
+
_run_single_task(client, renderer, task, model, max_iterations, options)
|
|
108
|
+
else:
|
|
109
|
+
# Interactive REPL mode
|
|
110
|
+
_run_interactive(client, renderer, model, max_iterations, options)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _get_clarification_response(clarification: dict) -> str | None:
|
|
114
|
+
"""Get user response for clarification with options.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
clarification: Dict with question, context, and options
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
User's selected option or typed response, or None if cancelled
|
|
121
|
+
"""
|
|
122
|
+
from prompt_toolkit import PromptSession
|
|
123
|
+
|
|
124
|
+
options = clarification.get("options", [])
|
|
125
|
+
|
|
126
|
+
session = PromptSession()
|
|
127
|
+
console.print("[dim]Enter number or type response:[/dim]")
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
response = session.prompt("choice > ").strip()
|
|
131
|
+
|
|
132
|
+
if not response:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
# Map number to option
|
|
136
|
+
if response.isdigit():
|
|
137
|
+
idx = int(response) - 1
|
|
138
|
+
if 0 <= idx < len(options):
|
|
139
|
+
return options[idx]
|
|
140
|
+
|
|
141
|
+
return response
|
|
142
|
+
except (KeyboardInterrupt, EOFError):
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _show_spec_approval_menu() -> tuple[str, str]:
|
|
147
|
+
"""Show spec approval menu with arrow-key selection.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Tuple of (choice, feedback) where feedback is only set for 'refine'
|
|
151
|
+
"""
|
|
152
|
+
from prompt_toolkit import Application
|
|
153
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
154
|
+
from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
|
|
155
|
+
from prompt_toolkit.styles import Style
|
|
156
|
+
|
|
157
|
+
options = [
|
|
158
|
+
("tasks", "Generate implementation tasks"),
|
|
159
|
+
("code", "Start coding directly"),
|
|
160
|
+
("refine", "Provide feedback to improve"),
|
|
161
|
+
("abort", "Cancel and discard"),
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
selected_index = [0] # Use list to allow mutation in closure
|
|
165
|
+
result = [None]
|
|
166
|
+
|
|
167
|
+
# Key bindings
|
|
168
|
+
kb = KeyBindings()
|
|
169
|
+
|
|
170
|
+
@kb.add("up")
|
|
171
|
+
@kb.add("k")
|
|
172
|
+
def move_up(event):
|
|
173
|
+
selected_index[0] = (selected_index[0] - 1) % len(options)
|
|
174
|
+
|
|
175
|
+
@kb.add("down")
|
|
176
|
+
@kb.add("j")
|
|
177
|
+
def move_down(event):
|
|
178
|
+
selected_index[0] = (selected_index[0] + 1) % len(options)
|
|
179
|
+
|
|
180
|
+
@kb.add("enter")
|
|
181
|
+
def select(event):
|
|
182
|
+
result[0] = options[selected_index[0]][0]
|
|
183
|
+
event.app.exit()
|
|
184
|
+
|
|
185
|
+
@kb.add("1")
|
|
186
|
+
def select_1(event):
|
|
187
|
+
result[0] = "tasks"
|
|
188
|
+
event.app.exit()
|
|
189
|
+
|
|
190
|
+
@kb.add("2")
|
|
191
|
+
def select_2(event):
|
|
192
|
+
result[0] = "code"
|
|
193
|
+
event.app.exit()
|
|
194
|
+
|
|
195
|
+
@kb.add("3")
|
|
196
|
+
def select_3(event):
|
|
197
|
+
result[0] = "refine"
|
|
198
|
+
event.app.exit()
|
|
199
|
+
|
|
200
|
+
@kb.add("4")
|
|
201
|
+
def select_4(event):
|
|
202
|
+
result[0] = "abort"
|
|
203
|
+
event.app.exit()
|
|
204
|
+
|
|
205
|
+
@kb.add("c-c")
|
|
206
|
+
@kb.add("q")
|
|
207
|
+
@kb.add("escape")
|
|
208
|
+
def cancel(event):
|
|
209
|
+
result[0] = "abort"
|
|
210
|
+
event.app.exit()
|
|
211
|
+
|
|
212
|
+
def get_formatted_options():
|
|
213
|
+
lines = [("class:title", "What would you like to do with this spec?\n\n")]
|
|
214
|
+
for i, (key, desc) in enumerate(options):
|
|
215
|
+
if i == selected_index[0]:
|
|
216
|
+
lines.append(("class:selected", f" ❯ {key:8} "))
|
|
217
|
+
lines.append(("class:selected-desc", f"- {desc}\n"))
|
|
218
|
+
else:
|
|
219
|
+
lines.append(("class:option", f" {key:8} "))
|
|
220
|
+
lines.append(("class:desc", f"- {desc}\n"))
|
|
221
|
+
lines.append(("class:hint", "\n↑/↓ to move, Enter to select, q to cancel"))
|
|
222
|
+
return lines
|
|
223
|
+
|
|
224
|
+
# Style
|
|
225
|
+
style = Style.from_dict({
|
|
226
|
+
"title": "#00ccff bold",
|
|
227
|
+
"selected": "#00cc66 bold",
|
|
228
|
+
"selected-desc": "#00cc66",
|
|
229
|
+
"option": "#888888",
|
|
230
|
+
"desc": "#666666",
|
|
231
|
+
"hint": "#444444 italic",
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
# Layout
|
|
235
|
+
layout = Layout(
|
|
236
|
+
HSplit([
|
|
237
|
+
Window(
|
|
238
|
+
FormattedTextControl(get_formatted_options),
|
|
239
|
+
height=8,
|
|
240
|
+
),
|
|
241
|
+
])
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Application
|
|
245
|
+
app = Application(
|
|
246
|
+
layout=layout,
|
|
247
|
+
key_bindings=kb,
|
|
248
|
+
style=style,
|
|
249
|
+
full_screen=False,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
console.print()
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
app.run()
|
|
256
|
+
except (KeyboardInterrupt, EOFError):
|
|
257
|
+
result[0] = "abort"
|
|
258
|
+
|
|
259
|
+
choice = result[0] or "abort"
|
|
260
|
+
|
|
261
|
+
# Get feedback if refine was chosen
|
|
262
|
+
feedback = ""
|
|
263
|
+
if choice == "refine":
|
|
264
|
+
from prompt_toolkit import PromptSession
|
|
265
|
+
console.print()
|
|
266
|
+
console.print("[dim]What changes would you like?[/dim]")
|
|
267
|
+
try:
|
|
268
|
+
session = PromptSession()
|
|
269
|
+
feedback = session.prompt("feedback > ").strip()
|
|
270
|
+
except (KeyboardInterrupt, EOFError):
|
|
271
|
+
return "abort", ""
|
|
272
|
+
|
|
273
|
+
return choice, feedback
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _show_tasks_approval_menu() -> tuple[str, str]:
|
|
277
|
+
"""Show tasks approval menu with arrow-key selection.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Tuple of (choice, feedback) where feedback is only set for 'refine'
|
|
281
|
+
"""
|
|
282
|
+
from prompt_toolkit import Application
|
|
283
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
284
|
+
from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
|
|
285
|
+
from prompt_toolkit.styles import Style
|
|
286
|
+
|
|
287
|
+
options = [
|
|
288
|
+
("code", "Start implementing these tasks"),
|
|
289
|
+
("refine", "Refine tasks with more details"),
|
|
290
|
+
("export", "Export tasks to file"),
|
|
291
|
+
("abort", "Cancel and discard"),
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
selected_index = [0] # Use list to allow mutation in closure
|
|
295
|
+
result = [None]
|
|
296
|
+
|
|
297
|
+
# Key bindings
|
|
298
|
+
kb = KeyBindings()
|
|
299
|
+
|
|
300
|
+
@kb.add("up")
|
|
301
|
+
@kb.add("k")
|
|
302
|
+
def move_up(event):
|
|
303
|
+
selected_index[0] = (selected_index[0] - 1) % len(options)
|
|
304
|
+
|
|
305
|
+
@kb.add("down")
|
|
306
|
+
@kb.add("j")
|
|
307
|
+
def move_down(event):
|
|
308
|
+
selected_index[0] = (selected_index[0] + 1) % len(options)
|
|
309
|
+
|
|
310
|
+
@kb.add("enter")
|
|
311
|
+
def select(event):
|
|
312
|
+
result[0] = options[selected_index[0]][0]
|
|
313
|
+
event.app.exit()
|
|
314
|
+
|
|
315
|
+
@kb.add("1")
|
|
316
|
+
def select_1(event):
|
|
317
|
+
result[0] = "code"
|
|
318
|
+
event.app.exit()
|
|
319
|
+
|
|
320
|
+
@kb.add("2")
|
|
321
|
+
def select_2(event):
|
|
322
|
+
result[0] = "refine"
|
|
323
|
+
event.app.exit()
|
|
324
|
+
|
|
325
|
+
@kb.add("3")
|
|
326
|
+
def select_3(event):
|
|
327
|
+
result[0] = "export"
|
|
328
|
+
event.app.exit()
|
|
329
|
+
|
|
330
|
+
@kb.add("4")
|
|
331
|
+
def select_4(event):
|
|
332
|
+
result[0] = "abort"
|
|
333
|
+
event.app.exit()
|
|
334
|
+
|
|
335
|
+
@kb.add("c-c")
|
|
336
|
+
@kb.add("q")
|
|
337
|
+
@kb.add("escape")
|
|
338
|
+
def cancel(event):
|
|
339
|
+
result[0] = "abort"
|
|
340
|
+
event.app.exit()
|
|
341
|
+
|
|
342
|
+
def get_formatted_options():
|
|
343
|
+
lines = [("class:title", "What would you like to do with these tasks?\n\n")]
|
|
344
|
+
for i, (key, desc) in enumerate(options):
|
|
345
|
+
if i == selected_index[0]:
|
|
346
|
+
lines.append(("class:selected", f" ❯ {key:8} "))
|
|
347
|
+
lines.append(("class:selected-desc", f"- {desc}\n"))
|
|
348
|
+
else:
|
|
349
|
+
lines.append(("class:option", f" {key:8} "))
|
|
350
|
+
lines.append(("class:desc", f"- {desc}\n"))
|
|
351
|
+
lines.append(("class:hint", "\n↑/↓ to move, Enter to select, q to cancel"))
|
|
352
|
+
return lines
|
|
353
|
+
|
|
354
|
+
# Style
|
|
355
|
+
style = Style.from_dict({
|
|
356
|
+
"title": "#cc66ff bold", # Purple for tasks
|
|
357
|
+
"selected": "#cc66ff bold",
|
|
358
|
+
"selected-desc": "#cc66ff",
|
|
359
|
+
"option": "#888888",
|
|
360
|
+
"desc": "#666666",
|
|
361
|
+
"hint": "#444444 italic",
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
# Layout
|
|
365
|
+
layout = Layout(
|
|
366
|
+
HSplit([
|
|
367
|
+
Window(
|
|
368
|
+
FormattedTextControl(get_formatted_options),
|
|
369
|
+
height=8,
|
|
370
|
+
),
|
|
371
|
+
])
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Application
|
|
375
|
+
app = Application(
|
|
376
|
+
layout=layout,
|
|
377
|
+
key_bindings=kb,
|
|
378
|
+
style=style,
|
|
379
|
+
full_screen=False,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
console.print()
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
app.run()
|
|
386
|
+
except (KeyboardInterrupt, EOFError):
|
|
387
|
+
result[0] = "abort"
|
|
388
|
+
|
|
389
|
+
choice = result[0] or "abort"
|
|
390
|
+
|
|
391
|
+
# Get feedback if refine was chosen
|
|
392
|
+
feedback = ""
|
|
393
|
+
if choice == "refine":
|
|
394
|
+
from prompt_toolkit import PromptSession
|
|
395
|
+
console.print()
|
|
396
|
+
console.print("[dim]What changes would you like to the tasks?[/dim]")
|
|
397
|
+
try:
|
|
398
|
+
session = PromptSession()
|
|
399
|
+
feedback = session.prompt("feedback > ").strip()
|
|
400
|
+
except (KeyboardInterrupt, EOFError):
|
|
401
|
+
return "abort", ""
|
|
402
|
+
|
|
403
|
+
return choice, feedback
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _run_single_task(
|
|
407
|
+
client: EmdashClient,
|
|
408
|
+
renderer: SSERenderer,
|
|
409
|
+
task: str,
|
|
410
|
+
model: str | None,
|
|
411
|
+
max_iterations: int,
|
|
412
|
+
options: dict,
|
|
413
|
+
):
|
|
414
|
+
"""Run a single agent task."""
|
|
415
|
+
try:
|
|
416
|
+
stream = client.agent_chat_stream(
|
|
417
|
+
message=task,
|
|
418
|
+
model=model,
|
|
419
|
+
max_iterations=max_iterations,
|
|
420
|
+
options=options,
|
|
421
|
+
)
|
|
422
|
+
renderer.render_stream(stream)
|
|
423
|
+
except Exception as e:
|
|
424
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
425
|
+
raise click.Abort()
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _run_slash_command_task(
|
|
429
|
+
client: EmdashClient,
|
|
430
|
+
renderer: SSERenderer,
|
|
431
|
+
model: str | None,
|
|
432
|
+
max_iterations: int,
|
|
433
|
+
task: str,
|
|
434
|
+
options: dict,
|
|
435
|
+
):
|
|
436
|
+
"""Run a task from a slash command."""
|
|
437
|
+
try:
|
|
438
|
+
stream = client.agent_chat_stream(
|
|
439
|
+
message=task,
|
|
440
|
+
model=model,
|
|
441
|
+
max_iterations=max_iterations,
|
|
442
|
+
options=options,
|
|
443
|
+
)
|
|
444
|
+
renderer.render_stream(stream)
|
|
445
|
+
console.print()
|
|
446
|
+
except Exception as e:
|
|
447
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _run_interactive(
|
|
451
|
+
client: EmdashClient,
|
|
452
|
+
renderer: SSERenderer,
|
|
453
|
+
model: str | None,
|
|
454
|
+
max_iterations: int,
|
|
455
|
+
options: dict,
|
|
456
|
+
):
|
|
457
|
+
"""Run interactive REPL mode with slash commands."""
|
|
458
|
+
from prompt_toolkit import PromptSession
|
|
459
|
+
from prompt_toolkit.history import FileHistory
|
|
460
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
461
|
+
from prompt_toolkit.styles import Style
|
|
462
|
+
from pathlib import Path
|
|
463
|
+
|
|
464
|
+
# Current mode
|
|
465
|
+
current_mode = AgentMode(options.get("mode", "code"))
|
|
466
|
+
session_id = None
|
|
467
|
+
current_spec = None
|
|
468
|
+
|
|
469
|
+
# Style for prompt
|
|
470
|
+
PROMPT_STYLE = Style.from_dict({
|
|
471
|
+
"prompt.mode.plan": "#ffcc00 bold",
|
|
472
|
+
"prompt.mode.tasks": "#cc66ff bold",
|
|
473
|
+
"prompt.mode.code": "#00cc66 bold",
|
|
474
|
+
"prompt.prefix": "#888888",
|
|
475
|
+
"completion-menu": "bg:#1a1a2e #ffffff",
|
|
476
|
+
"completion-menu.completion": "bg:#1a1a2e #ffffff",
|
|
477
|
+
"completion-menu.completion.current": "bg:#4a4a6e #ffffff bold",
|
|
478
|
+
"completion-menu.meta.completion": "bg:#1a1a2e #888888",
|
|
479
|
+
"completion-menu.meta.completion.current": "bg:#4a4a6e #aaaaaa",
|
|
480
|
+
"command": "#00ccff bold",
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
class SlashCommandCompleter(Completer):
|
|
484
|
+
"""Completer for slash commands."""
|
|
485
|
+
|
|
486
|
+
def get_completions(self, document, complete_event):
|
|
487
|
+
text = document.text_before_cursor
|
|
488
|
+
if not text.startswith("/"):
|
|
489
|
+
return
|
|
490
|
+
for cmd, description in SLASH_COMMANDS.items():
|
|
491
|
+
# Extract base command (e.g., "/pr" from "/pr [url]")
|
|
492
|
+
base_cmd = cmd.split()[0]
|
|
493
|
+
if base_cmd.startswith(text):
|
|
494
|
+
yield Completion(
|
|
495
|
+
base_cmd,
|
|
496
|
+
start_position=-len(text),
|
|
497
|
+
display=cmd,
|
|
498
|
+
display_meta=description,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Setup history file
|
|
502
|
+
history_file = Path.home() / ".emdash" / "cli_history"
|
|
503
|
+
history_file.parent.mkdir(parents=True, exist_ok=True)
|
|
504
|
+
history = FileHistory(str(history_file))
|
|
505
|
+
|
|
506
|
+
session = PromptSession(
|
|
507
|
+
history=history,
|
|
508
|
+
completer=SlashCommandCompleter(),
|
|
509
|
+
style=PROMPT_STYLE,
|
|
510
|
+
complete_while_typing=True,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
def get_prompt():
|
|
514
|
+
"""Get formatted prompt based on current mode."""
|
|
515
|
+
mode_colors = {
|
|
516
|
+
AgentMode.PLAN: "class:prompt.mode.plan",
|
|
517
|
+
AgentMode.TASKS: "class:prompt.mode.tasks",
|
|
518
|
+
AgentMode.CODE: "class:prompt.mode.code",
|
|
519
|
+
}
|
|
520
|
+
mode_name = current_mode.value
|
|
521
|
+
color_class = mode_colors.get(current_mode, "class:prompt.mode.code")
|
|
522
|
+
return [(color_class, f"[{mode_name}]"), ("", " "), ("class:prompt.prefix", "> ")]
|
|
523
|
+
|
|
524
|
+
def show_help():
|
|
525
|
+
"""Show available commands."""
|
|
526
|
+
console.print()
|
|
527
|
+
console.print("[bold cyan]Available Commands[/bold cyan]")
|
|
528
|
+
console.print()
|
|
529
|
+
for cmd, desc in SLASH_COMMANDS.items():
|
|
530
|
+
console.print(f" [cyan]{cmd:12}[/cyan] {desc}")
|
|
531
|
+
console.print()
|
|
532
|
+
console.print("[dim]Type your task or question to interact with the agent.[/dim]")
|
|
533
|
+
console.print()
|
|
534
|
+
|
|
535
|
+
def handle_slash_command(cmd: str) -> bool:
|
|
536
|
+
"""Handle a slash command. Returns True if should continue, False to exit."""
|
|
537
|
+
nonlocal current_mode, session_id, current_spec
|
|
538
|
+
|
|
539
|
+
cmd_parts = cmd.strip().split(maxsplit=1)
|
|
540
|
+
command = cmd_parts[0].lower()
|
|
541
|
+
args = cmd_parts[1] if len(cmd_parts) > 1 else ""
|
|
542
|
+
|
|
543
|
+
if command == "/quit" or command == "/exit" or command == "/q":
|
|
544
|
+
return False
|
|
545
|
+
|
|
546
|
+
elif command == "/help":
|
|
547
|
+
show_help()
|
|
548
|
+
|
|
549
|
+
elif command == "/plan":
|
|
550
|
+
current_mode = AgentMode.PLAN
|
|
551
|
+
console.print("[yellow]Switched to plan mode[/yellow]")
|
|
552
|
+
|
|
553
|
+
elif command == "/tasks":
|
|
554
|
+
current_mode = AgentMode.TASKS
|
|
555
|
+
console.print("[magenta]Switched to tasks mode[/magenta]")
|
|
556
|
+
|
|
557
|
+
elif command == "/code":
|
|
558
|
+
current_mode = AgentMode.CODE
|
|
559
|
+
console.print("[green]Switched to code mode[/green]")
|
|
560
|
+
|
|
561
|
+
elif command == "/mode":
|
|
562
|
+
console.print(f"Current mode: [bold]{current_mode.value}[/bold]")
|
|
563
|
+
|
|
564
|
+
elif command == "/reset":
|
|
565
|
+
session_id = None
|
|
566
|
+
current_spec = None
|
|
567
|
+
console.print("[dim]Session reset[/dim]")
|
|
568
|
+
|
|
569
|
+
elif command == "/spec":
|
|
570
|
+
if current_spec:
|
|
571
|
+
console.print(Panel(Markdown(current_spec), title="Current Spec"))
|
|
572
|
+
else:
|
|
573
|
+
console.print("[dim]No spec available. Use plan mode to create one.[/dim]")
|
|
574
|
+
|
|
575
|
+
elif command == "/save":
|
|
576
|
+
if current_spec:
|
|
577
|
+
# TODO: Save spec via API
|
|
578
|
+
console.print("[yellow]Save not implemented yet[/yellow]")
|
|
579
|
+
else:
|
|
580
|
+
console.print("[dim]No spec to save[/dim]")
|
|
581
|
+
|
|
582
|
+
elif command == "/pr":
|
|
583
|
+
# PR review
|
|
584
|
+
if not args:
|
|
585
|
+
console.print("[yellow]Usage: /pr <pr-url-or-number>[/yellow]")
|
|
586
|
+
console.print("[dim]Example: /pr 123 or /pr https://github.com/org/repo/pull/123[/dim]")
|
|
587
|
+
else:
|
|
588
|
+
console.print(f"[cyan]Reviewing PR: {args}[/cyan]")
|
|
589
|
+
_run_slash_command_task(
|
|
590
|
+
client, renderer, model, max_iterations,
|
|
591
|
+
f"Review this pull request and provide feedback: {args}",
|
|
592
|
+
{"mode": "code"}
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
elif command == "/projectmd":
|
|
596
|
+
# Generate PROJECT.md
|
|
597
|
+
console.print("[cyan]Generating PROJECT.md...[/cyan]")
|
|
598
|
+
_run_slash_command_task(
|
|
599
|
+
client, renderer, model, max_iterations,
|
|
600
|
+
"Analyze this codebase and generate a comprehensive PROJECT.md file that describes the architecture, main components, how to get started, and key design decisions.",
|
|
601
|
+
{"mode": "code"}
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
elif command == "/research":
|
|
605
|
+
# Deep research
|
|
606
|
+
if not args:
|
|
607
|
+
console.print("[yellow]Usage: /research <goal>[/yellow]")
|
|
608
|
+
console.print("[dim]Example: /research How does authentication work in this codebase?[/dim]")
|
|
609
|
+
else:
|
|
610
|
+
console.print(f"[cyan]Researching: {args}[/cyan]")
|
|
611
|
+
_run_slash_command_task(
|
|
612
|
+
client, renderer, model, 50, # More iterations for research
|
|
613
|
+
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.",
|
|
614
|
+
{"mode": "plan"} # Use plan mode for research
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
elif command == "/status":
|
|
618
|
+
# Show index and PROJECT.md status
|
|
619
|
+
from datetime import datetime
|
|
620
|
+
|
|
621
|
+
console.print("\n[bold cyan]Status[/bold cyan]\n")
|
|
622
|
+
|
|
623
|
+
# Index status
|
|
624
|
+
console.print("[bold]Index Status[/bold]")
|
|
625
|
+
try:
|
|
626
|
+
status = client.index_status(str(Path.cwd()))
|
|
627
|
+
is_indexed = status.get("is_indexed", False)
|
|
628
|
+
console.print(f" Indexed: {'[green]Yes[/green]' if is_indexed else '[yellow]No[/yellow]'}")
|
|
629
|
+
|
|
630
|
+
if is_indexed:
|
|
631
|
+
console.print(f" Files: {status.get('file_count', 0)}")
|
|
632
|
+
console.print(f" Functions: {status.get('function_count', 0)}")
|
|
633
|
+
console.print(f" Classes: {status.get('class_count', 0)}")
|
|
634
|
+
console.print(f" Communities: {status.get('community_count', 0)}")
|
|
635
|
+
if status.get("last_indexed"):
|
|
636
|
+
console.print(f" Last indexed: {status.get('last_indexed')}")
|
|
637
|
+
if status.get("last_commit"):
|
|
638
|
+
console.print(f" Last commit: {status.get('last_commit')}")
|
|
639
|
+
except Exception as e:
|
|
640
|
+
console.print(f" [red]Error fetching index status: {e}[/red]")
|
|
641
|
+
|
|
642
|
+
console.print()
|
|
643
|
+
|
|
644
|
+
# PROJECT.md status
|
|
645
|
+
console.print("[bold]PROJECT.md Status[/bold]")
|
|
646
|
+
projectmd_path = Path.cwd() / "PROJECT.md"
|
|
647
|
+
if projectmd_path.exists():
|
|
648
|
+
stat = projectmd_path.stat()
|
|
649
|
+
modified_time = datetime.fromtimestamp(stat.st_mtime)
|
|
650
|
+
size_kb = stat.st_size / 1024
|
|
651
|
+
console.print(f" Exists: [green]Yes[/green]")
|
|
652
|
+
console.print(f" Path: {projectmd_path}")
|
|
653
|
+
console.print(f" Size: {size_kb:.1f} KB")
|
|
654
|
+
console.print(f" Last modified: {modified_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
655
|
+
else:
|
|
656
|
+
console.print(f" Exists: [yellow]No[/yellow]")
|
|
657
|
+
console.print("[dim] Run /projectmd to generate it[/dim]")
|
|
658
|
+
|
|
659
|
+
console.print()
|
|
660
|
+
|
|
661
|
+
else:
|
|
662
|
+
console.print(f"[yellow]Unknown command: {command}[/yellow]")
|
|
663
|
+
console.print("[dim]Type /help for available commands[/dim]")
|
|
664
|
+
|
|
665
|
+
return True
|
|
666
|
+
|
|
667
|
+
# Show welcome message
|
|
668
|
+
from .. import __version__
|
|
669
|
+
import subprocess
|
|
670
|
+
|
|
671
|
+
# Get current working directory
|
|
672
|
+
cwd = Path.cwd()
|
|
673
|
+
|
|
674
|
+
# Get git repo name (if in a git repo)
|
|
675
|
+
git_repo = None
|
|
676
|
+
try:
|
|
677
|
+
result = subprocess.run(
|
|
678
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
679
|
+
capture_output=True, text=True, cwd=cwd
|
|
680
|
+
)
|
|
681
|
+
if result.returncode == 0:
|
|
682
|
+
git_repo = Path(result.stdout.strip()).name
|
|
683
|
+
except Exception:
|
|
684
|
+
pass
|
|
685
|
+
|
|
686
|
+
console.print()
|
|
687
|
+
console.print(f"[bold cyan]EmDash Agent[/bold cyan] [dim]v{__version__}[/dim]")
|
|
688
|
+
console.print(f"Mode: [bold]{current_mode.value}[/bold] | Model: [dim]{model or 'default'}[/dim]")
|
|
689
|
+
if git_repo:
|
|
690
|
+
console.print(f"Repo: [bold green]{git_repo}[/bold green] | Path: [dim]{cwd}[/dim]")
|
|
691
|
+
else:
|
|
692
|
+
console.print(f"Path: [dim]{cwd}[/dim]")
|
|
693
|
+
console.print("Type your task or [cyan]/help[/cyan] for commands. Use Ctrl+C to exit.")
|
|
694
|
+
console.print()
|
|
695
|
+
|
|
696
|
+
while True:
|
|
697
|
+
try:
|
|
698
|
+
# Get user input
|
|
699
|
+
user_input = session.prompt(get_prompt()).strip()
|
|
700
|
+
|
|
701
|
+
if not user_input:
|
|
702
|
+
continue
|
|
703
|
+
|
|
704
|
+
# Handle slash commands
|
|
705
|
+
if user_input.startswith("/"):
|
|
706
|
+
if not handle_slash_command(user_input):
|
|
707
|
+
break
|
|
708
|
+
continue
|
|
709
|
+
|
|
710
|
+
# Handle quit shortcuts
|
|
711
|
+
if user_input.lower() in ("quit", "exit", "q"):
|
|
712
|
+
break
|
|
713
|
+
|
|
714
|
+
# Build options with current mode
|
|
715
|
+
request_options = {
|
|
716
|
+
**options,
|
|
717
|
+
"mode": current_mode.value,
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
# Run agent with current mode
|
|
721
|
+
try:
|
|
722
|
+
if session_id:
|
|
723
|
+
stream = client.agent_continue_stream(session_id, user_input)
|
|
724
|
+
else:
|
|
725
|
+
stream = client.agent_chat_stream(
|
|
726
|
+
message=user_input,
|
|
727
|
+
model=model,
|
|
728
|
+
max_iterations=max_iterations,
|
|
729
|
+
options=request_options,
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
# Render the stream and capture any spec output
|
|
733
|
+
result = renderer.render_stream(stream)
|
|
734
|
+
|
|
735
|
+
# Check if we got a session ID back
|
|
736
|
+
if result and result.get("session_id"):
|
|
737
|
+
session_id = result["session_id"]
|
|
738
|
+
|
|
739
|
+
# Check for spec output
|
|
740
|
+
if result and result.get("spec"):
|
|
741
|
+
current_spec = result["spec"]
|
|
742
|
+
|
|
743
|
+
# Handle clarification with options (interactive selection)
|
|
744
|
+
clarification = result.get("clarification")
|
|
745
|
+
if clarification and clarification.get("options") and session_id:
|
|
746
|
+
response = _get_clarification_response(clarification)
|
|
747
|
+
if response:
|
|
748
|
+
# Continue session with user's choice
|
|
749
|
+
stream = client.agent_continue_stream(session_id, response)
|
|
750
|
+
result = renderer.render_stream(stream)
|
|
751
|
+
|
|
752
|
+
# Update mode if user chose tasks or code
|
|
753
|
+
if "tasks" in response.lower():
|
|
754
|
+
current_mode = AgentMode.TASKS
|
|
755
|
+
elif "code" in response.lower():
|
|
756
|
+
current_mode = AgentMode.CODE
|
|
757
|
+
|
|
758
|
+
# Handle plan mode completion (show approval menu)
|
|
759
|
+
# In plan mode, show menu after any substantial response
|
|
760
|
+
content = result.get("content", "")
|
|
761
|
+
should_show_plan_menu = (
|
|
762
|
+
current_mode == AgentMode.PLAN and
|
|
763
|
+
session_id and
|
|
764
|
+
len(content) > 100 # Has substantial content
|
|
765
|
+
)
|
|
766
|
+
if should_show_plan_menu:
|
|
767
|
+
choice, feedback = _show_spec_approval_menu()
|
|
768
|
+
|
|
769
|
+
if choice == "tasks":
|
|
770
|
+
current_mode = AgentMode.TASKS
|
|
771
|
+
# Include the spec content explicitly in the message
|
|
772
|
+
tasks_prompt = f"""Generate implementation tasks from this approved specification.
|
|
773
|
+
|
|
774
|
+
## Approved Specification
|
|
775
|
+
|
|
776
|
+
{content}
|
|
777
|
+
|
|
778
|
+
## Your Task
|
|
779
|
+
|
|
780
|
+
Use your tools to explore the codebase and create implementation tasks:
|
|
781
|
+
|
|
782
|
+
1. **Explore**: Use semantic_search and code graph tools to find:
|
|
783
|
+
- Existing related code to modify
|
|
784
|
+
- Patterns and conventions used in the codebase
|
|
785
|
+
- Files that will need changes
|
|
786
|
+
|
|
787
|
+
2. **Generate Tasks**: Create detailed tasks that include:
|
|
788
|
+
- Task ID (T1, T2, T3...)
|
|
789
|
+
- Description of what to implement
|
|
790
|
+
- Specific files to modify (based on your exploration)
|
|
791
|
+
- Dependencies on other tasks
|
|
792
|
+
- Complexity estimate (S/M/L)
|
|
793
|
+
- Acceptance criteria
|
|
794
|
+
|
|
795
|
+
3. **Order**: Arrange tasks in implementation order, starting with foundational changes.
|
|
796
|
+
|
|
797
|
+
Output a comprehensive task list that a developer can follow step-by-step."""
|
|
798
|
+
stream = client.agent_continue_stream(
|
|
799
|
+
session_id,
|
|
800
|
+
tasks_prompt
|
|
801
|
+
)
|
|
802
|
+
result = renderer.render_stream(stream)
|
|
803
|
+
# After generating tasks, show tasks menu
|
|
804
|
+
content = result.get("content", "")
|
|
805
|
+
elif choice == "code":
|
|
806
|
+
current_mode = AgentMode.CODE
|
|
807
|
+
stream = client.agent_continue_stream(
|
|
808
|
+
session_id,
|
|
809
|
+
"Start implementing the approved spec."
|
|
810
|
+
)
|
|
811
|
+
renderer.render_stream(stream)
|
|
812
|
+
elif choice == "refine":
|
|
813
|
+
stream = client.agent_continue_stream(
|
|
814
|
+
session_id,
|
|
815
|
+
f"Please update the spec based on this feedback: {feedback}"
|
|
816
|
+
)
|
|
817
|
+
renderer.render_stream(stream)
|
|
818
|
+
elif choice == "abort":
|
|
819
|
+
console.print("[dim]Spec discarded[/dim]")
|
|
820
|
+
session_id = None
|
|
821
|
+
current_spec = None
|
|
822
|
+
|
|
823
|
+
# Handle tasks mode completion (show tasks approval menu)
|
|
824
|
+
should_show_tasks_menu = (
|
|
825
|
+
current_mode == AgentMode.TASKS and
|
|
826
|
+
session_id and
|
|
827
|
+
len(content) > 100 # Has substantial content
|
|
828
|
+
)
|
|
829
|
+
if should_show_tasks_menu:
|
|
830
|
+
choice, feedback = _show_tasks_approval_menu()
|
|
831
|
+
|
|
832
|
+
if choice == "code":
|
|
833
|
+
current_mode = AgentMode.CODE
|
|
834
|
+
stream = client.agent_continue_stream(
|
|
835
|
+
session_id,
|
|
836
|
+
"Start implementing the first task from the task list."
|
|
837
|
+
)
|
|
838
|
+
renderer.render_stream(stream)
|
|
839
|
+
elif choice == "refine":
|
|
840
|
+
stream = client.agent_continue_stream(
|
|
841
|
+
session_id,
|
|
842
|
+
f"Please update the tasks based on this feedback: {feedback}"
|
|
843
|
+
)
|
|
844
|
+
renderer.render_stream(stream)
|
|
845
|
+
elif choice == "export":
|
|
846
|
+
console.print("[dim]Exporting tasks...[/dim]")
|
|
847
|
+
# TODO: Implement export functionality
|
|
848
|
+
console.print("[yellow]Export not implemented yet[/yellow]")
|
|
849
|
+
elif choice == "abort":
|
|
850
|
+
console.print("[dim]Tasks discarded[/dim]")
|
|
851
|
+
session_id = None
|
|
852
|
+
|
|
853
|
+
console.print()
|
|
854
|
+
|
|
855
|
+
except Exception as e:
|
|
856
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
857
|
+
|
|
858
|
+
except KeyboardInterrupt:
|
|
859
|
+
console.print("\n[dim]Interrupted[/dim]")
|
|
860
|
+
break
|
|
861
|
+
except EOFError:
|
|
862
|
+
break
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
@agent.command("sessions")
|
|
866
|
+
def list_sessions():
|
|
867
|
+
"""List active agent sessions."""
|
|
868
|
+
server = get_server_manager()
|
|
869
|
+
base_url = server.get_server_url()
|
|
870
|
+
|
|
871
|
+
client = EmdashClient(base_url)
|
|
872
|
+
sessions = client.list_sessions()
|
|
873
|
+
|
|
874
|
+
if not sessions:
|
|
875
|
+
console.print("[dim]No active sessions[/dim]")
|
|
876
|
+
return
|
|
877
|
+
|
|
878
|
+
for s in sessions:
|
|
879
|
+
console.print(
|
|
880
|
+
f" {s['session_id'][:8]}... "
|
|
881
|
+
f"[dim]({s.get('model', 'unknown')}, "
|
|
882
|
+
f"{s.get('message_count', 0)} messages)[/dim]"
|
|
883
|
+
)
|