devcoach 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.
- devcoach/SKILL.md +288 -0
- devcoach/__init__.py +3 -0
- devcoach/cli/__init__.py +0 -0
- devcoach/cli/commands.py +793 -0
- devcoach/core/__init__.py +0 -0
- devcoach/core/coach.py +141 -0
- devcoach/core/db.py +768 -0
- devcoach/core/detect.py +132 -0
- devcoach/core/git.py +97 -0
- devcoach/core/models.py +104 -0
- devcoach/core/prompts.py +52 -0
- devcoach/mcp/__init__.py +0 -0
- devcoach/mcp/server.py +545 -0
- devcoach/web/__init__.py +0 -0
- devcoach/web/app.py +319 -0
- devcoach/web/static/favicon.svg +3 -0
- devcoach/web/static/relative-time.js +24 -0
- devcoach/web/static/style.css +163 -0
- devcoach/web/static/vendor/alpinejs.min.js +5 -0
- devcoach/web/static/vendor/flatpickr-dark.min.css +795 -0
- devcoach/web/static/vendor/flatpickr.min.css +13 -0
- devcoach/web/static/vendor/flatpickr.min.js +2 -0
- devcoach/web/static/vendor/highlight.min.js +1232 -0
- devcoach/web/static/vendor/hljs-dark.min.css +1 -0
- devcoach/web/static/vendor/hljs-light.min.css +1 -0
- devcoach/web/static/vendor/htmx.min.js +1 -0
- devcoach/web/static/vendor/icons/bitbucket.svg +1 -0
- devcoach/web/static/vendor/icons/github.svg +1 -0
- devcoach/web/static/vendor/icons/gitlab.svg +1 -0
- devcoach/web/static/vendor/icons/vscode.svg +41 -0
- devcoach/web/static/vendor/marked.min.js +6 -0
- devcoach/web/static/vendor/tailwind.js +83 -0
- devcoach/web/templates/base.html +80 -0
- devcoach/web/templates/lesson_detail.html +215 -0
- devcoach/web/templates/lessons.html +546 -0
- devcoach/web/templates/profile.html +240 -0
- devcoach/web/templates/settings.html +144 -0
- devcoach-0.1.0.dist-info/METADATA +443 -0
- devcoach-0.1.0.dist-info/RECORD +43 -0
- devcoach-0.1.0.dist-info/WHEEL +4 -0
- devcoach-0.1.0.dist-info/entry_points.txt +2 -0
- devcoach-0.1.0.dist-info/licenses/LICENSE +201 -0
- devcoach-0.1.0.dist-info/licenses/NOTICE +20 -0
devcoach/mcp/server.py
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
"""FastMCP server for devcoach — tools, resources, prompt, and entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.resources
|
|
6
|
+
import json
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from fastmcp import Context, FastMCP
|
|
14
|
+
|
|
15
|
+
from devcoach.core import coach, db
|
|
16
|
+
from devcoach.core.detect import detect_stack
|
|
17
|
+
from devcoach.core.git import detect_git_context
|
|
18
|
+
from devcoach.core.models import Lesson, Level, Profile, RepositoryPlatform
|
|
19
|
+
|
|
20
|
+
# ── FastMCP app ────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
mcp = FastMCP(
|
|
23
|
+
name="devcoach",
|
|
24
|
+
instructions=(
|
|
25
|
+
"Progressive technical coaching server. "
|
|
26
|
+
"Use the devcoach_instructions prompt for full coaching behaviour guidelines."
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ── MCP Tools ─────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@mcp.tool
|
|
35
|
+
async def log_lesson(
|
|
36
|
+
ctx: Context,
|
|
37
|
+
id: str,
|
|
38
|
+
timestamp: str,
|
|
39
|
+
topic_id: str,
|
|
40
|
+
categories: list[str],
|
|
41
|
+
title: str,
|
|
42
|
+
level: Level,
|
|
43
|
+
summary: str,
|
|
44
|
+
task_context: str | None = None,
|
|
45
|
+
project: str | None = None,
|
|
46
|
+
repository: str | None = None,
|
|
47
|
+
branch: str | None = None,
|
|
48
|
+
commit_hash: str | None = None,
|
|
49
|
+
folder: str | None = None,
|
|
50
|
+
repository_platform: RepositoryPlatform | None = None,
|
|
51
|
+
) -> str:
|
|
52
|
+
"""Save a delivered lesson to the coaching log.
|
|
53
|
+
|
|
54
|
+
Git metadata fields (project, repository, branch, commit_hash, folder,
|
|
55
|
+
repository_platform) are auto-detected from the current workspace when
|
|
56
|
+
not provided. Detection order: caller value → git auto-detect → usage
|
|
57
|
+
default from past lessons → None.
|
|
58
|
+
|
|
59
|
+
Returns 'ok' on success.
|
|
60
|
+
"""
|
|
61
|
+
git_ctx = detect_git_context()
|
|
62
|
+
try:
|
|
63
|
+
with db.connection() as conn:
|
|
64
|
+
usage = db.get_usage_defaults(conn)
|
|
65
|
+
except Exception:
|
|
66
|
+
usage = {}
|
|
67
|
+
|
|
68
|
+
resolved_project = project or git_ctx["project"] or usage.get("project")
|
|
69
|
+
resolved_repository = repository or git_ctx["repository"] or usage.get("repository")
|
|
70
|
+
resolved_branch = branch or git_ctx["branch"] or usage.get("branch")
|
|
71
|
+
resolved_commit = commit_hash or git_ctx["commit_hash"]
|
|
72
|
+
resolved_folder = folder or git_ctx["folder"]
|
|
73
|
+
resolved_platform = (
|
|
74
|
+
repository_platform or git_ctx["repository_platform"] or usage.get("repository_platform")
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
auto_filled = {
|
|
78
|
+
k: v
|
|
79
|
+
for k, v in {
|
|
80
|
+
"project": resolved_project if not project else None,
|
|
81
|
+
"branch": resolved_branch if not branch else None,
|
|
82
|
+
"commit_hash": resolved_commit if not commit_hash else None,
|
|
83
|
+
}.items()
|
|
84
|
+
if v is not None
|
|
85
|
+
}
|
|
86
|
+
if auto_filled:
|
|
87
|
+
await ctx.info(f"log_lesson: auto-filled git context {auto_filled}")
|
|
88
|
+
|
|
89
|
+
lesson = Lesson(
|
|
90
|
+
id=id,
|
|
91
|
+
timestamp=timestamp,
|
|
92
|
+
topic_id=topic_id,
|
|
93
|
+
categories=categories,
|
|
94
|
+
title=title,
|
|
95
|
+
level=level,
|
|
96
|
+
summary=summary,
|
|
97
|
+
task_context=task_context,
|
|
98
|
+
project=resolved_project,
|
|
99
|
+
repository=resolved_repository,
|
|
100
|
+
branch=resolved_branch,
|
|
101
|
+
commit_hash=resolved_commit,
|
|
102
|
+
folder=resolved_folder,
|
|
103
|
+
repository_platform=resolved_platform,
|
|
104
|
+
)
|
|
105
|
+
try:
|
|
106
|
+
with db.connection() as conn:
|
|
107
|
+
db.insert_lesson(conn, lesson)
|
|
108
|
+
return "ok"
|
|
109
|
+
except Exception as exc:
|
|
110
|
+
return f"error: {exc}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@mcp.tool
|
|
114
|
+
def update_knowledge(topic: str, delta: int) -> Profile:
|
|
115
|
+
"""Adjust the confidence score for a topic by delta (e.g. +1 or -1).
|
|
116
|
+
|
|
117
|
+
Returns the updated Profile. Confidence is clamped to 0-10.
|
|
118
|
+
If the topic does not exist it is created with a base confidence of 5.
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
with db.connection() as conn:
|
|
122
|
+
return coach.apply_knowledge_delta(conn, topic, delta)
|
|
123
|
+
except Exception:
|
|
124
|
+
return Profile(knowledge=[], groups=[])
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@mcp.tool
|
|
128
|
+
def get_lessons(
|
|
129
|
+
period: Literal["today", "week", "month", "year", "all"] | None = None,
|
|
130
|
+
category: str | None = None,
|
|
131
|
+
level: Literal["junior", "mid", "senior"] | None = None,
|
|
132
|
+
project: str | None = None,
|
|
133
|
+
repository: str | None = None,
|
|
134
|
+
branch: str | None = None,
|
|
135
|
+
commit: str | None = None,
|
|
136
|
+
starred: bool | None = None,
|
|
137
|
+
feedback: Literal["know", "dont_know", "none"] | None = None,
|
|
138
|
+
search: str | None = None,
|
|
139
|
+
date_from: str | None = None,
|
|
140
|
+
date_to: str | None = None,
|
|
141
|
+
) -> list[Lesson]:
|
|
142
|
+
"""Query the coaching lesson history.
|
|
143
|
+
|
|
144
|
+
period: today | week | month | year | all (default: all)
|
|
145
|
+
category: filter by a specific category tag (e.g. "python", "docker")
|
|
146
|
+
level: filter by difficulty level — junior | mid | senior
|
|
147
|
+
project / repository / branch: fuzzy match on git metadata
|
|
148
|
+
commit: fuzzy match on commit hash
|
|
149
|
+
starred: True to return only starred (favourite) lessons
|
|
150
|
+
feedback: "know" | "dont_know" | "none" (no response given)
|
|
151
|
+
search: full-text search across title, topic_id, and summary
|
|
152
|
+
date_from / date_to: ISO date/datetime strings; override period when set.
|
|
153
|
+
Date-only (YYYY-MM-DD) or with time (YYYY-MM-DDTHH:MM or YYYY-MM-DDTHH:MM:SS).
|
|
154
|
+
date_to with date-only is treated as end-of-day (23:59:59).
|
|
155
|
+
All filters can be combined.
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
with db.connection() as conn:
|
|
159
|
+
return db.get_lessons(
|
|
160
|
+
conn,
|
|
161
|
+
period=period,
|
|
162
|
+
category=category,
|
|
163
|
+
level=level,
|
|
164
|
+
project=project,
|
|
165
|
+
repository=repository,
|
|
166
|
+
branch=branch,
|
|
167
|
+
commit=commit,
|
|
168
|
+
starred=starred,
|
|
169
|
+
feedback=feedback,
|
|
170
|
+
search=search,
|
|
171
|
+
date_from=date_from,
|
|
172
|
+
date_to=date_to,
|
|
173
|
+
)
|
|
174
|
+
except Exception:
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@mcp.tool
|
|
179
|
+
def star_lesson(lesson_id: str) -> str:
|
|
180
|
+
"""Toggle the starred (favourite) flag on a lesson.
|
|
181
|
+
|
|
182
|
+
Returns 'starred' or 'unstarred' to indicate the new state.
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
with db.connection() as conn:
|
|
186
|
+
new_state = db.toggle_star(conn, lesson_id)
|
|
187
|
+
return "starred" if new_state else "unstarred"
|
|
188
|
+
except Exception as exc:
|
|
189
|
+
return f"error: {exc}"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@mcp.tool
|
|
193
|
+
def submit_feedback(lesson_id: str, feedback: str) -> Profile:
|
|
194
|
+
"""Record user comprehension feedback for a lesson and update knowledge confidence.
|
|
195
|
+
|
|
196
|
+
feedback: "know" (understood, confidence +1) | "dont_know" (needs practice, confidence -1)
|
|
197
|
+
| "clear" (remove feedback only — confidence is NOT adjusted)
|
|
198
|
+
Returns the updated Profile.
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
feedback_value = None if feedback == "clear" else feedback
|
|
202
|
+
with db.connection() as conn:
|
|
203
|
+
coach.record_feedback(conn, lesson_id, feedback_value)
|
|
204
|
+
return coach.get_profile(conn)
|
|
205
|
+
except Exception:
|
|
206
|
+
return Profile(knowledge=[], groups=[])
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@mcp.tool
|
|
210
|
+
def add_topic(topic: str, confidence: int = 5, group: str | None = None) -> Profile:
|
|
211
|
+
"""Add a new topic to the knowledge map, or update confidence if it already exists.
|
|
212
|
+
|
|
213
|
+
topic: topic identifier, e.g. 'rust_lifetimes'
|
|
214
|
+
confidence: initial confidence score 0-10 (default 5)
|
|
215
|
+
group: optional group name; topic appears under 'Other' if omitted
|
|
216
|
+
Returns the updated Profile.
|
|
217
|
+
"""
|
|
218
|
+
try:
|
|
219
|
+
with db.connection() as conn:
|
|
220
|
+
db.upsert_knowledge(conn, topic, confidence)
|
|
221
|
+
if group and group != "Other":
|
|
222
|
+
db.assign_topic_to_group(conn, topic, group)
|
|
223
|
+
return coach.get_profile(conn)
|
|
224
|
+
except Exception:
|
|
225
|
+
return Profile(knowledge=[], groups=[])
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@mcp.tool
|
|
229
|
+
def remove_topic(topic: str) -> Profile:
|
|
230
|
+
"""Remove a topic from the knowledge map entirely.
|
|
231
|
+
|
|
232
|
+
Returns the updated Profile.
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
with db.connection() as conn:
|
|
236
|
+
db.delete_knowledge(conn, topic)
|
|
237
|
+
return coach.get_profile(conn)
|
|
238
|
+
except Exception:
|
|
239
|
+
return Profile(knowledge=[], groups=[])
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@mcp.tool
|
|
243
|
+
def add_group(name: str) -> Profile:
|
|
244
|
+
"""Create a new (initially empty) knowledge group.
|
|
245
|
+
|
|
246
|
+
name: group name, e.g. 'Machine Learning'
|
|
247
|
+
Note: add_topic(group=name) also auto-creates the group when assigning a topic.
|
|
248
|
+
Returns the updated Profile.
|
|
249
|
+
"""
|
|
250
|
+
try:
|
|
251
|
+
name = name.strip()
|
|
252
|
+
with db.connection() as conn:
|
|
253
|
+
db.add_group(conn, name)
|
|
254
|
+
return coach.get_profile(conn)
|
|
255
|
+
except Exception:
|
|
256
|
+
return Profile(knowledge=[], groups=[])
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@mcp.tool
|
|
260
|
+
def remove_group(name: str) -> Profile:
|
|
261
|
+
"""Delete a knowledge group. Topics in the group move to Other.
|
|
262
|
+
|
|
263
|
+
Returns the updated Profile.
|
|
264
|
+
"""
|
|
265
|
+
try:
|
|
266
|
+
with db.connection() as conn:
|
|
267
|
+
db.delete_group(conn, name)
|
|
268
|
+
return coach.get_profile(conn)
|
|
269
|
+
except Exception:
|
|
270
|
+
return Profile(knowledge=[], groups=[])
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@mcp.tool
|
|
274
|
+
def update_settings(key: str, value: str) -> dict:
|
|
275
|
+
"""Update a coaching setting.
|
|
276
|
+
|
|
277
|
+
key: 'max_per_day' or 'min_gap_minutes'
|
|
278
|
+
value: new value as a string
|
|
279
|
+
- max_per_day: integer 1-20 (max lessons delivered in a 24h window)
|
|
280
|
+
- min_gap_minutes: integer 0-1440 (minimum minutes between lessons; 0 = no cooldown)
|
|
281
|
+
Returns the updated settings dict, or {"error": "..."} if validation fails.
|
|
282
|
+
"""
|
|
283
|
+
try:
|
|
284
|
+
valid_keys = {"max_per_day", "min_gap_minutes"}
|
|
285
|
+
if key not in valid_keys:
|
|
286
|
+
return {"error": f"Unknown key '{key}'. Valid keys: {', '.join(sorted(valid_keys))}"}
|
|
287
|
+
try:
|
|
288
|
+
int_val = int(value)
|
|
289
|
+
except ValueError:
|
|
290
|
+
return {"error": f"Value must be an integer, got '{value}'"}
|
|
291
|
+
if key == "max_per_day" and not (1 <= int_val <= 20):
|
|
292
|
+
return {"error": "max_per_day must be between 1 and 20"}
|
|
293
|
+
if key == "min_gap_minutes" and not (0 <= int_val <= 1440):
|
|
294
|
+
return {"error": "min_gap_minutes must be between 0 and 1440"}
|
|
295
|
+
with db.connection() as conn:
|
|
296
|
+
db.set_setting(conn, key, str(int_val))
|
|
297
|
+
s = db.get_settings(conn)
|
|
298
|
+
return {"max_per_day": s.max_per_day, "min_gap_minutes": s.min_gap_minutes}
|
|
299
|
+
except Exception as exc:
|
|
300
|
+
return {"error": str(exc)}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@mcp.tool
|
|
304
|
+
def open_ui(port: int = 7860) -> str:
|
|
305
|
+
"""Launch the devcoach web dashboard in the background.
|
|
306
|
+
|
|
307
|
+
Opens http://localhost:<port> — defaults to 7860.
|
|
308
|
+
port must be in the range 1024-65535.
|
|
309
|
+
"""
|
|
310
|
+
if not (1024 <= port <= 65535):
|
|
311
|
+
return f"error: port {port} is out of valid range (1024-65535)"
|
|
312
|
+
cmd = (
|
|
313
|
+
["uvx", "devcoach", "ui", "--port", str(port)]
|
|
314
|
+
if shutil.which("uvx")
|
|
315
|
+
else ["devcoach", "ui", "--port", str(port)]
|
|
316
|
+
)
|
|
317
|
+
subprocess.Popen(cmd)
|
|
318
|
+
return f"devcoach UI starting at http://localhost:{port}"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@mcp.tool
|
|
322
|
+
async def complete_onboarding(
|
|
323
|
+
ctx: Context,
|
|
324
|
+
topics: dict[str, int],
|
|
325
|
+
groups: dict[str, list[str]] | None = None,
|
|
326
|
+
) -> Profile:
|
|
327
|
+
"""Save the user's initial knowledge profile and mark onboarding complete.
|
|
328
|
+
|
|
329
|
+
topics: {topic_id: confidence_0_to_10} — all topics the user confirmed.
|
|
330
|
+
groups: {group_name: [topic_id, ...]} — optional grouping structure.
|
|
331
|
+
Topics not listed in any group are placed in 'Other'.
|
|
332
|
+
Groups are dynamically defined by the onboarding conversation — there
|
|
333
|
+
is no predefined catalogue. Suggest logical groupings based on what
|
|
334
|
+
the user selected (e.g. Languages, Backend, DevOps, Version Control)
|
|
335
|
+
and confirm with the user before calling this tool.
|
|
336
|
+
|
|
337
|
+
Wipes any default-seeded profile, saves selections, marks onboarding done.
|
|
338
|
+
Returns the updated Profile.
|
|
339
|
+
"""
|
|
340
|
+
try:
|
|
341
|
+
with db.connection() as conn:
|
|
342
|
+
conn.execute("DELETE FROM knowledge")
|
|
343
|
+
conn.execute("DELETE FROM knowledge_groups")
|
|
344
|
+
conn.execute("DELETE FROM knowledge_group_names")
|
|
345
|
+
conn.commit()
|
|
346
|
+
for topic, confidence in topics.items():
|
|
347
|
+
db.upsert_knowledge(conn, topic, max(0, min(10, confidence)))
|
|
348
|
+
if groups:
|
|
349
|
+
for group_name, group_topics in groups.items():
|
|
350
|
+
for t in group_topics:
|
|
351
|
+
if t in topics:
|
|
352
|
+
db.assign_topic_to_group(conn, t, group_name)
|
|
353
|
+
db.set_setting(conn, "onboarding_completed", "1")
|
|
354
|
+
profile = coach.get_profile(conn)
|
|
355
|
+
await ctx.info(f"Onboarding complete — {len(topics)} topics, {len(groups or {})} groups")
|
|
356
|
+
return profile
|
|
357
|
+
except Exception:
|
|
358
|
+
return Profile(knowledge=[], groups=[])
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# ── MCP Resources ──────────────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@mcp.resource("devcoach://profile")
|
|
365
|
+
def profile_resource() -> str:
|
|
366
|
+
"""Current knowledge map — topics, confidence scores, and groups."""
|
|
367
|
+
try:
|
|
368
|
+
with db.connection() as conn:
|
|
369
|
+
return coach.get_profile(conn).model_dump_json(indent=2)
|
|
370
|
+
except Exception as exc:
|
|
371
|
+
return json.dumps({"error": str(exc)})
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@mcp.resource("devcoach://settings")
|
|
375
|
+
def settings_resource() -> str:
|
|
376
|
+
"""Current coaching settings (rate limits)."""
|
|
377
|
+
try:
|
|
378
|
+
with db.connection() as conn:
|
|
379
|
+
settings = db.get_settings(conn)
|
|
380
|
+
return json.dumps(settings.model_dump(), indent=2)
|
|
381
|
+
except Exception as exc:
|
|
382
|
+
return json.dumps({"error": str(exc)})
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@mcp.resource("devcoach://lessons/recent")
|
|
386
|
+
def recent_lessons_resource() -> str:
|
|
387
|
+
"""Last 10 lessons from the current week."""
|
|
388
|
+
try:
|
|
389
|
+
with db.connection() as conn:
|
|
390
|
+
lessons = db.get_lessons(conn, period="week")
|
|
391
|
+
return json.dumps(
|
|
392
|
+
[lesson.model_dump() for lesson in lessons[:10]],
|
|
393
|
+
indent=2,
|
|
394
|
+
ensure_ascii=False,
|
|
395
|
+
)
|
|
396
|
+
except Exception as exc:
|
|
397
|
+
return json.dumps({"error": str(exc)})
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@mcp.resource("devcoach://stats")
|
|
401
|
+
def stats_resource() -> str:
|
|
402
|
+
"""Aggregate coaching statistics: lesson counts, rate-limit state, weakest/strongest topics."""
|
|
403
|
+
try:
|
|
404
|
+
with db.connection() as conn:
|
|
405
|
+
return json.dumps(coach.get_stats(conn), indent=2)
|
|
406
|
+
except Exception as exc:
|
|
407
|
+
return json.dumps({"error": str(exc)})
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@mcp.resource("devcoach://taught-topics")
|
|
411
|
+
def taught_topics_resource() -> str:
|
|
412
|
+
"""All topic_ids that have already been taught.
|
|
413
|
+
|
|
414
|
+
Read this before selecting a new lesson topic to avoid repetition.
|
|
415
|
+
"""
|
|
416
|
+
try:
|
|
417
|
+
with db.connection() as conn:
|
|
418
|
+
return json.dumps(coach.list_taught_topics(conn))
|
|
419
|
+
except Exception as exc:
|
|
420
|
+
return json.dumps({"error": str(exc)})
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@mcp.resource("devcoach://rate-limit")
|
|
424
|
+
def rate_limit_resource() -> str:
|
|
425
|
+
"""Current rate-limit status.
|
|
426
|
+
|
|
427
|
+
Returns {allowed, reason} — check this before delivering a lesson.
|
|
428
|
+
"""
|
|
429
|
+
try:
|
|
430
|
+
with db.connection() as conn:
|
|
431
|
+
result = coach.check_rate_limit(conn)
|
|
432
|
+
return result.model_dump_json(indent=2)
|
|
433
|
+
except Exception as exc:
|
|
434
|
+
return json.dumps({"allowed": False, "reason": f"Rate limit check unavailable: {exc}"})
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@mcp.resource("devcoach://context")
|
|
438
|
+
def context_resource() -> str:
|
|
439
|
+
"""Current workspace git context and most-used lesson metadata defaults.
|
|
440
|
+
|
|
441
|
+
git: auto-detected from cwd (branch, commit, repository, platform, folder).
|
|
442
|
+
usage_defaults: most-frequently used values from past lessons — used as
|
|
443
|
+
fallback when git detection finds nothing.
|
|
444
|
+
"""
|
|
445
|
+
try:
|
|
446
|
+
git = detect_git_context()
|
|
447
|
+
with db.connection() as conn:
|
|
448
|
+
usage = db.get_usage_defaults(conn)
|
|
449
|
+
return json.dumps({"git": git, "usage_defaults": usage}, indent=2)
|
|
450
|
+
except Exception as exc:
|
|
451
|
+
return json.dumps({"error": str(exc)})
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@mcp.resource("devcoach://onboarding")
|
|
455
|
+
def onboarding_resource() -> str:
|
|
456
|
+
"""Onboarding status and auto-detected stack for first-run setup.
|
|
457
|
+
|
|
458
|
+
needs_onboarding: true if the user has not yet completed the setup flow.
|
|
459
|
+
detected_stack: {topic_id: confidence} inferred from project files in cwd.
|
|
460
|
+
These are suggestions only — the user confirms or adjusts them during
|
|
461
|
+
the onboarding conversation before complete_onboarding is called.
|
|
462
|
+
context_ready: true if a git branch was successfully detected in cwd.
|
|
463
|
+
"""
|
|
464
|
+
try:
|
|
465
|
+
with db.connection() as conn:
|
|
466
|
+
done = db.is_onboarding_complete(conn)
|
|
467
|
+
git = detect_git_context()
|
|
468
|
+
detected = detect_stack(git["folder"] or str(Path.cwd()))
|
|
469
|
+
return json.dumps(
|
|
470
|
+
{
|
|
471
|
+
"needs_onboarding": not done,
|
|
472
|
+
"detected_stack": detected,
|
|
473
|
+
"context_ready": git["branch"] is not None,
|
|
474
|
+
},
|
|
475
|
+
indent=2,
|
|
476
|
+
)
|
|
477
|
+
except Exception as exc:
|
|
478
|
+
return json.dumps({"error": str(exc)})
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@mcp.resource("devcoach://lessons/{lesson_id}")
|
|
482
|
+
def lesson_resource(lesson_id: str) -> str:
|
|
483
|
+
"""A single lesson by ID.
|
|
484
|
+
|
|
485
|
+
Returns the full lesson JSON, or {"error": "..."} if not found.
|
|
486
|
+
"""
|
|
487
|
+
try:
|
|
488
|
+
with db.connection() as conn:
|
|
489
|
+
lesson = db.get_lesson_by_id(conn, lesson_id)
|
|
490
|
+
if lesson is None:
|
|
491
|
+
return json.dumps({"error": f"Lesson '{lesson_id}' not found"})
|
|
492
|
+
return lesson.model_dump_json(indent=2)
|
|
493
|
+
except Exception as exc:
|
|
494
|
+
return json.dumps({"error": str(exc)})
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
# ── MCP Prompt ────────────────────────────────────────────────────────────
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
@mcp.prompt
|
|
501
|
+
def devcoach_instructions() -> str:
|
|
502
|
+
"""Full coaching instructions for the devcoach skill (content of SKILL.md)."""
|
|
503
|
+
try:
|
|
504
|
+
return (
|
|
505
|
+
importlib.resources.files("devcoach").joinpath("SKILL.md").read_text(encoding="utf-8")
|
|
506
|
+
)
|
|
507
|
+
except Exception:
|
|
508
|
+
return "devcoach: coaching instructions unavailable (SKILL.md not found in package)."
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
# ── Entry point ───────────────────────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def main() -> None:
|
|
515
|
+
"""Start devcoach: CLI subcommand if given, else stdio MCP server."""
|
|
516
|
+
cli_commands = {
|
|
517
|
+
"profile",
|
|
518
|
+
"lessons",
|
|
519
|
+
"lesson",
|
|
520
|
+
"star",
|
|
521
|
+
"feedback",
|
|
522
|
+
"settings",
|
|
523
|
+
"set",
|
|
524
|
+
"stats",
|
|
525
|
+
"backup",
|
|
526
|
+
"restore",
|
|
527
|
+
"ui",
|
|
528
|
+
"knowledge-add",
|
|
529
|
+
"knowledge-remove",
|
|
530
|
+
"group-add",
|
|
531
|
+
"group-remove",
|
|
532
|
+
"group-assign",
|
|
533
|
+
"install",
|
|
534
|
+
"setup",
|
|
535
|
+
}
|
|
536
|
+
if len(sys.argv) > 1 and sys.argv[1] in cli_commands:
|
|
537
|
+
from devcoach.cli.commands import run_cli
|
|
538
|
+
|
|
539
|
+
run_cli()
|
|
540
|
+
else:
|
|
541
|
+
mcp.run(transport="stdio")
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
if __name__ == "__main__":
|
|
545
|
+
main()
|
devcoach/web/__init__.py
ADDED
|
File without changes
|