a4e 0.1.5__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 (70) hide show
  1. a4e/__init__.py +0 -0
  2. a4e/cli.py +47 -0
  3. a4e/cli_commands/__init__.py +5 -0
  4. a4e/cli_commands/add.py +376 -0
  5. a4e/cli_commands/deploy.py +149 -0
  6. a4e/cli_commands/dev.py +162 -0
  7. a4e/cli_commands/info.py +206 -0
  8. a4e/cli_commands/init.py +211 -0
  9. a4e/cli_commands/list.py +227 -0
  10. a4e/cli_commands/mcp.py +504 -0
  11. a4e/cli_commands/remove.py +197 -0
  12. a4e/cli_commands/update.py +285 -0
  13. a4e/cli_commands/validate.py +117 -0
  14. a4e/core.py +109 -0
  15. a4e/dev_runner.py +425 -0
  16. a4e/server.py +86 -0
  17. a4e/templates/agent.md.j2 +168 -0
  18. a4e/templates/agent.py.j2 +15 -0
  19. a4e/templates/agents.md.j2 +99 -0
  20. a4e/templates/metadata.json.j2 +20 -0
  21. a4e/templates/prompt.md.j2 +20 -0
  22. a4e/templates/prompts/agent.md.j2 +206 -0
  23. a4e/templates/skills/agents.md.j2 +110 -0
  24. a4e/templates/skills/skill.md.j2 +120 -0
  25. a4e/templates/support_module.py.j2 +84 -0
  26. a4e/templates/tool.py.j2 +60 -0
  27. a4e/templates/tools/agent.md.j2 +192 -0
  28. a4e/templates/view.tsx.j2 +21 -0
  29. a4e/templates/views/agent.md.j2 +219 -0
  30. a4e/tools/__init__.py +70 -0
  31. a4e/tools/agent_tools/__init__.py +12 -0
  32. a4e/tools/agent_tools/add_support_module.py +95 -0
  33. a4e/tools/agent_tools/add_tool.py +115 -0
  34. a4e/tools/agent_tools/list_tools.py +28 -0
  35. a4e/tools/agent_tools/remove_tool.py +69 -0
  36. a4e/tools/agent_tools/update_tool.py +123 -0
  37. a4e/tools/deploy/__init__.py +8 -0
  38. a4e/tools/deploy/deploy.py +59 -0
  39. a4e/tools/dev/__init__.py +10 -0
  40. a4e/tools/dev/check_environment.py +79 -0
  41. a4e/tools/dev/dev_start.py +30 -0
  42. a4e/tools/dev/dev_stop.py +26 -0
  43. a4e/tools/project/__init__.py +10 -0
  44. a4e/tools/project/get_agent_info.py +66 -0
  45. a4e/tools/project/get_instructions.py +216 -0
  46. a4e/tools/project/initialize_project.py +231 -0
  47. a4e/tools/schemas/__init__.py +8 -0
  48. a4e/tools/schemas/generate_schemas.py +278 -0
  49. a4e/tools/skills/__init__.py +12 -0
  50. a4e/tools/skills/add_skill.py +105 -0
  51. a4e/tools/skills/helpers.py +137 -0
  52. a4e/tools/skills/list_skills.py +54 -0
  53. a4e/tools/skills/remove_skill.py +74 -0
  54. a4e/tools/skills/update_skill.py +150 -0
  55. a4e/tools/validation/__init__.py +8 -0
  56. a4e/tools/validation/validate.py +389 -0
  57. a4e/tools/views/__init__.py +12 -0
  58. a4e/tools/views/add_view.py +40 -0
  59. a4e/tools/views/helpers.py +91 -0
  60. a4e/tools/views/list_views.py +27 -0
  61. a4e/tools/views/remove_view.py +73 -0
  62. a4e/tools/views/update_view.py +124 -0
  63. a4e/utils/dev_manager.py +253 -0
  64. a4e/utils/schema_generator.py +255 -0
  65. a4e-0.1.5.dist-info/METADATA +427 -0
  66. a4e-0.1.5.dist-info/RECORD +70 -0
  67. a4e-0.1.5.dist-info/WHEEL +5 -0
  68. a4e-0.1.5.dist-info/entry_points.txt +2 -0
  69. a4e-0.1.5.dist-info/licenses/LICENSE +21 -0
  70. a4e-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,504 @@
1
+ """
2
+ MCP configuration commands for A4E.
3
+
4
+ This module provides commands to configure the A4E MCP server
5
+ for different IDEs like Cursor, Claude Code, and Antigravity.
6
+
7
+ Usage:
8
+ a4e mcp setup cursor
9
+ a4e mcp setup claude-code
10
+ a4e mcp setup antigravity
11
+ a4e mcp show cursor
12
+ a4e mcp remove cursor
13
+ a4e mcp test
14
+ """
15
+
16
+ import json
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ import typer
21
+
22
+ app = typer.Typer(
23
+ help="Configure MCP server for IDEs",
24
+ no_args_is_help=True,
25
+ )
26
+
27
+
28
+ # ============================================================================
29
+ # Configuration Paths
30
+ # ============================================================================
31
+
32
+
33
+ def _get_platform() -> str:
34
+ """Get the current platform: darwin, win32, or linux."""
35
+ return sys.platform
36
+
37
+
38
+ def _get_ide_configs() -> dict:
39
+ """
40
+ Get IDE configuration paths and formats.
41
+
42
+ Returns dict with IDE name as key and:
43
+ - path: Path to config file
44
+ - servers_key: Key name for MCP servers in config
45
+ - name: Display name for the IDE
46
+ """
47
+ platform = _get_platform()
48
+ home = Path.home()
49
+
50
+ # Antigravity config path varies by OS
51
+ if platform == "win32":
52
+ antigravity_path = home / ".gemini" / "antigravity" / "mcp_config.json"
53
+ else: # macOS and Linux
54
+ antigravity_path = home / ".gemini" / "antigravity" / "mcp_config.json"
55
+
56
+ return {
57
+ "cursor": {
58
+ "path": home / ".cursor" / "mcp.json",
59
+ "servers_key": "mcpServers",
60
+ "name": "Cursor",
61
+ },
62
+ "claude-code": {
63
+ "path": home / ".claude.json",
64
+ "servers_key": "mcpServers",
65
+ "name": "Claude Code",
66
+ },
67
+ "antigravity": {
68
+ "path": antigravity_path,
69
+ "servers_key": "mcpServers",
70
+ "name": "Antigravity",
71
+ },
72
+ }
73
+
74
+
75
+ def _get_a4e_server_config(ide: str) -> dict:
76
+ """
77
+ Generate the MCP server configuration for A4E.
78
+
79
+ Uses sys.executable to get the exact Python that has a4e installed.
80
+ This ensures the config works regardless of how Python was installed
81
+ (pyenv, system, venv, homebrew, etc.)
82
+
83
+ The env configuration varies by IDE:
84
+ - Cursor: Uses ${workspaceFolder} which it expands
85
+ - Claude Code / Antigravity: No workspace variable, uses cwd
86
+
87
+ Args:
88
+ ide: The IDE identifier (cursor, claude-code, antigravity)
89
+ """
90
+ base_config = {
91
+ "command": sys.executable,
92
+ "args": ["-m", "a4e.server"],
93
+ }
94
+
95
+ # IDEs that support ${workspaceFolder}
96
+ if ide == "cursor":
97
+ base_config["env"] = {
98
+ "A4E_WORKSPACE": "${workspaceFolder}"
99
+ }
100
+ # Claude Code and Antigravity: no workspace variable
101
+ # The server will use cwd or require explicit project_path in tool calls
102
+
103
+ return base_config
104
+
105
+
106
+ def _get_supported_ides() -> list[str]:
107
+ """Get list of supported IDE names."""
108
+ return list(_get_ide_configs().keys())
109
+
110
+
111
+ # ============================================================================
112
+ # Helper Functions
113
+ # ============================================================================
114
+
115
+
116
+ def _load_config(config_path: Path) -> tuple[dict, bool]:
117
+ """
118
+ Load existing config from path.
119
+
120
+ Returns:
121
+ Tuple of (config_dict, had_error)
122
+ If file doesn't exist, returns empty dict with no error.
123
+ If file has invalid JSON, returns empty dict with error=True.
124
+ """
125
+ if not config_path.exists():
126
+ return {}, False
127
+
128
+ try:
129
+ content = config_path.read_text().strip()
130
+ if not content:
131
+ return {}, False
132
+ return json.loads(content), False
133
+ except json.JSONDecodeError:
134
+ return {}, True
135
+
136
+
137
+ def _save_config(config_path: Path, config: dict) -> None:
138
+ """Save config to path, creating parent directories if needed."""
139
+ config_path.parent.mkdir(parents=True, exist_ok=True)
140
+ config_path.write_text(json.dumps(config, indent=2) + "\n")
141
+
142
+
143
+ # ============================================================================
144
+ # Commands
145
+ # ============================================================================
146
+
147
+
148
+ @app.command()
149
+ def setup(
150
+ ide: str = typer.Argument(
151
+ ...,
152
+ help="IDE to configure: cursor, claude-code, antigravity"
153
+ ),
154
+ dry_run: bool = typer.Option(
155
+ False, "--dry-run", "-n",
156
+ help="Show what would be done without making changes"
157
+ ),
158
+ force: bool = typer.Option(
159
+ False, "--force", "-f",
160
+ help="Overwrite existing A4E configuration"
161
+ ),
162
+ ):
163
+ """
164
+ Configure A4E MCP server for your IDE.
165
+
166
+ This writes the necessary configuration so your IDE can use
167
+ A4E tools through the Model Context Protocol (MCP).
168
+
169
+ Examples:
170
+
171
+ a4e mcp setup cursor
172
+
173
+ a4e mcp setup claude-code
174
+
175
+ a4e mcp setup antigravity
176
+
177
+ a4e mcp setup cursor --dry-run
178
+
179
+ a4e mcp setup cursor --force
180
+ """
181
+ ide_configs = _get_ide_configs()
182
+ ide_lower = ide.lower()
183
+
184
+ if ide_lower not in ide_configs:
185
+ supported = ", ".join(_get_supported_ides())
186
+ typer.echo(f"Error: Unknown IDE '{ide}'", err=True)
187
+ typer.echo(f"Supported IDEs: {supported}", err=True)
188
+ raise typer.Exit(1)
189
+
190
+ config_info = ide_configs[ide_lower]
191
+ config_path: Path = config_info["path"]
192
+ servers_key: str = config_info["servers_key"]
193
+ ide_name: str = config_info["name"]
194
+ a4e_config = _get_a4e_server_config(ide_lower)
195
+
196
+ # Dry run mode - show what would happen
197
+ if dry_run:
198
+ typer.echo(f"Would configure A4E for {ide_name}:")
199
+ typer.echo()
200
+ typer.echo(f" Config file: {config_path}")
201
+ typer.echo(f" Python path: {sys.executable}")
202
+ typer.echo()
203
+ typer.echo(" Configuration to add:")
204
+ preview = {servers_key: {"a4e": a4e_config}}
205
+ typer.echo(json.dumps(preview, indent=2))
206
+ return
207
+
208
+ # Load existing config
209
+ existing, had_json_error = _load_config(config_path)
210
+
211
+ # Always create backup if file exists (before any modification)
212
+ if config_path.exists() and not had_json_error:
213
+ backup_path = config_path.with_suffix(".json.backup")
214
+ backup_path.write_text(config_path.read_text())
215
+ typer.echo(f"Backup created: {backup_path}")
216
+
217
+ if had_json_error:
218
+ # Backup corrupted config
219
+ backup_path = config_path.with_suffix(".json.backup")
220
+ typer.echo(f"Warning: Invalid JSON in {config_path}", err=True)
221
+ typer.echo(f"Creating backup at {backup_path}", err=True)
222
+
223
+ if config_path.exists():
224
+ backup_path.write_text(config_path.read_text())
225
+
226
+ existing = {}
227
+
228
+ # Ensure servers key exists
229
+ existing.setdefault(servers_key, {})
230
+
231
+ # Check if a4e already configured
232
+ if "a4e" in existing[servers_key]:
233
+ if not force:
234
+ typer.echo(f"A4E is already configured for {ide_name}")
235
+ typer.echo()
236
+ typer.echo("Current configuration:")
237
+ typer.echo(json.dumps(existing[servers_key]["a4e"], indent=2))
238
+ typer.echo()
239
+ typer.echo("Use --force to overwrite, or --dry-run to preview")
240
+ raise typer.Exit(1)
241
+ else:
242
+ typer.echo("Overwriting existing A4E configuration...")
243
+
244
+ # Add/update a4e config
245
+ existing[servers_key]["a4e"] = a4e_config
246
+
247
+ # Save config
248
+ _save_config(config_path, existing)
249
+
250
+ typer.echo(f"A4E MCP configured for {ide_name}")
251
+ typer.echo()
252
+ typer.echo(f" Config file: {config_path}")
253
+ typer.echo(f" Python: {sys.executable}")
254
+ typer.echo()
255
+
256
+ # IDE-specific notes
257
+ if ide_lower == "cursor":
258
+ typer.echo(" Workspace: Uses ${workspaceFolder} (auto-detected)")
259
+ else:
260
+ typer.echo(" Workspace: Uses current directory or specify project_path in tool calls")
261
+
262
+ typer.echo()
263
+ typer.echo(f"Restart {ide_name} to activate the MCP server.")
264
+
265
+
266
+ @app.command()
267
+ def show(
268
+ ide: str = typer.Argument(
269
+ ...,
270
+ help="IDE to show config for: cursor, claude-code, antigravity"
271
+ )
272
+ ):
273
+ """
274
+ Show current MCP configuration for an IDE.
275
+
276
+ Example:
277
+
278
+ a4e mcp show cursor
279
+ """
280
+ ide_configs = _get_ide_configs()
281
+ ide_lower = ide.lower()
282
+
283
+ if ide_lower not in ide_configs:
284
+ supported = ", ".join(_get_supported_ides())
285
+ typer.echo(f"Error: Unknown IDE '{ide}'", err=True)
286
+ typer.echo(f"Supported IDEs: {supported}", err=True)
287
+ raise typer.Exit(1)
288
+
289
+ config_info = ide_configs[ide_lower]
290
+ config_path: Path = config_info["path"]
291
+ servers_key: str = config_info["servers_key"]
292
+ ide_name: str = config_info["name"]
293
+
294
+ if not config_path.exists():
295
+ typer.echo(f"No MCP config found for {ide_name}")
296
+ typer.echo(f"Expected path: {config_path}")
297
+ typer.echo()
298
+ typer.echo(f"Run: a4e mcp setup {ide}")
299
+ return
300
+
301
+ config, had_error = _load_config(config_path)
302
+
303
+ if had_error:
304
+ typer.echo(f"Error: Invalid JSON in {config_path}", err=True)
305
+ raise typer.Exit(1)
306
+
307
+ typer.echo(f"MCP configuration for {ide_name}")
308
+ typer.echo(f"File: {config_path}")
309
+ typer.echo()
310
+ typer.echo(json.dumps(config, indent=2))
311
+
312
+ # Check A4E status
313
+ if servers_key in config and "a4e" in config[servers_key]:
314
+ typer.echo()
315
+ typer.echo("A4E status: Configured")
316
+ else:
317
+ typer.echo()
318
+ typer.echo("A4E status: Not configured")
319
+ typer.echo(f"Run: a4e mcp setup {ide}")
320
+
321
+
322
+ @app.command()
323
+ def remove(
324
+ ide: str = typer.Argument(
325
+ ...,
326
+ help="IDE to remove A4E config from"
327
+ ),
328
+ ):
329
+ """
330
+ Remove A4E MCP configuration from an IDE.
331
+
332
+ This removes only the A4E configuration, preserving other MCP servers.
333
+
334
+ Example:
335
+
336
+ a4e mcp remove cursor
337
+ """
338
+ ide_configs = _get_ide_configs()
339
+ ide_lower = ide.lower()
340
+
341
+ if ide_lower not in ide_configs:
342
+ supported = ", ".join(_get_supported_ides())
343
+ typer.echo(f"Error: Unknown IDE '{ide}'", err=True)
344
+ typer.echo(f"Supported IDEs: {supported}", err=True)
345
+ raise typer.Exit(1)
346
+
347
+ config_info = ide_configs[ide_lower]
348
+ config_path: Path = config_info["path"]
349
+ servers_key: str = config_info["servers_key"]
350
+ ide_name: str = config_info["name"]
351
+
352
+ if not config_path.exists():
353
+ typer.echo(f"No MCP config found for {ide_name}")
354
+ return
355
+
356
+ config, had_error = _load_config(config_path)
357
+
358
+ if had_error:
359
+ typer.echo(f"Error: Invalid JSON in {config_path}", err=True)
360
+ raise typer.Exit(1)
361
+
362
+ # Check if a4e is configured
363
+ if servers_key not in config or "a4e" not in config[servers_key]:
364
+ typer.echo(f"A4E is not configured for {ide_name}")
365
+ return
366
+
367
+ # Remove a4e
368
+ del config[servers_key]["a4e"]
369
+
370
+ # Clean up empty servers dict
371
+ if not config[servers_key]:
372
+ del config[servers_key]
373
+
374
+ # Save config
375
+ _save_config(config_path, config)
376
+
377
+ typer.echo(f"A4E removed from {ide_name} configuration")
378
+ typer.echo()
379
+ typer.echo(f"Restart {ide_name} to apply changes.")
380
+
381
+
382
+ @app.command()
383
+ def test():
384
+ """
385
+ Test that the A4E MCP server can start correctly.
386
+
387
+ This verifies that:
388
+ - The server module can be imported
389
+ - The Python path is correct
390
+ - Dependencies are installed
391
+
392
+ Useful for debugging configuration issues.
393
+ """
394
+ import subprocess
395
+
396
+ typer.echo("Testing A4E MCP server...")
397
+ typer.echo()
398
+ typer.echo(f" Python: {sys.executable}")
399
+ typer.echo(f" Command: {sys.executable} -m a4e.server")
400
+ typer.echo()
401
+
402
+ # Test 1: Module import
403
+ typer.echo("1. Testing module import...")
404
+ try:
405
+ # Test that the module can be imported
406
+ import importlib
407
+ importlib.import_module("a4e.server")
408
+ typer.echo(" Module imports correctly")
409
+ except ImportError as e:
410
+ typer.echo(f" Error: Failed to import a4e.server: {e}", err=True)
411
+ raise typer.Exit(1)
412
+
413
+ # Test 2: FastMCP instance
414
+ typer.echo("2. Testing MCP server instance...")
415
+ try:
416
+ from a4e.core import mcp
417
+ tools = list(mcp._tool_manager._tools.keys()) if hasattr(mcp, '_tool_manager') else []
418
+ typer.echo(" MCP server initialized")
419
+ if tools:
420
+ typer.echo(f" Tools registered: {len(tools)}")
421
+ except Exception as e:
422
+ typer.echo(f" Warning: Could not inspect MCP instance: {e}", err=True)
423
+
424
+ # Test 3: Server process start
425
+ typer.echo("3. Testing server process...")
426
+ try:
427
+ # Run with --help to quickly test if it starts
428
+ result = subprocess.run(
429
+ [sys.executable, "-c", "from a4e.server import main; print('OK')"],
430
+ capture_output=True,
431
+ text=True,
432
+ timeout=10,
433
+ )
434
+
435
+ if result.returncode == 0 and "OK" in result.stdout:
436
+ typer.echo(" Server process starts correctly")
437
+ else:
438
+ typer.echo(" Warning: Unexpected output", err=True)
439
+ if result.stderr:
440
+ typer.echo(f" stderr: {result.stderr}", err=True)
441
+ except subprocess.TimeoutExpired:
442
+ typer.echo(" Server process timed out (may be normal for stdio server)")
443
+ except Exception as e:
444
+ typer.echo(f" Error testing server process: {e}", err=True)
445
+ raise typer.Exit(1)
446
+
447
+ typer.echo()
448
+ typer.echo("All tests passed. MCP server is ready.")
449
+ typer.echo()
450
+ typer.echo("Next steps:")
451
+ typer.echo(" 1. Run: a4e mcp setup <ide>")
452
+ typer.echo(" 2. Restart your IDE")
453
+ typer.echo(" 3. Start using A4E tools!")
454
+
455
+
456
+ @app.command()
457
+ def path(
458
+ ide: str = typer.Argument(
459
+ ...,
460
+ help="IDE to show config path for"
461
+ )
462
+ ):
463
+ """
464
+ Show the config file path for an IDE.
465
+
466
+ Useful for manual configuration or debugging.
467
+
468
+ Example:
469
+
470
+ a4e mcp path cursor
471
+ """
472
+ ide_configs = _get_ide_configs()
473
+ ide_lower = ide.lower()
474
+
475
+ if ide_lower not in ide_configs:
476
+ supported = ", ".join(_get_supported_ides())
477
+ typer.echo(f"Error: Unknown IDE '{ide}'", err=True)
478
+ typer.echo(f"Supported IDEs: {supported}", err=True)
479
+ raise typer.Exit(1)
480
+
481
+ config_path = ide_configs[ide_lower]["path"]
482
+ typer.echo(str(config_path))
483
+
484
+
485
+ @app.command(name="list")
486
+ def list_ides():
487
+ """
488
+ List all supported IDEs.
489
+
490
+ Example:
491
+
492
+ a4e mcp list
493
+ """
494
+ ide_configs = _get_ide_configs()
495
+
496
+ typer.echo("Supported IDEs:")
497
+ typer.echo()
498
+
499
+ for ide_key, config in ide_configs.items():
500
+ path_exists = "exists" if config["path"].exists() else "not found"
501
+ typer.echo(f" {ide_key}")
502
+ typer.echo(f" Name: {config['name']}")
503
+ typer.echo(f" Config: {config['path']} ({path_exists})")
504
+ typer.echo()
@@ -0,0 +1,197 @@
1
+ # a4e/cli_commands/remove.py
2
+ """
3
+ Commands for removing tools, views, and skills from an agent.
4
+ """
5
+
6
+ import typer
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from rich.console import Console
11
+ from rich.prompt import Confirm
12
+
13
+ console = Console()
14
+
15
+ # Create a 'Typer' app for the 'remove' command group
16
+ app = typer.Typer(
17
+ no_args_is_help=True,
18
+ help="Remove tools, views, or skills from your agent.",
19
+ )
20
+
21
+
22
+ def find_agent_dir(agent_name: Optional[str] = None) -> Optional[Path]:
23
+ """Find the agent directory from name or current working directory."""
24
+ cwd = Path.cwd()
25
+
26
+ if agent_name:
27
+ if Path(agent_name).is_absolute() and Path(agent_name).exists():
28
+ return Path(agent_name)
29
+ if (cwd / agent_name).exists():
30
+ return cwd / agent_name
31
+ agent_store = cwd / "file-store" / "agent-store" / agent_name
32
+ if agent_store.exists():
33
+ return agent_store
34
+ return None
35
+
36
+ if (cwd / "agent.py").exists() and (cwd / "metadata.json").exists():
37
+ return cwd
38
+
39
+ return None
40
+
41
+
42
+ @app.command("tool")
43
+ def remove_tool(
44
+ tool_name: str = typer.Argument(..., help="Name of the tool to remove"),
45
+ agent_name: Optional[str] = typer.Option(
46
+ None, "--agent", "-a", help="Agent name/path (defaults to current directory)"
47
+ ),
48
+ yes: bool = typer.Option(
49
+ False, "--yes", "-y", help="Skip confirmation prompt"
50
+ ),
51
+ ) -> None:
52
+ """
53
+ Remove a tool from the agent.
54
+
55
+ Example:
56
+ a4e remove tool calculate_bmi
57
+ a4e remove tool calculate_bmi --yes
58
+ """
59
+ agent_dir = find_agent_dir(agent_name)
60
+ if not agent_dir:
61
+ console.print("[red]Error: Not in an agent directory. Use --agent to specify the agent.[/red]")
62
+ raise typer.Exit(code=1)
63
+
64
+ # Confirm removal
65
+ if not yes:
66
+ if not Confirm.ask(f"Remove tool '{tool_name}'?", default=False):
67
+ console.print("[yellow]Cancelled.[/yellow]")
68
+ raise typer.Exit(code=0)
69
+
70
+ try:
71
+ from ..tools.agent_tools.remove_tool import remove_tool as mcp_remove_tool
72
+
73
+ result = mcp_remove_tool(
74
+ tool_name=tool_name,
75
+ agent_name=str(agent_dir),
76
+ )
77
+
78
+ if result.get("success"):
79
+ console.print(f"[green]✓ Tool '{tool_name}' removed![/green]")
80
+ console.print(f" Removed: {result.get('removed_file')}")
81
+ else:
82
+ console.print(f"[red]Error: {result.get('error')}[/red]")
83
+ raise typer.Exit(code=1)
84
+
85
+ except ImportError as e:
86
+ console.print(f"[red]Error importing tools: {e}[/red]")
87
+ raise typer.Exit(code=1)
88
+
89
+
90
+ @app.command("view")
91
+ def remove_view(
92
+ view_id: str = typer.Argument(..., help="ID of the view to remove"),
93
+ agent_name: Optional[str] = typer.Option(
94
+ None, "--agent", "-a", help="Agent name/path (defaults to current directory)"
95
+ ),
96
+ yes: bool = typer.Option(
97
+ False, "--yes", "-y", help="Skip confirmation prompt"
98
+ ),
99
+ ) -> None:
100
+ """
101
+ Remove a view from the agent.
102
+
103
+ Note: The 'welcome' view cannot be removed as it's required.
104
+
105
+ Example:
106
+ a4e remove view results-display
107
+ a4e remove view results-display --yes
108
+ """
109
+ agent_dir = find_agent_dir(agent_name)
110
+ if not agent_dir:
111
+ console.print("[red]Error: Not in an agent directory. Use --agent to specify the agent.[/red]")
112
+ raise typer.Exit(code=1)
113
+
114
+ # Warn about welcome view
115
+ if view_id == "welcome":
116
+ console.print("[red]Error: Cannot remove the 'welcome' view - it is required for all agents.[/red]")
117
+ raise typer.Exit(code=1)
118
+
119
+ # Confirm removal
120
+ if not yes:
121
+ if not Confirm.ask(f"Remove view '{view_id}'?", default=False):
122
+ console.print("[yellow]Cancelled.[/yellow]")
123
+ raise typer.Exit(code=0)
124
+
125
+ try:
126
+ from ..tools.views.remove_view import remove_view as mcp_remove_view
127
+
128
+ result = mcp_remove_view(
129
+ view_id=view_id,
130
+ agent_name=str(agent_dir),
131
+ )
132
+
133
+ if result.get("success"):
134
+ console.print(f"[green]✓ View '{view_id}' removed![/green]")
135
+ console.print(f" Removed: {result.get('removed_folder')}")
136
+ else:
137
+ console.print(f"[red]Error: {result.get('error')}[/red]")
138
+ raise typer.Exit(code=1)
139
+
140
+ except ImportError as e:
141
+ console.print(f"[red]Error importing tools: {e}[/red]")
142
+ raise typer.Exit(code=1)
143
+
144
+
145
+ @app.command("skill")
146
+ def remove_skill(
147
+ skill_id: str = typer.Argument(..., help="ID of the skill to remove"),
148
+ agent_name: Optional[str] = typer.Option(
149
+ None, "--agent", "-a", help="Agent name/path (defaults to current directory)"
150
+ ),
151
+ yes: bool = typer.Option(
152
+ False, "--yes", "-y", help="Skip confirmation prompt"
153
+ ),
154
+ ) -> None:
155
+ """
156
+ Remove a skill from the agent.
157
+
158
+ Note: The 'show_welcome' skill cannot be removed as it's required.
159
+
160
+ Example:
161
+ a4e remove skill show_results
162
+ a4e remove skill show_results --yes
163
+ """
164
+ agent_dir = find_agent_dir(agent_name)
165
+ if not agent_dir:
166
+ console.print("[red]Error: Not in an agent directory. Use --agent to specify the agent.[/red]")
167
+ raise typer.Exit(code=1)
168
+
169
+ # Warn about show_welcome skill
170
+ if skill_id == "show_welcome":
171
+ console.print("[red]Error: Cannot remove the 'show_welcome' skill - it is required for all agents.[/red]")
172
+ raise typer.Exit(code=1)
173
+
174
+ # Confirm removal
175
+ if not yes:
176
+ if not Confirm.ask(f"Remove skill '{skill_id}'?", default=False):
177
+ console.print("[yellow]Cancelled.[/yellow]")
178
+ raise typer.Exit(code=0)
179
+
180
+ try:
181
+ from ..tools.skills.remove_skill import remove_skill as mcp_remove_skill
182
+
183
+ result = mcp_remove_skill(
184
+ skill_id=skill_id,
185
+ agent_name=str(agent_dir),
186
+ )
187
+
188
+ if result.get("success"):
189
+ console.print(f"[green]✓ Skill '{skill_id}' removed![/green]")
190
+ console.print(f" Removed: {result.get('removed_folder')}")
191
+ else:
192
+ console.print(f"[red]Error: {result.get('error')}[/red]")
193
+ raise typer.Exit(code=1)
194
+
195
+ except ImportError as e:
196
+ console.print(f"[red]Error importing tools: {e}[/red]")
197
+ raise typer.Exit(code=1)