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.
- {bob_dev-0.2.2/src/bob_dev.egg-info → bob_dev-0.3.0}/PKG-INFO +3 -3
- {bob_dev-0.2.2 → bob_dev-0.3.0}/README.md +1 -1
- {bob_dev-0.2.2 → bob_dev-0.3.0}/pyproject.toml +2 -2
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/cli.py +76 -62
- bob_dev-0.3.0/src/bob_dev/services/claude.py +54 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/services/llm.py +95 -24
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/services/project.py +8 -3
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/services/terminal.py +1 -0
- bob_dev-0.3.0/src/bob_dev/settings.py +45 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0/src/bob_dev.egg-info}/PKG-INFO +3 -3
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev.egg-info/SOURCES.txt +1 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/tests/test_cli.py +1 -2
- bob_dev-0.2.2/src/bob_dev/services/claude.py +0 -27
- {bob_dev-0.2.2 → bob_dev-0.3.0}/LICENSE +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/setup.cfg +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/__init__.py +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/__init__.py +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/constants/__init__.py +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/constants/frameworks.py +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/services/__init__.py +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/services/config.py +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/services/gitlab.py +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev/services/jira.py +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev.egg-info/dependency_links.txt +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev.egg-info/entry_points.txt +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev.egg-info/requires.txt +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/src/bob_dev.egg-info/top_level.txt +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/tests/test_config.py +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/tests/test_jira.py +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/tests/test_llm.py +0 -0
- {bob_dev-0.2.2 → bob_dev-0.3.0}/tests/test_project.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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("
|
|
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("
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
214
|
-
|
|
212
|
+
if not prompt_md.strip():
|
|
213
|
+
print_error("Failed to generate a prompt for Claude Code.")
|
|
214
|
+
sys.exit(1)
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
print("─" * 68 + "\n")
|
|
219
|
+
# ── Step 3 – Analyse the prompt ──────────────────────────────────────────
|
|
220
|
+
print_step("[3/4]", "Analysing the prompt for issues …")
|
|
226
221
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
27
|
-
if
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
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=
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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
|
|
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
|
|
File without changes
|