codeyak 0.0.8__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 (47) hide show
  1. codeyak/__init__.py +8 -0
  2. codeyak/__main__.py +27 -0
  3. codeyak/apps/__init__.py +3 -0
  4. codeyak/apps/cli/__init__.py +7 -0
  5. codeyak/apps/cli/configure.py +199 -0
  6. codeyak/apps/cli/main.py +362 -0
  7. codeyak/config.py +145 -0
  8. codeyak/domain/__init__.py +52 -0
  9. codeyak/domain/constants.py +41 -0
  10. codeyak/domain/exceptions.py +55 -0
  11. codeyak/domain/models.py +411 -0
  12. codeyak/infrastructure/__init__.py +17 -0
  13. codeyak/infrastructure/llm/azure.py +59 -0
  14. codeyak/infrastructure/vcs/diff_parser.py +110 -0
  15. codeyak/infrastructure/vcs/gitlab.py +342 -0
  16. codeyak/infrastructure/vcs/local_git.py +322 -0
  17. codeyak/prebuilt/__init__.py +0 -0
  18. codeyak/prebuilt/code-quality.yaml +32 -0
  19. codeyak/prebuilt/default.yaml +6 -0
  20. codeyak/prebuilt/security.yaml +51 -0
  21. codeyak/protocols/__init__.py +184 -0
  22. codeyak/py.typed +0 -0
  23. codeyak/services/__init__.py +23 -0
  24. codeyak/services/code.py +92 -0
  25. codeyak/services/context/__init__.py +26 -0
  26. codeyak/services/context/planner.py +192 -0
  27. codeyak/services/context/renderer.py +267 -0
  28. codeyak/services/context/skeleton.py +445 -0
  29. codeyak/services/context/symbol_index.py +820 -0
  30. codeyak/services/context_builder.py +293 -0
  31. codeyak/services/feedback/__init__.py +11 -0
  32. codeyak/services/feedback/console.py +86 -0
  33. codeyak/services/feedback/merge_request.py +90 -0
  34. codeyak/services/guidelines/__init__.py +30 -0
  35. codeyak/services/guidelines/generator.py +494 -0
  36. codeyak/services/guidelines/parser.py +461 -0
  37. codeyak/services/guidelines/provider.py +376 -0
  38. codeyak/services/reviewer.py +341 -0
  39. codeyak/services/summary.py +131 -0
  40. codeyak/ui/__init__.py +10 -0
  41. codeyak/ui/console.py +25 -0
  42. codeyak/ui/progress.py +268 -0
  43. codeyak-0.0.8.dist-info/METADATA +136 -0
  44. codeyak-0.0.8.dist-info/RECORD +47 -0
  45. codeyak-0.0.8.dist-info/WHEEL +4 -0
  46. codeyak-0.0.8.dist-info/entry_points.txt +2 -0
  47. codeyak-0.0.8.dist-info/licenses/LICENSE +21 -0
codeyak/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """CodeYak - AI-powered code review tool."""
2
+
3
+ from importlib.metadata import version, PackageNotFoundError
4
+
5
+ try:
6
+ __version__ = version("codeyak")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0.dev0"
codeyak/__main__.py ADDED
@@ -0,0 +1,27 @@
1
+ """
2
+ Entry point for running codeyak as a module.
3
+
4
+ Supports both the new CLI interface and backwards-compatible direct invocation:
5
+ python -m codeyak review # New: local review
6
+ python -m codeyak mr <MR_ID> [PROJECT_ID] # New: MR review
7
+ python -m codeyak <MR_ID> [PROJECT_ID] # Legacy: MR review
8
+ """
9
+
10
+ import sys
11
+
12
+
13
+ def main():
14
+ """Entry point that handles both CLI and legacy invocation."""
15
+ # Check if using legacy invocation (first arg is a number = MR_ID)
16
+ if len(sys.argv) > 1 and sys.argv[1].isdigit():
17
+ # Legacy mode: convert to new CLI format
18
+ # python -m codeyak <MR_ID> [PROJECT_ID] -> yak mr <MR_ID> [PROJECT_ID]
19
+ sys.argv = [sys.argv[0], "mr"] + sys.argv[1:]
20
+
21
+ # Import and run CLI
22
+ from .apps.cli import main as cli_main
23
+ cli_main()
24
+
25
+
26
+ if __name__ == "__main__":
27
+ main()
@@ -0,0 +1,3 @@
1
+ """
2
+ CodeYak applications.
3
+ """
@@ -0,0 +1,7 @@
1
+ """
2
+ CLI application for CodeYak.
3
+ """
4
+
5
+ from .main import main
6
+
7
+ __all__ = ["main"]
@@ -0,0 +1,199 @@
1
+ """
2
+ Interactive init flow for CodeYak configuration.
3
+
4
+ This module provides functions to interactively configure CodeYak settings
5
+ when users run commands without having configured the tool first.
6
+ """
7
+
8
+ import os
9
+ import tomllib
10
+ from pathlib import Path
11
+
12
+ import click
13
+ import tomli_w
14
+ from rich.panel import Panel
15
+
16
+ from ...config import get_config_path, reset_settings
17
+ from ...ui import console, BRAND_BORDER
18
+
19
+
20
+ def _show_key_feedback(key: str, label: str) -> None:
21
+ """Show feedback about entered key without revealing it."""
22
+ if not key:
23
+ console.print(f" [warning]Warning: No {label} was entered[/warning]")
24
+ elif len(key) < 10:
25
+ console.print(f" [success]{label} entered[/success] [muted]({len(key)} characters)[/muted]")
26
+ else:
27
+ # Show first 4 and last 4 chars for verification
28
+ masked = f"{key[:4]}...{key[-4:]}"
29
+ console.print(f" [success]{label} entered:[/success] {masked} [muted]({len(key)} characters)[/muted]")
30
+
31
+
32
+ def _load_existing_config() -> dict:
33
+ """Load existing config if it exists, otherwise return empty dict."""
34
+ config_path = get_config_path()
35
+ if config_path.exists():
36
+ with open(config_path, "rb") as f:
37
+ return tomllib.load(f)
38
+ return {}
39
+
40
+
41
+ def _save_config(config: dict) -> None:
42
+ """Save config to TOML file with restrictive permissions."""
43
+ config_path = get_config_path()
44
+
45
+ # Create parent directories if needed
46
+ config_path.parent.mkdir(parents=True, exist_ok=True)
47
+
48
+ # Write config with restrictive permissions (owner read/write only)
49
+ with open(config_path, "wb") as f:
50
+ tomli_w.dump(config, f)
51
+
52
+ # Set file permissions to 600 (owner read/write only)
53
+ os.chmod(config_path, 0o600)
54
+
55
+
56
+ def run_llm_init() -> None:
57
+ """Run interactive init flow for LLM (Azure OpenAI) configuration only."""
58
+ console.print()
59
+ console.print(Panel(
60
+ "[brand]LLM Provider[/brand]",
61
+ border_style=BRAND_BORDER,
62
+ padding=(0, 2)
63
+ ))
64
+ console.print("Available providers:")
65
+ console.print(" 1. Azure OpenAI")
66
+ console.print()
67
+
68
+ # Azure OpenAI Endpoint
69
+ console.print(" [muted]Example: https://your-resource.openai.azure.com/[/muted]")
70
+ endpoint = click.prompt(" Azure OpenAI Endpoint", type=str)
71
+
72
+ # API Key
73
+ console.print()
74
+ console.print(" [muted]Found in Azure Portal > Your OpenAI Resource > Keys and Endpoint[/muted]")
75
+ api_key = click.prompt(" Azure OpenAI API Key", type=str, hide_input=True)
76
+ _show_key_feedback(api_key, "API Key")
77
+
78
+ # Deployment Name
79
+ console.print()
80
+ deployment_name = click.prompt(
81
+ " Deployment Name", type=str, default="gpt-4o", show_default=True
82
+ )
83
+
84
+ # API Version
85
+ api_version = click.prompt(
86
+ " API Version", type=str, default="2024-02-15-preview", show_default=True
87
+ )
88
+
89
+ # Load existing config and update with new values
90
+ config = _load_existing_config()
91
+ config["AZURE_OPENAI_ENDPOINT"] = endpoint
92
+ config["AZURE_OPENAI_API_KEY"] = api_key
93
+ config["AZURE_DEPLOYMENT_NAME"] = deployment_name
94
+ config["AZURE_OPENAI_API_VERSION"] = api_version
95
+
96
+ _save_config(config)
97
+ reset_settings()
98
+
99
+ config_path = get_config_path()
100
+ console.print()
101
+ console.print(f"[success]Configuration saved to {config_path}[/success]")
102
+
103
+
104
+ def run_gitlab_init() -> None:
105
+ """Run interactive init flow for GitLab configuration only."""
106
+ console.print()
107
+ console.print(Panel(
108
+ "[brand]GitLab Configuration[/brand]",
109
+ border_style=BRAND_BORDER,
110
+ padding=(0, 2)
111
+ ))
112
+
113
+ # GitLab URL
114
+ gitlab_url = click.prompt(
115
+ " GitLab URL", type=str, default="https://gitlab.com", show_default=True
116
+ )
117
+
118
+ # GitLab Token
119
+ console.print()
120
+ console.print(" [muted]Personal Access Token (create at GitLab > Settings > Access Tokens)[/muted]")
121
+ gitlab_token = click.prompt(" GitLab Token", type=str, hide_input=True)
122
+ _show_key_feedback(gitlab_token, "Token")
123
+
124
+ # Load existing config and update with new values
125
+ config = _load_existing_config()
126
+ config["GITLAB_URL"] = gitlab_url
127
+ config["GITLAB_TOKEN"] = gitlab_token
128
+
129
+ _save_config(config)
130
+ reset_settings()
131
+
132
+ console.print()
133
+ console.print(f"[success]Configuration saved to {get_config_path()}[/success]")
134
+
135
+
136
+ def run_langfuse_init() -> None:
137
+ """Run interactive init flow for Langfuse configuration only."""
138
+ console.print()
139
+ console.print(Panel(
140
+ "[brand]Langfuse Configuration (Optional)[/brand]",
141
+ border_style=BRAND_BORDER,
142
+ padding=(0, 2)
143
+ ))
144
+ console.print(" [muted]Langfuse provides observability for your LLM calls.[/muted]")
145
+ console.print()
146
+
147
+ # Secret Key
148
+ secret_key = click.prompt(" Langfuse Secret Key", type=str, hide_input=True)
149
+ _show_key_feedback(secret_key, "Secret Key")
150
+
151
+ # Public Key
152
+ public_key = click.prompt(" Langfuse Public Key", type=str)
153
+
154
+ # Host
155
+ host = click.prompt(
156
+ " Langfuse Host",
157
+ type=str,
158
+ default="https://cloud.langfuse.com",
159
+ show_default=True,
160
+ )
161
+
162
+ # Load existing config and update with new values
163
+ config = _load_existing_config()
164
+ config["LANGFUSE_SECRET_KEY"] = secret_key
165
+ config["LANGFUSE_PUBLIC_KEY"] = public_key
166
+ config["LANGFUSE_HOST"] = host
167
+
168
+ _save_config(config)
169
+ reset_settings()
170
+
171
+ console.print()
172
+ console.print(f"[success]Configuration saved to {get_config_path()}[/success]")
173
+
174
+
175
+ def run_full_init(include_gitlab: bool = False) -> None:
176
+ """
177
+ Run the complete first-time setup flow.
178
+
179
+ Args:
180
+ include_gitlab: If True, also prompt for GitLab configuration.
181
+ """
182
+ console.print()
183
+ console.print("[info]Looks like you haven't configured CodeYak yet. Let's get you set up![/info]")
184
+
185
+ # Always configure LLM
186
+ run_llm_init()
187
+
188
+ # Optionally configure GitLab
189
+ if include_gitlab:
190
+ run_gitlab_init()
191
+
192
+ # Ask about Langfuse
193
+ console.print()
194
+ if click.confirm("Would you like to configure Langfuse for observability?", default=False):
195
+ run_langfuse_init()
196
+
197
+ console.print()
198
+ console.print("[info]Continuing with your command...[/info]")
199
+ console.print()
@@ -0,0 +1,362 @@
1
+ """
2
+ CLI for CodeYak - Local and MR code review.
3
+
4
+ Usage:
5
+ yak review # Review local uncommitted changes
6
+ yak mr <MR_ID> [PROJECT_ID] # Review GitLab MR
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import click
14
+
15
+ from ... import __version__
16
+ from ...config import (
17
+ get_settings,
18
+ is_gitlab_configured,
19
+ is_llm_configured,
20
+ is_langfuse_configured,
21
+ config_file_exists,
22
+ )
23
+ from .configure import run_full_init, run_gitlab_init, run_llm_init
24
+ from ...infrastructure import GitLabAdapter, LocalGitAdapter, AzureAdapter
25
+ from ...services import (
26
+ CodeReviewer,
27
+ GuidelinesProvider,
28
+ GuidelinesGenerator,
29
+ CodeProvider,
30
+ CodeReviewContextBuilder,
31
+ MergeRequestFeedbackPublisher,
32
+ ConsoleFeedbackPublisher,
33
+ SummaryGenerator,
34
+ )
35
+ from ...ui import console, RichProgressReporter, CIProgressReporter
36
+
37
+
38
+ def ensure_llm_configured() -> None:
39
+ """Ensure LLM (Azure OpenAI) is configured. Triggers init if not."""
40
+ if is_llm_configured():
41
+ return
42
+
43
+ # Check if this is a CI/CD environment (no TTY)
44
+ if not sys.stdin.isatty():
45
+ click.echo(
46
+ "Error: Azure OpenAI is not configured. "
47
+ "Set AZURE_OPENAI_API_KEY and AZURE_OPENAI_ENDPOINT environment variables.",
48
+ err=True,
49
+ )
50
+ sys.exit(1)
51
+
52
+ # First time setup - no config file exists
53
+ if not config_file_exists():
54
+ run_full_init(include_gitlab=False)
55
+ else:
56
+ # Config file exists but LLM not configured
57
+ console.print()
58
+ console.print("[info]LLM provider is not configured. Let's set it up![/info]")
59
+ run_llm_init()
60
+ console.print()
61
+ console.print("[info]Continuing with your command...[/info]")
62
+ console.print()
63
+
64
+
65
+ def ensure_gitlab_configured() -> None:
66
+ """Ensure GitLab is configured. Triggers init if not."""
67
+ # First ensure LLM is configured
68
+ ensure_llm_configured()
69
+
70
+ if is_gitlab_configured():
71
+ return
72
+
73
+ # Check if this is a CI/CD environment (no TTY)
74
+ if not sys.stdin.isatty():
75
+ click.echo(
76
+ "Error: GitLab is not configured. "
77
+ "Set GITLAB_TOKEN environment variable.",
78
+ err=True,
79
+ )
80
+ sys.exit(1)
81
+
82
+ # First time setup - no config file exists
83
+ if not config_file_exists():
84
+ run_full_init(include_gitlab=True)
85
+ else:
86
+ # Config file exists but GitLab not configured
87
+ console.print()
88
+ console.print("[info]GitLab integration is not configured. Let's set it up![/info]")
89
+ run_gitlab_init()
90
+ console.print()
91
+ console.print("[info]Continuing with your command...[/info]")
92
+ console.print()
93
+
94
+
95
+ @click.group()
96
+ @click.version_option(version=__version__, prog_name="yak")
97
+ def main():
98
+ """CodeYak - AI-powered code review tool."""
99
+ pass
100
+
101
+
102
+ @main.command()
103
+ @click.option(
104
+ "--path",
105
+ type=click.Path(exists=True, file_okay=False, path_type=Path),
106
+ default=None,
107
+ help="Path to git repository. Defaults to current directory."
108
+ )
109
+ def review(path: Path | None):
110
+ """Review local uncommitted changes."""
111
+ # Show banner first
112
+ progress = RichProgressReporter()
113
+ progress.banner("Codeyak", __version__)
114
+
115
+ # Ensure LLM is configured before proceeding
116
+ ensure_llm_configured()
117
+
118
+ repo_path = path or Path.cwd()
119
+
120
+ # Show observability status
121
+ obs_status = "ON" if is_langfuse_configured() else "OFF"
122
+ progress.info(f"Observability: {obs_status}")
123
+ progress.info(f"Reviewing uncommitted changes in {repo_path}...")
124
+
125
+ try:
126
+ # Initialize adapters
127
+ vcs = LocalGitAdapter(repo_path)
128
+ llm = AzureAdapter(
129
+ api_key=get_settings().AZURE_OPENAI_API_KEY,
130
+ endpoint=get_settings().AZURE_OPENAI_ENDPOINT,
131
+ deployment_name=get_settings().AZURE_DEPLOYMENT_NAME,
132
+ api_version=get_settings().AZURE_OPENAI_API_VERSION
133
+ )
134
+ except ValueError as e:
135
+ click.echo(f"Error: {e}", err=True)
136
+ sys.exit(1)
137
+ except Exception as e:
138
+ click.echo(f"Error initializing: {e}", err=True)
139
+ sys.exit(1)
140
+
141
+ langfuse_enabled = bool(
142
+ get_settings().LANGFUSE_SECRET_KEY and
143
+ get_settings().LANGFUSE_PUBLIC_KEY
144
+ )
145
+
146
+ langfuse = None
147
+ if langfuse_enabled:
148
+ from langfuse import Langfuse
149
+ langfuse = Langfuse(
150
+ secret_key=get_settings().LANGFUSE_SECRET_KEY,
151
+ public_key=get_settings().LANGFUSE_PUBLIC_KEY,
152
+ host=get_settings().LANGFUSE_HOST
153
+ )
154
+
155
+ # Initialize services - CodeProvider handles all MergeRequest construction
156
+ context = CodeReviewContextBuilder(
157
+ llm_client=llm,
158
+ repo_path=repo_path,
159
+ use_smart_context=True,
160
+ progress=progress,
161
+ )
162
+ guidelines = GuidelinesProvider(vcs)
163
+ code = CodeProvider(vcs)
164
+ feedback = ConsoleFeedbackPublisher()
165
+ summary = SummaryGenerator(llm, langfuse=langfuse)
166
+
167
+ bot = CodeReviewer(
168
+ context=context,
169
+ code=code,
170
+ guidelines=guidelines,
171
+ llm=llm,
172
+ feedback=feedback,
173
+ summary=summary,
174
+ langfuse=langfuse,
175
+ progress=progress,
176
+ )
177
+
178
+ bot.review_local_changes()
179
+
180
+ # Flush Langfuse traces
181
+ if langfuse:
182
+ langfuse.flush()
183
+
184
+
185
+ @main.command()
186
+ @click.argument("mr_id")
187
+ @click.argument("project_id", required=False)
188
+ def mr(mr_id: str, project_id: str | None):
189
+ """Review a GitLab merge request.
190
+
191
+ MR_ID is the merge request ID to review.
192
+ PROJECT_ID is optional (uses CI_PROJECT_ID env var if not provided).
193
+ """
194
+ # Ensure both LLM and GitLab are configured before proceeding
195
+ ensure_gitlab_configured()
196
+
197
+ # Get project ID from argument or environment
198
+ project_id = project_id or os.getenv("CI_PROJECT_ID")
199
+
200
+ if not project_id:
201
+ click.echo(
202
+ "Error: Project ID is required. "
203
+ "Pass it as the second argument or set CI_PROJECT_ID.",
204
+ err=True
205
+ )
206
+ sys.exit(1)
207
+
208
+ # Show observability status
209
+ obs_status = "[success]ON[/success]" if is_langfuse_configured() else "[muted]OFF[/muted]"
210
+ console.print(f"Observability: {obs_status}")
211
+ console.print(f"[info]Reviewing MR {mr_id} in project {project_id}...[/info]")
212
+
213
+ # Initialize adapters
214
+ try:
215
+ vcs = GitLabAdapter(
216
+ url=get_settings().GITLAB_URL,
217
+ token=get_settings().GITLAB_TOKEN,
218
+ project_id=project_id
219
+ )
220
+
221
+ llm = AzureAdapter(
222
+ api_key=get_settings().AZURE_OPENAI_API_KEY,
223
+ endpoint=get_settings().AZURE_OPENAI_ENDPOINT,
224
+ deployment_name=get_settings().AZURE_DEPLOYMENT_NAME,
225
+ api_version=get_settings().AZURE_OPENAI_API_VERSION
226
+ )
227
+ except Exception as e:
228
+ click.echo(f"Configuration Error: {e}", err=True)
229
+ sys.exit(1)
230
+
231
+ # Initialize Langfuse if configured
232
+ langfuse_enabled = bool(
233
+ get_settings().LANGFUSE_SECRET_KEY and
234
+ get_settings().LANGFUSE_PUBLIC_KEY
235
+ )
236
+
237
+ langfuse = None
238
+ if langfuse_enabled:
239
+ from langfuse import Langfuse
240
+ langfuse = Langfuse(
241
+ secret_key=get_settings().LANGFUSE_SECRET_KEY,
242
+ public_key=get_settings().LANGFUSE_PUBLIC_KEY,
243
+ host=get_settings().LANGFUSE_HOST
244
+ )
245
+
246
+ # Initialize services
247
+ # Use current directory as repo path - in CI, the repo is checked out
248
+ progress = CIProgressReporter()
249
+ context = CodeReviewContextBuilder(
250
+ llm_client=llm,
251
+ repo_path=Path.cwd(),
252
+ use_smart_context=True,
253
+ progress=progress,
254
+ )
255
+ guidelines = GuidelinesProvider(vcs)
256
+ code = CodeProvider(vcs)
257
+ feedback = MergeRequestFeedbackPublisher(vcs, mr_id)
258
+ summary = SummaryGenerator(llm, langfuse)
259
+
260
+ # Create reviewer and run
261
+ bot = CodeReviewer(
262
+ context=context,
263
+ guidelines=guidelines,
264
+ code=code,
265
+ feedback=feedback,
266
+ llm=llm,
267
+ summary=summary,
268
+ langfuse=langfuse,
269
+ progress=progress,
270
+ )
271
+
272
+ bot.review_merge_request(mr_id)
273
+
274
+ # Flush Langfuse traces
275
+ if langfuse:
276
+ langfuse.flush()
277
+
278
+ progress.success("Review complete.")
279
+
280
+
281
+ @main.command()
282
+ @click.option(
283
+ "--days",
284
+ type=int,
285
+ default=365,
286
+ help="Number of days of history to analyze (default: 365)"
287
+ )
288
+ def learn(days: int):
289
+ """Generate guidelines from git history analysis.
290
+
291
+ Analyzes commits to identify patterns of mistakes and problematic areas,
292
+ then generates codeyak guidelines to help avoid future issues.
293
+
294
+ Output is written to .codeyak/project.yaml
295
+ """
296
+ # Show banner first
297
+ progress = RichProgressReporter()
298
+ progress.banner("Codeyak", __version__)
299
+
300
+ # Ensure LLM is configured before proceeding
301
+ ensure_llm_configured()
302
+
303
+ repo_path = Path.cwd()
304
+
305
+ # Show observability status
306
+ obs_status = "ON" if is_langfuse_configured() else "OFF"
307
+ progress.info(f"Observability: {obs_status}")
308
+
309
+ # Verify we're in a git repository
310
+ try:
311
+ vcs = LocalGitAdapter(repo_path)
312
+ except ValueError as e:
313
+ click.echo(f"Error: {e}", err=True)
314
+ click.echo("The 'learn' command must be run inside a git repository.", err=True)
315
+ sys.exit(1)
316
+
317
+ progress.info(f"Analyzing git history for the last {days} days...")
318
+
319
+ # Initialize LLM adapter
320
+ try:
321
+ llm = AzureAdapter(
322
+ api_key=get_settings().AZURE_OPENAI_API_KEY,
323
+ endpoint=get_settings().AZURE_OPENAI_ENDPOINT,
324
+ deployment_name=get_settings().AZURE_DEPLOYMENT_NAME,
325
+ api_version=get_settings().AZURE_OPENAI_API_VERSION
326
+ )
327
+ except Exception as e:
328
+ click.echo(f"Error initializing LLM: {e}", err=True)
329
+ sys.exit(1)
330
+
331
+ # Initialize Langfuse if configured
332
+ langfuse = None
333
+ if get_settings().LANGFUSE_SECRET_KEY and get_settings().LANGFUSE_PUBLIC_KEY:
334
+ from langfuse import Langfuse
335
+ langfuse = Langfuse(
336
+ secret_key=get_settings().LANGFUSE_SECRET_KEY,
337
+ public_key=get_settings().LANGFUSE_PUBLIC_KEY,
338
+ host=get_settings().LANGFUSE_HOST
339
+ )
340
+
341
+ # Generate guidelines
342
+ generator = GuidelinesGenerator(vcs=vcs, llm=llm, langfuse=langfuse, progress=progress)
343
+ yaml_output = generator.generate_from_history(since_days=days)
344
+
345
+ # Create .codeyak/ directory if it doesn't exist
346
+ codeyak_dir = repo_path / ".codeyak"
347
+ codeyak_dir.mkdir(exist_ok=True)
348
+
349
+ # Write output to project.yaml
350
+ output_path = codeyak_dir / "project.yaml"
351
+ output_path.write_text(yaml_output)
352
+
353
+ progress.success(f"Guidelines written to {output_path}")
354
+ progress.info("Review and customize the generated guidelines before using them.")
355
+
356
+ # Flush Langfuse traces
357
+ if langfuse:
358
+ langfuse.flush()
359
+
360
+
361
+ if __name__ == "__main__":
362
+ main()