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.
- emdash_cli/client.py +35 -0
- emdash_cli/clipboard.py +30 -61
- emdash_cli/commands/agent/__init__.py +14 -0
- emdash_cli/commands/agent/cli.py +100 -0
- emdash_cli/commands/agent/constants.py +53 -0
- emdash_cli/commands/agent/file_utils.py +178 -0
- emdash_cli/commands/agent/handlers/__init__.py +41 -0
- emdash_cli/commands/agent/handlers/agents.py +421 -0
- emdash_cli/commands/agent/handlers/auth.py +69 -0
- emdash_cli/commands/agent/handlers/doctor.py +319 -0
- emdash_cli/commands/agent/handlers/hooks.py +121 -0
- emdash_cli/commands/agent/handlers/mcp.py +183 -0
- emdash_cli/commands/agent/handlers/misc.py +200 -0
- emdash_cli/commands/agent/handlers/rules.py +394 -0
- emdash_cli/commands/agent/handlers/sessions.py +168 -0
- emdash_cli/commands/agent/handlers/setup.py +582 -0
- emdash_cli/commands/agent/handlers/skills.py +440 -0
- emdash_cli/commands/agent/handlers/todos.py +98 -0
- emdash_cli/commands/agent/handlers/verify.py +648 -0
- emdash_cli/commands/agent/interactive.py +657 -0
- emdash_cli/commands/agent/menus.py +728 -0
- emdash_cli/commands/agent.py +7 -1321
- emdash_cli/commands/server.py +99 -40
- emdash_cli/server_manager.py +70 -10
- emdash_cli/sse_renderer.py +36 -5
- {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.46.dist-info}/METADATA +2 -4
- emdash_cli-0.1.46.dist-info/RECORD +49 -0
- emdash_cli-0.1.35.dist-info/RECORD +0 -30
- {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.46.dist-info}/WHEEL +0 -0
- {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)
|