bob-dev 0.2.2__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 (32) hide show
  1. {bob_dev-0.2.2/src/bob_dev.egg-info → bob_dev-0.3.0}/PKG-INFO +3 -3
  2. {bob_dev-0.2.2 → bob_dev-0.3.0}/README.md +1 -1
  3. {bob_dev-0.2.2 → bob_dev-0.3.0}/pyproject.toml +2 -2
  4. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/cli.py +76 -62
  5. bob_dev-0.3.0/src/bob_dev/services/claude.py +54 -0
  6. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/services/llm.py +95 -24
  7. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/services/project.py +8 -3
  8. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/services/terminal.py +1 -0
  9. bob_dev-0.3.0/src/bob_dev/settings.py +45 -0
  10. {bob_dev-0.2.2 → bob_dev-0.3.0/src/bob_dev.egg-info}/PKG-INFO +3 -3
  11. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev.egg-info/SOURCES.txt +1 -0
  12. {bob_dev-0.2.2 → bob_dev-0.3.0}/tests/test_cli.py +1 -2
  13. bob_dev-0.2.2/src/bob_dev/services/claude.py +0 -27
  14. {bob_dev-0.2.2 → bob_dev-0.3.0}/LICENSE +0 -0
  15. {bob_dev-0.2.2 → bob_dev-0.3.0}/setup.cfg +0 -0
  16. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/__init__.py +0 -0
  17. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/__init__.py +0 -0
  18. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/constants/__init__.py +0 -0
  19. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/constants/frameworks.py +0 -0
  20. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/services/__init__.py +0 -0
  21. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/services/config.py +0 -0
  22. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/services/gitlab.py +0 -0
  23. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/services/jira.py +0 -0
  24. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev.egg-info/dependency_links.txt +0 -0
  25. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev.egg-info/entry_points.txt +0 -0
  26. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev.egg-info/requires.txt +0 -0
  27. {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev.egg-info/top_level.txt +0 -0
  28. {bob_dev-0.2.2 → bob_dev-0.3.0}/tests/test_config.py +0 -0
  29. {bob_dev-0.2.2 → bob_dev-0.3.0}/tests/test_jira.py +0 -0
  30. {bob_dev-0.2.2 → bob_dev-0.3.0}/tests/test_llm.py +0 -0
  31. {bob_dev-0.2.2 → bob_dev-0.3.0}/tests/test_project.py +0 -0
  32. {bob_dev-0.2.2 → 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.2
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.2"
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,6 +33,7 @@ import sys
33
33
  import argparse
34
34
  import asyncio
35
35
  from pathlib import Path
36
+ from time import sleep
36
37
 
37
38
  from InquirerPy import inquirer
38
39
  from dotenv import load_dotenv
@@ -50,8 +51,7 @@ from .services.terminal import (
50
51
  )
51
52
  from .services.jira import get_jira_task
52
53
  from .services.gitlab import get_gitlab_task
53
- from .services.claude import read_agents_from_claude
54
- 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
55
55
  from .services.project import build_md_context, identify_framework
56
56
  from .services.config import check_configuration, update_env_file
57
57
 
@@ -60,11 +60,9 @@ from .services.config import check_configuration, update_env_file
60
60
  # ---------------------------------------------------------------------------
61
61
 
62
62
  SCRIPT_DIR = Path(__file__).resolve().parent
63
- load_dotenv(SCRIPT_DIR / ".env")
64
63
  ENV_PATH = Path.home() / ".bob_dev" / ".env"
64
+ load_dotenv(ENV_PATH) # Load .env if it exists, but don't require it
65
65
 
66
- OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
67
- GROK_API_KEY = os.environ.get("GROK_API_KEY", "")
68
66
  AGENT = os.environ.get("AGENT", "GROK").upper() # "GROK" or "OPENAI"
69
67
  TASK_MANAGER = os.environ.get("TASK_MANAGER", "JIRA").upper() # "JIRA" or "GITLAB"
70
68
 
@@ -72,12 +70,18 @@ JIRA_URL = os.environ.get("JIRA_URL", "")
72
70
  JIRA_EMAIL = os.environ.get("JIRA_EMAIL", "")
73
71
  JIRA_API_TOKEN = os.environ.get("JIRA_API_TOKEN", "")
74
72
 
73
+ GROK_API_KEY = os.environ.get("GROK_API_KEY", "")
74
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
75
+
75
76
  GITLAB_URL = os.environ.get("GITLAB_URL", "")
76
77
  GITLAB_API_TOKEN = os.environ.get("GITLAB_API_TOKEN", "")
77
78
 
78
79
  REPO_BASE_PATH = Path("./") # Overridden by --path at runtime.
79
80
  CLAUDE_CODE_CMD = "claude" # Must be on $PATH.
80
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))
81
85
 
82
86
  # ---------------------------------------------------------------------------
83
87
  # Main entry point
@@ -131,11 +135,11 @@ def main() -> None:
131
135
  sys.exit(1)
132
136
 
133
137
  if agent == "GROK" and not GROK_API_KEY:
134
- 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`.")
135
139
  sys.exit(1)
136
140
 
137
141
  if agent == "OPENAI" and not OPENAI_API_KEY:
138
- 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`.")
139
143
  sys.exit(1)
140
144
 
141
145
  # ── Resolve and validate the repository path ─────────────────────────────
@@ -146,9 +150,9 @@ def main() -> None:
146
150
  print_error(f"Invalid project path: {REPO_BASE_PATH}")
147
151
  sys.exit(1)
148
152
 
149
- print_info(f"Project path : {REPO_BASE_PATH}")
150
- print_info(f"{TASK_MANAGER} task ID : {task_id}")
151
- 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)})")
152
156
  print()
153
157
 
154
158
  # ── Step 1 – Fetch task ─────────────────────────────────────────────
@@ -183,7 +187,7 @@ def main() -> None:
183
187
 
184
188
  # Collect Markdown context (blocking I/O) inside the spinner thread.
185
189
  md_context = asyncio.run(run_with_spinner(
186
- build_md_context, REPO_BASE_PATH,
190
+ build_md_context, REPO_BASE_PATH, MAX_SUMMARY_WORDS,
187
191
  label="Reading project docs",
188
192
  ))
189
193
 
@@ -195,65 +199,75 @@ def main() -> None:
195
199
  print_warn(str(exc))
196
200
  framework = "the project"
197
201
 
198
- prompt_md = asyncio.run(run_with_spinner(
199
- prompt_claude_code,
200
- acceptance_criteria, md_context, framework,
201
- agent, GROK_API_KEY, OPENAI_API_KEY,
202
- label="Generating prompt",
203
- task_meta=task,
204
- ))
205
-
206
- if not prompt_md.strip():
207
- print_error("Failed to generate a prompt for Claude Code.")
208
- sys.exit(1)
209
-
210
- print_success("Prompt generated.")
211
- print()
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
+ ))
212
211
 
213
- # ── Step 3 – Analyse the prompt ──────────────────────────────────────────
214
- print_step("[3/4]", "Analysing the prompt for issues ")
212
+ if not prompt_md.strip():
213
+ print_error("Failed to generate a prompt for Claude Code.")
214
+ sys.exit(1)
215
215
 
216
- analysis = asyncio.run(run_with_spinner(
217
- analyse_prompt,
218
- prompt_md, acceptance_criteria,
219
- agent, GROK_API_KEY, OPENAI_API_KEY,
220
- label="Analysing prompt",
221
- ))
216
+ print_success("Prompt generated.")
217
+ print()
222
218
 
223
- print(f"\n{BOLD}── Prompt Analysis {'─' * 50}{RESET}")
224
- print(analysis)
225
- print("─" * 68 + "\n")
219
+ # ── Step 3 Analyse the prompt ──────────────────────────────────────────
220
+ print_step("[3/4]", "Analysing the prompt for issues …")
226
221
 
227
- # ── Confirm before handing off to Claude Code ────────────────────────────
228
- answer = input("Proceed and send prompt to Claude Code? [y/N] ").strip().lower()
229
- if answer != "y":
230
- prompt_file = SCRIPT_DIR / f"claude_prompt-{task_id}.md"
231
- prompt_file.write_text(prompt_md, encoding="utf-8")
232
- print_info(f"Aborted. Prompt saved to {prompt_file}")
233
- 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
+ ))
234
228
 
235
- print_info("Prompt preview:")
236
- print("-" * 68)
237
- print(prompt_md)
238
- print("-" * 68)
239
- answer = input("\nAre you sure? This will run the Claude Code CLI with the generated prompt. [y/N] ").strip().lower()
240
- if answer != "y":
241
- print_info("Aborted by user.")
242
- sys.exit(0)
243
-
244
- answer = input("Do you want select a agent to do this development? [y/N] ").strip().upper()
245
- agent_claude = ""
246
- if answer == "Y":
247
- agents_of_claude = read_agents_from_claude(CLAUDE_CODE_CMD)
248
- agent_claude = inquirer.select(
249
- message="Select the agent to do the development:",
250
- choices=agents_of_claude,
251
- ).execute()
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
+
252
267
 
253
268
  # ── Step 4 – Pass prompt to Claude Code ──────────────────────────────────
254
- print_step("[4/4]", "Passing prompt to Claude Code …")
255
269
  print()
256
- asyncio.run(_pass_to_claude_code(prompt_md, task_id, agent_claude))
270
+ asyncio.run(_pass_to_claude_code(prompt_md, task_id, None))
257
271
 
258
272
 
259
273
  # ---------------------------------------------------------------------------
@@ -0,0 +1,54 @@
1
+ """claude.py
2
+
3
+ Claude Code utilities for bob_dev:
4
+ - List available agents from the Claude Code CLI.
5
+ - Run Claude Code commands and handle errors.
6
+ """
7
+
8
+ import subprocess
9
+ import json
10
+
11
+ from ..services.terminal import print_error
12
+
13
+
14
+ def read_agents_from_claude(claude_cmd: str) -> list[str]:
15
+ """Run *claude_cmd* with --list-agents and parse the output into a list."""
16
+ try:
17
+ # list the installed agents by running the CLI command. This is more reliable than
18
+ # trying to read the plugin directory directly, which may have different permissions or structure.
19
+ # The expected output is a list of agent names, one per line. We strip whitespace and ignore empty lines.
20
+
21
+ result_marketplace = subprocess.run(
22
+ ["ls", "~/.claude/plugins/marketplaces/claude-plugins-official/plugins"],
23
+ stdout=subprocess.PIPE,
24
+ stderr=subprocess.PIPE,
25
+ text=True,
26
+ check=True,
27
+ )
28
+ result_agents = subprocess.run(
29
+ [claude_cmd, "agents", "--json"],
30
+ stdout=subprocess.PIPE,
31
+ stderr=subprocess.PIPE,
32
+ text=True,
33
+ check=True,
34
+ )
35
+
36
+ agents_data = json.loads(result_agents.stdout)
37
+ # We want to extract the "name" field from each agent,
38
+ # but only if it has one (background agents may not have a name).
39
+ # We also want to include the marketplace plugins as agents,
40
+ # since they can be invoked by name as well.
41
+ # The marketplace plugins are just the filenames in the marketplace directory.
42
+ result_agents = [str(agent.get("name")).strip() for agent in agents_data if agent.get("name")]
43
+ agents_marketplace = [line.strip() for line in result_marketplace.stdout.splitlines() if line.strip()]
44
+ # Combine agents from both sources, ensuring uniqueness while preserving order.
45
+ seen = set()
46
+ combined_agents = []
47
+ for agent in result_agents + agents_marketplace:
48
+ if agent not in seen:
49
+ seen.add(agent)
50
+ combined_agents.append(agent)
51
+ return combined_agents
52
+ except Exception as exc:
53
+ print_error(f"Failed to list agents from Claude Code CLI: {exc}")
54
+ return []
@@ -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
@@ -11,6 +11,7 @@ from __future__ import annotations
11
11
 
12
12
  import asyncio
13
13
  import time
14
+ import sys
14
15
  from pathlib import Path
15
16
 
16
17
  # ---------------------------------------------------------------------------
@@ -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.2
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
 
@@ -1,27 +0,0 @@
1
- """claude.py
2
-
3
- Claude Code utilities for bob_dev:
4
- - List available agents from the Claude Code CLI.
5
- - Run Claude Code commands and handle errors.
6
- """
7
-
8
- import subprocess
9
-
10
- from ..services.terminal import print_error
11
-
12
-
13
- def read_agents_from_claude(claude_cmd: str) -> list[str]:
14
- """Run *claude_cmd* with --list-agents and parse the output into a list."""
15
- try:
16
- result = subprocess.run(
17
- [claude_cmd, "--list-agents"],
18
- stdout=subprocess.PIPE,
19
- stderr=subprocess.PIPE,
20
- text=True,
21
- check=True,
22
- )
23
- agents = [line.strip() for line in result.stdout.splitlines() if line.strip()]
24
- return agents
25
- except Exception as exc:
26
- print_error(f"Failed to list agents from Claude Code CLI: {exc}")
27
- return []
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