bob-dev 0.1.0__tar.gz → 0.2.1__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/src/bob_dev.egg-info → bob_dev-0.2.1}/PKG-INFO +34 -13
- {bob_dev-0.1.0 → bob_dev-0.2.1}/README.md +33 -12
- {bob_dev-0.1.0 → bob_dev-0.2.1}/pyproject.toml +1 -1
- {bob_dev-0.1.0 → bob_dev-0.2.1}/src/bob_dev/cli.py +100 -34
- bob_dev-0.2.1/src/bob_dev/services/claude.py +27 -0
- bob_dev-0.1.0/src/bob_dev/helpers/config_helper.py → bob_dev-0.2.1/src/bob_dev/services/config.py +28 -12
- bob_dev-0.2.1/src/bob_dev/services/gitlab.py +42 -0
- bob_dev-0.1.0/src/bob_dev/helpers/jira_helper.py → bob_dev-0.2.1/src/bob_dev/services/jira.py +1 -1
- bob_dev-0.1.0/src/bob_dev/helpers/llm_helper.py → bob_dev-0.2.1/src/bob_dev/services/llm.py +1 -1
- bob_dev-0.1.0/src/bob_dev/helpers/project_helper.py → bob_dev-0.2.1/src/bob_dev/services/project.py +1 -1
- {bob_dev-0.1.0 → bob_dev-0.2.1/src/bob_dev.egg-info}/PKG-INFO +34 -13
- {bob_dev-0.1.0 → bob_dev-0.2.1}/src/bob_dev.egg-info/SOURCES.txt +12 -10
- bob_dev-0.1.0/tests/test_config_helper.py → bob_dev-0.2.1/tests/test_config.py +2 -2
- bob_dev-0.1.0/tests/test_jira_helper.py → bob_dev-0.2.1/tests/test_jira.py +11 -11
- bob_dev-0.1.0/tests/test_llm_helper.py → bob_dev-0.2.1/tests/test_llm.py +16 -16
- bob_dev-0.1.0/tests/test_project_helper.py → bob_dev-0.2.1/tests/test_project.py +5 -5
- {bob_dev-0.1.0 → bob_dev-0.2.1}/tests/test_terminal.py +2 -2
- {bob_dev-0.1.0 → bob_dev-0.2.1}/LICENSE +0 -0
- {bob_dev-0.1.0 → bob_dev-0.2.1}/setup.cfg +0 -0
- {bob_dev-0.1.0 → bob_dev-0.2.1}/src/__init__.py +0 -0
- {bob_dev-0.1.0 → bob_dev-0.2.1}/src/bob_dev/__init__.py +0 -0
- {bob_dev-0.1.0 → bob_dev-0.2.1}/src/bob_dev/constants/__init__.py +0 -0
- {bob_dev-0.1.0 → bob_dev-0.2.1}/src/bob_dev/constants/frameworks.py +0 -0
- {bob_dev-0.1.0/src/bob_dev/helpers → bob_dev-0.2.1/src/bob_dev/services}/__init__.py +0 -0
- {bob_dev-0.1.0/src/bob_dev/helpers → bob_dev-0.2.1/src/bob_dev/services}/terminal.py +0 -0
- {bob_dev-0.1.0 → bob_dev-0.2.1}/src/bob_dev.egg-info/dependency_links.txt +0 -0
- {bob_dev-0.1.0 → bob_dev-0.2.1}/src/bob_dev.egg-info/entry_points.txt +0 -0
- {bob_dev-0.1.0 → bob_dev-0.2.1}/src/bob_dev.egg-info/requires.txt +0 -0
- {bob_dev-0.1.0 → bob_dev-0.2.1}/src/bob_dev.egg-info/top_level.txt +0 -0
- {bob_dev-0.1.0 → bob_dev-0.2.1}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bob-dev
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: An AI developer to write code for you
|
|
5
5
|
Author-email: Samuel Pereira dos Santos <samuelsantosdev@gmail.com>
|
|
6
6
|
Requires-Python: >=3.14
|
|
@@ -17,21 +17,22 @@ Dynamic: license-file
|
|
|
17
17
|
|
|
18
18
|

|
|
19
19
|
|
|
20
|
-
Given a
|
|
20
|
+
Given a task ID (Jira or GitLab) it will:
|
|
21
21
|
|
|
22
|
-
1. **Fetch** the task title, description, and fix versions from Jira.
|
|
22
|
+
1. **Fetch** the task title, description, and fix versions from Jira or GitLab.
|
|
23
23
|
2. **Read** your project's Markdown documentation to build rich LLM context.
|
|
24
24
|
3. **Generate** a precise Claude Code prompt (via GROK or OpenAI), including project-framework context, implementation steps, and test scenarios.
|
|
25
25
|
4. **Analyse** the prompt for ambiguities and security concerns.
|
|
26
|
-
5. **
|
|
26
|
+
5. **Select** (optionally) a Claude Code agent to run the implementation.
|
|
27
|
+
6. **Execute** the prompt with the Claude Code CLI (optional – you can review first).
|
|
27
28
|
|
|
28
29
|
---
|
|
29
30
|
|
|
30
31
|
## Requirements
|
|
31
32
|
|
|
32
|
-
- Python 3.
|
|
33
|
+
- Python 3.14+
|
|
33
34
|
- [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
|
+
- 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)
|
|
35
36
|
- An [xAI / GROK](https://console.x.ai/) **or** [OpenAI](https://platform.openai.com/) API key
|
|
36
37
|
|
|
37
38
|
---
|
|
@@ -52,19 +53,37 @@ Run the interactive setup wizard the first time:
|
|
|
52
53
|
bob-dev --configure
|
|
53
54
|
```
|
|
54
55
|
|
|
55
|
-
This will prompt for your API
|
|
56
|
+
This will prompt for your LLM API key, task manager (Jira or GitLab) credentials, and Claude Code API key.
|
|
57
|
+
|
|
58
|
+
### Environment variables
|
|
59
|
+
|
|
60
|
+
| Variable | Description |
|
|
61
|
+
|----------|-------------|
|
|
62
|
+
| `AGENT` | LLM backend: `GROK` (default) or `OPENAI` |
|
|
63
|
+
| `GROK_API_KEY` | xAI / GROK secret key |
|
|
64
|
+
| `OPENAI_API_KEY` | OpenAI secret key |
|
|
65
|
+
| `TASK_MANAGER` | Task manager: `JIRA` (default) or `GITLAB` |
|
|
66
|
+
| `JIRA_URL` | e.g. `https://your-org.atlassian.net` |
|
|
67
|
+
| `JIRA_EMAIL` | Atlassian account e-mail |
|
|
68
|
+
| `JIRA_API_TOKEN` | Atlassian API token |
|
|
69
|
+
| `GITLAB_URL` | e.g. `https://gitlab.com` |
|
|
70
|
+
| `GITLAB_API_TOKEN` | GitLab personal access token |
|
|
56
71
|
|
|
57
72
|
---
|
|
58
73
|
|
|
59
74
|
## Usage
|
|
60
75
|
|
|
61
76
|
```bash
|
|
77
|
+
# Jira
|
|
62
78
|
bob-dev --task_id PROJ-123 --path /path/to/your/repo
|
|
79
|
+
|
|
80
|
+
# GitLab (issue IID)
|
|
81
|
+
bob-dev --task_id 42 --path /path/to/your/repo
|
|
63
82
|
```
|
|
64
83
|
|
|
65
84
|
| Flag | Description |
|
|
66
85
|
|------|-------------|
|
|
67
|
-
| `--task_id` |
|
|
86
|
+
| `--task_id` | Task ID to process — Jira key (`PROJ-123`) or GitLab issue IID (`42`) |
|
|
68
87
|
| `--path` | Path to the target repository (default: current directory) |
|
|
69
88
|
| `--agent` | LLM backend: `GROK` or `OPENAI` (default: value of `AGENT` in `.env`) |
|
|
70
89
|
| `--configure` | Run the interactive configuration wizard |
|
|
@@ -76,12 +95,14 @@ bob-dev --task_id PROJ-123 --path /path/to/your/repo
|
|
|
76
95
|
```
|
|
77
96
|
src/bob_dev/
|
|
78
97
|
├── cli.py # Entry point & main workflow orchestration
|
|
79
|
-
├──
|
|
98
|
+
├── services/
|
|
80
99
|
│ ├── terminal.py # ANSI colours, print helpers, spinner animation
|
|
81
|
-
│ ├──
|
|
82
|
-
│ ├──
|
|
83
|
-
│ ├──
|
|
84
|
-
│
|
|
100
|
+
│ ├── jira.py # Jira API connection + ADF-to-text parsing
|
|
101
|
+
│ ├── gitlab.py # GitLab API connection via python-gitlab
|
|
102
|
+
│ ├── claude.py # Claude Code CLI utilities (agent listing)
|
|
103
|
+
│ ├── llm.py # LLM client, model selection, prompt generation
|
|
104
|
+
│ ├── project.py # Markdown context collection + framework detection
|
|
105
|
+
│ └── config.py # .env management + credential validation
|
|
85
106
|
└── constants/
|
|
86
107
|
└── frameworks.py # Known framework names used for auto-detection
|
|
87
108
|
```
|
|
@@ -4,21 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
Given a
|
|
7
|
+
Given a task ID (Jira or GitLab) it will:
|
|
8
8
|
|
|
9
|
-
1. **Fetch** the task title, description, and fix versions from Jira.
|
|
9
|
+
1. **Fetch** the task title, description, and fix versions from Jira or GitLab.
|
|
10
10
|
2. **Read** your project's Markdown documentation to build rich LLM context.
|
|
11
11
|
3. **Generate** a precise Claude Code prompt (via GROK or OpenAI), including project-framework context, implementation steps, and test scenarios.
|
|
12
12
|
4. **Analyse** the prompt for ambiguities and security concerns.
|
|
13
|
-
5. **
|
|
13
|
+
5. **Select** (optionally) a Claude Code agent to run the implementation.
|
|
14
|
+
6. **Execute** the prompt with the Claude Code CLI (optional – you can review first).
|
|
14
15
|
|
|
15
16
|
---
|
|
16
17
|
|
|
17
18
|
## Requirements
|
|
18
19
|
|
|
19
|
-
- Python 3.
|
|
20
|
+
- Python 3.14+
|
|
20
21
|
- [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
|
+
- 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)
|
|
22
23
|
- An [xAI / GROK](https://console.x.ai/) **or** [OpenAI](https://platform.openai.com/) API key
|
|
23
24
|
|
|
24
25
|
---
|
|
@@ -39,19 +40,37 @@ Run the interactive setup wizard the first time:
|
|
|
39
40
|
bob-dev --configure
|
|
40
41
|
```
|
|
41
42
|
|
|
42
|
-
This will prompt for your API
|
|
43
|
+
This will prompt for your LLM API key, task manager (Jira or GitLab) credentials, and Claude Code API key.
|
|
44
|
+
|
|
45
|
+
### Environment variables
|
|
46
|
+
|
|
47
|
+
| Variable | Description |
|
|
48
|
+
|----------|-------------|
|
|
49
|
+
| `AGENT` | LLM backend: `GROK` (default) or `OPENAI` |
|
|
50
|
+
| `GROK_API_KEY` | xAI / GROK secret key |
|
|
51
|
+
| `OPENAI_API_KEY` | OpenAI secret key |
|
|
52
|
+
| `TASK_MANAGER` | Task manager: `JIRA` (default) or `GITLAB` |
|
|
53
|
+
| `JIRA_URL` | e.g. `https://your-org.atlassian.net` |
|
|
54
|
+
| `JIRA_EMAIL` | Atlassian account e-mail |
|
|
55
|
+
| `JIRA_API_TOKEN` | Atlassian API token |
|
|
56
|
+
| `GITLAB_URL` | e.g. `https://gitlab.com` |
|
|
57
|
+
| `GITLAB_API_TOKEN` | GitLab personal access token |
|
|
43
58
|
|
|
44
59
|
---
|
|
45
60
|
|
|
46
61
|
## Usage
|
|
47
62
|
|
|
48
63
|
```bash
|
|
64
|
+
# Jira
|
|
49
65
|
bob-dev --task_id PROJ-123 --path /path/to/your/repo
|
|
66
|
+
|
|
67
|
+
# GitLab (issue IID)
|
|
68
|
+
bob-dev --task_id 42 --path /path/to/your/repo
|
|
50
69
|
```
|
|
51
70
|
|
|
52
71
|
| Flag | Description |
|
|
53
72
|
|------|-------------|
|
|
54
|
-
| `--task_id` |
|
|
73
|
+
| `--task_id` | Task ID to process — Jira key (`PROJ-123`) or GitLab issue IID (`42`) |
|
|
55
74
|
| `--path` | Path to the target repository (default: current directory) |
|
|
56
75
|
| `--agent` | LLM backend: `GROK` or `OPENAI` (default: value of `AGENT` in `.env`) |
|
|
57
76
|
| `--configure` | Run the interactive configuration wizard |
|
|
@@ -63,12 +82,14 @@ bob-dev --task_id PROJ-123 --path /path/to/your/repo
|
|
|
63
82
|
```
|
|
64
83
|
src/bob_dev/
|
|
65
84
|
├── cli.py # Entry point & main workflow orchestration
|
|
66
|
-
├──
|
|
85
|
+
├── services/
|
|
67
86
|
│ ├── terminal.py # ANSI colours, print helpers, spinner animation
|
|
68
|
-
│ ├──
|
|
69
|
-
│ ├──
|
|
70
|
-
│ ├──
|
|
71
|
-
│
|
|
87
|
+
│ ├── jira.py # Jira API connection + ADF-to-text parsing
|
|
88
|
+
│ ├── gitlab.py # GitLab API connection via python-gitlab
|
|
89
|
+
│ ├── claude.py # Claude Code CLI utilities (agent listing)
|
|
90
|
+
│ ├── llm.py # LLM client, model selection, prompt generation
|
|
91
|
+
│ ├── project.py # Markdown context collection + framework detection
|
|
92
|
+
│ └── config.py # .env management + credential validation
|
|
72
93
|
└── constants/
|
|
73
94
|
└── frameworks.py # Known framework names used for auto-detection
|
|
74
95
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bob-dev"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
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" }]
|
|
@@ -23,6 +23,7 @@ Environment variables (.env next to this file or exported):
|
|
|
23
23
|
JIRA_URL – https://your-org.atlassian.net
|
|
24
24
|
JIRA_EMAIL – Atlassian account e-mail
|
|
25
25
|
JIRA_API_TOKEN – Atlassian API token
|
|
26
|
+
TASK_MANAGER – "JIRA" (default) or "GITLAB"
|
|
26
27
|
"""
|
|
27
28
|
|
|
28
29
|
from __future__ import annotations
|
|
@@ -36,7 +37,7 @@ from pathlib import Path
|
|
|
36
37
|
from InquirerPy import inquirer
|
|
37
38
|
from dotenv import load_dotenv
|
|
38
39
|
|
|
39
|
-
from .
|
|
40
|
+
from .services.terminal import (
|
|
40
41
|
BOLD,
|
|
41
42
|
RESET,
|
|
42
43
|
print_error,
|
|
@@ -47,10 +48,12 @@ from .helpers.terminal import (
|
|
|
47
48
|
run_subprocess,
|
|
48
49
|
run_with_spinner,
|
|
49
50
|
)
|
|
50
|
-
from .
|
|
51
|
-
from .
|
|
52
|
-
from .
|
|
53
|
-
from .
|
|
51
|
+
from .services.jira import get_jira_task
|
|
52
|
+
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
|
|
55
|
+
from .services.project import build_md_context, identify_framework
|
|
56
|
+
from .services.config import check_configuration, update_env_file
|
|
54
57
|
|
|
55
58
|
# ---------------------------------------------------------------------------
|
|
56
59
|
# Module-level configuration
|
|
@@ -58,15 +61,20 @@ from .helpers.config_helper import check_configuration, update_env_file
|
|
|
58
61
|
|
|
59
62
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
60
63
|
load_dotenv(SCRIPT_DIR / ".env")
|
|
64
|
+
ENV_PATH = Path.home() / ".bob_dev" / ".env"
|
|
61
65
|
|
|
62
66
|
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
|
|
63
67
|
GROK_API_KEY = os.environ.get("GROK_API_KEY", "")
|
|
64
68
|
AGENT = os.environ.get("AGENT", "GROK").upper() # "GROK" or "OPENAI"
|
|
69
|
+
TASK_MANAGER = os.environ.get("TASK_MANAGER", "JIRA").upper() # "JIRA" or "GITLAB"
|
|
65
70
|
|
|
66
71
|
JIRA_URL = os.environ.get("JIRA_URL", "")
|
|
67
72
|
JIRA_EMAIL = os.environ.get("JIRA_EMAIL", "")
|
|
68
73
|
JIRA_API_TOKEN = os.environ.get("JIRA_API_TOKEN", "")
|
|
69
74
|
|
|
75
|
+
GITLAB_URL = os.environ.get("GITLAB_URL", "")
|
|
76
|
+
GITLAB_API_TOKEN = os.environ.get("GITLAB_API_TOKEN", "")
|
|
77
|
+
|
|
70
78
|
REPO_BASE_PATH = Path("./") # Overridden by --path at runtime.
|
|
71
79
|
CLAUDE_CODE_CMD = "claude" # Must be on $PATH.
|
|
72
80
|
|
|
@@ -84,7 +92,7 @@ def main() -> None:
|
|
|
84
92
|
)
|
|
85
93
|
parser.add_argument(
|
|
86
94
|
"--task_id", type=str,
|
|
87
|
-
help="
|
|
95
|
+
help="Task ID to process (e.g. PROJ-123 for Jira or 42 for GitLab).",
|
|
88
96
|
)
|
|
89
97
|
parser.add_argument(
|
|
90
98
|
"--path", type=str, default="./",
|
|
@@ -107,17 +115,21 @@ def main() -> None:
|
|
|
107
115
|
|
|
108
116
|
# ── Require task_id for normal workflow ──────────────────────────────────
|
|
109
117
|
if not args.task_id:
|
|
110
|
-
print_error("
|
|
118
|
+
print_error("Task ID is required. Use --task_id PROJ-123 for Jira or 42 for GitLab.")
|
|
111
119
|
sys.exit(1)
|
|
112
120
|
|
|
113
121
|
task_id = args.task_id.strip().upper()
|
|
114
122
|
agent = args.agent.upper()
|
|
115
123
|
|
|
116
124
|
# ── Validate credentials before making any API calls ────────────────────
|
|
117
|
-
if not all([JIRA_URL, JIRA_EMAIL, JIRA_API_TOKEN]):
|
|
125
|
+
if TASK_MANAGER == "JIRA" and not all([JIRA_URL, JIRA_EMAIL, JIRA_API_TOKEN]):
|
|
118
126
|
print_error("Jira credentials are not configured. Run `bob-dev --configure`.")
|
|
119
127
|
sys.exit(1)
|
|
120
128
|
|
|
129
|
+
if TASK_MANAGER == "GITLAB" and not all([GITLAB_URL, GITLAB_API_TOKEN]):
|
|
130
|
+
print_error("GitLab credentials are not configured. Run `bob-dev --configure`.")
|
|
131
|
+
sys.exit(1)
|
|
132
|
+
|
|
121
133
|
if agent == "GROK" and not GROK_API_KEY:
|
|
122
134
|
print_error("GROK_API_KEY is not set. Run `bob-dev --configure`.")
|
|
123
135
|
sys.exit(1)
|
|
@@ -135,18 +147,26 @@ def main() -> None:
|
|
|
135
147
|
sys.exit(1)
|
|
136
148
|
|
|
137
149
|
print_info(f"Project path : {REPO_BASE_PATH}")
|
|
138
|
-
print_info(f"
|
|
150
|
+
print_info(f"{TASK_MANAGER} task ID : {task_id}")
|
|
139
151
|
print_info(f"LLM backend : {agent} ({llm_model(agent)})")
|
|
140
152
|
print()
|
|
141
153
|
|
|
142
|
-
# ── Step 1 – Fetch
|
|
143
|
-
print_step("[1/4]", f"Fetching
|
|
154
|
+
# ── Step 1 – Fetch task ─────────────────────────────────────────────
|
|
155
|
+
print_step("[1/4]", f"Fetching {TASK_MANAGER} task {task_id} …")
|
|
144
156
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
157
|
+
if TASK_MANAGER == "JIRA":
|
|
158
|
+
task = asyncio.run(run_with_spinner(
|
|
159
|
+
get_jira_task,
|
|
160
|
+
task_id, JIRA_URL, JIRA_EMAIL, JIRA_API_TOKEN,
|
|
161
|
+
label="Fetching Jira task",
|
|
162
|
+
))
|
|
163
|
+
|
|
164
|
+
if TASK_MANAGER == "GITLAB":
|
|
165
|
+
task = asyncio.run(run_with_spinner(
|
|
166
|
+
get_gitlab_task,
|
|
167
|
+
task_id, GITLAB_URL, GITLAB_API_TOKEN,
|
|
168
|
+
label="Fetching GitLab task",
|
|
169
|
+
))
|
|
150
170
|
|
|
151
171
|
print_success(f"Title : {task['title']}")
|
|
152
172
|
print_success(f"Fix versions : {', '.join(task['fix_versions']) or 'N/A'}")
|
|
@@ -154,9 +174,10 @@ def main() -> None:
|
|
|
154
174
|
|
|
155
175
|
acceptance_criteria = task["description"]
|
|
156
176
|
if not acceptance_criteria.strip():
|
|
157
|
-
print_error("The
|
|
177
|
+
print_error(f"The {TASK_MANAGER} task has no description / acceptance criteria.")
|
|
158
178
|
sys.exit(1)
|
|
159
179
|
|
|
180
|
+
|
|
160
181
|
# ── Step 2 – Read project docs & generate prompt ─────────────────────────
|
|
161
182
|
print_step("[2/4]", f"Generating Claude Code prompt via {agent} ({llm_model(agent)}) …")
|
|
162
183
|
|
|
@@ -211,17 +232,35 @@ def main() -> None:
|
|
|
211
232
|
print_info(f"Aborted. Prompt saved to {prompt_file}")
|
|
212
233
|
sys.exit(0)
|
|
213
234
|
|
|
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()
|
|
252
|
+
|
|
214
253
|
# ── Step 4 – Pass prompt to Claude Code ──────────────────────────────────
|
|
215
254
|
print_step("[4/4]", "Passing prompt to Claude Code …")
|
|
216
255
|
print()
|
|
217
|
-
asyncio.run(_pass_to_claude_code(prompt_md, task_id))
|
|
256
|
+
asyncio.run(_pass_to_claude_code(prompt_md, task_id, agent_claude))
|
|
218
257
|
|
|
219
258
|
|
|
220
259
|
# ---------------------------------------------------------------------------
|
|
221
260
|
# Internal helpers
|
|
222
261
|
# ---------------------------------------------------------------------------
|
|
223
262
|
|
|
224
|
-
async def _pass_to_claude_code(prompt_md: str, task_id: str) -> None:
|
|
263
|
+
async def _pass_to_claude_code(prompt_md: str, task_id: str, agent_claude: str=None) -> None:
|
|
225
264
|
"""Write the prompt to a temp file, then run Claude Code non-interactively."""
|
|
226
265
|
|
|
227
266
|
# Persist the prompt so the user can review it regardless of outcome.
|
|
@@ -231,6 +270,8 @@ async def _pass_to_claude_code(prompt_md: str, task_id: str) -> None:
|
|
|
231
270
|
print_info(f"Prompt saved to: {prompt_file}")
|
|
232
271
|
|
|
233
272
|
cmd = [CLAUDE_CODE_CMD, "--dangerously-skip-permissions", "--print", prompt_md]
|
|
273
|
+
if agent_claude:
|
|
274
|
+
cmd.extend(["--agent", agent_claude])
|
|
234
275
|
print_info(f"Running: {' '.join(cmd[:2])} <prompt>")
|
|
235
276
|
print()
|
|
236
277
|
|
|
@@ -242,7 +283,9 @@ async def _pass_to_claude_code(prompt_md: str, task_id: str) -> None:
|
|
|
242
283
|
|
|
243
284
|
def _run_configure() -> None:
|
|
244
285
|
"""Interactive wizard to write API keys and Jira credentials to .env."""
|
|
245
|
-
env_path =
|
|
286
|
+
env_path = ENV_PATH
|
|
287
|
+
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
288
|
+
print(f"Configuration file will be saved to: {env_path}")
|
|
246
289
|
print_step("[CONFIGURE]", "Running initial configuration …")
|
|
247
290
|
|
|
248
291
|
# Choose the LLM backend.
|
|
@@ -258,20 +301,40 @@ def _run_configure() -> None:
|
|
|
258
301
|
update_env_file(env_key, api_key, env_path)
|
|
259
302
|
print_success(f"{system_choice} API key saved.")
|
|
260
303
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
304
|
+
print("\n Tasks manager configuration:")
|
|
305
|
+
task_manager = inquirer.select(
|
|
306
|
+
message="Select the default task manager:",
|
|
307
|
+
choices=["JIRA", "GITLAB"],
|
|
308
|
+
).execute()
|
|
309
|
+
update_env_file("TASK_MANAGER", task_manager, env_path)
|
|
310
|
+
print_success(f"Task manager set to {task_manager}.")
|
|
311
|
+
|
|
312
|
+
print("\nNote: The rest of the configuration depends on the selected task manager. You can run `bob-dev --configure` again to set it up later.")
|
|
313
|
+
|
|
314
|
+
if task_manager == "JIRA":
|
|
315
|
+
# Jira credentials.
|
|
316
|
+
print("\nJira configuration:")
|
|
317
|
+
jira_url = input("JIRA_URL (e.g. https://your-org.atlassian.net): ").strip()
|
|
318
|
+
jira_email = input("JIRA_EMAIL (your Atlassian account e-mail) : ").strip()
|
|
319
|
+
jira_token = input("JIRA_API_TOKEN (Atlassian API token) : ").strip()
|
|
320
|
+
for key, val in [
|
|
321
|
+
("JIRA_URL", jira_url),
|
|
322
|
+
("JIRA_EMAIL", jira_email),
|
|
323
|
+
("JIRA_API_TOKEN", jira_token),
|
|
324
|
+
]:
|
|
325
|
+
update_env_file(key, val, env_path)
|
|
326
|
+
if task_manager == "GITLAB":
|
|
327
|
+
# GitLab credentials.
|
|
328
|
+
print("\nGitLab configuration:")
|
|
329
|
+
gitlab_url = input("GITLAB_URL (e.g. https://gitlab.com) : ").strip()
|
|
330
|
+
gitlab_token = input("GITLAB_API_TOKEN (GitLab API token) : ").strip()
|
|
331
|
+
for key, val in [
|
|
332
|
+
("GITLAB_URL", gitlab_url),
|
|
333
|
+
("GITLAB_API_TOKEN", gitlab_token),
|
|
334
|
+
]:
|
|
335
|
+
update_env_file(key, val, env_path)
|
|
336
|
+
|
|
337
|
+
print_success(f"{task_manager} configuration saved.")
|
|
275
338
|
|
|
276
339
|
# Claude Code API key.
|
|
277
340
|
print("\nClaude Code API key:")
|
|
@@ -286,6 +349,9 @@ def _run_configure() -> None:
|
|
|
286
349
|
agent = os.environ.get("AGENT", "GROK").upper(),
|
|
287
350
|
grok_api_key = os.environ.get("GROK_API_KEY", ""),
|
|
288
351
|
openai_api_key = os.environ.get("OPENAI_API_KEY", ""),
|
|
352
|
+
gitlab_url = os.environ.get("GITLAB_URL", ""),
|
|
353
|
+
gitlab_api_token = os.environ.get("GITLAB_API_TOKEN", ""),
|
|
354
|
+
task_manager = os.environ.get("TASK_MANAGER", "JIRA").upper(),
|
|
289
355
|
jira_url = jira_url,
|
|
290
356
|
jira_email = jira_email,
|
|
291
357
|
jira_api_token = jira_token,
|
|
@@ -0,0 +1,27 @@
|
|
|
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 []
|
bob_dev-0.1.0/src/bob_dev/helpers/config_helper.py → bob_dev-0.2.1/src/bob_dev/services/config.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""config.py
|
|
2
2
|
|
|
3
3
|
Configuration utilities for bob_dev:
|
|
4
4
|
- Write / update key-value pairs in the .env file.
|
|
@@ -53,6 +53,9 @@ def check_configuration(
|
|
|
53
53
|
agent: str,
|
|
54
54
|
grok_api_key: str,
|
|
55
55
|
openai_api_key: str,
|
|
56
|
+
gitlab_url: str,
|
|
57
|
+
gitlab_api_token: str,
|
|
58
|
+
task_manager: str,
|
|
56
59
|
jira_url: str,
|
|
57
60
|
jira_email: str,
|
|
58
61
|
jira_api_token: str,
|
|
@@ -65,7 +68,7 @@ def check_configuration(
|
|
|
65
68
|
# ── LLM API key ───────────────────────────────────────────────────────
|
|
66
69
|
print_info(f"Checking {agent} API key …")
|
|
67
70
|
try:
|
|
68
|
-
from .
|
|
71
|
+
from .llm import build_llm_client, llm_model
|
|
69
72
|
|
|
70
73
|
client = build_llm_client(agent, grok_api_key, openai_api_key)
|
|
71
74
|
model = llm_model(agent)
|
|
@@ -79,16 +82,29 @@ def check_configuration(
|
|
|
79
82
|
except Exception as exc:
|
|
80
83
|
print_error(f"Failed to connect to {agent} API: {exc}")
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
if task_manager == "GITLAB":
|
|
86
|
+
# ── GitLab credentials ───────────────────────────────────────────────
|
|
87
|
+
print_info("Checking GitLab credentials …")
|
|
88
|
+
try:
|
|
89
|
+
from gitlab import Gitlab
|
|
90
|
+
|
|
91
|
+
gl = Gitlab(url=gitlab_url, private_token=gitlab_api_token)
|
|
92
|
+
user = gl.user
|
|
93
|
+
print_success(f"GitLab credentials OK. Authenticated as: {user['name']}")
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
print_error(f"Failed to connect to GitLab: {exc}")
|
|
96
|
+
|
|
97
|
+
if task_manager == "JIRA":
|
|
98
|
+
# ── Jira credentials ──────────────────────────────────────────────────
|
|
99
|
+
print_info("Checking Jira credentials …")
|
|
100
|
+
try:
|
|
101
|
+
from atlassian import Jira
|
|
102
|
+
|
|
103
|
+
jira = Jira(url=jira_url, username=jira_email, password=jira_api_token, cloud=True)
|
|
104
|
+
user = jira.myself()
|
|
105
|
+
print_success(f"Jira credentials OK. Authenticated as: {user.get('displayName')}")
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
print_error(f"Failed to connect to Jira: {exc}")
|
|
92
108
|
|
|
93
109
|
# ── Claude Code CLI ───────────────────────────────────────────────────
|
|
94
110
|
print_info(f"Checking Claude Code CLI ({claude_cmd}) …")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""gitlab.py
|
|
2
|
+
|
|
3
|
+
GitLab integration utilities for bob_dev:
|
|
4
|
+
- Fetch a GitLab issue and normalise its fields into a plain dict.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import gitlab
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_gitlab_task(
|
|
13
|
+
task_id: int | str,
|
|
14
|
+
gitlab_url: str,
|
|
15
|
+
gitlab_token: str,
|
|
16
|
+
project_id: int | str,
|
|
17
|
+
) -> dict:
|
|
18
|
+
"""Connect to a GitLab instance and return normalised fields for *task_id*.
|
|
19
|
+
|
|
20
|
+
*task_id* is the issue IID (the number shown in the GitLab UI, e.g. #42).
|
|
21
|
+
*project_id* can be the numeric ID or the path namespace, e.g. "group/repo".
|
|
22
|
+
|
|
23
|
+
Returns a dict with keys:
|
|
24
|
+
task_id (str)
|
|
25
|
+
title (str)
|
|
26
|
+
description (str)
|
|
27
|
+
fix_versions (list[str] – milestone title wrapped in a list, or empty)
|
|
28
|
+
"""
|
|
29
|
+
gl = gitlab.Gitlab(url=gitlab_url, private_token=gitlab_token)
|
|
30
|
+
|
|
31
|
+
project = gl.projects.get(project_id)
|
|
32
|
+
issue = project.issues.get(int(task_id))
|
|
33
|
+
|
|
34
|
+
milestone = issue.milestone or {}
|
|
35
|
+
fix_versions = [milestone["title"]] if milestone.get("title") else []
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
"task_id": str(task_id),
|
|
39
|
+
"title": issue.title or "",
|
|
40
|
+
"description": issue.description or "",
|
|
41
|
+
"fix_versions": fix_versions,
|
|
42
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bob-dev
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: An AI developer to write code for you
|
|
5
5
|
Author-email: Samuel Pereira dos Santos <samuelsantosdev@gmail.com>
|
|
6
6
|
Requires-Python: >=3.14
|
|
@@ -17,21 +17,22 @@ Dynamic: license-file
|
|
|
17
17
|
|
|
18
18
|

|
|
19
19
|
|
|
20
|
-
Given a
|
|
20
|
+
Given a task ID (Jira or GitLab) it will:
|
|
21
21
|
|
|
22
|
-
1. **Fetch** the task title, description, and fix versions from Jira.
|
|
22
|
+
1. **Fetch** the task title, description, and fix versions from Jira or GitLab.
|
|
23
23
|
2. **Read** your project's Markdown documentation to build rich LLM context.
|
|
24
24
|
3. **Generate** a precise Claude Code prompt (via GROK or OpenAI), including project-framework context, implementation steps, and test scenarios.
|
|
25
25
|
4. **Analyse** the prompt for ambiguities and security concerns.
|
|
26
|
-
5. **
|
|
26
|
+
5. **Select** (optionally) a Claude Code agent to run the implementation.
|
|
27
|
+
6. **Execute** the prompt with the Claude Code CLI (optional – you can review first).
|
|
27
28
|
|
|
28
29
|
---
|
|
29
30
|
|
|
30
31
|
## Requirements
|
|
31
32
|
|
|
32
|
-
- Python 3.
|
|
33
|
+
- Python 3.14+
|
|
33
34
|
- [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
|
+
- 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)
|
|
35
36
|
- An [xAI / GROK](https://console.x.ai/) **or** [OpenAI](https://platform.openai.com/) API key
|
|
36
37
|
|
|
37
38
|
---
|
|
@@ -52,19 +53,37 @@ Run the interactive setup wizard the first time:
|
|
|
52
53
|
bob-dev --configure
|
|
53
54
|
```
|
|
54
55
|
|
|
55
|
-
This will prompt for your API
|
|
56
|
+
This will prompt for your LLM API key, task manager (Jira or GitLab) credentials, and Claude Code API key.
|
|
57
|
+
|
|
58
|
+
### Environment variables
|
|
59
|
+
|
|
60
|
+
| Variable | Description |
|
|
61
|
+
|----------|-------------|
|
|
62
|
+
| `AGENT` | LLM backend: `GROK` (default) or `OPENAI` |
|
|
63
|
+
| `GROK_API_KEY` | xAI / GROK secret key |
|
|
64
|
+
| `OPENAI_API_KEY` | OpenAI secret key |
|
|
65
|
+
| `TASK_MANAGER` | Task manager: `JIRA` (default) or `GITLAB` |
|
|
66
|
+
| `JIRA_URL` | e.g. `https://your-org.atlassian.net` |
|
|
67
|
+
| `JIRA_EMAIL` | Atlassian account e-mail |
|
|
68
|
+
| `JIRA_API_TOKEN` | Atlassian API token |
|
|
69
|
+
| `GITLAB_URL` | e.g. `https://gitlab.com` |
|
|
70
|
+
| `GITLAB_API_TOKEN` | GitLab personal access token |
|
|
56
71
|
|
|
57
72
|
---
|
|
58
73
|
|
|
59
74
|
## Usage
|
|
60
75
|
|
|
61
76
|
```bash
|
|
77
|
+
# Jira
|
|
62
78
|
bob-dev --task_id PROJ-123 --path /path/to/your/repo
|
|
79
|
+
|
|
80
|
+
# GitLab (issue IID)
|
|
81
|
+
bob-dev --task_id 42 --path /path/to/your/repo
|
|
63
82
|
```
|
|
64
83
|
|
|
65
84
|
| Flag | Description |
|
|
66
85
|
|------|-------------|
|
|
67
|
-
| `--task_id` |
|
|
86
|
+
| `--task_id` | Task ID to process — Jira key (`PROJ-123`) or GitLab issue IID (`42`) |
|
|
68
87
|
| `--path` | Path to the target repository (default: current directory) |
|
|
69
88
|
| `--agent` | LLM backend: `GROK` or `OPENAI` (default: value of `AGENT` in `.env`) |
|
|
70
89
|
| `--configure` | Run the interactive configuration wizard |
|
|
@@ -76,12 +95,14 @@ bob-dev --task_id PROJ-123 --path /path/to/your/repo
|
|
|
76
95
|
```
|
|
77
96
|
src/bob_dev/
|
|
78
97
|
├── cli.py # Entry point & main workflow orchestration
|
|
79
|
-
├──
|
|
98
|
+
├── services/
|
|
80
99
|
│ ├── terminal.py # ANSI colours, print helpers, spinner animation
|
|
81
|
-
│ ├──
|
|
82
|
-
│ ├──
|
|
83
|
-
│ ├──
|
|
84
|
-
│
|
|
100
|
+
│ ├── jira.py # Jira API connection + ADF-to-text parsing
|
|
101
|
+
│ ├── gitlab.py # GitLab API connection via python-gitlab
|
|
102
|
+
│ ├── claude.py # Claude Code CLI utilities (agent listing)
|
|
103
|
+
│ ├── llm.py # LLM client, model selection, prompt generation
|
|
104
|
+
│ ├── project.py # Markdown context collection + framework detection
|
|
105
|
+
│ └── config.py # .env management + credential validation
|
|
85
106
|
└── constants/
|
|
86
107
|
└── frameworks.py # Known framework names used for auto-detection
|
|
87
108
|
```
|
|
@@ -12,15 +12,17 @@ src/bob_dev.egg-info/requires.txt
|
|
|
12
12
|
src/bob_dev.egg-info/top_level.txt
|
|
13
13
|
src/bob_dev/constants/__init__.py
|
|
14
14
|
src/bob_dev/constants/frameworks.py
|
|
15
|
-
src/bob_dev/
|
|
16
|
-
src/bob_dev/
|
|
17
|
-
src/bob_dev/
|
|
18
|
-
src/bob_dev/
|
|
19
|
-
src/bob_dev/
|
|
20
|
-
src/bob_dev/
|
|
15
|
+
src/bob_dev/services/__init__.py
|
|
16
|
+
src/bob_dev/services/claude.py
|
|
17
|
+
src/bob_dev/services/config.py
|
|
18
|
+
src/bob_dev/services/gitlab.py
|
|
19
|
+
src/bob_dev/services/jira.py
|
|
20
|
+
src/bob_dev/services/llm.py
|
|
21
|
+
src/bob_dev/services/project.py
|
|
22
|
+
src/bob_dev/services/terminal.py
|
|
21
23
|
tests/test_cli.py
|
|
22
|
-
tests/
|
|
23
|
-
tests/
|
|
24
|
-
tests/
|
|
25
|
-
tests/
|
|
24
|
+
tests/test_config.py
|
|
25
|
+
tests/test_jira.py
|
|
26
|
+
tests/test_llm.py
|
|
27
|
+
tests/test_project.py
|
|
26
28
|
tests/test_terminal.py
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tests for bob_dev.
|
|
1
|
+
"""Tests for bob_dev.services.config."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ import os
|
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
|
-
from bob_dev.
|
|
9
|
+
from bob_dev.services.config import update_env_file
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class TestUpdateEnvFile:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tests for bob_dev.
|
|
1
|
+
"""Tests for bob_dev.services.jira."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
|
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
|
-
from bob_dev.
|
|
9
|
+
from bob_dev.services.jira import _adf_to_text, get_jira_task
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class TestAdfToText:
|
|
@@ -128,32 +128,32 @@ class TestGetJiraTask:
|
|
|
128
128
|
}
|
|
129
129
|
return mock_jira
|
|
130
130
|
|
|
131
|
-
@patch("bob_dev.
|
|
131
|
+
@patch("bob_dev.services.jira.Jira")
|
|
132
132
|
def test_returns_required_keys(self, mock_jira_cls):
|
|
133
133
|
mock_jira_cls.return_value = self._build_mock_jira()
|
|
134
134
|
result = get_jira_task("PROJ-1", "https://example.atlassian.net", "u@e.com", "tok")
|
|
135
135
|
for key in ("task_id", "title", "description", "fix_versions"):
|
|
136
136
|
assert key in result
|
|
137
137
|
|
|
138
|
-
@patch("bob_dev.
|
|
138
|
+
@patch("bob_dev.services.jira.Jira")
|
|
139
139
|
def test_task_id_preserved(self, mock_jira_cls):
|
|
140
140
|
mock_jira_cls.return_value = self._build_mock_jira()
|
|
141
141
|
result = get_jira_task("PROJ-42", "https://example.atlassian.net", "u@e.com", "tok")
|
|
142
142
|
assert result["task_id"] == "PROJ-42"
|
|
143
143
|
|
|
144
|
-
@patch("bob_dev.
|
|
144
|
+
@patch("bob_dev.services.jira.Jira")
|
|
145
145
|
def test_title_extracted(self, mock_jira_cls):
|
|
146
146
|
mock_jira_cls.return_value = self._build_mock_jira(summary="My Task Title")
|
|
147
147
|
result = get_jira_task("PROJ-1", "https://example.atlassian.net", "u@e.com", "tok")
|
|
148
148
|
assert result["title"] == "My Task Title"
|
|
149
149
|
|
|
150
|
-
@patch("bob_dev.
|
|
150
|
+
@patch("bob_dev.services.jira.Jira")
|
|
151
151
|
def test_plain_text_description_passed_through(self, mock_jira_cls):
|
|
152
152
|
mock_jira_cls.return_value = self._build_mock_jira(description="Plain description")
|
|
153
153
|
result = get_jira_task("PROJ-1", "https://example.atlassian.net", "u@e.com", "tok")
|
|
154
154
|
assert result["description"] == "Plain description"
|
|
155
155
|
|
|
156
|
-
@patch("bob_dev.
|
|
156
|
+
@patch("bob_dev.services.jira.Jira")
|
|
157
157
|
def test_adf_description_parsed_to_text(self, mock_jira_cls):
|
|
158
158
|
adf = {
|
|
159
159
|
"type": "doc",
|
|
@@ -168,7 +168,7 @@ class TestGetJiraTask:
|
|
|
168
168
|
result = get_jira_task("PROJ-1", "https://example.atlassian.net", "u@e.com", "tok")
|
|
169
169
|
assert "ADF content" in result["description"]
|
|
170
170
|
|
|
171
|
-
@patch("bob_dev.
|
|
171
|
+
@patch("bob_dev.services.jira.Jira")
|
|
172
172
|
def test_fix_versions_extracted(self, mock_jira_cls):
|
|
173
173
|
mock_jira_cls.return_value = self._build_mock_jira(
|
|
174
174
|
fix_versions=[{"name": "v1.0"}, {"name": "v1.1"}]
|
|
@@ -176,13 +176,13 @@ class TestGetJiraTask:
|
|
|
176
176
|
result = get_jira_task("PROJ-1", "https://example.atlassian.net", "u@e.com", "tok")
|
|
177
177
|
assert result["fix_versions"] == ["v1.0", "v1.1"]
|
|
178
178
|
|
|
179
|
-
@patch("bob_dev.
|
|
179
|
+
@patch("bob_dev.services.jira.Jira")
|
|
180
180
|
def test_empty_fix_versions(self, mock_jira_cls):
|
|
181
181
|
mock_jira_cls.return_value = self._build_mock_jira(fix_versions=[])
|
|
182
182
|
result = get_jira_task("PROJ-1", "https://example.atlassian.net", "u@e.com", "tok")
|
|
183
183
|
assert result["fix_versions"] == []
|
|
184
184
|
|
|
185
|
-
@patch("bob_dev.
|
|
185
|
+
@patch("bob_dev.services.jira.Jira")
|
|
186
186
|
def test_none_description_becomes_empty_string(self, mock_jira_cls):
|
|
187
187
|
mock_jira = MagicMock()
|
|
188
188
|
mock_jira.issue.return_value = {
|
|
@@ -196,7 +196,7 @@ class TestGetJiraTask:
|
|
|
196
196
|
result = get_jira_task("PROJ-1", "https://example.atlassian.net", "u@e.com", "tok")
|
|
197
197
|
assert result["description"] == ""
|
|
198
198
|
|
|
199
|
-
@patch("bob_dev.
|
|
199
|
+
@patch("bob_dev.services.jira.Jira")
|
|
200
200
|
def test_jira_instantiated_with_cloud_true(self, mock_jira_cls):
|
|
201
201
|
mock_jira_cls.return_value = self._build_mock_jira()
|
|
202
202
|
get_jira_task("PROJ-1", "https://org.atlassian.net", "user@test.com", "token123")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tests for bob_dev.
|
|
1
|
+
"""Tests for bob_dev.services.llm."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
|
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
|
-
from bob_dev.
|
|
9
|
+
from bob_dev.services.llm import (
|
|
10
10
|
analyse_prompt,
|
|
11
11
|
build_llm_client,
|
|
12
12
|
llm_model,
|
|
@@ -35,25 +35,25 @@ class TestBuildLlmClient:
|
|
|
35
35
|
with pytest.raises(EnvironmentError, match="OPENAI_API_KEY"):
|
|
36
36
|
build_llm_client("OPENAI", "", "")
|
|
37
37
|
|
|
38
|
-
@patch("bob_dev.
|
|
38
|
+
@patch("bob_dev.services.llm.OpenAI")
|
|
39
39
|
def test_grok_sets_xai_base_url(self, mock_openai_cls):
|
|
40
40
|
build_llm_client("GROK", "grok-key-123", "")
|
|
41
41
|
_, kwargs = mock_openai_cls.call_args
|
|
42
42
|
assert "x.ai" in kwargs.get("base_url", "")
|
|
43
43
|
|
|
44
|
-
@patch("bob_dev.
|
|
44
|
+
@patch("bob_dev.services.llm.OpenAI")
|
|
45
45
|
def test_grok_passes_api_key(self, mock_openai_cls):
|
|
46
46
|
build_llm_client("GROK", "my-grok-key", "")
|
|
47
47
|
_, kwargs = mock_openai_cls.call_args
|
|
48
48
|
assert kwargs.get("api_key") == "my-grok-key"
|
|
49
49
|
|
|
50
|
-
@patch("bob_dev.
|
|
50
|
+
@patch("bob_dev.services.llm.OpenAI")
|
|
51
51
|
def test_openai_no_base_url(self, mock_openai_cls):
|
|
52
52
|
build_llm_client("OPENAI", "", "openai-key-123")
|
|
53
53
|
_, kwargs = mock_openai_cls.call_args
|
|
54
54
|
assert "base_url" not in kwargs
|
|
55
55
|
|
|
56
|
-
@patch("bob_dev.
|
|
56
|
+
@patch("bob_dev.services.llm.OpenAI")
|
|
57
57
|
def test_openai_passes_api_key(self, mock_openai_cls):
|
|
58
58
|
build_llm_client("OPENAI", "", "openai-secret")
|
|
59
59
|
_, kwargs = mock_openai_cls.call_args
|
|
@@ -70,7 +70,7 @@ def _make_mock_client(content: str) -> MagicMock:
|
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
class TestPromptClaudeCode:
|
|
73
|
-
@patch("bob_dev.
|
|
73
|
+
@patch("bob_dev.services.llm.build_llm_client")
|
|
74
74
|
def test_returns_llm_content(self, mock_build):
|
|
75
75
|
mock_build.return_value = _make_mock_client("My generated prompt")
|
|
76
76
|
result = prompt_claude_code(
|
|
@@ -83,20 +83,20 @@ class TestPromptClaudeCode:
|
|
|
83
83
|
)
|
|
84
84
|
assert result == "My generated prompt"
|
|
85
85
|
|
|
86
|
-
@patch("bob_dev.
|
|
86
|
+
@patch("bob_dev.services.llm.build_llm_client")
|
|
87
87
|
def test_returns_empty_string_when_content_is_none(self, mock_build):
|
|
88
88
|
mock_build.return_value = _make_mock_client(None)
|
|
89
89
|
result = prompt_claude_code("AC", "docs", "Django", "GROK", "key", "")
|
|
90
90
|
assert result == ""
|
|
91
91
|
|
|
92
|
-
@patch("bob_dev.
|
|
92
|
+
@patch("bob_dev.services.llm.build_llm_client")
|
|
93
93
|
def test_calls_create_with_temperature(self, mock_build):
|
|
94
94
|
mock_build.return_value = _make_mock_client("ok")
|
|
95
95
|
prompt_claude_code("AC", "docs", "FastAPI", "OPENAI", "", "key")
|
|
96
96
|
create_kwargs = mock_build.return_value.chat.completions.create.call_args[1]
|
|
97
97
|
assert "temperature" in create_kwargs
|
|
98
98
|
|
|
99
|
-
@patch("bob_dev.
|
|
99
|
+
@patch("bob_dev.services.llm.build_llm_client")
|
|
100
100
|
def test_task_meta_injected_into_user_message(self, mock_build):
|
|
101
101
|
mock_client = _make_mock_client("prompt with meta")
|
|
102
102
|
mock_build.return_value = mock_client
|
|
@@ -115,14 +115,14 @@ class TestPromptClaudeCode:
|
|
|
115
115
|
assert "PROJ-1" in user_content
|
|
116
116
|
assert "Test task" in user_content
|
|
117
117
|
|
|
118
|
-
@patch("bob_dev.
|
|
118
|
+
@patch("bob_dev.services.llm.build_llm_client")
|
|
119
119
|
def test_task_meta_none_does_not_raise(self, mock_build):
|
|
120
120
|
mock_build.return_value = _make_mock_client("ok")
|
|
121
121
|
# Should not raise when task_meta is omitted.
|
|
122
122
|
result = prompt_claude_code("AC", "docs", "Django", "GROK", "key", "")
|
|
123
123
|
assert isinstance(result, str)
|
|
124
124
|
|
|
125
|
-
@patch("bob_dev.
|
|
125
|
+
@patch("bob_dev.services.llm.build_llm_client")
|
|
126
126
|
def test_framework_included_in_system_prompt(self, mock_build):
|
|
127
127
|
mock_client = _make_mock_client("ok")
|
|
128
128
|
mock_build.return_value = mock_client
|
|
@@ -133,19 +133,19 @@ class TestPromptClaudeCode:
|
|
|
133
133
|
|
|
134
134
|
|
|
135
135
|
class TestAnalysePrompt:
|
|
136
|
-
@patch("bob_dev.
|
|
136
|
+
@patch("bob_dev.services.llm.build_llm_client")
|
|
137
137
|
def test_returns_analysis_string(self, mock_build):
|
|
138
138
|
mock_build.return_value = _make_mock_client("- No issues found")
|
|
139
139
|
result = analyse_prompt("prompt md", "AC text", "GROK", "key", "")
|
|
140
140
|
assert result == "- No issues found"
|
|
141
141
|
|
|
142
|
-
@patch("bob_dev.
|
|
142
|
+
@patch("bob_dev.services.llm.build_llm_client")
|
|
143
143
|
def test_returns_empty_string_when_content_is_none(self, mock_build):
|
|
144
144
|
mock_build.return_value = _make_mock_client(None)
|
|
145
145
|
result = analyse_prompt("prompt", "AC", "GROK", "key", "")
|
|
146
146
|
assert result == ""
|
|
147
147
|
|
|
148
|
-
@patch("bob_dev.
|
|
148
|
+
@patch("bob_dev.services.llm.build_llm_client")
|
|
149
149
|
def test_passes_prompt_and_ac_to_user_message(self, mock_build):
|
|
150
150
|
mock_client = _make_mock_client("analysis")
|
|
151
151
|
mock_build.return_value = mock_client
|
|
@@ -155,7 +155,7 @@ class TestAnalysePrompt:
|
|
|
155
155
|
assert "the prompt content" in user_content
|
|
156
156
|
assert "the AC content" in user_content
|
|
157
157
|
|
|
158
|
-
@patch("bob_dev.
|
|
158
|
+
@patch("bob_dev.services.llm.build_llm_client")
|
|
159
159
|
def test_calls_create_with_temperature(self, mock_build):
|
|
160
160
|
mock_build.return_value = _make_mock_client("ok")
|
|
161
161
|
analyse_prompt("p", "ac", "GROK", "key", "")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tests for bob_dev.
|
|
1
|
+
"""Tests for bob_dev.services.project."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ from unittest.mock import patch
|
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
10
|
-
from bob_dev.
|
|
10
|
+
from bob_dev.services.project import (
|
|
11
11
|
build_md_context,
|
|
12
12
|
collect_md_context,
|
|
13
13
|
identify_framework,
|
|
@@ -102,14 +102,14 @@ class TestBuildMdContext:
|
|
|
102
102
|
result = build_md_context(tmp_path)
|
|
103
103
|
assert "API documentation" in result
|
|
104
104
|
|
|
105
|
-
@patch("bob_dev.
|
|
105
|
+
@patch("bob_dev.services.project.summarize")
|
|
106
106
|
def test_readme_summary_included(self, mock_summarize, tmp_path):
|
|
107
107
|
(tmp_path / "README.md").write_text("# Django REST Framework")
|
|
108
108
|
mock_summarize.return_value = "Summary text"
|
|
109
109
|
result = build_md_context(tmp_path)
|
|
110
110
|
assert "Summary text" in result
|
|
111
111
|
|
|
112
|
-
@patch("bob_dev.
|
|
112
|
+
@patch("bob_dev.services.project.summarize")
|
|
113
113
|
def test_summarize_called_with_readme_content(self, mock_summarize, tmp_path):
|
|
114
114
|
(tmp_path / "README.md").write_text("readme body")
|
|
115
115
|
mock_summarize.return_value = ""
|
|
@@ -119,7 +119,7 @@ class TestBuildMdContext:
|
|
|
119
119
|
assert "readme body" in args[0]
|
|
120
120
|
|
|
121
121
|
def test_empty_readme_does_not_call_summarize(self, tmp_path):
|
|
122
|
-
with patch("bob_dev.
|
|
122
|
+
with patch("bob_dev.services.project.summarize") as mock_summarize:
|
|
123
123
|
build_md_context(tmp_path)
|
|
124
124
|
mock_summarize.assert_not_called()
|
|
125
125
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tests for bob_dev.
|
|
1
|
+
"""Tests for bob_dev.services.terminal."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ import asyncio
|
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
|
-
from bob_dev.
|
|
9
|
+
from bob_dev.services.terminal import (
|
|
10
10
|
BOLD,
|
|
11
11
|
GREEN,
|
|
12
12
|
RED,
|
|
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
|