emdash-cli 0.1.35__py3-none-any.whl → 0.1.67__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 (50) hide show
  1. emdash_cli/client.py +41 -22
  2. emdash_cli/clipboard.py +30 -61
  3. emdash_cli/commands/__init__.py +2 -2
  4. emdash_cli/commands/agent/__init__.py +14 -0
  5. emdash_cli/commands/agent/cli.py +100 -0
  6. emdash_cli/commands/agent/constants.py +63 -0
  7. emdash_cli/commands/agent/file_utils.py +178 -0
  8. emdash_cli/commands/agent/handlers/__init__.py +51 -0
  9. emdash_cli/commands/agent/handlers/agents.py +449 -0
  10. emdash_cli/commands/agent/handlers/auth.py +69 -0
  11. emdash_cli/commands/agent/handlers/doctor.py +319 -0
  12. emdash_cli/commands/agent/handlers/hooks.py +121 -0
  13. emdash_cli/commands/agent/handlers/index.py +183 -0
  14. emdash_cli/commands/agent/handlers/mcp.py +183 -0
  15. emdash_cli/commands/agent/handlers/misc.py +319 -0
  16. emdash_cli/commands/agent/handlers/registry.py +72 -0
  17. emdash_cli/commands/agent/handlers/rules.py +411 -0
  18. emdash_cli/commands/agent/handlers/sessions.py +168 -0
  19. emdash_cli/commands/agent/handlers/setup.py +715 -0
  20. emdash_cli/commands/agent/handlers/skills.py +478 -0
  21. emdash_cli/commands/agent/handlers/telegram.py +475 -0
  22. emdash_cli/commands/agent/handlers/todos.py +119 -0
  23. emdash_cli/commands/agent/handlers/verify.py +653 -0
  24. emdash_cli/commands/agent/help.py +236 -0
  25. emdash_cli/commands/agent/interactive.py +842 -0
  26. emdash_cli/commands/agent/menus.py +760 -0
  27. emdash_cli/commands/agent/onboarding.py +619 -0
  28. emdash_cli/commands/agent/session_restore.py +210 -0
  29. emdash_cli/commands/agent.py +7 -1321
  30. emdash_cli/commands/index.py +111 -13
  31. emdash_cli/commands/registry.py +635 -0
  32. emdash_cli/commands/server.py +99 -40
  33. emdash_cli/commands/skills.py +72 -6
  34. emdash_cli/design.py +328 -0
  35. emdash_cli/diff_renderer.py +438 -0
  36. emdash_cli/integrations/__init__.py +1 -0
  37. emdash_cli/integrations/telegram/__init__.py +15 -0
  38. emdash_cli/integrations/telegram/bot.py +402 -0
  39. emdash_cli/integrations/telegram/bridge.py +865 -0
  40. emdash_cli/integrations/telegram/config.py +155 -0
  41. emdash_cli/integrations/telegram/formatter.py +385 -0
  42. emdash_cli/main.py +52 -2
  43. emdash_cli/server_manager.py +70 -10
  44. emdash_cli/sse_renderer.py +659 -167
  45. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/METADATA +2 -4
  46. emdash_cli-0.1.67.dist-info/RECORD +63 -0
  47. emdash_cli/commands/swarm.py +0 -86
  48. emdash_cli-0.1.35.dist-info/RECORD +0 -30
  49. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/WHEEL +0 -0
  50. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,449 @@
1
+ """Handler for /agents command."""
2
+
3
+ import os
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+
10
+ from ..menus import show_agents_interactive_menu, prompt_agent_name, confirm_delete
11
+ from ....design import (
12
+ Colors,
13
+ header,
14
+ footer,
15
+ SEPARATOR_WIDTH,
16
+ STATUS_ACTIVE,
17
+ ARROW_PROMPT,
18
+ )
19
+
20
+ console = Console()
21
+
22
+
23
+ def create_agent(name: str) -> bool:
24
+ """Create a new agent with the given name."""
25
+ agents_dir = Path.cwd() / ".emdash" / "agents"
26
+ agent_file = agents_dir / f"{name}.md"
27
+
28
+ if agent_file.exists():
29
+ console.print(f"[yellow]Agent '{name}' already exists[/yellow]")
30
+ return False
31
+
32
+ agents_dir.mkdir(parents=True, exist_ok=True)
33
+
34
+ template = f'''---
35
+ description: Custom agent for specific tasks
36
+ tools: [grep, glob, read_file, semantic_search]
37
+ # rules: [typescript, security] # Optional: reference rules from .emdash/rules/
38
+ # skills: [code-review] # Optional: reference skills from .emdash/skills/
39
+ # verifiers: [eslint] # Optional: reference verifiers from .emdash/verifiers.json
40
+ ---
41
+
42
+ # System Prompt
43
+
44
+ You are a specialized assistant for {name.replace("-", " ")} tasks.
45
+
46
+ ## Your Mission
47
+
48
+ Describe what this agent should accomplish:
49
+ - Task 1
50
+ - Task 2
51
+ - Task 3
52
+
53
+ ## Approach
54
+
55
+ 1. **Step One**
56
+ - Details about the first step
57
+
58
+ 2. **Step Two**
59
+ - Details about the second step
60
+
61
+ ## Output Format
62
+
63
+ Describe how the agent should format its responses.
64
+ '''
65
+ agent_file.write_text(template)
66
+ console.print()
67
+ console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] [{Colors.TEXT}]created:[/{Colors.TEXT}] {name}")
68
+ console.print(f" [{Colors.DIM}]{agent_file}[/{Colors.DIM}]")
69
+ console.print()
70
+ return True
71
+
72
+
73
+ def show_agent_details(name: str) -> None:
74
+ """Show detailed view of an agent."""
75
+ from emdash_core.agent.toolkits import get_custom_agent
76
+
77
+ builtin_agents = ["Explore", "Plan"]
78
+
79
+ console.print()
80
+ console.print(f"[{Colors.MUTED}]{header(name, SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
81
+ console.print()
82
+
83
+ if name in builtin_agents:
84
+ console.print(f" [{Colors.DIM}]type[/{Colors.DIM}] [{Colors.MUTED}]built-in[/{Colors.MUTED}]")
85
+ if name == "Explore":
86
+ console.print(f" [{Colors.DIM}]desc[/{Colors.DIM}] Fast codebase exploration (read-only)")
87
+ console.print(f" [{Colors.DIM}]tools[/{Colors.DIM}] glob, grep, read_file, list_files, semantic_search")
88
+ elif name == "Plan":
89
+ console.print(f" [{Colors.DIM}]desc[/{Colors.DIM}] Design implementation plans")
90
+ console.print(f" [{Colors.DIM}]tools[/{Colors.DIM}] glob, grep, read_file, list_files, semantic_search")
91
+ console.print()
92
+ console.print(f" [{Colors.DIM}]Built-in agents cannot be edited or deleted.[/{Colors.DIM}]")
93
+ else:
94
+ agent = get_custom_agent(name, Path.cwd())
95
+ if agent:
96
+ console.print(f" [{Colors.DIM}]type[/{Colors.DIM}] [{Colors.PRIMARY}]custom[/{Colors.PRIMARY}]")
97
+
98
+ if agent.description:
99
+ console.print(f" [{Colors.DIM}]desc[/{Colors.DIM}] {agent.description}")
100
+
101
+ if agent.model:
102
+ console.print(f" [{Colors.DIM}]model[/{Colors.DIM}] {agent.model}")
103
+
104
+ if agent.tools:
105
+ console.print(f" [{Colors.DIM}]tools[/{Colors.DIM}] {', '.join(agent.tools)}")
106
+
107
+ if agent.mcp_servers:
108
+ console.print()
109
+ console.print(f" [{Colors.DIM}]mcp servers:[/{Colors.DIM}]")
110
+ for server in agent.mcp_servers:
111
+ status = f"[{Colors.SUCCESS}]●[/{Colors.SUCCESS}]" if server.enabled else f"[{Colors.MUTED}]○[/{Colors.MUTED}]"
112
+ console.print(f" {status} [{Colors.PRIMARY}]{server.name}[/{Colors.PRIMARY}]")
113
+ console.print(f" [{Colors.DIM}]{server.command} {' '.join(server.args)}[/{Colors.DIM}]")
114
+
115
+ if agent.rules:
116
+ console.print(f" [{Colors.DIM}]rules[/{Colors.DIM}] {', '.join(agent.rules)}")
117
+
118
+ if agent.skills:
119
+ console.print(f" [{Colors.DIM}]skills[/{Colors.DIM}] {', '.join(agent.skills)}")
120
+
121
+ if agent.verifiers:
122
+ console.print(f" [{Colors.DIM}]verify[/{Colors.DIM}] {', '.join(agent.verifiers)}")
123
+
124
+ if agent.file_path:
125
+ console.print()
126
+ console.print(f" [{Colors.DIM}]file[/{Colors.DIM}] {agent.file_path}")
127
+
128
+ if agent.system_prompt:
129
+ console.print()
130
+ console.print(f" [{Colors.DIM}]prompt preview:[/{Colors.DIM}]")
131
+ preview = agent.system_prompt[:250]
132
+ if len(agent.system_prompt) > 250:
133
+ preview += "..."
134
+ for line in preview.split('\n')[:6]:
135
+ console.print(f" [{Colors.MUTED}]{line}[/{Colors.MUTED}]")
136
+ else:
137
+ console.print(f" [{Colors.WARNING}]Agent '{name}' not found[/{Colors.WARNING}]")
138
+
139
+ console.print()
140
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
141
+
142
+
143
+ def delete_agent(name: str) -> bool:
144
+ """Delete a custom agent."""
145
+ agents_dir = Path.cwd() / ".emdash" / "agents"
146
+ agent_file = agents_dir / f"{name}.md"
147
+
148
+ if not agent_file.exists():
149
+ console.print(f"[yellow]Agent file not found: {agent_file}[/yellow]")
150
+ return False
151
+
152
+ if confirm_delete(name):
153
+ agent_file.unlink()
154
+ console.print(f"[green]Deleted agent: {name}[/green]")
155
+ return True
156
+ else:
157
+ console.print("[dim]Cancelled[/dim]")
158
+ return False
159
+
160
+
161
+ def edit_agent(name: str) -> None:
162
+ """Open agent file in editor."""
163
+ agents_dir = Path.cwd() / ".emdash" / "agents"
164
+ agent_file = agents_dir / f"{name}.md"
165
+
166
+ if not agent_file.exists():
167
+ console.print(f"[yellow]Agent file not found: {agent_file}[/yellow]")
168
+ return
169
+
170
+ # Try to open in editor
171
+ editor = os.environ.get("EDITOR", "")
172
+ if not editor:
173
+ # Try common editors
174
+ for ed in ["code", "vim", "nano", "vi"]:
175
+ try:
176
+ subprocess.run(["which", ed], capture_output=True, check=True)
177
+ editor = ed
178
+ break
179
+ except (subprocess.CalledProcessError, FileNotFoundError):
180
+ continue
181
+
182
+ if editor:
183
+ console.print(f"[dim]Opening {agent_file} in {editor}...[/dim]")
184
+ try:
185
+ subprocess.run([editor, str(agent_file)])
186
+ except Exception as e:
187
+ console.print(f"[red]Failed to open editor: {e}[/red]")
188
+ console.print(f"[dim]Edit manually: {agent_file}[/dim]")
189
+ else:
190
+ console.print(f"[yellow]No editor found. Edit manually:[/yellow]")
191
+ console.print(f" {agent_file}")
192
+
193
+
194
+ def chat_edit_agent(name: str, client, renderer, model, max_iterations, render_with_interrupt) -> None:
195
+ """Start a chat session to edit an agent with AI assistance."""
196
+ from prompt_toolkit import PromptSession
197
+ from prompt_toolkit.styles import Style
198
+
199
+ agents_dir = Path.cwd() / ".emdash" / "agents"
200
+ agent_file = agents_dir / f"{name}.md"
201
+
202
+ if not agent_file.exists():
203
+ console.print(f" [{Colors.WARNING}]Agent file not found: {agent_file}[/{Colors.WARNING}]")
204
+ return
205
+
206
+ # Read current content
207
+ content = agent_file.read_text()
208
+
209
+ console.print()
210
+ console.print(f"[{Colors.MUTED}]{header(f'Edit: {name}', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
211
+ console.print()
212
+ console.print(f" [{Colors.DIM}]Describe changes. Type 'done' to finish.[/{Colors.DIM}]")
213
+ console.print()
214
+
215
+ chat_style = Style.from_dict({
216
+ "prompt": f"{Colors.PRIMARY} bold",
217
+ })
218
+
219
+ ps = PromptSession(style=chat_style)
220
+ chat_session_id = None
221
+ first_message = True
222
+
223
+ # Chat loop
224
+ while True:
225
+ try:
226
+ user_input = ps.prompt([("class:prompt", "› ")]).strip()
227
+
228
+ if not user_input:
229
+ continue
230
+
231
+ if user_input.lower() in ("done", "quit", "exit", "q"):
232
+ console.print("[dim]Finished editing agent[/dim]")
233
+ break
234
+
235
+ # First message includes agent context
236
+ if first_message:
237
+ message_with_context = f"""I want to edit my custom agent "{name}".
238
+
239
+ **File:** `{agent_file}`
240
+
241
+ **Current content:**
242
+ ```markdown
243
+ {content}
244
+ ```
245
+
246
+ **My request:** {user_input}
247
+
248
+ Please make the requested changes using the Edit tool."""
249
+ stream = client.agent_chat_stream(
250
+ message=message_with_context,
251
+ model=model,
252
+ max_iterations=max_iterations,
253
+ options={"mode": "code"},
254
+ )
255
+ first_message = False
256
+ elif chat_session_id:
257
+ stream = client.agent_continue_stream(
258
+ chat_session_id, user_input
259
+ )
260
+ else:
261
+ stream = client.agent_chat_stream(
262
+ message=user_input,
263
+ model=model,
264
+ max_iterations=max_iterations,
265
+ options={"mode": "code"},
266
+ )
267
+
268
+ result = render_with_interrupt(renderer, stream)
269
+ if result and result.get("session_id"):
270
+ chat_session_id = result["session_id"]
271
+
272
+ except (KeyboardInterrupt, EOFError):
273
+ console.print()
274
+ console.print("[dim]Finished editing agent[/dim]")
275
+ break
276
+ except Exception as e:
277
+ console.print(f"[red]Error: {e}[/red]")
278
+
279
+
280
+ def chat_create_agent(client, renderer, model, max_iterations, render_with_interrupt) -> str | None:
281
+ """Start a chat session to create a new agent with AI assistance.
282
+
283
+ Returns:
284
+ The name of the created agent, or None if cancelled.
285
+ """
286
+ from prompt_toolkit import PromptSession
287
+ from prompt_toolkit.styles import Style
288
+
289
+ agents_dir = Path.cwd() / ".emdash" / "agents"
290
+
291
+ console.print()
292
+ console.print(f"[{Colors.MUTED}]{header('Create Agent', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
293
+ console.print()
294
+ console.print(f" [{Colors.DIM}]Describe your agent. AI will help design it.[/{Colors.DIM}]")
295
+ console.print(f" [{Colors.DIM}]Type 'done' to finish.[/{Colors.DIM}]")
296
+ console.print()
297
+
298
+ chat_style = Style.from_dict({
299
+ "prompt": f"{Colors.PRIMARY} bold",
300
+ })
301
+
302
+ ps = PromptSession(style=chat_style)
303
+ chat_session_id = None
304
+ first_message = True
305
+
306
+ # Ensure agents directory exists
307
+ agents_dir.mkdir(parents=True, exist_ok=True)
308
+
309
+ # Chat loop
310
+ while True:
311
+ try:
312
+ user_input = ps.prompt([("class:prompt", "› ")]).strip()
313
+
314
+ if not user_input:
315
+ continue
316
+
317
+ if user_input.lower() in ("done", "quit", "exit", "q"):
318
+ console.print("[dim]Finished[/dim]")
319
+ break
320
+
321
+ # First message includes context about agents
322
+ if first_message:
323
+ message_with_context = f"""I want to create a new custom agent for my project.
324
+
325
+ **Agents directory:** `{agents_dir}`
326
+
327
+ Agents are markdown files with YAML frontmatter that define specialized assistants with custom system prompts and tools.
328
+
329
+ **Agent file format:**
330
+ ```markdown
331
+ ---
332
+ description: Brief description of what this agent does
333
+ model: claude-sonnet # optional, defaults to main model
334
+ tools: [grep, glob, read_file, edit_file, bash] # tools this agent can use
335
+ mcp_servers: # optional, MCP servers for this agent
336
+ - name: server-name
337
+ command: npx
338
+ args: ["-y", "@modelcontextprotocol/server-name"]
339
+ ---
340
+
341
+ # System Prompt
342
+
343
+ You are a specialized assistant for [purpose].
344
+
345
+ ## Your Mission
346
+ [What this agent should accomplish]
347
+
348
+ ## Approach
349
+ [How this agent should work]
350
+
351
+ ## Output Format
352
+ [How the agent should format responses]
353
+ ```
354
+
355
+ **Available tools:** grep, glob, read_file, edit_file, write_file, bash, semantic_search, list_files, etc.
356
+
357
+ **My request:** {user_input}
358
+
359
+ Please help me design and create an agent. Ask me questions about what I need, then use the Write tool to create the file at `{agents_dir}/<agent-name>.md`."""
360
+ stream = client.agent_chat_stream(
361
+ message=message_with_context,
362
+ model=model,
363
+ max_iterations=max_iterations,
364
+ options={"mode": "code"},
365
+ )
366
+ first_message = False
367
+ elif chat_session_id:
368
+ stream = client.agent_continue_stream(
369
+ chat_session_id, user_input
370
+ )
371
+ else:
372
+ stream = client.agent_chat_stream(
373
+ message=user_input,
374
+ model=model,
375
+ max_iterations=max_iterations,
376
+ options={"mode": "code"},
377
+ )
378
+
379
+ result = render_with_interrupt(renderer, stream)
380
+ if result and result.get("session_id"):
381
+ chat_session_id = result["session_id"]
382
+
383
+ except (KeyboardInterrupt, EOFError):
384
+ console.print()
385
+ console.print("[dim]Cancelled[/dim]")
386
+ break
387
+ except Exception as e:
388
+ console.print(f"[red]Error: {e}[/red]")
389
+
390
+ return None
391
+
392
+
393
+ def handle_agents(args: str, client, renderer, model, max_iterations, render_with_interrupt) -> None:
394
+ """Handle /agents command."""
395
+ from prompt_toolkit import PromptSession
396
+
397
+ # Handle subcommands for backward compatibility
398
+ if args:
399
+ subparts = args.split(maxsplit=1)
400
+ subcommand = subparts[0].lower()
401
+ subargs = subparts[1] if len(subparts) > 1 else ""
402
+
403
+ if subcommand == "create" and subargs:
404
+ create_agent(subargs.strip().lower().replace(" ", "-"))
405
+ elif subcommand == "show" and subargs:
406
+ show_agent_details(subargs.strip())
407
+ elif subcommand == "delete" and subargs:
408
+ delete_agent(subargs.strip())
409
+ elif subcommand == "edit" and subargs:
410
+ edit_agent(subargs.strip())
411
+ else:
412
+ console.print("[yellow]Usage: /agents [create|show|delete|edit] <name>[/yellow]")
413
+ console.print("[dim]Or just /agents for interactive menu[/dim]")
414
+ else:
415
+ # Interactive menu
416
+ while True:
417
+ action, agent_name = show_agents_interactive_menu()
418
+
419
+ if action == "cancel":
420
+ break
421
+ elif action == "view":
422
+ show_agent_details(agent_name)
423
+ # After viewing, show options based on agent type
424
+ is_custom = agent_name not in ("Explore", "Plan")
425
+ try:
426
+ if is_custom:
427
+ console.print("[cyan]'c'[/cyan] chat • [cyan]'e'[/cyan] edit • [red]'d'[/red] delete • [dim]Enter back[/dim]", end="")
428
+ else:
429
+ console.print("[dim]Press Enter to go back...[/dim]", end="")
430
+ ps = PromptSession()
431
+ resp = ps.prompt(" ").strip().lower()
432
+ if is_custom and resp == 'c':
433
+ chat_edit_agent(agent_name, client, renderer, model, max_iterations, render_with_interrupt)
434
+ elif is_custom and resp == 'e':
435
+ edit_agent(agent_name)
436
+ elif is_custom and resp == 'd':
437
+ if delete_agent(agent_name):
438
+ continue # Refresh menu after deletion
439
+ console.print() # Add spacing before menu reappears
440
+ except (KeyboardInterrupt, EOFError):
441
+ break
442
+ elif action == "create":
443
+ # Use AI-assisted creation
444
+ chat_create_agent(client, renderer, model, max_iterations, render_with_interrupt)
445
+ elif action == "delete":
446
+ delete_agent(agent_name)
447
+ elif action == "edit":
448
+ edit_agent(agent_name)
449
+ break # Exit menu after editing
@@ -0,0 +1,69 @@
1
+ """Handler for /auth command."""
2
+
3
+ from rich.console import Console
4
+
5
+ console = Console()
6
+
7
+
8
+ def handle_auth(args: str) -> None:
9
+ """Handle /auth command.
10
+
11
+ Args:
12
+ args: Command arguments
13
+ """
14
+ from emdash_core.auth.github import GitHubAuth, get_auth_status
15
+
16
+ # Parse subcommand
17
+ subparts = args.split(maxsplit=1) if args else []
18
+ subcommand = subparts[0].lower() if subparts else "status"
19
+
20
+ if subcommand == "status" or subcommand == "":
21
+ # Show auth status
22
+ status = get_auth_status()
23
+ console.print()
24
+ console.print("[bold cyan]GitHub Authentication[/bold cyan]\n")
25
+
26
+ if status["authenticated"]:
27
+ console.print(f" Status: [green]Authenticated[/green]")
28
+ console.print(f" Source: {status['source']}")
29
+ if status["username"]:
30
+ console.print(f" Username: @{status['username']}")
31
+ if status["scopes"]:
32
+ console.print(f" Scopes: {', '.join(status['scopes'])}")
33
+ else:
34
+ console.print(f" Status: [yellow]Not authenticated[/yellow]")
35
+ console.print("\n[dim]Run /auth login to authenticate with GitHub[/dim]")
36
+
37
+ console.print()
38
+
39
+ elif subcommand == "login":
40
+ # Start GitHub OAuth device flow
41
+ console.print()
42
+ console.print("[bold cyan]GitHub Login[/bold cyan]")
43
+ console.print("[dim]Starting device authorization flow...[/dim]\n")
44
+
45
+ auth = GitHubAuth()
46
+ try:
47
+ config = auth.login(open_browser=True)
48
+ if config:
49
+ console.print()
50
+ console.print("[green]Authentication successful![/green]")
51
+ console.print("[dim]MCP servers can now use ${GITHUB_TOKEN}[/dim]")
52
+ else:
53
+ console.print("[red]Authentication failed or was cancelled.[/red]")
54
+ except Exception as e:
55
+ console.print(f"[red]Login failed: {e}[/red]")
56
+
57
+ console.print()
58
+
59
+ elif subcommand == "logout":
60
+ # Remove stored authentication
61
+ auth = GitHubAuth()
62
+ if auth.logout():
63
+ console.print("[green]Logged out successfully[/green]")
64
+ else:
65
+ console.print("[dim]No stored authentication to remove[/dim]")
66
+
67
+ else:
68
+ console.print(f"[yellow]Unknown subcommand: {subcommand}[/yellow]")
69
+ console.print("[dim]Usage: /auth [status|login|logout][/dim]")