Compare commits
2 Commits
d609974f8e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c617c96929 | |||
| b5c843ca69 |
@@ -1,96 +0,0 @@
|
|||||||
# YouTrack API References
|
|
||||||
|
|
||||||
## Core API Endpoints
|
|
||||||
|
|
||||||
### Base URL
|
|
||||||
Your YouTrack instance: `https://your-instance.youtrack.cloud/`
|
|
||||||
API base: `https://your-instance.youtrack.cloud/api/`
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
- Use permanent token with `Authorization: Bearer <token>` header
|
|
||||||
- To generate a token: From main navigation menu, select **Administration** > **Access Management** > **Users**, find your user, and generate a permanent API token
|
|
||||||
|
|
||||||
### Projects
|
|
||||||
|
|
||||||
**List all projects**
|
|
||||||
- `GET /api/admin/projects?fields=id,name,shortName`
|
|
||||||
- Returns array of project objects with id, name, shortName
|
|
||||||
|
|
||||||
**Get specific project**
|
|
||||||
- `GET /api/admin/projects/{projectId}?fields=id,name,shortName,description`
|
|
||||||
|
|
||||||
### Issues
|
|
||||||
|
|
||||||
**List issues with query**
|
|
||||||
- `GET /api/issues?query={query}&fields={fields}`
|
|
||||||
- Query examples:
|
|
||||||
- `project: MyProject`
|
|
||||||
- `project: MyProject updated: 2026-01-01 ..`
|
|
||||||
- `assignee: me`
|
|
||||||
- Common fields to request:
|
|
||||||
- `id,summary,description,created,updated,project(id,name),customFields(name,value)`
|
|
||||||
|
|
||||||
**Get issue**
|
|
||||||
- `GET /api/issues/{issueId}`
|
|
||||||
|
|
||||||
**Create issue**
|
|
||||||
- `POST /api/issues`
|
|
||||||
- Body: `{"project": {"id": "projectId"}, "summary": "Summary", "description": "Description"}`
|
|
||||||
|
|
||||||
**Update issue**
|
|
||||||
- `POST /api/issues/{issueId}`
|
|
||||||
- Body: `{"summary": "New summary", "description": "New description"}`
|
|
||||||
|
|
||||||
### Time Tracking
|
|
||||||
|
|
||||||
**Get work items for an issue**
|
|
||||||
- `GET /api/issues/{issueId}/timeTracking/workItems`
|
|
||||||
- Returns array of work items with:
|
|
||||||
- `id`, `date`, `duration` (in minutes), `author`, `text`
|
|
||||||
|
|
||||||
**Work item structure:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "...",
|
|
||||||
"date": "2026-01-15T10:30:00.000+0000",
|
|
||||||
"duration": {"minutes": 30},
|
|
||||||
"author": {"name": "User Name", "id": "..."},
|
|
||||||
"text": "Work description",
|
|
||||||
"type": {...}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Knowledge Base (Articles)
|
|
||||||
|
|
||||||
**List articles**
|
|
||||||
- `GET /api/articles?project={projectId}`
|
|
||||||
|
|
||||||
**Get article**
|
|
||||||
- `GET /api/articles/{articleId}`
|
|
||||||
|
|
||||||
**Create article**
|
|
||||||
- `POST /api/articles`
|
|
||||||
- Body: `{"project": {"id": "projectId"}, "title": "Title", "content": "Content"}`
|
|
||||||
|
|
||||||
## Query Language Examples
|
|
||||||
|
|
||||||
```
|
|
||||||
project: MyProject
|
|
||||||
project: MyProject assignee: me
|
|
||||||
project: MyProject updated >= 2026-01-01
|
|
||||||
priority: Critical
|
|
||||||
has: time
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** Date filtering in REST API uses `updated >= YYYY-MM-DD` format, not `updated: date ..` format.
|
|
||||||
|
|
||||||
## Field IDs Reference
|
|
||||||
|
|
||||||
Common custom field IDs (may vary by setup):
|
|
||||||
- Priority: `priority`
|
|
||||||
- State: `State`
|
|
||||||
- Assignee: `Assignee`
|
|
||||||
- Due Date: `Due Date`
|
|
||||||
- Type: `Type`
|
|
||||||
|
|
||||||
Check your instance's actual field IDs in YouTrack UI or via API.
|
|
||||||
@@ -1,218 +1,104 @@
|
|||||||
---
|
---
|
||||||
name: youtrack
|
name: youtrack
|
||||||
description: Interact with YouTrack project management system via REST API. Read projects and issues, create tasks, generate invoices from time tracking data, and manage knowledge base articles. Use for reading projects and work items, creating or updating issues, generating client invoices from time tracking, and working with knowledge base articles.
|
description: Dynamic access to youtrack MCP server (19 tools)
|
||||||
|
user-invocable: false
|
||||||
|
disable-model-invocation: false
|
||||||
---
|
---
|
||||||
|
|
||||||
# YouTrack
|
# youtrack Skill
|
||||||
|
|
||||||
YouTrack integration for project management, time tracking, and knowledge base.
|
This skill provides dynamic access to the youtrack MCP server without loading all tool definitions into context.
|
||||||
|
|
||||||
## Quick Start
|
## Available Tools
|
||||||
|
|
||||||
### Authentication
|
- `log_work`: Adds a work item (spent time) to the specified issue. You can specify the duration (in minutes), optional date, work type, description, and optional work item attributes. Use get_project to retrieve the workTypes and workItemAttributesSchema for the target project.
|
||||||
|
- `manage_issue_tags`: Adds a tag to or removes a tag from an issue. If a name is used, the first tag that matches the provided name is added. If no matching tags are found, an error message with suggestions for similar tags is returned. When successful, it returns the ID of the updated issue and the updated list of issue tags.
|
||||||
|
- `search_issues`: Searches for issues using YouTrack’s query language. The 'query' can combine attribute filters, keywords, and free text. Examples of common patterns include:
|
||||||
|
|
||||||
To generate a permanent token:
|
- Free text: Find matching words in the issue summary, description, and comments. Use wildcards: '*' for any characters, '?' for single characters (e.g., 'summary: log*', 'fix??'). Examples: 'login button bug', 'some other text', 'summary: log*', 'description: fix??'.
|
||||||
1. From the main navigation menu, select **Administration** > **Access Management** > **Users**
|
- Linked issues: '<linkType>: <issueId>' (by link type), 'links: <issueId>' (all linked to issueId issues). Examples: 'relates to: DEMO-123', 'subtask of: DEMO-123' (issues where DEMO-123 is a parent), 'links: DEMO-12' (issues linked to DEMO-12 with any link type). Hint: get_issue returns 'linkedIssueCounts' property which shows the available link types for the issue.
|
||||||
2. Find your user and click to open settings
|
- Issues where an issue is mentioned: 'mentions: <issueId>'. Examples: 'mentions: DEMO-123'.
|
||||||
3. Generate a new permanent API token
|
- Project filter: 'project: <ProjectName>'. Use project name or project key. Examples: 'project: {Mobile App}', 'project: MA'.
|
||||||
4. Set the token as an environment variable:
|
- Assignee filter: 'for: <login>'. Use 'me' for the currently authenticated user. Examples: 'for: me', 'for: john.smith'.
|
||||||
|
- Reporter filter: 'reporter: <login>'. Use 'me' for the currently authenticated user. Examples: 'reporter: me', 'reporter: admin'.
|
||||||
```bash
|
- Tag filter: 'tag: <TagName>'. Wrap multi-word tags in braces { }. Examples: 'tag: urgent', 'tag: {customer feedback}'.
|
||||||
export YOUTRACK_TOKEN=your-permanent-token-here
|
- Field filter: '<FieldName>: <Value>'. For any project field, for example, State, Type, Priority, and so on. Wrap multi-word names or values in { }. Use get_project to get the possible fields and values for the project issues to search. Use '-' as 'not', e.g., 'State: -Fixed' filters out fixed issues. Examples: 'Priority: High', 'State: {In Progress}, Fixed' (searches issues with 'In Progress' state + issues with 'Fixed' state), 'Due Date: {plus 5d}' (issues that are due in five days).
|
||||||
```
|
- Date filters: 'created:', 'updated:', 'resolved date:' (or any date field) plus a date, range, or relative period. Relative periods: 'today', 'yesterday', '{This week}', '{Last week}', '{This month}', etc. Examples: 'created: {This month}', 'updated: today', 'resolved date: 2025-06-01 .. 2025-06-30', 'updated: {minus 2h} .. *' (issues updated last 2 hours), 'created: * .. {minus 1y 6M}' (issues that are at least one and a half years old).
|
||||||
|
- Keywords: '#Unresolved' to find unresolved issues based on the State; '#Resolved' to find resolved issues.
|
||||||
**Important:** Configure your hourly rate (default $100/hour) by passing `--rate` to invoice_generator.py or updating `hourly_rate` parameter in your code.
|
- Empty/Non-Empty Fields: Use the 'has: <attribute>'. Example: 'has: attachments' finds issues with attachments, while 'has: -comments' finds issues with no comments. Other attributes: 'links', '<linkType>' (e.g. 'has: {subtask of}'), 'star' (subscription), 'votes', 'work'.
|
||||||
|
- Combining filters: List multiple conditions separated by spaces (logical AND). For OR operator, add it explicitly. Examples: '(project: MA) and (for: me) and (created: {minus 8h} .. *) and runtime error' (issues in project MA and assigned to currently authenticated user and created during last 8h and contains 'runtime error' text), '(Type: Task and State: Open) or (Type: Bug and Priority: Critical)'.
|
||||||
Then use any YouTrack script:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# List all projects
|
|
||||||
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-projects
|
|
||||||
|
|
||||||
# List issues in a project
|
|
||||||
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-issues "project: MyProject"
|
|
||||||
|
|
||||||
# Generate invoice for a project
|
|
||||||
python3 scripts/invoice_generator.py --url https://your-instance.youtrack.cloud --project MyProject --month "January 2026" --from-date "2026-01-01"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Python Scripts
|
|
||||||
|
|
||||||
### `scripts/youtrack_api.py`
|
|
||||||
|
|
||||||
Core API client for all YouTrack operations.
|
|
||||||
|
|
||||||
**In your Python code:**
|
|
||||||
```python
|
|
||||||
from youtrack_api import YouTrackAPI
|
|
||||||
|
|
||||||
api = YouTrackAPI('https://your-instance.youtrack.cloud', token='your-token')
|
|
||||||
|
|
||||||
# Projects
|
|
||||||
projects = api.get_projects()
|
|
||||||
project = api.get_project('project-id')
|
|
||||||
|
|
||||||
# Issues
|
|
||||||
issues = api.get_issues(query='project: MyProject')
|
|
||||||
issue = api.get_issue('issue-id')
|
|
||||||
|
|
||||||
# Create issue
|
|
||||||
api.create_issue('project-id', 'Summary', 'Description')
|
|
||||||
|
|
||||||
# Work items (time tracking)
|
|
||||||
work_items = api.get_work_items('issue-id')
|
|
||||||
issue_with_time = api.get_issue_with_work_items('issue-id')
|
|
||||||
|
|
||||||
# Knowledge base
|
|
||||||
articles = api.get_articles()
|
|
||||||
article = api.get_article('article-id')
|
|
||||||
api.create_article('project-id', 'Title', 'Content')
|
|
||||||
```
|
|
||||||
|
|
||||||
**CLI usage:**
|
|
||||||
```bash
|
|
||||||
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud \
|
|
||||||
--token YOUR_TOKEN \
|
|
||||||
--list-projects
|
|
||||||
|
|
||||||
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud \
|
|
||||||
--get-issue ABC-123
|
|
||||||
|
|
||||||
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud \
|
|
||||||
--get-articles
|
|
||||||
```
|
|
||||||
|
|
||||||
### `scripts/invoice_generator.py`
|
|
||||||
|
|
||||||
Generate client invoices from time tracking data.
|
|
||||||
|
|
||||||
**In your Python code:**
|
|
||||||
```python
|
|
||||||
from youtrack_api import YouTrackAPI
|
|
||||||
from invoice_generator import InvoiceGenerator
|
|
||||||
|
|
||||||
api = YouTrackAPI('https://your-instance.youtrack.cloud', token='your-token')
|
|
||||||
generator = InvoiceGenerator(api, hourly_rate=100.0)
|
|
||||||
|
|
||||||
# Get time data for a project
|
|
||||||
project_data = generator.get_project_time_data('project-id', from_date='2026-01-01')
|
|
||||||
|
|
||||||
# Generate invoice
|
|
||||||
invoice_text = generator.generate_invoice_text(project_data, month='January 2026')
|
|
||||||
print(invoice_text)
|
|
||||||
```
|
|
||||||
|
|
||||||
**CLI usage:**
|
|
||||||
```bash
|
|
||||||
python3 scripts/invoice_generator.py \
|
|
||||||
--url https://your-instance.youtrack.cloud \
|
|
||||||
--project MyProject \
|
|
||||||
--from-date 2026-01-01 \
|
|
||||||
--month "January 2026" \
|
|
||||||
--rate 100 \
|
|
||||||
--format text
|
|
||||||
```
|
|
||||||
|
|
||||||
Save the text output and print to PDF for clients.
|
|
||||||
|
|
||||||
## Common Workflows
|
|
||||||
|
|
||||||
### 1. List All Projects
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-projects
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Find Issues in a Project
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# All issues in a project
|
|
||||||
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-issues "project: MyProject"
|
|
||||||
|
|
||||||
# Issues updated since a date
|
|
||||||
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-issues "project: MyProject updated >= 2026-01-01"
|
|
||||||
|
|
||||||
# Issues assigned to you
|
|
||||||
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-issues "assignee: me"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Create a New Issue
|
|
||||||
|
|
||||||
```python
|
|
||||||
from youtrack_api import YouTrackAPI
|
|
||||||
|
|
||||||
api = YouTrackAPI('https://your-instance.youtrack.cloud')
|
|
||||||
api.create_issue(
|
|
||||||
project_id='MyProject',
|
|
||||||
summary='Task title',
|
|
||||||
description='Task description'
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Generate Monthly Invoice
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate invoice for January 2026
|
|
||||||
python3 scripts/invoice_generator.py \
|
|
||||||
--url https://your-instance.youtrack.cloud \
|
|
||||||
--project ClientProject \
|
|
||||||
--from-date 2026-01-01 \
|
|
||||||
--month "January 2026" \
|
|
||||||
--rate 100 \
|
|
||||||
--format text > invoice.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
Save the text output and print to PDF for clients.
|
|
||||||
|
|
||||||
### 5. Read Knowledge Base
|
|
||||||
|
|
||||||
```python
|
|
||||||
from youtrack_api import YouTrackAPI
|
|
||||||
|
|
||||||
api = YouTrackAPI('https://your-instance.youtrack.cloud')
|
|
||||||
|
|
||||||
# All articles
|
|
||||||
articles = api.get_articles()
|
|
||||||
|
|
||||||
# Articles for specific project
|
|
||||||
articles = api.get_articles(project_id='MyProject')
|
|
||||||
|
|
||||||
# Get specific article
|
|
||||||
article = api.get_article('article-id')
|
|
||||||
```
|
|
||||||
|
|
||||||
## Billing Logic
|
|
||||||
|
|
||||||
Invoice generator uses this calculation:
|
|
||||||
|
|
||||||
1. Sum all time tracked per issue (in minutes)
|
|
||||||
2. Convert to 30-minute increments (round up)
|
|
||||||
3. Minimum charge is 30 minutes (at configured rate/2)
|
|
||||||
4. Multiply by rate (default $100/hour = $50 per half-hour)
|
|
||||||
|
|
||||||
|
Returns basic info: id, summary, project, resolved, reporter, created, updated and default custom fields. For full details, use get_issue. The response is paginated using the specified offset and limit.
|
||||||
|
- `update_issue`: Updates an existing issue and its fields (customFields). Pass any of the arguments to partially update the issue:
|
||||||
|
- 'summary' or 'description' arguments to update only the issue summary or description.
|
||||||
|
- 'customFields' argument as key-value JSON object to update issue fields like State, Type, Priority, etc. Use get_issue_fields_schema to discover 'customFields' and their possible values.
|
||||||
|
- 'subscription' argument to star (true) or unstar (false) the issue on behalf of the current user. The current user is notified about subsequent issue updates according to their subscription settings for the Star tag.
|
||||||
|
- 'vote' argument to vote (true) or remove a vote (false) on behalf of the current user for the issue.
|
||||||
|
Returns the ID of the updated issue and the confirmation what was updated.
|
||||||
|
- `get_project`: Retrieves full details for a specific project.
|
||||||
|
- `get_saved_issue_searches`: Returns saved searches marked as favorites by the current user. The output search queries can be used in search_issues. The response is paginated using the specified offset and/or limit.
|
||||||
|
- `get_user_group_members`: Lists users who are members of a specified group or project team. Project teams are essentially groups that are always associated with a specific project. The response is paginated using the specified offset and/or limit.
|
||||||
|
- `link_issues`: Links two issues with the specified link type.
|
||||||
Examples:
|
Examples:
|
||||||
- 15 minutes → $50 (30 min minimum)
|
- TS-1 is a subtask of TS-2: {"targetIssueId": "TS-1", "linkType": "subtask of", "issueToLinkId": "TS-2"};
|
||||||
- 35 minutes → $100 (rounded to 60 min)
|
- TS-4 is a duplicate of TS-3: {"targetIssueId": "TS-4", "linkType": "duplicates", "issueToLinkId": "TS-3"};
|
||||||
- 60 minutes → $100
|
- TS-1 is blocked by TS-2: {"targetIssueId": "TS-1", "linkType": "blocked by", "issueToLinkId": "TS-2"};
|
||||||
- 67 minutes → $150 (rounded to 90 min)
|
Returns updated link counts for all target issue link types.
|
||||||
|
|
||||||
## Environment Variables
|
- `get_current_user`: Returns details about the currently authenticated user (me): login, email, full name, time zone.
|
||||||
|
- `get_issue`: Returns detailed information for an issue or issue draft, including the summary, description, URL, project, reporter (login), tags, votes, and custom fields. The `customFields` output property provides more important issue details, including Type, State, Assignee, Priority, Subsystem, and so on. Use get_issue_fields_schema for the full list of custom fields and their possible values.
|
||||||
|
- `get_issue_comments`: Returns a list of issue comments with detailed information for each. The response is paginated using the specified offset and/or limit
|
||||||
|
- `get_issue_fields_schema`: Returns the JSON schema for custom fields in the specified project. Must be used to provide relevant custom fields and values for create_issue and update_issue actions.
|
||||||
|
- `find_projects`: Finds projects whose names contain the specified substring (case-insensitive). Returns minimal information (ID and name) to help pick a project for get_project. The response is paginated using the specified offset and/or limit.
|
||||||
|
- `find_user`: Finds users by login or email (provide either login or email). Returns profile data for the matching user. This includes the login, full name, email, and local time zone.
|
||||||
|
- `find_user_groups`: Finds user groups or project teams whose names contain the specified substring (case-insensitive). The response is paginated using the specified offset and/or limit.
|
||||||
|
- `add_issue_comment`: Adds a new comment to the specified issue. Supports Markdown.
|
||||||
|
- `change_issue_assignee`: Sets the value for the Assignee field in an issue to the specified user. If the `assigneeLogin` argument is `null`, the issue will be unassigned.
|
||||||
|
- `create_draft_issue`: Creates a new issue draft in the specified project. If project is not defined, ask for assistance. Draft issues are only visible to the current user and can be edited using update_issue. Returns the ID assigned to the issue draft and a URL that opens the draft in a web browser.
|
||||||
|
- `create_issue`: Creates a new issue in the specified project. Call the get_issue_fields_schema tool first to identify required `customFields` and permitted values (projects may require them at creation). If project is not defined, ask for assistance. Returns the created issue ID and URL. Use get_issue for full details.
|
||||||
|
|
||||||
- `YOUTRACK_TOKEN`: Your permanent API token (recommended over passing as argument)
|
## Usage Pattern
|
||||||
- Set with `export YOUTRACK_TOKEN=your-token`
|
|
||||||
|
|
||||||
## API Details
|
When the user's request matches this skill's capabilities:
|
||||||
|
|
||||||
See `REFERENCES.md` for:
|
**Step 1: Identify the right tool** from the list above
|
||||||
- Complete API endpoint documentation
|
|
||||||
- Query language examples
|
**Step 2: Generate a tool call** in this JSON format:
|
||||||
- Field IDs and structures
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "tool_name",
|
||||||
|
"arguments": {
|
||||||
|
"param1": "value1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Execute via bash:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd $SKILL_DIR
|
||||||
|
./executor.py --call 'YOUR_JSON_HERE'
|
||||||
|
```
|
||||||
|
|
||||||
|
IMPORTANT: Replace $SKILL_DIR with the actual discovered path of this skill directory.
|
||||||
|
|
||||||
|
## Getting Tool Details
|
||||||
|
|
||||||
|
If you need detailed information about a specific tool's parameters:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd $SKILL_DIR
|
||||||
|
./executor.py --describe tool_name
|
||||||
|
```
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
Scripts will raise errors for:
|
If the executor returns an error:
|
||||||
- Missing or invalid token
|
- Check the tool name is correct
|
||||||
- Network issues
|
- Verify required arguments are provided
|
||||||
- API errors (404, 403, etc.)
|
- Ensure the MCP server is accessible
|
||||||
|
|
||||||
Check stderr for error details.
|
---
|
||||||
|
|
||||||
|
*Auto-generated from MCP server configuration by mcp_to_skill.py*
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"owner": "digisal",
|
|
||||||
"slug": "youtrack-digisal",
|
|
||||||
"displayName": "YouTrack Project Management",
|
|
||||||
"latest": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"publishedAt": 1769572337862,
|
|
||||||
"commit": "https://github.com/clawdbot/skills/commit/62ea9dc7577ed1e4c2a99c7d470bc5afbc58d356"
|
|
||||||
},
|
|
||||||
"history": []
|
|
||||||
}
|
|
||||||
81
.factory/skills/youtrack/executor.py
Executable file
81
.factory/skills/youtrack/executor.py
Executable file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env -S uv run --script
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.10"
|
||||||
|
# dependencies = [
|
||||||
|
# "mcp>=1.0.0",
|
||||||
|
# "httpx",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
"""MCP Skill Executor - HTTP (Streamable HTTP) transport"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
import httpx
|
||||||
|
from mcp import ClientSession
|
||||||
|
from mcp.client.streamable_http import streamable_http_client
|
||||||
|
|
||||||
|
|
||||||
|
async def run(config, args):
|
||||||
|
url = config["url"]
|
||||||
|
headers = config.get("headers", {})
|
||||||
|
|
||||||
|
http_client = httpx.AsyncClient(headers=headers, timeout=httpx.Timeout(30, read=60))
|
||||||
|
|
||||||
|
async with http_client:
|
||||||
|
async with streamable_http_client(url=url, http_client=http_client) as (
|
||||||
|
read_stream,
|
||||||
|
write_stream,
|
||||||
|
_,
|
||||||
|
):
|
||||||
|
async with ClientSession(read_stream, write_stream) as session:
|
||||||
|
await session.initialize()
|
||||||
|
|
||||||
|
if args.list:
|
||||||
|
response = await session.list_tools()
|
||||||
|
tools = [{"name": t.name, "description": t.description} for t in response.tools]
|
||||||
|
print(json.dumps(tools, indent=2))
|
||||||
|
|
||||||
|
elif args.describe:
|
||||||
|
response = await session.list_tools()
|
||||||
|
for tool in response.tools:
|
||||||
|
if tool.name == args.describe:
|
||||||
|
print(json.dumps({"name": tool.name, "description": tool.description, "inputSchema": tool.inputSchema}, indent=2))
|
||||||
|
return
|
||||||
|
print(f"Tool not found: {args.describe}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
elif args.call:
|
||||||
|
call_data = json.loads(args.call)
|
||||||
|
result = await session.call_tool(call_data["tool"], call_data.get("arguments", {}))
|
||||||
|
for item in result.content:
|
||||||
|
if hasattr(item, "text"):
|
||||||
|
print(item.text)
|
||||||
|
else:
|
||||||
|
print(json.dumps(item.model_dump(), indent=2))
|
||||||
|
else:
|
||||||
|
parser.print_help()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="MCP Skill Executor (HTTP)")
|
||||||
|
parser.add_argument("--call", help="JSON tool call to execute")
|
||||||
|
parser.add_argument("--describe", help="Get tool schema")
|
||||||
|
parser.add_argument("--list", action="store_true", help="List all tools")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
config_path = Path(__file__).parent / "mcp-config.json"
|
||||||
|
if not config_path.exists():
|
||||||
|
print(f"Error: {config_path} not found", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with open(config_path) as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
asyncio.run(run(config, args))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
7
.factory/skills/youtrack/mcp-config.json
Normal file
7
.factory/skills/youtrack/mcp-config.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"url": "https://<YourYouTrackInstance>.youtrack.cloud/mcp",
|
||||||
|
"transport": "http",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer <YourYouTrackToken>"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
YouTrack Invoice Generator
|
|
||||||
Generates invoices from time tracking data in YouTrack projects.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import argparse
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import List, Dict, Any, Optional
|
|
||||||
from youtrack_api import YouTrackAPI
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceGenerator:
|
|
||||||
"""Generate invoices from YouTrack time tracking data."""
|
|
||||||
|
|
||||||
def __init__(self, api: YouTrackAPI, hourly_rate: float = 100.0):
|
|
||||||
"""
|
|
||||||
Initialize invoice generator.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
api: YouTrackAPI instance
|
|
||||||
hourly_rate: Rate per hour (default $100)
|
|
||||||
"""
|
|
||||||
self.api = api
|
|
||||||
self.hourly_rate = hourly_rate
|
|
||||||
self.rate_per_half_hour = hourly_rate / 2
|
|
||||||
|
|
||||||
def get_project_time_data(self, project_id: str, from_date: Optional[str] = None) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Get time tracking data for a project.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
project_id: Project ID
|
|
||||||
from_date: Optional start date (not currently supported in REST API)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary with time data per issue
|
|
||||||
"""
|
|
||||||
# Build query for project issues
|
|
||||||
# Note: Date filtering with 'updated:' syntax not supported in REST API
|
|
||||||
# Use 'updated >= YYYY-MM-DD' format instead
|
|
||||||
query = f'project: {project_id}'
|
|
||||||
if from_date:
|
|
||||||
query += f' updated >= {from_date}'
|
|
||||||
|
|
||||||
issues = self.api.get_issues(query=query)
|
|
||||||
|
|
||||||
project_data = {
|
|
||||||
'project': None,
|
|
||||||
'issues': [],
|
|
||||||
'total_minutes': 0,
|
|
||||||
'total_hours': 0,
|
|
||||||
'total_cost': 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get project info
|
|
||||||
try:
|
|
||||||
project_data['project'] = self.api.get_project(project_id)
|
|
||||||
except:
|
|
||||||
# Fallback: use project name from first issue
|
|
||||||
if issues:
|
|
||||||
project_data['project'] = {'name': issues[0].get('project', {}).get('name', project_id)}
|
|
||||||
|
|
||||||
for issue in issues:
|
|
||||||
issue_id = issue.get('id')
|
|
||||||
issue_with_time = self.api.get_issue_with_work_items(issue_id)
|
|
||||||
|
|
||||||
work_items = issue_with_time.get('workItems', [])
|
|
||||||
total_minutes = sum(
|
|
||||||
wi.get('duration', {}).get('minutes', 0)
|
|
||||||
for wi in work_items
|
|
||||||
)
|
|
||||||
|
|
||||||
if total_minutes > 0:
|
|
||||||
hours = total_minutes / 60
|
|
||||||
cost = self._calculate_cost(total_minutes)
|
|
||||||
|
|
||||||
project_data['issues'].append({
|
|
||||||
'id': issue_id,
|
|
||||||
'summary': issue.get('summary', 'No summary'),
|
|
||||||
'description': issue.get('description', ''),
|
|
||||||
'work_items': work_items,
|
|
||||||
'total_minutes': total_minutes,
|
|
||||||
'total_hours': hours,
|
|
||||||
'cost': cost
|
|
||||||
})
|
|
||||||
|
|
||||||
project_data['total_minutes'] += total_minutes
|
|
||||||
project_data['total_cost'] += cost
|
|
||||||
|
|
||||||
project_data['total_hours'] = project_data['total_minutes'] / 60
|
|
||||||
|
|
||||||
return project_data
|
|
||||||
|
|
||||||
def _calculate_cost(self, minutes: int) -> float:
|
|
||||||
"""
|
|
||||||
Calculate cost based on time, billed in 30-minute increments.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
minutes: Total minutes
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Total cost
|
|
||||||
"""
|
|
||||||
# Convert to 30-minute increments, round up
|
|
||||||
half_hour_units = (minutes + 29) // 30
|
|
||||||
# Minimum 30 minutes (1 half-hour unit)
|
|
||||||
half_hour_units = max(half_hour_units, 1)
|
|
||||||
return half_hour_units * self.rate_per_half_hour
|
|
||||||
|
|
||||||
def generate_invoice_text(self, project_data: Dict[str, Any], month: Optional[str] = None) -> str:
|
|
||||||
"""
|
|
||||||
Generate invoice as plain text (can be printed to PDF).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
project_data: Project data from get_project_time_data()
|
|
||||||
month: Optional month label (e.g., "January 2026")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Invoice text
|
|
||||||
"""
|
|
||||||
project = project_data['project'] or {}
|
|
||||||
project_name = project.get('name', 'Unknown Project')
|
|
||||||
|
|
||||||
lines = []
|
|
||||||
lines.append("=" * 70)
|
|
||||||
lines.append(f"INVOICE - {project_name}")
|
|
||||||
|
|
||||||
if month:
|
|
||||||
lines.append(f"Period: {month}")
|
|
||||||
|
|
||||||
lines.append("")
|
|
||||||
lines.append("WORK ITEMS")
|
|
||||||
lines.append("-" * 70)
|
|
||||||
|
|
||||||
for issue in project_data['issues']:
|
|
||||||
lines.append("")
|
|
||||||
lines.append(f"Task: {issue['summary']}")
|
|
||||||
lines.append(f"ID: {issue['id']}")
|
|
||||||
|
|
||||||
if issue['description']:
|
|
||||||
desc = issue['description'][:200] + "..." if len(issue['description']) > 200 else issue['description']
|
|
||||||
lines.append(f"Description: {desc}")
|
|
||||||
|
|
||||||
for wi in issue['work_items']:
|
|
||||||
duration = wi.get('duration', {})
|
|
||||||
mins = duration.get('minutes', 0)
|
|
||||||
date = wi.get('date', '')
|
|
||||||
author = wi.get('author', {}).get('name', 'Unknown')
|
|
||||||
lines.append(f" - {date}: {mins} min ({author})")
|
|
||||||
|
|
||||||
lines.append(f" Task total: {issue['total_hours']:.2f} hours (${issue['cost']:.2f})")
|
|
||||||
|
|
||||||
lines.append("")
|
|
||||||
lines.append("-" * 70)
|
|
||||||
lines.append(f"TOTAL: {project_data['total_hours']:.2f} hours")
|
|
||||||
lines.append(f"TOTAL COST: ${project_data['total_cost']:.2f}")
|
|
||||||
lines.append("=" * 70)
|
|
||||||
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
def generate_invoice_json(self, project_data: Dict[str, Any], month: Optional[str] = None) -> str:
|
|
||||||
"""
|
|
||||||
Generate invoice as JSON for programmatic use.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
project_data: Project data from get_project_time_data()
|
|
||||||
month: Optional month label
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON string
|
|
||||||
"""
|
|
||||||
invoice = {
|
|
||||||
'project': project_data['project'],
|
|
||||||
'period': month,
|
|
||||||
'items': project_data['issues'],
|
|
||||||
'summary': {
|
|
||||||
'total_minutes': project_data['total_minutes'],
|
|
||||||
'total_hours': project_data['total_hours'],
|
|
||||||
'total_cost': project_data['total_cost']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return json.dumps(invoice, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""CLI interface for generating invoices."""
|
|
||||||
parser = argparse.ArgumentParser(description='YouTrack Invoice Generator')
|
|
||||||
parser.add_argument('--url', required=True, help='YouTrack instance URL')
|
|
||||||
parser.add_argument('--token', help='API token (or set YOUTRACK_TOKEN env var)')
|
|
||||||
parser.add_argument('--project', required=True, help='Project ID to generate invoice for')
|
|
||||||
parser.add_argument('--from-date', help='Start date (YYYY-MM-DD)')
|
|
||||||
parser.add_argument('--month', help='Month label (e.g., "January 2026")')
|
|
||||||
parser.add_argument('--rate', type=float, default=100.0, help='Hourly rate (default: 100)')
|
|
||||||
parser.add_argument('--format', choices=['text', 'json'], default='text', help='Output format')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
try:
|
|
||||||
api = YouTrackAPI(args.url, args.token)
|
|
||||||
generator = InvoiceGenerator(api, hourly_rate=args.rate)
|
|
||||||
|
|
||||||
project_data = generator.get_project_time_data(args.project, args.from_date)
|
|
||||||
|
|
||||||
if args.format == 'text':
|
|
||||||
output = generator.generate_invoice_text(project_data, args.month)
|
|
||||||
else:
|
|
||||||
output = generator.generate_invoice_json(project_data, args.month)
|
|
||||||
|
|
||||||
print(output)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
YouTrack REST API Client
|
|
||||||
Handles authentication and basic API calls for YouTrack Cloud instances.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import argparse
|
|
||||||
from urllib.parse import urljoin
|
|
||||||
from typing import Optional, Dict, List, Any
|
|
||||||
import urllib.request
|
|
||||||
import urllib.error
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
class YouTrackAPI:
|
|
||||||
"""Simple YouTrack REST API client."""
|
|
||||||
|
|
||||||
def __init__(self, base_url: str, token: Optional[str] = None):
|
|
||||||
"""
|
|
||||||
Initialize YouTrack API client.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
base_url: Your YouTrack instance URL (e.g., https://sl.youtrack.cloud)
|
|
||||||
token: Permanent API token (or set YOUTRACK_TOKEN env var)
|
|
||||||
"""
|
|
||||||
# Normalize base URL
|
|
||||||
self.base_url = base_url.rstrip('/')
|
|
||||||
self.token = token or os.environ.get('YOUTRACK_TOKEN')
|
|
||||||
|
|
||||||
if not self.token:
|
|
||||||
raise ValueError(
|
|
||||||
"YouTrack token required. Set YOUTRACK_TOKEN env var or pass as argument."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set up headers with bearer token auth
|
|
||||||
self.headers = {
|
|
||||||
'Authorization': f'Bearer {self.token}',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
def _make_request(self, method: str, endpoint: str, data: Optional[Dict] = None) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Make an authenticated API request.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
method: HTTP method (GET, POST, PUT, DELETE)
|
|
||||||
endpoint: API endpoint (e.g., '/api/issues')
|
|
||||||
data: Request body for POST/PUT
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Parsed JSON response
|
|
||||||
"""
|
|
||||||
url = urljoin(self.base_url, endpoint)
|
|
||||||
|
|
||||||
req_data = None
|
|
||||||
if data is not None:
|
|
||||||
req_data = json.dumps(data).encode('utf-8')
|
|
||||||
|
|
||||||
req = urllib.request.Request(
|
|
||||||
url,
|
|
||||||
data=req_data,
|
|
||||||
headers=self.headers,
|
|
||||||
method=method
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(req) as response:
|
|
||||||
if response.status >= 400:
|
|
||||||
error_body = response.read().decode('utf-8')
|
|
||||||
raise RuntimeError(f"API Error {response.status}: {error_body}")
|
|
||||||
|
|
||||||
result = response.read().decode('utf-8')
|
|
||||||
if result:
|
|
||||||
return json.loads(result)
|
|
||||||
return {}
|
|
||||||
except urllib.error.HTTPError as e:
|
|
||||||
error_body = e.read().decode('utf-8') if e.fp else ''
|
|
||||||
raise RuntimeError(f"HTTP Error {e.code}: {error_body}")
|
|
||||||
except Exception as e:
|
|
||||||
raise RuntimeError(f"Request failed: {e}")
|
|
||||||
|
|
||||||
# Projects
|
|
||||||
|
|
||||||
def get_projects(self) -> List[Dict]:
|
|
||||||
"""Get all projects."""
|
|
||||||
result = self._make_request('GET', '/api/admin/projects?fields=id,name,shortName')
|
|
||||||
return result if isinstance(result, list) else []
|
|
||||||
|
|
||||||
def get_project(self, project_id: str) -> Dict:
|
|
||||||
"""Get a specific project by ID."""
|
|
||||||
return self._make_request('GET', f'/api/admin/projects/{project_id}?fields=id,name,shortName,description')
|
|
||||||
|
|
||||||
# Issues
|
|
||||||
|
|
||||||
def get_issues(self, query: Optional[str] = None, fields: str = 'id,summary,description,created,updated,project(id,name),customFields(name,value)') -> List[Dict]:
|
|
||||||
"""
|
|
||||||
Get issues, optionally filtered by a query.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
query: YouTrack query language (e.g., 'project: MyProject')
|
|
||||||
fields: Comma-separated list of fields to return
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of issues
|
|
||||||
"""
|
|
||||||
params = {'fields': fields}
|
|
||||||
if query:
|
|
||||||
params['query'] = query
|
|
||||||
|
|
||||||
# Build query string
|
|
||||||
query_string = '&'.join(f'{k}={urllib.parse.quote(str(v))}' for k, v in params.items())
|
|
||||||
endpoint = f'/api/issues?{query_string}'
|
|
||||||
|
|
||||||
result = self._make_request('GET', endpoint)
|
|
||||||
return result if isinstance(result, list) else []
|
|
||||||
|
|
||||||
def get_issue(self, issue_id: str) -> Dict:
|
|
||||||
"""Get a specific issue by ID."""
|
|
||||||
return self._make_request('GET', f'/api/issues/{issue_id}')
|
|
||||||
|
|
||||||
def create_issue(self, project_id: str, summary: str, description: str = '') -> Dict:
|
|
||||||
"""
|
|
||||||
Create a new issue.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
project_id: Project ID or short name
|
|
||||||
summary: Issue summary
|
|
||||||
description: Issue description
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Created issue
|
|
||||||
"""
|
|
||||||
data = {
|
|
||||||
'project': {'id': project_id},
|
|
||||||
'summary': summary,
|
|
||||||
'description': description
|
|
||||||
}
|
|
||||||
return self._make_request('POST', '/api/issues', data)
|
|
||||||
|
|
||||||
def update_issue(self, issue_id: str, summary: Optional[str] = None, description: Optional[str] = None) -> Dict:
|
|
||||||
"""Update an issue's summary and/or description."""
|
|
||||||
data = {}
|
|
||||||
if summary is not None:
|
|
||||||
data['summary'] = summary
|
|
||||||
if description is not None:
|
|
||||||
data['description'] = description
|
|
||||||
return self._make_request('POST', f'/api/issues/{issue_id}', data)
|
|
||||||
|
|
||||||
# Time Tracking
|
|
||||||
|
|
||||||
def get_work_items(self, issue_id: str) -> List[Dict]:
|
|
||||||
"""Get all work items (time entries) for an issue."""
|
|
||||||
result = self._make_request('GET', f'/api/issues/{issue_id}/timeTracking/workItems?fields=id,date,duration(minutes),author(name),text')
|
|
||||||
# Convert date from milliseconds to ISO format
|
|
||||||
for wi in result:
|
|
||||||
if 'date' in wi and wi['date']:
|
|
||||||
wi['date'] = datetime.fromtimestamp(wi['date'] / 1000).isoformat()
|
|
||||||
return result if isinstance(result, list) else []
|
|
||||||
|
|
||||||
def get_issue_with_work_items(self, issue_id: str) -> Dict:
|
|
||||||
"""Get an issue with all its work items included."""
|
|
||||||
issue = self.get_issue(issue_id)
|
|
||||||
work_items = self.get_work_items(issue_id)
|
|
||||||
issue['workItems'] = work_items
|
|
||||||
return issue
|
|
||||||
|
|
||||||
# Knowledge Base (Articles)
|
|
||||||
|
|
||||||
def get_articles(self, project_id: Optional[str] = None) -> List[Dict]:
|
|
||||||
"""
|
|
||||||
Get knowledge base articles.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
project_id: Optional project ID to filter by
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of articles
|
|
||||||
"""
|
|
||||||
endpoint = '/api/articles'
|
|
||||||
if project_id:
|
|
||||||
endpoint += f'?project={project_id}'
|
|
||||||
|
|
||||||
result = self._make_request('GET', endpoint)
|
|
||||||
return result if isinstance(result, list) else []
|
|
||||||
|
|
||||||
def get_article(self, article_id: str) -> Dict:
|
|
||||||
"""Get a specific article by ID."""
|
|
||||||
return self._make_request('GET', f'/api/articles/{article_id}')
|
|
||||||
|
|
||||||
def create_article(self, project_id: str, title: str, content: str) -> Dict:
|
|
||||||
"""
|
|
||||||
Create a new knowledge base article.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
project_id: Project ID
|
|
||||||
title: Article title
|
|
||||||
content: Article content
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Created article
|
|
||||||
"""
|
|
||||||
data = {
|
|
||||||
'project': {'id': project_id},
|
|
||||||
'title': title,
|
|
||||||
'content': content
|
|
||||||
}
|
|
||||||
return self._make_request('POST', '/api/articles', data)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""CLI interface for testing the YouTrack API."""
|
|
||||||
parser = argparse.ArgumentParser(description='YouTrack API Client')
|
|
||||||
parser.add_argument('--url', required=True, help='YouTrack instance URL')
|
|
||||||
parser.add_argument('--token', help='API token (or set YOUTRACK_TOKEN env var)')
|
|
||||||
parser.add_argument('--list-projects', action='store_true', help='List all projects')
|
|
||||||
parser.add_argument('--list-issues', help='List issues (optional query)')
|
|
||||||
parser.add_argument('--get-issue', help='Get specific issue ID')
|
|
||||||
parser.add_argument('--get-articles', action='store_true', help='List articles')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
try:
|
|
||||||
api = YouTrackAPI(args.url, args.token)
|
|
||||||
|
|
||||||
if args.list_projects:
|
|
||||||
projects = api.get_projects()
|
|
||||||
print(json.dumps(projects, indent=2))
|
|
||||||
elif args.list_issues is not None:
|
|
||||||
issues = api.get_issues(query=args.list_issues)
|
|
||||||
print(json.dumps(issues, indent=2))
|
|
||||||
elif args.get_issue:
|
|
||||||
issue = api.get_issue_with_work_items(args.get_issue)
|
|
||||||
print(json.dumps(issue, indent=2))
|
|
||||||
elif args.get_articles:
|
|
||||||
articles = api.get_articles()
|
|
||||||
print(json.dumps(articles, indent=2))
|
|
||||||
else:
|
|
||||||
print("No action specified. Use --help for options.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
Reference in New Issue
Block a user