emdash-cli 0.1.17__py3-none-any.whl → 0.1.30__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 +42 -20
- emdash_cli/clipboard.py +123 -0
- emdash_cli/commands/__init__.py +2 -0
- emdash_cli/commands/agent.py +193 -236
- emdash_cli/commands/skills.py +337 -0
- emdash_cli/keyboard.py +146 -0
- emdash_cli/main.py +5 -1
- emdash_cli/sse_renderer.py +124 -11
- {emdash_cli-0.1.17.dist-info → emdash_cli-0.1.30.dist-info}/METADATA +4 -2
- {emdash_cli-0.1.17.dist-info → emdash_cli-0.1.30.dist-info}/RECORD +12 -9
- {emdash_cli-0.1.17.dist-info → emdash_cli-0.1.30.dist-info}/WHEEL +0 -0
- {emdash_cli-0.1.17.dist-info → emdash_cli-0.1.30.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""Skills management CLI commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_skills_dir() -> Path:
|
|
13
|
+
"""Get the skills directory."""
|
|
14
|
+
return Path.cwd() / ".emdash" / "skills"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group()
|
|
18
|
+
def skills():
|
|
19
|
+
"""Manage agent skills."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@skills.command("list")
|
|
24
|
+
def skills_list():
|
|
25
|
+
"""List all available skills."""
|
|
26
|
+
from emdash_core.agent.skills import SkillRegistry
|
|
27
|
+
|
|
28
|
+
skills_dir = _get_skills_dir()
|
|
29
|
+
registry = SkillRegistry.get_instance()
|
|
30
|
+
registry.load_skills(skills_dir)
|
|
31
|
+
|
|
32
|
+
all_skills = registry.get_all_skills()
|
|
33
|
+
|
|
34
|
+
if not all_skills:
|
|
35
|
+
console.print("[yellow]No skills found.[/yellow]")
|
|
36
|
+
console.print(f"[dim]Create skills in {skills_dir}/<skill-name>/SKILL.md[/dim]")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
table = Table(title="Available Skills")
|
|
40
|
+
table.add_column("Name", style="cyan")
|
|
41
|
+
table.add_column("Description")
|
|
42
|
+
table.add_column("User Invocable", style="green")
|
|
43
|
+
table.add_column("Tools")
|
|
44
|
+
|
|
45
|
+
for skill in all_skills.values():
|
|
46
|
+
invocable = "Yes (/{})".format(skill.name) if skill.user_invocable else "No"
|
|
47
|
+
tools = ", ".join(skill.tools) if skill.tools else "-"
|
|
48
|
+
table.add_row(skill.name, skill.description, invocable, tools)
|
|
49
|
+
|
|
50
|
+
console.print(table)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@skills.command("show")
|
|
54
|
+
@click.argument("name")
|
|
55
|
+
def skills_show(name: str):
|
|
56
|
+
"""Show details of a specific skill."""
|
|
57
|
+
from emdash_core.agent.skills import SkillRegistry
|
|
58
|
+
|
|
59
|
+
skills_dir = _get_skills_dir()
|
|
60
|
+
registry = SkillRegistry.get_instance()
|
|
61
|
+
registry.load_skills(skills_dir)
|
|
62
|
+
|
|
63
|
+
skill = registry.get_skill(name)
|
|
64
|
+
|
|
65
|
+
if skill is None:
|
|
66
|
+
console.print(f"[red]Skill '{name}' not found.[/red]")
|
|
67
|
+
available = registry.list_skills()
|
|
68
|
+
if available:
|
|
69
|
+
console.print(f"[dim]Available skills: {', '.join(available)}[/dim]")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
# Show skill details
|
|
73
|
+
console.print(Panel(
|
|
74
|
+
f"[bold]Description:[/bold] {skill.description}\n\n"
|
|
75
|
+
f"[bold]User Invocable:[/bold] {'Yes (/' + skill.name + ')' if skill.user_invocable else 'No'}\n\n"
|
|
76
|
+
f"[bold]Tools:[/bold] {', '.join(skill.tools) if skill.tools else 'None'}\n\n"
|
|
77
|
+
f"[bold]File:[/bold] {skill.file_path}",
|
|
78
|
+
title=f"[cyan]{skill.name}[/cyan]",
|
|
79
|
+
border_style="cyan",
|
|
80
|
+
))
|
|
81
|
+
|
|
82
|
+
console.print()
|
|
83
|
+
console.print("[bold]Instructions:[/bold]")
|
|
84
|
+
console.print(Panel(
|
|
85
|
+
skill.instructions,
|
|
86
|
+
border_style="dim",
|
|
87
|
+
))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@skills.command("create")
|
|
91
|
+
@click.argument("name")
|
|
92
|
+
@click.option("--description", "-d", default="", help="Skill description")
|
|
93
|
+
@click.option("--user-invocable/--no-user-invocable", default=True, help="Can be invoked with /name")
|
|
94
|
+
@click.option("--tools", "-t", multiple=True, help="Tools this skill needs (can specify multiple)")
|
|
95
|
+
def skills_create(name: str, description: str, user_invocable: bool, tools: tuple):
|
|
96
|
+
"""Create a new skill.
|
|
97
|
+
|
|
98
|
+
Creates a skill directory with SKILL.md template.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
emdash skills create commit -d "Generate commit messages" -t execute_command -t read_file
|
|
102
|
+
"""
|
|
103
|
+
# Validate name
|
|
104
|
+
name = name.lower().strip()
|
|
105
|
+
if len(name) > 64:
|
|
106
|
+
console.print("[red]Skill name must be 64 characters or less.[/red]")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
if not name.replace("-", "").replace("_", "").isalnum():
|
|
110
|
+
console.print("[red]Skill name must contain only lowercase letters, numbers, hyphens, and underscores.[/red]")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
skills_dir = _get_skills_dir()
|
|
114
|
+
skill_dir = skills_dir / name
|
|
115
|
+
skill_file = skill_dir / "SKILL.md"
|
|
116
|
+
|
|
117
|
+
if skill_dir.exists():
|
|
118
|
+
console.print(f"[red]Skill '{name}' already exists at {skill_dir}[/red]")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
# Build content
|
|
122
|
+
tools_str = ", ".join(tools) if tools else ""
|
|
123
|
+
description = description or f"Description for {name} skill"
|
|
124
|
+
|
|
125
|
+
content = f"""---
|
|
126
|
+
name: {name}
|
|
127
|
+
description: {description}
|
|
128
|
+
user_invocable: {str(user_invocable).lower()}
|
|
129
|
+
tools: [{tools_str}]
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
# {name.replace('-', ' ').title()}
|
|
133
|
+
|
|
134
|
+
{description}
|
|
135
|
+
|
|
136
|
+
## Instructions
|
|
137
|
+
|
|
138
|
+
Add your skill instructions here. These will be provided to the agent when the skill is invoked.
|
|
139
|
+
|
|
140
|
+
## Usage
|
|
141
|
+
|
|
142
|
+
Describe how this skill should be used.
|
|
143
|
+
|
|
144
|
+
## Examples
|
|
145
|
+
|
|
146
|
+
Provide example scenarios here.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
skill_file.write_text(content)
|
|
152
|
+
console.print(f"[green]Created skill '{name}' at {skill_file}[/green]")
|
|
153
|
+
console.print(f"[dim]Edit the SKILL.md file to customize the skill instructions.[/dim]")
|
|
154
|
+
except Exception as e:
|
|
155
|
+
console.print(f"[red]Error creating skill: {e}[/red]")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@skills.command("delete")
|
|
159
|
+
@click.argument("name")
|
|
160
|
+
@click.option("--force", "-f", is_flag=True, help="Delete without confirmation")
|
|
161
|
+
def skills_delete(name: str, force: bool):
|
|
162
|
+
"""Delete a skill."""
|
|
163
|
+
skills_dir = _get_skills_dir()
|
|
164
|
+
skill_dir = skills_dir / name
|
|
165
|
+
|
|
166
|
+
if not skill_dir.exists():
|
|
167
|
+
console.print(f"[red]Skill '{name}' not found.[/red]")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
if not force:
|
|
171
|
+
if not click.confirm(f"Are you sure you want to delete skill '{name}'?"):
|
|
172
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
import shutil
|
|
176
|
+
shutil.rmtree(skill_dir)
|
|
177
|
+
console.print(f"[green]Deleted skill '{name}'.[/green]")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@skills.command("init")
|
|
181
|
+
def skills_init():
|
|
182
|
+
"""Initialize skills directory with example skills."""
|
|
183
|
+
skills_dir = _get_skills_dir()
|
|
184
|
+
|
|
185
|
+
if skills_dir.exists() and list(skills_dir.iterdir()):
|
|
186
|
+
console.print(f"[yellow]Skills directory already exists at {skills_dir}[/yellow]")
|
|
187
|
+
if not click.confirm("Do you want to add example skills anyway?"):
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
|
|
192
|
+
# Create example commit skill
|
|
193
|
+
commit_dir = skills_dir / "commit"
|
|
194
|
+
if not commit_dir.exists():
|
|
195
|
+
commit_dir.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
(commit_dir / "SKILL.md").write_text("""---
|
|
197
|
+
name: commit
|
|
198
|
+
description: Generate commit messages following conventional commits format
|
|
199
|
+
user_invocable: true
|
|
200
|
+
tools: [execute_command, read_file]
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
# Commit Message Generation
|
|
204
|
+
|
|
205
|
+
Generate clear, conventional commit messages based on staged changes.
|
|
206
|
+
|
|
207
|
+
## Instructions
|
|
208
|
+
|
|
209
|
+
1. Run `git diff --cached` to see staged changes
|
|
210
|
+
2. Analyze the changes to understand what was modified
|
|
211
|
+
3. Generate a commit message following conventional commits format:
|
|
212
|
+
- feat: A new feature
|
|
213
|
+
- fix: A bug fix
|
|
214
|
+
- docs: Documentation only changes
|
|
215
|
+
- style: Changes that don't affect meaning (formatting, etc)
|
|
216
|
+
- refactor: Code change that neither fixes a bug nor adds a feature
|
|
217
|
+
- test: Adding or modifying tests
|
|
218
|
+
- chore: Changes to build process or auxiliary tools
|
|
219
|
+
|
|
220
|
+
4. Format: `<type>(<scope>): <description>`
|
|
221
|
+
|
|
222
|
+
## Examples
|
|
223
|
+
|
|
224
|
+
- `feat(auth): add OAuth2 support`
|
|
225
|
+
- `fix(api): handle null response in user endpoint`
|
|
226
|
+
- `docs(readme): update installation instructions`
|
|
227
|
+
""")
|
|
228
|
+
console.print("[green]Created example skill: commit[/green]")
|
|
229
|
+
|
|
230
|
+
# Create example review-pr skill
|
|
231
|
+
review_dir = skills_dir / "review-pr"
|
|
232
|
+
if not review_dir.exists():
|
|
233
|
+
review_dir.mkdir(parents=True, exist_ok=True)
|
|
234
|
+
(review_dir / "SKILL.md").write_text("""---
|
|
235
|
+
name: review-pr
|
|
236
|
+
description: Review pull requests with code quality and security focus
|
|
237
|
+
user_invocable: true
|
|
238
|
+
tools: [read_file, semantic_search, grep]
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
# Pull Request Review
|
|
242
|
+
|
|
243
|
+
Conduct thorough code reviews focusing on quality, security, and best practices.
|
|
244
|
+
|
|
245
|
+
## Instructions
|
|
246
|
+
|
|
247
|
+
1. Review the PR changes systematically
|
|
248
|
+
2. Check for:
|
|
249
|
+
- Code quality and readability
|
|
250
|
+
- Security vulnerabilities (injection, XSS, etc.)
|
|
251
|
+
- Performance implications
|
|
252
|
+
- Test coverage
|
|
253
|
+
- Documentation updates
|
|
254
|
+
3. Provide constructive feedback with specific suggestions
|
|
255
|
+
4. Highlight both issues and good practices
|
|
256
|
+
|
|
257
|
+
## Review Checklist
|
|
258
|
+
|
|
259
|
+
- [ ] Code follows project conventions
|
|
260
|
+
- [ ] No obvious security vulnerabilities
|
|
261
|
+
- [ ] Error handling is appropriate
|
|
262
|
+
- [ ] Tests cover new functionality
|
|
263
|
+
- [ ] No unnecessary complexity
|
|
264
|
+
- [ ] Documentation is updated if needed
|
|
265
|
+
|
|
266
|
+
## Output Format
|
|
267
|
+
|
|
268
|
+
Provide feedback in sections:
|
|
269
|
+
1. **Summary**: Overall assessment
|
|
270
|
+
2. **Positives**: What's done well
|
|
271
|
+
3. **Concerns**: Issues that should be addressed
|
|
272
|
+
4. **Suggestions**: Optional improvements
|
|
273
|
+
""")
|
|
274
|
+
console.print("[green]Created example skill: review-pr[/green]")
|
|
275
|
+
|
|
276
|
+
# Create example security-review skill
|
|
277
|
+
security_dir = skills_dir / "security-review"
|
|
278
|
+
if not security_dir.exists():
|
|
279
|
+
security_dir.mkdir(parents=True, exist_ok=True)
|
|
280
|
+
(security_dir / "SKILL.md").write_text("""---
|
|
281
|
+
name: security-review
|
|
282
|
+
description: Security-focused code review for vulnerabilities
|
|
283
|
+
user_invocable: true
|
|
284
|
+
tools: [read_file, grep, semantic_search]
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
# Security Review
|
|
288
|
+
|
|
289
|
+
Conduct security-focused code review to identify vulnerabilities.
|
|
290
|
+
|
|
291
|
+
## Instructions
|
|
292
|
+
|
|
293
|
+
1. Search for common vulnerability patterns:
|
|
294
|
+
- SQL injection
|
|
295
|
+
- XSS (Cross-Site Scripting)
|
|
296
|
+
- Command injection
|
|
297
|
+
- Path traversal
|
|
298
|
+
- Insecure deserialization
|
|
299
|
+
- Hardcoded secrets
|
|
300
|
+
- Improper input validation
|
|
301
|
+
|
|
302
|
+
2. Review authentication and authorization:
|
|
303
|
+
- Session management
|
|
304
|
+
- Password handling
|
|
305
|
+
- Access control
|
|
306
|
+
|
|
307
|
+
3. Check data handling:
|
|
308
|
+
- Sensitive data exposure
|
|
309
|
+
- Encryption usage
|
|
310
|
+
- Data validation
|
|
311
|
+
|
|
312
|
+
## OWASP Top 10 Checklist
|
|
313
|
+
|
|
314
|
+
- [ ] A01: Broken Access Control
|
|
315
|
+
- [ ] A02: Cryptographic Failures
|
|
316
|
+
- [ ] A03: Injection
|
|
317
|
+
- [ ] A04: Insecure Design
|
|
318
|
+
- [ ] A05: Security Misconfiguration
|
|
319
|
+
- [ ] A06: Vulnerable Components
|
|
320
|
+
- [ ] A07: Authentication Failures
|
|
321
|
+
- [ ] A08: Software Integrity Failures
|
|
322
|
+
- [ ] A09: Logging Failures
|
|
323
|
+
- [ ] A10: SSRF
|
|
324
|
+
|
|
325
|
+
## Output
|
|
326
|
+
|
|
327
|
+
Report findings with:
|
|
328
|
+
- Severity (Critical/High/Medium/Low)
|
|
329
|
+
- Location (file:line)
|
|
330
|
+
- Description
|
|
331
|
+
- Remediation suggestion
|
|
332
|
+
""")
|
|
333
|
+
console.print("[green]Created example skill: security-review[/green]")
|
|
334
|
+
|
|
335
|
+
console.print()
|
|
336
|
+
console.print(f"[cyan]Skills directory initialized at {skills_dir}[/cyan]")
|
|
337
|
+
console.print("[dim]Use 'emdash skills list' to see available skills.[/dim]")
|
emdash_cli/keyboard.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Non-blocking keyboard input detection for ESC interruption."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from typing import Callable, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Unix implementation
|
|
10
|
+
if sys.platform != 'win32':
|
|
11
|
+
import select
|
|
12
|
+
import tty
|
|
13
|
+
import termios
|
|
14
|
+
|
|
15
|
+
def check_key_pressed() -> Optional[str]:
|
|
16
|
+
"""Check if a key was pressed (non-blocking).
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
The key character if pressed, None otherwise
|
|
20
|
+
"""
|
|
21
|
+
if select.select([sys.stdin], [], [], 0)[0]:
|
|
22
|
+
return sys.stdin.read(1)
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
class KeyListener:
|
|
26
|
+
"""Background thread that listens for ESC key press.
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
interrupt_event = threading.Event()
|
|
30
|
+
listener = KeyListener(lambda: interrupt_event.set())
|
|
31
|
+
listener.start()
|
|
32
|
+
# ... do work, checking interrupt_event.is_set() ...
|
|
33
|
+
listener.stop()
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, on_escape: Callable[[], None]):
|
|
37
|
+
"""Initialize the key listener.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
on_escape: Callback to invoke when ESC is pressed
|
|
41
|
+
"""
|
|
42
|
+
self.on_escape = on_escape
|
|
43
|
+
self._running = False
|
|
44
|
+
self._thread: Optional[threading.Thread] = None
|
|
45
|
+
self._old_settings = None
|
|
46
|
+
|
|
47
|
+
def start(self) -> None:
|
|
48
|
+
"""Start listening for keys in background thread."""
|
|
49
|
+
if self._running:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
self._running = True
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
# Save terminal settings
|
|
56
|
+
self._old_settings = termios.tcgetattr(sys.stdin)
|
|
57
|
+
# Set terminal to cbreak mode (no buffering, no echo)
|
|
58
|
+
tty.setcbreak(sys.stdin.fileno())
|
|
59
|
+
except termios.error:
|
|
60
|
+
# Not a TTY (e.g., piped input)
|
|
61
|
+
self._running = False
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
# Start listener thread
|
|
65
|
+
self._thread = threading.Thread(target=self._listen, daemon=True)
|
|
66
|
+
self._thread.start()
|
|
67
|
+
|
|
68
|
+
def stop(self) -> None:
|
|
69
|
+
"""Stop listening and restore terminal settings."""
|
|
70
|
+
self._running = False
|
|
71
|
+
|
|
72
|
+
# Restore terminal settings
|
|
73
|
+
if self._old_settings:
|
|
74
|
+
try:
|
|
75
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self._old_settings)
|
|
76
|
+
except termios.error:
|
|
77
|
+
pass # Terminal may have been closed
|
|
78
|
+
self._old_settings = None
|
|
79
|
+
|
|
80
|
+
# Wait for thread to finish
|
|
81
|
+
if self._thread:
|
|
82
|
+
self._thread.join(timeout=0.2)
|
|
83
|
+
self._thread = None
|
|
84
|
+
|
|
85
|
+
def _listen(self) -> None:
|
|
86
|
+
"""Background listener loop."""
|
|
87
|
+
while self._running:
|
|
88
|
+
try:
|
|
89
|
+
key = check_key_pressed()
|
|
90
|
+
if key == '\x1b': # ESC key
|
|
91
|
+
self.on_escape()
|
|
92
|
+
break
|
|
93
|
+
except Exception:
|
|
94
|
+
# stdin may be closed or unavailable
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
# Small sleep to avoid busy-waiting
|
|
98
|
+
time.sleep(0.05)
|
|
99
|
+
|
|
100
|
+
# Windows implementation
|
|
101
|
+
else:
|
|
102
|
+
import msvcrt
|
|
103
|
+
|
|
104
|
+
class KeyListener:
|
|
105
|
+
"""Background thread that listens for ESC key press (Windows)."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, on_escape: Callable[[], None]):
|
|
108
|
+
"""Initialize the key listener.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
on_escape: Callback to invoke when ESC is pressed
|
|
112
|
+
"""
|
|
113
|
+
self.on_escape = on_escape
|
|
114
|
+
self._running = False
|
|
115
|
+
self._thread: Optional[threading.Thread] = None
|
|
116
|
+
|
|
117
|
+
def start(self) -> None:
|
|
118
|
+
"""Start listening for keys in background thread."""
|
|
119
|
+
if self._running:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
self._running = True
|
|
123
|
+
self._thread = threading.Thread(target=self._listen, daemon=True)
|
|
124
|
+
self._thread.start()
|
|
125
|
+
|
|
126
|
+
def stop(self) -> None:
|
|
127
|
+
"""Stop listening."""
|
|
128
|
+
self._running = False
|
|
129
|
+
if self._thread:
|
|
130
|
+
self._thread.join(timeout=0.2)
|
|
131
|
+
self._thread = None
|
|
132
|
+
|
|
133
|
+
def _listen(self) -> None:
|
|
134
|
+
"""Background listener loop."""
|
|
135
|
+
while self._running:
|
|
136
|
+
try:
|
|
137
|
+
if msvcrt.kbhit():
|
|
138
|
+
key = msvcrt.getch()
|
|
139
|
+
if key == b'\x1b': # ESC
|
|
140
|
+
self.on_escape()
|
|
141
|
+
break
|
|
142
|
+
except Exception:
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
# Small sleep to avoid busy-waiting
|
|
146
|
+
time.sleep(0.05)
|
emdash_cli/main.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Main CLI entry point for emdash-cli."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
4
|
+
|
|
3
5
|
import click
|
|
4
6
|
|
|
5
7
|
from .commands import (
|
|
@@ -13,6 +15,7 @@ from .commands import (
|
|
|
13
15
|
rules,
|
|
14
16
|
search,
|
|
15
17
|
server,
|
|
18
|
+
skills,
|
|
16
19
|
team,
|
|
17
20
|
swarm,
|
|
18
21
|
projectmd,
|
|
@@ -42,6 +45,7 @@ cli.add_command(index)
|
|
|
42
45
|
cli.add_command(plan)
|
|
43
46
|
cli.add_command(rules)
|
|
44
47
|
cli.add_command(server)
|
|
48
|
+
cli.add_command(skills)
|
|
45
49
|
cli.add_command(team)
|
|
46
50
|
cli.add_command(swarm)
|
|
47
51
|
|
|
@@ -64,7 +68,7 @@ cli.add_command(server_killall, name="killall")
|
|
|
64
68
|
@click.option("--mode", type=click.Choice(["plan", "tasks", "code"]), default="code",
|
|
65
69
|
help="Starting mode")
|
|
66
70
|
@click.option("--quiet", "-q", is_flag=True, help="Less verbose output")
|
|
67
|
-
@click.option("--max-iterations", default=
|
|
71
|
+
@click.option("--max-iterations", default=int(os.getenv("EMDASH_MAX_ITERATIONS", "100")), help="Max agent iterations")
|
|
68
72
|
@click.option("--no-graph-tools", is_flag=True, help="Skip graph exploration tools")
|
|
69
73
|
@click.option("--save", is_flag=True, help="Save specs to specs/<feature>/")
|
|
70
74
|
def start_coding_agent(
|