1 Commits

Author SHA1 Message Date
b3abe863af Merge pull request 'development' (#117) from development into main
Reviewed-on: #117
2026-01-23 17:50:25 +00:00
10 changed files with 136 additions and 629 deletions

View File

@@ -6,7 +6,7 @@
}, },
"metadata": { "metadata": {
"description": "Project management plugins with Gitea and NetBox integrations", "description": "Project management plugins with Gitea and NetBox integrations",
"version": "3.2.0" "version": "3.1.0"
}, },
"plugins": [ "plugins": [
{ {

View File

@@ -4,23 +4,14 @@ All notable changes to the Leo Claude Marketplace will be documented in this fil
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [3.1.2] - 2026-01-23
*Changes staged for the next release*
---
## [3.2.0] - 2026-01-24
### Added ### Added
- **git-flow:** `/commit` now detects protected branches before committing - **git-flow:** `/commit` now detects protected branches before committing
- Warns when on protected branch (main, master, development, staging, production) - Warns when on protected branch (main, master, development, staging, production)
- Offers to create feature branch automatically instead of committing directly - Offers to create feature branch automatically instead of committing directly
- Configurable via `GIT_PROTECTED_BRANCHES` environment variable - Configurable via `GIT_PROTECTED_BRANCHES` environment variable
- **netbox:** Platform and primary_ip parameters added to device update tools - Resolves issue where commits to protected branches would fail on push
- **claude-config-maintainer:** Auto-enforce mandatory behavior rules via SessionStart hook
- **scripts:** `release.sh` - Versioning workflow script for consistent releases
- **scripts:** `verify-hooks.sh` - Verify all hooks are command type
### Changed ### Changed
- **doc-guardian:** Hook switched from `prompt` type to `command` type - **doc-guardian:** Hook switched from `prompt` type to `command` type
@@ -28,24 +19,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- New `notify.sh` bash script guarantees exact output behavior - New `notify.sh` bash script guarantees exact output behavior
- Only notifies for config file changes (commands/, agents/, skills/, hooks/) - Only notifies for config file changes (commands/, agents/, skills/, hooks/)
- Silent exit for all other files - no blocking possible - Silent exit for all other files - no blocking possible
- **All hooks:** Converted to command type with stricter plugin prefix enforcement - **All hooks:** Stricter plugin prefix enforcement
- All hooks now mandate `[plugin-name]` prefix with "NO EXCEPTIONS" rule - All prompts now mandate `[plugin-name]` prefix with "NO EXCEPTIONS" rule
- Simplified output formats with word limits - Simplified output formats with word limits
- Consistent structure across projman, pr-review, code-sentinel, doc-guardian - Consistent structure across projman, pr-review, code-sentinel, doc-guardian
- **CLAUDE.md:** Replaced destructive "ALWAYS CLEAR CACHE" rule with "VERIFY AND RESTART"
- Cache clearing mid-session breaks MCP tools
- Added guidance for proper plugin development workflow
### Fixed ### Fixed
- **cmdb-assistant:** Complete MCP tool schemas for update operations (#138) - Protected branch workflow: Claude no longer commits directly to protected branches and then fails on push (fixes #109)
- **netbox:** Shorten tool names to meet 64-char API limit (#134) - doc-guardian hook no longer blocks workflow - switched to command hook that can't be overridden by model (fixes #110)
- **cmdb-assistant:** Correct NetBox API URL format in setup wizard (#132)
- **gitea/projman:** Type safety for `create_label_smart`, curl-based debug-report (#124)
- **netbox:** Add diagnostic logging for JSON parse errors (#121)
- **labels:** Add duplicate check before creating labels (#116)
- **hooks:** Convert ALL hooks to command type with proper prefixes (#114)
- Protected branch workflow: Claude no longer commits directly to protected branches (fixes #109)
- doc-guardian hook no longer blocks workflow (fixes #110)
--- ---

View File

@@ -286,56 +286,13 @@ See `docs/DEBUGGING-CHECKLIST.md` for systematic troubleshooting.
- `/debug-report` - Run full diagnostics, create issue if needed - `/debug-report` - Run full diagnostics, create issue if needed
- `/debug-review` - Investigate and propose fixes - `/debug-review` - Investigate and propose fixes
## Versioning Workflow ## Versioning Rules
This project follows [SemVer](https://semver.org/) and [Keep a Changelog](https://keepachangelog.com). - Version displayed ONLY in main `README.md` title: `# Leo Claude Marketplace - vX.Y.Z`
- `CHANGELOG.md` is authoritative for version history
### Version Locations (must stay in sync) - Follow [SemVer](https://semver.org/): MAJOR.MINOR.PATCH
- On release: Update README title → CHANGELOG → marketplace.json → plugin.json files
| Location | Format | Example |
|----------|--------|---------|
| Git tags | `vX.Y.Z` | `v3.2.0` |
| README.md title | `# Leo Claude Marketplace - vX.Y.Z` | `v3.2.0` |
| marketplace.json | `"version": "X.Y.Z"` | `3.2.0` |
| CHANGELOG.md | `## [X.Y.Z] - YYYY-MM-DD` | `[3.2.0] - 2026-01-24` |
### During Development
**All changes go under `[Unreleased]` in CHANGELOG.md.** Never create a versioned section until release time.
```markdown
## [Unreleased]
### Added
- New feature description
### Fixed
- Bug fix description
```
### Creating a Release
Use the release script to ensure consistency:
```bash
./scripts/release.sh 3.2.0
```
The script will:
1. Validate `[Unreleased]` section has content
2. Replace `[Unreleased]` with `[3.2.0] - YYYY-MM-DD`
3. Update README.md title
4. Update marketplace.json version
5. Commit and create git tag
### SemVer Guidelines
| Change Type | Version Bump | Example |
|-------------|--------------|---------|
| Bug fixes only | PATCH (x.y.**Z**) | 3.1.1 → 3.1.2 |
| New features (backwards compatible) | MINOR (x.**Y**.0) | 3.1.2 → 3.2.0 |
| Breaking changes | MAJOR (**X**.0.0) | 3.2.0 → 4.0.0 |
--- ---
**Last Updated:** 2026-01-24 **Last Updated:** 2026-01-23

View File

@@ -1,4 +1,4 @@
# Leo Claude Marketplace - v3.2.0 # Leo Claude Marketplace - v3.1.1
A collection of Claude Code plugins for project management, infrastructure automation, and development workflows. A collection of Claude Code plugins for project management, infrastructure automation, and development workflows.
@@ -106,11 +106,11 @@ Full Gitea API integration for project management.
| Category | Tools | | Category | Tools |
|----------|-------| |----------|-------|
| Issues | `list_issues`, `get_issue`, `create_issue`, `update_issue`, `add_comment`, `aggregate_issues` | | Issues | `list_issues`, `get_issue`, `create_issue`, `update_issue`, `add_comment` |
| Labels | `get_labels`, `suggest_labels`, `create_label`, `create_label_smart` | | Labels | `get_labels`, `suggest_labels`, `create_label` |
| Wiki | `list_wiki_pages`, `get_wiki_page`, `create_wiki_page`, `update_wiki_page`, `create_lesson`, `search_lessons` | | Wiki | `list_wiki_pages`, `get_wiki_page`, `create_wiki_page`, `create_lesson`, `search_lessons` |
| Milestones | `list_milestones`, `get_milestone`, `create_milestone`, `update_milestone`, `delete_milestone` | | Milestones | `list_milestones`, `get_milestone`, `create_milestone`, `update_milestone` |
| Dependencies | `list_issue_dependencies`, `create_issue_dependency`, `remove_issue_dependency`, `get_execution_order` | | Dependencies | `list_issue_dependencies`, `create_issue_dependency`, `get_execution_order` |
| **Pull Requests** | `list_pull_requests`, `get_pull_request`, `get_pr_diff`, `get_pr_comments`, `create_pr_review`, `add_pr_comment` *(NEW in v3.0.0)* | | **Pull Requests** | `list_pull_requests`, `get_pull_request`, `get_pr_diff`, `get_pr_comments`, `create_pr_review`, `add_pr_comment` *(NEW in v3.0.0)* |
| Validation | `validate_repo_org`, `get_branch_protection` | | Validation | `validate_repo_org`, `get_branch_protection` |
@@ -245,8 +245,7 @@ leo-claude-mktplace/
├── docs/ # Documentation ├── docs/ # Documentation
│ ├── CANONICAL-PATHS.md # Path reference │ ├── CANONICAL-PATHS.md # Path reference
│ └── CONFIGURATION.md # Setup guide │ └── CONFIGURATION.md # Setup guide
── scripts/ # Setup scripts ── scripts/ # Setup scripts
└── CHANGELOG.md # Version history
``` ```
## Documentation ## Documentation

View File

@@ -343,15 +343,6 @@ class LabelTools:
None, None,
lambda: self.gitea.create_org_label(owner, name, color, description) lambda: self.gitea.create_org_label(owner, name, color, description)
) )
# Handle unexpected response types (API may return list or non-dict)
if not isinstance(result, dict):
logger.error(f"Unexpected API response type for org label: {type(result)} - {result}")
return {
'name': name,
'error': True,
'reason': f"API returned {type(result).__name__} instead of dict: {result}",
'level': 'organization'
}
result['level'] = 'organization' result['level'] = 'organization'
result['skipped'] = False result['skipped'] = False
logger.info(f"Created organization label '{name}' in {owner}") logger.info(f"Created organization label '{name}' in {owner}")
@@ -361,15 +352,6 @@ class LabelTools:
None, None,
lambda: self.gitea.create_label(name, color, description, target_repo) lambda: self.gitea.create_label(name, color, description, target_repo)
) )
# Handle unexpected response types (API may return list or non-dict)
if not isinstance(result, dict):
logger.error(f"Unexpected API response type for repo label: {type(result)} - {result}")
return {
'name': name,
'error': True,
'reason': f"API returned {type(result).__name__} instead of dict: {result}",
'level': 'repository'
}
result['level'] = 'repository' result['level'] = 'repository'
result['skipped'] = False result['skipped'] = False
logger.info(f"Created repository label '{name}' in {target_repo}") logger.info(f"Created repository label '{name}' in {target_repo}")

View File

@@ -4,7 +4,6 @@ NetBox API client for interacting with NetBox REST API.
Provides a generic HTTP client with methods for all standard REST operations. Provides a generic HTTP client with methods for all standard REST operations.
Individual tool modules use this client for their specific endpoints. Individual tool modules use this client for their specific endpoints.
""" """
import json
import requests import requests
import logging import logging
from typing import List, Dict, Optional, Any, Union from typing import List, Dict, Optional, Any, Union
@@ -84,20 +83,7 @@ class NetBoxClient:
if response.status_code == 204 or not response.content: if response.status_code == 204 or not response.content:
return None return None
# Parse JSON with diagnostic error handling return response.json()
try:
return response.json()
except json.JSONDecodeError as e:
logger.error(
f"JSON decode failed. Status: {response.status_code}, "
f"Content-Length: {len(response.content)}, "
f"Content preview: {response.content[:200]!r}"
)
raise ValueError(
f"Invalid JSON response from NetBox: {e}. "
f"Status code: {response.status_code}, "
f"Content length: {len(response.content)} bytes"
) from e
def list( def list(
self, self,

View File

@@ -103,19 +103,7 @@ TOOL_DEFINITIONS = {
'properties': { 'properties': {
'id': {'type': 'integer', 'description': 'Site ID'}, 'id': {'type': 'integer', 'description': 'Site ID'},
'name': {'type': 'string', 'description': 'New name'}, 'name': {'type': 'string', 'description': 'New name'},
'slug': {'type': 'string', 'description': 'New slug'}, 'status': {'type': 'string', 'description': 'New status'}
'status': {'type': 'string', 'description': 'Status'},
'region': {'type': 'integer', 'description': 'Region ID'},
'group': {'type': 'integer', 'description': 'Site group ID'},
'tenant': {'type': 'integer', 'description': 'Tenant ID'},
'facility': {'type': 'string', 'description': 'Facility name'},
'time_zone': {'type': 'string', 'description': 'Time zone'},
'description': {'type': 'string', 'description': 'Description'},
'physical_address': {'type': 'string', 'description': 'Physical address'},
'shipping_address': {'type': 'string', 'description': 'Shipping address'},
'latitude': {'type': 'number', 'description': 'Latitude'},
'longitude': {'type': 'number', 'description': 'Longitude'},
'comments': {'type': 'string', 'description': 'Comments'}
}, },
'required': ['id'] 'required': ['id']
}, },
@@ -148,14 +136,7 @@ TOOL_DEFINITIONS = {
}, },
'dcim_update_location': { 'dcim_update_location': {
'description': 'Update an existing location', 'description': 'Update an existing location',
'properties': { 'properties': {'id': {'type': 'integer', 'description': 'Location ID'}},
'id': {'type': 'integer', 'description': 'Location ID'},
'name': {'type': 'string', 'description': 'New name'},
'slug': {'type': 'string', 'description': 'New slug'},
'site': {'type': 'integer', 'description': 'Site ID'},
'parent': {'type': 'integer', 'description': 'Parent location ID'},
'description': {'type': 'string', 'description': 'Description'}
},
'required': ['id'] 'required': ['id']
}, },
'dcim_delete_location': { 'dcim_delete_location': {
@@ -190,18 +171,7 @@ TOOL_DEFINITIONS = {
}, },
'dcim_update_rack': { 'dcim_update_rack': {
'description': 'Update an existing rack', 'description': 'Update an existing rack',
'properties': { 'properties': {'id': {'type': 'integer', 'description': 'Rack ID'}},
'id': {'type': 'integer', 'description': 'Rack ID'},
'name': {'type': 'string', 'description': 'New name'},
'site': {'type': 'integer', 'description': 'Site ID'},
'location': {'type': 'integer', 'description': 'Location ID'},
'status': {'type': 'string', 'description': 'Status'},
'role': {'type': 'integer', 'description': 'Role ID'},
'tenant': {'type': 'integer', 'description': 'Tenant ID'},
'u_height': {'type': 'integer', 'description': 'Rack height in U'},
'description': {'type': 'string', 'description': 'Description'},
'comments': {'type': 'string', 'description': 'Comments'}
},
'required': ['id'] 'required': ['id']
}, },
'dcim_delete_rack': { 'dcim_delete_rack': {
@@ -228,12 +198,7 @@ TOOL_DEFINITIONS = {
}, },
'dcim_update_manufacturer': { 'dcim_update_manufacturer': {
'description': 'Update an existing manufacturer', 'description': 'Update an existing manufacturer',
'properties': { 'properties': {'id': {'type': 'integer', 'description': 'Manufacturer ID'}},
'id': {'type': 'integer', 'description': 'Manufacturer ID'},
'name': {'type': 'string', 'description': 'New name'},
'slug': {'type': 'string', 'description': 'New slug'},
'description': {'type': 'string', 'description': 'Description'}
},
'required': ['id'] 'required': ['id']
}, },
'dcim_delete_manufacturer': { 'dcim_delete_manufacturer': {
@@ -265,16 +230,7 @@ TOOL_DEFINITIONS = {
}, },
'dcim_update_device_type': { 'dcim_update_device_type': {
'description': 'Update an existing device type', 'description': 'Update an existing device type',
'properties': { 'properties': {'id': {'type': 'integer', 'description': 'Device type ID'}},
'id': {'type': 'integer', 'description': 'Device type ID'},
'manufacturer': {'type': 'integer', 'description': 'Manufacturer ID'},
'model': {'type': 'string', 'description': 'Model name'},
'slug': {'type': 'string', 'description': 'New slug'},
'u_height': {'type': 'number', 'description': 'Height in rack units'},
'is_full_depth': {'type': 'boolean', 'description': 'Is full depth'},
'description': {'type': 'string', 'description': 'Description'},
'comments': {'type': 'string', 'description': 'Comments'}
},
'required': ['id'] 'required': ['id']
}, },
'dcim_delete_device_type': { 'dcim_delete_device_type': {
@@ -303,14 +259,7 @@ TOOL_DEFINITIONS = {
}, },
'dcim_update_device_role': { 'dcim_update_device_role': {
'description': 'Update an existing device role', 'description': 'Update an existing device role',
'properties': { 'properties': {'id': {'type': 'integer', 'description': 'Device role ID'}},
'id': {'type': 'integer', 'description': 'Device role ID'},
'name': {'type': 'string', 'description': 'New name'},
'slug': {'type': 'string', 'description': 'New slug'},
'color': {'type': 'string', 'description': 'Hex color code'},
'vm_role': {'type': 'boolean', 'description': 'Can be assigned to VMs'},
'description': {'type': 'string', 'description': 'Description'}
},
'required': ['id'] 'required': ['id']
}, },
'dcim_delete_device_role': { 'dcim_delete_device_role': {
@@ -341,13 +290,7 @@ TOOL_DEFINITIONS = {
}, },
'dcim_update_platform': { 'dcim_update_platform': {
'description': 'Update an existing platform', 'description': 'Update an existing platform',
'properties': { 'properties': {'id': {'type': 'integer', 'description': 'Platform ID'}},
'id': {'type': 'integer', 'description': 'Platform ID'},
'name': {'type': 'string', 'description': 'New name'},
'slug': {'type': 'string', 'description': 'New slug'},
'manufacturer': {'type': 'integer', 'description': 'Manufacturer ID'},
'description': {'type': 'string', 'description': 'Description'}
},
'required': ['id'] 'required': ['id']
}, },
'dcim_delete_platform': { 'dcim_delete_platform': {
@@ -383,13 +326,7 @@ TOOL_DEFINITIONS = {
'status': {'type': 'string', 'description': 'Device status'}, 'status': {'type': 'string', 'description': 'Device status'},
'rack': {'type': 'integer', 'description': 'Rack ID'}, 'rack': {'type': 'integer', 'description': 'Rack ID'},
'position': {'type': 'number', 'description': 'Position in rack'}, 'position': {'type': 'number', 'description': 'Position in rack'},
'serial': {'type': 'string', 'description': 'Serial number'}, 'serial': {'type': 'string', 'description': 'Serial number'}
'platform': {'type': 'integer', 'description': 'Platform ID'},
'primary_ip4': {'type': 'integer', 'description': 'Primary IPv4 address ID'},
'primary_ip6': {'type': 'integer', 'description': 'Primary IPv6 address ID'},
'asset_tag': {'type': 'string', 'description': 'Asset tag'},
'description': {'type': 'string', 'description': 'Description'},
'comments': {'type': 'string', 'description': 'Comments'}
}, },
'required': ['name', 'device_type', 'role', 'site'] 'required': ['name', 'device_type', 'role', 'site']
}, },
@@ -398,17 +335,7 @@ TOOL_DEFINITIONS = {
'properties': { 'properties': {
'id': {'type': 'integer', 'description': 'Device ID'}, 'id': {'type': 'integer', 'description': 'Device ID'},
'name': {'type': 'string', 'description': 'New name'}, 'name': {'type': 'string', 'description': 'New name'},
'status': {'type': 'string', 'description': 'New status'}, 'status': {'type': 'string', 'description': 'New status'}
'platform': {'type': 'integer', 'description': 'Platform ID'},
'primary_ip4': {'type': 'integer', 'description': 'Primary IPv4 address ID'},
'primary_ip6': {'type': 'integer', 'description': 'Primary IPv6 address ID'},
'serial': {'type': 'string', 'description': 'Serial number'},
'asset_tag': {'type': 'string', 'description': 'Asset tag'},
'site': {'type': 'integer', 'description': 'Site ID'},
'rack': {'type': 'integer', 'description': 'Rack ID'},
'position': {'type': 'number', 'description': 'Position in rack'},
'description': {'type': 'string', 'description': 'Description'},
'comments': {'type': 'string', 'description': 'Comments'}
}, },
'required': ['id'] 'required': ['id']
}, },
@@ -443,18 +370,7 @@ TOOL_DEFINITIONS = {
}, },
'dcim_update_interface': { 'dcim_update_interface': {
'description': 'Update an existing interface', 'description': 'Update an existing interface',
'properties': { 'properties': {'id': {'type': 'integer', 'description': 'Interface ID'}},
'id': {'type': 'integer', 'description': 'Interface ID'},
'name': {'type': 'string', 'description': 'New name'},
'type': {'type': 'string', 'description': 'Interface type'},
'enabled': {'type': 'boolean', 'description': 'Interface enabled'},
'mtu': {'type': 'integer', 'description': 'MTU'},
'mac_address': {'type': 'string', 'description': 'MAC address'},
'description': {'type': 'string', 'description': 'Description'},
'mode': {'type': 'string', 'description': 'VLAN mode'},
'untagged_vlan': {'type': 'integer', 'description': 'Untagged VLAN ID'},
'tagged_vlans': {'type': 'array', 'description': 'Tagged VLAN IDs'}
},
'required': ['id'] 'required': ['id']
}, },
'dcim_delete_interface': { 'dcim_delete_interface': {
@@ -488,15 +404,7 @@ TOOL_DEFINITIONS = {
}, },
'dcim_update_cable': { 'dcim_update_cable': {
'description': 'Update an existing cable', 'description': 'Update an existing cable',
'properties': { 'properties': {'id': {'type': 'integer', 'description': 'Cable ID'}},
'id': {'type': 'integer', 'description': 'Cable ID'},
'type': {'type': 'string', 'description': 'Cable type'},
'status': {'type': 'string', 'description': 'Cable status'},
'label': {'type': 'string', 'description': 'Cable label'},
'color': {'type': 'string', 'description': 'Cable color'},
'length': {'type': 'number', 'description': 'Cable length'},
'length_unit': {'type': 'string', 'description': 'Length unit'}
},
'required': ['id'] 'required': ['id']
}, },
'dcim_delete_cable': { 'dcim_delete_cable': {
@@ -584,15 +492,7 @@ TOOL_DEFINITIONS = {
}, },
'ipam_update_vrf': { 'ipam_update_vrf': {
'description': 'Update an existing VRF', 'description': 'Update an existing VRF',
'properties': { 'properties': {'id': {'type': 'integer', 'description': 'VRF ID'}},
'id': {'type': 'integer', 'description': 'VRF ID'},
'name': {'type': 'string', 'description': 'New name'},
'rd': {'type': 'string', 'description': 'Route distinguisher'},
'tenant': {'type': 'integer', 'description': 'Tenant ID'},
'enforce_unique': {'type': 'boolean', 'description': 'Enforce unique IPs'},
'description': {'type': 'string', 'description': 'Description'},
'comments': {'type': 'string', 'description': 'Comments'}
},
'required': ['id'] 'required': ['id']
}, },
'ipam_delete_vrf': { 'ipam_delete_vrf': {
@@ -631,19 +531,7 @@ TOOL_DEFINITIONS = {
}, },
'ipam_update_prefix': { 'ipam_update_prefix': {
'description': 'Update an existing prefix', 'description': 'Update an existing prefix',
'properties': { 'properties': {'id': {'type': 'integer', 'description': 'Prefix ID'}},
'id': {'type': 'integer', 'description': 'Prefix ID'},
'prefix': {'type': 'string', 'description': 'Prefix in CIDR notation'},
'status': {'type': 'string', 'description': 'Status'},
'site': {'type': 'integer', 'description': 'Site ID'},
'vrf': {'type': 'integer', 'description': 'VRF ID'},
'vlan': {'type': 'integer', 'description': 'VLAN ID'},
'role': {'type': 'integer', 'description': 'Role ID'},
'tenant': {'type': 'integer', 'description': 'Tenant ID'},
'is_pool': {'type': 'boolean', 'description': 'Is a pool'},
'description': {'type': 'string', 'description': 'Description'},
'comments': {'type': 'string', 'description': 'Comments'}
},
'required': ['id'] 'required': ['id']
}, },
'ipam_delete_prefix': { 'ipam_delete_prefix': {
@@ -694,18 +582,7 @@ TOOL_DEFINITIONS = {
}, },
'ipam_update_ip_address': { 'ipam_update_ip_address': {
'description': 'Update an existing IP address', 'description': 'Update an existing IP address',
'properties': { 'properties': {'id': {'type': 'integer', 'description': 'IP address ID'}},
'id': {'type': 'integer', 'description': 'IP address ID'},
'address': {'type': 'string', 'description': 'IP address with prefix length'},
'status': {'type': 'string', 'description': 'Status'},
'vrf': {'type': 'integer', 'description': 'VRF ID'},
'tenant': {'type': 'integer', 'description': 'Tenant ID'},
'dns_name': {'type': 'string', 'description': 'DNS name'},
'description': {'type': 'string', 'description': 'Description'},
'comments': {'type': 'string', 'description': 'Comments'},
'assigned_object_type': {'type': 'string', 'description': 'Object type to assign to'},
'assigned_object_id': {'type': 'integer', 'description': 'Object ID to assign to'}
},
'required': ['id'] 'required': ['id']
}, },
'ipam_delete_ip_address': { 'ipam_delete_ip_address': {
@@ -770,18 +647,7 @@ TOOL_DEFINITIONS = {
}, },
'ipam_update_vlan': { 'ipam_update_vlan': {
'description': 'Update an existing VLAN', 'description': 'Update an existing VLAN',
'properties': { 'properties': {'id': {'type': 'integer', 'description': 'VLAN ID'}},
'id': {'type': 'integer', 'description': 'VLAN ID'},
'vid': {'type': 'integer', 'description': 'VLAN ID number'},
'name': {'type': 'string', 'description': 'VLAN name'},
'status': {'type': 'string', 'description': 'Status'},
'site': {'type': 'integer', 'description': 'Site ID'},
'group': {'type': 'integer', 'description': 'VLAN group ID'},
'tenant': {'type': 'integer', 'description': 'Tenant ID'},
'role': {'type': 'integer', 'description': 'Role ID'},
'description': {'type': 'string', 'description': 'Description'},
'comments': {'type': 'string', 'description': 'Comments'}
},
'required': ['id'] 'required': ['id']
}, },
'ipam_delete_vlan': { 'ipam_delete_vlan': {
@@ -891,17 +757,16 @@ TOOL_DEFINITIONS = {
'properties': {'id': {'type': 'integer', 'description': 'Provider ID'}}, 'properties': {'id': {'type': 'integer', 'description': 'Provider ID'}},
'required': ['id'] 'required': ['id']
}, },
# NOTE: circuit_types tools shortened to meet 28-char limit 'circuits_list_circuit_types': {
'circ_list_types': {
'description': 'List all circuit types in NetBox', 'description': 'List all circuit types in NetBox',
'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}}
}, },
'circ_get_type': { 'circuits_get_circuit_type': {
'description': 'Get a specific circuit type by ID', 'description': 'Get a specific circuit type by ID',
'properties': {'id': {'type': 'integer', 'description': 'Circuit type ID'}}, 'properties': {'id': {'type': 'integer', 'description': 'Circuit type ID'}},
'required': ['id'] 'required': ['id']
}, },
'circ_create_type': { 'circuits_create_circuit_type': {
'description': 'Create a new circuit type', 'description': 'Create a new circuit type',
'properties': { 'properties': {
'name': {'type': 'string', 'description': 'Type name'}, 'name': {'type': 'string', 'description': 'Type name'},
@@ -944,20 +809,19 @@ TOOL_DEFINITIONS = {
'properties': {'id': {'type': 'integer', 'description': 'Circuit ID'}}, 'properties': {'id': {'type': 'integer', 'description': 'Circuit ID'}},
'required': ['id'] 'required': ['id']
}, },
# NOTE: circuit_terminations tools shortened to meet 28-char limit 'circuits_list_circuit_terminations': {
'circ_list_terminations': {
'description': 'List all circuit terminations in NetBox', 'description': 'List all circuit terminations in NetBox',
'properties': { 'properties': {
'circuit_id': {'type': 'integer', 'description': 'Filter by circuit ID'}, 'circuit_id': {'type': 'integer', 'description': 'Filter by circuit ID'},
'site_id': {'type': 'integer', 'description': 'Filter by site ID'} 'site_id': {'type': 'integer', 'description': 'Filter by site ID'}
} }
}, },
'circ_get_termination': { 'circuits_get_circuit_termination': {
'description': 'Get a specific circuit termination by ID', 'description': 'Get a specific circuit termination by ID',
'properties': {'id': {'type': 'integer', 'description': 'Termination ID'}}, 'properties': {'id': {'type': 'integer', 'description': 'Termination ID'}},
'required': ['id'] 'required': ['id']
}, },
'circ_create_termination': { 'circuits_create_circuit_termination': {
'description': 'Create a new circuit termination', 'description': 'Create a new circuit termination',
'properties': { 'properties': {
'circuit': {'type': 'integer', 'description': 'Circuit ID'}, 'circuit': {'type': 'integer', 'description': 'Circuit ID'},
@@ -968,18 +832,16 @@ TOOL_DEFINITIONS = {
}, },
# ==================== Virtualization Tools ==================== # ==================== Virtualization Tools ====================
# NOTE: Tool names shortened from 'virtualization_' to 'virt_' to meet 'virtualization_list_cluster_types': {
# 28-char limit (Claude API 64-char limit minus 36-char prefix)
'virt_list_cluster_types': {
'description': 'List all cluster types in NetBox', 'description': 'List all cluster types in NetBox',
'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}}
}, },
'virt_get_cluster_type': { 'virtualization_get_cluster_type': {
'description': 'Get a specific cluster type by ID', 'description': 'Get a specific cluster type by ID',
'properties': {'id': {'type': 'integer', 'description': 'Cluster type ID'}}, 'properties': {'id': {'type': 'integer', 'description': 'Cluster type ID'}},
'required': ['id'] 'required': ['id']
}, },
'virt_create_cluster_type': { 'virtualization_create_cluster_type': {
'description': 'Create a new cluster type', 'description': 'Create a new cluster type',
'properties': { 'properties': {
'name': {'type': 'string', 'description': 'Type name'}, 'name': {'type': 'string', 'description': 'Type name'},
@@ -987,16 +849,16 @@ TOOL_DEFINITIONS = {
}, },
'required': ['name', 'slug'] 'required': ['name', 'slug']
}, },
'virt_list_cluster_groups': { 'virtualization_list_cluster_groups': {
'description': 'List all cluster groups in NetBox', 'description': 'List all cluster groups in NetBox',
'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}}
}, },
'virt_get_cluster_group': { 'virtualization_get_cluster_group': {
'description': 'Get a specific cluster group by ID', 'description': 'Get a specific cluster group by ID',
'properties': {'id': {'type': 'integer', 'description': 'Cluster group ID'}}, 'properties': {'id': {'type': 'integer', 'description': 'Cluster group ID'}},
'required': ['id'] 'required': ['id']
}, },
'virt_create_cluster_group': { 'virtualization_create_cluster_group': {
'description': 'Create a new cluster group', 'description': 'Create a new cluster group',
'properties': { 'properties': {
'name': {'type': 'string', 'description': 'Group name'}, 'name': {'type': 'string', 'description': 'Group name'},
@@ -1004,7 +866,7 @@ TOOL_DEFINITIONS = {
}, },
'required': ['name', 'slug'] 'required': ['name', 'slug']
}, },
'virt_list_clusters': { 'virtualization_list_clusters': {
'description': 'List all clusters in NetBox', 'description': 'List all clusters in NetBox',
'properties': { 'properties': {
'name': {'type': 'string', 'description': 'Filter by name'}, 'name': {'type': 'string', 'description': 'Filter by name'},
@@ -1013,12 +875,12 @@ TOOL_DEFINITIONS = {
'site_id': {'type': 'integer', 'description': 'Filter by site ID'} 'site_id': {'type': 'integer', 'description': 'Filter by site ID'}
} }
}, },
'virt_get_cluster': { 'virtualization_get_cluster': {
'description': 'Get a specific cluster by ID', 'description': 'Get a specific cluster by ID',
'properties': {'id': {'type': 'integer', 'description': 'Cluster ID'}}, 'properties': {'id': {'type': 'integer', 'description': 'Cluster ID'}},
'required': ['id'] 'required': ['id']
}, },
'virt_create_cluster': { 'virtualization_create_cluster': {
'description': 'Create a new cluster', 'description': 'Create a new cluster',
'properties': { 'properties': {
'name': {'type': 'string', 'description': 'Cluster name'}, 'name': {'type': 'string', 'description': 'Cluster name'},
@@ -1029,27 +891,17 @@ TOOL_DEFINITIONS = {
}, },
'required': ['name', 'type'] 'required': ['name', 'type']
}, },
'virt_update_cluster': { 'virtualization_update_cluster': {
'description': 'Update an existing cluster', 'description': 'Update an existing cluster',
'properties': { 'properties': {'id': {'type': 'integer', 'description': 'Cluster ID'}},
'id': {'type': 'integer', 'description': 'Cluster ID'},
'name': {'type': 'string', 'description': 'New name'},
'type': {'type': 'integer', 'description': 'Cluster type ID'},
'group': {'type': 'integer', 'description': 'Cluster group ID'},
'site': {'type': 'integer', 'description': 'Site ID'},
'status': {'type': 'string', 'description': 'Status'},
'tenant': {'type': 'integer', 'description': 'Tenant ID'},
'description': {'type': 'string', 'description': 'Description'},
'comments': {'type': 'string', 'description': 'Comments'}
},
'required': ['id'] 'required': ['id']
}, },
'virt_delete_cluster': { 'virtualization_delete_cluster': {
'description': 'Delete a cluster', 'description': 'Delete a cluster',
'properties': {'id': {'type': 'integer', 'description': 'Cluster ID'}}, 'properties': {'id': {'type': 'integer', 'description': 'Cluster ID'}},
'required': ['id'] 'required': ['id']
}, },
'virt_list_vms': { 'virtualization_list_virtual_machines': {
'description': 'List all virtual machines in NetBox', 'description': 'List all virtual machines in NetBox',
'properties': { 'properties': {
'name': {'type': 'string', 'description': 'Filter by name'}, 'name': {'type': 'string', 'description': 'Filter by name'},
@@ -1058,12 +910,12 @@ TOOL_DEFINITIONS = {
'status': {'type': 'string', 'description': 'Filter by status'} 'status': {'type': 'string', 'description': 'Filter by status'}
} }
}, },
'virt_get_vm': { 'virtualization_get_virtual_machine': {
'description': 'Get a specific virtual machine by ID', 'description': 'Get a specific virtual machine by ID',
'properties': {'id': {'type': 'integer', 'description': 'VM ID'}}, 'properties': {'id': {'type': 'integer', 'description': 'VM ID'}},
'required': ['id'] 'required': ['id']
}, },
'virt_create_vm': { 'virtualization_create_virtual_machine': {
'description': 'Create a new virtual machine', 'description': 'Create a new virtual machine',
'properties': { 'properties': {
'name': {'type': 'string', 'description': 'VM name'}, 'name': {'type': 'string', 'description': 'VM name'},
@@ -1076,45 +928,29 @@ TOOL_DEFINITIONS = {
}, },
'required': ['name'] 'required': ['name']
}, },
'virt_update_vm': { 'virtualization_update_virtual_machine': {
'description': 'Update an existing virtual machine', 'description': 'Update an existing virtual machine',
'properties': { 'properties': {'id': {'type': 'integer', 'description': 'VM ID'}},
'id': {'type': 'integer', 'description': 'VM ID'},
'name': {'type': 'string', 'description': 'New name'},
'status': {'type': 'string', 'description': 'Status'},
'cluster': {'type': 'integer', 'description': 'Cluster ID'},
'site': {'type': 'integer', 'description': 'Site ID'},
'role': {'type': 'integer', 'description': 'Role ID'},
'tenant': {'type': 'integer', 'description': 'Tenant ID'},
'platform': {'type': 'integer', 'description': 'Platform ID'},
'vcpus': {'type': 'number', 'description': 'Number of vCPUs'},
'memory': {'type': 'integer', 'description': 'Memory in MB'},
'disk': {'type': 'integer', 'description': 'Disk in GB'},
'primary_ip4': {'type': 'integer', 'description': 'Primary IPv4 address ID'},
'primary_ip6': {'type': 'integer', 'description': 'Primary IPv6 address ID'},
'description': {'type': 'string', 'description': 'Description'},
'comments': {'type': 'string', 'description': 'Comments'}
},
'required': ['id'] 'required': ['id']
}, },
'virt_delete_vm': { 'virtualization_delete_virtual_machine': {
'description': 'Delete a virtual machine', 'description': 'Delete a virtual machine',
'properties': {'id': {'type': 'integer', 'description': 'VM ID'}}, 'properties': {'id': {'type': 'integer', 'description': 'VM ID'}},
'required': ['id'] 'required': ['id']
}, },
'virt_list_vm_ifaces': { 'virtualization_list_vm_interfaces': {
'description': 'List all VM interfaces in NetBox', 'description': 'List all VM interfaces in NetBox',
'properties': { 'properties': {
'virtual_machine_id': {'type': 'integer', 'description': 'Filter by VM ID'}, 'virtual_machine_id': {'type': 'integer', 'description': 'Filter by VM ID'},
'name': {'type': 'string', 'description': 'Filter by name'} 'name': {'type': 'string', 'description': 'Filter by name'}
} }
}, },
'virt_get_vm_iface': { 'virtualization_get_vm_interface': {
'description': 'Get a specific VM interface by ID', 'description': 'Get a specific VM interface by ID',
'properties': {'id': {'type': 'integer', 'description': 'Interface ID'}}, 'properties': {'id': {'type': 'integer', 'description': 'Interface ID'}},
'required': ['id'] 'required': ['id']
}, },
'virt_create_vm_iface': { 'virtualization_create_vm_interface': {
'description': 'Create a new VM interface', 'description': 'Create a new VM interface',
'properties': { 'properties': {
'virtual_machine': {'type': 'integer', 'description': 'VM ID'}, 'virtual_machine': {'type': 'integer', 'description': 'VM ID'},
@@ -1252,18 +1088,16 @@ TOOL_DEFINITIONS = {
}, },
# ==================== Wireless Tools ==================== # ==================== Wireless Tools ====================
# NOTE: Tool names shortened from 'wireless_' to 'wlan_' to meet 'wireless_list_wireless_lan_groups': {
# 28-char limit (Claude API 64-char limit minus 36-char prefix)
'wlan_list_groups': {
'description': 'List all wireless LAN groups in NetBox', 'description': 'List all wireless LAN groups in NetBox',
'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}}
}, },
'wlan_get_group': { 'wireless_get_wireless_lan_group': {
'description': 'Get a specific wireless LAN group by ID', 'description': 'Get a specific wireless LAN group by ID',
'properties': {'id': {'type': 'integer', 'description': 'WLAN group ID'}}, 'properties': {'id': {'type': 'integer', 'description': 'WLAN group ID'}},
'required': ['id'] 'required': ['id']
}, },
'wlan_create_group': { 'wireless_create_wireless_lan_group': {
'description': 'Create a new wireless LAN group', 'description': 'Create a new wireless LAN group',
'properties': { 'properties': {
'name': {'type': 'string', 'description': 'Group name'}, 'name': {'type': 'string', 'description': 'Group name'},
@@ -1271,7 +1105,7 @@ TOOL_DEFINITIONS = {
}, },
'required': ['name', 'slug'] 'required': ['name', 'slug']
}, },
'wlan_list_lans': { 'wireless_list_wireless_lans': {
'description': 'List all wireless LANs in NetBox', 'description': 'List all wireless LANs in NetBox',
'properties': { 'properties': {
'ssid': {'type': 'string', 'description': 'Filter by SSID'}, 'ssid': {'type': 'string', 'description': 'Filter by SSID'},
@@ -1279,12 +1113,12 @@ TOOL_DEFINITIONS = {
'status': {'type': 'string', 'description': 'Filter by status'} 'status': {'type': 'string', 'description': 'Filter by status'}
} }
}, },
'wlan_get_lan': { 'wireless_get_wireless_lan': {
'description': 'Get a specific wireless LAN by ID', 'description': 'Get a specific wireless LAN by ID',
'properties': {'id': {'type': 'integer', 'description': 'WLAN ID'}}, 'properties': {'id': {'type': 'integer', 'description': 'WLAN ID'}},
'required': ['id'] 'required': ['id']
}, },
'wlan_create_lan': { 'wireless_create_wireless_lan': {
'description': 'Create a new wireless LAN', 'description': 'Create a new wireless LAN',
'properties': { 'properties': {
'ssid': {'type': 'string', 'description': 'SSID'}, 'ssid': {'type': 'string', 'description': 'SSID'},
@@ -1294,14 +1128,14 @@ TOOL_DEFINITIONS = {
}, },
'required': ['ssid'] 'required': ['ssid']
}, },
'wlan_list_links': { 'wireless_list_wireless_links': {
'description': 'List all wireless links in NetBox', 'description': 'List all wireless links in NetBox',
'properties': { 'properties': {
'ssid': {'type': 'string', 'description': 'Filter by SSID'}, 'ssid': {'type': 'string', 'description': 'Filter by SSID'},
'status': {'type': 'string', 'description': 'Filter by status'} 'status': {'type': 'string', 'description': 'Filter by status'}
} }
}, },
'wlan_get_link': { 'wireless_get_wireless_link': {
'description': 'Get a specific wireless link by ID', 'description': 'Get a specific wireless link by ID',
'properties': {'id': {'type': 'integer', 'description': 'Link ID'}}, 'properties': {'id': {'type': 'integer', 'description': 'Link ID'}},
'required': ['id'] 'required': ['id']
@@ -1407,52 +1241,6 @@ TOOL_DEFINITIONS = {
} }
# Map shortened tool names to (category, method_name) for routing.
# This is necessary because tool names were shortened to meet the 28-character
# limit imposed by Claude API's 64-character tool name limit minus the
# 36-character prefix used by Claude Code for MCP tools.
TOOL_NAME_MAP = {
# Virtualization tools (virt_ -> virtualization category)
'virt_list_cluster_types': ('virtualization', 'list_cluster_types'),
'virt_get_cluster_type': ('virtualization', 'get_cluster_type'),
'virt_create_cluster_type': ('virtualization', 'create_cluster_type'),
'virt_list_cluster_groups': ('virtualization', 'list_cluster_groups'),
'virt_get_cluster_group': ('virtualization', 'get_cluster_group'),
'virt_create_cluster_group': ('virtualization', 'create_cluster_group'),
'virt_list_clusters': ('virtualization', 'list_clusters'),
'virt_get_cluster': ('virtualization', 'get_cluster'),
'virt_create_cluster': ('virtualization', 'create_cluster'),
'virt_update_cluster': ('virtualization', 'update_cluster'),
'virt_delete_cluster': ('virtualization', 'delete_cluster'),
'virt_list_vms': ('virtualization', 'list_virtual_machines'),
'virt_get_vm': ('virtualization', 'get_virtual_machine'),
'virt_create_vm': ('virtualization', 'create_virtual_machine'),
'virt_update_vm': ('virtualization', 'update_virtual_machine'),
'virt_delete_vm': ('virtualization', 'delete_virtual_machine'),
'virt_list_vm_ifaces': ('virtualization', 'list_vm_interfaces'),
'virt_get_vm_iface': ('virtualization', 'get_vm_interface'),
'virt_create_vm_iface': ('virtualization', 'create_vm_interface'),
# Circuits tools (circ_ -> circuits category, for shortened names only)
'circ_list_types': ('circuits', 'list_circuit_types'),
'circ_get_type': ('circuits', 'get_circuit_type'),
'circ_create_type': ('circuits', 'create_circuit_type'),
'circ_list_terminations': ('circuits', 'list_circuit_terminations'),
'circ_get_termination': ('circuits', 'get_circuit_termination'),
'circ_create_termination': ('circuits', 'create_circuit_termination'),
# Wireless tools (wlan_ -> wireless category)
'wlan_list_groups': ('wireless', 'list_wireless_lan_groups'),
'wlan_get_group': ('wireless', 'get_wireless_lan_group'),
'wlan_create_group': ('wireless', 'create_wireless_lan_group'),
'wlan_list_lans': ('wireless', 'list_wireless_lans'),
'wlan_get_lan': ('wireless', 'get_wireless_lan'),
'wlan_create_lan': ('wireless', 'create_wireless_lan'),
'wlan_list_links': ('wireless', 'list_wireless_links'),
'wlan_get_link': ('wireless', 'get_wireless_link'),
}
class NetBoxMCPServer: class NetBoxMCPServer:
"""MCP Server for NetBox integration""" """MCP Server for NetBox integration"""
@@ -1526,21 +1314,12 @@ class NetBoxMCPServer:
)] )]
async def _route_tool(self, name: str, arguments: dict): async def _route_tool(self, name: str, arguments: dict):
"""Route tool call to appropriate handler. """Route tool call to appropriate handler."""
parts = name.split('_', 1)
if len(parts) != 2:
raise ValueError(f"Invalid tool name format: {name}")
Tool names may be shortened (e.g., 'virt_list_vms' instead of category, method_name = parts[0], parts[1]
'virtualization_list_virtual_machines') to meet the 28-character
limit. TOOL_NAME_MAP handles the translation to actual method names.
"""
# Check if this is a mapped short name
if name in TOOL_NAME_MAP:
category, method_name = TOOL_NAME_MAP[name]
else:
# Fall back to original logic for unchanged tools
parts = name.split('_', 1)
if len(parts) != 2:
raise ValueError(f"Invalid tool name format: {name}")
category, method_name = parts[0], parts[1]
# Map category to tool class # Map category to tool class
tool_map = { tool_map = {

View File

@@ -70,15 +70,13 @@ cat ~/.config/claude/netbox.env 2>/dev/null || echo "FILE_NOT_FOUND"
### Step 3.3: Gather NetBox Information ### Step 3.3: Gather NetBox Information
Use AskUserQuestion: Use AskUserQuestion:
- Question: "What is your NetBox API URL? (e.g., https://netbox.company.com/api)" - Question: "What is your NetBox server URL? (e.g., https://netbox.company.com)"
- Header: "NetBox URL" - Header: "NetBox URL"
- Options: - Options:
- "Other (I'll provide the URL)" - "Other (I'll provide the URL)"
Ask user to provide the URL. Ask user to provide the URL.
**Important:** The URL must include `/api` at the end. If the user provides a URL without `/api`, append it automatically.
### Step 3.4: Create Configuration File ### Step 3.4: Create Configuration File
```bash ```bash
@@ -122,11 +120,9 @@ Use AskUserQuestion:
### Step 4.1: Test Configuration (if token was added) ### Step 4.1: Test Configuration (if token was added)
```bash ```bash
source ~/.config/claude/netbox.env && curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Token $NETBOX_API_TOKEN" "$NETBOX_API_URL/" source ~/.config/claude/netbox.env && curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Token $NETBOX_API_TOKEN" "$NETBOX_API_URL/api/"
``` ```
**Note:** The URL already includes `/api`, so we just append `/` for the root API endpoint.
Report result: Report result:
- 200: Success - 200: Success
- 403: Invalid token - 403: Invalid token

View File

@@ -259,89 +259,94 @@ Use this exact template:
### Step 7: Create Issue in Marketplace ### Step 7: Create Issue in Marketplace
**IMPORTANT:** Always use curl to create issues in the marketplace repo. This avoids branch protection restrictions and MCP context issues that can block issue creation when working on protected branches. **First, check if MCP tools are available.** Attempt to use an MCP tool. If you receive "tool not found", "not in function list", or similar error, the MCP server is not accessible in this session - use the curl fallback.
**1. Load Gitea credentials:** #### Option A: MCP Available (preferred)
**If ASSOCIATE_WITH_SPRINT is true:**
```
mcp__plugin_projman_gitea__create_issue(
repo=MARKETPLACE_REPO,
title="[Diagnostic] [summary of main failure]",
body=[generated content from Step 6],
labels=FINAL_LABELS,
milestone=ACTIVE_SPRINT.id
)
```
**If ASSOCIATE_WITH_SPRINT is false (standalone fix):**
```
mcp__plugin_projman_gitea__create_issue(
repo=MARKETPLACE_REPO,
title="[Diagnostic] [summary of main failure]",
body=[generated content from Step 6],
labels=FINAL_LABELS
)
```
If some labels don't exist, create issue with available labels only.
#### Option B: MCP Unavailable - Use curl Fallback
If MCP tools are not available (the very issue you may be diagnosing), use this fallback:
**1. Check for Gitea credentials:**
```bash ```bash
if [[ -f ~/.config/claude/gitea.env ]]; then if [[ -f ~/.config/claude/gitea.env ]]; then
source ~/.config/claude/gitea.env source ~/.config/claude/gitea.env
echo "Credentials loaded. API URL: $GITEA_API_URL" echo "Credentials found. API URL: $GITEA_API_URL"
else else
echo "ERROR: No credentials at ~/.config/claude/gitea.env" echo "No credentials at ~/.config/claude/gitea.env"
fi fi
``` ```
**2. Fetch label IDs from marketplace repo:** **2. If credentials exist, create issue via curl with proper JSON escaping:**
The diagnostic labels to apply are: Create secure temp files and save content:
- `Source/Diagnostic` (always)
- `Type/Bug` (always)
```bash
# Fetch all labels and extract IDs for our target labels
LABELS_JSON=$(curl -s "${GITEA_API_URL}/repos/${MARKETPLACE_REPO}/labels" \
-H "Authorization: token ${GITEA_API_TOKEN}")
# Extract label IDs (handles both org and repo labels)
SOURCE_DIAG_ID=$(echo "$LABELS_JSON" | jq -r '.[] | select(.name == "Source/Diagnostic") | .id')
TYPE_BUG_ID=$(echo "$LABELS_JSON" | jq -r '.[] | select(.name == "Type/Bug") | .id')
# Build label array (only include IDs that were found)
LABEL_IDS="[]"
if [[ -n "$SOURCE_DIAG_ID" && -n "$TYPE_BUG_ID" ]]; then
LABEL_IDS="[$SOURCE_DIAG_ID, $TYPE_BUG_ID]"
elif [[ -n "$SOURCE_DIAG_ID" ]]; then
LABEL_IDS="[$SOURCE_DIAG_ID]"
elif [[ -n "$TYPE_BUG_ID" ]]; then
LABEL_IDS="[$TYPE_BUG_ID]"
fi
echo "Label IDs to apply: $LABEL_IDS"
```
**3. Create issue with labels via curl:**
```bash ```bash
# Create temp files with restrictive permissions # Create temp files with restrictive permissions
DIAG_TITLE=$(mktemp -t diag-title.XXXXXX) DIAG_TITLE=$(mktemp -p /tmp -m 600 diag-title.XXXXXX)
DIAG_BODY=$(mktemp -t diag-body.XXXXXX) DIAG_BODY=$(mktemp -p /tmp -m 600 diag-body.XXXXXX)
DIAG_PAYLOAD=$(mktemp -t diag-payload.XXXXXX) DIAG_PAYLOAD=$(mktemp -p /tmp -m 600 diag-payload.XXXXXX)
# Save title # Save title
echo "[Diagnostic] [summary of main failure]" > "$DIAG_TITLE" echo "[Diagnostic] [summary of main failure]" > "$DIAG_TITLE"
# Save body (paste Step 6 content) - heredoc delimiter prevents shell expansion # Save body (paste Step 5 content) - heredoc delimiter prevents shell expansion
cat > "$DIAG_BODY" << 'DIAGNOSTIC_EOF' cat > "$DIAG_BODY" << 'DIAGNOSTIC_EOF'
[Paste the full issue content from Step 6 here] [Paste the full issue content from Step 5 here]
DIAGNOSTIC_EOF DIAGNOSTIC_EOF
```
# Build JSON payload with labels using jq Construct JSON safely using jq's --rawfile (avoids command substitution):
```bash
# Build JSON payload using jq with --rawfile for safe content handling
jq -n \ jq -n \
--rawfile title "$DIAG_TITLE" \ --rawfile title "$DIAG_TITLE" \
--rawfile body "$DIAG_BODY" \ --rawfile body "$DIAG_BODY" \
--argjson labels "$LABEL_IDS" \ '{title: ($title | rtrimstr("\n")), body: $body}' > "$DIAG_PAYLOAD"
'{title: ($title | rtrimstr("\n")), body: $body, labels: $labels}' > "$DIAG_PAYLOAD"
# Create issue using the JSON file # Create issue using the JSON file
RESULT=$(curl -s -X POST "${GITEA_API_URL}/repos/${MARKETPLACE_REPO}/issues" \ curl -s -X POST "${GITEA_API_URL}/repos/${MARKETPLACE_REPO}/issues" \
-H "Authorization: token ${GITEA_API_TOKEN}" \ -H "Authorization: token ${GITEA_API_TOKEN}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d @"$DIAG_PAYLOAD") -d @"$DIAG_PAYLOAD" | jq '.html_url // .'
# Extract and display the issue URL
echo "$RESULT" | jq -r '.html_url // "Error: " + (.message // "Unknown error")'
# Secure cleanup # Secure cleanup
rm -f "$DIAG_TITLE" "$DIAG_BODY" "$DIAG_PAYLOAD" rm -f "$DIAG_TITLE" "$DIAG_BODY" "$DIAG_PAYLOAD"
``` ```
**4. If no credentials found, save report locally:** **3. If no credentials found, save report locally:**
```bash ```bash
REPORT_FILE=$(mktemp -t diagnostic-report-XXXXXX.md) REPORT_FILE=$(mktemp -p /tmp -m 600 diagnostic-report-XXXXXX.md)
cat > "$REPORT_FILE" << 'DIAGNOSTIC_EOF' cat > "$REPORT_FILE" << 'DIAGNOSTIC_EOF'
[Paste the full issue content from Step 6 here] [Paste the full issue content from Step 5 here]
DIAGNOSTIC_EOF DIAGNOSTIC_EOF
echo "Report saved to: $REPORT_FILE" echo "Report saved to: $REPORT_FILE"
``` ```
@@ -349,7 +354,7 @@ echo "Report saved to: $REPORT_FILE"
Then inform the user: Then inform the user:
``` ```
No Gitea credentials found at ~/.config/claude/gitea.env. MCP tools are unavailable and no Gitea credentials found at ~/.config/claude/gitea.env.
Diagnostic report saved to: [REPORT_FILE] Diagnostic report saved to: [REPORT_FILE]
@@ -390,7 +395,6 @@ Next Steps:
- **DO NOT** skip any diagnostic test - **DO NOT** skip any diagnostic test
- **DO NOT** call MCP tools without the `repo` parameter - **DO NOT** call MCP tools without the `repo` parameter
- **DO NOT** ask user questions during execution - run autonomously - **DO NOT** ask user questions during execution - run autonomously
- **DO NOT** use MCP tools to create issues in the marketplace - always use curl (avoids branch restrictions)
## If All Tests Pass ## If All Tests Pass
@@ -421,12 +425,7 @@ and I can create a manual bug report.
- Check if in a git repository: `git rev-parse --git-dir` - Check if in a git repository: `git rev-parse --git-dir`
- If not a git repo, ask user for the repository path - If not a git repo, ask user for the repository path
**Gitea credentials not found** **MCP tools not available**
- Credentials must be at `~/.config/claude/gitea.env` - Use the curl fallback in Step 6, Option B
- If missing, the report will be saved locally for manual submission - Requires Gitea credentials at `~/.config/claude/gitea.env`
- See docs/CONFIGURATION.md for setup instructions - If no credentials, report will be saved locally for manual submission
**Labels not applied to issue**
- Verify labels exist in the marketplace repo: `Source/Diagnostic`, `Type/Bug`
- Check the label fetch output in Step 7.2 for errors
- If labels don't exist, create them first with `/labels-sync` in the marketplace repo

View File

@@ -1,172 +0,0 @@
#!/bin/bash
# release.sh - Create a new release with version consistency
#
# Usage: ./scripts/release.sh X.Y.Z
#
# This script ensures all version references are updated consistently:
# 1. CHANGELOG.md - [Unreleased] becomes [X.Y.Z] - YYYY-MM-DD
# 2. README.md - Title updated to vX.Y.Z
# 3. marketplace.json - version field updated
# 4. Git commit and tag created
#
# Prerequisites:
# - Clean working directory (no uncommitted changes)
# - [Unreleased] section in CHANGELOG.md with content
# - On development branch
set -e
VERSION=$1
DATE=$(date +%Y-%m-%d)
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
error() { echo -e "${RED}ERROR: $1${NC}" >&2; exit 1; }
warn() { echo -e "${YELLOW}WARNING: $1${NC}"; }
success() { echo -e "${GREEN}$1${NC}"; }
info() { echo -e "$1"; }
# Validate arguments
if [ -z "$VERSION" ]; then
echo "Usage: ./scripts/release.sh X.Y.Z"
echo ""
echo "Example: ./scripts/release.sh 3.2.0"
echo ""
echo "This will:"
echo " 1. Update CHANGELOG.md [Unreleased] -> [X.Y.Z] - $(date +%Y-%m-%d)"
echo " 2. Update README.md title to vX.Y.Z"
echo " 3. Update marketplace.json version to X.Y.Z"
echo " 4. Commit with message 'chore: release vX.Y.Z'"
echo " 5. Create git tag vX.Y.Z"
exit 1
fi
# Validate version format
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
error "Invalid version format. Use X.Y.Z (e.g., 3.2.0)"
fi
# Check we're in the right directory
if [ ! -f "CHANGELOG.md" ] || [ ! -f "README.md" ] || [ ! -f ".claude-plugin/marketplace.json" ]; then
error "Must run from repository root (CHANGELOG.md, README.md, .claude-plugin/marketplace.json must exist)"
fi
# Check for clean working directory
if [ -n "$(git status --porcelain)" ]; then
warn "Working directory has uncommitted changes"
echo ""
git status --short
echo ""
read -p "Continue anyway? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
# Check current branch
BRANCH=$(git branch --show-current)
if [ "$BRANCH" != "development" ] && [ "$BRANCH" != "main" ]; then
warn "Not on development or main branch (current: $BRANCH)"
read -p "Continue anyway? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
# Check [Unreleased] section has content
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
error "CHANGELOG.md missing [Unreleased] section"
fi
# Check if tag already exists
if git tag -l | grep -q "^v$VERSION$"; then
error "Tag v$VERSION already exists"
fi
info ""
info "=== Release v$VERSION ==="
info ""
# Show what will change
info "Changes to be made:"
info " CHANGELOG.md: [Unreleased] -> [$VERSION] - $DATE"
info " README.md: title -> v$VERSION"
info " marketplace.json: version -> $VERSION"
info " Git: commit + tag v$VERSION"
info ""
# Preview CHANGELOG [Unreleased] content
info "Current [Unreleased] content:"
info "---"
sed -n '/^## \[Unreleased\]/,/^## \[/p' CHANGELOG.md | head -30
info "---"
info ""
read -p "Proceed with release? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
info "Aborted"
exit 0
fi
info ""
info "Updating files..."
# 1. Update CHANGELOG.md
# Replace [Unreleased] with [X.Y.Z] - DATE and add new [Unreleased] section
sed -i "s/^## \[Unreleased\]$/## [Unreleased]\n\n*Changes staged for the next release*\n\n---\n\n## [$VERSION] - $DATE/" CHANGELOG.md
# Remove the placeholder text if it exists after the new [Unreleased]
sed -i '/^\*Changes staged for the next release\*$/d' CHANGELOG.md
# Clean up any double blank lines
sed -i '/^$/N;/^\n$/d' CHANGELOG.md
success " CHANGELOG.md updated"
# 2. Update README.md title
sed -i "s/^# Leo Claude Marketplace - v[0-9]\+\.[0-9]\+\.[0-9]\+$/# Leo Claude Marketplace - v$VERSION/" README.md
success " README.md updated"
# 3. Update marketplace.json version
sed -i "s/\"version\": \"[0-9]\+\.[0-9]\+\.[0-9]\+\"/\"version\": \"$VERSION\"/" .claude-plugin/marketplace.json
success " marketplace.json updated"
info ""
info "Files updated. Review changes:"
info ""
git diff --stat
info ""
git diff CHANGELOG.md | head -40
info ""
read -p "Commit and tag? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
warn "Changes made but not committed. Run 'git checkout -- .' to revert."
exit 0
fi
# Commit
git add CHANGELOG.md README.md .claude-plugin/marketplace.json
git commit -m "chore: release v$VERSION"
success " Committed"
# Tag
git tag "v$VERSION"
success " Tagged v$VERSION"
info ""
success "=== Release v$VERSION created ==="
info ""
info "Next steps:"
info " 1. Review the commit: git show HEAD"
info " 2. Push to remote: git push && git push --tags"
info " 3. Merge to main if on development branch"
info ""