CopilotTaskMaster 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- copilottaskmaster-0.1.1.dist-info/METADATA +136 -0
- copilottaskmaster-0.1.1.dist-info/RECORD +12 -0
- copilottaskmaster-0.1.1.dist-info/WHEEL +5 -0
- copilottaskmaster-0.1.1.dist-info/entry_points.txt +3 -0
- copilottaskmaster-0.1.1.dist-info/top_level.txt +1 -0
- taskmaster/__init__.py +39 -0
- taskmaster/_version.py +34 -0
- taskmaster/cli.py +333 -0
- taskmaster/mcp_server.py +496 -0
- taskmaster/search.py +241 -0
- taskmaster/task_manager.py +388 -0
- taskmaster/utils.py +12 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: CopilotTaskMaster
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python tools for managing markdown task cards with MCP integration
|
|
5
|
+
Author: CopilotTaskMaster Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: click>=8.0.0
|
|
17
|
+
Requires-Dist: pyyaml>=6.0
|
|
18
|
+
Requires-Dist: python-frontmatter>=1.0.0
|
|
19
|
+
Requires-Dist: mcp>=0.1.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
23
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
25
|
+
|
|
26
|
+
# CopilotTaskMaster
|
|
27
|
+
|
|
28
|
+
Markdown-based task management for humans and AI agents. Manage tasks via CLI or MCP (Model Context Protocol).
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Project-Scoped:** All tasks are organized by project folders.
|
|
33
|
+
- **Agent Optimized:** Token-efficient responses designed for LLM integration.
|
|
34
|
+
- **Markdown First:** Tasks are stored as plain `.md` files with YAML frontmatter.
|
|
35
|
+
- **Dual Interface:** Full parity between CLI and MCP server tools.
|
|
36
|
+
- **Hierarchical:** Supports nested subpaths and tag-based filtering.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
### From Source
|
|
41
|
+
```bash
|
|
42
|
+
git clone https://github.com/geekbozu/CopilotTaskMaster.git
|
|
43
|
+
cd CopilotTaskMaster
|
|
44
|
+
pip install -e .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Via Docker
|
|
48
|
+
Pull the pre-built image from GHCR:
|
|
49
|
+
```bash
|
|
50
|
+
docker pull ghcr.io/geekbozu/copilottaskmaster:latest
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or build locally:
|
|
54
|
+
```bash
|
|
55
|
+
docker build -t taskmaster .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
### CLI Basics
|
|
61
|
+
All operations require a **project** scope, either via `--project` or as a path prefix.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Create
|
|
65
|
+
taskmaster create backend/login.md "Implement Auth" --status open --priority high
|
|
66
|
+
|
|
67
|
+
# List & Search
|
|
68
|
+
taskmaster list --project backend
|
|
69
|
+
taskmaster search --query "Auth" --status open
|
|
70
|
+
|
|
71
|
+
taskmaster tags --project backend # list tags for a project (omit --project to list all tags)
|
|
72
|
+
|
|
73
|
+
# Update & Move
|
|
74
|
+
taskmaster update backend/login.md --status in-progress
|
|
75
|
+
taskmaster move backend/login.md backend/completed/login.md
|
|
76
|
+
|
|
77
|
+
# Show Structure
|
|
78
|
+
# Show the whole workspace tree
|
|
79
|
+
taskmaster tree
|
|
80
|
+
# Show tree scoped to a project
|
|
81
|
+
taskmaster tree --project backend
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### MCP Server (VS Code)
|
|
85
|
+
Enable task management tools in VS Code by adding the server to your settings:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"servers ": {
|
|
90
|
+
"taskmaster": {
|
|
91
|
+
"command": "docker",
|
|
92
|
+
"args": [
|
|
93
|
+
"run", "-i", "--rm",
|
|
94
|
+
"-v", "${workspaceFolder}/tasks:/tasks",
|
|
95
|
+
"ghcr.io/geekbozu/copilottaskmaster:latest",
|
|
96
|
+
"mcp-server"
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Available Tools:** `create_task`, `read_task`, `update_task`, `delete_task`, `list_tasks`, `search_tasks`, `move_task`, `get_structure`, `get_all_tags`.
|
|
104
|
+
|
|
105
|
+
Tool details (selected):
|
|
106
|
+
|
|
107
|
+
- `get_structure(subpath: str = "", project: Optional[str] = None)`: Return hierarchical folder/task structure. When `project` is provided, the structure is scoped to that project (raises ValueError if the project does not exist).
|
|
108
|
+
- `get_all_tags(project: Optional[str] = None)`: Return the set of tags used across tasks. When `project` is provided, tags are collected only from that project's tasks (returns empty set if the project does not exist).
|
|
109
|
+
|
|
110
|
+
## Task Format
|
|
111
|
+
Tasks are standard Markdown files:
|
|
112
|
+
|
|
113
|
+
```markdown
|
|
114
|
+
---
|
|
115
|
+
title: Implement Auth
|
|
116
|
+
status: open
|
|
117
|
+
priority: high
|
|
118
|
+
tags: [auth, security]
|
|
119
|
+
---
|
|
120
|
+
# Implementation Details
|
|
121
|
+
- [ ] Setup JWT
|
|
122
|
+
- [ ] Hash Passwords
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Configuration
|
|
126
|
+
- `TASKMASTER_TASKS_DIR`: Path to storage (default: `./tasks`).
|
|
127
|
+
- Use `--tasks-dir` on any CLI command to override.
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
```bash
|
|
131
|
+
pytest # Run tests
|
|
132
|
+
black taskmaster/ # Format code
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
taskmaster/__init__.py,sha256=RJot_-TEwO9pLbE3ChcIj5oSKhsgoTjuAHVWJoIYBK8,1302
|
|
2
|
+
taskmaster/_version.py,sha256=m8HxkqoKGw_wAJtc4ZokpJKNLXqp4zwnNhbnfDtro7w,704
|
|
3
|
+
taskmaster/cli.py,sha256=iHDh9RiK9o8S3Y9aBBjz22OL5uugn1Glp8ZpBNs7aFM,10556
|
|
4
|
+
taskmaster/mcp_server.py,sha256=VOYowbFhH0SqUVnbt-Vx4PKh1FLBcvBCkx3J7b0KMvo,18079
|
|
5
|
+
taskmaster/search.py,sha256=x7fgJrFxXnpfOLK7hYDgkanbQbcIFc4Lkf-lMse6UR8,8442
|
|
6
|
+
taskmaster/task_manager.py,sha256=isq-tY4YTAU94BBqkcMK1FwwrsFrTLzr37TjL6rUU3Q,13693
|
|
7
|
+
taskmaster/utils.py,sha256=60z4FyHWI91NEcVS0djk0fiBg5ncdIKA-FFGK60n2sI,442
|
|
8
|
+
copilottaskmaster-0.1.1.dist-info/METADATA,sha256=3GOAPCXvaE5c7hc9UEPM6Px6u9X_Z8pjUqMQZ9X7JQY,3899
|
|
9
|
+
copilottaskmaster-0.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
10
|
+
copilottaskmaster-0.1.1.dist-info/entry_points.txt,sha256=x2l9TVb92kKXMCB7RgkFt6CbsnB7_bhNcbbsrvMxT6E,95
|
|
11
|
+
copilottaskmaster-0.1.1.dist-info/top_level.txt,sha256=SPHiThSPLrfMwnXlniF0KxlcfMygYlWwvVFfEaDbZh0,11
|
|
12
|
+
copilottaskmaster-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
taskmaster
|
taskmaster/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CopilotTaskMaster - Markdown task card management with MCP integration
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from importlib.metadata import PackageNotFoundError, version as _version
|
|
7
|
+
|
|
8
|
+
# Resolve package version with a simple precedence:
|
|
9
|
+
# 1) TASKMASTER_VERSION env var (CI / Docker build-arg)
|
|
10
|
+
# 2) setuptools_scm-generated file `taskmaster/_version.py` (if present)
|
|
11
|
+
# 3) installed package metadata (importlib.metadata)
|
|
12
|
+
# 4) fallback to "0.0.0"
|
|
13
|
+
|
|
14
|
+
v = os.environ.get("TASKMASTER_VERSION")
|
|
15
|
+
if v:
|
|
16
|
+
__version__ = v
|
|
17
|
+
else:
|
|
18
|
+
# Prefer the file written by setuptools_scm at build time
|
|
19
|
+
ver_file = os.path.join(os.path.dirname(__file__), "_version.py")
|
|
20
|
+
if os.path.exists(ver_file):
|
|
21
|
+
try:
|
|
22
|
+
data = {}
|
|
23
|
+
with open(ver_file, "r", encoding="utf-8") as f:
|
|
24
|
+
exec(f.read(), data)
|
|
25
|
+
__version__ = data.get("version") or data.get("__version__")
|
|
26
|
+
if not __version__:
|
|
27
|
+
raise ValueError("no version in _version.py")
|
|
28
|
+
except Exception:
|
|
29
|
+
__version__ = "0.0.0"
|
|
30
|
+
else:
|
|
31
|
+
try:
|
|
32
|
+
__version__ = _version("CopilotTaskMaster")
|
|
33
|
+
except PackageNotFoundError:
|
|
34
|
+
__version__ = "0.0.0"
|
|
35
|
+
|
|
36
|
+
from .task_manager import TaskManager
|
|
37
|
+
from .search import TaskSearcher
|
|
38
|
+
|
|
39
|
+
__all__ = ["TaskManager", "TaskSearcher", "__version__"]
|
taskmaster/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.1.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 1)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
taskmaster/cli.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI - Command-line interface for TaskMaster
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import click
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .task_manager import TaskManager
|
|
12
|
+
from .search import TaskSearcher
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group()
|
|
16
|
+
@click.version_option(__version__, prog_name="CopilotTaskMaster")
|
|
17
|
+
@click.option('--tasks-dir', default=None, help='Path to tasks directory')
|
|
18
|
+
@click.pass_context
|
|
19
|
+
def main(ctx, tasks_dir):
|
|
20
|
+
"""CopilotTaskMaster - Manage markdown task cards"""
|
|
21
|
+
ctx.ensure_object(dict)
|
|
22
|
+
|
|
23
|
+
if tasks_dir is None:
|
|
24
|
+
tasks_dir = os.environ.get('TASKMASTER_TASKS_DIR', './tasks')
|
|
25
|
+
|
|
26
|
+
ctx.obj['tasks_dir'] = tasks_dir
|
|
27
|
+
ctx.obj['manager'] = TaskManager(tasks_dir)
|
|
28
|
+
ctx.obj['searcher'] = TaskSearcher(tasks_dir)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@main.command()
|
|
32
|
+
@click.argument('path')
|
|
33
|
+
@click.argument('title')
|
|
34
|
+
@click.option('--content', default="", help='Task content')
|
|
35
|
+
@click.option('--status', default='open', help='Task status')
|
|
36
|
+
@click.option('--priority', default='medium', help='Task priority')
|
|
37
|
+
@click.option('--tags', multiple=True, help='Task tags')
|
|
38
|
+
@click.option('--project', default=None, help='Project name to scope operation')
|
|
39
|
+
@click.pass_context
|
|
40
|
+
def create(ctx, path, title, content, status, priority, tags, project):
|
|
41
|
+
"""Create a new task card"""
|
|
42
|
+
manager = ctx.obj['manager']
|
|
43
|
+
|
|
44
|
+
metadata = {
|
|
45
|
+
'status': status,
|
|
46
|
+
'priority': priority
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if tags:
|
|
50
|
+
metadata['tags'] = list(tags)
|
|
51
|
+
|
|
52
|
+
from .utils import project_resolution_error_msg
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
result = manager.create_task(path, title, content, metadata, project=project)
|
|
56
|
+
except ValueError as e:
|
|
57
|
+
click.echo(project_resolution_error_msg(e), err=True)
|
|
58
|
+
sys.exit(2)
|
|
59
|
+
|
|
60
|
+
click.echo(f"✓ Created task: {result['path']}")
|
|
61
|
+
click.echo(f" Title: {result['title']}")
|
|
62
|
+
click.echo(f" Created: {result['created']}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@main.command()
|
|
66
|
+
@click.argument('path')
|
|
67
|
+
@click.option('--full', is_flag=True, help='Show full content')
|
|
68
|
+
@click.option('--project', default=None, help='Project name to scope operation')
|
|
69
|
+
@click.pass_context
|
|
70
|
+
def show(ctx, path, full, project):
|
|
71
|
+
"""Show a task card"""
|
|
72
|
+
manager = ctx.obj['manager']
|
|
73
|
+
|
|
74
|
+
from .utils import project_resolution_error_msg
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
task = manager.read_task(path, project=project)
|
|
78
|
+
except ValueError as e:
|
|
79
|
+
click.echo(project_resolution_error_msg(e), err=True)
|
|
80
|
+
sys.exit(2)
|
|
81
|
+
|
|
82
|
+
if not task:
|
|
83
|
+
click.echo(f"✗ Task not found: {path}", err=True)
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
click.echo(f"Path: {task['path']}")
|
|
87
|
+
click.echo(f"Title: {task['title']}")
|
|
88
|
+
click.echo(f"\nMetadata:")
|
|
89
|
+
for key, value in task['metadata'].items():
|
|
90
|
+
click.echo(f" {key}: {value}")
|
|
91
|
+
|
|
92
|
+
if full:
|
|
93
|
+
click.echo(f"\nContent:\n{task['content']}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@main.command()
|
|
97
|
+
@click.argument('path')
|
|
98
|
+
@click.option('--title', default=None, help='New title')
|
|
99
|
+
@click.option('--content', default=None, help='New content')
|
|
100
|
+
@click.option('--status', default=None, help='New status')
|
|
101
|
+
@click.option('--priority', default=None, help='New priority')
|
|
102
|
+
@click.option('--add-tag', multiple=True, help='Add tags')
|
|
103
|
+
@click.option('--project', default=None, help='Project name to scope operation')
|
|
104
|
+
@click.pass_context
|
|
105
|
+
def update(ctx, path, title, content, status, priority, add_tag, project):
|
|
106
|
+
"""Update a task card"""
|
|
107
|
+
manager = ctx.obj['manager']
|
|
108
|
+
|
|
109
|
+
metadata = {}
|
|
110
|
+
if status:
|
|
111
|
+
metadata['status'] = status
|
|
112
|
+
if priority:
|
|
113
|
+
metadata['priority'] = priority
|
|
114
|
+
|
|
115
|
+
from .utils import project_resolution_error_msg
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
if add_tag:
|
|
119
|
+
# Read existing tags first
|
|
120
|
+
task = manager.read_task(path, project=project)
|
|
121
|
+
if task:
|
|
122
|
+
existing_tags = task['metadata'].get('tags', [])
|
|
123
|
+
if isinstance(existing_tags, str):
|
|
124
|
+
existing_tags = [existing_tags]
|
|
125
|
+
metadata['tags'] = list(set(existing_tags + list(add_tag)))
|
|
126
|
+
|
|
127
|
+
result = manager.update_task(path, title, content, metadata, project=project)
|
|
128
|
+
except ValueError as e:
|
|
129
|
+
click.echo(project_resolution_error_msg(e), err=True)
|
|
130
|
+
sys.exit(2)
|
|
131
|
+
|
|
132
|
+
if not result:
|
|
133
|
+
click.echo(f"✗ Task not found: {path}", err=True)
|
|
134
|
+
sys.exit(1)
|
|
135
|
+
|
|
136
|
+
click.echo(f"✓ Updated task: {result['path']}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@main.command()
|
|
140
|
+
@click.argument('path')
|
|
141
|
+
@click.option('--project', default=None, help='Project name to scope operation')
|
|
142
|
+
@click.pass_context
|
|
143
|
+
def delete(ctx, path, project):
|
|
144
|
+
"""Delete a task card"""
|
|
145
|
+
manager = ctx.obj['manager']
|
|
146
|
+
|
|
147
|
+
from .utils import project_resolution_error_msg
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
success = manager.delete_task(path, project=project)
|
|
151
|
+
except ValueError as e:
|
|
152
|
+
click.echo(project_resolution_error_msg(e), err=True)
|
|
153
|
+
sys.exit(2)
|
|
154
|
+
|
|
155
|
+
if success:
|
|
156
|
+
click.echo(f"✓ Deleted task: {path}")
|
|
157
|
+
else:
|
|
158
|
+
click.echo(f"✗ Task not found: {path}", err=True)
|
|
159
|
+
sys.exit(1)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@main.command()
|
|
163
|
+
@click.option('--subpath', default="", help='Subdirectory to list')
|
|
164
|
+
@click.option('--recursive/--no-recursive', default=True, help='Include subdirectories')
|
|
165
|
+
@click.option('--full', is_flag=True, help='Show full content')
|
|
166
|
+
@click.option('--project', default=None, help='Project name to scope operation')
|
|
167
|
+
@click.pass_context
|
|
168
|
+
def list(ctx, subpath, recursive, full, project):
|
|
169
|
+
"""List all tasks"""
|
|
170
|
+
manager = ctx.obj['manager']
|
|
171
|
+
|
|
172
|
+
from .utils import project_resolution_error_msg
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
tasks = manager.list_tasks(subpath=subpath, recursive=recursive, include_content=full, project=project)
|
|
176
|
+
except ValueError as e:
|
|
177
|
+
click.echo(project_resolution_error_msg(e), err=True)
|
|
178
|
+
sys.exit(2)
|
|
179
|
+
|
|
180
|
+
if not tasks:
|
|
181
|
+
click.echo("No tasks found.")
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
click.echo(f"Found {len(tasks)} task(s):\n")
|
|
185
|
+
for task in tasks:
|
|
186
|
+
click.echo(f"• {task['path']}")
|
|
187
|
+
click.echo(f" {task['title']}")
|
|
188
|
+
|
|
189
|
+
if 'status' in task['metadata']:
|
|
190
|
+
click.echo(f" Status: {task['metadata']['status']}")
|
|
191
|
+
|
|
192
|
+
if full and 'content' in task:
|
|
193
|
+
click.echo(f" Content: {task['content'][:100]}...")
|
|
194
|
+
|
|
195
|
+
click.echo()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@main.command()
|
|
199
|
+
@click.option('--subpath', default="", help='Subdirectory to show structure for')
|
|
200
|
+
@click.option('--project', default=None, help='Project name to scope operation')
|
|
201
|
+
@click.pass_context
|
|
202
|
+
def tree(ctx, subpath, project):
|
|
203
|
+
"""Show hierarchical structure of tasks"""
|
|
204
|
+
manager = ctx.obj['manager']
|
|
205
|
+
|
|
206
|
+
from .utils import project_resolution_error_msg
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
structure = manager.get_structure(subpath, project=project)
|
|
210
|
+
except ValueError as e:
|
|
211
|
+
click.echo(project_resolution_error_msg(e), err=True)
|
|
212
|
+
sys.exit(2)
|
|
213
|
+
|
|
214
|
+
def print_tree(node, prefix="", is_last=True):
|
|
215
|
+
connector = "└── " if is_last else "├── "
|
|
216
|
+
click.echo(f"{prefix}{connector}{node['name']}")
|
|
217
|
+
|
|
218
|
+
if node['type'] == 'directory' and 'children' in node:
|
|
219
|
+
extension = " " if is_last else "│ "
|
|
220
|
+
for i, child in enumerate(node['children']):
|
|
221
|
+
is_last_child = i == len(node['children']) - 1
|
|
222
|
+
print_tree(child, prefix + extension, is_last_child)
|
|
223
|
+
elif node['type'] == 'task':
|
|
224
|
+
if 'title' in node and node['title']:
|
|
225
|
+
extension = " " if is_last else "│ "
|
|
226
|
+
click.echo(f"{prefix}{extension} {node['title']}")
|
|
227
|
+
|
|
228
|
+
print_tree(structure, "", True)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@main.command()
|
|
232
|
+
@click.option('--query', default="", help='Text to search for')
|
|
233
|
+
@click.option('--status', default=None, help='Filter by status')
|
|
234
|
+
@click.option('--priority', default=None, help='Filter by priority')
|
|
235
|
+
@click.option('--tags', multiple=True, help='Filter by tags')
|
|
236
|
+
@click.option('--path-pattern', default="", help='Path pattern to search')
|
|
237
|
+
@click.option('--max-results', default=50, help='Maximum results')
|
|
238
|
+
@click.option('--full', is_flag=True, help='Show full content')
|
|
239
|
+
@click.pass_context
|
|
240
|
+
def search(ctx, query, status, priority, tags, path_pattern, max_results, full):
|
|
241
|
+
"""Search for tasks"""
|
|
242
|
+
searcher = ctx.obj['searcher']
|
|
243
|
+
|
|
244
|
+
metadata_filters = {}
|
|
245
|
+
if status:
|
|
246
|
+
metadata_filters['status'] = status
|
|
247
|
+
if priority:
|
|
248
|
+
metadata_filters['priority'] = priority
|
|
249
|
+
if tags:
|
|
250
|
+
metadata_filters['tags'] = list(tags)
|
|
251
|
+
|
|
252
|
+
results = searcher.search(
|
|
253
|
+
query=query,
|
|
254
|
+
metadata_filters=metadata_filters if metadata_filters else None,
|
|
255
|
+
path_pattern=path_pattern,
|
|
256
|
+
max_results=max_results,
|
|
257
|
+
include_content=full
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if not results:
|
|
261
|
+
click.echo("No tasks found.")
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
click.echo(f"Found {len(results)} task(s):\n")
|
|
265
|
+
for result in results:
|
|
266
|
+
click.echo(f"• {result['path']} (score: {result['score']})")
|
|
267
|
+
click.echo(f" {result['title']}")
|
|
268
|
+
|
|
269
|
+
if 'snippet' in result:
|
|
270
|
+
click.echo(f" {result['snippet']}")
|
|
271
|
+
|
|
272
|
+
if full and 'content' in result:
|
|
273
|
+
click.echo(f"\n{result['content']}\n")
|
|
274
|
+
|
|
275
|
+
click.echo()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@main.command()
|
|
279
|
+
@click.option('--project', default=None, help='Project name to scope operation')
|
|
280
|
+
@click.pass_context
|
|
281
|
+
def tags(ctx, project):
|
|
282
|
+
"""List all tags"""
|
|
283
|
+
searcher = ctx.obj['searcher']
|
|
284
|
+
|
|
285
|
+
all_tags = searcher.get_all_tags(project=project)
|
|
286
|
+
|
|
287
|
+
if not all_tags:
|
|
288
|
+
click.echo("No tags found.")
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
click.echo("Tags:")
|
|
292
|
+
for tag in sorted(all_tags):
|
|
293
|
+
click.echo(f" • {tag}")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@main.command()
|
|
297
|
+
@click.argument('old_path')
|
|
298
|
+
@click.argument('new_path')
|
|
299
|
+
@click.option('--project', default=None, help='Project name to scope operation')
|
|
300
|
+
@click.pass_context
|
|
301
|
+
def move(ctx, old_path, new_path, project):
|
|
302
|
+
"""Move/rename a task"""
|
|
303
|
+
manager = ctx.obj['manager']
|
|
304
|
+
|
|
305
|
+
from .utils import project_resolution_error_msg
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
success = manager.move_task(old_path, new_path, project=project)
|
|
309
|
+
except ValueError as e:
|
|
310
|
+
click.echo(project_resolution_error_msg(e), err=True)
|
|
311
|
+
sys.exit(2)
|
|
312
|
+
|
|
313
|
+
if success:
|
|
314
|
+
click.echo(f"✓ Moved task from {old_path} to {new_path}")
|
|
315
|
+
else:
|
|
316
|
+
click.echo(f"✗ Failed to move task", err=True)
|
|
317
|
+
sys.exit(1)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@main.command(name="mcp-server")
|
|
321
|
+
@click.pass_context
|
|
322
|
+
def mcp_server_cmd(ctx):
|
|
323
|
+
"""Start the MCP server for LLM integration"""
|
|
324
|
+
from .mcp_server import main as run_mcp
|
|
325
|
+
|
|
326
|
+
# Pass the tasks directory to the MCP server via environment variable
|
|
327
|
+
os.environ['TASKMASTER_TASKS_DIR'] = ctx.obj['tasks_dir']
|
|
328
|
+
|
|
329
|
+
run_mcp()
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
if __name__ == '__main__':
|
|
333
|
+
main()
|