fips-agents-cli 0.1.0__py3-none-any.whl → 0.1.1__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.
fips_agents_cli/cli.py CHANGED
@@ -4,6 +4,7 @@ import click
4
4
  from rich.console import Console
5
5
 
6
6
  from fips_agents_cli.commands.create import create
7
+ from fips_agents_cli.commands.generate import generate
7
8
  from fips_agents_cli.version import __version__
8
9
 
9
10
  console = Console()
@@ -23,6 +24,7 @@ def cli(ctx):
23
24
 
24
25
  # Register commands
25
26
  cli.add_command(create)
27
+ cli.add_command(generate)
26
28
 
27
29
 
28
30
  def main():
@@ -1,7 +1,6 @@
1
1
  """Create command for generating new projects from templates."""
2
2
 
3
3
  import sys
4
- from typing import Optional
5
4
 
6
5
  import click
7
6
  from rich.console import Console
@@ -43,7 +42,7 @@ def create():
43
42
  default=False,
44
43
  help="Skip git repository initialization",
45
44
  )
46
- def mcp_server(project_name: str, target_dir: Optional[str], no_git: bool):
45
+ def mcp_server(project_name: str, target_dir: str | None, no_git: bool):
47
46
  """
48
47
  Create a new MCP server project from template.
49
48
 
@@ -0,0 +1,405 @@
1
+ """Generate command for scaffolding MCP components in existing projects."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import click
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.progress import Progress, SpinnerColumn, TextColumn
11
+
12
+ from fips_agents_cli.tools.generators import (
13
+ get_project_info,
14
+ load_params_file,
15
+ load_template,
16
+ render_component,
17
+ run_component_tests,
18
+ validate_python_syntax,
19
+ write_component_file,
20
+ )
21
+ from fips_agents_cli.tools.validation import (
22
+ component_exists,
23
+ find_project_root,
24
+ is_valid_component_name,
25
+ validate_generator_templates,
26
+ )
27
+
28
+ console = Console()
29
+
30
+
31
+ def generate_component_workflow(
32
+ component_type: str,
33
+ name: str,
34
+ template_vars: dict[str, Any],
35
+ params_path: str | None,
36
+ dry_run: bool,
37
+ description: str | None,
38
+ ) -> None:
39
+ """
40
+ Common workflow for generating any component type.
41
+
42
+ Args:
43
+ component_type: Type of component ('tool', 'resource', 'prompt', 'middleware')
44
+ name: Component name
45
+ template_vars: Template variables dictionary
46
+ params_path: Optional path to params JSON file
47
+ dry_run: If True, only show what would be generated
48
+ description: Component description
49
+ """
50
+ # Step 1: Find project root
51
+ project_root = find_project_root()
52
+ if not project_root:
53
+ console.print(
54
+ "[red]✗[/red] Not in an MCP server project directory\n"
55
+ "[yellow]Hint:[/yellow] Run this command from within an MCP server project, "
56
+ "or use 'fips-agents create mcp-server' to create one"
57
+ )
58
+ sys.exit(1)
59
+
60
+ console.print(f"[green]✓[/green] Found project root: {project_root}")
61
+
62
+ # Step 2: Validate component name
63
+ is_valid, error_msg = is_valid_component_name(name)
64
+ if not is_valid:
65
+ console.print(f"[red]✗[/red] Invalid component name: {error_msg}")
66
+ sys.exit(1)
67
+
68
+ console.print(f"[green]✓[/green] Component name '{name}' is valid")
69
+
70
+ # Step 3: Check if component already exists
71
+ if component_exists(project_root, component_type, name):
72
+ console.print(
73
+ f"[red]✗[/red] Component '{name}' already exists in src/{component_type}s/\n"
74
+ "[yellow]Hint:[/yellow] Choose a different name or manually remove the existing file"
75
+ )
76
+ sys.exit(1)
77
+
78
+ # Step 4: Validate generator templates
79
+ is_valid, error_msg = validate_generator_templates(project_root, component_type)
80
+ if not is_valid:
81
+ console.print(f"[red]✗[/red] {error_msg}")
82
+ sys.exit(1)
83
+
84
+ console.print(f"[green]✓[/green] Generator templates found for '{component_type}'")
85
+
86
+ # Step 5: Prompt for description if not provided
87
+ if not description:
88
+ description = click.prompt(
89
+ f"Enter a description for the {component_type}",
90
+ type=str,
91
+ default="TODO: Add description",
92
+ )
93
+
94
+ template_vars["description"] = description
95
+ template_vars["component_name"] = name
96
+
97
+ # Step 6: Load params file if provided
98
+ if params_path:
99
+ try:
100
+ params = load_params_file(Path(params_path))
101
+ template_vars["params"] = params
102
+ console.print(f"[green]✓[/green] Loaded {len(params)} parameter(s) from {params_path}")
103
+ except Exception as e:
104
+ console.print(f"[red]✗[/red] Failed to load parameters file: {e}")
105
+ sys.exit(1)
106
+ elif "params" not in template_vars:
107
+ template_vars["params"] = []
108
+
109
+ # Step 7: Get project info
110
+ try:
111
+ project_info = get_project_info(project_root)
112
+ template_vars["project_name"] = project_info["name"]
113
+ except Exception as e:
114
+ console.print(f"[yellow]⚠[/yellow] Could not load project info: {e}")
115
+ template_vars["project_name"] = "unknown"
116
+
117
+ # Step 8: Define file paths
118
+ component_dir_map = {
119
+ "tool": "tools",
120
+ "resource": "resources",
121
+ "prompt": "prompts",
122
+ "middleware": "middleware",
123
+ }
124
+ component_dir = component_dir_map[component_type]
125
+
126
+ component_file = project_root / "src" / component_dir / f"{name}.py"
127
+ test_file = project_root / "tests" / component_dir / f"test_{name}.py"
128
+
129
+ # Step 9: Dry run - show paths and exit
130
+ if dry_run:
131
+ console.print("\n[bold cyan]Dry Run - Files that would be generated:[/bold cyan]\n")
132
+ console.print(f" [cyan]Component:[/cyan] {component_file}")
133
+ console.print(f" [cyan]Test:[/cyan] {test_file}")
134
+ console.print("\n[dim]Template variables:[/dim]")
135
+ for key, value in template_vars.items():
136
+ if key == "params" and isinstance(value, list):
137
+ console.print(f" {key}: {len(value)} parameter(s)")
138
+ else:
139
+ console.print(f" {key}: {value}")
140
+ sys.exit(0)
141
+
142
+ # Step 10: Load and render templates
143
+ with Progress(
144
+ SpinnerColumn(),
145
+ TextColumn("[progress.description]{task.description}"),
146
+ console=console,
147
+ ) as progress:
148
+ progress.add_task(description="Generating component files...", total=None)
149
+
150
+ try:
151
+ # Load templates
152
+ component_template = load_template(project_root, component_type, "component.py.j2")
153
+ test_template = load_template(project_root, component_type, "test.py.j2")
154
+
155
+ # Render templates
156
+ component_code = render_component(component_template, template_vars)
157
+ test_code = render_component(test_template, template_vars)
158
+
159
+ except Exception as e:
160
+ console.print(f"\n[red]✗[/red] Failed to render templates: {e}")
161
+ sys.exit(1)
162
+
163
+ # Step 11: Validate Python syntax
164
+ is_valid, error_msg = validate_python_syntax(component_code)
165
+ if not is_valid:
166
+ console.print(f"[red]✗[/red] Generated component has syntax errors: {error_msg}")
167
+ console.print("[red]This is a bug in the template. Please report this issue.[/red]")
168
+ sys.exit(1)
169
+
170
+ is_valid, error_msg = validate_python_syntax(test_code)
171
+ if not is_valid:
172
+ console.print(f"[red]✗[/red] Generated test has syntax errors: {error_msg}")
173
+ console.print("[red]This is a bug in the template. Please report this issue.[/red]")
174
+ sys.exit(1)
175
+
176
+ console.print("[green]✓[/green] Generated code passed syntax validation")
177
+
178
+ # Step 12: Write files
179
+ try:
180
+ write_component_file(component_code, component_file)
181
+ console.print(f"[green]✓[/green] Created: {component_file.relative_to(project_root)}")
182
+
183
+ write_component_file(test_code, test_file)
184
+ console.print(f"[green]✓[/green] Created: {test_file.relative_to(project_root)}")
185
+
186
+ except Exception as e:
187
+ console.print(f"[red]✗[/red] Failed to write files: {e}")
188
+ sys.exit(1)
189
+
190
+ # Step 13: Run tests
191
+ console.print("\n[cyan]Running generated tests...[/cyan]")
192
+ success, output = run_component_tests(project_root, test_file)
193
+
194
+ if success:
195
+ console.print("[green]✓[/green] Tests passed!\n")
196
+ else:
197
+ console.print("[yellow]⚠[/yellow] Tests failed or had issues:\n")
198
+ console.print(output)
199
+ console.print(
200
+ "\n[yellow]Note:[/yellow] Generated tests are placeholders. "
201
+ "Update them with your actual test cases."
202
+ )
203
+
204
+ # Step 14: Success message
205
+ success_message = f"""
206
+ [bold green]✓ Successfully generated {component_type} component![/bold green]
207
+
208
+ [bold cyan]Files Created:[/bold cyan]
209
+ • {component_file.relative_to(project_root)}
210
+ • {test_file.relative_to(project_root)}
211
+
212
+ [bold cyan]Next Steps:[/bold cyan]
213
+ 1. Review the generated code and implement the business logic
214
+ 2. Update the test file with real test cases
215
+ 3. Run tests: [dim]pytest {test_file.relative_to(project_root)}[/dim]
216
+ 4. Import and use your {component_type} in your MCP server
217
+
218
+ [bold cyan]Implementation Notes:[/bold cyan]
219
+ • Replace TODO comments with actual implementation
220
+ • Remove placeholder return values
221
+ • Add proper error handling
222
+ • Update docstrings as needed
223
+ """
224
+
225
+ console.print(Panel(success_message, border_style="green", padding=(1, 2)))
226
+
227
+
228
+ @click.group()
229
+ def generate():
230
+ """Generate new MCP components in existing projects."""
231
+ pass
232
+
233
+
234
+ @generate.command("tool")
235
+ @click.argument("name")
236
+ @click.option(
237
+ "--async/--sync",
238
+ "is_async",
239
+ default=True,
240
+ help="Generate async or sync function (default: async)",
241
+ )
242
+ @click.option("--with-context", is_flag=True, help="Include FastMCP Context parameter")
243
+ @click.option("--with-auth", is_flag=True, help="Include authentication decorator")
244
+ @click.option("--description", "-d", help="Tool description")
245
+ @click.option("--params", type=click.Path(exists=True), help="JSON file with parameter definitions")
246
+ @click.option(
247
+ "--read-only", is_flag=True, default=True, help="Mark as read-only operation (default: true)"
248
+ )
249
+ @click.option("--idempotent", is_flag=True, default=True, help="Mark as idempotent (default: true)")
250
+ @click.option("--open-world", is_flag=True, help="Mark as open-world operation")
251
+ @click.option("--return-type", default="str", help="Return type annotation (default: str)")
252
+ @click.option("--dry-run", is_flag=True, help="Show what would be generated without creating files")
253
+ def tool(
254
+ name: str,
255
+ is_async: bool,
256
+ with_context: bool,
257
+ with_auth: bool,
258
+ description: str | None,
259
+ params: str | None,
260
+ read_only: bool,
261
+ idempotent: bool,
262
+ open_world: bool,
263
+ return_type: str,
264
+ dry_run: bool,
265
+ ):
266
+ """
267
+ Generate a new tool component.
268
+
269
+ NAME is the tool name in snake_case (e.g., search_documents, fetch_data)
270
+
271
+ Example:
272
+ fips-agents generate tool search_documents --description "Search through documents"
273
+ fips-agents generate tool fetch_data --params params.json --with-context
274
+ """
275
+ console.print("\n[bold cyan]Generating Tool Component[/bold cyan]\n")
276
+
277
+ template_vars = {
278
+ "async": is_async,
279
+ "with_context": with_context,
280
+ "with_auth": with_auth,
281
+ "read_only": read_only,
282
+ "idempotent": idempotent,
283
+ "open_world": open_world,
284
+ "return_type": return_type,
285
+ }
286
+
287
+ generate_component_workflow("tool", name, template_vars, params, dry_run, description)
288
+
289
+
290
+ @generate.command("resource")
291
+ @click.argument("name")
292
+ @click.option(
293
+ "--async/--sync",
294
+ "is_async",
295
+ default=True,
296
+ help="Generate async or sync function (default: async)",
297
+ )
298
+ @click.option("--with-context", is_flag=True, help="Include FastMCP Context parameter")
299
+ @click.option("--description", "-d", help="Resource description")
300
+ @click.option("--uri", help="Resource URI (default: resource://<name>)")
301
+ @click.option(
302
+ "--mime-type", default="text/plain", help="MIME type for resource (default: text/plain)"
303
+ )
304
+ @click.option("--dry-run", is_flag=True, help="Show what would be generated without creating files")
305
+ def resource(
306
+ name: str,
307
+ is_async: bool,
308
+ with_context: bool,
309
+ description: str | None,
310
+ uri: str | None,
311
+ mime_type: str,
312
+ dry_run: bool,
313
+ ):
314
+ """
315
+ Generate a new resource component.
316
+
317
+ NAME is the resource name in snake_case (e.g., config_data, user_profile)
318
+
319
+ Example:
320
+ fips-agents generate resource config_data --description "Application configuration"
321
+ fips-agents generate resource user_profile --uri "resource://users/{id}"
322
+ """
323
+ console.print("\n[bold cyan]Generating Resource Component[/bold cyan]\n")
324
+
325
+ # Default URI if not provided
326
+ if not uri:
327
+ uri = f"resource://{name}"
328
+
329
+ template_vars = {
330
+ "async": is_async,
331
+ "with_context": with_context,
332
+ "uri": uri,
333
+ "mime_type": mime_type,
334
+ "return_type": "str", # Resources typically return strings
335
+ }
336
+
337
+ generate_component_workflow("resource", name, template_vars, None, dry_run, description)
338
+
339
+
340
+ @generate.command("prompt")
341
+ @click.argument("name")
342
+ @click.option("--description", "-d", help="Prompt description")
343
+ @click.option("--params", type=click.Path(exists=True), help="JSON file with parameter definitions")
344
+ @click.option("--with-schema", is_flag=True, help="Include JSON schema in prompt")
345
+ @click.option("--dry-run", is_flag=True, help="Show what would be generated without creating files")
346
+ def prompt(
347
+ name: str,
348
+ description: str | None,
349
+ params: str | None,
350
+ with_schema: bool,
351
+ dry_run: bool,
352
+ ):
353
+ """
354
+ Generate a new prompt component.
355
+
356
+ NAME is the prompt name in snake_case (e.g., code_review, summarize_text)
357
+
358
+ Example:
359
+ fips-agents generate prompt code_review --description "Review code for best practices"
360
+ fips-agents generate prompt summarize_text --params params.json
361
+ """
362
+ console.print("\n[bold cyan]Generating Prompt Component[/bold cyan]\n")
363
+
364
+ template_vars = {
365
+ "with_schema": with_schema,
366
+ "async": False, # Prompts are typically not async
367
+ "return_type": "list[PromptMessage]",
368
+ }
369
+
370
+ generate_component_workflow("prompt", name, template_vars, params, dry_run, description)
371
+
372
+
373
+ @generate.command("middleware")
374
+ @click.argument("name")
375
+ @click.option(
376
+ "--async/--sync",
377
+ "is_async",
378
+ default=True,
379
+ help="Generate async or sync function (default: async)",
380
+ )
381
+ @click.option("--description", "-d", help="Middleware description")
382
+ @click.option("--dry-run", is_flag=True, help="Show what would be generated without creating files")
383
+ def middleware(
384
+ name: str,
385
+ is_async: bool,
386
+ description: str | None,
387
+ dry_run: bool,
388
+ ):
389
+ """
390
+ Generate a new middleware component.
391
+
392
+ NAME is the middleware name in snake_case (e.g., auth_middleware, rate_limiter)
393
+
394
+ Example:
395
+ fips-agents generate middleware auth_middleware --description "Authentication middleware"
396
+ fips-agents generate middleware rate_limiter --sync
397
+ """
398
+ console.print("\n[bold cyan]Generating Middleware Component[/bold cyan]\n")
399
+
400
+ template_vars = {
401
+ "async": is_async,
402
+ "return_type": "None", # Middleware typically doesn't return values
403
+ }
404
+
405
+ generate_component_workflow("middleware", name, template_vars, None, dry_run, description)
@@ -1,7 +1,6 @@
1
1
  """Filesystem utilities for project operations."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Optional
5
4
 
6
5
  from rich.console import Console
7
6
 
@@ -54,7 +53,7 @@ def check_directory_empty(path: Path) -> bool:
54
53
 
55
54
  def validate_target_directory(
56
55
  target_path: Path, allow_existing: bool = False
57
- ) -> tuple[bool, Optional[str]]:
56
+ ) -> tuple[bool, str | None]:
58
57
  """
59
58
  Validate that a target directory is suitable for project creation.
60
59
 
@@ -86,7 +85,7 @@ def validate_target_directory(
86
85
  return True, None
87
86
 
88
87
 
89
- def resolve_target_path(project_name: str, target_dir: Optional[str] = None) -> Path:
88
+ def resolve_target_path(project_name: str, target_dir: str | None = None) -> Path:
90
89
  """
91
90
  Resolve the target path for project creation.
92
91
 
@@ -105,7 +104,7 @@ def resolve_target_path(project_name: str, target_dir: Optional[str] = None) ->
105
104
  return base / project_name
106
105
 
107
106
 
108
- def get_relative_path(path: Path, base: Optional[Path] = None) -> str:
107
+ def get_relative_path(path: Path, base: Path | None = None) -> str:
109
108
  """
110
109
  Get a relative path string for display purposes.
111
110
 
@@ -0,0 +1,311 @@
1
+ """Generator utilities for rendering MCP component templates."""
2
+
3
+ import ast
4
+ import json
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import jinja2
10
+ import tomlkit
11
+ from rich.console import Console
12
+
13
+ console = Console()
14
+
15
+
16
+ def get_project_info(project_root: Path) -> dict[str, Any]:
17
+ """
18
+ Extract project metadata from pyproject.toml.
19
+
20
+ Args:
21
+ project_root: Path to the project root directory
22
+
23
+ Returns:
24
+ dict: Project metadata including:
25
+ - name: Project name
26
+ - module_name: Module name (with underscores)
27
+ - version: Project version
28
+
29
+ Raises:
30
+ FileNotFoundError: If pyproject.toml doesn't exist
31
+ ValueError: If pyproject.toml is malformed
32
+
33
+ Example:
34
+ >>> info = get_project_info(Path("/path/to/project"))
35
+ >>> print(info["name"])
36
+ 'my-mcp-server'
37
+ """
38
+ pyproject_path = project_root / "pyproject.toml"
39
+
40
+ if not pyproject_path.exists():
41
+ raise FileNotFoundError(f"pyproject.toml not found at {pyproject_path}")
42
+
43
+ try:
44
+ with open(pyproject_path) as f:
45
+ pyproject = tomlkit.parse(f.read())
46
+
47
+ project_name = pyproject.get("project", {}).get("name", "unknown")
48
+ project_version = pyproject.get("project", {}).get("version", "0.1.0")
49
+ module_name = project_name.replace("-", "_")
50
+
51
+ return {
52
+ "name": project_name,
53
+ "module_name": module_name,
54
+ "version": project_version,
55
+ }
56
+
57
+ except Exception as e:
58
+ raise ValueError(f"Failed to parse pyproject.toml: {e}") from e
59
+
60
+
61
+ def load_template(project_root: Path, component_type: str, template_name: str) -> jinja2.Template:
62
+ """
63
+ Load a Jinja2 template from the project's generator templates.
64
+
65
+ Args:
66
+ project_root: Path to the project root directory
67
+ component_type: Type of component ('tool', 'resource', 'prompt', 'middleware')
68
+ template_name: Name of the template file (e.g., 'component.py.j2')
69
+
70
+ Returns:
71
+ jinja2.Template: Loaded Jinja2 template
72
+
73
+ Raises:
74
+ FileNotFoundError: If template file doesn't exist
75
+ jinja2.TemplateError: If template is malformed
76
+
77
+ Example:
78
+ >>> template = load_template(root, "tool", "component.py.j2")
79
+ >>> rendered = template.render(component_name="my_tool")
80
+ """
81
+ template_path = (
82
+ project_root / ".fips-agents-cli" / "generators" / component_type / template_name
83
+ )
84
+
85
+ if not template_path.exists():
86
+ raise FileNotFoundError(f"Template not found: {template_path}")
87
+
88
+ try:
89
+ with open(template_path) as f:
90
+ template_content = f.read()
91
+
92
+ # Create a Jinja2 environment with the template directory as the loader path
93
+ template_dir = template_path.parent
94
+ env = jinja2.Environment(
95
+ loader=jinja2.FileSystemLoader(str(template_dir)),
96
+ trim_blocks=True,
97
+ lstrip_blocks=True,
98
+ )
99
+
100
+ return env.from_string(template_content)
101
+
102
+ except jinja2.TemplateError as e:
103
+ raise jinja2.TemplateError(f"Failed to load template {template_name}: {e}") from e
104
+
105
+
106
+ def load_params_file(params_path: Path) -> list[dict[str, Any]]:
107
+ """
108
+ Load and validate parameter definitions from a JSON file.
109
+
110
+ Expected schema:
111
+ [
112
+ {
113
+ "name": "query",
114
+ "type": "str",
115
+ "description": "Search query",
116
+ "required": true,
117
+ "min_length": 1,
118
+ "max_length": 100
119
+ }
120
+ ]
121
+
122
+ Args:
123
+ params_path: Path to the JSON parameter file
124
+
125
+ Returns:
126
+ list: List of parameter definition dictionaries
127
+
128
+ Raises:
129
+ FileNotFoundError: If params file doesn't exist
130
+ ValueError: If JSON is invalid or schema is incorrect
131
+
132
+ Example:
133
+ >>> params = load_params_file(Path("params.json"))
134
+ >>> print(params[0]["name"])
135
+ 'query'
136
+ """
137
+ if not params_path.exists():
138
+ raise FileNotFoundError(f"Parameters file not found: {params_path}")
139
+
140
+ try:
141
+ with open(params_path) as f:
142
+ params = json.load(f)
143
+
144
+ except json.JSONDecodeError as e:
145
+ raise ValueError(f"Invalid JSON in parameters file: {e}") from e
146
+
147
+ # Validate schema
148
+ if not isinstance(params, list):
149
+ raise ValueError("Parameters file must contain a JSON array of parameter definitions")
150
+
151
+ for i, param in enumerate(params):
152
+ if not isinstance(param, dict):
153
+ raise ValueError(f"Parameter {i} must be a JSON object")
154
+
155
+ # Check required fields
156
+ required_fields = ["name", "type", "description"]
157
+ for field in required_fields:
158
+ if field not in param:
159
+ raise ValueError(f"Parameter {i} missing required field: {field}")
160
+
161
+ # Validate name is a valid Python identifier
162
+ if not param["name"].isidentifier():
163
+ raise ValueError(f"Parameter {i} has invalid name: {param['name']}")
164
+
165
+ # Validate type
166
+ valid_types = [
167
+ "str",
168
+ "int",
169
+ "float",
170
+ "bool",
171
+ "list[str]",
172
+ "list[int]",
173
+ "list[float]",
174
+ "Optional[str]",
175
+ "Optional[int]",
176
+ "Optional[float]",
177
+ "Optional[bool]",
178
+ ]
179
+ if param["type"] not in valid_types:
180
+ raise ValueError(
181
+ f"Parameter {i} has invalid type: {param['type']}. "
182
+ f"Valid types: {', '.join(valid_types)}"
183
+ )
184
+
185
+ return params
186
+
187
+
188
+ def render_component(template: jinja2.Template, variables: dict[str, Any]) -> str:
189
+ """
190
+ Render a Jinja2 template with the provided variables.
191
+
192
+ Args:
193
+ template: Jinja2 template object
194
+ variables: Dictionary of template variables
195
+
196
+ Returns:
197
+ str: Rendered template as a string
198
+
199
+ Raises:
200
+ jinja2.TemplateError: If rendering fails
201
+
202
+ Example:
203
+ >>> template = load_template(root, "tool", "component.py.j2")
204
+ >>> code = render_component(template, {"component_name": "my_tool"})
205
+ """
206
+ try:
207
+ return template.render(**variables)
208
+ except jinja2.TemplateError as e:
209
+ raise jinja2.TemplateError(f"Failed to render template: {e}") from e
210
+
211
+
212
+ def validate_python_syntax(code: str) -> tuple[bool, str]:
213
+ """
214
+ Validate Python code syntax using ast.parse().
215
+
216
+ Args:
217
+ code: Python code as a string
218
+
219
+ Returns:
220
+ tuple: (is_valid, error_message)
221
+ is_valid is True if syntax is valid, False otherwise
222
+ error_message is empty string if valid, otherwise contains error description
223
+
224
+ Example:
225
+ >>> code = "def my_func():\\n return 42"
226
+ >>> is_valid, msg = validate_python_syntax(code)
227
+ >>> print(is_valid)
228
+ True
229
+ """
230
+ try:
231
+ ast.parse(code)
232
+ return True, ""
233
+ except SyntaxError as e:
234
+ return False, f"Syntax error at line {e.lineno}: {e.msg}"
235
+ except Exception as e:
236
+ return False, f"Failed to validate syntax: {e}"
237
+
238
+
239
+ def write_component_file(content: str, file_path: Path) -> None:
240
+ """
241
+ Write component content to a file, creating parent directories if needed.
242
+
243
+ Args:
244
+ content: File content as a string
245
+ file_path: Path where the file should be written
246
+
247
+ Raises:
248
+ OSError: If file cannot be written (permissions, etc.)
249
+
250
+ Example:
251
+ >>> write_component_file("print('hello')", Path("src/tools/my_tool.py"))
252
+ """
253
+ try:
254
+ # Create parent directories if they don't exist
255
+ file_path.parent.mkdir(parents=True, exist_ok=True)
256
+
257
+ # Write the file
258
+ with open(file_path, "w") as f:
259
+ f.write(content)
260
+
261
+ except OSError as e:
262
+ raise OSError(f"Failed to write file {file_path}: {e}") from e
263
+
264
+
265
+ def run_component_tests(project_root: Path, test_file: Path) -> tuple[bool, str]:
266
+ """
267
+ Run pytest on a generated test file and capture output.
268
+
269
+ Args:
270
+ project_root: Path to the project root directory
271
+ test_file: Path to the test file to run (relative or absolute)
272
+
273
+ Returns:
274
+ tuple: (success, output)
275
+ success is True if tests passed, False if failed
276
+ output is the pytest output as a string
277
+
278
+ Example:
279
+ >>> success, output = run_component_tests(root, Path("tests/tools/test_my_tool.py"))
280
+ >>> if success:
281
+ ... print("Tests passed!")
282
+ """
283
+ try:
284
+ # Make test_file relative to project_root if it's absolute
285
+ if test_file.is_absolute():
286
+ try:
287
+ test_file = test_file.relative_to(project_root)
288
+ except ValueError:
289
+ # test_file is not relative to project_root, use as-is
290
+ pass
291
+
292
+ # Run pytest with minimal output
293
+ result = subprocess.run(
294
+ ["pytest", str(test_file), "-v", "--tb=short"],
295
+ cwd=str(project_root),
296
+ capture_output=True,
297
+ text=True,
298
+ timeout=30,
299
+ )
300
+
301
+ output = result.stdout + result.stderr
302
+ success = result.returncode == 0
303
+
304
+ return success, output
305
+
306
+ except subprocess.TimeoutExpired:
307
+ return False, "Test execution timed out after 30 seconds"
308
+ except FileNotFoundError:
309
+ return False, "pytest not found. Install with: pip install pytest"
310
+ except Exception as e:
311
+ return False, f"Failed to run tests: {e}"
@@ -3,7 +3,6 @@
3
3
  import re
4
4
  import shutil
5
5
  from pathlib import Path
6
- from typing import Optional
7
6
 
8
7
  import tomlkit
9
8
  from rich.console import Console
@@ -11,7 +10,7 @@ from rich.console import Console
11
10
  console = Console()
12
11
 
13
12
 
14
- def validate_project_name(name: str) -> tuple[bool, Optional[str]]:
13
+ def validate_project_name(name: str) -> tuple[bool, str | None]:
15
14
  """
16
15
  Validate project name according to Python package naming conventions.
17
16
 
@@ -0,0 +1,183 @@
1
+ """Validation utilities for MCP component generation."""
2
+
3
+ import keyword
4
+ import re
5
+ from pathlib import Path
6
+
7
+ import tomlkit
8
+ from rich.console import Console
9
+
10
+ console = Console()
11
+
12
+
13
+ def find_project_root() -> Path | None:
14
+ """
15
+ Find the project root by walking up from current directory.
16
+
17
+ Looks for pyproject.toml with fastmcp dependency to identify MCP server projects.
18
+
19
+ Returns:
20
+ Path: Project root path if found
21
+ None: If no valid MCP project root is found
22
+
23
+ Example:
24
+ >>> root = find_project_root()
25
+ >>> if root:
26
+ ... print(f"Found project at {root}")
27
+ """
28
+ current_path = Path.cwd()
29
+
30
+ # Walk up the directory tree
31
+ for parent in [current_path] + list(current_path.parents):
32
+ pyproject_path = parent / "pyproject.toml"
33
+
34
+ if pyproject_path.exists():
35
+ try:
36
+ with open(pyproject_path) as f:
37
+ pyproject = tomlkit.parse(f.read())
38
+
39
+ # Check if this is an MCP server project
40
+ dependencies = pyproject.get("project", {}).get("dependencies", [])
41
+
42
+ # Check for fastmcp dependency
43
+ for dep in dependencies:
44
+ if isinstance(dep, str) and "fastmcp" in dep.lower():
45
+ return parent
46
+
47
+ except Exception as e:
48
+ console.print(f"[yellow]⚠[/yellow] Could not parse {pyproject_path}: {e}")
49
+ continue
50
+
51
+ return None
52
+
53
+
54
+ def is_valid_component_name(name: str) -> tuple[bool, str]:
55
+ """
56
+ Validate component name as a valid Python identifier.
57
+
58
+ Component names must:
59
+ - Be valid Python identifiers (snake_case)
60
+ - Not be Python keywords
61
+ - Not be empty
62
+ - Start with a letter or underscore
63
+ - Contain only letters, numbers, and underscores
64
+
65
+ Args:
66
+ name: The component name to validate
67
+
68
+ Returns:
69
+ tuple: (is_valid, error_message)
70
+ is_valid is True if valid, False otherwise
71
+ error_message is empty string if valid, otherwise contains error description
72
+
73
+ Examples:
74
+ >>> is_valid_component_name("my_tool")
75
+ (True, '')
76
+ >>> is_valid_component_name("123invalid")
77
+ (False, 'Component name must start with a letter or underscore')
78
+ """
79
+ if not name:
80
+ return False, "Component name cannot be empty"
81
+
82
+ # Check if it's a valid Python identifier
83
+ if not name.isidentifier():
84
+ if name[0].isdigit():
85
+ return False, "Component name must start with a letter or underscore"
86
+ return False, (
87
+ "Component name must be a valid Python identifier (use snake_case: "
88
+ "letters, numbers, underscores only)"
89
+ )
90
+
91
+ # Check if it's a Python keyword
92
+ if keyword.iskeyword(name):
93
+ return False, f"Component name '{name}' is a Python keyword and cannot be used"
94
+
95
+ # Recommend snake_case
96
+ if not re.match(r"^[a-z_][a-z0-9_]*$", name):
97
+ return False, (
98
+ "Component name should use snake_case (lowercase letters, numbers, " "underscores only)"
99
+ )
100
+
101
+ return True, ""
102
+
103
+
104
+ def component_exists(project_root: Path, component_type: str, name: str) -> bool:
105
+ """
106
+ Check if a component file already exists.
107
+
108
+ Args:
109
+ project_root: Path to the project root directory
110
+ component_type: Type of component ('tool', 'resource', 'prompt', 'middleware')
111
+ name: Component name (will check for {name}.py)
112
+
113
+ Returns:
114
+ bool: True if component file exists, False otherwise
115
+
116
+ Example:
117
+ >>> root = Path("/path/to/project")
118
+ >>> component_exists(root, "tool", "my_tool")
119
+ False
120
+ """
121
+ # Map component types to their directory locations
122
+ component_dirs = {
123
+ "tool": "tools",
124
+ "resource": "resources",
125
+ "prompt": "prompts",
126
+ "middleware": "middleware",
127
+ }
128
+
129
+ if component_type not in component_dirs:
130
+ return False
131
+
132
+ component_dir = component_dirs[component_type]
133
+ component_file = project_root / "src" / component_dir / f"{name}.py"
134
+
135
+ return component_file.exists()
136
+
137
+
138
+ def validate_generator_templates(project_root: Path, component_type: str) -> tuple[bool, str]:
139
+ """
140
+ Validate that generator templates exist for the component type.
141
+
142
+ Args:
143
+ project_root: Path to the project root directory
144
+ component_type: Type of component ('tool', 'resource', 'prompt', 'middleware')
145
+
146
+ Returns:
147
+ tuple: (is_valid, error_message)
148
+ is_valid is True if templates exist, False otherwise
149
+ error_message is empty string if valid, otherwise contains error description
150
+
151
+ Example:
152
+ >>> root = Path("/path/to/project")
153
+ >>> is_valid, msg = validate_generator_templates(root, "tool")
154
+ >>> if is_valid:
155
+ ... print("Templates found!")
156
+ """
157
+ generators_dir = project_root / ".fips-agents-cli" / "generators" / component_type
158
+
159
+ if not generators_dir.exists():
160
+ return False, (
161
+ f"Generator templates not found for '{component_type}'\n"
162
+ f"Expected: {generators_dir}\n"
163
+ "Was this project created with fips-agents create mcp-server?"
164
+ )
165
+
166
+ # Check for required template files
167
+ component_template = generators_dir / "component.py.j2"
168
+ test_template = generators_dir / "test.py.j2"
169
+
170
+ missing_files = []
171
+ if not component_template.exists():
172
+ missing_files.append("component.py.j2")
173
+ if not test_template.exists():
174
+ missing_files.append("test.py.j2")
175
+
176
+ if missing_files:
177
+ return False, (
178
+ f"Missing template files for '{component_type}':\n"
179
+ f" {', '.join(missing_files)}\n"
180
+ f"Expected location: {generators_dir}"
181
+ )
182
+
183
+ return True, ""
@@ -1,3 +1,3 @@
1
1
  """Version information for fips-agents-cli."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fips-agents-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: CLI tool for creating and managing FIPS-compliant AI agent projects
5
5
  Project-URL: Homepage, https://github.com/rdwj/fips-agents-cli
6
6
  Project-URL: Repository, https://github.com/rdwj/fips-agents-cli
@@ -13,15 +13,15 @@ Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: MIT License
15
15
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.9
17
16
  Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
19
  Classifier: Topic :: Software Development :: Code Generators
21
20
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
- Requires-Python: >=3.9
21
+ Requires-Python: >=3.10
23
22
  Requires-Dist: click>=8.1.0
24
23
  Requires-Dist: gitpython>=3.1.0
24
+ Requires-Dist: jinja2>=3.1.2
25
25
  Requires-Dist: rich>=13.0.0
26
26
  Requires-Dist: tomlkit>=0.12.0
27
27
  Provides-Extra: dev
@@ -40,9 +40,10 @@ A command-line tool for creating and managing FIPS-compliant AI agent projects,
40
40
  - 🚀 Quick project scaffolding from templates
41
41
  - 📦 MCP server project generation
42
42
  - 🔧 Automatic project customization
43
+ - ⚡ Component generation (tools, resources, prompts, middleware)
43
44
  - 🎨 Beautiful CLI output with Rich
44
45
  - ✅ Git repository initialization
45
- - 🧪 Comprehensive test coverage
46
+ - 🧪 Comprehensive test coverage with auto-run
46
47
 
47
48
  ## Installation
48
49
 
@@ -99,6 +100,25 @@ fips-agents create mcp-server my-server --target-dir ~/projects
99
100
  fips-agents create mcp-server my-server --no-git
100
101
  ```
101
102
 
103
+ ### Generate components in an existing project
104
+
105
+ ```bash
106
+ # Navigate to your MCP server project
107
+ cd my-mcp-server
108
+
109
+ # Generate a new tool
110
+ fips-agents generate tool search_documents --description "Search through documents"
111
+
112
+ # Generate a resource
113
+ fips-agents generate resource config_data --description "Application configuration"
114
+
115
+ # Generate a prompt
116
+ fips-agents generate prompt code_review --description "Review code for best practices"
117
+
118
+ # Generate middleware
119
+ fips-agents generate middleware auth_middleware --description "Authentication middleware"
120
+ ```
121
+
102
122
  ## Usage
103
123
 
104
124
  ### Basic Commands
@@ -111,6 +131,8 @@ fips-agents --version
111
131
  fips-agents --help
112
132
  fips-agents create --help
113
133
  fips-agents create mcp-server --help
134
+ fips-agents generate --help
135
+ fips-agents generate tool --help
114
136
  ```
115
137
 
116
138
  ### Create MCP Server
@@ -140,6 +162,168 @@ fips-agents create mcp-server my-server -t ~/projects
140
162
  fips-agents create mcp-server my-server --no-git
141
163
  ```
142
164
 
165
+ ### Generate Components
166
+
167
+ The `generate` command group allows you to scaffold MCP components (tools, resources, prompts, middleware) in existing MCP server projects.
168
+
169
+ **Important**: Run these commands from within your MCP server project directory.
170
+
171
+ #### Generate Tool
172
+
173
+ ```bash
174
+ fips-agents generate tool <name> [OPTIONS]
175
+ ```
176
+
177
+ **Arguments:**
178
+ - `name`: Tool name in snake_case (e.g., `search_documents`, `fetch_data`)
179
+
180
+ **Options:**
181
+ - `--description, -d TEXT`: Tool description
182
+ - `--async/--sync`: Generate async or sync function (default: async)
183
+ - `--with-context`: Include FastMCP Context parameter
184
+ - `--with-auth`: Include authentication decorator
185
+ - `--params PATH`: JSON file with parameter definitions
186
+ - `--read-only`: Mark as read-only operation (default: true)
187
+ - `--idempotent`: Mark as idempotent (default: true)
188
+ - `--open-world`: Mark as open-world operation
189
+ - `--return-type TEXT`: Return type annotation (default: str)
190
+ - `--dry-run`: Show what would be generated without creating files
191
+
192
+ **Examples:**
193
+
194
+ ```bash
195
+ # Basic tool generation
196
+ fips-agents generate tool search_documents --description "Search through documents"
197
+
198
+ # Tool with context and authentication
199
+ fips-agents generate tool fetch_user_data --description "Fetch user data" --with-context --with-auth
200
+
201
+ # Tool with parameters from JSON file
202
+ fips-agents generate tool advanced_search --params params.json
203
+
204
+ # Sync tool with custom return type
205
+ fips-agents generate tool process_data --sync --return-type "dict[str, Any]"
206
+
207
+ # Dry run to preview
208
+ fips-agents generate tool test_tool --description "Test" --dry-run
209
+ ```
210
+
211
+ #### Generate Resource
212
+
213
+ ```bash
214
+ fips-agents generate resource <name> [OPTIONS]
215
+ ```
216
+
217
+ **Arguments:**
218
+ - `name`: Resource name in snake_case (e.g., `config_data`, `user_profile`)
219
+
220
+ **Options:**
221
+ - `--description, -d TEXT`: Resource description
222
+ - `--async/--sync`: Generate async or sync function (default: async)
223
+ - `--with-context`: Include FastMCP Context parameter
224
+ - `--uri TEXT`: Resource URI (default: `resource://<name>`)
225
+ - `--mime-type TEXT`: MIME type for resource (default: text/plain)
226
+ - `--dry-run`: Show what would be generated without creating files
227
+
228
+ **Examples:**
229
+
230
+ ```bash
231
+ # Basic resource
232
+ fips-agents generate resource config_data --description "Application configuration"
233
+
234
+ # Resource with custom URI
235
+ fips-agents generate resource user_profile --uri "resource://users/{id}" --description "User profile data"
236
+
237
+ # Resource with specific MIME type
238
+ fips-agents generate resource json_config --mime-type "application/json"
239
+ ```
240
+
241
+ #### Generate Prompt
242
+
243
+ ```bash
244
+ fips-agents generate prompt <name> [OPTIONS]
245
+ ```
246
+
247
+ **Arguments:**
248
+ - `name`: Prompt name in snake_case (e.g., `code_review`, `summarize_text`)
249
+
250
+ **Options:**
251
+ - `--description, -d TEXT`: Prompt description
252
+ - `--params PATH`: JSON file with parameter definitions
253
+ - `--with-schema`: Include JSON schema in prompt
254
+ - `--dry-run`: Show what would be generated without creating files
255
+
256
+ **Examples:**
257
+
258
+ ```bash
259
+ # Basic prompt
260
+ fips-agents generate prompt code_review --description "Review code for best practices"
261
+
262
+ # Prompt with parameters
263
+ fips-agents generate prompt summarize_text --params params.json --with-schema
264
+ ```
265
+
266
+ #### Generate Middleware
267
+
268
+ ```bash
269
+ fips-agents generate middleware <name> [OPTIONS]
270
+ ```
271
+
272
+ **Arguments:**
273
+ - `name`: Middleware name in snake_case (e.g., `auth_middleware`, `rate_limiter`)
274
+
275
+ **Options:**
276
+ - `--description, -d TEXT`: Middleware description
277
+ - `--async/--sync`: Generate async or sync function (default: async)
278
+ - `--dry-run`: Show what would be generated without creating files
279
+
280
+ **Examples:**
281
+
282
+ ```bash
283
+ # Basic middleware
284
+ fips-agents generate middleware auth_middleware --description "Authentication middleware"
285
+
286
+ # Sync middleware
287
+ fips-agents generate middleware rate_limiter --sync --description "Rate limiting middleware"
288
+ ```
289
+
290
+ #### Parameters JSON Schema
291
+
292
+ When using `--params` flag, provide a JSON file with parameter definitions:
293
+
294
+ ```json
295
+ [
296
+ {
297
+ "name": "query",
298
+ "type": "str",
299
+ "description": "Search query",
300
+ "required": true,
301
+ "min_length": 1,
302
+ "max_length": 100
303
+ },
304
+ {
305
+ "name": "limit",
306
+ "type": "int",
307
+ "description": "Maximum results to return",
308
+ "required": false,
309
+ "default": 10,
310
+ "ge": 1,
311
+ "le": 100
312
+ }
313
+ ]
314
+ ```
315
+
316
+ **Supported Types:**
317
+ - `str`, `int`, `float`, `bool`
318
+ - `list[str]`, `list[int]`, `list[float]`
319
+ - `Optional[str]`, `Optional[int]`, `Optional[float]`, `Optional[bool]`
320
+
321
+ **Pydantic Field Constraints:**
322
+ - `min_length`, `max_length` (for strings)
323
+ - `ge`, `le`, `gt`, `lt` (for numbers)
324
+ - `pattern` (for regex validation on strings)
325
+ - `default` (default value when optional)
326
+
143
327
  ## Project Name Requirements
144
328
 
145
329
  Project names must follow these rules:
@@ -249,7 +433,7 @@ fips-agents-cli/
249
433
 
250
434
  ## Requirements
251
435
 
252
- - Python 3.9 or higher
436
+ - Python 3.10 or higher
253
437
  - Git (for cloning templates and initializing repositories)
254
438
 
255
439
  ## Dependencies
@@ -258,6 +442,7 @@ fips-agents-cli/
258
442
  - **rich** (>=13.0.0): Terminal output formatting
259
443
  - **gitpython** (>=3.1.0): Git operations
260
444
  - **tomlkit** (>=0.12.0): TOML file manipulation
445
+ - **jinja2** (>=3.1.2): Template rendering for component generation
261
446
 
262
447
  ## Contributing
263
448
 
@@ -295,6 +480,28 @@ If template cloning fails:
295
480
  - Verify the template repository is accessible: https://github.com/rdwj/mcp-server-template
296
481
  - Try again later if GitHub is experiencing issues
297
482
 
483
+ ### "Not in an MCP server project directory"
484
+
485
+ When using `generate` commands:
486
+ - Ensure you're running the command from within an MCP server project
487
+ - Check that `pyproject.toml` exists with `fastmcp` dependency
488
+ - If the project wasn't created with `fips-agents create mcp-server`, generator templates may be missing
489
+
490
+ ### "Component already exists"
491
+
492
+ If you see this error:
493
+ - Choose a different component name
494
+ - Manually remove the existing component file from `src/<component-type>/`
495
+ - Check the component type directory for existing files
496
+
497
+ ### Invalid component name
498
+
499
+ Component names must:
500
+ - Be valid Python identifiers (snake_case)
501
+ - Not be Python keywords (`for`, `class`, etc.)
502
+ - Start with a letter or underscore
503
+ - Contain only letters, numbers, and underscores
504
+
298
505
  ## License
299
506
 
300
507
  MIT License - see LICENSE file for details
@@ -307,6 +514,16 @@ MIT License - see LICENSE file for details
307
514
 
308
515
  ## Changelog
309
516
 
517
+ ### Version 0.1.1 (Current)
518
+
519
+ - Added `fips-agents generate` command group
520
+ - Component generation: tools, resources, prompts, middleware
521
+ - Jinja2-based template rendering
522
+ - Parameter validation and JSON schema support
523
+ - Auto-run pytest on generated components
524
+ - Dry-run mode for previewing changes
525
+ - Comprehensive error handling and validation
526
+
310
527
  ### Version 0.1.0 (MVP)
311
528
 
312
529
  - Initial release
@@ -0,0 +1,18 @@
1
+ fips_agents_cli/__init__.py,sha256=Jfo6y6T4HIQ-BeeF89w7F-F4BED63KyCIc1yoFGn9OM,167
2
+ fips_agents_cli/__main__.py,sha256=rUeQY3jrV6hQVAI2IE0qZCcUnvXDMj5LiCjhlXsc9PQ,130
3
+ fips_agents_cli/cli.py,sha256=c6ZSIfqZXTAfGc_2sKXanWDuWbxXTUjp4nSbl7yeDig,799
4
+ fips_agents_cli/version.py,sha256=N1v0FJOEwbpJriPhyukh9-v8PmOs6ppJbMd7pTWGYmc,70
5
+ fips_agents_cli/commands/__init__.py,sha256=AGxi2-Oc-gKE3sR9F39nIxwnzz-4bcfkkJzaP1qhMvU,40
6
+ fips_agents_cli/commands/create.py,sha256=wWJZTa2NOoF40lnqIGKXKNcZT5jIZxInGk3CAkUfa3w,6372
7
+ fips_agents_cli/commands/generate.py,sha256=hoPERkaYGlt-R79PRWnxE6vwNtWfun7SMtJN2zaOIKo,14113
8
+ fips_agents_cli/tools/__init__.py,sha256=ah4OrYFuyQavuhwguFwFS0o-7ZLGIm5ZWWQK5ZTZZSs,47
9
+ fips_agents_cli/tools/filesystem.py,sha256=1AOOJtkDSw-pqkuUJyVUbquuczpZBZMT4HLl3qCPB3k,3276
10
+ fips_agents_cli/tools/generators.py,sha256=bwHKBzWkfJxug4YQXrkRQB_-BxR78kZqe46AjC7haHY,9372
11
+ fips_agents_cli/tools/git.py,sha256=-z2EO7vNZhN6Vuzh34ZbqNteZE4vNnLG8oysgzhmypk,3042
12
+ fips_agents_cli/tools/project.py,sha256=vgnctNgdRJJB14uqy7WM0bqpAoPoK_fZu16Io5Rn2hA,5699
13
+ fips_agents_cli/tools/validation.py,sha256=3947i5UI46BmwBw4F0yLNdkWvkGJig8qH1nJzvQS6RA,5771
14
+ fips_agents_cli-0.1.1.dist-info/METADATA,sha256=O8D_AVK27NvVPL61BdbWHAwpgbFhz7gsZOuGAZXh2eA,14660
15
+ fips_agents_cli-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ fips_agents_cli-0.1.1.dist-info/entry_points.txt,sha256=srO4LAGNp6zcB9zuPW1toLGPyLbcsad9YWsfNxgz20s,57
17
+ fips_agents_cli-0.1.1.dist-info/licenses/LICENSE,sha256=dQJIqi2t9SinZu0yALTYJ8juzosu29KPbjU8WhyboRc,1068
18
+ fips_agents_cli-0.1.1.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- fips_agents_cli/__init__.py,sha256=Jfo6y6T4HIQ-BeeF89w7F-F4BED63KyCIc1yoFGn9OM,167
2
- fips_agents_cli/__main__.py,sha256=rUeQY3jrV6hQVAI2IE0qZCcUnvXDMj5LiCjhlXsc9PQ,130
3
- fips_agents_cli/cli.py,sha256=e2QGiAH73pHAjpdf7JdovyNZtc2nyb83LaVaLbLqS5o,718
4
- fips_agents_cli/version.py,sha256=EeROn-Cy9mXSPq-OrZ--hVR0oQvwMNqIyAgy4w-YowU,70
5
- fips_agents_cli/commands/__init__.py,sha256=AGxi2-Oc-gKE3sR9F39nIxwnzz-4bcfkkJzaP1qhMvU,40
6
- fips_agents_cli/commands/create.py,sha256=GvGm8VgsIfqZV_yIXXYzeV-nasLIwuD85_l831YjAho,6403
7
- fips_agents_cli/tools/__init__.py,sha256=ah4OrYFuyQavuhwguFwFS0o-7ZLGIm5ZWWQK5ZTZZSs,47
8
- fips_agents_cli/tools/filesystem.py,sha256=G9wzHrgQpoMw3z5EslsyEL91VLlUT6Cf5Rt80DtkwOw,3313
9
- fips_agents_cli/tools/git.py,sha256=-z2EO7vNZhN6Vuzh34ZbqNteZE4vNnLG8oysgzhmypk,3042
10
- fips_agents_cli/tools/project.py,sha256=d_qIhsEzPOM55ONSLzheTozXuyuiEEf9OG4hVXISAbk,5730
11
- fips_agents_cli-0.1.0.dist-info/METADATA,sha256=j3-wAEIftpKnjwj4BVKykYPQh4eDXGva_RNIKh55Qiw,8220
12
- fips_agents_cli-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
- fips_agents_cli-0.1.0.dist-info/entry_points.txt,sha256=srO4LAGNp6zcB9zuPW1toLGPyLbcsad9YWsfNxgz20s,57
14
- fips_agents_cli-0.1.0.dist-info/licenses/LICENSE,sha256=dQJIqi2t9SinZu0yALTYJ8juzosu29KPbjU8WhyboRc,1068
15
- fips_agents_cli-0.1.0.dist-info/RECORD,,