bob-dev 0.2.3__tar.gz → 0.3.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.
Files changed (31) hide show
  1. {bob_dev-0.2.3/src/bob_dev.egg-info → bob_dev-0.3.0}/PKG-INFO +3 -3
  2. {bob_dev-0.2.3 → bob_dev-0.3.0}/README.md +1 -1
  3. {bob_dev-0.2.3 → bob_dev-0.3.0}/pyproject.toml +2 -2
  4. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev/cli.py +74 -56
  5. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev/services/llm.py +95 -24
  6. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev/services/project.py +8 -3
  7. bob_dev-0.3.0/src/bob_dev/settings.py +45 -0
  8. {bob_dev-0.2.3 → bob_dev-0.3.0/src/bob_dev.egg-info}/PKG-INFO +3 -3
  9. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev.egg-info/SOURCES.txt +1 -0
  10. {bob_dev-0.2.3 → bob_dev-0.3.0}/tests/test_cli.py +1 -2
  11. {bob_dev-0.2.3 → bob_dev-0.3.0}/LICENSE +0 -0
  12. {bob_dev-0.2.3 → bob_dev-0.3.0}/setup.cfg +0 -0
  13. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/__init__.py +0 -0
  14. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev/__init__.py +0 -0
  15. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev/constants/__init__.py +0 -0
  16. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev/constants/frameworks.py +0 -0
  17. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev/services/__init__.py +0 -0
  18. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev/services/claude.py +0 -0
  19. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev/services/config.py +0 -0
  20. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev/services/gitlab.py +0 -0
  21. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev/services/jira.py +0 -0
  22. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev/services/terminal.py +0 -0
  23. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev.egg-info/dependency_links.txt +0 -0
  24. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev.egg-info/entry_points.txt +0 -0
  25. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev.egg-info/requires.txt +0 -0
  26. {bob_dev-0.2.3 → bob_dev-0.3.0}/src/bob_dev.egg-info/top_level.txt +0 -0
  27. {bob_dev-0.2.3 → bob_dev-0.3.0}/tests/test_config.py +0 -0
  28. {bob_dev-0.2.3 → bob_dev-0.3.0}/tests/test_jira.py +0 -0
  29. {bob_dev-0.2.3 → bob_dev-0.3.0}/tests/test_llm.py +0 -0
  30. {bob_dev-0.2.3 → bob_dev-0.3.0}/tests/test_project.py +0 -0
  31. {bob_dev-0.2.3 → bob_dev-0.3.0}/tests/test_terminal.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bob-dev
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: An AI developer to write code for you
5
5
  Author-email: Samuel Pereira dos Santos <samuelsantosdev@gmail.com>
6
- Requires-Python: >=3.14
6
+ Requires-Python: >=3.12.10
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Provides-Extra: dev
@@ -30,7 +30,7 @@ Given a task ID (Jira or GitLab) it will:
30
30
 
31
31
  ## Requirements
32
32
 
33
- - Python 3.14+
33
+ - Python 3.12+
34
34
  - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and available on `$PATH` as `claude`
35
35
  - A **Jira Cloud** account with an [API token](https://id.atlassian.com/manage-profile/security/api-tokens) **or** a **GitLab** account with a [personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html)
36
36
  - An [xAI / GROK](https://console.x.ai/) **or** [OpenAI](https://platform.openai.com/) API key
@@ -17,7 +17,7 @@ Given a task ID (Jira or GitLab) it will:
17
17
 
18
18
  ## Requirements
19
19
 
20
- - Python 3.14+
20
+ - Python 3.12+
21
21
  - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and available on `$PATH` as `claude`
22
22
  - A **Jira Cloud** account with an [API token](https://id.atlassian.com/manage-profile/security/api-tokens) **or** a **GitLab** account with a [personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html)
23
23
  - An [xAI / GROK](https://console.x.ai/) **or** [OpenAI](https://platform.openai.com/) API key
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "bob-dev"
7
- version = "0.2.3"
7
+ version = "0.3.0"
8
8
  description = "An AI developer to write code for you"
9
9
  readme = "README.md"
10
10
  authors = [{ name="Samuel Pereira dos Santos", email="samuelsantosdev@gmail.com" }]
11
- requires-python = ">=3.14"
11
+ requires-python = ">=3.12.10"
12
12
  dependencies = []
13
13
 
14
14
  [project.scripts]
@@ -33,7 +33,7 @@ import sys
33
33
  import argparse
34
34
  import asyncio
35
35
  from pathlib import Path
36
- from time import sleep, time
36
+ from time import sleep
37
37
 
38
38
  from InquirerPy import inquirer
39
39
  from dotenv import load_dotenv
@@ -51,8 +51,7 @@ from .services.terminal import (
51
51
  )
52
52
  from .services.jira import get_jira_task
53
53
  from .services.gitlab import get_gitlab_task
54
- from .services.claude import read_agents_from_claude
55
- from .services.llm import analyse_prompt, llm_model, prompt_claude_code
54
+ from .services.llm import analyse_prompt, llm_model, prompt_claude_code, review_prompt
56
55
  from .services.project import build_md_context, identify_framework
57
56
  from .services.config import check_configuration, update_env_file
58
57
 
@@ -64,8 +63,6 @@ SCRIPT_DIR = Path(__file__).resolve().parent
64
63
  ENV_PATH = Path.home() / ".bob_dev" / ".env"
65
64
  load_dotenv(ENV_PATH) # Load .env if it exists, but don't require it
66
65
 
67
- OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
68
- GROK_API_KEY = os.environ.get("GROK_API_KEY", "")
69
66
  AGENT = os.environ.get("AGENT", "GROK").upper() # "GROK" or "OPENAI"
70
67
  TASK_MANAGER = os.environ.get("TASK_MANAGER", "JIRA").upper() # "JIRA" or "GITLAB"
71
68
 
@@ -73,12 +70,18 @@ JIRA_URL = os.environ.get("JIRA_URL", "")
73
70
  JIRA_EMAIL = os.environ.get("JIRA_EMAIL", "")
74
71
  JIRA_API_TOKEN = os.environ.get("JIRA_API_TOKEN", "")
75
72
 
73
+ GROK_API_KEY = os.environ.get("GROK_API_KEY", "")
74
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
75
+
76
76
  GITLAB_URL = os.environ.get("GITLAB_URL", "")
77
77
  GITLAB_API_TOKEN = os.environ.get("GITLAB_API_TOKEN", "")
78
78
 
79
79
  REPO_BASE_PATH = Path("./") # Overridden by --path at runtime.
80
80
  CLAUDE_CODE_CMD = "claude" # Must be on $PATH.
81
81
 
82
+ MAX_FILES = int(os.environ.get("MAX_FILES", 30))
83
+ MAX_CHARS = int(os.environ.get("MAX_CHARS", 40_000))
84
+ MAX_SUMMARY_WORDS = int(os.environ.get("MAX_SUMMARY_WORDS", 2000))
82
85
 
83
86
  # ---------------------------------------------------------------------------
84
87
  # Main entry point
@@ -132,11 +135,11 @@ def main() -> None:
132
135
  sys.exit(1)
133
136
 
134
137
  if agent == "GROK" and not GROK_API_KEY:
135
- print_error("GROK_API_KEY is not set. Run `bob-dev --configure`.")
138
+ print_error("GROK API key is not configured. Run `bob-dev --configure`.")
136
139
  sys.exit(1)
137
140
 
138
141
  if agent == "OPENAI" and not OPENAI_API_KEY:
139
- print_error("OPENAI_API_KEY is not set. Run `bob-dev --configure`.")
142
+ print_error("OpenAI API key is not configured. Run `bob-dev --configure`.")
140
143
  sys.exit(1)
141
144
 
142
145
  # ── Resolve and validate the repository path ─────────────────────────────
@@ -147,9 +150,9 @@ def main() -> None:
147
150
  print_error(f"Invalid project path: {REPO_BASE_PATH}")
148
151
  sys.exit(1)
149
152
 
150
- print_info(f"Project path : {REPO_BASE_PATH}")
151
- print_info(f"{TASK_MANAGER} task ID : {task_id}")
152
- print_info(f"LLM backend : {agent} ({llm_model(agent)})")
153
+ print_success(f"Project path : {REPO_BASE_PATH}")
154
+ print_success(f"{TASK_MANAGER} task ID : {task_id}")
155
+ print_success(f"LLM backend : {agent} ({llm_model(agent)})")
153
156
  print()
154
157
 
155
158
  # ── Step 1 – Fetch task ─────────────────────────────────────────────
@@ -184,7 +187,7 @@ def main() -> None:
184
187
 
185
188
  # Collect Markdown context (blocking I/O) inside the spinner thread.
186
189
  md_context = asyncio.run(run_with_spinner(
187
- build_md_context, REPO_BASE_PATH,
190
+ build_md_context, REPO_BASE_PATH, MAX_SUMMARY_WORDS,
188
191
  label="Reading project docs",
189
192
  ))
190
193
 
@@ -196,58 +199,73 @@ def main() -> None:
196
199
  print_warn(str(exc))
197
200
  framework = "the project"
198
201
 
199
- prompt_md = asyncio.run(run_with_spinner(
200
- prompt_claude_code,
201
- acceptance_criteria, md_context, framework,
202
- agent, GROK_API_KEY, OPENAI_API_KEY,
203
- label="Generating prompt",
204
- task_meta=task,
205
- ))
206
-
207
- if not prompt_md.strip():
208
- print_error("Failed to generate a prompt for Claude Code.")
209
- sys.exit(1)
202
+ execute_prompt = False
203
+ while execute_prompt is False:
204
+ prompt_md = asyncio.run(run_with_spinner(
205
+ prompt_claude_code,
206
+ acceptance_criteria, md_context, framework,
207
+ agent, GROK_API_KEY, OPENAI_API_KEY,
208
+ label="Generating prompt",
209
+ task_meta=task,
210
+ ))
210
211
 
211
- print_success("Prompt generated.")
212
- print()
212
+ if not prompt_md.strip():
213
+ print_error("Failed to generate a prompt for Claude Code.")
214
+ sys.exit(1)
213
215
 
214
- # ── Step 3 – Analyse the prompt ──────────────────────────────────────────
215
- print_step("[3/4]", "Analysing the prompt for issues …")
216
+ print_success("Prompt generated.")
217
+ print()
216
218
 
217
- analysis = asyncio.run(run_with_spinner(
218
- analyse_prompt,
219
- prompt_md, acceptance_criteria,
220
- agent, GROK_API_KEY, OPENAI_API_KEY,
221
- label="Analysing prompt",
222
- ))
219
+ # ── Step 3 – Analyse the prompt ──────────────────────────────────────────
220
+ print_step("[3/4]", "Analysing the prompt for issues …")
223
221
 
224
- print(f"\n{BOLD}── Prompt Analysis {'─' * 50}{RESET}")
225
- for line in analysis.splitlines():
226
- print(line)
227
- sleep(0.02) # Simulate a "typing" effect for better readability
228
- print("─" * 68 + "\n")
229
-
230
- # ── Confirm before handing off to Claude Code ────────────────────────────
231
- answer = input("Proceed to prompt preview? [y/N] ").strip().lower()
232
- if answer != "y":
233
- prompt_file = SCRIPT_DIR / f"claude_prompt-{task_id}.md"
234
- prompt_file.write_text(prompt_md, encoding="utf-8")
235
- print_info(f"Aborted. Prompt saved to {prompt_file}")
236
- sys.exit(0)
222
+ analysis = asyncio.run(run_with_spinner(
223
+ analyse_prompt,
224
+ prompt_md, acceptance_criteria,
225
+ agent, GROK_API_KEY, OPENAI_API_KEY,
226
+ label="Analysing prompt",
227
+ ))
237
228
 
238
- print_info("\n\n\nPrompt preview:")
239
- print("-" * 68)
240
- for line in prompt_md.splitlines():
241
- print(line)
242
- sleep(0.02) # Simulate a "typing" effect for better readability
243
- print("-" * 68)
244
- answer = input("\nAre you sure? This will run the Claude Code CLI with the generated prompt. [y/N] ").strip().lower()
245
- if answer != "y":
246
- print_info("Aborted by user.")
247
- sys.exit(0)
229
+ print(f"\n{BOLD}── Prompt Analysis {'─' * 50}{RESET}")
230
+ for line in analysis.splitlines():
231
+ print(line)
232
+ sleep(0.02) # Simulate a "typing" effect for better readability
233
+ print("" * 68 + "\n")
234
+
235
+ # ── Confirm before handing off to Claude Code ────────────────────────────
236
+ answer = input("Proceed to prompt preview? [y/N/c] ").strip().lower()
237
+ if answer == "n":
238
+ prompt_file = SCRIPT_DIR / f"claude_prompt-{task_id}.md"
239
+ prompt_file.write_text(prompt_md, encoding="utf-8")
240
+ print_info(f"Aborted. Prompt saved to {prompt_file}")
241
+ sys.exit(0)
242
+ elif answer == "c":
243
+ answer = input("Write your considerations to change the prompt: ").strip().lower()
244
+ md_context = review_prompt(acceptance_criteria, md_context, answer, agent, GROK_API_KEY, OPENAI_API_KEY)
245
+ continue # Regenerate the prompt with the new context and considerations.
246
+
247
+ print("\n\n")
248
+ print_step("[4/4]", "Passing prompt to Claude Code …")
249
+ print_success("Prompt preview:")
250
+ print("-" * 68)
251
+ for line in prompt_md.splitlines():
252
+ print(line)
253
+ sleep(0.02) # Simulate a "typing" effect for better readability
254
+ print("-" * 68)
255
+ answer = input("\nAre you sure? This will run the Claude Code CLI with the generated prompt. [y/N/c] ").strip().lower()
256
+ if answer == "n":
257
+ print_info("Aborted by user.")
258
+ sys.exit(0)
259
+ elif answer == "c":
260
+ answer = input("Write your considerations to change the prompt: ").strip().lower()
261
+ md_context = review_prompt(acceptance_criteria, md_context, answer, agent, GROK_API_KEY, OPENAI_API_KEY)
262
+ continue # Regenerate the prompt with the new context and considerations.
263
+
264
+ execute_prompt = True
265
+ break
266
+
248
267
 
249
268
  # ── Step 4 – Pass prompt to Claude Code ──────────────────────────────────
250
- print_step("[4/4]", "Passing prompt to Claude Code …")
251
269
  print()
252
270
  asyncio.run(_pass_to_claude_code(prompt_md, task_id, None))
253
271
 
@@ -3,8 +3,9 @@
3
3
  LLM integration for bob_dev:
4
4
  - Build an OpenAI-compatible client for GROK or OpenAI backends.
5
5
  - Return the appropriate model name for a given backend.
6
- - Generate a Claude Code prompt from Jira acceptance criteria.
6
+ - Generate a Claude Code prompt from task acceptance criteria.
7
7
  - Analyse a generated prompt for gaps and security concerns.
8
+ - Regenerate a prompt based on user feedback after analysis.
8
9
  """
9
10
 
10
11
  from __future__ import annotations
@@ -12,26 +13,27 @@ from __future__ import annotations
12
13
  import textwrap
13
14
 
14
15
  from openai import OpenAI
16
+ from openai import OpenAIError
15
17
 
16
18
 
17
- # ---------------------------------------------------------------------------
18
- # Client & model helpers
19
- # ---------------------------------------------------------------------------
20
-
21
19
  def build_llm_client(agent: str, grok_api_key: str, openai_api_key: str) -> OpenAI:
22
20
  """Return an OpenAI-compatible client for *agent* ("GROK" or "OPENAI").
23
21
 
24
22
  Raises EnvironmentError if the required API key is missing.
23
+ Raises ConnectionError if the LLM client cannot be created.
25
24
  """
26
- if agent == "GROK":
27
- if not grok_api_key:
28
- raise EnvironmentError("GROK_API_KEY is not set in .env")
29
- return OpenAI(api_key=grok_api_key, base_url="https://api.x.ai/v1")
25
+ try:
26
+ if agent == "GROK":
27
+ if not grok_api_key:
28
+ raise EnvironmentError("GROK_API_KEY is not set in .env")
29
+ return OpenAI(api_key=grok_api_key, base_url="https://api.x.ai/v1")
30
30
 
31
- # Default: OpenAI
32
- if not openai_api_key:
33
- raise EnvironmentError("OPENAI_API_KEY is not set in .env")
34
- return OpenAI(api_key=openai_api_key)
31
+ # Default: OpenAI
32
+ if not openai_api_key:
33
+ raise EnvironmentError("OPENAI_API_KEY is not set in .env")
34
+ return OpenAI(api_key=openai_api_key)
35
+ except OpenAIError as exc:
36
+ raise ConnectionError(f"Failed to create LLM client for {agent}: {exc}") from exc
35
37
 
36
38
 
37
39
  def llm_model(agent: str) -> str:
@@ -39,10 +41,6 @@ def llm_model(agent: str) -> str:
39
41
  return "grok-3" if agent == "GROK" else "gpt-4o"
40
42
 
41
43
 
42
- # ---------------------------------------------------------------------------
43
- # Prompt generation
44
- # ---------------------------------------------------------------------------
45
-
46
44
  def prompt_claude_code(
47
45
  acceptance_criteria: str,
48
46
  md_context: str,
@@ -64,8 +62,6 @@ def prompt_claude_code(
64
62
  Detected framework name (e.g. "Django REST Framework").
65
63
  agent:
66
64
  "GROK" or "OPENAI".
67
- grok_api_key / openai_api_key:
68
- API keys; the unused one may be an empty string.
69
65
  task_meta:
70
66
  Optional dict with 'task_id', 'title', 'fix_versions' for context.
71
67
 
@@ -141,10 +137,6 @@ def prompt_claude_code(
141
137
  return response.choices[0].message.content or ""
142
138
 
143
139
 
144
- # ---------------------------------------------------------------------------
145
- # Prompt analysis
146
- # ---------------------------------------------------------------------------
147
-
148
140
  def analyse_prompt(
149
141
  prompt_md: str,
150
142
  acceptance_criteria: str,
@@ -154,7 +146,18 @@ def analyse_prompt(
154
146
  ) -> str:
155
147
  """Ask the LLM to review *prompt_md* for ambiguities and security concerns.
156
148
 
157
- Returns a short bullet-point analysis string (max ~300 words).
149
+ Parameters
150
+ ----------
151
+ prompt_md:
152
+ The generated Claude Code prompt in Markdown format.
153
+ acceptance_criteria:
154
+ The original acceptance criteria text from Jira.
155
+ agent:
156
+ "GROK" or "OPENAI".
157
+ Returns
158
+ -------
159
+ str
160
+ A concise analysis of the prompt's gaps, ambiguities, and security issues.
158
161
  """
159
162
  client = build_llm_client(agent, grok_api_key, openai_api_key)
160
163
  model = llm_model(agent)
@@ -185,3 +188,71 @@ def analyse_prompt(
185
188
  )
186
189
 
187
190
  return response.choices[0].message.content or ""
191
+
192
+
193
+ def review_prompt(
194
+ acceptance_criteria: str,
195
+ md_context: str,
196
+ considerations: str,
197
+ agent: str,
198
+ grok_api_key: str,
199
+ openai_api_key: str,
200
+ ) -> str:
201
+ """Ask the LLM to regenerate the prompt based on *considerations*.
202
+
203
+ This is used when the user wants to refine the prompt after an initial review.
204
+
205
+ Parameters
206
+ ----------
207
+ acceptance_criteria:
208
+ Raw description text from the Jira task.
209
+ md_context:
210
+ Pre-built string of project Markdown files + summarised README.
211
+ considerations:
212
+ User-provided feedback on what to improve in the prompt.
213
+ agent:
214
+ "GROK" or "OPENAI".
215
+ Returns
216
+ -------
217
+ str
218
+ A new version of the Claude Code prompt, improved according to the feedback.
219
+ """
220
+ client = build_llm_client(agent, grok_api_key, openai_api_key)
221
+ model = llm_model(agent)
222
+
223
+ system_prompt = textwrap.dedent(f"""
224
+ You are a senior software engineer refining a Claude Code prompt based on feedback.
225
+ The original prompt had the following issues:
226
+ {considerations}
227
+
228
+ Regenerate the Claude Code prompt, improving it according to the feedback above.
229
+ Maintain all original requirements and formatting instructions, but address the
230
+ identified issues to make it more actionable and secure.
231
+ """).strip()
232
+
233
+ user_message = textwrap.dedent(f"""
234
+ ## Project Documentation
235
+
236
+ {md_context}
237
+
238
+ ---
239
+
240
+ ## Original Acceptance Criteria
241
+
242
+ {acceptance_criteria}
243
+
244
+ ---
245
+
246
+ Regenerate the Claude Code prompt based on the feedback provided.
247
+ """).strip()
248
+
249
+ response = client.chat.completions.create(
250
+ model=model,
251
+ messages=[
252
+ {"role": "system", "content": system_prompt},
253
+ {"role": "user", "content": user_message},
254
+ ],
255
+ temperature=0.3,
256
+ )
257
+
258
+ return response.choices[0].message.content or ""
@@ -23,6 +23,7 @@ def collect_md_context(
23
23
  root: Path,
24
24
  max_files: int = 30,
25
25
  max_chars: int = 40_000,
26
+ max_words: int = 2000,
26
27
  ) -> str:
27
28
  """Walk *root* and return concatenated content of Markdown files.
28
29
 
@@ -33,6 +34,7 @@ def collect_md_context(
33
34
  parts: list[str] = []
34
35
  total = 0
35
36
 
37
+ summarize_context = sum(md.stat().st_size for md in md_files) > max_chars
36
38
  for md in md_files:
37
39
  try:
38
40
  text = md.read_text(encoding="utf-8", errors="ignore")
@@ -44,6 +46,9 @@ def collect_md_context(
44
46
 
45
47
  if total + len(chunk) > max_chars:
46
48
  break
49
+
50
+ if summarize_context:
51
+ chunk = f"### {relative}\n\n" + summarize(text, words=max_words) + "\n\n"
47
52
 
48
53
  parts.append(chunk)
49
54
  total += len(chunk)
@@ -66,14 +71,14 @@ def read_readme(repo_path: Path) -> str:
66
71
  return ""
67
72
 
68
73
 
69
- def build_md_context(repo_path: Path) -> str:
74
+ def build_md_context(repo_path: Path, max_summary_words: int = 2000) -> str:
70
75
  """Build a combined context string for LLM consumption.
71
76
 
72
- Combines a summarised README (up to 300 words) with the full
77
+ Combines a summarised README (up to max_summary_words) with the full
73
78
  Markdown context collected from the repository.
74
79
  """
75
80
  readme_raw = read_readme(repo_path)
76
- readme_summary = summarize(readme_raw, words=300) if readme_raw.strip() else ""
81
+ readme_summary = summarize(readme_raw, words=max_summary_words) if readme_raw.strip() else ""
77
82
  md_context = collect_md_context(repo_path)
78
83
 
79
84
  return "# README:\n" + readme_summary + "\n\n# Context:\n" + md_context
@@ -0,0 +1,45 @@
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ def validate_variables():
7
+ """
8
+ Validates the environment variables for the application.
9
+ Raises a ValueError if any required variable is missing or invalid.
10
+
11
+ Required variables:
12
+ - AGENT: Must be either "GROK" or "OPENAI".
13
+ - TASK_MANAGER: Must be either "GITLAB" or "JIRA".
14
+ - For GitLab: GITLAB_URL and GITLAB_API_TOKEN must be set.
15
+ - For Jira: JIRA_URL, JIRA_EMAIL, and JIRA_API_TOKEN must be set.
16
+ """
17
+
18
+ if AGENT not in ("GROK", "OPENAI"):
19
+ raise ValueError("AGENT must be either 'GROK' or 'OPENAI'")
20
+ if TASK_MANAGER not in ("GITLAB", "JIRA"):
21
+ raise ValueError("TASK_MANAGER must be either 'GITLAB' or 'JIRA'")
22
+
23
+ if TASK_MANAGER == "GITLAB" and (not GITLAB_URL or not GITLAB_API_TOKEN):
24
+ raise ValueError("GITLAB_URL and GITLAB_API_TOKEN must be set for GitLab task management")
25
+ if TASK_MANAGER == "JIRA" and (not JIRA_URL or not JIRA_EMAIL or not JIRA_API_TOKEN):
26
+ raise ValueError("JIRA_URL, JIRA_EMAIL, and JIRA_API_TOKEN must be set for Jira task management")
27
+
28
+ # LLM backend: GROK (default) or OPENAI
29
+ AGENT = os.environ.get("AGENT", "GROK").upper()
30
+ TASK_MANAGER = os.environ.get("TASK_MANAGER", "JIRA").upper()
31
+
32
+ # Grok (xAI)
33
+ GROK_API_KEY = os.environ.get("GROK_API_KEY", "")
34
+
35
+ # OpenAI (only needed when AGENT=OPENAI)
36
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
37
+
38
+ # Jira / Atlassian
39
+ JIRA_URL = os.environ.get("JIRA_URL", "")
40
+ JIRA_EMAIL = os.environ.get("JIRA_EMAIL", "")
41
+ JIRA_API_TOKEN = os.environ.get("JIRA_API_TOKEN", "")
42
+
43
+ # GitLab
44
+ GITLAB_URL = os.environ.get("GITLAB_URL", "")
45
+ GITLAB_API_TOKEN = os.environ.get("GITLAB_API_TOKEN", "")
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bob-dev
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: An AI developer to write code for you
5
5
  Author-email: Samuel Pereira dos Santos <samuelsantosdev@gmail.com>
6
- Requires-Python: >=3.14
6
+ Requires-Python: >=3.12.10
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Provides-Extra: dev
@@ -30,7 +30,7 @@ Given a task ID (Jira or GitLab) it will:
30
30
 
31
31
  ## Requirements
32
32
 
33
- - Python 3.14+
33
+ - Python 3.12+
34
34
  - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and available on `$PATH` as `claude`
35
35
  - A **Jira Cloud** account with an [API token](https://id.atlassian.com/manage-profile/security/api-tokens) **or** a **GitLab** account with a [personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html)
36
36
  - An [xAI / GROK](https://console.x.ai/) **or** [OpenAI](https://platform.openai.com/) API key
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  src/__init__.py
5
5
  src/bob_dev/__init__.py
6
6
  src/bob_dev/cli.py
7
+ src/bob_dev/settings.py
7
8
  src/bob_dev.egg-info/PKG-INFO
8
9
  src/bob_dev.egg-info/SOURCES.txt
9
10
  src/bob_dev.egg-info/dependency_links.txt
@@ -3,8 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import sys
6
- from pathlib import Path
7
- from unittest.mock import AsyncMock, MagicMock, patch
6
+ from unittest.mock import AsyncMock, patch
8
7
 
9
8
  import pytest
10
9
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes