bob-dev 0.1.0__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.
bob_dev-0.1.0/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ PRIVATE LICENSE
2
+
3
+ Copyright (c) 2024. All rights reserved.
4
+
5
+ This project and all its contents, including but not limited to source code, documentation,
6
+ and other materials, are proprietary and confidential. Unauthorized copying, distribution,
7
+ modification, or use of this material is strictly prohibited without express written permission
8
+ from the copyright holder.
9
+
10
+ RESTRICTIONS:
11
+ - This software is provided for authorized use only
12
+ - Copying, modifying, or distributing the software is prohibited
13
+ - Reverse engineering or decompiling is strictly forbidden
14
+ - Commercial use is not permitted without explicit authorization
15
+ - No warranties or guarantees are provided
16
+
17
+ For permission or licensing inquiries, please contact the copyright holder.
bob_dev-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: bob-dev
3
+ Version: 0.1.0
4
+ Summary: An AI developer to write code for you
5
+ Author-email: Samuel Pereira dos Santos <samuelsantosdev@gmail.com>
6
+ Requires-Python: >=3.14
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=8.0; extra == "dev"
11
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
12
+ Dynamic: license-file
13
+
14
+ # BOB Dev
15
+
16
+ **BOB Dev** is an AI-powered developer workflow CLI that bridges a task requirements, your codebase, and Claude Code.
17
+
18
+ ![BOB-Dev Banner](https://github.com/samuelsantosdev/bob-dev/blob/main/assets/banner.png)
19
+
20
+ Given a Jira task ID it will:
21
+
22
+ 1. **Fetch** the task title, description, and fix versions from Jira.
23
+ 2. **Read** your project's Markdown documentation to build rich LLM context.
24
+ 3. **Generate** a precise Claude Code prompt (via GROK or OpenAI), including project-framework context, implementation steps, and test scenarios.
25
+ 4. **Analyse** the prompt for ambiguities and security concerns.
26
+ 5. **Execute** the prompt with the Claude Code CLI (optional – you can review first).
27
+
28
+ ---
29
+
30
+ ## Requirements
31
+
32
+ - Python 3.11+
33
+ - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and available on `$PATH` as `claude`
34
+ - A Jira Cloud account with an [API token](https://id.atlassian.com/manage-profile/security/api-tokens)
35
+ - An [xAI / GROK](https://console.x.ai/) **or** [OpenAI](https://platform.openai.com/) API key
36
+
37
+ ---
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install bob-dev
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Configuration
48
+
49
+ Run the interactive setup wizard the first time:
50
+
51
+ ```bash
52
+ bob-dev --configure
53
+ ```
54
+
55
+ This will prompt for your API keys, Jira credentials, and Claude Code API key.
56
+
57
+ ---
58
+
59
+ ## Usage
60
+
61
+ ```bash
62
+ bob-dev --task_id PROJ-123 --path /path/to/your/repo
63
+ ```
64
+
65
+ | Flag | Description |
66
+ |------|-------------|
67
+ | `--task_id` | Jira task ID to process (required for normal workflow) |
68
+ | `--path` | Path to the target repository (default: current directory) |
69
+ | `--agent` | LLM backend: `GROK` or `OPENAI` (default: value of `AGENT` in `.env`) |
70
+ | `--configure` | Run the interactive configuration wizard |
71
+
72
+ ---
73
+
74
+ ## Project structure
75
+
76
+ ```
77
+ src/bob_dev/
78
+ ├── cli.py # Entry point & main workflow orchestration
79
+ ├── helpers/
80
+ │ ├── terminal.py # ANSI colours, print helpers, spinner animation
81
+ │ ├── jira_helper.py # Jira API connection + ADF-to-text parsing
82
+ │ ├── llm_helper.py # LLM client, model selection, prompt generation
83
+ │ ├── project_helper.py # Markdown context collection + framework detection
84
+ │ └── config_helper.py # .env management + credential validation
85
+ └── constants/
86
+ └── frameworks.py # Known framework names used for auto-detection
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Colour conventions
92
+
93
+ | Colour | Meaning |
94
+ |--------|---------|
95
+ | Red | Errors that stop execution |
96
+ | Green | Success messages |
97
+ | Plain | Informational / default output |
98
+
99
+ ---
100
+
101
+ ## License
102
+
103
+ MIT
@@ -0,0 +1,90 @@
1
+ # BOB Dev
2
+
3
+ **BOB Dev** is an AI-powered developer workflow CLI that bridges a task requirements, your codebase, and Claude Code.
4
+
5
+ ![BOB-Dev Banner](https://github.com/samuelsantosdev/bob-dev/blob/main/assets/banner.png)
6
+
7
+ Given a Jira task ID it will:
8
+
9
+ 1. **Fetch** the task title, description, and fix versions from Jira.
10
+ 2. **Read** your project's Markdown documentation to build rich LLM context.
11
+ 3. **Generate** a precise Claude Code prompt (via GROK or OpenAI), including project-framework context, implementation steps, and test scenarios.
12
+ 4. **Analyse** the prompt for ambiguities and security concerns.
13
+ 5. **Execute** the prompt with the Claude Code CLI (optional – you can review first).
14
+
15
+ ---
16
+
17
+ ## Requirements
18
+
19
+ - Python 3.11+
20
+ - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and available on `$PATH` as `claude`
21
+ - A Jira Cloud account with an [API token](https://id.atlassian.com/manage-profile/security/api-tokens)
22
+ - An [xAI / GROK](https://console.x.ai/) **or** [OpenAI](https://platform.openai.com/) API key
23
+
24
+ ---
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install bob-dev
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Configuration
35
+
36
+ Run the interactive setup wizard the first time:
37
+
38
+ ```bash
39
+ bob-dev --configure
40
+ ```
41
+
42
+ This will prompt for your API keys, Jira credentials, and Claude Code API key.
43
+
44
+ ---
45
+
46
+ ## Usage
47
+
48
+ ```bash
49
+ bob-dev --task_id PROJ-123 --path /path/to/your/repo
50
+ ```
51
+
52
+ | Flag | Description |
53
+ |------|-------------|
54
+ | `--task_id` | Jira task ID to process (required for normal workflow) |
55
+ | `--path` | Path to the target repository (default: current directory) |
56
+ | `--agent` | LLM backend: `GROK` or `OPENAI` (default: value of `AGENT` in `.env`) |
57
+ | `--configure` | Run the interactive configuration wizard |
58
+
59
+ ---
60
+
61
+ ## Project structure
62
+
63
+ ```
64
+ src/bob_dev/
65
+ ├── cli.py # Entry point & main workflow orchestration
66
+ ├── helpers/
67
+ │ ├── terminal.py # ANSI colours, print helpers, spinner animation
68
+ │ ├── jira_helper.py # Jira API connection + ADF-to-text parsing
69
+ │ ├── llm_helper.py # LLM client, model selection, prompt generation
70
+ │ ├── project_helper.py # Markdown context collection + framework detection
71
+ │ └── config_helper.py # .env management + credential validation
72
+ └── constants/
73
+ └── frameworks.py # Known framework names used for auto-detection
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Colour conventions
79
+
80
+ | Colour | Meaning |
81
+ |--------|---------|
82
+ | Red | Errors that stop execution |
83
+ | Green | Success messages |
84
+ | Plain | Informational / default output |
85
+
86
+ ---
87
+
88
+ ## License
89
+
90
+ MIT
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "bob-dev"
7
+ version = "0.1.0"
8
+ description = "An AI developer to write code for you"
9
+ readme = "README.md"
10
+ authors = [{ name="Samuel Pereira dos Santos", email="samuelsantosdev@gmail.com" }]
11
+ requires-python = ">=3.14"
12
+ dependencies = []
13
+
14
+ [project.scripts]
15
+ bob-dev = "bob_dev.cli:main"
16
+
17
+ [project.optional-dependencies]
18
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]
19
+
20
+ [tool.pytest.ini_options]
21
+ testpaths = ["tests"]
22
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
File without changes
@@ -0,0 +1,293 @@
1
+ """cli.py
2
+
3
+ Entry point for the BOB Dev CLI tool.
4
+
5
+ Workflow
6
+ --------
7
+ 1. Fetch a Jira task by ID (title, description, fix versions).
8
+ 2. Read the project's Markdown documentation and build an LLM context string.
9
+ 3. Ask an LLM (GROK or OpenAI) to convert the acceptance criteria into a
10
+ Claude Code prompt, enriched with project context and framework info.
11
+ 4. Analyse the generated prompt for ambiguities and security concerns.
12
+ 5. Pass the final prompt to the Claude Code CLI for implementation.
13
+
14
+ Usage
15
+ -----
16
+ bob-dev --task_id PROJ-123 --path /path/to/repo
17
+ bob-dev --configure
18
+
19
+ Environment variables (.env next to this file or exported):
20
+ GROK_API_KEY – xAI / GROK secret key
21
+ OPENAI_API_KEY – OpenAI secret key
22
+ AGENT – "GROK" (default) or "OPENAI"
23
+ JIRA_URL – https://your-org.atlassian.net
24
+ JIRA_EMAIL – Atlassian account e-mail
25
+ JIRA_API_TOKEN – Atlassian API token
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import os
31
+ import sys
32
+ import argparse
33
+ import asyncio
34
+ from pathlib import Path
35
+
36
+ from InquirerPy import inquirer
37
+ from dotenv import load_dotenv
38
+
39
+ from .helpers.terminal import (
40
+ BOLD,
41
+ RESET,
42
+ print_error,
43
+ print_info,
44
+ print_step,
45
+ print_success,
46
+ print_warn,
47
+ run_subprocess,
48
+ run_with_spinner,
49
+ )
50
+ from .helpers.jira_helper import get_jira_task
51
+ from .helpers.llm_helper import analyse_prompt, llm_model, prompt_claude_code
52
+ from .helpers.project_helper import build_md_context, identify_framework
53
+ from .helpers.config_helper import check_configuration, update_env_file
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Module-level configuration
57
+ # ---------------------------------------------------------------------------
58
+
59
+ SCRIPT_DIR = Path(__file__).resolve().parent
60
+ load_dotenv(SCRIPT_DIR / ".env")
61
+
62
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
63
+ GROK_API_KEY = os.environ.get("GROK_API_KEY", "")
64
+ AGENT = os.environ.get("AGENT", "GROK").upper() # "GROK" or "OPENAI"
65
+
66
+ JIRA_URL = os.environ.get("JIRA_URL", "")
67
+ JIRA_EMAIL = os.environ.get("JIRA_EMAIL", "")
68
+ JIRA_API_TOKEN = os.environ.get("JIRA_API_TOKEN", "")
69
+
70
+ REPO_BASE_PATH = Path("./") # Overridden by --path at runtime.
71
+ CLAUDE_CODE_CMD = "claude" # Must be on $PATH.
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Main entry point
76
+ # ---------------------------------------------------------------------------
77
+
78
+ def main() -> None:
79
+ """Parse CLI arguments and orchestrate the four-step workflow."""
80
+
81
+ parser = argparse.ArgumentParser(
82
+ prog="bob-dev",
83
+ description="BOB Dev – AI-assisted developer workflow tool.",
84
+ )
85
+ parser.add_argument(
86
+ "--task_id", type=str,
87
+ help="Jira task ID to process (e.g. PROJ-123).",
88
+ )
89
+ parser.add_argument(
90
+ "--path", type=str, default="./",
91
+ help="Path to the target project repository (default: current directory).",
92
+ )
93
+ parser.add_argument(
94
+ "--agent", type=str, choices=["GROK", "OPENAI"], default=AGENT,
95
+ help="LLM backend to use (default: GROK).",
96
+ )
97
+ parser.add_argument(
98
+ "--configure", action="store_true",
99
+ help="Run interactive setup to save API keys to .env.",
100
+ )
101
+ args = parser.parse_args()
102
+
103
+ # ── Interactive configuration wizard ────────────────────────────────────
104
+ if args.configure:
105
+ _run_configure()
106
+ sys.exit(0)
107
+
108
+ # ── Require task_id for normal workflow ──────────────────────────────────
109
+ if not args.task_id:
110
+ print_error("Jira task ID is required. Use --task_id PROJ-123.")
111
+ sys.exit(1)
112
+
113
+ task_id = args.task_id.strip().upper()
114
+ agent = args.agent.upper()
115
+
116
+ # ── Validate credentials before making any API calls ────────────────────
117
+ if not all([JIRA_URL, JIRA_EMAIL, JIRA_API_TOKEN]):
118
+ print_error("Jira credentials are not configured. Run `bob-dev --configure`.")
119
+ sys.exit(1)
120
+
121
+ if agent == "GROK" and not GROK_API_KEY:
122
+ print_error("GROK_API_KEY is not set. Run `bob-dev --configure`.")
123
+ sys.exit(1)
124
+
125
+ if agent == "OPENAI" and not OPENAI_API_KEY:
126
+ print_error("OPENAI_API_KEY is not set. Run `bob-dev --configure`.")
127
+ sys.exit(1)
128
+
129
+ # ── Resolve and validate the repository path ─────────────────────────────
130
+ global REPO_BASE_PATH
131
+ REPO_BASE_PATH = Path(args.path).resolve()
132
+
133
+ if not REPO_BASE_PATH.exists() or not REPO_BASE_PATH.is_dir():
134
+ print_error(f"Invalid project path: {REPO_BASE_PATH}")
135
+ sys.exit(1)
136
+
137
+ print_info(f"Project path : {REPO_BASE_PATH}")
138
+ print_info(f"Jira task ID : {task_id}")
139
+ print_info(f"LLM backend : {agent} ({llm_model(agent)})")
140
+ print()
141
+
142
+ # ── Step 1 – Fetch Jira task ─────────────────────────────────────────────
143
+ print_step("[1/4]", f"Fetching Jira task {task_id} …")
144
+
145
+ task = asyncio.run(run_with_spinner(
146
+ get_jira_task,
147
+ task_id, JIRA_URL, JIRA_EMAIL, JIRA_API_TOKEN,
148
+ label="Fetching Jira task",
149
+ ))
150
+
151
+ print_success(f"Title : {task['title']}")
152
+ print_success(f"Fix versions : {', '.join(task['fix_versions']) or 'N/A'}")
153
+ print()
154
+
155
+ acceptance_criteria = task["description"]
156
+ if not acceptance_criteria.strip():
157
+ print_error("The Jira task has no description / acceptance criteria.")
158
+ sys.exit(1)
159
+
160
+ # ── Step 2 – Read project docs & generate prompt ─────────────────────────
161
+ print_step("[2/4]", f"Generating Claude Code prompt via {agent} ({llm_model(agent)}) …")
162
+
163
+ # Collect Markdown context (blocking I/O) inside the spinner thread.
164
+ md_context = asyncio.run(run_with_spinner(
165
+ build_md_context, REPO_BASE_PATH,
166
+ label="Reading project docs",
167
+ ))
168
+
169
+ # Framework detection is fast – no spinner needed.
170
+ try:
171
+ framework = identify_framework(md_context)
172
+ print_success(f"Detected framework : {framework}")
173
+ except ValueError as exc:
174
+ print_warn(str(exc))
175
+ framework = "the project"
176
+
177
+ prompt_md = asyncio.run(run_with_spinner(
178
+ prompt_claude_code,
179
+ acceptance_criteria, md_context, framework,
180
+ agent, GROK_API_KEY, OPENAI_API_KEY,
181
+ label="Generating prompt",
182
+ task_meta=task,
183
+ ))
184
+
185
+ if not prompt_md.strip():
186
+ print_error("Failed to generate a prompt for Claude Code.")
187
+ sys.exit(1)
188
+
189
+ print_success("Prompt generated.")
190
+ print()
191
+
192
+ # ── Step 3 – Analyse the prompt ──────────────────────────────────────────
193
+ print_step("[3/4]", "Analysing the prompt for issues …")
194
+
195
+ analysis = asyncio.run(run_with_spinner(
196
+ analyse_prompt,
197
+ prompt_md, acceptance_criteria,
198
+ agent, GROK_API_KEY, OPENAI_API_KEY,
199
+ label="Analysing prompt",
200
+ ))
201
+
202
+ print(f"\n{BOLD}── Prompt Analysis {'─' * 50}{RESET}")
203
+ print(analysis)
204
+ print("─" * 68 + "\n")
205
+
206
+ # ── Confirm before handing off to Claude Code ────────────────────────────
207
+ answer = input("Proceed and send prompt to Claude Code? [y/N] ").strip().lower()
208
+ if answer != "y":
209
+ prompt_file = SCRIPT_DIR / f"claude_prompt-{task_id}.md"
210
+ prompt_file.write_text(prompt_md, encoding="utf-8")
211
+ print_info(f"Aborted. Prompt saved to {prompt_file}")
212
+ sys.exit(0)
213
+
214
+ # ── Step 4 – Pass prompt to Claude Code ──────────────────────────────────
215
+ print_step("[4/4]", "Passing prompt to Claude Code …")
216
+ print()
217
+ asyncio.run(_pass_to_claude_code(prompt_md, task_id))
218
+
219
+
220
+ # ---------------------------------------------------------------------------
221
+ # Internal helpers
222
+ # ---------------------------------------------------------------------------
223
+
224
+ async def _pass_to_claude_code(prompt_md: str, task_id: str) -> None:
225
+ """Write the prompt to a temp file, then run Claude Code non-interactively."""
226
+
227
+ # Persist the prompt so the user can review it regardless of outcome.
228
+ prompt_file = SCRIPT_DIR / "tmp" / f"claude_prompt_{task_id}.md"
229
+ prompt_file.parent.mkdir(parents=True, exist_ok=True)
230
+ prompt_file.write_text(prompt_md, encoding="utf-8")
231
+ print_info(f"Prompt saved to: {prompt_file}")
232
+
233
+ cmd = [CLAUDE_CODE_CMD, "--dangerously-skip-permissions", "--print", prompt_md]
234
+ print_info(f"Running: {' '.join(cmd[:2])} <prompt>")
235
+ print()
236
+
237
+ returncode, _, _ = await run_subprocess(cmd, REPO_BASE_PATH)
238
+
239
+ if returncode != 0:
240
+ print_warn(f"Claude Code exited with code {returncode}")
241
+
242
+
243
+ def _run_configure() -> None:
244
+ """Interactive wizard to write API keys and Jira credentials to .env."""
245
+ env_path = SCRIPT_DIR / ".env"
246
+ print_step("[CONFIGURE]", "Running initial configuration …")
247
+
248
+ # Choose the LLM backend.
249
+ system_choice = inquirer.select(
250
+ message="Select the default LLM backend:",
251
+ choices=["GROK", "OPENAI"],
252
+ ).execute()
253
+
254
+ # Collect the appropriate API key.
255
+ print(f"Enter your {system_choice} API key:")
256
+ api_key = input("> ").strip()
257
+ env_key = "GROK_API_KEY" if system_choice == "GROK" else "OPENAI_API_KEY"
258
+ update_env_file(env_key, api_key, env_path)
259
+ print_success(f"{system_choice} API key saved.")
260
+
261
+ # Jira credentials.
262
+ print("\nJira configuration:")
263
+ jira_url = input("JIRA_URL (e.g. https://your-org.atlassian.net): ").strip()
264
+ jira_email = input("JIRA_EMAIL (your Atlassian account e-mail) : ").strip()
265
+ jira_token = input("JIRA_API_TOKEN (Atlassian API token) : ").strip()
266
+
267
+ for key, val in [
268
+ ("JIRA_URL", jira_url),
269
+ ("JIRA_EMAIL", jira_email),
270
+ ("JIRA_API_TOKEN", jira_token),
271
+ ]:
272
+ update_env_file(key, val, env_path)
273
+
274
+ print_success("Jira configuration saved.")
275
+
276
+ # Claude Code API key.
277
+ print("\nClaude Code API key:")
278
+ claude_key = input("CLAUDE_API_KEY: ").strip()
279
+ update_env_file("CLAUDE_API_KEY", claude_key, env_path)
280
+ print_success("Claude Code configuration saved.")
281
+
282
+ # Optional: verify all keys immediately.
283
+ answer = input("\nVerify all keys now? [y/N] ").strip().lower()
284
+ if answer == "y":
285
+ check_configuration(
286
+ agent = os.environ.get("AGENT", "GROK").upper(),
287
+ grok_api_key = os.environ.get("GROK_API_KEY", ""),
288
+ openai_api_key = os.environ.get("OPENAI_API_KEY", ""),
289
+ jira_url = jira_url,
290
+ jira_email = jira_email,
291
+ jira_api_token = jira_token,
292
+ claude_cmd = CLAUDE_CODE_CMD,
293
+ )
File without changes
@@ -0,0 +1,28 @@
1
+ AVAILABLE_FRAMEWORKS = [
2
+ "Django REST Framework",
3
+ "Flask",
4
+ "FastAPI",
5
+ "Express.js",
6
+ "Spring Boot",
7
+ "Ruby on Rails",
8
+ "Laravel",
9
+ "ASP.NET Core",
10
+ "Symfony",
11
+ "CakePHP",
12
+ "CodeIgniter",
13
+ "Angular",
14
+ "React",
15
+ "Vue.js",
16
+ "Svelte",
17
+ "Next.js",
18
+ "Nuxt.js",
19
+ "Flutter",
20
+ "React Native",
21
+ "Ionic",
22
+ "Xamarin",
23
+ "Electron",
24
+ "NestJS",
25
+ "AdonisJS",
26
+ "Hadoop",
27
+ "Spark",
28
+ ]
@@ -0,0 +1 @@
1
+ # helpers package – utility modules for bob_dev
@@ -0,0 +1,107 @@
1
+ """config_helper.py
2
+
3
+ Configuration utilities for bob_dev:
4
+ - Write / update key-value pairs in the .env file.
5
+ - Validate all external service credentials and print coloured results.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import subprocess
12
+ from pathlib import Path
13
+
14
+ from dotenv import load_dotenv
15
+
16
+ from .terminal import print_error, print_success, print_info
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # .env management
21
+ # ---------------------------------------------------------------------------
22
+
23
+ def update_env_file(key: str, value: str, env_path: Path) -> None:
24
+ """Write or update *key=value* in *env_path* and apply it to os.environ.
25
+
26
+ If the key already exists in the file it is updated in place; otherwise
27
+ a new line is appended. The updated file is also re-loaded via dotenv.
28
+ """
29
+ lines: list[str] = []
30
+ if env_path.exists():
31
+ lines = env_path.read_text(encoding="utf-8").splitlines()
32
+
33
+ updated = False
34
+ for i, line in enumerate(lines):
35
+ if line.startswith(f"{key}="):
36
+ lines[i] = f"{key}={value}"
37
+ updated = True
38
+ break
39
+
40
+ if not updated:
41
+ lines.append(f"{key}={value}")
42
+
43
+ env_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
44
+ os.environ[key] = value
45
+ load_dotenv(env_path)
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Configuration verification
50
+ # ---------------------------------------------------------------------------
51
+
52
+ def check_configuration(
53
+ agent: str,
54
+ grok_api_key: str,
55
+ openai_api_key: str,
56
+ jira_url: str,
57
+ jira_email: str,
58
+ jira_api_token: str,
59
+ claude_cmd: str,
60
+ ) -> None:
61
+ """Validate all API keys and the Claude Code CLI, printing coloured results.
62
+
63
+ Each check is independent: a failure in one does not stop the others.
64
+ """
65
+ # ── LLM API key ───────────────────────────────────────────────────────
66
+ print_info(f"Checking {agent} API key …")
67
+ try:
68
+ from .llm_helper import build_llm_client, llm_model
69
+
70
+ client = build_llm_client(agent, grok_api_key, openai_api_key)
71
+ model = llm_model(agent)
72
+ response = client.chat.completions.create(
73
+ model=model,
74
+ messages=[{"role": "system", "content": "Say: API key OK"}],
75
+ temperature=0,
76
+ )
77
+ reply = response.choices[0].message.content or ""
78
+ print_success(f"{agent} API key working. Response: {reply}")
79
+ except Exception as exc:
80
+ print_error(f"Failed to connect to {agent} API: {exc}")
81
+
82
+ # ── Jira credentials ──────────────────────────────────────────────────
83
+ print_info("Checking Jira credentials …")
84
+ try:
85
+ from atlassian import Jira
86
+
87
+ jira = Jira(url=jira_url, username=jira_email, password=jira_api_token, cloud=True)
88
+ user = jira.myself()
89
+ print_success(f"Jira credentials OK. Authenticated as: {user.get('displayName')}")
90
+ except Exception as exc:
91
+ print_error(f"Failed to connect to Jira: {exc}")
92
+
93
+ # ── Claude Code CLI ───────────────────────────────────────────────────
94
+ print_info(f"Checking Claude Code CLI ({claude_cmd}) …")
95
+ try:
96
+ result = subprocess.run(
97
+ [claude_cmd, "--version"],
98
+ capture_output=True,
99
+ text=True,
100
+ timeout=10,
101
+ )
102
+ if result.returncode == 0:
103
+ print_success(f"Claude Code CLI OK. Version: {result.stdout.strip()}")
104
+ else:
105
+ print_error(f"Claude Code CLI error: {result.stderr.strip()}")
106
+ except Exception as exc:
107
+ print_error(f"Failed to run Claude Code CLI: {exc}")