From d28d8624a9fe5ef3ea165c431e23a3ff1baca77d Mon Sep 17 00:00:00 2001 From: John Lambert Date: Fri, 21 Aug 2026 11:35:34 -0400 Subject: [PATCH 1/4] Fix eight unusable modules in atlassian-readonly-skills confluence_comments, confluence_labels, confluence_pages, jira_agile, jira_links, jira_projects, jira_workflow and jira_worklog all raised NameError on import: Optional or AtlassianCredentials was referenced by a surviving function signature but missing from the import lines. Eight of the twelve read-only modules were therefore unreachable, and callers had to fall back to the write skill for reads. The read-only variant looks generated from the write variant by stripping write functions, with the stripper also pruning those two names. The fix restores them and nothing else. It is an upstream bug and a re-sync will reintroduce it until reported. Verified: all twelve modules in both skills now import, and jira_get_transitions returns live data through the read-only skill. Both skills gain a PROVENANCE.md recording the upstream repository, the MIT declaration, the local modifications, and the Data Center behaviours that upstream's Cloud-oriented docstrings get wrong. Upstream declares MIT in its README but ships no LICENSE file and no copyright line, so the frontmatter reference to LICENSE dangles there as well as here; it is left unchanged to keep this copy diffable. Co-Authored-By: Claude Opus 5 --- .../atlassian-readonly-skills/PROVENANCE.md | 46 ++++++++++++++++ .../scripts/confluence_comments.py | 3 +- .../scripts/confluence_labels.py | 3 +- .../scripts/confluence_pages.py | 1 + .../scripts/jira_agile.py | 1 + .../scripts/jira_links.py | 3 +- .../scripts/jira_projects.py | 3 +- .../scripts/jira_workflow.py | 3 +- .../scripts/jira_worklog.py | 3 +- .claude/skills/atlassian-skills/PROVENANCE.md | 52 +++++++++++++++++++ 10 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 .claude/skills/atlassian-readonly-skills/PROVENANCE.md create mode 100644 .claude/skills/atlassian-skills/PROVENANCE.md diff --git a/.claude/skills/atlassian-readonly-skills/PROVENANCE.md b/.claude/skills/atlassian-readonly-skills/PROVENANCE.md new file mode 100644 index 0000000000..ee7703f640 --- /dev/null +++ b/.claude/skills/atlassian-readonly-skills/PROVENANCE.md @@ -0,0 +1,46 @@ +# Provenance + +Vendored from **https://github.com/langpingxue/atlassian-skills** +(`atlassian-readonly-skills/`), first committed here in d1a9bc66d. + +## License + +Upstream's README declares **MIT License**. Note two gaps in that declaration, +recorded here so nobody has to rediscover them: + +- **Upstream ships no `LICENSE` file.** GitHub's license endpoint returns 404 + for the repository, and there is no copyright line anywhere in it. +- `SKILL.md`'s frontmatter says `license: Complete terms in LICENSE`, which is + therefore a dangling reference upstream as well as here. It is left unchanged + so this copy stays diffable against upstream. + +MIT permits use and modification; the attribution it asks for is this file. + +## Local modifications + +Keep these few and listed, so re-syncing upstream stays possible. + +1. **`SKILL.md`** gains a leading `## FieldWorks / SIL JIRA Integration` + section (~39 lines) covering `jira.sil.org`, the `LT` project key and the + Data Center specifics. Everything below it matches upstream. +2. **`scripts/*.py` import fix.** Eight modules were unusable: they raised + `NameError: name 'Optional' is not defined` or + `NameError: name 'AtlassianCredentials' is not defined` on import, because + the read-only variant appears to be generated from the write variant by + stripping write functions, and the stripper also pruned those two names + from the import lines while the surviving signatures still referenced them. + + Affected: `confluence_comments`, `confluence_labels`, `confluence_pages`, + `jira_agile`, `jira_links`, `jira_projects`, `jira_workflow`, + `jira_worklog`. The fix restores the pruned names, nothing else. + + **This is an upstream bug and should be reported there.** Until it is fixed + upstream, a re-sync will reintroduce it. + +## Deliberately not done + +This skill has **not** been compressed or restructured, unlike the +FieldWorks-owned skills. It is ~560 lines of API reference that loads only +when Atlassian work happens, and keeping it close to upstream is worth more +than the context saving. The near-total duplication between this variant and +`atlassian-skills` is upstream's design, not something introduced here. diff --git a/.claude/skills/atlassian-readonly-skills/scripts/confluence_comments.py b/.claude/skills/atlassian-readonly-skills/scripts/confluence_comments.py index 69b67de6ad..e5c6e75b24 100644 --- a/.claude/skills/atlassian-readonly-skills/scripts/confluence_comments.py +++ b/.claude/skills/atlassian-readonly-skills/scripts/confluence_comments.py @@ -8,9 +8,10 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) -from typing import Any, Dict +from typing import Any, Dict, Optional from _common import ( + AtlassianCredentials, get_confluence_client, format_json_response, format_error_response, diff --git a/.claude/skills/atlassian-readonly-skills/scripts/confluence_labels.py b/.claude/skills/atlassian-readonly-skills/scripts/confluence_labels.py index 6bd3f603e8..0f03c1aee8 100644 --- a/.claude/skills/atlassian-readonly-skills/scripts/confluence_labels.py +++ b/.claude/skills/atlassian-readonly-skills/scripts/confluence_labels.py @@ -8,9 +8,10 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) -from typing import Any, Dict +from typing import Any, Dict, Optional from _common import ( + AtlassianCredentials, get_confluence_client, format_json_response, format_error_response, diff --git a/.claude/skills/atlassian-readonly-skills/scripts/confluence_pages.py b/.claude/skills/atlassian-readonly-skills/scripts/confluence_pages.py index 84f126eb26..c9cafb0a0c 100644 --- a/.claude/skills/atlassian-readonly-skills/scripts/confluence_pages.py +++ b/.claude/skills/atlassian-readonly-skills/scripts/confluence_pages.py @@ -11,6 +11,7 @@ from typing import Any, Dict, Optional from _common import ( + AtlassianCredentials, get_confluence_client, format_json_response, format_error_response, diff --git a/.claude/skills/atlassian-readonly-skills/scripts/jira_agile.py b/.claude/skills/atlassian-readonly-skills/scripts/jira_agile.py index 3175559b18..9b7011501f 100644 --- a/.claude/skills/atlassian-readonly-skills/scripts/jira_agile.py +++ b/.claude/skills/atlassian-readonly-skills/scripts/jira_agile.py @@ -14,6 +14,7 @@ from typing import Any, Dict, Optional from _common import ( + AtlassianCredentials, get_jira_client, simplify_issue, format_json_response, diff --git a/.claude/skills/atlassian-readonly-skills/scripts/jira_links.py b/.claude/skills/atlassian-readonly-skills/scripts/jira_links.py index 80d08a88e4..607767dded 100644 --- a/.claude/skills/atlassian-readonly-skills/scripts/jira_links.py +++ b/.claude/skills/atlassian-readonly-skills/scripts/jira_links.py @@ -8,9 +8,10 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) -from typing import Any, Dict +from typing import Any, Dict, Optional from _common import ( + AtlassianCredentials, get_jira_client, format_json_response, format_error_response, diff --git a/.claude/skills/atlassian-readonly-skills/scripts/jira_projects.py b/.claude/skills/atlassian-readonly-skills/scripts/jira_projects.py index ffa48eb528..14f2703147 100644 --- a/.claude/skills/atlassian-readonly-skills/scripts/jira_projects.py +++ b/.claude/skills/atlassian-readonly-skills/scripts/jira_projects.py @@ -10,9 +10,10 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) -from typing import Any, Dict +from typing import Any, Dict, Optional from _common import ( + AtlassianCredentials, get_jira_client, simplify_issue, format_json_response, diff --git a/.claude/skills/atlassian-readonly-skills/scripts/jira_workflow.py b/.claude/skills/atlassian-readonly-skills/scripts/jira_workflow.py index 4ba1570a93..7b5a70c6bc 100644 --- a/.claude/skills/atlassian-readonly-skills/scripts/jira_workflow.py +++ b/.claude/skills/atlassian-readonly-skills/scripts/jira_workflow.py @@ -8,9 +8,10 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) -from typing import Any, Dict +from typing import Any, Dict, Optional from _common import ( + AtlassianCredentials, get_jira_client, format_json_response, format_error_response, diff --git a/.claude/skills/atlassian-readonly-skills/scripts/jira_worklog.py b/.claude/skills/atlassian-readonly-skills/scripts/jira_worklog.py index fe0c26df05..c9d23f946c 100644 --- a/.claude/skills/atlassian-readonly-skills/scripts/jira_worklog.py +++ b/.claude/skills/atlassian-readonly-skills/scripts/jira_worklog.py @@ -8,9 +8,10 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) -from typing import Any, Dict +from typing import Any, Dict, Optional from _common import ( + AtlassianCredentials, get_jira_client, format_json_response, format_error_response, diff --git a/.claude/skills/atlassian-skills/PROVENANCE.md b/.claude/skills/atlassian-skills/PROVENANCE.md new file mode 100644 index 0000000000..67b13a6168 --- /dev/null +++ b/.claude/skills/atlassian-skills/PROVENANCE.md @@ -0,0 +1,52 @@ +# Provenance + +Vendored from **https://github.com/langpingxue/atlassian-skills** +(`atlassian-skills/`), first committed here in d1a9bc66d. + +## License + +Upstream's README declares **MIT License**. Note two gaps in that declaration, +recorded here so nobody has to rediscover them: + +- **Upstream ships no `LICENSE` file.** GitHub's license endpoint returns 404 + for the repository, and there is no copyright line anywhere in it. +- `SKILL.md`'s frontmatter says `license: Complete terms in LICENSE`, which is + therefore a dangling reference upstream as well as here. It is left unchanged + so this copy stays diffable against upstream. + +MIT permits use and modification; the attribution it asks for is this file. + +## Local modifications + +Keep these few and listed, so re-syncing upstream stays possible. + +1. **`SKILL.md`** gains a leading `## FieldWorks / SIL JIRA Integration` + section (~39 lines) covering `jira.sil.org`, the `LT` project key and the + Data Center specifics. Everything below it matches upstream. + +No script changes. The import bug that made eight modules unusable affects only +the read-only variant -- see its `PROVENANCE.md`. + +## Data Center gotchas worth knowing before reading the docs + +Upstream's docstrings describe Jira Cloud. SIL's instance is Data Center, so: + +- `assignee` wants a username, **not** an `accountId`. + `jira_create_issue`/`jira_update_issue` send `{"accountId": ...}` and are + rejected. Pass `custom_fields={"assignee": {"name": ""}}`. +- Affects Version is not exposed at all. Pass + `custom_fields={"versions": [{"name": "FW 9.3"}]}`. +- `resolution` cannot be set by an update -- it is not on the edit screen. + Only a transition sets it. + +These and the link-type list live in +`.claude/skills/jira-issue/references/publish.md`, which is the place to look +first for LT tickets. + +## Deliberately not done + +This skill has **not** been compressed or restructured, unlike the +FieldWorks-owned skills. It is ~740 lines of API reference that loads only when +Atlassian work happens, and keeping it close to upstream is worth more than the +context saving. The near-total duplication with `atlassian-readonly-skills` is +upstream's design, not something introduced here. From db0a1412d65e7313f2a86c2cbd6f39d73734a1af Mon Sep 17 00:00:00 2001 From: John Lambert Date: Fri, 21 Aug 2026 11:50:06 -0400 Subject: [PATCH 2/4] Replace the duplicated utility catalogue with a function index atlassian-readonly-skills/SKILL.md restated all 48 function signatures that REFERENCE.md already documents in the same folder, 237 lines of it, copied from the write variant. It now carries a module-to-function table naming every function the variant has, and points at REFERENCE.md for signatures. 560 lines to 265. The delegation is narrower than it first looked. Only Response Data Structures, Error Handling and Dependencies are byte-identical between the two variants and safe to document once; Configuration, Core Workflow and Philosophy differ, because the write variant carries write examples. Those first two are kept here verbatim rather than delegated, and the file says which is which. Verified: every one of the 48 functions named here is documented in REFERENCE.md, and the list is generated from the scripts themselves rather than from the prose it replaces. Refs LT-22723 --- .../skills/atlassian-readonly-skills/SKILL.md | 390 +++--------------- 1 file changed, 47 insertions(+), 343 deletions(-) diff --git a/.claude/skills/atlassian-readonly-skills/SKILL.md b/.claude/skills/atlassian-readonly-skills/SKILL.md index 4413ce7406..49c533ea5e 100644 --- a/.claude/skills/atlassian-readonly-skills/SKILL.md +++ b/.claude/skills/atlassian-readonly-skills/SKILL.md @@ -6,9 +6,9 @@ license: Complete terms in LICENSE # Atlassian Readonly Skills -Read-only Python utilities for Jira, Confluence, and Bitbucket integration, supporting both Cloud and Data Center deployments. - -> **Note**: This is a read-only variant that excludes all write operations (create, update, delete). For full functionality including write operations, use `atlassian-skills`. +The read-only half of `atlassian-skills`: same client, same configuration, same +response shapes, with every create/update/delete function removed. Prefer it +whenever the task only reads -- it cannot modify anything by accident. ## FieldWorks / SIL JIRA Integration @@ -42,8 +42,6 @@ python -c "import sys; sys.path.insert(0, '.claude/skills/atlassian-readonly-ski python -c "import sys; sys.path.insert(0, '.claude/skills/atlassian-readonly-skills/scripts'); from jira_workflow import jira_get_transitions; print(jira_get_transitions('LT-22382'))" ``` -Use the script modules in this skill directly. - ## Configuration Two configuration modes are supported: @@ -220,341 +218,47 @@ result = confluence_get_page( ) ``` -## Available Utilities - -### Jira Issue Management (`scripts.jira_issues`) - -```python -from scripts.jira_issues import jira_get_issue - -# Get issue by key -jira_get_issue( - issue_key="PROJ-123", - credentials=credentials # Optional -) -``` - -### Jira Search (`scripts.jira_search`) - -```python -from scripts.jira_search import jira_search, jira_search_fields - -# Search with JQL -jira_search( - jql="project = PROJ AND status = 'In Progress'", - fields="summary,status,assignee", - limit=50 -) - -# Find field definitions -jira_search_fields(keyword="custom") -``` - -### Jira Workflow (`scripts.jira_workflow`) - -```python -from scripts.jira_workflow import jira_get_transitions - -# Get available transitions for an issue -jira_get_transitions(issue_key="PROJ-123") -``` - -### Jira Agile (`scripts.jira_agile`) - -```python -from scripts.jira_agile import ( - jira_get_agile_boards, - jira_get_board_issues, - jira_get_sprints_from_board, - jira_get_sprint_issues -) - -# Get boards -jira_get_agile_boards(project_key="PROJ") - -# Get issues on a board -jira_get_board_issues(board_id=1, jql="status = 'In Progress'") - -# Get sprints from a board -jira_get_sprints_from_board(board_id=1, state="active") - -# Get issues in a sprint -jira_get_sprint_issues(sprint_id=42) -``` - -### Jira Links (`scripts.jira_links`) - -```python -from scripts.jira_links import jira_get_link_types - -# Get available link types -jira_get_link_types() -``` - -### Jira Worklog (`scripts.jira_worklog`) - -```python -from scripts.jira_worklog import jira_get_worklog - -# Get worklog entries for an issue -jira_get_worklog(issue_key="PROJ-123") -``` - -### Jira Projects (`scripts.jira_projects`) - -```python -from scripts.jira_projects import ( - jira_get_all_projects, - jira_get_project_issues, - jira_get_project_versions -) - -# Get all projects -jira_get_all_projects() - -# Get issues in a project -jira_get_project_issues(project_key="PROJ", limit=100) - -# Get project versions -jira_get_project_versions(project_key="PROJ") -``` - -### Jira Users (`scripts.jira_users`) - -```python -from scripts.jira_users import jira_get_user_profile - -# Get user profile -jira_get_user_profile(user_identifier="user@company.com") -``` - -### Confluence Pages (`scripts.confluence_pages`) - -```python -from scripts.confluence_pages import confluence_get_page - -# Get page by title -confluence_get_page(title="Meeting Notes", space_key="TEAM") - -# Get page by ID -confluence_get_page(page_id="12345") -``` - -### Confluence Search (`scripts.confluence_search`) - -```python -from scripts.confluence_search import confluence_search - -# Search with CQL -confluence_search( - query="space = DEV AND type = page AND text ~ 'API'", - limit=25 -) -``` - -### Confluence Comments (`scripts.confluence_comments`) - -```python -from scripts.confluence_comments import confluence_get_comments - -# Get comments on a page -confluence_get_comments(page_id="12345") -``` - -### Confluence Labels (`scripts.confluence_labels`) - -```python -from scripts.confluence_labels import confluence_get_labels - -# Get labels on a page -confluence_get_labels(page_id="12345") -``` - -### Bitbucket Projects (`scripts.bitbucket_projects`) - -```python -from scripts.bitbucket_projects import ( - bitbucket_list_projects, - bitbucket_list_repositories -) - -# List all projects -bitbucket_list_projects(limit=25) - -# List repositories in a project -bitbucket_list_repositories(project_key="PROJ", limit=50) -``` - -### Bitbucket Pull Requests (`scripts.bitbucket_pull_requests`) - -```python -from scripts.bitbucket_pull_requests import ( - bitbucket_get_pull_request, - bitbucket_get_pr_diff -) - -# Get PR details -bitbucket_get_pull_request( - project_key="PROJ", - repository_slug="my-repo", - pr_id=123 -) - -# Get PR diff -bitbucket_get_pr_diff( - project_key="PROJ", - repository_slug="my-repo", - pr_id=123 -) -``` - -### Bitbucket Files & Search (`scripts.bitbucket_files`) - -```python -from scripts.bitbucket_files import ( - bitbucket_get_file_content, - bitbucket_search -) - -# Get file content -bitbucket_get_file_content( - project_key="PROJ", - repository_slug="my-repo", - file_path="src/main.py", - branch="develop" -) - -# Search code -bitbucket_search( - query="def authenticate", - project_key="PROJ", - search_type="code", - limit=25 -) -``` - -### Bitbucket Commits (`scripts.bitbucket_commits`) - -```python -from scripts.bitbucket_commits import ( - bitbucket_get_commits, - bitbucket_get_commit -) - -# Get recent commits from a branch -bitbucket_get_commits( - project_key="PROJ", - repository_slug="my-repo", - branch="master", - limit=10 -) - -# Get details of a specific commit -bitbucket_get_commit( - project_key="PROJ", - repository_slug="my-repo", - commit_id="1da11eaec25aed8b251de24841885c91493b3173" -) -``` - -## Response Data Structures - -All functions return JSON strings with **flattened** data structures (not nested API responses). - -### Jira Issue Structure - -```json -{ - "key": "PROJ-123", - "id": "10001", - "summary": "Issue title", - "description": "Issue description", - "status": "In Progress", - "issue_type": "Task", - "priority": "High", - "assignee": "user@company.com", - "reporter": "reporter@company.com", - "created": "2024-01-15T10:30:00.000+0000", - "updated": "2024-01-16T14:20:00.000+0000", - "labels": ["backend", "urgent"], - "components": ["API", "Auth"], - "custom_fields": {} -} -``` - -### Confluence Page Structure - -```json -{ - "id": "12345", - "title": "Page Title", - "space_key": "DEV", - "status": "current", - "created": "2024-01-15T10:30:00.000Z", - "updated": "2024-01-16T14:20:00.000Z", - "author": "user@company.com", - "version": 3, - "url": "https://company.atlassian.net/wiki/spaces/DEV/pages/12345" -} -``` - -> **Note**: These are simplified structures. The original Jira API returns nested data like `{"key": "...", "fields": {"summary": "...", "status": {"name": "..."}}}`, but this skill flattens it for easier use. - -## Error Handling - -All functions return JSON strings. Check for errors: - -```python -import json - -result = jira_get_issue(issue_key="PROJ-999") -data = json.loads(result) - -if not data.get("success", True): - print(f"Error: {data['error']}") - print(f"Type: {data['error_type']}") -else: - print(f"Issue: {data['key']}") -``` - -### Error Types - -- `ConfigurationError` - Missing environment variables -- `AuthenticationError` - Invalid credentials -- `ValidationError` - Invalid input parameters -- `NotFoundError` - Resource not found -- `APIError` - Atlassian API error -- `NetworkError` - Connection issues - -## Philosophy - -This skill provides: -- **Read-Only Access**: Query and retrieve data without modification -- **Token Efficiency**: Reduced context size by excluding write operations -- **Safety**: Prevents accidental data modifications -- **Flexibility**: Support for both Cloud and Data Center deployments -- **Consistency**: Unified error handling and response format - -It does NOT provide: -- Write operations (create, update, delete) -- Direct API access (use the provided functions instead) -- Webhook handling or event processing -- Bulk import/export operations - -**Best practices**: -- Always check return values for errors -- Use JQL/CQL for efficient searching -- For write operations, use the full `atlassian-skills` package - -## Dependencies - -```bash -pip install requests python-dotenv -``` - -Or use the requirements file: - -```bash -pip install -r requirements.txt -``` - +## What is available here + +Every function in this variant, by module. Anything not listed is a write +operation and lives in `atlassian-skills` instead. + +| Module | Functions | +| --- | --- | +| `jira_issues` | `jira_get_issue` | +| `jira_search` | `jira_search`, `jira_search_fields` | +| `jira_workflow` | `jira_get_transitions` | +| `jira_projects` | `jira_get_all_projects`, `jira_get_project_issues`, `jira_get_project_versions` | +| `jira_agile` | `jira_get_agile_boards`, `jira_get_board_issues`, `jira_get_sprints_from_board`, `jira_get_sprint_issues` | +| `jira_links` | `jira_get_link_types` | +| `jira_worklog` | `jira_get_worklog` | +| `jira_users` | `jira_get_user_profile` | +| `confluence_pages` | `confluence_get_page` | +| `confluence_search` | `confluence_search` | +| `confluence_comments` | `confluence_get_comments` | +| `confluence_labels` | `confluence_get_labels` | +| `bitbucket_projects` | `bitbucket_list_projects`, `bitbucket_list_repositories` | +| `bitbucket_pull_requests` | `bitbucket_get_pull_request`, `bitbucket_get_pr_diff` | +| `bitbucket_files` | `bitbucket_get_file_content`, `bitbucket_search` | +| `bitbucket_commits` | `bitbucket_get_commits`, `bitbucket_get_commit` | + +Signatures, arguments and per-function detail are in `REFERENCE.md`. + +## Shared with `atlassian-skills`, documented once + +These three sections are **byte-identical** between the two variants, so they +live only in `../atlassian-skills/SKILL.md`. Read them there: + +- **Response data structures** -- the simplified issue and page shapes +- **Error handling** -- the error envelope and the error type list +- **Dependencies** -- `requirements.txt` + +Its *Configuration*, *Core workflow* and *Philosophy* sections are supersets of +the ones above, carrying write examples that do not apply here. The versions +above are the ones that apply to this variant. + +## Provenance and known bugs + +See `PROVENANCE.md`: upstream repository, the MIT declaration and its gaps, the +local modifications, and the Data Center behaviours that upstream's +Cloud-oriented docstrings get wrong. From 26a90ea3fc6093383998e09489f9b0328902f770 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Fri, 21 Aug 2026 11:50:49 -0400 Subject: [PATCH 3/4] Correct the provenance note the restructure invalidated PROVENANCE.md said this skill had deliberately not been compressed. It has been, in the commit before this one, so the note now records what changed and warns that a re-sync must re-apply it alongside the import fix and the SIL section. The read-only note also cites the upstream issue for the import bug now that one exists: langpingxue/atlassian-skills#14. Refs LT-22723 Co-Authored-By: Claude Opus 5 --- .../atlassian-readonly-skills/PROVENANCE.md | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/.claude/skills/atlassian-readonly-skills/PROVENANCE.md b/.claude/skills/atlassian-readonly-skills/PROVENANCE.md index ee7703f640..375cab2a9c 100644 --- a/.claude/skills/atlassian-readonly-skills/PROVENANCE.md +++ b/.claude/skills/atlassian-readonly-skills/PROVENANCE.md @@ -34,13 +34,21 @@ Keep these few and listed, so re-syncing upstream stays possible. `jira_agile`, `jira_links`, `jira_projects`, `jira_workflow`, `jira_worklog`. The fix restores the pruned names, nothing else. - **This is an upstream bug and should be reported there.** Until it is fixed - upstream, a re-sync will reintroduce it. + **Reported upstream as langpingxue/atlassian-skills#14.** Until it is fixed + there, a re-sync will reintroduce it. -## Deliberately not done +## Restructuring done here -This skill has **not** been compressed or restructured, unlike the -FieldWorks-owned skills. It is ~560 lines of API reference that loads only -when Atlassian work happens, and keeping it close to upstream is worth more -than the context saving. The near-total duplication between this variant and -`atlassian-skills` is upstream's design, not something introduced here. +`SKILL.md` dropped its `## Available Utilities` section -- 237 lines restating +all 48 function signatures that `REFERENCE.md` already documents in the same +folder, copied from the write variant. It is now a module-to-function index +generated from the scripts, with `REFERENCE.md` carrying the signatures. 560 +lines to 265. + +Only three sections proved byte-identical to the write variant and were safe to +document once there: Response Data Structures, Error Handling, Dependencies. +Configuration, Core Workflow and Philosophy differ -- the write variant's are +supersets carrying write examples -- so the first two are kept here verbatim. + +This is the largest local divergence from upstream. A re-sync has to re-apply +it, along with the import fix and the SIL section. From f5c09c3467353cbb2b1f7373fbfa68358c48a3cc Mon Sep 17 00:00:00 2001 From: John Lambert Date: Fri, 21 Aug 2026 12:54:43 -0400 Subject: [PATCH 4/4] Remove Confluence and Bitbucket from both Atlassian skills FieldWorks uses Jira and nothing else. Nothing in the repository referenced a Confluence or Bitbucket helper, yet both skills carried full support for them: eight script modules each, their documentation, their configuration blocks, and their plumbing in _common.py. Removed across both variants: the confluence_* and bitbucket_* modules, their SKILL.md and REFERENCE.md sections, the CQL query reference, the partial-service configuration guidance, and in _common.py the dataclass fields, is_*_available checks, get_*_client factories and the service branches in AtlassianConfig.from_credentials. Streamlined what remained rather than leaving holes. Configuration is now two short modes, SIL Data Center first, with Jira Cloud kept only for completeness. Core Workflow, the agent-mode example and the credentials reference were rewritten around Jira instead of having their other two thirds cut out. Both frontmatter descriptions claimed Confluence and Bitbucket support, so they are rewritten too. A description that overstates what a skill does is how the wrong skill gets loaded. Verified: zero Confluence or Bitbucket references remain outside the provenance notes. All 9 modules in each skill import, and a live read against SIL Jira through the read-only skill still works. Refs LT-22723 --- .../atlassian-readonly-skills/PROVENANCE.md | 14 + .../atlassian-readonly-skills/REFERENCE.md | 352 +-------------- .../skills/atlassian-readonly-skills/SKILL.md | 173 ++----- .../scripts/_common.py | 138 +----- .../scripts/bitbucket_commits.py | 166 ------- .../scripts/bitbucket_files.py | 190 -------- .../scripts/bitbucket_projects.py | 145 ------ .../scripts/bitbucket_pull_requests.py | 151 ------- .../scripts/confluence_comments.py | 90 ---- .../scripts/confluence_labels.py | 72 --- .../scripts/confluence_pages.py | 97 ---- .../scripts/confluence_search.py | 102 ----- .claude/skills/atlassian-skills/PROVENANCE.md | 14 + .claude/skills/atlassian-skills/REFERENCE.md | 346 ++------------ .claude/skills/atlassian-skills/SKILL.md | 413 ++--------------- .../atlassian-skills/scripts/__init__.py | 2 +- .../atlassian-skills/scripts/_common.py | 138 +----- .../scripts/bitbucket_commits.py | 166 ------- .../scripts/bitbucket_files.py | 190 -------- .../scripts/bitbucket_projects.py | 145 ------ .../scripts/bitbucket_pull_requests.py | 425 ------------------ .../scripts/confluence_comments.py | 140 ------ .../scripts/confluence_labels.py | 159 ------- .../scripts/confluence_pages.py | 257 ----------- .../scripts/confluence_search.py | 102 ----- 25 files changed, 150 insertions(+), 4037 deletions(-) delete mode 100644 .claude/skills/atlassian-readonly-skills/scripts/bitbucket_commits.py delete mode 100644 .claude/skills/atlassian-readonly-skills/scripts/bitbucket_files.py delete mode 100644 .claude/skills/atlassian-readonly-skills/scripts/bitbucket_projects.py delete mode 100644 .claude/skills/atlassian-readonly-skills/scripts/bitbucket_pull_requests.py delete mode 100644 .claude/skills/atlassian-readonly-skills/scripts/confluence_comments.py delete mode 100644 .claude/skills/atlassian-readonly-skills/scripts/confluence_labels.py delete mode 100644 .claude/skills/atlassian-readonly-skills/scripts/confluence_pages.py delete mode 100644 .claude/skills/atlassian-readonly-skills/scripts/confluence_search.py delete mode 100644 .claude/skills/atlassian-skills/scripts/bitbucket_commits.py delete mode 100644 .claude/skills/atlassian-skills/scripts/bitbucket_files.py delete mode 100644 .claude/skills/atlassian-skills/scripts/bitbucket_projects.py delete mode 100644 .claude/skills/atlassian-skills/scripts/bitbucket_pull_requests.py delete mode 100644 .claude/skills/atlassian-skills/scripts/confluence_comments.py delete mode 100644 .claude/skills/atlassian-skills/scripts/confluence_labels.py delete mode 100644 .claude/skills/atlassian-skills/scripts/confluence_pages.py delete mode 100644 .claude/skills/atlassian-skills/scripts/confluence_search.py diff --git a/.claude/skills/atlassian-readonly-skills/PROVENANCE.md b/.claude/skills/atlassian-readonly-skills/PROVENANCE.md index 375cab2a9c..4abb699a08 100644 --- a/.claude/skills/atlassian-readonly-skills/PROVENANCE.md +++ b/.claude/skills/atlassian-readonly-skills/PROVENANCE.md @@ -52,3 +52,17 @@ supersets carrying write examples -- so the first two are kept here verbatim. This is the largest local divergence from upstream. A re-sync has to re-apply it, along with the import fix and the SIL section. + +## Jira only + +Confluence and Bitbucket were removed entirely on 2026-08-21. FieldWorks uses +Jira and nothing else, and nothing in the repository referenced either. + +Removed: the eight `confluence_*` and `bitbucket_*` script modules, their +documentation in `SKILL.md` and `REFERENCE.md`, their configuration blocks and +credential fields, and their plumbing in `_common.py` -- the dataclass fields, +`is_*_available` checks, `get_*_client` factories and the service branches in +`AtlassianConfig.from_credentials`. + +This is a hard fork from upstream for these two skills. A re-sync is no longer +a merge; treat upstream as a source to cherry-pick Jira fixes from. diff --git a/.claude/skills/atlassian-readonly-skills/REFERENCE.md b/.claude/skills/atlassian-readonly-skills/REFERENCE.md index aa3de774ad..1fa19cb432 100644 --- a/.claude/skills/atlassian-readonly-skills/REFERENCE.md +++ b/.claude/skills/atlassian-readonly-skills/REFERENCE.md @@ -1,6 +1,6 @@ # Atlassian Readonly Skills API Reference -Detailed usage examples and API documentation for read-only Jira, Confluence, and Bitbucket operations. +Detailed usage examples and API documentation for the read-only Jira operations. > **Note**: This is a read-only variant. For write operations (create, update, delete), use `atlassian-skills`. @@ -177,127 +177,6 @@ sprints = jira_get_sprints_from_board(board_id="123", state="active") sprint_issues = jira_get_sprint_issues(sprint_id="456", limit=50) ``` -## Confluence Examples - -### Search for Pages - -```python -from scripts.confluence_search import confluence_search - -# Search for pages -result = confluence_search("API documentation", limit=10) - -# Search in specific space -result = confluence_search("guide", space_key="DEV", limit=20) -``` - -### Get a Page - -```python -from scripts.confluence_pages import confluence_get_page - -# Get by ID -result = confluence_get_page(page_id="123456") - -# Get by title and space -result = confluence_get_page(title="API Guide", space_key="DEV") -``` - -### Get Comments - -```python -from scripts.confluence_comments import confluence_get_comments - -# Get comments for a page -comments = confluence_get_comments(page_id="123456") -``` - -### Get Labels - -```python -from scripts.confluence_labels import confluence_get_labels - -# Get labels for a page -labels = confluence_get_labels(page_id="123456") -``` - -## Bitbucket Examples - -### List Projects and Repositories - -```python -from scripts.bitbucket_projects import bitbucket_list_projects, bitbucket_list_repositories - -# List all projects -projects = bitbucket_list_projects(limit=25) - -# List repositories in a project -repos = bitbucket_list_repositories(project_key="PROJ", limit=50) -``` - -### Get Pull Request Details - -```python -from scripts.bitbucket_pull_requests import bitbucket_get_pull_request, bitbucket_get_pr_diff - -# Get PR details -result = bitbucket_get_pull_request( - project_key="PROJ", - repository_slug="my-repo", - pr_id=123 -) - -# Get PR diff -diff = bitbucket_get_pr_diff( - project_key="PROJ", - repository_slug="my-repo", - pr_id=123 -) -``` - -### Get File Content - -```python -from scripts.bitbucket_files import bitbucket_get_file_content, bitbucket_search - -# Get file content -result = bitbucket_get_file_content( - project_key="PROJ", - repository_slug="my-repo", - file_path="src/main.py", - branch="master" -) - -# Search for code -result = bitbucket_search( - query="def authenticate", - project_key="PROJ", - search_type="code", - limit=25 -) -``` - -### Get Commits - -```python -from scripts.bitbucket_commits import bitbucket_get_commits, bitbucket_get_commit - -# Get recent commits from a branch -result = bitbucket_get_commits( - project_key="PROJ", - repository_slug="my-repo", - branch="master", - limit=10 -) - -# Get specific commit details -result = bitbucket_get_commit( - project_key="PROJ", - repository_slug="my-repo", - commit_id="1da11eaec25aed8b251de24841885c91493b3173" -) -``` - ## JQL Query Examples Common JQL patterns for searching Jira issues: @@ -331,33 +210,6 @@ reporter = "user@example.com" project = MYPROJ AND status = "In Progress" AND assignee = currentUser() ORDER BY priority DESC ``` -## CQL Query Examples - -Common CQL patterns for searching Confluence: - -``` -# Search by text -text ~ "API documentation" - -# Search in specific space -space = DEV AND text ~ "guide" - -# Search by title -title ~ "Getting Started" - -# Search by label -label = "api-docs" - -# Search by type -type = page AND space = DEV - -# Recently modified -lastModified >= now("-7d") - -# Created by user -creator = "user@example.com" -``` - ## Response Format ### Success Response @@ -393,206 +245,50 @@ creator = "user@example.com" } ``` -## Bitbucket Response Examples - -### Commit Response Structure - -```json -{ - "project_key": "PROJ", - "repository": "my-repo", - "branch": "master", - "total": 5, - "is_last_page": false, - "commits": [ - { - "id": "1da11eaec25aed8b251de24841885c91493b3173", - "display_id": "1da11eaec25", - "message": "Feature: Add new API endpoint", - "author_name": "John Doe", - "author_email": "john.doe@company.com", - "committer_name": "John Doe", - "committer_email": "john.doe@company.com", - "timestamp": 1765534577000, - "parents": ["b97ad25d330e36480b045c5dea36f97999297ff6"] - } - ] -} -``` - -### Pull Request Response Structure - -```json -{ - "id": 123, - "title": "Feature: Add authentication", - "description": "Implements OAuth2 authentication", - "state": "OPEN", - "author": { - "name": "John Doe", - "email": "john.doe@company.com" - }, - "from_ref": { - "branch": "feature/auth", - "commit": "abc123" - }, - "to_ref": { - "branch": "master", - "commit": "def456" - }, - "created_date": 1765534577000, - "updated_date": 1765534577000 -} -``` - ## Agent Mode Complete Example -When deploying in Agent environments without environment variables: - ```python +import json from scripts._common import AtlassianCredentials, check_available_skills from scripts.jira_issues import jira_get_issue from scripts.jira_search import jira_search -from scripts.confluence_pages import confluence_get_page -from scripts.bitbucket_commits import bitbucket_get_commits -import json -# Step 1: Create credentials object with all needed services credentials = AtlassianCredentials( - # Jira configuration - jira_url="https://company.atlassian.net", - jira_username="user@company.com", - jira_api_token="jira_token_here", - - # Confluence configuration - confluence_url="https://company.atlassian.net/wiki", - confluence_username="user@company.com", - confluence_api_token="confluence_token_here", - - # Bitbucket configuration (optional) - bitbucket_url="https://bitbucket.company.com", - bitbucket_pat_token="bitbucket_pat_here" + jira_url="https://jira.sil.org", + jira_pat_token="your_pat_token", ) -# Step 2: Check which services are available availability = check_available_skills(credentials) -print(f"Available: {availability['available_services']}") -print(f"Unavailable: {availability['unavailable_services']}") - -# Step 3: Use read-only skills with credentials parameter -if "jira" in availability["available_services"]: - # Get an issue - result = jira_get_issue( - issue_key="PROJ-123", - credentials=credentials - ) - issue = json.loads(result) - - if not issue.get("error"): - print(f"Issue: {issue['key']} - {issue['summary']}") - - # Search for issues - result = jira_search( - jql="project = PROJ AND status = 'In Progress'", - limit=10, - credentials=credentials - ) - search_results = json.loads(result) - print(f"Found {search_results['total']} issues") - -if "confluence" in availability["available_services"]: - # Get a Confluence page - result = confluence_get_page( - title="Documentation", - space_key="TEAM", - credentials=credentials - ) - page = json.loads(result) - if not page.get("error"): - print(f"Page: {page['title']}") - -if "bitbucket" in availability["available_services"]: - # Get recent commits - result = bitbucket_get_commits( - project_key="PROJ", - repository_slug="my-repo", - branch="master", - limit=5, - credentials=credentials - ) - commits = json.loads(result) - if not commits.get("error"): - print(f"Found {commits['total']} commits") - -# Step 4: Handle unavailable services -for service, reason in availability["unavailable_services"].items(): - print(f"{service} unavailable: {reason}") -``` - -## Partial Service Configuration - -You can configure only the services you need: - -```python -from scripts._common import AtlassianCredentials, check_available_skills - -# Only Jira configured -jira_only_creds = AtlassianCredentials( - jira_url="https://company.atlassian.net", - jira_username="user@company.com", - jira_api_token="token" -) - -# Check availability -availability = check_available_skills(jira_only_creds) -# Returns: { -# "available_services": ["jira"], -# "unavailable_services": { -# "confluence": "Missing confluence_url", -# "bitbucket": "Missing bitbucket_url" -# } -# } - -# Jira functions work -from scripts.jira_issues import jira_get_issue -result = jira_get_issue("PROJ-123", credentials=jira_only_creds) # ✓ Works - -# Confluence functions fail with clear error -from scripts.confluence_pages import confluence_get_page -result = confluence_get_page("Page", "SPACE", credentials=jira_only_creds) # ✗ Fails -# Returns: {"error": "Confluence credentials not provided...", "error_type": "ConfigurationError"} +if "jira" not in availability["available_services"]: + raise SystemExit(availability["unavailable_services"]["jira"]) + +issue = json.loads(jira_get_issue("LT-22382", credentials=credentials)) +print(f"{issue['key']}: {issue['summary']}") + +results = json.loads(jira_search( + jql="project = LT AND status = 'In Progress'", + fields="summary,status,assignee", + limit=50, + credentials=credentials, +)) ``` ## Credentials Object Reference ```python AtlassianCredentials( - # Jira jira_url: Optional[str] = None, jira_username: Optional[str] = None, jira_api_token: Optional[str] = None, jira_pat_token: Optional[str] = None, - jira_api_version: Optional[str] = None, # '2' or '3', auto-detected if not set - jira_ssl_verify: bool = False, - - # Confluence - confluence_url: Optional[str] = None, - confluence_username: Optional[str] = None, - confluence_api_token: Optional[str] = None, - confluence_pat_token: Optional[str] = None, - confluence_api_version: Optional[str] = None, - confluence_ssl_verify: bool = False, - - # Bitbucket - bitbucket_url: Optional[str] = None, - bitbucket_username: Optional[str] = None, - bitbucket_api_token: Optional[str] = None, - bitbucket_pat_token: Optional[str] = None, - bitbucket_api_version: Optional[str] = None, - bitbucket_ssl_verify: bool = False + jira_api_version: Optional[str] = None, + jira_ssl_verify: bool = True, ) ``` -For each service, provide either: -- **PAT Token** (for Data Center/Server): `{service}_pat_token` -- **Username + API Token** (for Cloud): `{service}_username` + `{service}_api_token` +For SIL's Data Center instance, `jira_url` plus `jira_pat_token` is enough. +`jira_username` and `jira_api_token` are the Cloud pairing. A PAT token wins if +both are supplied. + +`check_available_skills(credentials)` returns `available_services` and +`unavailable_services`, the latter naming the missing field. diff --git a/.claude/skills/atlassian-readonly-skills/SKILL.md b/.claude/skills/atlassian-readonly-skills/SKILL.md index 49c533ea5e..cebdc5f50c 100644 --- a/.claude/skills/atlassian-readonly-skills/SKILL.md +++ b/.claude/skills/atlassian-readonly-skills/SKILL.md @@ -1,6 +1,6 @@ --- name: atlassian-readonly-skills -description: Read-only Python utilities for Jira, Confluence, and Bitbucket integration. Provides read access to issues, search, workflows, pages, pull requests, commit history, and more. Use when users need to query Atlassian products like "get a Jira issue", "search Confluence pages", "view pull request details", or "get commit history". This variant excludes all write operations for token efficiency and safety. +description: Read-only Python utilities for Jira (SIL Data Center): fetch an issue, search with JQL, read transitions, links, worklogs, agile boards and projects. Use when users need to look up or query a Jira issue -- including any LT-prefixed FieldWorks ticket. Excludes every write operation, so it cannot modify anything. license: Complete terms in LICENSE --- @@ -10,6 +10,8 @@ The read-only half of `atlassian-skills`: same client, same configuration, same response shapes, with every create/update/delete function removed. Prefer it whenever the task only reads -- it cannot modify anything by accident. +Jira only; Confluence and Bitbucket were removed from this copy. + ## FieldWorks / SIL JIRA Integration **LT-prefixed tickets** (e.g., `LT-22382`, `LT-19288`) are JIRA issues from SIL's JIRA instance: @@ -44,180 +46,63 @@ python -c "import sys; sys.path.insert(0, '.claude/skills/atlassian-readonly-ski ## Configuration -Two configuration modes are supported: - -### Mode 1: Environment Variables (Traditional) +Jira only. Confluence and Bitbucket support was removed from this copy -- see +`PROVENANCE.md`. -Set environment variables based on your deployment type. This mode is used when `credentials` parameter is not provided to skill functions. +### Mode 1: environment variables -#### SIL JIRA (Data Center / PAT Token) +SIL JIRA (Data Center, PAT token) -- this is the one FieldWorks uses: ```bash -# SIL JIRA instance for LT-* tickets JIRA_URL=https://jira.sil.org -# Personal Access Token - generate at: https://jira.sil.org/secure/ViewProfile.jspa → Personal Access Tokens +# Generate at https://jira.sil.org/secure/ViewProfile.jspa -> Personal Access Tokens JIRA_PAT_TOKEN=your_jira_pat_token_here ``` -#### Cloud (API Token) +Jira Cloud (API token), for completeness: ```bash -# Jira Cloud JIRA_URL=https://your-company.atlassian.net JIRA_USERNAME=your.email@company.com JIRA_API_TOKEN=your_api_token - -# Confluence Cloud -CONFLUENCE_URL=https://your-company.atlassian.net/wiki -CONFLUENCE_USERNAME=your.email@company.com -CONFLUENCE_API_TOKEN=your_api_token -``` - -Generate API tokens at: https://id.atlassian.com/manage-profile/security/api-tokens - -#### Data Center / Server (PAT Token) - -```bash -# Jira Data Center -JIRA_URL=https://jira.your-company.com -JIRA_PAT_TOKEN=your_pat_token - -# Confluence Data Center -CONFLUENCE_URL=https://confluence.your-company.com -CONFLUENCE_PAT_TOKEN=your_pat_token - -# Bitbucket Server/Data Center -BITBUCKET_URL=https://bitbucket.your-company.com -BITBUCKET_PAT_TOKEN=your_pat_token ``` -> **Note**: PAT Token takes precedence if both are provided. - -### Mode 2: Parameter-Based (Agent Environments) +A PAT token takes precedence if both are provided. -Alternatively, call the scripts in this skill directly. - -# Create credentials object -credentials = AtlassianCredentials( - # Jira configuration - jira_url="https://your-company.atlassian.net", - jira_username="your.email@company.com", - jira_api_token="your_api_token", - - # Confluence configuration (optional) - confluence_url="https://your-company.atlassian.net/wiki", - confluence_username="your.email@company.com", - confluence_api_token="your_api_token", - - # Bitbucket configuration (optional) - # bitbucket_url="https://bitbucket.your-company.com", - # bitbucket_pat_token="your_pat_token" -) - -# Check which services are available -availability = check_available_skills(credentials) -print(availability["available_services"]) # ["jira", "confluence"] -print(availability["unavailable_services"]) # {"bitbucket": "Missing bitbucket_url"} - -# Use skills with credentials parameter -result = jira_get_issue( - issue_key="PROJ-123", - credentials=credentials # Pass credentials here -) -``` - -#### Partial Service Configuration - -You can configure only the services you need. Services without complete credentials will be unavailable: +### Mode 2: credentials parameter (agent environments) ```python -# Only configure Jira +from scripts._common import AtlassianCredentials + credentials = AtlassianCredentials( - jira_url="https://your-company.atlassian.net", - jira_username="your.email@company.com", - jira_api_token="your_api_token" + jira_url="https://jira.sil.org", + jira_pat_token="your_pat_token", ) - -# Jira skills will work -jira_get_issue("PROJ-123", credentials=credentials) # ✓ Works - -# Confluence/Bitbucket skills will fail with ConfigurationError -confluence_get_page("Page Title", "SPACE", credentials=credentials) # ✗ Fails ``` -For credentials object fields and authentication options, see the full documentation in `atlassian-skills`. +Every function takes an optional `credentials` argument. Without one, the +environment variables above are used. ## Core Workflow -### Using Environment Variables - ```python -from scripts.jira_issues import jira_get_issue -from scripts.jira_search import jira_search -from scripts.confluence_pages import confluence_get_page import json - -# 1. Get a Jira issue -result = jira_get_issue(issue_key="PROJ-123") -issue = json.loads(result) -print(f"Issue: {issue['key']} - {issue['summary']}") - -# 2. Search for issues -result = jira_search( - jql="project = PROJ AND status = 'In Progress'", - fields="summary,status,assignee", - limit=50 -) -issues = json.loads(result) - -# 3. Get a Confluence page -result = confluence_get_page(title="Feature Documentation", space_key="DEV") -page = json.loads(result) -print(f"Page: {page['title']}") -``` - -### Using Credentials Parameter (Agent Mode) - -```python -from scripts._common import AtlassianCredentials from scripts.jira_issues import jira_get_issue from scripts.jira_search import jira_search -from scripts.confluence_pages import confluence_get_page -import json -# Create credentials -credentials = AtlassianCredentials( - jira_url="https://company.atlassian.net", - jira_username="user@company.com", - jira_api_token="token123", - confluence_url="https://company.atlassian.net/wiki", - confluence_username="user@company.com", - confluence_api_token="token123" -) +issue = json.loads(jira_get_issue(issue_key="LT-22382")) +print(f"{issue['key']} - {issue['summary']}") -# 1. Get a Jira issue with credentials -result = jira_get_issue( - issue_key="PROJ-123", - credentials=credentials -) -issue = json.loads(result) - -# 2. Search for issues with credentials -result = jira_search( - jql="project = PROJ AND status = 'In Progress'", +results = json.loads(jira_search( + jql="project = LT AND status = 'In Progress'", fields="summary,status,assignee", limit=50, - credentials=credentials -) - -# 3. Get a Confluence page with credentials -result = confluence_get_page( - title="Feature Documentation", - space_key="DEV", - credentials=credentials -) +)) ``` +Pass `credentials=credentials` to any of them to use Mode 2 instead of the +environment. + ## What is available here Every function in this variant, by module. Anything not listed is a write @@ -233,14 +118,6 @@ operation and lives in `atlassian-skills` instead. | `jira_links` | `jira_get_link_types` | | `jira_worklog` | `jira_get_worklog` | | `jira_users` | `jira_get_user_profile` | -| `confluence_pages` | `confluence_get_page` | -| `confluence_search` | `confluence_search` | -| `confluence_comments` | `confluence_get_comments` | -| `confluence_labels` | `confluence_get_labels` | -| `bitbucket_projects` | `bitbucket_list_projects`, `bitbucket_list_repositories` | -| `bitbucket_pull_requests` | `bitbucket_get_pull_request`, `bitbucket_get_pr_diff` | -| `bitbucket_files` | `bitbucket_get_file_content`, `bitbucket_search` | -| `bitbucket_commits` | `bitbucket_get_commits`, `bitbucket_get_commit` | Signatures, arguments and per-function detail are in `REFERENCE.md`. diff --git a/.claude/skills/atlassian-readonly-skills/scripts/_common.py b/.claude/skills/atlassian-readonly-skills/scripts/_common.py index 176a395dfa..1c657f0ec4 100644 --- a/.claude/skills/atlassian-readonly-skills/scripts/_common.py +++ b/.claude/skills/atlassian-readonly-skills/scripts/_common.py @@ -1,6 +1,6 @@ """Common utilities for Atlassian Skills scripts. -This module provides shared functionality used by all Jira and Confluence scripts: +This module provides shared functionality used by all Jira scripts: - Configuration management - HTTP client for API requests - Error handling @@ -108,7 +108,7 @@ def format_json_response(data: Any) -> str: class AtlassianCredentials: """Unified credentials configuration for all Atlassian services. - This class wraps authentication credentials for Jira, Confluence, and Bitbucket. + This class wraps authentication credentials for Jira. When deployed in an Agent environment without environment variables, pass this object to skill functions to provide credentials programmatically. @@ -127,21 +127,7 @@ class AtlassianCredentials: jira_api_version: Optional[str] = None jira_ssl_verify: bool = False - # Confluence configuration - confluence_url: Optional[str] = None - confluence_username: Optional[str] = None - confluence_api_token: Optional[str] = None - confluence_pat_token: Optional[str] = None - confluence_api_version: Optional[str] = None - confluence_ssl_verify: bool = False - - # Bitbucket configuration - bitbucket_url: Optional[str] = None - bitbucket_username: Optional[str] = None - bitbucket_api_token: Optional[str] = None - bitbucket_pat_token: Optional[str] = None - bitbucket_api_version: Optional[str] = None - bitbucket_ssl_verify: bool = False + def is_jira_available(self) -> bool: """Check if Jira credentials are complete and valid. @@ -155,44 +141,18 @@ def is_jira_available(self) -> bool: has_basic = bool(self.jira_username and self.jira_api_token) return has_pat or has_basic - def is_confluence_available(self) -> bool: - """Check if Confluence credentials are complete and valid. - - Returns: - True if Confluence can be used, False otherwise - """ - if not self.confluence_url: - return False - has_pat = bool(self.confluence_pat_token) - has_basic = bool(self.confluence_username and self.confluence_api_token) - return has_pat or has_basic - def is_bitbucket_available(self) -> bool: - """Check if Bitbucket credentials are complete and valid. - - Returns: - True if Bitbucket can be used, False otherwise - """ - if not self.bitbucket_url: - return False - has_pat = bool(self.bitbucket_pat_token) - has_basic = bool(self.bitbucket_username and self.bitbucket_api_token) - return has_pat or has_basic def get_available_services(self) -> List[str]: """Get list of available services based on provided credentials. Returns: List of service names that have complete credentials - Example: ["jira", "confluence"] + Example: ["jira"] """ services = [] if self.is_jira_available(): services.append("jira") - if self.is_confluence_available(): - services.append("confluence") - if self.is_bitbucket_available(): - services.append("bitbucket") return services def get_unavailable_services(self) -> Dict[str, str]: @@ -200,7 +160,7 @@ def get_unavailable_services(self) -> Dict[str, str]: Returns: Dictionary mapping service name to reason for unavailability - Example: {"bitbucket": "Missing bitbucket_url"} + Example: {"jira": "Missing jira_url"} """ unavailable = {} @@ -210,17 +170,7 @@ def get_unavailable_services(self) -> Dict[str, str]: else: unavailable["jira"] = "Missing authentication credentials (provide jira_pat_token or jira_username + jira_api_token)" - if not self.is_confluence_available(): - if not self.confluence_url: - unavailable["confluence"] = "Missing confluence_url" - else: - unavailable["confluence"] = "Missing authentication credentials (provide confluence_pat_token or confluence_username + confluence_api_token)" - if not self.is_bitbucket_available(): - if not self.bitbucket_url: - unavailable["bitbucket"] = "Missing bitbucket_url" - else: - unavailable["bitbucket"] = "Missing authentication credentials (provide bitbucket_pat_token or bitbucket_username + bitbucket_api_token)" return unavailable @@ -270,7 +220,7 @@ def from_env(cls, prefix: str) -> "AtlassianConfig": """Load configuration from environment variables. Args: - prefix: Prefix for environment variables (e.g., 'JIRA' or 'CONFLUENCE') + prefix: Prefix for environment variables (e.g., 'JIRA') Returns: AtlassianConfig instance with values from environment @@ -303,7 +253,7 @@ def from_credentials(cls, credentials: AtlassianCredentials, service: str) -> "A Args: credentials: AtlassianCredentials instance with service credentials - service: Service name ('jira', 'confluence', or 'bitbucket') + service: Service name ('jira') Returns: AtlassianConfig instance @@ -327,36 +277,8 @@ def from_credentials(cls, credentials: AtlassianCredentials, service: str) -> "A api_version=credentials.jira_api_version, ssl_verify=credentials.jira_ssl_verify ) - elif service == "confluence": - if not credentials.is_confluence_available(): - raise ConfigurationError( - "Confluence credentials not provided or incomplete. " - "Please provide confluence_url and either confluence_pat_token or (confluence_username + confluence_api_token)." - ) - config = cls( - url=credentials.confluence_url or "", - username=credentials.confluence_username, - api_token=credentials.confluence_api_token, - pat_token=credentials.confluence_pat_token, - api_version=credentials.confluence_api_version, - ssl_verify=credentials.confluence_ssl_verify - ) - elif service == "bitbucket": - if not credentials.is_bitbucket_available(): - raise ConfigurationError( - "Bitbucket credentials not provided or incomplete. " - "Please provide bitbucket_url and either bitbucket_pat_token or (bitbucket_username + bitbucket_api_token)." - ) - config = cls( - url=credentials.bitbucket_url or "", - username=credentials.bitbucket_username, - api_token=credentials.bitbucket_api_token, - pat_token=credentials.bitbucket_pat_token, - api_version=credentials.bitbucket_api_version, - ssl_verify=credentials.bitbucket_ssl_verify - ) else: - raise ConfigurationError(f"Unknown service: {service}. Must be 'jira', 'confluence', or 'bitbucket'.") + raise ConfigurationError(f"Unknown service: {service}. Must be 'jira'.") return config @@ -567,46 +489,6 @@ def get_jira_client(credentials: Optional[AtlassianCredentials] = None) -> Atlas return AtlassianClient(config) -def get_confluence_client(credentials: Optional[AtlassianCredentials] = None) -> AtlassianClient: - """Get configured Confluence client. - - Args: - credentials: Optional AtlassianCredentials object. If not provided, - configuration will be loaded from environment variables. - - Returns: - Configured AtlassianClient instance - - Raises: - ConfigurationError: If configuration is missing or invalid - """ - if credentials: - config = AtlassianConfig.from_credentials(credentials, 'confluence') - else: - config = AtlassianConfig.from_env('CONFLUENCE') - return AtlassianClient(config) - - -def get_bitbucket_client(credentials: Optional[AtlassianCredentials] = None) -> AtlassianClient: - """Get configured Bitbucket client. - - Args: - credentials: Optional AtlassianCredentials object. If not provided, - configuration will be loaded from environment variables. - - Returns: - Configured AtlassianClient instance - - Raises: - ConfigurationError: If configuration is missing or invalid - """ - if credentials: - config = AtlassianConfig.from_credentials(credentials, 'bitbucket') - else: - config = AtlassianConfig.from_env('BITBUCKET') - return AtlassianClient(config) - - def check_available_skills(credentials: AtlassianCredentials) -> Dict[str, Any]: """Check which Atlassian skills are available based on provided credentials. @@ -619,9 +501,9 @@ def check_available_skills(credentials: AtlassianCredentials) -> Dict[str, Any]: Returns: Dictionary with availability information: { - "available_services": ["jira", "confluence"], + "available_services": ["jira"], "unavailable_services": { - "bitbucket": "Missing bitbucket_url" + "jira": "Missing jira_url" } } diff --git a/.claude/skills/atlassian-readonly-skills/scripts/bitbucket_commits.py b/.claude/skills/atlassian-readonly-skills/scripts/bitbucket_commits.py deleted file mode 100644 index 041ba6e067..0000000000 --- a/.claude/skills/atlassian-readonly-skills/scripts/bitbucket_commits.py +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env python3 -"""Bitbucket commit utilities. - -This module provides functions for retrieving commit information -from Bitbucket Server/Data Center repositories. -""" - -from typing import Optional, Dict, Any, List -from ._common import ( - AtlassianCredentials, - get_bitbucket_client, - format_json_response, - format_error_response, - ConfigurationError, - AuthenticationError, - ValidationError, - NotFoundError, - APIError, - NetworkError, -) - - -def bitbucket_get_commits( - project_key: str, - repository_slug: str, - branch: str = "master", - limit: int = 10, - start: int = 0, - credentials: Optional[AtlassianCredentials] = None -) -> str: - """Get commits from a repository. - - Args: - project_key: Project key (e.g., 'IN') - repository_slug: Repository slug (e.g., 'insights-pipeline') - branch: Branch name or commit reference (default: 'master') - limit: Maximum number of commits to retrieve (default: 10, max: 100) - start: Start index for pagination (default: 0) - - Returns: - JSON string with commit history or error information - """ - try: - if not project_key: - raise ValidationError("project_key is required") - if not repository_slug: - raise ValidationError("repository_slug is required") - - client = get_bitbucket_client(credentials) - - # Use the commits API endpoint - endpoint = f"/rest/api/1.0/projects/{project_key}/repos/{repository_slug}/commits" - params = { - "until": branch, - "limit": min(limit, 100), - "start": start - } - - data = client.get(endpoint, params=params) - - commits = [] - for commit in data.get("values", []): - author = commit.get("author", {}) - committer = commit.get("committer", {}) - - commits.append({ - "id": commit.get("id", ""), - "display_id": commit.get("displayId", ""), - "message": commit.get("message", ""), - "author_name": author.get("name", ""), - "author_email": author.get("emailAddress", ""), - "committer_name": committer.get("name", ""), - "committer_email": committer.get("emailAddress", ""), - "timestamp": commit.get("committerTimestamp", 0), - "parents": [p.get("id", "") for p in commit.get("parents", [])], - }) - - result = { - "project_key": project_key, - "repository": repository_slug, - "branch": branch, - "total": data.get("size", 0), - "is_last_page": data.get("isLastPage", True), - "commits": commits, - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except ValidationError as e: - return format_error_response("ValidationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) - - -def bitbucket_get_commit( - project_key: str, - repository_slug: str, - commit_id: str -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Get details of a specific commit. - - Args: - project_key: Project key (e.g., 'IN') - repository_slug: Repository slug (e.g., 'insights-pipeline') - commit_id: Commit ID or hash - - Returns: - JSON string with commit details or error information - """ - try: - if not project_key: - raise ValidationError("project_key is required") - if not repository_slug: - raise ValidationError("repository_slug is required") - if not commit_id: - raise ValidationError("commit_id is required") - - client = get_bitbucket_client(credentials) - - # Use the commit details API endpoint - endpoint = f"/rest/api/1.0/projects/{project_key}/repos/{repository_slug}/commits/{commit_id}" - - commit = client.get(endpoint) - - author = commit.get("author", {}) - committer = commit.get("committer", {}) - - result = { - "id": commit.get("id", ""), - "display_id": commit.get("displayId", ""), - "message": commit.get("message", ""), - "author_name": author.get("name", ""), - "author_email": author.get("emailAddress", ""), - "committer_name": committer.get("name", ""), - "committer_email": committer.get("emailAddress", ""), - "timestamp": commit.get("committerTimestamp", 0), - "parents": [p.get("id", "") for p in commit.get("parents", [])], - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except ValidationError as e: - return format_error_response("ValidationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) diff --git a/.claude/skills/atlassian-readonly-skills/scripts/bitbucket_files.py b/.claude/skills/atlassian-readonly-skills/scripts/bitbucket_files.py deleted file mode 100644 index 49c7a7d76a..0000000000 --- a/.claude/skills/atlassian-readonly-skills/scripts/bitbucket_files.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env python3 -"""Bitbucket file and search utilities. - -This module provides functions for retrieving file content -and searching code in Bitbucket Server/Data Center. -""" - -from typing import Optional, Dict, Any -from ._common import ( - AtlassianCredentials, - get_bitbucket_client, - format_json_response, - format_error_response, - ConfigurationError, - AuthenticationError, - ValidationError, - NotFoundError, - APIError, - NetworkError, -) - - -def bitbucket_get_file_content( - project_key: str, - repository_slug: str, - file_path: str, - branch: str = "master", - credentials: Optional[AtlassianCredentials] = None -) -> str: - """Get the content of a file from a repository. - - Args: - project_key: Project key (e.g., 'PROJ') - repository_slug: Repository slug - file_path: Path to the file in the repository - branch: Branch or commit reference (default: 'master') - - Returns: - JSON string with file content or error information - """ - try: - if not project_key: - raise ValidationError("project_key is required") - if not repository_slug: - raise ValidationError("repository_slug is required") - if not file_path: - raise ValidationError("file_path is required") - - client = get_bitbucket_client(credentials) - - # Use the browse API to get file content - endpoint = f"/rest/api/1.0/projects/{project_key}/repos/{repository_slug}/browse/{file_path}" - params = {"at": branch} - - data = client.get(endpoint, params=params) - - # Extract content from lines - lines = data.get("lines", []) - content = "\n".join([line.get("text", "") for line in lines]) - - result = { - "path": file_path, - "branch": branch, - "content": content, - "size": data.get("size"), - "is_last_page": data.get("isLastPage", True), - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except ValidationError as e: - return format_error_response("ValidationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) - - -def bitbucket_search( - query: str, - project_key: Optional[str] = None, - repository_slug: Optional[str] = None, - search_type: str = "code", - limit: int = 25, - start: int = 0 -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Search for code or files in Bitbucket Server. - - Args: - query: Search query string - project_key: Limit search to a specific project (optional) - repository_slug: Limit search to a specific repository (optional) - search_type: Type of search - 'code' or 'file' (default: 'code') - limit: Maximum number of results (default: 25, max: 100) - start: Start index for pagination (default: 0) - - Returns: - JSON string with search results or error information - """ - try: - if not query: - raise ValidationError("query is required") - - valid_types = ["code", "file"] - if search_type not in valid_types: - raise ValidationError(f"search_type must be one of: {', '.join(valid_types)}") - - client = get_bitbucket_client(credentials) - - # Build search query with filters - search_query = query - if search_type == "file": - if "ext:" not in query and not query.startswith('"'): - search_query = f'"{query}"' - - if project_key: - search_query += f" project:{project_key}" - if repository_slug and project_key: - search_query += f" repo:{project_key}/{repository_slug}" - - payload = { - "query": search_query, - "entities": { - "code": { - "start": start, - "limit": min(limit, 100) - } - } - } - - # Search API uses a different endpoint - endpoint = "/rest/search/latest/search" - data = client.post(endpoint, json=payload) - - code_results = data.get("code", {}) - results = [] - - for item in code_results.get("values", []): - repo = item.get("repository", {}) - project = repo.get("project", {}) - file_info = item.get("file", {}) - - # Handle file field - can be string or object depending on API version - if isinstance(file_info, str): - file_path = file_info - else: - file_path = file_info.get("toString", file_info.get("path", "")) - - results.append({ - "repository": repo.get("slug", ""), - "project": project.get("key", ""), - "file": file_path, - "hit_count": item.get("hitCount", 0), - "matches": [ - m.get("text", "") if isinstance(m, dict) else str(m) if not isinstance(m, list) else "" - for m in item.get("hitContexts", []) - ], - }) - - result = { - "query": search_query, - "total": code_results.get("count", 0), - "results": results, - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except ValidationError as e: - return format_error_response("ValidationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) diff --git a/.claude/skills/atlassian-readonly-skills/scripts/bitbucket_projects.py b/.claude/skills/atlassian-readonly-skills/scripts/bitbucket_projects.py deleted file mode 100644 index e46aadbe2c..0000000000 --- a/.claude/skills/atlassian-readonly-skills/scripts/bitbucket_projects.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env python3 -"""Bitbucket project and repository management utilities. - -This module provides functions for listing projects and repositories -in Bitbucket Server/Data Center. -""" - -from typing import Optional, Dict, Any, List -from ._common import ( - AtlassianCredentials, - get_bitbucket_client, - format_json_response, - format_error_response, - ConfigurationError, - AuthenticationError, - ValidationError, - NotFoundError, - APIError, - NetworkError, -) - - -def bitbucket_list_projects( - limit: int = 25, - start: int = 0, - credentials: Optional[AtlassianCredentials] = None -) -> str: - """List projects in Bitbucket Server. - - Args: - limit: Number of projects to return (default: 25, max: 100) - start: Start index for pagination (default: 0) - - Returns: - JSON string with list of projects or error information - """ - try: - client = get_bitbucket_client(credentials) - - params = { - "limit": min(limit, 100), - "start": start - } - - data = client.get("/rest/api/1.0/projects", params=params) - - projects = [] - for project in data.get("values", []): - projects.append({ - "key": project.get("key", ""), - "name": project.get("name", ""), - "description": project.get("description", ""), - "public": project.get("public", False), - "type": project.get("type", ""), - }) - - result = { - "projects": projects, - "total": len(projects), - "is_last_page": data.get("isLastPage", True), - "next_page_start": data.get("nextPageStart") - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) - - -def bitbucket_list_repositories( - project_key: Optional[str] = None, - limit: int = 25, - start: int = 0 -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """List repositories in Bitbucket Server. - - Args: - project_key: Project key to filter repositories (optional) - limit: Number of repositories to return (default: 25, max: 100) - start: Start index for pagination (default: 0) - - Returns: - JSON string with list of repositories or error information - """ - try: - client = get_bitbucket_client(credentials) - - params = { - "limit": min(limit, 100), - "start": start - } - - if project_key: - endpoint = f"/rest/api/1.0/projects/{project_key}/repos" - else: - endpoint = "/rest/api/1.0/repos" - - data = client.get(endpoint, params=params) - - repositories = [] - for repo in data.get("values", []): - project = repo.get("project", {}) - repositories.append({ - "slug": repo.get("slug", ""), - "name": repo.get("name", ""), - "description": repo.get("description", ""), - "project_key": project.get("key", ""), - "project_name": project.get("name", ""), - "public": repo.get("public", False), - "state": repo.get("state", ""), - "forkable": repo.get("forkable", True), - }) - - result = { - "repositories": repositories, - "total": len(repositories), - "is_last_page": data.get("isLastPage", True), - "next_page_start": data.get("nextPageStart") - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) diff --git a/.claude/skills/atlassian-readonly-skills/scripts/bitbucket_pull_requests.py b/.claude/skills/atlassian-readonly-skills/scripts/bitbucket_pull_requests.py deleted file mode 100644 index bd64b4716a..0000000000 --- a/.claude/skills/atlassian-readonly-skills/scripts/bitbucket_pull_requests.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env python3 -"""Bitbucket pull request management utilities. - -This module provides functions for viewing pull requests -in Bitbucket Server/Data Center. -""" - -from typing import Optional, Dict, Any, List -from ._common import ( - get_bitbucket_client, - format_json_response, - format_error_response, - ConfigurationError, - AuthenticationError, - ValidationError, - NotFoundError, - APIError, - NetworkError, -) - - -def _simplify_pull_request(pr_data: Dict[str, Any]) -> Dict[str, Any]: - """Simplify pull request data to essential fields.""" - from_ref = pr_data.get("fromRef", {}) - to_ref = pr_data.get("toRef", {}) - author = pr_data.get("author", {}).get("user", {}) - - reviewers = [] - for reviewer in pr_data.get("reviewers", []): - user = reviewer.get("user", {}) - reviewers.append({ - "name": user.get("name", ""), - "email": user.get("emailAddress", ""), - "status": reviewer.get("status", ""), - "approved": reviewer.get("approved", False), - }) - - return { - "id": pr_data.get("id"), - "title": pr_data.get("title", ""), - "description": pr_data.get("description", ""), - "state": pr_data.get("state", ""), - "version": pr_data.get("version"), - "source_branch": from_ref.get("displayId", ""), - "target_branch": to_ref.get("displayId", ""), - "source_repo": from_ref.get("repository", {}).get("slug", ""), - "target_repo": to_ref.get("repository", {}).get("slug", ""), - "project_key": to_ref.get("repository", {}).get("project", {}).get("key", ""), - "author": author.get("name", ""), - "author_email": author.get("emailAddress", ""), - "created": pr_data.get("createdDate"), - "updated": pr_data.get("updatedDate"), - "reviewers": reviewers, - "open": pr_data.get("open", False), - "closed": pr_data.get("closed", False), - "locked": pr_data.get("locked", False), - } - - -def bitbucket_get_pull_request( - project_key: str, - repository_slug: str, - pr_id: int -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Get details of a pull request. - - Args: - project_key: Project key (e.g., 'PROJ') - repository_slug: Repository slug - pr_id: Pull request ID - - Returns: - JSON string with pull request details or error information - """ - try: - if not project_key: - raise ValidationError("project_key is required") - if not repository_slug: - raise ValidationError("repository_slug is required") - if not pr_id: - raise ValidationError("pr_id is required") - - client = get_bitbucket_client(credentials) - - endpoint = f"/rest/api/1.0/projects/{project_key}/repos/{repository_slug}/pull-requests/{pr_id}" - data = client.get(endpoint) - - return format_json_response(_simplify_pull_request(data)) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except ValidationError as e: - return format_error_response("ValidationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) - - -def bitbucket_get_pr_diff( - project_key: str, - repository_slug: str, - pr_id: int -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Get the diff of a pull request. - - Args: - project_key: Project key (e.g., 'PROJ') - repository_slug: Repository slug - pr_id: Pull request ID - - Returns: - JSON string with diff information or error information - """ - try: - if not project_key: - raise ValidationError("project_key is required") - if not repository_slug: - raise ValidationError("repository_slug is required") - if not pr_id: - raise ValidationError("pr_id is required") - - client = get_bitbucket_client(credentials) - - endpoint = f"/rest/api/1.0/projects/{project_key}/repos/{repository_slug}/pull-requests/{pr_id}/diff" - data = client.get(endpoint) - - return format_json_response(data) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except ValidationError as e: - return format_error_response("ValidationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) diff --git a/.claude/skills/atlassian-readonly-skills/scripts/confluence_comments.py b/.claude/skills/atlassian-readonly-skills/scripts/confluence_comments.py deleted file mode 100644 index e5c6e75b24..0000000000 --- a/.claude/skills/atlassian-readonly-skills/scripts/confluence_comments.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Confluence comment management tools. - -Tools: - - confluence_get_comments: Get comments for a page -""" - -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent)) - -from typing import Any, Dict, Optional - -from _common import ( - AtlassianCredentials, - get_confluence_client, - format_json_response, - format_error_response, - ConfigurationError, - AuthenticationError, - ValidationError, - NotFoundError, - APIError, - NetworkError, -) - - -def _simplify_comment(comment_data: Dict[str, Any]) -> Dict[str, Any]: - """Simplify comment data to essential fields.""" - body = comment_data.get('body', {}) - storage = body.get('storage', {}) or body.get('view', {}) - - return { - 'id': comment_data.get('id', ''), - 'content': storage.get('value', ''), - 'created': comment_data.get('history', {}).get('createdDate', ''), - 'author': comment_data.get('history', {}).get('createdBy', {}).get( - 'displayName', '' - ) - } - - -def confluence_get_comments( - page_id: str, - credentials: Optional[AtlassianCredentials] = None -) -> str: - """Get all comments for a Confluence page. - - Args: - page_id: Page ID to get comments for - - Returns: - JSON string with list of comments or error information - """ - try: - client = get_confluence_client(credentials) - - if not page_id: - raise ValidationError('page_id is required') - - params = { - 'expand': 'body.storage,history', - 'depth': 'all' - } - response = client.get( - f'/rest/api/content/{page_id}/child/comment', params=params - ) - - comments = response.get('results', []) - simplified_comments = [_simplify_comment(c) for c in comments] - - result = { - 'comments': simplified_comments, - 'count': len(simplified_comments), - 'page_id': page_id - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response('ConfigurationError', str(e)) - except AuthenticationError as e: - return format_error_response('AuthenticationError', str(e)) - except ValidationError as e: - return format_error_response('ValidationError', str(e)) - except NotFoundError as e: - return format_error_response('NotFoundError', str(e)) - except (APIError, NetworkError) as e: - return format_error_response(type(e).__name__, str(e)) - except Exception as e: - return format_error_response('UnexpectedError', f'Unexpected error: {str(e)}') diff --git a/.claude/skills/atlassian-readonly-skills/scripts/confluence_labels.py b/.claude/skills/atlassian-readonly-skills/scripts/confluence_labels.py deleted file mode 100644 index 0f03c1aee8..0000000000 --- a/.claude/skills/atlassian-readonly-skills/scripts/confluence_labels.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Confluence label management tools. - -Tools: - - confluence_get_labels: Get labels for a page -""" - -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent)) - -from typing import Any, Dict, Optional - -from _common import ( - AtlassianCredentials, - get_confluence_client, - format_json_response, - format_error_response, - ConfigurationError, - AuthenticationError, - ValidationError, - NotFoundError, - APIError, - NetworkError, -) - - -def confluence_get_labels( - page_id: str, - credentials: Optional[AtlassianCredentials] = None -) -> str: - """Get all labels for a Confluence page. - - Args: - page_id: Page ID to get labels for - - Returns: - JSON string with list of labels or error information - """ - try: - client = get_confluence_client(credentials) - - if not page_id: - raise ValidationError('page_id is required') - - response = client.get(f'/rest/api/content/{page_id}/label') - - labels = response.get('results', []) - simplified_labels = [ - {'name': label.get('name', ''), 'prefix': label.get('prefix', '')} - for label in labels - ] - - result = { - 'labels': simplified_labels, - 'count': len(simplified_labels), - 'page_id': page_id - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response('ConfigurationError', str(e)) - except AuthenticationError as e: - return format_error_response('AuthenticationError', str(e)) - except ValidationError as e: - return format_error_response('ValidationError', str(e)) - except NotFoundError as e: - return format_error_response('NotFoundError', str(e)) - except (APIError, NetworkError) as e: - return format_error_response(type(e).__name__, str(e)) - except Exception as e: - return format_error_response('UnexpectedError', f'Unexpected error: {str(e)}') diff --git a/.claude/skills/atlassian-readonly-skills/scripts/confluence_pages.py b/.claude/skills/atlassian-readonly-skills/scripts/confluence_pages.py deleted file mode 100644 index c9cafb0a0c..0000000000 --- a/.claude/skills/atlassian-readonly-skills/scripts/confluence_pages.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Confluence page management tools. - -Tools: - - confluence_get_page: Get a page by ID or title -""" - -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent)) - -from typing import Any, Dict, Optional - -from _common import ( - AtlassianCredentials, - get_confluence_client, - format_json_response, - format_error_response, - ConfigurationError, - AuthenticationError, - ValidationError, - NotFoundError, - APIError, - NetworkError, -) - - -def _simplify_page(page_data: Dict[str, Any]) -> Dict[str, Any]: - """Simplify page data to essential fields.""" - body = page_data.get('body', {}) - storage = body.get('storage', {}) or body.get('view', {}) - - return { - 'id': page_data.get('id', ''), - 'title': page_data.get('title', ''), - 'space_key': page_data.get('space', {}).get('key', ''), - 'version': page_data.get('version', {}).get('number', 1), - 'content': storage.get('value', ''), - 'created': page_data.get('history', {}).get('createdDate', ''), - 'updated': page_data.get('version', {}).get('when', ''), - 'url': page_data.get('_links', {}).get('webui', '') - } - - -def confluence_get_page( - page_id: Optional[str] = None, - title: Optional[str] = None, - space_key: Optional[str] = None -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Get a Confluence page by ID or by title and space. - - Args: - page_id: Page ID (optional if title and space_key provided) - title: Page title (optional if page_id provided) - space_key: Space key (required if using title) - - Returns: - JSON string with page data or error information - """ - try: - client = get_confluence_client(credentials) - - if not page_id and not title: - raise ValidationError('Either page_id or title is required') - if title and not space_key: - raise ValidationError('space_key is required when using title') - - if page_id: - params = {'expand': 'body.storage,version,space,history'} - page_data = client.get(f'/rest/api/content/{page_id}', params=params) - else: - params = { - 'title': title, - 'spaceKey': space_key, - 'expand': 'body.storage,version,space,history' - } - response = client.get('/rest/api/content', params=params) - results = response.get('results', []) - if not results: - raise NotFoundError(f'Page not found: {title} in space {space_key}') - page_data = results[0] - - simplified = _simplify_page(page_data) - return format_json_response(simplified) - - except ConfigurationError as e: - return format_error_response('ConfigurationError', str(e)) - except AuthenticationError as e: - return format_error_response('AuthenticationError', str(e)) - except ValidationError as e: - return format_error_response('ValidationError', str(e)) - except NotFoundError as e: - return format_error_response('NotFoundError', str(e)) - except (APIError, NetworkError) as e: - return format_error_response(type(e).__name__, str(e)) - except Exception as e: - return format_error_response('UnexpectedError', f'Unexpected error: {str(e)}') diff --git a/.claude/skills/atlassian-readonly-skills/scripts/confluence_search.py b/.claude/skills/atlassian-readonly-skills/scripts/confluence_search.py deleted file mode 100644 index f03bd04d20..0000000000 --- a/.claude/skills/atlassian-readonly-skills/scripts/confluence_search.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Confluence search tools. - -Tools: - - confluence_search: Search content using CQL -""" - -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent)) - -from typing import Any, Dict, Optional - -from _common import ( - AtlassianCredentials, - get_confluence_client, - format_json_response, - format_error_response, - ConfigurationError, - AuthenticationError, - ValidationError, - APIError, - NetworkError, -) - - -def _simplify_search_result(result: Dict[str, Any]) -> Dict[str, Any]: - """Simplify search result to essential fields.""" - content = result.get('content', result) - return { - 'id': content.get('id', ''), - 'title': content.get('title', result.get('title', '')), - 'type': content.get('type', ''), - 'space_key': content.get('space', {}).get('key', ''), - 'url': result.get('url', content.get('_links', {}).get('webui', '')), - 'excerpt': result.get('excerpt', ''), - 'last_modified': result.get('lastModified', '') - } - - -def confluence_search( - query: str, - limit: int = 10, - start_at: int = 0 -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Search for Confluence content using CQL. - - Args: - query: Search query (text or CQL) - limit: Maximum number of results (default: 10) - start_at: Index of first result for pagination (default: 0) - - Returns: - JSON string with search results or error information - """ - try: - client = get_confluence_client(credentials) - - if not query: - raise ValidationError('query is required') - if limit < 0: - raise ValidationError('limit must be non-negative') - if start_at < 0: - raise ValidationError('start_at must be non-negative') - - # Build CQL query - if not any(op in query for op in ['=', '~', 'AND', 'OR', 'NOT']): - cql = f'text ~ "{query}" OR title ~ "{query}"' - else: - cql = query - - params: Dict[str, Any] = { - 'cql': cql, - 'limit': limit, - 'start': start_at - } - - response = client.get('/rest/api/content/search', params=params) - - results = response.get('results', []) - simplified_results = [_simplify_search_result(r) for r in results] - - result = { - 'results': simplified_results, - 'total': response.get('totalSize', len(results)), - 'start_at': start_at, - 'limit': limit, - 'is_last': start_at + len(results) >= response.get('totalSize', 0) - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response('ConfigurationError', str(e)) - except AuthenticationError as e: - return format_error_response('AuthenticationError', str(e)) - except ValidationError as e: - return format_error_response('ValidationError', str(e)) - except (APIError, NetworkError) as e: - return format_error_response(type(e).__name__, str(e)) - except Exception as e: - return format_error_response('UnexpectedError', f'Unexpected error: {str(e)}') diff --git a/.claude/skills/atlassian-skills/PROVENANCE.md b/.claude/skills/atlassian-skills/PROVENANCE.md index 67b13a6168..c7dc598698 100644 --- a/.claude/skills/atlassian-skills/PROVENANCE.md +++ b/.claude/skills/atlassian-skills/PROVENANCE.md @@ -50,3 +50,17 @@ FieldWorks-owned skills. It is ~740 lines of API reference that loads only when Atlassian work happens, and keeping it close to upstream is worth more than the context saving. The near-total duplication with `atlassian-readonly-skills` is upstream's design, not something introduced here. + +## Jira only + +Confluence and Bitbucket were removed entirely on 2026-08-21. FieldWorks uses +Jira and nothing else, and nothing in the repository referenced either. + +Removed: the eight `confluence_*` and `bitbucket_*` script modules, their +documentation in `SKILL.md` and `REFERENCE.md`, their configuration blocks and +credential fields, and their plumbing in `_common.py` -- the dataclass fields, +`is_*_available` checks, `get_*_client` factories and the service branches in +`AtlassianConfig.from_credentials`. + +This is a hard fork from upstream for these two skills. A re-sync is no longer +a merge; treat upstream as a source to cherry-pick Jira fixes from. diff --git a/.claude/skills/atlassian-skills/REFERENCE.md b/.claude/skills/atlassian-skills/REFERENCE.md index 0dfa0218d6..73e1f8fe95 100644 --- a/.claude/skills/atlassian-skills/REFERENCE.md +++ b/.claude/skills/atlassian-skills/REFERENCE.md @@ -1,6 +1,6 @@ # Atlassian Skills API Reference -Detailed usage examples and API documentation for Jira, Confluence, and Bitbucket tools. +Detailed usage examples and API documentation for the Jira tools. ## Configuration Modes @@ -199,80 +199,6 @@ result = jira_create_sprint( ) ``` -## Confluence Examples - -### Search for Pages - -```python -from scripts.confluence_search import confluence_search - -result = confluence_search("API documentation", limit=10) -``` - -### Get a Page - -```python -from scripts.confluence_pages import confluence_get_page - -# Get by ID -result = confluence_get_page(page_id="123456") - -# Get by title and space -result = confluence_get_page(title="API Guide", space_key="DEV") -``` - -### Create a Page - -```python -from scripts.confluence_pages import confluence_create_page - -result = confluence_create_page( - space_key="DEV", - title="New Documentation Page", - content="# Welcome\n\nThis is **markdown** content.", - parent_id="123456" -) -``` - -### Update a Page - -```python -from scripts.confluence_pages import confluence_update_page - -result = confluence_update_page( - page_id="123456", - title="Updated Title", - content="# Updated Content\n\nNew information here." -) -``` - -### Manage Comments - -```python -from scripts.confluence_comments import confluence_add_comment, confluence_get_comments - -# Get comments -comments = confluence_get_comments(page_id="123456") - -# Add comment -result = confluence_add_comment( - page_id="123456", - content="Great documentation!" -) -``` - -### Manage Labels - -```python -from scripts.confluence_labels import confluence_add_label, confluence_remove_label - -# Add label -result = confluence_add_label(page_id="123456", name="api-docs") - -# Remove label -result = confluence_remove_label(page_id="123456", name="outdated") -``` - ## JQL Query Examples Common JQL patterns for searching Jira issues: @@ -306,33 +232,6 @@ reporter = "user@example.com" project = MYPROJ AND status = "In Progress" AND assignee = currentUser() ORDER BY priority DESC ``` -## CQL Query Examples - -Common CQL patterns for searching Confluence: - -``` -# Search by text -text ~ "API documentation" - -# Search in specific space -space = DEV AND text ~ "guide" - -# Search by title -title ~ "Getting Started" - -# Search by label -label = "api-docs" - -# Search by type -type = page AND space = DEV - -# Recently modified -lastModified >= now("-7d") - -# Created by user -creator = "user@example.com" -``` - ## Time Format Reference For worklog entries, use these time formats: @@ -381,244 +280,49 @@ For worklog entries, use these time formats: } ``` -## Bitbucket Examples - -### Get Recent Commits - -```python -from scripts.bitbucket_commits import bitbucket_get_commits -import json - -# Get latest 10 commits from master branch -result = bitbucket_get_commits( - project_key="PROJ", - repository_slug="my-repo", - branch="master", - limit=10 -) - -# Parse and display commits -data = json.loads(result) -for commit in data['commits']: - print(f"{commit['display_id']}: {commit['message']}") - print(f" Author: {commit['author_name']}") - print(f" Date: {commit['timestamp']}") -``` - -### Get Specific Commit Details - -```python -from scripts.bitbucket_commits import bitbucket_get_commit -import json - -# Get details of a specific commit -result = bitbucket_get_commit( - project_key="PROJ", - repository_slug="my-repo", - commit_id="1da11eaec25aed8b251de24841885c91493b3173" -) - -# Parse commit details -commit = json.loads(result) -print(f"Commit: {commit['display_id']}") -print(f"Message: {commit['message']}") -print(f"Author: {commit['author_name']} <{commit['author_email']}>") -print(f"Timestamp: {commit['timestamp']}") -``` - -### List Pull Requests - -```python -from scripts.bitbucket_pull_requests import bitbucket_get_pull_request - -# Get PR details -result = bitbucket_get_pull_request( - project_key="PROJ", - repository_slug="my-repo", - pr_id=123 -) -``` - -### Search Code - -```python -from scripts.bitbucket_files import bitbucket_search - -# Search for specific code pattern -result = bitbucket_search( - query="def authenticate", - project_key="PROJ", - search_type="code", - limit=25 -) -``` - -### Bitbucket Commit Response Structure - -```json -{ - "project_key": "PROJ", - "repository": "my-repo", - "branch": "master", - "total": 5, - "is_last_page": false, - "commits": [ - { - "id": "1da11eaec25aed8b251de24841885c91493b3173", - "display_id": "1da11eaec25", - "message": "Feature: Add new API endpoint", - "author_name": "John Doe", - "author_email": "john.doe@company.com", - "committer_name": "John Doe", - "committer_email": "john.doe@company.com", - "timestamp": 1765534577000, - "parents": ["b97ad25d330e36480b045c5dea36f97999297ff6"] - } - ] -} -``` - ## Agent Mode Complete Example -When deploying in Agent environments without environment variables: - ```python -from scripts._common import AtlassianCredentials, check_available_skills -from scripts.jira_issues import jira_create_issue, jira_get_issue -from scripts.jira_search import jira_search -from scripts.confluence_pages import confluence_create_page import json +from scripts._common import AtlassianCredentials, check_available_skills +from scripts.jira_issues import jira_create_issue, jira_add_comment -# Step 1: Create credentials object with all needed services credentials = AtlassianCredentials( - # Jira configuration - jira_url="https://company.atlassian.net", - jira_username="user@company.com", - jira_api_token="jira_token_here", - - # Confluence configuration - confluence_url="https://company.atlassian.net/wiki", - confluence_username="user@company.com", - confluence_api_token="confluence_token_here", - - # Bitbucket configuration (optional) - bitbucket_url="https://bitbucket.company.com", - bitbucket_pat_token="bitbucket_pat_here" + jira_url="https://jira.sil.org", + jira_pat_token="your_pat_token", ) -# Step 2: Check which services are available availability = check_available_skills(credentials) -print(f"Available: {availability['available_services']}") -print(f"Unavailable: {availability['unavailable_services']}") - -# Step 3: Use skills with credentials parameter -if "jira" in availability["available_services"]: - # Create an issue - result = jira_create_issue( - project_key="PROJ", - summary="New task from Agent", - issue_type="Task", - credentials=credentials # Pass credentials - ) - issue = json.loads(result) - - if not issue.get("error"): - print(f"Created issue: {issue['key']}") - - # Get the issue - result = jira_get_issue( - issue_key=issue['key'], - credentials=credentials - ) - - # Search for issues - result = jira_search( - jql=f"project = PROJ AND key = {issue['key']}", - credentials=credentials - ) - -if "confluence" in availability["available_services"]: - # Create a Confluence page - result = confluence_create_page( - space_key="TEAM", - title="Documentation from Agent", - content="

Created by Agent

", - credentials=credentials - ) - page = json.loads(result) - if not page.get("error"): - print(f"Created page: {page['title']}") - -# Step 4: Handle unavailable services -if "bitbucket" not in availability["available_services"]: - reason = availability["unavailable_services"].get("bitbucket", "Unknown") - print(f"Bitbucket unavailable: {reason}") -``` - -## Partial Service Configuration - -You can configure only the services you need: - -```python -from scripts._common import AtlassianCredentials, check_available_skills - -# Only Jira configured -jira_only_creds = AtlassianCredentials( - jira_url="https://company.atlassian.net", - jira_username="user@company.com", - jira_api_token="token" -) - -# Check availability -availability = check_available_skills(jira_only_creds) -# Returns: { -# "available_services": ["jira"], -# "unavailable_services": { -# "confluence": "Missing confluence_url", -# "bitbucket": "Missing bitbucket_url" -# } -# } - -# Jira functions work -from scripts.jira_issues import jira_get_issue -result = jira_get_issue("PROJ-123", credentials=jira_only_creds) # ✓ Works - -# Confluence functions fail with clear error -from scripts.confluence_pages import confluence_get_page -result = confluence_get_page("Page", "SPACE", credentials=jira_only_creds) # ✗ Fails -# Returns: {"error": "Confluence credentials not provided...", "error_type": "ConfigurationError"} +if "jira" not in availability["available_services"]: + raise SystemExit(availability["unavailable_services"]["jira"]) + +created = json.loads(jira_create_issue( + project_key="LT", + summary="Example issue", + issue_type="Bug", + description="Filed from agent mode.", + custom_fields={"versions": [{"name": "FW 9.3"}]}, + credentials=credentials, +)) +jira_add_comment(created["key"], "First comment.", credentials=credentials) ``` ## Credentials Object Reference ```python AtlassianCredentials( - # Jira jira_url: Optional[str] = None, jira_username: Optional[str] = None, jira_api_token: Optional[str] = None, jira_pat_token: Optional[str] = None, - jira_api_version: Optional[str] = None, # '2' or '3', auto-detected if not set - jira_ssl_verify: bool = False, - - # Confluence - confluence_url: Optional[str] = None, - confluence_username: Optional[str] = None, - confluence_api_token: Optional[str] = None, - confluence_pat_token: Optional[str] = None, - confluence_api_version: Optional[str] = None, - confluence_ssl_verify: bool = False, - - # Bitbucket - bitbucket_url: Optional[str] = None, - bitbucket_username: Optional[str] = None, - bitbucket_api_token: Optional[str] = None, - bitbucket_pat_token: Optional[str] = None, - bitbucket_api_version: Optional[str] = None, - bitbucket_ssl_verify: bool = False + jira_api_version: Optional[str] = None, + jira_ssl_verify: bool = True, ) ``` -For each service, provide either: -- **PAT Token** (for Data Center/Server): `{service}_pat_token` -- **Username + API Token** (for Cloud): `{service}_username` + `{service}_api_token` +For SIL's Data Center instance, `jira_url` plus `jira_pat_token` is enough. +`jira_username` and `jira_api_token` are the Cloud pairing. A PAT token wins if +both are supplied. + +`check_available_skills(credentials)` returns `available_services` and +`unavailable_services`, the latter naming the missing field. diff --git a/.claude/skills/atlassian-skills/SKILL.md b/.claude/skills/atlassian-skills/SKILL.md index ed05cb53b7..be3ef0c0f9 100644 --- a/.claude/skills/atlassian-skills/SKILL.md +++ b/.claude/skills/atlassian-skills/SKILL.md @@ -1,12 +1,12 @@ --- name: atlassian-skills -description: Python utilities for Jira, Confluence, and Bitbucket integration. Provides issue management, search, workflows, page management, pull requests, commit history, and more. Use when users need to interact with Atlassian products like "create a Jira issue", "search Confluence pages", "create a pull request", "get commit history", or "update sprint status". +description: Python utilities for Jira (SIL Data Center) covering issue management, JQL search, workflows and transitions, links, agile boards, worklogs and projects. Use when users need to create, update, comment on, link or transition a Jira issue, or search Jira with JQL -- including any LT-prefixed FieldWorks ticket. license: Complete terms in LICENSE --- # Atlassian Skills -Python utilities for Jira, Confluence, and Bitbucket integration, supporting both Cloud and Data Center deployments. +Python utilities for Jira, supporting both Cloud and Data Center deployments. ## FieldWorks / SIL JIRA Integration @@ -49,225 +49,63 @@ For read-only operations (get issue, search, get comments), use `atlassian-reado ## Configuration -Two configuration modes are supported: +Jira only. Confluence and Bitbucket support was removed from this copy -- see +`PROVENANCE.md`. -### Mode 1: Environment Variables (Traditional) +### Mode 1: environment variables -Set environment variables based on your deployment type. This mode is used when `credentials` parameter is not provided to skill functions. - -#### SIL JIRA (Data Center / PAT Token) +SIL JIRA (Data Center, PAT token) -- this is the one FieldWorks uses: ```bash -# SIL JIRA instance for LT-* tickets JIRA_URL=https://jira.sil.org -# Personal Access Token - generate at: https://jira.sil.org/secure/ViewProfile.jspa → Personal Access Tokens +# Generate at https://jira.sil.org/secure/ViewProfile.jspa -> Personal Access Tokens JIRA_PAT_TOKEN=your_jira_pat_token_here ``` -#### Cloud (API Token) +Jira Cloud (API token), for completeness: ```bash -# Jira Cloud JIRA_URL=https://your-company.atlassian.net JIRA_USERNAME=your.email@company.com JIRA_API_TOKEN=your_api_token - -# Confluence Cloud -CONFLUENCE_URL=https://your-company.atlassian.net/wiki -CONFLUENCE_USERNAME=your.email@company.com -CONFLUENCE_API_TOKEN=your_api_token -``` - -Generate API tokens at: https://id.atlassian.com/manage-profile/security/api-tokens - -#### Data Center / Server (PAT Token) - -```bash -# Jira Data Center -JIRA_URL=https://jira.your-company.com -JIRA_PAT_TOKEN=your_pat_token - -# Confluence Data Center -CONFLUENCE_URL=https://confluence.your-company.com -CONFLUENCE_PAT_TOKEN=your_pat_token - -# Bitbucket Server/Data Center -BITBUCKET_URL=https://bitbucket.your-company.com -BITBUCKET_PAT_TOKEN=your_pat_token ``` -> **Note**: PAT Token takes precedence if both are provided. +A PAT token takes precedence if both are provided. -### Mode 2: Parameter-Based (Agent Environments) - -When deploying skills in Agent environments where environment variables are not available, pass credentials directly to skill functions using the `AtlassianCredentials` object. +### Mode 2: credentials parameter (agent environments) ```python -from scripts._common import AtlassianCredentials, check_available_skills -from scripts.jira_issues import jira_create_issue, jira_get_issue - -# Create credentials object -credentials = AtlassianCredentials( - # Jira configuration - jira_url="https://your-company.atlassian.net", - jira_username="your.email@company.com", - jira_api_token="your_api_token", - - # Confluence configuration (optional) - confluence_url="https://your-company.atlassian.net/wiki", - confluence_username="your.email@company.com", - confluence_api_token="your_api_token", - - # Bitbucket configuration (optional) - # bitbucket_url="https://bitbucket.your-company.com", - # bitbucket_pat_token="your_pat_token" -) - -# Check which services are available -availability = check_available_skills(credentials) -print(availability["available_services"]) # ["jira", "confluence"] -print(availability["unavailable_services"]) # {"bitbucket": "Missing bitbucket_url"} - -# Use skills with credentials parameter -result = jira_create_issue( - project_key="PROJ", - summary="New task", - issue_type="Task", - credentials=credentials # Pass credentials here -) -``` - -#### Partial Service Configuration - -You can configure only the services you need. Services without complete credentials will be unavailable: +from scripts._common import AtlassianCredentials -```python -# Only configure Jira credentials = AtlassianCredentials( - jira_url="https://your-company.atlassian.net", - jira_username="your.email@company.com", - jira_api_token="your_api_token" + jira_url="https://jira.sil.org", + jira_pat_token="your_pat_token", ) - -# Jira skills will work -jira_get_issue("PROJ-123", credentials=credentials) # ✓ Works - -# Confluence/Bitbucket skills will fail with ConfigurationError -confluence_get_page("Page Title", "SPACE", credentials=credentials) # ✗ Fails ``` -#### Credentials Object Fields - -```python -AtlassianCredentials( - # Jira - jira_url: Optional[str] = None, - jira_username: Optional[str] = None, - jira_api_token: Optional[str] = None, - jira_pat_token: Optional[str] = None, - jira_api_version: Optional[str] = None, # '2' or '3', auto-detected if not set - jira_ssl_verify: bool = False, - - # Confluence - confluence_url: Optional[str] = None, - confluence_username: Optional[str] = None, - confluence_api_token: Optional[str] = None, - confluence_pat_token: Optional[str] = None, - confluence_api_version: Optional[str] = None, - confluence_ssl_verify: bool = False, - - # Bitbucket - bitbucket_url: Optional[str] = None, - bitbucket_username: Optional[str] = None, - bitbucket_api_token: Optional[str] = None, - bitbucket_pat_token: Optional[str] = None, - bitbucket_api_version: Optional[str] = None, - bitbucket_ssl_verify: bool = False -) -``` - -For each service, provide either: -- PAT Token (for Data Center/Server): `{service}_pat_token` -- Username + API Token (for Cloud): `{service}_username` + `{service}_api_token` +Every function takes an optional `credentials` argument. Without one, the +environment variables above are used. ## Core Workflow -### Using Environment Variables - ```python -from scripts.jira_issues import jira_create_issue, jira_get_issue -from scripts.confluence_pages import confluence_create_page import json +from scripts.jira_issues import jira_get_issue +from scripts.jira_search import jira_search -# 1. Create a Jira issue -result = jira_create_issue( - project_key="PROJ", - summary="Implement new feature", - issue_type="Task", - description="Feature description here", - priority="High" -) -issue = json.loads(result) -print(f"Created: {issue['key']}") - -# 2. Get issue details -result = jira_get_issue(issue_key="PROJ-123") -issue = json.loads(result) - -# 3. Create a Confluence page -result = confluence_create_page( - space_key="DEV", - title="Feature Documentation", - content="

Documentation content here

" -) -``` - -### Using Credentials Parameter (Agent Mode) - -```python -from scripts._common import AtlassianCredentials -from scripts.jira_issues import jira_create_issue, jira_get_issue -from scripts.confluence_pages import confluence_create_page -import json +issue = json.loads(jira_get_issue(issue_key="LT-22382")) +print(f"{issue['key']} - {issue['summary']}") -# Create credentials -credentials = AtlassianCredentials( - jira_url="https://company.atlassian.net", - jira_username="user@company.com", - jira_api_token="token123", - confluence_url="https://company.atlassian.net/wiki", - confluence_username="user@company.com", - confluence_api_token="token123" -) - -# 1. Create a Jira issue with credentials -result = jira_create_issue( - project_key="PROJ", - summary="Implement new feature", - issue_type="Task", - description="Feature description here", - priority="High", - credentials=credentials # Pass credentials -) -issue = json.loads(result) -print(f"Created: {issue['key']}") - -# 2. Get issue details with credentials -result = jira_get_issue( - issue_key="PROJ-123", - credentials=credentials -) -issue = json.loads(result) - -# 3. Create a Confluence page with credentials -result = confluence_create_page( - space_key="DEV", - title="Feature Documentation", - content="

Documentation content here

", - credentials=credentials -) +results = json.loads(jira_search( + jql="project = LT AND status = 'In Progress'", + fields="summary,status,assignee", + limit=50, +)) ``` +Pass `credentials=credentials` to any of them to use Mode 2 instead of the +environment. + ## Available Utilities ### Jira Issue Management (`scripts.jira_issues`) @@ -411,185 +249,6 @@ jira_create_version( ) ``` -### Confluence Pages (`scripts.confluence_pages`) - -```python -from scripts.confluence_pages import ( - confluence_get_page, - confluence_create_page, - confluence_update_page, - confluence_delete_page -) - -# Get page by title -confluence_get_page(title="Meeting Notes", space_key="TEAM") - -# Create page with parent -confluence_create_page( - space_key="DEV", - title="API Documentation", - content="

API Docs

Content here

", - parent_id="12345" -) - -# Update page -confluence_update_page( - page_id="67890", - title="Updated Title", - content="

New content

" -) -``` - -### Confluence Search (`scripts.confluence_search`) - -```python -from scripts.confluence_search import confluence_search - -# Search with CQL -confluence_search( - query="space = DEV AND type = page AND text ~ 'API'", - limit=25 -) -``` - -### Confluence Comments (`scripts.confluence_comments`) - -```python -from scripts.confluence_comments import confluence_get_comments, confluence_add_comment - -# Add comment -confluence_add_comment( - page_id="12345", - content="Great documentation!" -) -``` - -### Confluence Labels (`scripts.confluence_labels`) - -```python -from scripts.confluence_labels import ( - confluence_get_labels, - confluence_add_label, - confluence_remove_label -) - -# Manage labels -confluence_add_label(page_id="12345", name="reviewed") -confluence_remove_label(page_id="12345", name="draft") -``` - -### Bitbucket Projects (`scripts.bitbucket_projects`) - -```python -from scripts.bitbucket_projects import ( - bitbucket_list_projects, - bitbucket_list_repositories -) - -# List all projects -bitbucket_list_projects(limit=25) - -# List repositories in a project -bitbucket_list_repositories(project_key="PROJ", limit=50) -``` - -### Bitbucket Pull Requests (`scripts.bitbucket_pull_requests`) - -```python -from scripts.bitbucket_pull_requests import ( - bitbucket_create_pull_request, - bitbucket_get_pull_request, - bitbucket_merge_pull_request, - bitbucket_decline_pull_request, - bitbucket_add_pr_comment, - bitbucket_get_pr_diff -) - -# Create a pull request -bitbucket_create_pull_request( - project_key="PROJ", - repository_slug="my-repo", - title="Feature: Add new API", - source_branch="feature/new-api", - target_branch="master", - description="Implements the new API endpoint", - reviewers=["john.doe", "jane.smith"] -) - -# Get PR details -bitbucket_get_pull_request( - project_key="PROJ", - repository_slug="my-repo", - pr_id=123 -) - -# Merge PR (version from get_pull_request) -bitbucket_merge_pull_request( - project_key="PROJ", - repository_slug="my-repo", - pr_id=123, - version=5, - strategy="squash" -) - -# Add comment to PR -bitbucket_add_pr_comment( - project_key="PROJ", - repository_slug="my-repo", - pr_id=123, - text="LGTM!" -) -``` - -### Bitbucket Files & Search (`scripts.bitbucket_files`) - -```python -from scripts.bitbucket_files import ( - bitbucket_get_file_content, - bitbucket_search -) - -# Get file content -bitbucket_get_file_content( - project_key="PROJ", - repository_slug="my-repo", - file_path="src/main.py", - branch="develop" -) - -# Search code -bitbucket_search( - query="def authenticate", - project_key="PROJ", - search_type="code", - limit=25 -) -``` - -### Bitbucket Commits (`scripts.bitbucket_commits`) - -```python -from scripts.bitbucket_commits import ( - bitbucket_get_commits, - bitbucket_get_commit -) - -# Get recent commits from a branch -bitbucket_get_commits( - project_key="PROJ", - repository_slug="my-repo", - branch="master", - limit=10 -) - -# Get details of a specific commit -bitbucket_get_commit( - project_key="PROJ", - repository_slug="my-repo", - commit_id="1da11eaec25aed8b251de24841885c91493b3173" -) -``` - ### Jira Users (`scripts.jira_users`) ```python @@ -657,24 +316,6 @@ All functions return JSON strings with **flattened** data structures (not nested } ``` -### Confluence Page Structure - -```json -{ - "id": "12345", - "title": "Page Title", - "space_key": "DEV", - "status": "current", - "created": "2024-01-15T10:30:00.000Z", - "updated": "2024-01-16T14:20:00.000Z", - "author": "user@company.com", - "version": 3, - "url": "https://company.atlassian.net/wiki/spaces/DEV/pages/12345" -} -``` - -> **Note**: These are simplified structures. The original Jira API returns nested data like `{"key": "...", "fields": {"summary": "...", "status": {"name": "..."}}}`, but this skill flattens it for easier use. - ## Error Handling All functions return JSON strings. Check for errors: diff --git a/.claude/skills/atlassian-skills/scripts/__init__.py b/.claude/skills/atlassian-skills/scripts/__init__.py index c4f7554f10..da1393ee23 100644 --- a/.claude/skills/atlassian-skills/scripts/__init__.py +++ b/.claude/skills/atlassian-skills/scripts/__init__.py @@ -1,6 +1,6 @@ """Atlassian Skills Scripts. -This package contains self-contained Python scripts for Jira and Confluence integration. +This package contains self-contained Python scripts for Jira integration. Each script includes all necessary dependencies inline. """ diff --git a/.claude/skills/atlassian-skills/scripts/_common.py b/.claude/skills/atlassian-skills/scripts/_common.py index 176a395dfa..1c657f0ec4 100644 --- a/.claude/skills/atlassian-skills/scripts/_common.py +++ b/.claude/skills/atlassian-skills/scripts/_common.py @@ -1,6 +1,6 @@ """Common utilities for Atlassian Skills scripts. -This module provides shared functionality used by all Jira and Confluence scripts: +This module provides shared functionality used by all Jira scripts: - Configuration management - HTTP client for API requests - Error handling @@ -108,7 +108,7 @@ def format_json_response(data: Any) -> str: class AtlassianCredentials: """Unified credentials configuration for all Atlassian services. - This class wraps authentication credentials for Jira, Confluence, and Bitbucket. + This class wraps authentication credentials for Jira. When deployed in an Agent environment without environment variables, pass this object to skill functions to provide credentials programmatically. @@ -127,21 +127,7 @@ class AtlassianCredentials: jira_api_version: Optional[str] = None jira_ssl_verify: bool = False - # Confluence configuration - confluence_url: Optional[str] = None - confluence_username: Optional[str] = None - confluence_api_token: Optional[str] = None - confluence_pat_token: Optional[str] = None - confluence_api_version: Optional[str] = None - confluence_ssl_verify: bool = False - - # Bitbucket configuration - bitbucket_url: Optional[str] = None - bitbucket_username: Optional[str] = None - bitbucket_api_token: Optional[str] = None - bitbucket_pat_token: Optional[str] = None - bitbucket_api_version: Optional[str] = None - bitbucket_ssl_verify: bool = False + def is_jira_available(self) -> bool: """Check if Jira credentials are complete and valid. @@ -155,44 +141,18 @@ def is_jira_available(self) -> bool: has_basic = bool(self.jira_username and self.jira_api_token) return has_pat or has_basic - def is_confluence_available(self) -> bool: - """Check if Confluence credentials are complete and valid. - - Returns: - True if Confluence can be used, False otherwise - """ - if not self.confluence_url: - return False - has_pat = bool(self.confluence_pat_token) - has_basic = bool(self.confluence_username and self.confluence_api_token) - return has_pat or has_basic - def is_bitbucket_available(self) -> bool: - """Check if Bitbucket credentials are complete and valid. - - Returns: - True if Bitbucket can be used, False otherwise - """ - if not self.bitbucket_url: - return False - has_pat = bool(self.bitbucket_pat_token) - has_basic = bool(self.bitbucket_username and self.bitbucket_api_token) - return has_pat or has_basic def get_available_services(self) -> List[str]: """Get list of available services based on provided credentials. Returns: List of service names that have complete credentials - Example: ["jira", "confluence"] + Example: ["jira"] """ services = [] if self.is_jira_available(): services.append("jira") - if self.is_confluence_available(): - services.append("confluence") - if self.is_bitbucket_available(): - services.append("bitbucket") return services def get_unavailable_services(self) -> Dict[str, str]: @@ -200,7 +160,7 @@ def get_unavailable_services(self) -> Dict[str, str]: Returns: Dictionary mapping service name to reason for unavailability - Example: {"bitbucket": "Missing bitbucket_url"} + Example: {"jira": "Missing jira_url"} """ unavailable = {} @@ -210,17 +170,7 @@ def get_unavailable_services(self) -> Dict[str, str]: else: unavailable["jira"] = "Missing authentication credentials (provide jira_pat_token or jira_username + jira_api_token)" - if not self.is_confluence_available(): - if not self.confluence_url: - unavailable["confluence"] = "Missing confluence_url" - else: - unavailable["confluence"] = "Missing authentication credentials (provide confluence_pat_token or confluence_username + confluence_api_token)" - if not self.is_bitbucket_available(): - if not self.bitbucket_url: - unavailable["bitbucket"] = "Missing bitbucket_url" - else: - unavailable["bitbucket"] = "Missing authentication credentials (provide bitbucket_pat_token or bitbucket_username + bitbucket_api_token)" return unavailable @@ -270,7 +220,7 @@ def from_env(cls, prefix: str) -> "AtlassianConfig": """Load configuration from environment variables. Args: - prefix: Prefix for environment variables (e.g., 'JIRA' or 'CONFLUENCE') + prefix: Prefix for environment variables (e.g., 'JIRA') Returns: AtlassianConfig instance with values from environment @@ -303,7 +253,7 @@ def from_credentials(cls, credentials: AtlassianCredentials, service: str) -> "A Args: credentials: AtlassianCredentials instance with service credentials - service: Service name ('jira', 'confluence', or 'bitbucket') + service: Service name ('jira') Returns: AtlassianConfig instance @@ -327,36 +277,8 @@ def from_credentials(cls, credentials: AtlassianCredentials, service: str) -> "A api_version=credentials.jira_api_version, ssl_verify=credentials.jira_ssl_verify ) - elif service == "confluence": - if not credentials.is_confluence_available(): - raise ConfigurationError( - "Confluence credentials not provided or incomplete. " - "Please provide confluence_url and either confluence_pat_token or (confluence_username + confluence_api_token)." - ) - config = cls( - url=credentials.confluence_url or "", - username=credentials.confluence_username, - api_token=credentials.confluence_api_token, - pat_token=credentials.confluence_pat_token, - api_version=credentials.confluence_api_version, - ssl_verify=credentials.confluence_ssl_verify - ) - elif service == "bitbucket": - if not credentials.is_bitbucket_available(): - raise ConfigurationError( - "Bitbucket credentials not provided or incomplete. " - "Please provide bitbucket_url and either bitbucket_pat_token or (bitbucket_username + bitbucket_api_token)." - ) - config = cls( - url=credentials.bitbucket_url or "", - username=credentials.bitbucket_username, - api_token=credentials.bitbucket_api_token, - pat_token=credentials.bitbucket_pat_token, - api_version=credentials.bitbucket_api_version, - ssl_verify=credentials.bitbucket_ssl_verify - ) else: - raise ConfigurationError(f"Unknown service: {service}. Must be 'jira', 'confluence', or 'bitbucket'.") + raise ConfigurationError(f"Unknown service: {service}. Must be 'jira'.") return config @@ -567,46 +489,6 @@ def get_jira_client(credentials: Optional[AtlassianCredentials] = None) -> Atlas return AtlassianClient(config) -def get_confluence_client(credentials: Optional[AtlassianCredentials] = None) -> AtlassianClient: - """Get configured Confluence client. - - Args: - credentials: Optional AtlassianCredentials object. If not provided, - configuration will be loaded from environment variables. - - Returns: - Configured AtlassianClient instance - - Raises: - ConfigurationError: If configuration is missing or invalid - """ - if credentials: - config = AtlassianConfig.from_credentials(credentials, 'confluence') - else: - config = AtlassianConfig.from_env('CONFLUENCE') - return AtlassianClient(config) - - -def get_bitbucket_client(credentials: Optional[AtlassianCredentials] = None) -> AtlassianClient: - """Get configured Bitbucket client. - - Args: - credentials: Optional AtlassianCredentials object. If not provided, - configuration will be loaded from environment variables. - - Returns: - Configured AtlassianClient instance - - Raises: - ConfigurationError: If configuration is missing or invalid - """ - if credentials: - config = AtlassianConfig.from_credentials(credentials, 'bitbucket') - else: - config = AtlassianConfig.from_env('BITBUCKET') - return AtlassianClient(config) - - def check_available_skills(credentials: AtlassianCredentials) -> Dict[str, Any]: """Check which Atlassian skills are available based on provided credentials. @@ -619,9 +501,9 @@ def check_available_skills(credentials: AtlassianCredentials) -> Dict[str, Any]: Returns: Dictionary with availability information: { - "available_services": ["jira", "confluence"], + "available_services": ["jira"], "unavailable_services": { - "bitbucket": "Missing bitbucket_url" + "jira": "Missing jira_url" } } diff --git a/.claude/skills/atlassian-skills/scripts/bitbucket_commits.py b/.claude/skills/atlassian-skills/scripts/bitbucket_commits.py deleted file mode 100644 index 041ba6e067..0000000000 --- a/.claude/skills/atlassian-skills/scripts/bitbucket_commits.py +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env python3 -"""Bitbucket commit utilities. - -This module provides functions for retrieving commit information -from Bitbucket Server/Data Center repositories. -""" - -from typing import Optional, Dict, Any, List -from ._common import ( - AtlassianCredentials, - get_bitbucket_client, - format_json_response, - format_error_response, - ConfigurationError, - AuthenticationError, - ValidationError, - NotFoundError, - APIError, - NetworkError, -) - - -def bitbucket_get_commits( - project_key: str, - repository_slug: str, - branch: str = "master", - limit: int = 10, - start: int = 0, - credentials: Optional[AtlassianCredentials] = None -) -> str: - """Get commits from a repository. - - Args: - project_key: Project key (e.g., 'IN') - repository_slug: Repository slug (e.g., 'insights-pipeline') - branch: Branch name or commit reference (default: 'master') - limit: Maximum number of commits to retrieve (default: 10, max: 100) - start: Start index for pagination (default: 0) - - Returns: - JSON string with commit history or error information - """ - try: - if not project_key: - raise ValidationError("project_key is required") - if not repository_slug: - raise ValidationError("repository_slug is required") - - client = get_bitbucket_client(credentials) - - # Use the commits API endpoint - endpoint = f"/rest/api/1.0/projects/{project_key}/repos/{repository_slug}/commits" - params = { - "until": branch, - "limit": min(limit, 100), - "start": start - } - - data = client.get(endpoint, params=params) - - commits = [] - for commit in data.get("values", []): - author = commit.get("author", {}) - committer = commit.get("committer", {}) - - commits.append({ - "id": commit.get("id", ""), - "display_id": commit.get("displayId", ""), - "message": commit.get("message", ""), - "author_name": author.get("name", ""), - "author_email": author.get("emailAddress", ""), - "committer_name": committer.get("name", ""), - "committer_email": committer.get("emailAddress", ""), - "timestamp": commit.get("committerTimestamp", 0), - "parents": [p.get("id", "") for p in commit.get("parents", [])], - }) - - result = { - "project_key": project_key, - "repository": repository_slug, - "branch": branch, - "total": data.get("size", 0), - "is_last_page": data.get("isLastPage", True), - "commits": commits, - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except ValidationError as e: - return format_error_response("ValidationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) - - -def bitbucket_get_commit( - project_key: str, - repository_slug: str, - commit_id: str -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Get details of a specific commit. - - Args: - project_key: Project key (e.g., 'IN') - repository_slug: Repository slug (e.g., 'insights-pipeline') - commit_id: Commit ID or hash - - Returns: - JSON string with commit details or error information - """ - try: - if not project_key: - raise ValidationError("project_key is required") - if not repository_slug: - raise ValidationError("repository_slug is required") - if not commit_id: - raise ValidationError("commit_id is required") - - client = get_bitbucket_client(credentials) - - # Use the commit details API endpoint - endpoint = f"/rest/api/1.0/projects/{project_key}/repos/{repository_slug}/commits/{commit_id}" - - commit = client.get(endpoint) - - author = commit.get("author", {}) - committer = commit.get("committer", {}) - - result = { - "id": commit.get("id", ""), - "display_id": commit.get("displayId", ""), - "message": commit.get("message", ""), - "author_name": author.get("name", ""), - "author_email": author.get("emailAddress", ""), - "committer_name": committer.get("name", ""), - "committer_email": committer.get("emailAddress", ""), - "timestamp": commit.get("committerTimestamp", 0), - "parents": [p.get("id", "") for p in commit.get("parents", [])], - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except ValidationError as e: - return format_error_response("ValidationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) diff --git a/.claude/skills/atlassian-skills/scripts/bitbucket_files.py b/.claude/skills/atlassian-skills/scripts/bitbucket_files.py deleted file mode 100644 index 49c7a7d76a..0000000000 --- a/.claude/skills/atlassian-skills/scripts/bitbucket_files.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env python3 -"""Bitbucket file and search utilities. - -This module provides functions for retrieving file content -and searching code in Bitbucket Server/Data Center. -""" - -from typing import Optional, Dict, Any -from ._common import ( - AtlassianCredentials, - get_bitbucket_client, - format_json_response, - format_error_response, - ConfigurationError, - AuthenticationError, - ValidationError, - NotFoundError, - APIError, - NetworkError, -) - - -def bitbucket_get_file_content( - project_key: str, - repository_slug: str, - file_path: str, - branch: str = "master", - credentials: Optional[AtlassianCredentials] = None -) -> str: - """Get the content of a file from a repository. - - Args: - project_key: Project key (e.g., 'PROJ') - repository_slug: Repository slug - file_path: Path to the file in the repository - branch: Branch or commit reference (default: 'master') - - Returns: - JSON string with file content or error information - """ - try: - if not project_key: - raise ValidationError("project_key is required") - if not repository_slug: - raise ValidationError("repository_slug is required") - if not file_path: - raise ValidationError("file_path is required") - - client = get_bitbucket_client(credentials) - - # Use the browse API to get file content - endpoint = f"/rest/api/1.0/projects/{project_key}/repos/{repository_slug}/browse/{file_path}" - params = {"at": branch} - - data = client.get(endpoint, params=params) - - # Extract content from lines - lines = data.get("lines", []) - content = "\n".join([line.get("text", "") for line in lines]) - - result = { - "path": file_path, - "branch": branch, - "content": content, - "size": data.get("size"), - "is_last_page": data.get("isLastPage", True), - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except ValidationError as e: - return format_error_response("ValidationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) - - -def bitbucket_search( - query: str, - project_key: Optional[str] = None, - repository_slug: Optional[str] = None, - search_type: str = "code", - limit: int = 25, - start: int = 0 -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Search for code or files in Bitbucket Server. - - Args: - query: Search query string - project_key: Limit search to a specific project (optional) - repository_slug: Limit search to a specific repository (optional) - search_type: Type of search - 'code' or 'file' (default: 'code') - limit: Maximum number of results (default: 25, max: 100) - start: Start index for pagination (default: 0) - - Returns: - JSON string with search results or error information - """ - try: - if not query: - raise ValidationError("query is required") - - valid_types = ["code", "file"] - if search_type not in valid_types: - raise ValidationError(f"search_type must be one of: {', '.join(valid_types)}") - - client = get_bitbucket_client(credentials) - - # Build search query with filters - search_query = query - if search_type == "file": - if "ext:" not in query and not query.startswith('"'): - search_query = f'"{query}"' - - if project_key: - search_query += f" project:{project_key}" - if repository_slug and project_key: - search_query += f" repo:{project_key}/{repository_slug}" - - payload = { - "query": search_query, - "entities": { - "code": { - "start": start, - "limit": min(limit, 100) - } - } - } - - # Search API uses a different endpoint - endpoint = "/rest/search/latest/search" - data = client.post(endpoint, json=payload) - - code_results = data.get("code", {}) - results = [] - - for item in code_results.get("values", []): - repo = item.get("repository", {}) - project = repo.get("project", {}) - file_info = item.get("file", {}) - - # Handle file field - can be string or object depending on API version - if isinstance(file_info, str): - file_path = file_info - else: - file_path = file_info.get("toString", file_info.get("path", "")) - - results.append({ - "repository": repo.get("slug", ""), - "project": project.get("key", ""), - "file": file_path, - "hit_count": item.get("hitCount", 0), - "matches": [ - m.get("text", "") if isinstance(m, dict) else str(m) if not isinstance(m, list) else "" - for m in item.get("hitContexts", []) - ], - }) - - result = { - "query": search_query, - "total": code_results.get("count", 0), - "results": results, - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except ValidationError as e: - return format_error_response("ValidationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) diff --git a/.claude/skills/atlassian-skills/scripts/bitbucket_projects.py b/.claude/skills/atlassian-skills/scripts/bitbucket_projects.py deleted file mode 100644 index e46aadbe2c..0000000000 --- a/.claude/skills/atlassian-skills/scripts/bitbucket_projects.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env python3 -"""Bitbucket project and repository management utilities. - -This module provides functions for listing projects and repositories -in Bitbucket Server/Data Center. -""" - -from typing import Optional, Dict, Any, List -from ._common import ( - AtlassianCredentials, - get_bitbucket_client, - format_json_response, - format_error_response, - ConfigurationError, - AuthenticationError, - ValidationError, - NotFoundError, - APIError, - NetworkError, -) - - -def bitbucket_list_projects( - limit: int = 25, - start: int = 0, - credentials: Optional[AtlassianCredentials] = None -) -> str: - """List projects in Bitbucket Server. - - Args: - limit: Number of projects to return (default: 25, max: 100) - start: Start index for pagination (default: 0) - - Returns: - JSON string with list of projects or error information - """ - try: - client = get_bitbucket_client(credentials) - - params = { - "limit": min(limit, 100), - "start": start - } - - data = client.get("/rest/api/1.0/projects", params=params) - - projects = [] - for project in data.get("values", []): - projects.append({ - "key": project.get("key", ""), - "name": project.get("name", ""), - "description": project.get("description", ""), - "public": project.get("public", False), - "type": project.get("type", ""), - }) - - result = { - "projects": projects, - "total": len(projects), - "is_last_page": data.get("isLastPage", True), - "next_page_start": data.get("nextPageStart") - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) - - -def bitbucket_list_repositories( - project_key: Optional[str] = None, - limit: int = 25, - start: int = 0 -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """List repositories in Bitbucket Server. - - Args: - project_key: Project key to filter repositories (optional) - limit: Number of repositories to return (default: 25, max: 100) - start: Start index for pagination (default: 0) - - Returns: - JSON string with list of repositories or error information - """ - try: - client = get_bitbucket_client(credentials) - - params = { - "limit": min(limit, 100), - "start": start - } - - if project_key: - endpoint = f"/rest/api/1.0/projects/{project_key}/repos" - else: - endpoint = "/rest/api/1.0/repos" - - data = client.get(endpoint, params=params) - - repositories = [] - for repo in data.get("values", []): - project = repo.get("project", {}) - repositories.append({ - "slug": repo.get("slug", ""), - "name": repo.get("name", ""), - "description": repo.get("description", ""), - "project_key": project.get("key", ""), - "project_name": project.get("name", ""), - "public": repo.get("public", False), - "state": repo.get("state", ""), - "forkable": repo.get("forkable", True), - }) - - result = { - "repositories": repositories, - "total": len(repositories), - "is_last_page": data.get("isLastPage", True), - "next_page_start": data.get("nextPageStart") - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) diff --git a/.claude/skills/atlassian-skills/scripts/bitbucket_pull_requests.py b/.claude/skills/atlassian-skills/scripts/bitbucket_pull_requests.py deleted file mode 100644 index 6682ee59fd..0000000000 --- a/.claude/skills/atlassian-skills/scripts/bitbucket_pull_requests.py +++ /dev/null @@ -1,425 +0,0 @@ -#!/usr/bin/env python3 -"""Bitbucket pull request management utilities. - -This module provides functions for creating, viewing, merging, -and managing pull requests in Bitbucket Server/Data Center. -""" - -from typing import Optional, Dict, Any, List -from ._common import ( - AtlassianCredentials, - get_bitbucket_client, - format_json_response, - format_error_response, - ConfigurationError, - AuthenticationError, - ValidationError, - NotFoundError, - APIError, - NetworkError, -) - - -def _simplify_pull_request(pr_data: Dict[str, Any]) -> Dict[str, Any]: - """Simplify pull request data to essential fields.""" - from_ref = pr_data.get("fromRef", {}) - to_ref = pr_data.get("toRef", {}) - author = pr_data.get("author", {}).get("user", {}) - - reviewers = [] - for reviewer in pr_data.get("reviewers", []): - user = reviewer.get("user", {}) - reviewers.append({ - "name": user.get("name", ""), - "email": user.get("emailAddress", ""), - "status": reviewer.get("status", ""), - "approved": reviewer.get("approved", False), - }) - - return { - "id": pr_data.get("id"), - "title": pr_data.get("title", ""), - "description": pr_data.get("description", ""), - "state": pr_data.get("state", ""), - "version": pr_data.get("version"), - "source_branch": from_ref.get("displayId", ""), - "target_branch": to_ref.get("displayId", ""), - "source_repo": from_ref.get("repository", {}).get("slug", ""), - "target_repo": to_ref.get("repository", {}).get("slug", ""), - "project_key": to_ref.get("repository", {}).get("project", {}).get("key", ""), - "author": author.get("name", ""), - "author_email": author.get("emailAddress", ""), - "created": pr_data.get("createdDate"), - "updated": pr_data.get("updatedDate"), - "reviewers": reviewers, - "open": pr_data.get("open", False), - "closed": pr_data.get("closed", False), - "locked": pr_data.get("locked", False), - } - - -def bitbucket_create_pull_request( - project_key: str, - repository_slug: str, - title: str, - source_branch: str, - target_branch: str, - description: Optional[str] = None, - reviewers: Optional[List[str]] = None, - credentials: Optional[AtlassianCredentials] = None -) -> str: - """Create a new pull request. - - Args: - project_key: Project key (e.g., 'PROJ') - repository_slug: Repository slug - title: Pull request title - source_branch: Source branch name - target_branch: Target branch name - description: Pull request description (optional) - reviewers: List of reviewer usernames (optional) - - Returns: - JSON string with created pull request details or error information - """ - try: - if not project_key: - raise ValidationError("project_key is required") - if not repository_slug: - raise ValidationError("repository_slug is required") - if not title: - raise ValidationError("title is required") - if not source_branch: - raise ValidationError("source_branch is required") - if not target_branch: - raise ValidationError("target_branch is required") - - client = get_bitbucket_client(credentials) - - payload: Dict[str, Any] = { - "title": title, - "description": description or "", - "fromRef": { - "id": f"refs/heads/{source_branch}", - "repository": { - "slug": repository_slug, - "project": {"key": project_key} - } - }, - "toRef": { - "id": f"refs/heads/{target_branch}", - "repository": { - "slug": repository_slug, - "project": {"key": project_key} - } - } - } - - if reviewers: - payload["reviewers"] = [{"user": {"name": r}} for r in reviewers] - - endpoint = f"/rest/api/1.0/projects/{project_key}/repos/{repository_slug}/pull-requests" - data = client.post(endpoint, json=payload) - - result = _simplify_pull_request(data) - result["success"] = True - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except ValidationError as e: - return format_error_response("ValidationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) - - -def bitbucket_get_pull_request( - project_key: str, - repository_slug: str, - pr_id: int -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Get details of a pull request. - - Args: - project_key: Project key (e.g., 'PROJ') - repository_slug: Repository slug - pr_id: Pull request ID - - Returns: - JSON string with pull request details or error information - """ - try: - if not project_key: - raise ValidationError("project_key is required") - if not repository_slug: - raise ValidationError("repository_slug is required") - if not pr_id: - raise ValidationError("pr_id is required") - - client = get_bitbucket_client(credentials) - - endpoint = f"/rest/api/1.0/projects/{project_key}/repos/{repository_slug}/pull-requests/{pr_id}" - data = client.get(endpoint) - - return format_json_response(_simplify_pull_request(data)) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except ValidationError as e: - return format_error_response("ValidationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) - - - -def bitbucket_merge_pull_request( - project_key: str, - repository_slug: str, - pr_id: int, - version: int, - strategy: str = "merge-commit" -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Merge a pull request. - - Args: - project_key: Project key (e.g., 'PROJ') - repository_slug: Repository slug - pr_id: Pull request ID - version: The version of the PR (from get_pull_request) - strategy: Merge strategy ('merge-commit', 'squash', 'fast-forward') - - Returns: - JSON string with merge result or error information - """ - try: - if not project_key: - raise ValidationError("project_key is required") - if not repository_slug: - raise ValidationError("repository_slug is required") - if not pr_id: - raise ValidationError("pr_id is required") - if version is None: - raise ValidationError("version is required") - - valid_strategies = ["merge-commit", "squash", "fast-forward"] - if strategy not in valid_strategies: - raise ValidationError(f"strategy must be one of: {', '.join(valid_strategies)}") - - client = get_bitbucket_client(credentials) - - payload = { - "version": version, - "strategy": strategy - } - - endpoint = f"/rest/api/1.0/projects/{project_key}/repos/{repository_slug}/pull-requests/{pr_id}/merge" - data = client.post(endpoint, json=payload) - - result = _simplify_pull_request(data) - result["success"] = True - result["message"] = "Pull request merged successfully" - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except ValidationError as e: - return format_error_response("ValidationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) - - -def bitbucket_decline_pull_request( - project_key: str, - repository_slug: str, - pr_id: int, - version: int -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Decline a pull request. - - Args: - project_key: Project key (e.g., 'PROJ') - repository_slug: Repository slug - pr_id: Pull request ID - version: The version of the PR (from get_pull_request) - - Returns: - JSON string with decline result or error information - """ - try: - if not project_key: - raise ValidationError("project_key is required") - if not repository_slug: - raise ValidationError("repository_slug is required") - if not pr_id: - raise ValidationError("pr_id is required") - if version is None: - raise ValidationError("version is required") - - client = get_bitbucket_client(credentials) - - payload = {"version": version} - - endpoint = f"/rest/api/1.0/projects/{project_key}/repos/{repository_slug}/pull-requests/{pr_id}/decline" - data = client.post(endpoint, json=payload) - - result = _simplify_pull_request(data) - result["success"] = True - result["message"] = "Pull request declined" - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except ValidationError as e: - return format_error_response("ValidationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) - - -def bitbucket_add_pr_comment( - project_key: str, - repository_slug: str, - pr_id: int, - text: str, - parent_id: Optional[int] = None -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Add a comment to a pull request. - - Args: - project_key: Project key (e.g., 'PROJ') - repository_slug: Repository slug - pr_id: Pull request ID - text: Comment text - parent_id: Parent comment ID for replies (optional) - - Returns: - JSON string with comment details or error information - """ - try: - if not project_key: - raise ValidationError("project_key is required") - if not repository_slug: - raise ValidationError("repository_slug is required") - if not pr_id: - raise ValidationError("pr_id is required") - if not text: - raise ValidationError("text is required") - - client = get_bitbucket_client(credentials) - - payload: Dict[str, Any] = {"text": text} - if parent_id: - payload["parent"] = {"id": parent_id} - - endpoint = f"/rest/api/1.0/projects/{project_key}/repos/{repository_slug}/pull-requests/{pr_id}/comments" - data = client.post(endpoint, json=payload) - - author = data.get("author", {}) - result = { - "success": True, - "id": data.get("id"), - "text": data.get("text", ""), - "author": author.get("name", ""), - "author_email": author.get("emailAddress", ""), - "created": data.get("createdDate"), - "updated": data.get("updatedDate"), - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except ValidationError as e: - return format_error_response("ValidationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) - - -def bitbucket_get_pr_diff( - project_key: str, - repository_slug: str, - pr_id: int -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Get the diff of a pull request. - - Args: - project_key: Project key (e.g., 'PROJ') - repository_slug: Repository slug - pr_id: Pull request ID - - Returns: - JSON string with diff information or error information - """ - try: - if not project_key: - raise ValidationError("project_key is required") - if not repository_slug: - raise ValidationError("repository_slug is required") - if not pr_id: - raise ValidationError("pr_id is required") - - client = get_bitbucket_client(credentials) - - endpoint = f"/rest/api/1.0/projects/{project_key}/repos/{repository_slug}/pull-requests/{pr_id}/diff" - data = client.get(endpoint) - - return format_json_response(data) - - except ConfigurationError as e: - return format_error_response("ConfigurationError", str(e)) - except AuthenticationError as e: - return format_error_response("AuthenticationError", str(e)) - except ValidationError as e: - return format_error_response("ValidationError", str(e)) - except NotFoundError as e: - return format_error_response("NotFoundError", str(e)) - except APIError as e: - return format_error_response("APIError", str(e)) - except NetworkError as e: - return format_error_response("NetworkError", str(e)) - except Exception as e: - return format_error_response("UnexpectedError", str(e)) diff --git a/.claude/skills/atlassian-skills/scripts/confluence_comments.py b/.claude/skills/atlassian-skills/scripts/confluence_comments.py deleted file mode 100644 index ddc588530d..0000000000 --- a/.claude/skills/atlassian-skills/scripts/confluence_comments.py +++ /dev/null @@ -1,140 +0,0 @@ -"""Confluence comment management tools. - -Tools: - - confluence_get_comments: Get comments for a page - - confluence_add_comment: Add a comment to a page -""" - -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent)) - -from typing import Any, Dict, Optional - -from _common import ( - AtlassianCredentials, - get_confluence_client, - format_json_response, - format_error_response, - ConfigurationError, - AuthenticationError, - ValidationError, - NotFoundError, - APIError, - NetworkError, -) - - -def _simplify_comment(comment_data: Dict[str, Any]) -> Dict[str, Any]: - """Simplify comment data to essential fields.""" - body = comment_data.get('body', {}) - storage = body.get('storage', {}) or body.get('view', {}) - - return { - 'id': comment_data.get('id', ''), - 'content': storage.get('value', ''), - 'created': comment_data.get('history', {}).get('createdDate', ''), - 'author': comment_data.get('history', {}).get('createdBy', {}).get( - 'displayName', '' - ) - } - - -def confluence_get_comments( - page_id: str, - credentials: Optional[AtlassianCredentials] = None -) -> str: - """Get all comments for a Confluence page. - - Args: - page_id: Page ID to get comments for - - Returns: - JSON string with list of comments or error information - """ - try: - client = get_confluence_client(credentials) - - if not page_id: - raise ValidationError('page_id is required') - - params = { - 'expand': 'body.storage,history', - 'depth': 'all' - } - response = client.get( - f'/rest/api/content/{page_id}/child/comment', params=params - ) - - comments = response.get('results', []) - simplified_comments = [_simplify_comment(c) for c in comments] - - result = { - 'comments': simplified_comments, - 'count': len(simplified_comments), - 'page_id': page_id - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response('ConfigurationError', str(e)) - except AuthenticationError as e: - return format_error_response('AuthenticationError', str(e)) - except ValidationError as e: - return format_error_response('ValidationError', str(e)) - except NotFoundError as e: - return format_error_response('NotFoundError', str(e)) - except (APIError, NetworkError) as e: - return format_error_response(type(e).__name__, str(e)) - except Exception as e: - return format_error_response('UnexpectedError', f'Unexpected error: {str(e)}') - - -def confluence_add_comment(page_id: str, content: str, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Add a comment to a Confluence page. - - Args: - page_id: Page ID to add comment to - content: Comment content (HTML or plain text) - - Returns: - JSON string with created comment data or error information - """ - try: - client = get_confluence_client(credentials) - - if not page_id: - raise ValidationError('page_id is required') - if not content: - raise ValidationError('content is required') - - payload: Dict[str, Any] = { - 'type': 'comment', - 'container': {'id': page_id, 'type': 'page'}, - 'body': { - 'storage': { - 'value': content, - 'representation': 'storage' - } - } - } - - response = client.post('/rest/api/content', json=payload) - simplified = _simplify_comment(response) - - return format_json_response(simplified) - - except ConfigurationError as e: - return format_error_response('ConfigurationError', str(e)) - except AuthenticationError as e: - return format_error_response('AuthenticationError', str(e)) - except ValidationError as e: - return format_error_response('ValidationError', str(e)) - except NotFoundError as e: - return format_error_response('NotFoundError', str(e)) - except (APIError, NetworkError) as e: - return format_error_response(type(e).__name__, str(e)) - except Exception as e: - return format_error_response('UnexpectedError', f'Unexpected error: {str(e)}') diff --git a/.claude/skills/atlassian-skills/scripts/confluence_labels.py b/.claude/skills/atlassian-skills/scripts/confluence_labels.py deleted file mode 100644 index e4b9a669d6..0000000000 --- a/.claude/skills/atlassian-skills/scripts/confluence_labels.py +++ /dev/null @@ -1,159 +0,0 @@ -"""Confluence label management tools. - -Tools: - - confluence_get_labels: Get labels for a page - - confluence_add_label: Add a label to a page - - confluence_remove_label: Remove a label from a page -""" - -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent)) - -from typing import Any, Dict, Optional - -from _common import ( - AtlassianCredentials, - get_confluence_client, - format_json_response, - format_error_response, - ConfigurationError, - AuthenticationError, - ValidationError, - NotFoundError, - APIError, - NetworkError, -) - - -def confluence_get_labels( - page_id: str, - credentials: Optional[AtlassianCredentials] = None -) -> str: - """Get all labels for a Confluence page. - - Args: - page_id: Page ID to get labels for - - Returns: - JSON string with list of labels or error information - """ - try: - client = get_confluence_client(credentials) - - if not page_id: - raise ValidationError('page_id is required') - - response = client.get(f'/rest/api/content/{page_id}/label') - - labels = response.get('results', []) - simplified_labels = [ - {'name': label.get('name', ''), 'prefix': label.get('prefix', '')} - for label in labels - ] - - result = { - 'labels': simplified_labels, - 'count': len(simplified_labels), - 'page_id': page_id - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response('ConfigurationError', str(e)) - except AuthenticationError as e: - return format_error_response('AuthenticationError', str(e)) - except ValidationError as e: - return format_error_response('ValidationError', str(e)) - except NotFoundError as e: - return format_error_response('NotFoundError', str(e)) - except (APIError, NetworkError) as e: - return format_error_response(type(e).__name__, str(e)) - except Exception as e: - return format_error_response('UnexpectedError', f'Unexpected error: {str(e)}') - - -def confluence_add_label(page_id: str, name: str, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Add a label to a Confluence page. - - Args: - page_id: Page ID to add label to - name: Label name - - Returns: - JSON string with success message or error information - """ - try: - client = get_confluence_client(credentials) - - if not page_id: - raise ValidationError('page_id is required') - if not name: - raise ValidationError('name is required') - - payload = [{'prefix': 'global', 'name': name}] - client.post(f'/rest/api/content/{page_id}/label', json=payload) - - return format_json_response({ - 'success': True, - 'message': f'Label "{name}" added to page {page_id}', - 'page_id': page_id, - 'label': name - }) - - except ConfigurationError as e: - return format_error_response('ConfigurationError', str(e)) - except AuthenticationError as e: - return format_error_response('AuthenticationError', str(e)) - except ValidationError as e: - return format_error_response('ValidationError', str(e)) - except NotFoundError as e: - return format_error_response('NotFoundError', str(e)) - except (APIError, NetworkError) as e: - return format_error_response(type(e).__name__, str(e)) - except Exception as e: - return format_error_response('UnexpectedError', f'Unexpected error: {str(e)}') - - -def confluence_remove_label(page_id: str, name: str, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Remove a label from a Confluence page. - - Args: - page_id: Page ID to remove label from - name: Label name to remove - - Returns: - JSON string with success message or error information - """ - try: - client = get_confluence_client(credentials) - - if not page_id: - raise ValidationError('page_id is required') - if not name: - raise ValidationError('name is required') - - client.delete(f'/rest/api/content/{page_id}/label/{name}') - - return format_json_response({ - 'success': True, - 'message': f'Label "{name}" removed from page {page_id}', - 'page_id': page_id, - 'label': name - }) - - except ConfigurationError as e: - return format_error_response('ConfigurationError', str(e)) - except AuthenticationError as e: - return format_error_response('AuthenticationError', str(e)) - except ValidationError as e: - return format_error_response('ValidationError', str(e)) - except NotFoundError as e: - return format_error_response('NotFoundError', str(e)) - except (APIError, NetworkError) as e: - return format_error_response(type(e).__name__, str(e)) - except Exception as e: - return format_error_response('UnexpectedError', f'Unexpected error: {str(e)}') diff --git a/.claude/skills/atlassian-skills/scripts/confluence_pages.py b/.claude/skills/atlassian-skills/scripts/confluence_pages.py deleted file mode 100644 index 4b140e1845..0000000000 --- a/.claude/skills/atlassian-skills/scripts/confluence_pages.py +++ /dev/null @@ -1,257 +0,0 @@ -"""Confluence page management tools. - -Tools: - - confluence_get_page: Get a page by ID or title - - confluence_create_page: Create a new page - - confluence_update_page: Update an existing page - - confluence_delete_page: Delete a page -""" - -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent)) - -from typing import Any, Dict, Optional - -from _common import ( - AtlassianCredentials, - get_confluence_client, - format_json_response, - format_error_response, - ConfigurationError, - AuthenticationError, - ValidationError, - NotFoundError, - APIError, - NetworkError, -) - - -def _simplify_page(page_data: Dict[str, Any]) -> Dict[str, Any]: - """Simplify page data to essential fields.""" - body = page_data.get('body', {}) - storage = body.get('storage', {}) or body.get('view', {}) - - return { - 'id': page_data.get('id', ''), - 'title': page_data.get('title', ''), - 'space_key': page_data.get('space', {}).get('key', ''), - 'version': page_data.get('version', {}).get('number', 1), - 'content': storage.get('value', ''), - 'created': page_data.get('history', {}).get('createdDate', ''), - 'updated': page_data.get('version', {}).get('when', ''), - 'url': page_data.get('_links', {}).get('webui', '') - } - - -def confluence_get_page( - page_id: Optional[str] = None, - title: Optional[str] = None, - space_key: Optional[str] = None -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Get a Confluence page by ID or by title and space. - - Args: - page_id: Page ID (optional if title and space_key provided) - title: Page title (optional if page_id provided) - space_key: Space key (required if using title) - - Returns: - JSON string with page data or error information - """ - try: - client = get_confluence_client(credentials) - - if not page_id and not title: - raise ValidationError('Either page_id or title is required') - if title and not space_key: - raise ValidationError('space_key is required when using title') - - if page_id: - params = {'expand': 'body.storage,version,space,history'} - page_data = client.get(f'/rest/api/content/{page_id}', params=params) - else: - params = { - 'title': title, - 'spaceKey': space_key, - 'expand': 'body.storage,version,space,history' - } - response = client.get('/rest/api/content', params=params) - results = response.get('results', []) - if not results: - raise NotFoundError(f'Page not found: {title} in space {space_key}') - page_data = results[0] - - simplified = _simplify_page(page_data) - return format_json_response(simplified) - - except ConfigurationError as e: - return format_error_response('ConfigurationError', str(e)) - except AuthenticationError as e: - return format_error_response('AuthenticationError', str(e)) - except ValidationError as e: - return format_error_response('ValidationError', str(e)) - except NotFoundError as e: - return format_error_response('NotFoundError', str(e)) - except (APIError, NetworkError) as e: - return format_error_response(type(e).__name__, str(e)) - except Exception as e: - return format_error_response('UnexpectedError', f'Unexpected error: {str(e)}') - - -def confluence_create_page( - space_key: str, - title: str, - content: str, - parent_id: Optional[str] = None -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Create a new Confluence page. - - Args: - space_key: Space key where the page will be created - title: Page title - content: Page content (HTML or storage format) - parent_id: Parent page ID (optional) - - Returns: - JSON string with created page data or error information - """ - try: - client = get_confluence_client(credentials) - - if not space_key: - raise ValidationError('space_key is required') - if not title: - raise ValidationError('title is required') - if not content: - raise ValidationError('content is required') - - payload: Dict[str, Any] = { - 'type': 'page', - 'title': title, - 'space': {'key': space_key}, - 'body': { - 'storage': { - 'value': content, - 'representation': 'storage' - } - } - } - - if parent_id: - payload['ancestors'] = [{'id': parent_id}] - - response = client.post('/rest/api/content', json=payload) - - # Get full page data - page_id = response.get('id') - if page_id: - return confluence_get_page(page_id=page_id) - - return format_json_response(_simplify_page(response)) - - except ConfigurationError as e: - return format_error_response('ConfigurationError', str(e)) - except AuthenticationError as e: - return format_error_response('AuthenticationError', str(e)) - except ValidationError as e: - return format_error_response('ValidationError', str(e)) - except (APIError, NetworkError) as e: - return format_error_response(type(e).__name__, str(e)) - except Exception as e: - return format_error_response('UnexpectedError', f'Unexpected error: {str(e)}') - - -def confluence_update_page(page_id: str, title: str, content: str, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Update an existing Confluence page. - - Args: - page_id: Page ID to update - title: New page title - content: New page content (HTML or storage format) - - Returns: - JSON string with updated page data or error information - """ - try: - client = get_confluence_client(credentials) - - if not page_id: - raise ValidationError('page_id is required') - if not title: - raise ValidationError('title is required') - if not content: - raise ValidationError('content is required') - - # Get current version - current = client.get(f'/rest/api/content/{page_id}', params={'expand': 'version'}) - current_version = current.get('version', {}).get('number', 0) - - payload: Dict[str, Any] = { - 'type': 'page', - 'title': title, - 'body': { - 'storage': { - 'value': content, - 'representation': 'storage' - } - }, - 'version': {'number': current_version + 1} - } - - response = client.put(f'/rest/api/content/{page_id}', json=payload) - - return confluence_get_page(page_id=page_id) - - except ConfigurationError as e: - return format_error_response('ConfigurationError', str(e)) - except AuthenticationError as e: - return format_error_response('AuthenticationError', str(e)) - except ValidationError as e: - return format_error_response('ValidationError', str(e)) - except NotFoundError as e: - return format_error_response('NotFoundError', str(e)) - except (APIError, NetworkError) as e: - return format_error_response(type(e).__name__, str(e)) - except Exception as e: - return format_error_response('UnexpectedError', f'Unexpected error: {str(e)}') - - -def confluence_delete_page(page_id: str, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Delete a Confluence page. - - Args: - page_id: Page ID to delete - - Returns: - JSON string with success message or error information - """ - try: - client = get_confluence_client(credentials) - - if not page_id: - raise ValidationError('page_id is required') - - client.delete(f'/rest/api/content/{page_id}') - - return format_json_response({ - 'success': True, - 'message': f'Page {page_id} deleted successfully' - }) - - except ConfigurationError as e: - return format_error_response('ConfigurationError', str(e)) - except AuthenticationError as e: - return format_error_response('AuthenticationError', str(e)) - except ValidationError as e: - return format_error_response('ValidationError', str(e)) - except NotFoundError as e: - return format_error_response('NotFoundError', str(e)) - except (APIError, NetworkError) as e: - return format_error_response(type(e).__name__, str(e)) - except Exception as e: - return format_error_response('UnexpectedError', f'Unexpected error: {str(e)}') diff --git a/.claude/skills/atlassian-skills/scripts/confluence_search.py b/.claude/skills/atlassian-skills/scripts/confluence_search.py deleted file mode 100644 index f03bd04d20..0000000000 --- a/.claude/skills/atlassian-skills/scripts/confluence_search.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Confluence search tools. - -Tools: - - confluence_search: Search content using CQL -""" - -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent)) - -from typing import Any, Dict, Optional - -from _common import ( - AtlassianCredentials, - get_confluence_client, - format_json_response, - format_error_response, - ConfigurationError, - AuthenticationError, - ValidationError, - APIError, - NetworkError, -) - - -def _simplify_search_result(result: Dict[str, Any]) -> Dict[str, Any]: - """Simplify search result to essential fields.""" - content = result.get('content', result) - return { - 'id': content.get('id', ''), - 'title': content.get('title', result.get('title', '')), - 'type': content.get('type', ''), - 'space_key': content.get('space', {}).get('key', ''), - 'url': result.get('url', content.get('_links', {}).get('webui', '')), - 'excerpt': result.get('excerpt', ''), - 'last_modified': result.get('lastModified', '') - } - - -def confluence_search( - query: str, - limit: int = 10, - start_at: int = 0 -, - credentials: Optional[AtlassianCredentials] = None) -> str: - """Search for Confluence content using CQL. - - Args: - query: Search query (text or CQL) - limit: Maximum number of results (default: 10) - start_at: Index of first result for pagination (default: 0) - - Returns: - JSON string with search results or error information - """ - try: - client = get_confluence_client(credentials) - - if not query: - raise ValidationError('query is required') - if limit < 0: - raise ValidationError('limit must be non-negative') - if start_at < 0: - raise ValidationError('start_at must be non-negative') - - # Build CQL query - if not any(op in query for op in ['=', '~', 'AND', 'OR', 'NOT']): - cql = f'text ~ "{query}" OR title ~ "{query}"' - else: - cql = query - - params: Dict[str, Any] = { - 'cql': cql, - 'limit': limit, - 'start': start_at - } - - response = client.get('/rest/api/content/search', params=params) - - results = response.get('results', []) - simplified_results = [_simplify_search_result(r) for r in results] - - result = { - 'results': simplified_results, - 'total': response.get('totalSize', len(results)), - 'start_at': start_at, - 'limit': limit, - 'is_last': start_at + len(results) >= response.get('totalSize', 0) - } - - return format_json_response(result) - - except ConfigurationError as e: - return format_error_response('ConfigurationError', str(e)) - except AuthenticationError as e: - return format_error_response('AuthenticationError', str(e)) - except ValidationError as e: - return format_error_response('ValidationError', str(e)) - except (APIError, NetworkError) as e: - return format_error_response(type(e).__name__, str(e)) - except Exception as e: - return format_error_response('UnexpectedError', f'Unexpected error: {str(e)}')