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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ taskmaster = taskmaster.cli:main
3
+ taskmaster-mcp = taskmaster.mcp_server:main
@@ -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()