chitty 0.1.0__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.
- chitty/__init__.py +0 -0
- chitty/check.py +67 -0
- chitty/cli.py +355 -0
- chitty/config.py +140 -0
- chitty/errors.py +2 -0
- chitty/git.py +29 -0
- chitty/guide.md +14 -0
- chitty/ids.py +73 -0
- chitty/mcp.py +178 -0
- chitty/model.py +576 -0
- chitty/store.py +449 -0
- chitty-0.1.0.dist-info/METADATA +79 -0
- chitty-0.1.0.dist-info/RECORD +16 -0
- chitty-0.1.0.dist-info/WHEEL +4 -0
- chitty-0.1.0.dist-info/entry_points.txt +2 -0
- chitty-0.1.0.dist-info/licenses/LICENSE +21 -0
chitty/mcp.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""stdio MCP server. Tools mirror the CLI one-to-one (same names, same argument
|
|
2
|
+
names) and return what the CLI prints with --json. Needs the `mcp` extra."""
|
|
3
|
+
|
|
4
|
+
import functools
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from datetime import date
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from mcp.server.mcpserver import MCPServer
|
|
11
|
+
from mcp.server.mcpserver.exceptions import ToolError
|
|
12
|
+
|
|
13
|
+
from chitty.check import check as check_repo
|
|
14
|
+
from chitty.config import find_root, search_root
|
|
15
|
+
from chitty.errors import ChittyError
|
|
16
|
+
from chitty.store import Edit, Repo, Which, guide, matches, resolve_add_target
|
|
17
|
+
|
|
18
|
+
Status = Literal["todo", "doing", "done"]
|
|
19
|
+
Priority = Literal["low", "medium", "high"]
|
|
20
|
+
|
|
21
|
+
server = MCPServer(name="chitty", instructions=guide())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _tool[F: Callable[..., Any]](name: str, description: str) -> Callable[[F], F]:
|
|
25
|
+
"""Register a tool whose ChittyErrors reach the client as tool errors."""
|
|
26
|
+
|
|
27
|
+
def decorator(func: F) -> F:
|
|
28
|
+
@functools.wraps(func)
|
|
29
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
30
|
+
try:
|
|
31
|
+
return func(*args, **kwargs)
|
|
32
|
+
except ChittyError as error:
|
|
33
|
+
raise ToolError(str(error)) from error
|
|
34
|
+
|
|
35
|
+
server.tool(name=name, description=description)(wrapper)
|
|
36
|
+
return func
|
|
37
|
+
|
|
38
|
+
return decorator
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _repo() -> Repo:
|
|
42
|
+
return Repo.open(find_root(Path.cwd()))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@server.resource("chitty://guide", name="guide", mime_type="text/markdown")
|
|
46
|
+
def guide_resource() -> str:
|
|
47
|
+
return guide()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@_tool("add", "Create a task with status todo. `context` may also name a user-level context.")
|
|
51
|
+
def add(
|
|
52
|
+
title: str,
|
|
53
|
+
description: str,
|
|
54
|
+
context: str | None = None,
|
|
55
|
+
ac: list[str] | None = None,
|
|
56
|
+
label: list[str] | None = None,
|
|
57
|
+
priority: Priority | None = None,
|
|
58
|
+
owner: str | None = None,
|
|
59
|
+
depends_on: list[str] | None = None,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
root = search_root(Path.cwd())
|
|
62
|
+
target = resolve_add_target(Repo.open(root) if root else None, context)
|
|
63
|
+
fields = {
|
|
64
|
+
"labels": list(dict.fromkeys(label or [])),
|
|
65
|
+
"priority": priority,
|
|
66
|
+
"owner": owner,
|
|
67
|
+
"depends_on": depends_on or [],
|
|
68
|
+
}
|
|
69
|
+
change = target.repo.add(target.context, title, description, ac or [], fields, date.today())
|
|
70
|
+
return change.task.as_dict()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@_tool("list", "List tasks. `status`: any of; `label`: all of; `search`: title and body.")
|
|
74
|
+
def list_(
|
|
75
|
+
context: str | None = None,
|
|
76
|
+
status: list[Status] | None = None,
|
|
77
|
+
label: list[str] | None = None,
|
|
78
|
+
owner: str | None = None,
|
|
79
|
+
search: str | None = None,
|
|
80
|
+
archived: bool = False,
|
|
81
|
+
all: bool = False,
|
|
82
|
+
) -> list[dict[str, Any]]:
|
|
83
|
+
if archived and all:
|
|
84
|
+
raise ChittyError("give archived or all, not both")
|
|
85
|
+
which: Which = "all" if all else "archived" if archived else "active"
|
|
86
|
+
repo = _repo()
|
|
87
|
+
target = repo.context(context) if context else repo.default_context
|
|
88
|
+
tasks = repo.tasks(target, which)
|
|
89
|
+
found = [t for t in tasks if matches(t, status or [], label or [], owner, search)]
|
|
90
|
+
return [t.as_dict() for t in found]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@_tool("show", "Show a task. Work on one task per session: start here.")
|
|
94
|
+
def show(id: str) -> dict[str, Any]:
|
|
95
|
+
repo = _repo()
|
|
96
|
+
return repo.find(repo.resolve(id)).as_dict()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@_tool(
|
|
100
|
+
"edit",
|
|
101
|
+
"Change a task. Write `plan` and set status doing before coding; tick criteria with "
|
|
102
|
+
"`check` (numbers from show) as you meet them; record surprises with `note`. "
|
|
103
|
+
"Criterion numbers refer to the list before this edit. '' unsets priority/owner/plan.",
|
|
104
|
+
)
|
|
105
|
+
def edit(
|
|
106
|
+
id: str,
|
|
107
|
+
title: str | None = None,
|
|
108
|
+
status: Status | None = None,
|
|
109
|
+
add_label: list[str] | None = None,
|
|
110
|
+
rm_label: list[str] | None = None,
|
|
111
|
+
priority: Priority | Literal[""] | None = None,
|
|
112
|
+
owner: str | None = None,
|
|
113
|
+
depends_on: list[str] | None = None,
|
|
114
|
+
rm_depends_on: list[str] | None = None,
|
|
115
|
+
description: str | None = None,
|
|
116
|
+
ac: list[str] | None = None,
|
|
117
|
+
rm_ac: list[int] | None = None,
|
|
118
|
+
check: list[int] | None = None,
|
|
119
|
+
uncheck: list[int] | None = None,
|
|
120
|
+
plan: str | None = None,
|
|
121
|
+
note: str | None = None,
|
|
122
|
+
) -> dict[str, Any]:
|
|
123
|
+
repo = _repo()
|
|
124
|
+
changes = Edit(
|
|
125
|
+
title=title,
|
|
126
|
+
status=status,
|
|
127
|
+
add_labels=add_label or (),
|
|
128
|
+
rm_labels=rm_label or (),
|
|
129
|
+
priority=priority,
|
|
130
|
+
owner=owner,
|
|
131
|
+
depends_on=depends_on or (),
|
|
132
|
+
rm_depends_on=rm_depends_on or (),
|
|
133
|
+
description=description,
|
|
134
|
+
add_criteria=ac or (),
|
|
135
|
+
rm_criteria=rm_ac or (),
|
|
136
|
+
check=check or (),
|
|
137
|
+
uncheck=uncheck or (),
|
|
138
|
+
plan=plan,
|
|
139
|
+
note=note,
|
|
140
|
+
)
|
|
141
|
+
return repo.edit(repo.resolve(id), changes).task.as_dict()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@_tool("move", "Move a task to another context in this repo; it gets a new ID there.")
|
|
145
|
+
def move(id: str, context: str) -> dict[str, Any]:
|
|
146
|
+
repo = _repo()
|
|
147
|
+
return repo.move(repo.resolve(id), repo.context(context)).task.as_dict()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@_tool("archive", "Move a task into its context's archive/.")
|
|
151
|
+
def archive(id: str) -> dict[str, Any]:
|
|
152
|
+
repo = _repo()
|
|
153
|
+
return repo.archive(repo.resolve(id)).task.as_dict()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@_tool("restore", "Move an archived task back out of archive/.")
|
|
157
|
+
def restore(id: str) -> dict[str, Any]:
|
|
158
|
+
repo = _repo()
|
|
159
|
+
return repo.restore(repo.resolve(id)).task.as_dict()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@_tool("check", "Validate task files: every context, one `context`, or only `paths`.")
|
|
163
|
+
def check(context: str | None = None, paths: list[str] | None = None) -> list[dict[str, str]]:
|
|
164
|
+
repo = _repo()
|
|
165
|
+
contexts = [repo.context(context)] if context else repo.contexts()
|
|
166
|
+
problems = check_repo(repo, contexts, [Path(p) for p in paths or []])
|
|
167
|
+
return [{"path": str(p.path), "message": p.message} for p in problems]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@_tool("path", "The task's file path.")
|
|
171
|
+
def path(id: str) -> dict[str, Any]:
|
|
172
|
+
repo = _repo()
|
|
173
|
+
stored = repo.find(repo.resolve(id))
|
|
174
|
+
return {"id": str(stored.id), "path": str(stored.path)}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def run() -> None:
|
|
178
|
+
server.run("stdio")
|