context-gen-cli 1.2.0__tar.gz → 1.2.4__tar.gz

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 (19) hide show
  1. {context_gen_cli-1.2.0 → context_gen_cli-1.2.4}/PKG-INFO +1 -1
  2. {context_gen_cli-1.2.0 → context_gen_cli-1.2.4}/context_gen/cli.py +23 -2
  3. context_gen_cli-1.2.4/context_gen/mcp/__init__.py +3 -0
  4. context_gen_cli-1.2.4/context_gen/mcp/advanced_tools.py +475 -0
  5. {context_gen_cli-1.2.0 → context_gen_cli-1.2.4}/context_gen/scanner.py +1 -1
  6. {context_gen_cli-1.2.0 → context_gen_cli-1.2.4}/context_gen_cli.egg-info/PKG-INFO +1 -1
  7. {context_gen_cli-1.2.0 → context_gen_cli-1.2.4}/context_gen_cli.egg-info/SOURCES.txt +4 -1
  8. {context_gen_cli-1.2.0 → context_gen_cli-1.2.4}/context_gen_cli.egg-info/entry_points.txt +1 -0
  9. {context_gen_cli-1.2.0 → context_gen_cli-1.2.4}/pyproject.toml +2 -1
  10. context_gen_cli-1.2.4/tests/test_advanced_tools.py +74 -0
  11. {context_gen_cli-1.2.0 → context_gen_cli-1.2.4}/README.md +0 -0
  12. {context_gen_cli-1.2.0 → context_gen_cli-1.2.4}/context_gen/__init__.py +0 -0
  13. {context_gen_cli-1.2.0 → context_gen_cli-1.2.4}/context_gen/extractor.py +0 -0
  14. {context_gen_cli-1.2.0 → context_gen_cli-1.2.4}/context_gen/generator.py +0 -0
  15. {context_gen_cli-1.2.0 → context_gen_cli-1.2.4}/context_gen/graph.py +0 -0
  16. {context_gen_cli-1.2.0 → context_gen_cli-1.2.4}/context_gen_cli.egg-info/dependency_links.txt +0 -0
  17. {context_gen_cli-1.2.0 → context_gen_cli-1.2.4}/context_gen_cli.egg-info/requires.txt +0 -0
  18. {context_gen_cli-1.2.0 → context_gen_cli-1.2.4}/context_gen_cli.egg-info/top_level.txt +0 -0
  19. {context_gen_cli-1.2.0 → context_gen_cli-1.2.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: context-gen-cli
3
- Version: 1.2.0
3
+ Version: 1.2.4
4
4
  Summary: A modern CLI tool that scans any project, builds an AST-based dependency graph, and generates structured LLM context (agent.md / agent.json).
5
5
  Author: yakkshit
6
6
  License-Expression: MIT
@@ -166,7 +166,16 @@ def main(
166
166
  path: Path = typer.Argument(
167
167
  default=Path("."),
168
168
  help="Target project directory to analyze.",
169
- exists=True,
169
+ file_okay=False,
170
+ dir_okay=True,
171
+ readable=True,
172
+ resolve_path=True,
173
+ ),
174
+ project: Optional[Path] = typer.Option(
175
+ None,
176
+ "--project",
177
+ "-p",
178
+ help="Target project directory to analyze.",
170
179
  file_okay=False,
171
180
  dir_okay=True,
172
181
  readable=True,
@@ -185,6 +194,18 @@ def main(
185
194
  help="Output format: 'all', 'md', 'json', or 'yaml'.",
186
195
  case_sensitive=False,
187
196
  ),
197
+ mcp: bool = typer.Option(
198
+ False,
199
+ "--mcp",
200
+ "-m",
201
+ help="Start MCP server mode.",
202
+ ),
203
+ interactive: bool = typer.Option(
204
+ False,
205
+ "--interactive",
206
+ "-i",
207
+ help="Run in interactive mode.",
208
+ ),
188
209
  output: Optional[Path] = typer.Option(
189
210
  None,
190
211
  "--output",
@@ -208,7 +229,7 @@ def main(
208
229
  """
209
230
  Scan a project, build an AST-powered dependency graph, and generate LLM context.
210
231
  """
211
- target_dir = path.resolve()
232
+ target_dir = (project or path).resolve()
212
233
 
213
234
  # If -d / --default is supplied, generate all formats and keep watch running
214
235
  if default_mode:
@@ -0,0 +1,3 @@
1
+ """
2
+ context_gen.mcp: Model Context Protocol tools and server integrations for context-gen-cli.
3
+ """
@@ -0,0 +1,475 @@
1
+ """
2
+ context_gen.mcp.advanced_tools: Advanced MCP tools for deep code inspection, search, AST analysis, git diffs, and security validation.
3
+ """
4
+
5
+ import ast
6
+ import fnmatch
7
+ from pathlib import Path
8
+ import re
9
+ import subprocess
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from context_gen.extractor import extract_metadata
13
+ from context_gen.graph import build_dependency_graph
14
+ from context_gen.scanner import scan_project
15
+
16
+ SENSITIVE_EXTENSIONS = {
17
+ ".pem", ".key", ".p12", ".pfx", ".pkcs12", ".keystore",
18
+ ".jks", ".ovpn", ".crt", ".der"
19
+ }
20
+
21
+ SENSITIVE_FILENAME_REGEX = re.compile(
22
+ r"^(id_rsa.*|id_ed25519.*|\.env(\..+)?|secrets?(\..+)?|credentials?(\..+)?)$",
23
+ re.IGNORECASE
24
+ )
25
+
26
+ CONTENT_SECRET_REGEXES = [
27
+ re.compile(r"-----BEGIN [A-Z0-9_-]+ PRIVATE KEY-----", re.IGNORECASE),
28
+ re.compile(r"-----BEGIN CERTIFICATE-----", re.IGNORECASE),
29
+ re.compile(r"ghp_[a-zA-Z0-9]{36}"),
30
+ re.compile(r"github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}"),
31
+ re.compile(r"sk-[a-zA-Z0-9]{20,}"),
32
+ re.compile(r"xox[baprs]-[a-zA-Z0-9]{10,}"),
33
+ re.compile(r"AIza[0-9A-Za-z-_]{35}"),
34
+ re.compile(r"AKIA[0-9A-Z]{16}")
35
+ ]
36
+
37
+ LANGUAGE_MAP = {
38
+ ".py": "python",
39
+ ".js": "javascript",
40
+ ".jsx": "javascript",
41
+ ".ts": "typescript",
42
+ ".tsx": "typescript",
43
+ ".json": "json",
44
+ ".yaml": "yaml",
45
+ ".yml": "yaml",
46
+ ".md": "markdown",
47
+ ".html": "html",
48
+ ".css": "css",
49
+ ".go": "go",
50
+ ".rs": "rust",
51
+ ".java": "java",
52
+ ".c": "c",
53
+ ".cpp": "cpp",
54
+ ".sh": "bash"
55
+ }
56
+
57
+
58
+ def is_sensitive_path(rel_path: str, full_path: Optional[Path] = None) -> Optional[str]:
59
+ """Checks whether a file path or its content represents a sensitive secret."""
60
+ path_obj = Path(rel_path)
61
+ filename = path_obj.name
62
+ ext = path_obj.suffix.lower()
63
+
64
+ if ext in SENSITIVE_EXTENSIONS:
65
+ return f"Sensitive key/certificate extension ({ext})"
66
+
67
+ if SENSITIVE_FILENAME_REGEX.match(filename):
68
+ return f"Sensitive secret/environment file ({filename})"
69
+
70
+ if full_path and full_path.is_file():
71
+ try:
72
+ if full_path.stat().st_size <= 512 * 1024:
73
+ content = full_path.read_text(encoding="utf-8", errors="ignore")
74
+ for pattern in CONTENT_SECRET_REGEXES:
75
+ if pattern.search(content):
76
+ return "Contains embedded secret token or cryptographic private key"
77
+ except Exception:
78
+ pass
79
+
80
+ return None
81
+
82
+
83
+ def get_code_snippet(
84
+ file_path: str,
85
+ function_name: Optional[str] = None,
86
+ class_name: Optional[str] = None,
87
+ start_line: Optional[int] = None,
88
+ end_line: Optional[int] = None,
89
+ project_path: str = "."
90
+ ) -> Dict[str, Any]:
91
+ """
92
+ Reads a file and extracts the requested section (by function/class name or line range).
93
+ Respects security boundaries.
94
+ """
95
+ root = Path(project_path).resolve()
96
+ target = (root / file_path).resolve()
97
+
98
+ if not target.is_file():
99
+ raise FileNotFoundError(f"File not found: {file_path}")
100
+
101
+ rel_path = target.relative_to(root).as_posix()
102
+ sec_issue = is_sensitive_path(rel_path, target)
103
+ if sec_issue:
104
+ raise PermissionError(f"Access denied: {rel_path} is protected for security ({sec_issue})")
105
+
106
+ content = target.read_text(encoding="utf-8", errors="replace")
107
+ lines = content.splitlines(keepends=True)
108
+ ext = target.suffix.lower()
109
+ language = LANGUAGE_MAP.get(ext, "text")
110
+
111
+ # 1. Function / Class extraction using Python AST for Python files
112
+ if (function_name or class_name) and ext == ".py":
113
+ try:
114
+ tree = ast.parse(content, filename=str(target))
115
+ target_node = None
116
+
117
+ for node in ast.walk(tree):
118
+ if function_name and isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
119
+ if node.name == function_name:
120
+ target_node = node
121
+ break
122
+ elif class_name and isinstance(node, ast.ClassDef):
123
+ if node.name == class_name:
124
+ target_node = node
125
+ break
126
+
127
+ if target_node:
128
+ s_line = target_node.lineno
129
+ e_line = getattr(target_node, "end_lineno", s_line)
130
+ snippet_lines = lines[s_line - 1:e_line]
131
+ return {
132
+ "path": rel_path,
133
+ "content": "".join(snippet_lines),
134
+ "startLine": s_line,
135
+ "endLine": e_line,
136
+ "language": language
137
+ }
138
+ except SyntaxError:
139
+ pass
140
+
141
+ # Fallback for JS/TS/other languages or when AST match was not found
142
+ if function_name or class_name:
143
+ query_name = function_name or class_name
144
+ pattern = re.compile(rf"(?:def|function|class|const|let|var)\s+{re.escape(query_name)}\b")
145
+ found_start = None
146
+ for idx, line in enumerate(lines):
147
+ if pattern.search(line):
148
+ found_start = idx + 1
149
+ break
150
+
151
+ if found_start:
152
+ s_line = max(1, found_start)
153
+ e_line = min(len(lines), s_line + 40)
154
+ return {
155
+ "path": rel_path,
156
+ "content": "".join(lines[s_line - 1:e_line]),
157
+ "startLine": s_line,
158
+ "endLine": e_line,
159
+ "language": language
160
+ }
161
+
162
+ # 2. Line range extraction
163
+ s_line = max(1, start_line) if start_line is not None else 1
164
+ e_line = min(len(lines), end_line) if end_line is not None else len(lines)
165
+
166
+ snippet = "".join(lines[s_line - 1:e_line])
167
+ return {
168
+ "path": rel_path,
169
+ "content": snippet,
170
+ "startLine": s_line,
171
+ "endLine": e_line,
172
+ "language": language
173
+ }
174
+
175
+
176
+ def search_code(
177
+ pattern: str,
178
+ file_pattern: Optional[str] = None,
179
+ case_sensitive: bool = False,
180
+ max_results: int = 50,
181
+ project_path: str = "."
182
+ ) -> Dict[str, Any]:
183
+ """
184
+ Grep-like search across all project files respecting security and ignore rules.
185
+ Limits to 50 results maximum.
186
+ """
187
+ root = Path(project_path).resolve()
188
+ scanned = scan_project(str(root))
189
+ files = scanned.get("files", [])
190
+
191
+ flags = 0 if case_sensitive else re.IGNORECASE
192
+ try:
193
+ regex = re.compile(pattern, flags)
194
+ except re.error:
195
+ regex = re.compile(re.escape(pattern), flags)
196
+
197
+ matches: List[Dict[str, Any]] = []
198
+ limit = min(max_results, 50)
199
+
200
+ for rel_file in files:
201
+ if file_pattern and not fnmatch.fnmatch(rel_file, file_pattern):
202
+ continue
203
+
204
+ full_path = root / rel_file
205
+ if is_sensitive_path(rel_file, full_path):
206
+ continue
207
+
208
+ try:
209
+ content = full_path.read_text(encoding="utf-8", errors="ignore")
210
+ lines = content.splitlines()
211
+
212
+ for idx, line in enumerate(lines):
213
+ if regex.search(line):
214
+ # Gather context (1 line before & after)
215
+ start_ctx = max(0, idx - 1)
216
+ end_ctx = min(len(lines), idx + 2)
217
+ context_block = "\n".join(lines[start_ctx:end_ctx])
218
+
219
+ matches.append({
220
+ "file": rel_file,
221
+ "line": line.strip(),
222
+ "lineNumber": idx + 1,
223
+ "context": context_block
224
+ })
225
+
226
+ if len(matches) >= limit:
227
+ return {"matches": matches, "total": len(matches), "capped": True}
228
+ except Exception:
229
+ continue
230
+
231
+ return {"matches": matches, "total": len(matches), "capped": False}
232
+
233
+
234
+ def list_functions(
235
+ file_path: str,
236
+ project_path: str = "."
237
+ ) -> Dict[str, Any]:
238
+ """
239
+ Parses a file and lists all declared/exported functions, classes, and signatures.
240
+ Uses Python AST for 100% accurate Python parsing and regex heuristics for JS/TS.
241
+ """
242
+ root = Path(project_path).resolve()
243
+ target = (root / file_path).resolve()
244
+
245
+ if not target.is_file():
246
+ raise FileNotFoundError(f"File not found: {file_path}")
247
+
248
+ rel_path = target.relative_to(root).as_posix()
249
+ sec_issue = is_sensitive_path(rel_path, target)
250
+ if sec_issue:
251
+ raise PermissionError(f"Access denied: {rel_path} is protected for security ({sec_issue})")
252
+
253
+ content = target.read_text(encoding="utf-8", errors="replace")
254
+ ext = target.suffix.lower()
255
+ functions: List[Dict[str, Any]] = []
256
+
257
+ # Python AST parsing
258
+ if ext == ".py":
259
+ try:
260
+ tree = ast.parse(content, filename=str(target))
261
+ for node in tree.body:
262
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
263
+ args = [a.arg for a in node.args.args]
264
+ sig = f"def {node.name}({', '.join(args)})"
265
+ doc = ast.get_docstring(node) or ""
266
+ functions.append({
267
+ "name": node.name,
268
+ "type": "async_function" if isinstance(node, ast.AsyncFunctionDef) else "function",
269
+ "line": node.lineno,
270
+ "signature": sig,
271
+ "description": doc.strip().split("\n")[0] if doc else ""
272
+ })
273
+ elif isinstance(node, ast.ClassDef):
274
+ doc = ast.get_docstring(node) or ""
275
+ functions.append({
276
+ "name": node.name,
277
+ "type": "class",
278
+ "line": node.lineno,
279
+ "signature": f"class {node.name}",
280
+ "description": doc.strip().split("\n")[0] if doc else ""
281
+ })
282
+ except SyntaxError:
283
+ pass
284
+
285
+ # JavaScript / TypeScript heuristic parsing
286
+ elif ext in {".js", ".jsx", ".ts", ".tsx", ".mjs"}:
287
+ lines = content.splitlines()
288
+ for idx, line in enumerate(lines):
289
+ line_no = idx + 1
290
+ # export function ...
291
+ m_func = re.search(r"export\s+(?:default\s+)?(?:async\s+)?function\s+([a-zA-Z0-9_$]+)\s*\((.*?)\)", line)
292
+ if m_func:
293
+ functions.append({
294
+ "name": m_func.group(1),
295
+ "type": "function",
296
+ "line": line_no,
297
+ "signature": f"function {m_func.group(1)}({m_func.group(2)})",
298
+ "description": "Exported function"
299
+ })
300
+ continue
301
+
302
+ # export const name = ...
303
+ m_const = re.search(r"export\s+const\s+([a-zA-Z0-9_$]+)\s*=\s*(?:async\s*)?\((.*?)\)\s*=>", line)
304
+ if m_const:
305
+ functions.append({
306
+ "name": m_const.group(1),
307
+ "type": "arrow_function",
308
+ "line": line_no,
309
+ "signature": f"const {m_const.group(1)} = ({m_const.group(2)}) =>",
310
+ "description": "Exported arrow function"
311
+ })
312
+ continue
313
+
314
+ # export class ...
315
+ m_class = re.search(r"export\s+(?:default\s+)?class\s+([a-zA-Z0-9_$]+)", line)
316
+ if m_class:
317
+ functions.append({
318
+ "name": m_class.group(1),
319
+ "type": "class",
320
+ "line": line_no,
321
+ "signature": f"class {m_class.group(1)}",
322
+ "description": "Exported class"
323
+ })
324
+
325
+ return {"functions": functions, "filePath": rel_path}
326
+
327
+
328
+ def get_git_diff(
329
+ commits: int = 5,
330
+ file_path: Optional[str] = None,
331
+ project_path: str = "."
332
+ ) -> Dict[str, Any]:
333
+ """
334
+ Executes git log/diff safely to return recent changes and commits.
335
+ """
336
+ root = Path(project_path).resolve()
337
+ commits_list: List[Dict[str, str]] = []
338
+ diff_output = ""
339
+
340
+ try:
341
+ # Get commit log
342
+ cmd_log = ["git", "log", f"-n{max(1, commits)}", "--pretty=format:%H|%an|%ad|%s", "--date=short"]
343
+ if file_path:
344
+ cmd_log.extend(["--", file_path])
345
+
346
+ res_log = subprocess.run(cmd_log, cwd=root, capture_output=True, text=True, check=False)
347
+ if res_log.returncode == 0 and res_log.stdout.strip():
348
+ for entry in res_log.stdout.strip().split("\n"):
349
+ parts = entry.split("|", 3)
350
+ if len(parts) == 4:
351
+ commits_list.append({
352
+ "hash": parts[0][:8],
353
+ "author": parts[1],
354
+ "date": parts[2],
355
+ "message": parts[3]
356
+ })
357
+
358
+ # Get diff of recent changes or unstaged changes
359
+ cmd_diff = ["git", "diff", "HEAD~1"] if commits_list else ["git", "diff"]
360
+ if file_path:
361
+ cmd_diff.extend(["--", file_path])
362
+
363
+ res_diff = subprocess.run(cmd_diff, cwd=root, capture_output=True, text=True, check=False)
364
+ if res_diff.returncode == 0:
365
+ diff_output = res_diff.stdout[:8000]
366
+ else:
367
+ diff_output = "No active diff or repository not initialized."
368
+ except Exception as err:
369
+ diff_output = f"Git execution error: {str(err)}"
370
+
371
+ return {
372
+ "diff": diff_output,
373
+ "commits": commits_list
374
+ }
375
+
376
+
377
+ def explain_architecture(
378
+ depth: str = "high",
379
+ project_path: str = "."
380
+ ) -> Dict[str, Any]:
381
+ """
382
+ Generates a natural language architecture summary from graph and metadata.
383
+ Supported depths: 'high', 'medium', 'deep'.
384
+ """
385
+ root = Path(project_path).resolve()
386
+ scanned = scan_project(str(root))
387
+ files = scanned.get("files", [])
388
+ metadata = extract_metadata(str(root), files)
389
+ graph = build_dependency_graph(files, str(root))
390
+
391
+ stack = ", ".join(metadata.get("techStack", [])) or "General Codebase"
392
+ nodes = graph.get("nodes", [])
393
+ edges = graph.get("edges", [])
394
+
395
+ entry_points = graph.get("entryPoints") or [n.get("file") or n.get("id") for n in nodes if n.get("type") == "entry" or n.get("isEntry")]
396
+ util_nodes = graph.get("utilities") or [n.get("file") or n.get("id") for n in nodes if n.get("type") == "util" or "util" in (n.get("file") or n.get("id") or "").lower()]
397
+ entry_points = [e for e in entry_points if e]
398
+ util_nodes = [u for u in util_nodes if u]
399
+
400
+ diagram = ""
401
+
402
+ if depth == "high":
403
+ summary = (
404
+ f"This project is a {metadata.get('name', root.name)} codebase built primarily with {stack}. "
405
+ f"It comprises {len(files)} source files organized across {len(nodes)} modules with {len(edges)} "
406
+ f"inter-module dependency connections. Key entry points include: {', '.join(entry_points) or 'main modules'}."
407
+ )
408
+ elif depth == "medium":
409
+ summary = (
410
+ f"### Architecture Overview: {metadata.get('name', root.name)}\n\n"
411
+ f"- **Primary Stack**: {stack}\n"
412
+ f"- **Entry Points**: {', '.join(entry_points) if entry_points else 'Root entry files'}\n"
413
+ f"- **Core Modules**: {len(nodes)} modules mapped in the dependency graph\n"
414
+ f"- **Utilities & Helpers**: {len(util_nodes)} utility modules detected\n\n"
415
+ f"The application follows a modular structure where entry modules coordinate domain functionality "
416
+ f"supported by core abstractions and shared utility libraries."
417
+ )
418
+ else: # deep
419
+ # Generate Mermaid diagram snippet
420
+ mermaid_lines = ["graph TD"]
421
+ for edge in edges[:25]:
422
+ src_val = edge.get("from") or edge.get("source") or ""
423
+ tgt_val = edge.get("to") or edge.get("target") or ""
424
+ src = Path(src_val).name.replace(".", "_")
425
+ tgt = Path(tgt_val).name.replace(".", "_")
426
+ if src and tgt:
427
+ mermaid_lines.append(f" {src} --> {tgt}")
428
+
429
+ diagram = "\n".join(mermaid_lines)
430
+ summary = (
431
+ f"### In-Depth Architectural & Graph Analysis\n\n"
432
+ f"**Workspace**: `{root.as_posix()}`\n"
433
+ f"**Technologies**: {stack}\n"
434
+ f"**Component Graph**: {len(nodes)} nodes, {len(edges)} directed dependency edges\n\n"
435
+ f"#### Module Classification\n"
436
+ f"- **Entry Points**: {len(entry_points)} ({', '.join(entry_points[:5])})\n"
437
+ f"- **Utilities & Shared**: {len(util_nodes)} ({', '.join(util_nodes[:5])})\n"
438
+ f"- **Graph Density**: {(len(edges) / max(1, len(nodes))):.2f} average connections per node."
439
+ )
440
+
441
+ return {
442
+ "summary": summary,
443
+ "diagrams": diagram if diagram else None
444
+ }
445
+
446
+
447
+ def validate_security(
448
+ project_path: str = "."
449
+ ) -> Dict[str, Any]:
450
+ """
451
+ Runs the security validator across all project files and reports sensitive files or embedded secrets.
452
+ """
453
+ root = Path(project_path).resolve()
454
+ scanned = scan_project(str(root))
455
+ files = scanned.get("files", [])
456
+
457
+ issues: List[Dict[str, Any]] = []
458
+
459
+ for rel_file in files:
460
+ full_path = root / rel_file
461
+ reason = is_sensitive_path(rel_file, full_path)
462
+ if reason:
463
+ severity = "HIGH" if "key" in reason.lower() or "secret" in reason.lower() else "MEDIUM"
464
+ issues.append({
465
+ "file": rel_file,
466
+ "reason": reason,
467
+ "severity": severity
468
+ })
469
+
470
+ return {
471
+ "safe": len(issues) == 0,
472
+ "issues": issues,
473
+ "totalScanned": len(files),
474
+ "totalIssues": len(issues)
475
+ }
@@ -72,7 +72,7 @@ def load_gitignore(root: Path) -> pathspec.PathSpec:
72
72
  except Exception:
73
73
  pass
74
74
 
75
- return pathspec.PathSpec.from_lines("gitwildmatch", patterns)
75
+ return pathspec.PathSpec.from_lines("gitignore", patterns)
76
76
 
77
77
 
78
78
  def generate_tree(root: Path, spec: pathspec.PathSpec, max_depth: int = 3) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: context-gen-cli
3
- Version: 1.2.0
3
+ Version: 1.2.4
4
4
  Summary: A modern CLI tool that scans any project, builds an AST-based dependency graph, and generates structured LLM context (agent.md / agent.json).
5
5
  Author: yakkshit
6
6
  License-Expression: MIT
@@ -6,9 +6,12 @@ context_gen/extractor.py
6
6
  context_gen/generator.py
7
7
  context_gen/graph.py
8
8
  context_gen/scanner.py
9
+ context_gen/mcp/__init__.py
10
+ context_gen/mcp/advanced_tools.py
9
11
  context_gen_cli.egg-info/PKG-INFO
10
12
  context_gen_cli.egg-info/SOURCES.txt
11
13
  context_gen_cli.egg-info/dependency_links.txt
12
14
  context_gen_cli.egg-info/entry_points.txt
13
15
  context_gen_cli.egg-info/requires.txt
14
- context_gen_cli.egg-info/top_level.txt
16
+ context_gen_cli.egg-info/top_level.txt
17
+ tests/test_advanced_tools.py
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  context-gen = context_gen.cli:app
3
+ ctx = context_gen.cli:app
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "context-gen-cli"
7
- version = "1.2.0"
7
+ version = "1.2.4"
8
8
  description = "A modern CLI tool that scans any project, builds an AST-based dependency graph, and generates structured LLM context (agent.md / agent.json)."
9
9
  readme = "README.md"
10
10
  authors = [{ name = "yakkshit" }]
@@ -39,6 +39,7 @@ dev = [
39
39
 
40
40
  [project.scripts]
41
41
  context-gen = "context_gen.cli:app"
42
+ ctx = "context_gen.cli:app"
42
43
 
43
44
  [tool.setuptools.packages.find]
44
45
  where = ["."]
@@ -0,0 +1,74 @@
1
+ """
2
+ Unit tests for context_gen.mcp.advanced_tools
3
+ """
4
+
5
+ import os
6
+ from pathlib import Path
7
+ import pytest
8
+
9
+ from context_gen.mcp.advanced_tools import (
10
+ get_code_snippet,
11
+ search_code,
12
+ list_functions,
13
+ get_git_diff,
14
+ explain_architecture,
15
+ validate_security
16
+ )
17
+
18
+
19
+ def test_get_code_snippet_function():
20
+ res = get_code_snippet(
21
+ file_path="context_gen/scanner.py",
22
+ function_name="scan_project",
23
+ project_path=str(Path(__file__).parent.parent)
24
+ )
25
+ assert res["path"] == "context_gen/scanner.py"
26
+ assert "def scan_project" in res["content"]
27
+ assert res["language"] == "python"
28
+
29
+
30
+ def test_get_code_snippet_lines():
31
+ res = get_code_snippet(
32
+ file_path="context_gen/scanner.py",
33
+ start_line=1,
34
+ end_line=10,
35
+ project_path=str(Path(__file__).parent.parent)
36
+ )
37
+ assert res["startLine"] == 1
38
+ assert res["endLine"] == 10
39
+
40
+
41
+ def test_search_code():
42
+ res = search_code(
43
+ pattern="scan_project",
44
+ project_path=str(Path(__file__).parent.parent)
45
+ )
46
+ assert len(res["matches"]) > 0
47
+ assert any("scan_project" in m["line"] for m in res["matches"])
48
+
49
+
50
+ def test_list_functions():
51
+ res = list_functions(
52
+ file_path="context_gen/scanner.py",
53
+ project_path=str(Path(__file__).parent.parent)
54
+ )
55
+ func_names = [f["name"] for f in res["functions"]]
56
+ assert "scan_project" in func_names
57
+ assert "load_gitignore" in func_names
58
+
59
+
60
+ def test_explain_architecture():
61
+ res = explain_architecture(
62
+ depth="high",
63
+ project_path=str(Path(__file__).parent.parent)
64
+ )
65
+ assert "summary" in res
66
+ assert len(res["summary"]) > 0
67
+
68
+
69
+ def test_validate_security():
70
+ res = validate_security(
71
+ project_path=str(Path(__file__).parent.parent)
72
+ )
73
+ assert "safe" in res
74
+ assert "issues" in res