devcoach 0.3.63 → 0.3.65
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.
- package/README.md +73 -109
- package/dist/bin.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,106 +49,97 @@ flowchart TD
|
|
|
49
49
|
|
|
50
50
|
---
|
|
51
51
|
|
|
52
|
-
##
|
|
52
|
+
## Installation
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
devcoach runs **locally** — a stdio MCP server that stores everything in `~/.devcoach/coaching.db` on the machine where your agent runs. It works in **Claude Code** and **Claude Desktop**, but **not** on claude.ai web (which only supports hosted/remote connectors). Requires **Node.js ≥ 24**.
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|--------|---------|--------------|
|
|
58
|
-
| **npx** (no install) | _(used directly in the MCP config below)_ | Node.js ≥ 24 |
|
|
59
|
-
| **npm** (global) | `npm install -g devcoach` | Node.js ≥ 24 |
|
|
60
|
-
| **Homebrew** (macOS/Linux) | `brew install UltimaPhoenix/tap/devcoach` | Homebrew (pulls Node in automatically) |
|
|
61
|
-
| **Desktop Extension** (`.mcpb`) | one-click — see below | Claude Desktop (bundles its own Node) |
|
|
62
|
-
| **Claude Code plugin** | `/plugin marketplace add …` + `/plugin install …` — see below | Claude Code (Node.js ≥ 24) |
|
|
56
|
+
Pick the method that matches your setup — each section below is self-contained (install **and** connect):
|
|
63
57
|
|
|
64
|
-
|
|
58
|
+
| Your setup | Method |
|
|
59
|
+
|------------|--------|
|
|
60
|
+
| **Claude Code** | **1. Plugin** — bundles the MCP server, hooks & skill in one install *(recommended)* |
|
|
61
|
+
| **Claude Code, Cursor, Windsurf, Cline, …** | **2. npx CLI** — `npx -y devcoach install` |
|
|
62
|
+
| **macOS / Linux** | **3. Homebrew** |
|
|
63
|
+
| **Claude Desktop** | **4. One-click extension** (`.mcpb`) |
|
|
64
|
+
| **claude.ai web** | **5. Skill copy** |
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
### 1. Claude Code plugin (recommended)
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
<summary><strong>Claude Desktop one-click extension</strong> (<code>.mcpb</code>)</summary>
|
|
70
|
-
|
|
71
|
-
Claude Desktop installs devcoach from a single bundle — no Node or terminal needed (it runs on Desktop's
|
|
72
|
-
bundled runtime). Build the bundle and install it:
|
|
68
|
+
The plugin bundles **everything** — the MCP server, the automatic-coaching Stop hooks, and the coaching skill — so there's nothing else to wire up and **no need to run `devcoach install`** (don't do both, or the Stop hooks get registered twice).
|
|
73
69
|
|
|
74
70
|
```bash
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
# Add the marketplace once, then install (you can install any UltimaPhoenix plugin from it later)
|
|
72
|
+
/plugin marketplace add UltimaPhoenix/claude-plugins-marketplace
|
|
73
|
+
/plugin install devcoach@ultimaphoenix
|
|
77
74
|
```
|
|
78
75
|
|
|
79
|
-
|
|
80
|
-
needed for a verified signature). Prebuilt `.mcpb` releases and a Desktop directory listing are planned.
|
|
81
|
-
|
|
82
|
-
</details>
|
|
76
|
+
The MCP server, hooks, and skill activate on install — no restart needed.
|
|
83
77
|
|
|
84
78
|
<details>
|
|
85
|
-
<summary><strong>
|
|
86
|
-
|
|
87
|
-
The plugin bundles everything — the MCP server, the automatic-coaching Stop hooks, and the coaching
|
|
88
|
-
skill — so there's nothing to wire up by hand and **no need to run `devcoach install`** (avoid running
|
|
89
|
-
both, or the Stop hooks get registered twice). Three ways to install:
|
|
79
|
+
<summary><strong>Other ways to install the plugin</strong> (straight from the repo · offline zip)</summary>
|
|
90
80
|
|
|
91
81
|
```bash
|
|
92
|
-
#
|
|
82
|
+
# Straight from the devcoach repo (no separate marketplace)
|
|
93
83
|
/plugin marketplace add UltimaPhoenix/dev-coach
|
|
94
84
|
/plugin install devcoach@devcoach
|
|
95
85
|
|
|
96
|
-
#
|
|
97
|
-
/plugin marketplace add UltimaPhoenix/claude-plugins-marketplace
|
|
98
|
-
/plugin install devcoach@ultimaphoenix
|
|
99
|
-
|
|
100
|
-
# C — offline: download devcoach-plugin-<version>.zip from the GitHub Release, unzip, then
|
|
86
|
+
# Offline — download devcoach-plugin-<version>.zip from a GitHub Release, unzip, then:
|
|
101
87
|
/plugin marketplace add /path/to/unzipped-folder
|
|
102
88
|
/plugin install devcoach@devcoach
|
|
103
89
|
```
|
|
104
90
|
|
|
105
|
-
The plugin still runs the published `devcoach` npm package via `npx`, so it needs **Node.js ≥ 24** and
|
|
106
|
-
runs **locally only** (Claude Code / Claude Desktop, not claude.ai web). See
|
|
107
|
-
[Claude Code plugin](docs/claude-code-plugin.md) for how it works and the local-only details.
|
|
108
|
-
|
|
109
91
|
</details>
|
|
110
92
|
|
|
111
|
-
|
|
112
|
-
|
|
93
|
+
The plugin runs the published `devcoach` npm package via `npx`, so it needs **Node.js ≥ 24** and runs **locally only**. See [Claude Code plugin](docs/claude-code-plugin.md) for how it works.
|
|
94
|
+
|
|
95
|
+
### 2. npx CLI (any MCP agent)
|
|
113
96
|
|
|
114
|
-
|
|
97
|
+
No install required — `npx` runs devcoach on demand. For **Claude Code** and **Claude Desktop**, one command registers the MCP server and wires up automatic lesson delivery:
|
|
115
98
|
|
|
116
99
|
```bash
|
|
117
|
-
|
|
118
|
-
|
|
100
|
+
npx -y devcoach install
|
|
101
|
+
```
|
|
119
102
|
|
|
120
|
-
|
|
121
|
-
brew trust --tap UltimaPhoenix/tap
|
|
103
|
+
Restart your agent afterward. Prefer a global binary? `npm install -g devcoach`, then run `devcoach install` (and drop the `npx -y` prefix everywhere).
|
|
122
104
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
```
|
|
105
|
+
<details>
|
|
106
|
+
<summary><strong>Manual MCP config for Claude Code</strong> (if <code>devcoach install</code> isn't available)</summary>
|
|
126
107
|
|
|
127
|
-
|
|
128
|
-
will load its formulae when `HOMEBREW_REQUIRE_TAP_TRUST` is set (the trust list lives in
|
|
129
|
-
`~/.homebrew/trust.json`). Both are one-time. To update later: `brew upgrade devcoach`.
|
|
108
|
+
**Option A — via the `claude mcp` CLI (recommended):**
|
|
130
109
|
|
|
131
|
-
|
|
132
|
-
|
|
110
|
+
```bash
|
|
111
|
+
claude mcp add devcoach npx -- -y devcoach mcp
|
|
133
112
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
113
|
+
# all projects (user scope)
|
|
114
|
+
claude mcp add --scope user devcoach npx -- -y devcoach mcp
|
|
115
|
+
```
|
|
137
116
|
|
|
138
|
-
|
|
117
|
+
**Option B — edit `~/.claude.json` directly:**
|
|
139
118
|
|
|
140
|
-
|
|
119
|
+
```json
|
|
120
|
+
{ "mcpServers": { "devcoach": { "type": "stdio", "command": "npx", "args": ["-y", "devcoach", "mcp"] } } }
|
|
121
|
+
```
|
|
141
122
|
|
|
142
|
-
|
|
143
|
-
|
|
123
|
+
Then add the Stop hooks to `~/.claude/settings.json` for automatic lesson delivery:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"hooks": {
|
|
128
|
+
"Stop": [
|
|
129
|
+
{ "hooks": [{ "type": "command", "command": "npx -y devcoach onboard-hook" }] },
|
|
130
|
+
{ "hooks": [{ "type": "command", "command": "npx -y devcoach lesson-ready" }] }
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
}
|
|
144
134
|
```
|
|
145
135
|
|
|
146
|
-
|
|
136
|
+
> Tip: a global install puts `devcoach` on your `PATH`, so you can drop the `npx -y` prefix. `devcoach install` detects this automatically.
|
|
137
|
+
> **Using the [Claude Code plugin](#1-claude-code-plugin-recommended)?** Skip the hooks above — the plugin already provides them.
|
|
147
138
|
|
|
148
|
-
>
|
|
139
|
+
</details>
|
|
149
140
|
|
|
150
141
|
<details>
|
|
151
|
-
<summary><strong>
|
|
142
|
+
<summary><strong>Other agents</strong> (Cursor, Windsurf, Cline, Continue, Zed…)</summary>
|
|
152
143
|
|
|
153
144
|
Add this to your agent's MCP config file:
|
|
154
145
|
|
|
@@ -175,63 +166,38 @@ Add this to your agent's MCP config file:
|
|
|
175
166
|
|
|
176
167
|
</details>
|
|
177
168
|
|
|
178
|
-
|
|
179
|
-
<summary><strong>Manual setup</strong> (if <code>devcoach install</code> is not available)</summary>
|
|
180
|
-
|
|
181
|
-
#### Claude Code
|
|
169
|
+
### 3. Homebrew (macOS / Linux)
|
|
182
170
|
|
|
183
|
-
|
|
171
|
+
devcoach ships from its own tap. Add and trust the repository once, install, then connect:
|
|
184
172
|
|
|
185
173
|
```bash
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
# all projects (user scope)
|
|
189
|
-
claude mcp add --scope user devcoach npx -- -y devcoach mcp
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
**Option B — edit `~/.claude.json` directly:**
|
|
174
|
+
# 1. Add the tap — registers github.com/UltimaPhoenix/homebrew-tap with Homebrew
|
|
175
|
+
brew tap UltimaPhoenix/tap
|
|
193
176
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
```
|
|
177
|
+
# 2. Trust the whole tap — required when Homebrew enforces HOMEBREW_REQUIRE_TAP_TRUST
|
|
178
|
+
brew trust --tap UltimaPhoenix/tap
|
|
197
179
|
|
|
198
|
-
|
|
180
|
+
# 3. Install
|
|
181
|
+
brew install devcoach
|
|
199
182
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
"hooks": {
|
|
203
|
-
"Stop": [
|
|
204
|
-
{ "hooks": [{ "type": "command", "command": "npx -y devcoach onboard-hook" }] },
|
|
205
|
-
{ "hooks": [{ "type": "command", "command": "npx -y devcoach lesson-ready" }] }
|
|
206
|
-
]
|
|
207
|
-
}
|
|
208
|
-
}
|
|
183
|
+
# 4. Connect (Homebrew puts `devcoach` on your PATH — no `npx -y` prefix needed)
|
|
184
|
+
devcoach install
|
|
209
185
|
```
|
|
210
186
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
#### Claude Desktop
|
|
187
|
+
`brew tap` registers the third-party repository; `brew trust --tap` marks it trusted so Homebrew will load its formulae when `HOMEBREW_REQUIRE_TAP_TRUST` is set. Both are one-time. To update later: `brew upgrade devcoach`. One-liner: `brew install UltimaPhoenix/tap/devcoach` (run `brew trust --tap UltimaPhoenix/tap` first if your Homebrew enforces tap trust). The formula declares `depends_on "node"`, so Homebrew pulls in a recent Node automatically.
|
|
214
188
|
|
|
215
|
-
|
|
189
|
+
### 4. Claude Desktop extension (`.mcpb`)
|
|
216
190
|
|
|
217
|
-
|
|
218
|
-
|----------|-------------|
|
|
219
|
-
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
220
|
-
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
|
|
221
|
-
| Linux | `~/.config/Claude/claude_desktop_config.json` |
|
|
191
|
+
A single bundle that runs on Claude Desktop's built-in runtime — no Node or terminal needed:
|
|
222
192
|
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
"devcoach": {
|
|
227
|
-
"command": "npx",
|
|
228
|
-
"args": ["-y", "devcoach", "mcp"]
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
193
|
+
```bash
|
|
194
|
+
npm run mcpb # → dist-mcpb/devcoach-<version>.mcpb
|
|
195
|
+
# Claude Desktop → Settings → Extensions → Install Extension… → pick the .mcpb
|
|
232
196
|
```
|
|
233
197
|
|
|
234
|
-
|
|
198
|
+
`npm run mcpb:sign` self-signs it (installs as an *unverified publisher*; a real code-signing cert is needed for a verified signature). Prebuilt `.mcpb` releases and a Desktop directory listing are planned.
|
|
199
|
+
|
|
200
|
+
### 5. claude.ai web (skill copy)
|
|
235
201
|
|
|
236
202
|
Claude.ai does not support MCP servers. Install the coaching instructions as a skill instead:
|
|
237
203
|
|
|
@@ -241,9 +207,7 @@ Claude.ai does not support MCP servers. Install the coaching instructions as a s
|
|
|
241
207
|
|
|
242
208
|
This gives claude.ai the coaching behaviour without the MCP tools (lesson logging and profile tracking will not work).
|
|
243
209
|
|
|
244
|
-
> **Keep the skill up to date.** For Claude Code / Claude Desktop, the skill is served automatically via the MCP prompt and is always current. If you copied it manually to
|
|
245
|
-
|
|
246
|
-
</details>
|
|
210
|
+
> **Keep the skill up to date.** For Claude Code / Claude Desktop, the skill is served automatically (via the MCP prompt or the plugin) and is always current. If you copied it manually to claude.ai, re-paste the latest `SKILL.md` after each devcoach update.
|
|
247
211
|
|
|
248
212
|
---
|
|
249
213
|
|
package/dist/bin.js
CHANGED
|
@@ -46,7 +46,7 @@ import {spawn,spawnSync,execFileSync}from'child_process';import {existsSync,writ
|
|
|
46
46
|
CREATE INDEX IF NOT EXISTS idx_lessons_feedback ON lessons (feedback);
|
|
47
47
|
CREATE INDEX IF NOT EXISTS idx_lessons_topic_id ON lessons (topic_id);
|
|
48
48
|
`),go(t),uo(t);}function go(t){try{t.exec("ALTER TABLE lessons ADD COLUMN body TEXT");}catch{}}function uo(t){for(let[e,o]of Object.entries(ao))S(t,"INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)",e,o);}var dt="(id, timestamp, topic_id, categories, title, level, summary, body, task_context, project, repository, branch, commit_hash, folder, repository_platform, starred, feedback)",ct="(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";function lt(t){return [t.id,t.timestamp,t.topic_id,JSON.stringify(t.categories),t.title,t.level,t.summary,t.body,t.task_context,t.project,t.repository,t.branch,t.commit_hash,t.folder,t.repository_platform,t.starred?1:0,t.feedback]}function pt(t,e){S(t,`INSERT OR REPLACE INTO lessons ${dt} VALUES ${ct}`,...lt(e));}var mo=[["project","project"],["repository","repository"],["branch","branch"],["commit","commit_hash"]];function fo(t,e){if(t.date_from==null&&t.date_to==null){let o=wo(t.period??null);o!=null&&e("timestamp >= ?",o);return}if(t.date_from!=null&&e("timestamp >= ?",t.date_from),t.date_to!=null){let o=t.date_to.includes("T")||t.date_to.includes(" ");e("timestamp <= ?",o?t.date_to:`${t.date_to}T23:59:59`);}}function gt(t){let e=[],o=[],r=(s,...a)=>{e.push(s),o.push(...a);};fo(t,r),t.category!=null&&r("categories LIKE ?",`%"${t.category}"%`),t.level!=null&&r("level = ?",t.level);for(let[s,a]of mo){let l=t[s];l!=null&&r(`${a} LIKE ?`,`%${l}%`);}if(t.starred!=null&&r("starred = ?",t.starred?1:0),t.search!=null){let s=`%${t.search}%`;r("(title LIKE ? OR topic_id LIKE ? OR summary LIKE ? OR body LIKE ?)",s,s,s,s);}return t.feedback==="none"?r("feedback IS NULL"):t.feedback!=null&&r("feedback = ?",t.feedback),{where:e.length?`WHERE ${e.join(" AND ")}`:"",params:o}}function X(t,e={}){let{where:o,params:r}=gt(e),n=G(t,`SELECT COUNT(*) AS n FROM lessons ${o}`,...r);return Number(n?.n??0)}var bo=new Set(["timestamp","level","topic_id","title","feedback"]),yo=new Set(["project","repository","branch","commit_hash","repository_platform"]);function B(t,e={}){let{where:o,params:r}=gt(e),n=e.sort&&bo.has(e.sort)?e.sort:"timestamp",s=(e.order??"desc").toLowerCase()==="asc"?"ASC":"DESC",a=`SELECT * FROM lessons ${o} ORDER BY ${n} ${s}`,l=[...r];if(e.page!=null){let d=e.per_page??25;a+=" LIMIT ? OFFSET ?",l.push(d,(e.page-1)*d);}return j(t,a,...l).map(Ge)}function he(t,e){return S(t,"DELETE FROM lessons WHERE id = ?",e)>0}function Y(t,e,o){return S(t,"UPDATE lessons SET starred = ? WHERE id = ?",o?1:0,e)>0}function ne(t,e,o){S(t,"UPDATE lessons SET feedback = ? WHERE id = ?",o||null,e);let r=G(t,"SELECT topic_id FROM lessons WHERE id = ?",e);return r?r.topic_id:null}function se(t){return j(t,"SELECT * FROM lessons ORDER BY timestamp DESC").map(Ge)}function Me(t,e){let o=0,r=0,n=0;for(let s of e){let a;try{a=re(s);}catch{n+=1;continue}S(t,`INSERT OR IGNORE INTO lessons ${dt} VALUES ${ct}`,...lt(a))>0?o+=1:r+=1;}return {inserted:o,duplicated:r,invalid:n}}function ie(t,e){if(!yo.has(e))throw new Error(`Column not allowed: ${JSON.stringify(e)}`);return j(t,`SELECT DISTINCT ${e} FROM lessons WHERE ${e} IS NOT NULL ORDER BY ${e}`).map(r=>r[e])}function K(t,e){let o=G(t,"SELECT * FROM lessons WHERE id = ?",e);return o?Ge(o):null}function ut(t){return j(t,"SELECT DISTINCT value FROM lessons, json_each(lessons.categories) ORDER BY value").map(o=>o.value)}function mt(t){return j(t,"SELECT DISTINCT topic_id FROM lessons").map(e=>e.topic_id)}function xe(t,e){let o=G(t,"SELECT COUNT(*) AS n FROM lessons WHERE timestamp >= ?",e);return Number(o?.n??0)}function ft(t){let e=G(t,"SELECT timestamp FROM lessons ORDER BY timestamp DESC LIMIT 1");return e?e.timestamp:null}function Fe(t){let e={};for(let o of j(t,"SELECT topic, confidence FROM knowledge"))e[o.topic]=Number(o.confidence);return e}function ae(t){return j(t,"SELECT topic, confidence FROM knowledge ORDER BY topic").map(e=>({topic:e.topic,confidence:Number(e.confidence)}))}function He(t){let e=j(t,"SELECT group_name FROM knowledge_group_names ORDER BY group_name").map(r=>r.group_name),o=new Map;for(let r of j(t,"SELECT group_name, topic FROM knowledge_groups ORDER BY group_name, topic")){let n=r.group_name,s=o.get(n)??[];s.push(r.topic),o.set(n,s);}return e.map(r=>({name:r,topics:o.get(r)??[]}))}function O(t,e,o){let r=Math.max(0,Math.min(10,o)),n=new Date().toISOString();S(t,`INSERT INTO knowledge (topic, confidence, updated_at) VALUES (?, ?, ?)
|
|
49
|
-
ON CONFLICT(topic) DO UPDATE SET confidence = excluded.confidence, updated_at = excluded.updated_at`,e,r,n);}function J(t,e){let o=S(t,"DELETE FROM knowledge WHERE topic = ?",e);return S(t,"DELETE FROM knowledge_groups WHERE topic = ?",e),o>0}function P(t,e){let o=e.trim();if(!o)throw new Error("Group name must not be empty");return S(t,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",o)>0}function V(t,e){return S(t,"DELETE FROM knowledge_groups WHERE group_name = ?",e),S(t,"DELETE FROM knowledge_group_names WHERE group_name = ?",e)>0}function N(t,e,o){S(t,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",o),S(t,"DELETE FROM knowledge_groups WHERE topic = ?",e),S(t,"INSERT OR IGNORE INTO knowledge_groups (group_name, topic) VALUES (?, ?)",o,e);}function ke(t,e){S(t,"DELETE FROM knowledge_groups WHERE topic = ?",e);}function E(t){let e={};for(let d of j(t,"SELECT key, value FROM settings"))e[d.key]=d.value;let o=e.min_gap_minutes,r=e.min_hours_between,n;o!==void 0?n=Number.parseInt(o,10):r!==void 0?n=Number.parseInt(r,10)*60:n=240;let s=e.max_per_day,a=e.ui_theme??"system",l=a==="dark"||a==="light"||a==="system"?a:"system";return {max_per_day:s!==void 0?Number.parseInt(s,10):2,min_gap_minutes:n,ui_theme:l}}function R(t,e,o){S(t,"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",e,o);}function de(t){let e=G(t,"SELECT COUNT(*) AS n FROM knowledge");return {knowledge_ready:Number(e?.n??0)>0}}function Ue(t){let e={};for(let o of ["project","repository","branch","repository_platform"]){let r=G(t,`SELECT ${o} AS v, COUNT(*) AS c FROM lessons WHERE ${o} IS NOT NULL GROUP BY ${o} ORDER BY c DESC LIMIT 1`);e[o]=r?r.v:null;}return e}function ve(t){let e=E(t),o=se(t),r=ae(t),n=He(t),s=new Map;for(let d of n)for(let f of d.topics)s.set(f,d.name);let a={groups:n.map(d=>d.name),topics:r.map(d=>({topic:d.topic,confidence:d.confidence,group:s.get(d.topic)??null}))},l={[Oe]:strToU8(JSON.stringify(e,null,2)),[De]:strToU8(JSON.stringify(o,null,2)),[Ne]:strToU8(JSON.stringify(a,null,2))};return existsSync(w)&&(l[je]=strToU8(readFileSync(w,"utf8"))),zipSync(l)}function ho(t,e,o){if(!(Oe in e))return;let r=JSON.parse(strFromU8(e[Oe]));"max_per_day"in r&&R(t,"max_per_day",String(r.max_per_day)),"min_gap_minutes"in r&&R(t,"min_gap_minutes",String(r.min_gap_minutes)),(r.ui_theme==="system"||r.ui_theme==="dark"||r.ui_theme==="light")&&R(t,"ui_theme",r.ui_theme),o.settings=1;}function xo(t,e,o){if(!(Ne in e))return;let r=JSON.parse(strFromU8(e[Ne]));if(!(r&&typeof r=="object"&&Array.isArray(r.topics))){for(let[s,a]of Object.entries(r))O(t,s,a);o.topics=Object.keys(r).length;return}let n=0;for(let s of r.groups??[])P(t,s)&&(n+=1);for(let s of r.topics){O(t,s.topic,s.confidence);let a=s.group;a&&a!=="Other"&&(P(t,a)&&(n+=1),N(t,s.topic,a));}o.topics=r.topics.length,o.groups=n;}function ko(t,e,o){if(!(De in e))return;let r=JSON.parse(strFromU8(e[De])),{inserted:n,duplicated:s,invalid:a}=Me(t,r);o.lessons=n,o.skipped=s,o.invalid=a;}function vo(t,e){je in t&&(mkdirSync(dirname(w),{recursive:true}),writeFileSync(w,strFromU8(t[je]),"utf8"),e.learning_state=1);}function ce(t,e){let o={settings:0,topics:0,groups:0,lessons:0,skipped:0,invalid:0,learning_state:0},r=unzipSync(e);return ho(t,r,o),xo(t,r,o),ko(t,r,o),vo(r,o),o}function wo(t){let e=new Date,o;if(t==="today")o=new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),0,0,0,0));else if(t==="week")o=new Date(e.getTime()-7*864e5);else if(t==="month")o=new Date(e.getTime()-30*864e5);else if(t==="year")o=new Date(e.getTime()-365*864e5);else return null;return o.toISOString()}function Ge(t){let e=[];try{let o=JSON.parse(t.categories);Array.isArray(o)&&(e=o);}catch{e=[];}return re({...t,categories:e,starred:!!t.starred})}function z(t){try{let e=E(t),o=new Date,r=new Date(o.getTime()-24*36e5).toISOString(),n=xe(t,r);if(n>=e.max_per_day)return {allowed:!1,reason:`Daily limit reached (${n}/${e.max_per_day} lessons in the last 24h)`};let s=ft(t);if(s!=null){let a=new Date(s),l=(o.getTime()-a.getTime())/6e4;if(l<e.min_gap_minutes){let d=e.min_gap_minutes-l,f=Math.floor(e.min_gap_minutes/60),b=e.min_gap_minutes%60,y=Math.trunc(d),k=Math.floor(y/60),v=y%60;return {allowed:!1,reason:`Too soon: last lesson ${l<0?`in ${Math.round(-l)}m (future timestamp)`:`${Math.round(l)}m ago`}, minimum interval is ${f}h ${b}m (${k}h ${v}m remaining)`}}}return {allowed:!0}}catch(e){return {allowed:true,reason:`Rate limit check failed: ${e}`}}}function M(t){try{return {knowledge:ae(t),groups:He(t)}}catch{return {knowledge:[],groups:[]}}}function q(t,e,o){let r=Fe(t)[e]??5,n=Math.max(0,Math.min(10,r+o));return O(t,e,n),n}function we(t,e,o){let r=ne(t,e,o);return r&&(o==="know"||o==="dont_know")&&q(t,r,o==="know"?1:-1),r}function Z(t){try{let e=new Date,o=X(t),r=new Date(e.getTime()-24*36e5).toISOString(),n=new Date(e.getTime()-7*864e5).toISOString(),s=xe(t,r),a=xe(t,n),l=Fe(t),d=Object.entries(l).sort((y,k)=>y[1]-k[1]),f=d.slice(0,5).map(([y,k])=>({topic:y,confidence:k})),b=d.slice(-5).reverse().map(([y,k])=>({topic:y,confidence:k}));return {total_lessons:o,lessons_today:s,lessons_this_week:a,weakest_topics:f,strongest_topics:b}}catch(e){return {error:String(e)}}}function bt(t){try{return mt(t)}catch{return []}}var To=[["package.json","javascript",6],["tsconfig*.json","typescript",6],["requirements*.txt","python",6],["pyproject.toml","python",6],["setup.py","python",6],["go.mod","go",6],["Cargo.toml","rust",6],["pom.xml","java",6],["build.gradle","java",6],["build.gradle.kts","java",6],["*.csproj","csharp",6],["Gemfile","ruby",6],["composer.json","php",6],["Dockerfile","docker",7],["Dockerfile.*","docker",7],["docker-compose.yml","docker_compose",7],["docker-compose.yaml","docker_compose",7],["docker-compose.*.yml","docker_compose",7],[".github/workflows/*.yml","github_actions",6],[".github/workflows/*.yaml","github_actions",6],[".gitlab-ci.yml","gitlab_ci",6],["Jenkinsfile","ci_cd",6],["*.tf","terraform",6],["*.sql","sql",5],[".git","git",7],["kubernetes/*.yaml","kubernetes",6],["kubernetes/*.yml","kubernetes",6],["k8s/*.yaml","kubernetes",6],["*.bicep","azure",6],["serverless.yml","aws",6],["serverless.yaml","aws",6],["template.yaml","aws",5],["angular.json","angular",6],["svelte.config.*","svelte",6],["nuxt.config.*","vue",6],["vite.config.*","javascript",6]],Eo={react:"react","react-dom":"react",vue:"vue","@angular/core":"angular",svelte:"svelte",next:"nextjs",nuxt:"vue",fastify:"fastify",express:"express",koa:"express","@nestjs/core":"node_js"},Co={django:"django",flask:"flask",fastapi:"fastapi",starlette:"fastapi",tornado:"python",aiohttp:"python",sqlalchemy:"postgresql",celery:"python"};function Ro(t){let e="";for(let o of t)o==="*"?e+="[^/]*":o==="?"?e+="[^/]":e+=o.replace(/[.+^${}()|[\]\\]/g,String.raw`\$&`);return new RegExp(`^${e}$`)}function Lo(t,e){let o=e.lastIndexOf("/"),r=o>=0?join(t,e.slice(0,o)):t,n=o>=0?e.slice(o+1):e,s;try{s=readdirSync(r);}catch{return false}let a=Ro(n);return s.some(l=>a.test(l))}function yt(t){try{return existsSync(t)?readFileSync(t,"utf8"):null}catch{return null}}function Io(t,e){let o=yt(join(t,"package.json"));if(!o)return;let r;try{r=JSON.parse(o);}catch{return}let n=new Set([...Object.keys(r.dependencies??{}),...Object.keys(r.devDependencies??{})]);for(let[s,a]of Object.entries(Eo))n.has(s)&&e(a,6);}function Oo(t,e){let o=["requirements.txt","requirements-dev.txt","requirements/base.txt","pyproject.toml"];for(let r of o){let n=yt(join(t,r))?.toLowerCase();if(n)for(let[s,a]of Object.entries(Co))n.includes(s)&&e(a,6);}}function _e(t){let e={},o=(r,n)=>{(e[r]??-1)<n&&(e[r]=n);};for(let[r,n,s]of To)Lo(t,r)&&o(n,s);return Io(t,o),Oo(t,o),e}function qe(...t){try{return execFileSync("git",t,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}}var ht={"github.com":"github","gitlab.com":"gitlab","bitbucket.org":"bitbucket"};function jo(t){if(!t)return [null,null];let e=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(t);if(e){let r=(e[1]??"").toLowerCase();return [e[2]??t,ht[r]??"local"]}let o=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(t);if(o){let r=(o[1]??"").toLowerCase();return [o[2]??t,ht[r]??"local"]}return [t,"local"]}function Q(){let t=process.cwd(),e=qe("rev-parse","--abbrev-ref","HEAD"),o=qe("rev-parse","HEAD"),r=qe("remote","get-url","origin"),[n,s]=jo(r),a;return n?a=n.replace(/\/+$/,"").split("/").pop()??null:a=basename(t)||null,e==="HEAD"&&(e=null),{project:a,repository:n,branch:e,commit_hash:o,folder:t,repository_platform:s}}var kt=dirname(fileURLToPath(import.meta.url)),Fo=[join(kt,"../assets/SKILL.md"),join(kt,"../../assets/SKILL.md")];function vt(){for(let t of Fo)try{return readFileSync(t,"utf8")}catch{}return "devcoach: coaching instructions unavailable (SKILL.md not found in package)."}var wt={version:"0.3.63"};var le=wt.version;var I=t=>({type:"text",text:t}),_t=t=>({content:[I(JSON.stringify(t))]}),L=t=>({isError:true,content:[I(t)]}),We=t=>({content:[I(JSON.stringify(t))],structuredContent:t}),Wo={id:z$1.string(),timestamp:z$1.string(),topic_id:z$1.string(),categories:z$1.array(z$1.string()),title:z$1.string(),level:me,summary:z$1.string(),body:z$1.string().nullable(),task_context:z$1.string().nullable(),project:z$1.string().nullable(),repository:z$1.string().nullable(),branch:z$1.string().nullable(),commit_hash:z$1.string().nullable(),folder:z$1.string().nullable(),repository_platform:fe.nullable(),starred:z$1.boolean(),feedback:Re.nullable()},Xo={knowledge:z$1.array(z$1.object({topic:z$1.string(),confidence:z$1.number().int()})),groups:z$1.array(z$1.object({name:z$1.string(),topics:z$1.array(z$1.string())}))},Yo={max_per_day:z$1.number().int(),min_gap_minutes:z$1.number().int(),ui_theme:Ie};function _(t,e){return {contents:[{uri:t.href,mimeType:"application/json",text:JSON.stringify(e)}]}}function Jo(){let t=new McpServer({name:"devcoach",version:le},{instructions:"Progressive technical coaching server. Use the devcoach_instructions prompt for full coaching behaviour guidelines."});t.registerTool("log_lesson",{title:"Log Lesson",description:"Save a delivered lesson to the coaching log. Git metadata (project, repository, branch, commit_hash, folder, repository_platform) is auto-detected from the workspace when omitted. Returns the saved Lesson with all resolved fields.",inputSchema:{id:z$1.string().describe("Unique lesson id (uuid or random string)"),timestamp:z$1.string().describe("ISO 8601 timestamp; normalized to UTC and clamped to now"),topic_id:z$1.string().describe("Primary topic id, e.g. 'python'"),categories:z$1.array(z$1.string()).describe("Category tags, e.g. ['python','performance']"),title:z$1.string().describe("Short lesson title"),level:me.describe("Difficulty level: junior | mid | senior"),summary:z$1.string().describe("1-3 sentence summary shown in the lesson card"),body:z$1.string().nullish().describe("Optional full lesson markdown"),task_context:z$1.string().nullish().describe("What the user was doing when taught"),project:z$1.string().nullish().describe("Project name (auto-detected from git if omitted)"),repository:z$1.string().nullish().describe("org/repo or path (auto-detected if omitted)"),branch:z$1.string().nullish().describe("git branch (auto-detected if omitted)"),commit_hash:z$1.string().nullish().describe("git commit hash (auto-detected if omitted)"),folder:z$1.string().nullish().describe("workspace folder (auto-detected if omitted)"),repository_platform:fe.nullish().describe("github | gitlab | bitbucket | local (auto-detected if omitted)")},outputSchema:Wo,annotations:{title:"Log Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=Q(),n={};try{n=m(a=>Ue(a));}catch{n={};}let s=re({id:o.id,timestamp:o.timestamp,topic_id:o.topic_id,categories:o.categories,title:o.title,level:o.level,summary:o.summary,body:o.body??null,task_context:o.task_context??null,project:o.project??r.project??n.project??null,repository:o.repository??r.repository??n.repository??null,branch:o.branch??r.branch??n.branch??null,commit_hash:o.commit_hash??r.commit_hash??null,folder:o.folder??r.folder??null,repository_platform:o.repository_platform??r.repository_platform??n.repository_platform??null});m(a=>pt(a,s));try{if(t.server.getClientCapabilities()?.elicitation){let l=await t.server.elicitInput({message:"Did that land?",requestedSchema:{type:"object",properties:{feedback:{type:"string",title:"Did that land?",description:"know = got it \xB7 dont_know = need to revisit \xB7 skip = no feedback",enum:["know","dont_know","skip"]}},required:[]}}),d=l.action==="accept"?l.content?.feedback:void 0;d&&d!=="skip"&&(m(f=>{ne(f,o.id,d),q(f,o.topic_id,d==="know"?1:-1);}),s={...s,feedback:d});}}catch{}return We(s)}catch(r){return L(`log_lesson failed: ${r}`)}}),t.registerTool("update_knowledge",{title:"Update Knowledge",description:"Adjust the confidence score for a topic by delta (e.g. +1 or -1). Returns the new confidence (0-10). Creates the topic at confidence 5 if it does not exist.",inputSchema:{topic:z$1.string().describe("Topic id, e.g. 'python'"),delta:z$1.number().int().describe("Signed change to apply, e.g. +1 or -1")},annotations:{title:"Update Knowledge",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>q(n,o.topic,o.delta));return {content:[I(String(r))]}}catch(r){return L(`update_knowledge failed for '${o.topic}': ${r}`)}}),t.registerTool("get_lessons",{title:"Get Lessons",description:"Query the coaching lesson history. All filters combine. period defaults to all; date_from/date_to override period; limit caps results newest-first (0 = all).",inputSchema:{period:z$1.enum(["today","week","month","year","all"]).nullish().describe("Time window"),category:z$1.string().nullish().describe("Filter by a category tag, e.g. 'python'"),level:z$1.enum(["junior","mid","senior"]).nullish().describe("Filter by difficulty level"),project:z$1.string().nullish().describe("Fuzzy match on git project"),repository:z$1.string().nullish().describe("Fuzzy match on git repository"),branch:z$1.string().nullish().describe("Fuzzy match on git branch"),commit:z$1.string().nullish().describe("Fuzzy match on commit hash"),starred:z$1.boolean().nullish().describe("True to return only starred lessons"),feedback:z$1.enum(["know","dont_know","none"]).nullish().describe("Filter by feedback ('none' = no response)"),search:z$1.string().nullish().describe("Full-text search over title, topic_id, summary, body"),date_from:z$1.string().nullish().describe("ISO date/datetime lower bound"),date_to:z$1.string().nullish().describe("ISO date/datetime upper bound (date-only = end-of-day)"),limit:z$1.number().int().default(10).describe("Max lessons, newest first. Pass 0 for all.")},annotations:{title:"Get Lessons",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}},o=>{try{let r=m(n=>B(n,{period:o.period,category:o.category,level:o.level,project:o.project,repository:o.repository,branch:o.branch,commit:o.commit,starred:o.starred,feedback:o.feedback,search:o.search,date_from:o.date_from,date_to:o.date_to,page:o.limit>0?1:null,per_page:o.limit}));return _t(r)}catch{return _t([])}}),t.registerTool("star_lesson",{title:"Star Lesson",description:"Set the starred (favourite) flag on a lesson. Returns true if found and updated. Idempotent.",inputSchema:{lesson_id:z$1.string().describe("Lesson id"),starred:z$1.boolean().describe("true to favourite, false to unmark")},annotations:{title:"Star Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>Y(n,o.lesson_id,o.starred));return {content:[I(String(r))]}}catch(r){return L(`star_lesson failed for '${o.lesson_id}': ${r}`)}}),t.registerTool("delete_lesson",{title:"Delete Lesson",description:"Permanently delete a lesson by id. Returns true if found and deleted.",inputSchema:{lesson_id:z$1.string().describe("Lesson id to delete")},annotations:{title:"Delete Lesson",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>he(n,o.lesson_id));return {content:[I(String(r))]}}catch(r){return L(`delete_lesson failed for '${o.lesson_id}': ${r}`)}}),t.registerTool("submit_feedback",{title:"Submit Feedback",description:"Record comprehension feedback for a lesson and adjust knowledge confidence. know = +1, dont_know = -1, clear = remove feedback (no confidence change). Idempotent \u2014 the same feedback twice adjusts confidence only once.",inputSchema:{lesson_id:z$1.string().describe("Lesson id"),feedback:z$1.enum(["know","dont_know","clear"]).describe("know | dont_know | clear")},annotations:{title:"Submit Feedback",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=o.feedback==="clear"?null:o.feedback,n=m(s=>{let a=s.prepare("SELECT feedback, topic_id FROM lessons WHERE id = ?").get(o.lesson_id);return a?(a.feedback===r||(ne(s,o.lesson_id,r),(r==="know"||r==="dont_know")&&a.topic_id&&q(s,a.topic_id,r==="know"?1:-1)),!0):!1});return {content:[I(String(n))]}}catch(r){return L(`submit_feedback failed for '${o.lesson_id}': ${r}`)}}),t.registerTool("add_topic",{title:"Add Topic",description:"Add a topic to the knowledge map, or update its confidence if it exists. Optionally assign to a group (auto-created). Prefer a single-word topic id. Idempotent.",inputSchema:{topic:z$1.string().describe("Topic id \u2014 prefer a single word, max 3 words"),confidence:Le.default(5).describe("Initial confidence 0-10 (default 5)"),group:z$1.string().nullish().describe("Optional group name; 'Other' if omitted")},annotations:{title:"Add Topic",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{return m(r=>{O(r,o.topic,o.confidence),o.group&&o.group!=="Other"&&N(r,o.topic,o.group);}),{content:[I("true")]}}catch(r){return L(`add_topic failed for '${o.topic}': ${r}`)}}),t.registerTool("remove_topic",{title:"Remove Topic",description:"Remove a topic from the knowledge map entirely. Returns true if it existed.",inputSchema:{topic:z$1.string().describe("Topic id to remove")},annotations:{title:"Remove Topic",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>J(n,o.topic));return {content:[I(String(r))]}}catch(r){return L(`remove_topic failed for '${o.topic}': ${r}`)}}),t.registerTool("add_group",{title:"Add Group",description:"Create a new (initially empty) knowledge group. Idempotent \u2014 returns true either way.",inputSchema:{name:z$1.string().describe("Group name, e.g. 'Machine Learning'")},annotations:{title:"Add Group",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{return m(r=>P(r,o.name.trim())),{content:[I("true")]}}catch(r){return L(`add_group failed for '${o.name}': ${r}`)}}),t.registerTool("remove_group",{title:"Remove Group",description:"Delete a knowledge group. Its topics move to Other. Returns true if it existed.",inputSchema:{name:z$1.string().describe("Group name to delete")},annotations:{title:"Remove Group",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>V(n,o.name));return {content:[I(String(r))]}}catch(r){return L(`remove_group failed for '${o.name}': ${r}`)}}),t.registerTool("update_settings",{title:"Update Settings",description:"Update a coaching setting. max_per_day: integer 1-20. min_gap_minutes: integer 0-1440 (0 = no cooldown). Returns the full updated Settings.",inputSchema:{key:z$1.enum(["max_per_day","min_gap_minutes"]).describe("Setting key"),value:z$1.string().describe("New value as an integer string")},outputSchema:Yo,annotations:{title:"Update Settings",destructiveHint:false,idempotentHint:true,openWorldHint:false}},o=>{let r=Number.parseInt(o.value,10);if(Number.isNaN(r))return L(`Value must be an integer, got '${o.value}'`);if(o.key==="max_per_day"&&!(r>=1&&r<=20))return L("max_per_day must be between 1 and 20");if(o.key==="min_gap_minutes"&&!(r>=0&&r<=1440))return L("min_gap_minutes must be between 0 and 1440");let n=m(s=>(R(s,o.key,String(r)),E(s)));return We(n)}),t.registerTool("open_ui",{title:"Open UI",description:"Launch the devcoach web dashboard in the background. port must be 1024-65535.",inputSchema:{port:z$1.number().int().default(7860).describe("Port (default 7860)")},annotations:{title:"Open UI",destructiveHint:false,idempotentHint:false,openWorldHint:true}},o=>{let r=o.port;return r>=1024&&r<=65535?(spawn(process.execPath,[process.argv[1]??"","ui","--port",String(r)],{detached:true,stdio:"ignore"}).unref(),{content:[I(`devcoach UI starting at http://localhost:${r}`)]}):{content:[I(`error: port ${r} is out of valid range (1024-65535)`)]}}),t.registerTool("complete_onboarding",{title:"Complete Onboarding",description:"Save the user's initial knowledge profile and mark onboarding complete. Wipes any default-seeded profile. topics: {topic_id: confidence_0_to_10}. groups: {group_name: [topic_id,...]} (topics not in any group go to 'Other'). Returns the updated Profile.",inputSchema:{topics:z$1.record(z$1.string(),Le).describe("{topic_id: confidence 0-10}"),groups:z$1.record(z$1.string(),z$1.array(z$1.string())).optional().describe("{group_name: [topic_id, ...]}")},outputSchema:Xo,annotations:{title:"Complete Onboarding",destructiveHint:true,idempotentHint:false,openWorldHint:false}},async o=>{try{let r=m(n=>{n.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[s,a]of Object.entries(o.topics))O(n,s,Math.max(0,Math.min(10,a)));if(o.groups)for(let[s,a]of Object.entries(o.groups))for(let l of a)l in o.topics&&N(n,l,s);return M(n)});return We(r)}catch(r){return L(`complete_onboarding failed: ${r}`)}});let e=(o,r)=>({title:o,description:r,mimeType:"application/json"});return t.registerResource("profile","devcoach://profile",e("Knowledge Profile","Current knowledge map \u2014 topics, confidence scores, and groups."),o=>{try{return _(o,m(r=>M(r)))}catch(r){return _(o,{error:String(r)})}}),t.registerResource("settings","devcoach://settings",e("Settings","Current coaching settings (rate limits, UI theme)."),o=>{try{return _(o,m(r=>E(r)))}catch(r){return _(o,{error:String(r)})}}),t.registerResource("recent-lessons","devcoach://lessons/recent",e("Recent Lessons","Last 10 lessons from the current week."),o=>{try{return _(o,m(r=>B(r,{period:"week",page:1,per_page:10})))}catch(r){return _(o,[{error:String(r)}])}}),t.registerResource("stats","devcoach://stats",e("Stats","Aggregate coaching statistics: counts and weakest/strongest topics."),o=>{try{return _(o,m(r=>Z(r)))}catch(r){return _(o,{error:String(r)})}}),t.registerResource("taught-topics","devcoach://taught-topics",e("Taught Topics","All topic_ids already taught \u2014 read before selecting a new lesson topic."),o=>{try{return _(o,m(r=>bt(r)))}catch{return _(o,[])}}),t.registerResource("rate-limit","devcoach://rate-limit",e("Rate Limit","Current rate-limit status and running lesson total."),o=>{try{let{r,total:n}=m(a=>({r:z(a),total:X(a)})),s={allowed:r.allowed,total_lessons:n};return r.reason!=null&&(s.reason=r.reason),_(o,s)}catch(r){return _(o,{allowed:false,reason:`Rate limit check unavailable: ${r}`})}}),t.registerResource("context","devcoach://context",e("Workspace Context","Auto-detected git context and most-used lesson metadata defaults."),o=>{try{let r=Q(),n=m(s=>Ue(s));return _(o,{git:r,usage_defaults:n})}catch(r){return _(o,{error:String(r)})}}),t.registerResource("onboarding","devcoach://onboarding",e("Onboarding","Onboarding status, auto-detected stack, and project topic defaults."),o=>{try{let n=m(d=>de(d)).knowledge_ready,s=existsSync(w)&&statSync(w).size>0,a=Q(),l=_e(a.folder??process.cwd());return _(o,{knowledge_ready:n,notebook_ready:s,needs_onboarding:!(n&&s),detected_stack:l,default_topics:at,context_ready:a.branch!==null})}catch(r){return _(o,{error:String(r)})}}),t.registerResource("lesson",new ResourceTemplate("devcoach://lessons/{lesson_id}",{list:void 0}),e("Lesson","A single lesson by id."),(o,r)=>{try{let n=String(r.lesson_id),s=m(a=>K(a,n));return s?_(o,s):_(o,{error:`Lesson '${n}' not found`})}catch(n){return _(o,{error:String(n)})}}),t.registerPrompt("devcoach_instructions",{title:"devcoach coaching instructions",description:"Full coaching instructions for the devcoach skill (content of SKILL.md)."},()=>({messages:[{role:"user",content:{type:"text",text:vt()}}]})),t}async function $t(){let t=Jo(),e=new StdioServerTransport;await t.connect(e);}function ee(t){return JSON.stringify(t??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function $e(t){let e=(o,r,n)=>html`<a href="${o}" class="text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition ${n?"text-gray-900 dark:text-white font-semibold":""}">${r}</a>`;return html`<!DOCTYPE html>
|
|
49
|
+
ON CONFLICT(topic) DO UPDATE SET confidence = excluded.confidence, updated_at = excluded.updated_at`,e,r,n);}function J(t,e){let o=S(t,"DELETE FROM knowledge WHERE topic = ?",e);return S(t,"DELETE FROM knowledge_groups WHERE topic = ?",e),o>0}function P(t,e){let o=e.trim();if(!o)throw new Error("Group name must not be empty");return S(t,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",o)>0}function V(t,e){return S(t,"DELETE FROM knowledge_groups WHERE group_name = ?",e),S(t,"DELETE FROM knowledge_group_names WHERE group_name = ?",e)>0}function N(t,e,o){S(t,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",o),S(t,"DELETE FROM knowledge_groups WHERE topic = ?",e),S(t,"INSERT OR IGNORE INTO knowledge_groups (group_name, topic) VALUES (?, ?)",o,e);}function ke(t,e){S(t,"DELETE FROM knowledge_groups WHERE topic = ?",e);}function E(t){let e={};for(let d of j(t,"SELECT key, value FROM settings"))e[d.key]=d.value;let o=e.min_gap_minutes,r=e.min_hours_between,n;o!==void 0?n=Number.parseInt(o,10):r!==void 0?n=Number.parseInt(r,10)*60:n=240;let s=e.max_per_day,a=e.ui_theme??"system",l=a==="dark"||a==="light"||a==="system"?a:"system";return {max_per_day:s!==void 0?Number.parseInt(s,10):2,min_gap_minutes:n,ui_theme:l}}function R(t,e,o){S(t,"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",e,o);}function de(t){let e=G(t,"SELECT COUNT(*) AS n FROM knowledge");return {knowledge_ready:Number(e?.n??0)>0}}function Ue(t){let e={};for(let o of ["project","repository","branch","repository_platform"]){let r=G(t,`SELECT ${o} AS v, COUNT(*) AS c FROM lessons WHERE ${o} IS NOT NULL GROUP BY ${o} ORDER BY c DESC LIMIT 1`);e[o]=r?r.v:null;}return e}function ve(t){let e=E(t),o=se(t),r=ae(t),n=He(t),s=new Map;for(let d of n)for(let f of d.topics)s.set(f,d.name);let a={groups:n.map(d=>d.name),topics:r.map(d=>({topic:d.topic,confidence:d.confidence,group:s.get(d.topic)??null}))},l={[Oe]:strToU8(JSON.stringify(e,null,2)),[De]:strToU8(JSON.stringify(o,null,2)),[Ne]:strToU8(JSON.stringify(a,null,2))};return existsSync(w)&&(l[je]=strToU8(readFileSync(w,"utf8"))),zipSync(l)}function ho(t,e,o){if(!(Oe in e))return;let r=JSON.parse(strFromU8(e[Oe]));"max_per_day"in r&&R(t,"max_per_day",String(r.max_per_day)),"min_gap_minutes"in r&&R(t,"min_gap_minutes",String(r.min_gap_minutes)),(r.ui_theme==="system"||r.ui_theme==="dark"||r.ui_theme==="light")&&R(t,"ui_theme",r.ui_theme),o.settings=1;}function xo(t,e,o){if(!(Ne in e))return;let r=JSON.parse(strFromU8(e[Ne]));if(!(r&&typeof r=="object"&&Array.isArray(r.topics))){for(let[s,a]of Object.entries(r))O(t,s,a);o.topics=Object.keys(r).length;return}let n=0;for(let s of r.groups??[])P(t,s)&&(n+=1);for(let s of r.topics){O(t,s.topic,s.confidence);let a=s.group;a&&a!=="Other"&&(P(t,a)&&(n+=1),N(t,s.topic,a));}o.topics=r.topics.length,o.groups=n;}function ko(t,e,o){if(!(De in e))return;let r=JSON.parse(strFromU8(e[De])),{inserted:n,duplicated:s,invalid:a}=Me(t,r);o.lessons=n,o.skipped=s,o.invalid=a;}function vo(t,e){je in t&&(mkdirSync(dirname(w),{recursive:true}),writeFileSync(w,strFromU8(t[je]),"utf8"),e.learning_state=1);}function ce(t,e){let o={settings:0,topics:0,groups:0,lessons:0,skipped:0,invalid:0,learning_state:0},r=unzipSync(e);return ho(t,r,o),xo(t,r,o),ko(t,r,o),vo(r,o),o}function wo(t){let e=new Date,o;if(t==="today")o=new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),0,0,0,0));else if(t==="week")o=new Date(e.getTime()-7*864e5);else if(t==="month")o=new Date(e.getTime()-30*864e5);else if(t==="year")o=new Date(e.getTime()-365*864e5);else return null;return o.toISOString()}function Ge(t){let e=[];try{let o=JSON.parse(t.categories);Array.isArray(o)&&(e=o);}catch{e=[];}return re({...t,categories:e,starred:!!t.starred})}function z(t){try{let e=E(t),o=new Date,r=new Date(o.getTime()-24*36e5).toISOString(),n=xe(t,r);if(n>=e.max_per_day)return {allowed:!1,reason:`Daily limit reached (${n}/${e.max_per_day} lessons in the last 24h)`};let s=ft(t);if(s!=null){let a=new Date(s),l=(o.getTime()-a.getTime())/6e4;if(l<e.min_gap_minutes){let d=e.min_gap_minutes-l,f=Math.floor(e.min_gap_minutes/60),b=e.min_gap_minutes%60,y=Math.trunc(d),k=Math.floor(y/60),v=y%60;return {allowed:!1,reason:`Too soon: last lesson ${l<0?`in ${Math.round(-l)}m (future timestamp)`:`${Math.round(l)}m ago`}, minimum interval is ${f}h ${b}m (${k}h ${v}m remaining)`}}}return {allowed:!0}}catch(e){return {allowed:true,reason:`Rate limit check failed: ${e}`}}}function M(t){try{return {knowledge:ae(t),groups:He(t)}}catch{return {knowledge:[],groups:[]}}}function q(t,e,o){let r=Fe(t)[e]??5,n=Math.max(0,Math.min(10,r+o));return O(t,e,n),n}function we(t,e,o){let r=ne(t,e,o);return r&&(o==="know"||o==="dont_know")&&q(t,r,o==="know"?1:-1),r}function Z(t){try{let e=new Date,o=X(t),r=new Date(e.getTime()-24*36e5).toISOString(),n=new Date(e.getTime()-7*864e5).toISOString(),s=xe(t,r),a=xe(t,n),l=Fe(t),d=Object.entries(l).sort((y,k)=>y[1]-k[1]),f=d.slice(0,5).map(([y,k])=>({topic:y,confidence:k})),b=d.slice(-5).reverse().map(([y,k])=>({topic:y,confidence:k}));return {total_lessons:o,lessons_today:s,lessons_this_week:a,weakest_topics:f,strongest_topics:b}}catch(e){return {error:String(e)}}}function bt(t){try{return mt(t)}catch{return []}}var To=[["package.json","javascript",6],["tsconfig*.json","typescript",6],["requirements*.txt","python",6],["pyproject.toml","python",6],["setup.py","python",6],["go.mod","go",6],["Cargo.toml","rust",6],["pom.xml","java",6],["build.gradle","java",6],["build.gradle.kts","java",6],["*.csproj","csharp",6],["Gemfile","ruby",6],["composer.json","php",6],["Dockerfile","docker",7],["Dockerfile.*","docker",7],["docker-compose.yml","docker_compose",7],["docker-compose.yaml","docker_compose",7],["docker-compose.*.yml","docker_compose",7],[".github/workflows/*.yml","github_actions",6],[".github/workflows/*.yaml","github_actions",6],[".gitlab-ci.yml","gitlab_ci",6],["Jenkinsfile","ci_cd",6],["*.tf","terraform",6],["*.sql","sql",5],[".git","git",7],["kubernetes/*.yaml","kubernetes",6],["kubernetes/*.yml","kubernetes",6],["k8s/*.yaml","kubernetes",6],["*.bicep","azure",6],["serverless.yml","aws",6],["serverless.yaml","aws",6],["template.yaml","aws",5],["angular.json","angular",6],["svelte.config.*","svelte",6],["nuxt.config.*","vue",6],["vite.config.*","javascript",6]],Eo={react:"react","react-dom":"react",vue:"vue","@angular/core":"angular",svelte:"svelte",next:"nextjs",nuxt:"vue",fastify:"fastify",express:"express",koa:"express","@nestjs/core":"node_js"},Co={django:"django",flask:"flask",fastapi:"fastapi",starlette:"fastapi",tornado:"python",aiohttp:"python",sqlalchemy:"postgresql",celery:"python"};function Ro(t){let e="";for(let o of t)o==="*"?e+="[^/]*":o==="?"?e+="[^/]":e+=o.replace(/[.+^${}()|[\]\\]/g,String.raw`\$&`);return new RegExp(`^${e}$`)}function Lo(t,e){let o=e.lastIndexOf("/"),r=o>=0?join(t,e.slice(0,o)):t,n=o>=0?e.slice(o+1):e,s;try{s=readdirSync(r);}catch{return false}let a=Ro(n);return s.some(l=>a.test(l))}function yt(t){try{return existsSync(t)?readFileSync(t,"utf8"):null}catch{return null}}function Io(t,e){let o=yt(join(t,"package.json"));if(!o)return;let r;try{r=JSON.parse(o);}catch{return}let n=new Set([...Object.keys(r.dependencies??{}),...Object.keys(r.devDependencies??{})]);for(let[s,a]of Object.entries(Eo))n.has(s)&&e(a,6);}function Oo(t,e){let o=["requirements.txt","requirements-dev.txt","requirements/base.txt","pyproject.toml"];for(let r of o){let n=yt(join(t,r))?.toLowerCase();if(n)for(let[s,a]of Object.entries(Co))n.includes(s)&&e(a,6);}}function _e(t){let e={},o=(r,n)=>{(e[r]??-1)<n&&(e[r]=n);};for(let[r,n,s]of To)Lo(t,r)&&o(n,s);return Io(t,o),Oo(t,o),e}function qe(...t){try{return execFileSync("git",t,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}}var ht={"github.com":"github","gitlab.com":"gitlab","bitbucket.org":"bitbucket"};function jo(t){if(!t)return [null,null];let e=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(t);if(e){let r=(e[1]??"").toLowerCase();return [e[2]??t,ht[r]??"local"]}let o=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(t);if(o){let r=(o[1]??"").toLowerCase();return [o[2]??t,ht[r]??"local"]}return [t,"local"]}function Q(){let t=process.cwd(),e=qe("rev-parse","--abbrev-ref","HEAD"),o=qe("rev-parse","HEAD"),r=qe("remote","get-url","origin"),[n,s]=jo(r),a;return n?a=n.replace(/\/+$/,"").split("/").pop()??null:a=basename(t)||null,e==="HEAD"&&(e=null),{project:a,repository:n,branch:e,commit_hash:o,folder:t,repository_platform:s}}var kt=dirname(fileURLToPath(import.meta.url)),Fo=[join(kt,"../assets/SKILL.md"),join(kt,"../../assets/SKILL.md")];function vt(){for(let t of Fo)try{return readFileSync(t,"utf8")}catch{}return "devcoach: coaching instructions unavailable (SKILL.md not found in package)."}var wt={version:"0.3.65"};var le=wt.version;var I=t=>({type:"text",text:t}),_t=t=>({content:[I(JSON.stringify(t))]}),L=t=>({isError:true,content:[I(t)]}),We=t=>({content:[I(JSON.stringify(t))],structuredContent:t}),Wo={id:z$1.string(),timestamp:z$1.string(),topic_id:z$1.string(),categories:z$1.array(z$1.string()),title:z$1.string(),level:me,summary:z$1.string(),body:z$1.string().nullable(),task_context:z$1.string().nullable(),project:z$1.string().nullable(),repository:z$1.string().nullable(),branch:z$1.string().nullable(),commit_hash:z$1.string().nullable(),folder:z$1.string().nullable(),repository_platform:fe.nullable(),starred:z$1.boolean(),feedback:Re.nullable()},Xo={knowledge:z$1.array(z$1.object({topic:z$1.string(),confidence:z$1.number().int()})),groups:z$1.array(z$1.object({name:z$1.string(),topics:z$1.array(z$1.string())}))},Yo={max_per_day:z$1.number().int(),min_gap_minutes:z$1.number().int(),ui_theme:Ie};function _(t,e){return {contents:[{uri:t.href,mimeType:"application/json",text:JSON.stringify(e)}]}}function Jo(){let t=new McpServer({name:"devcoach",version:le},{instructions:"Progressive technical coaching server. Use the devcoach_instructions prompt for full coaching behaviour guidelines."});t.registerTool("log_lesson",{title:"Log Lesson",description:"Save a delivered lesson to the coaching log. Git metadata (project, repository, branch, commit_hash, folder, repository_platform) is auto-detected from the workspace when omitted. Returns the saved Lesson with all resolved fields.",inputSchema:{id:z$1.string().describe("Unique lesson id (uuid or random string)"),timestamp:z$1.string().describe("ISO 8601 timestamp; normalized to UTC and clamped to now"),topic_id:z$1.string().describe("Primary topic id, e.g. 'python'"),categories:z$1.array(z$1.string()).describe("Category tags, e.g. ['python','performance']"),title:z$1.string().describe("Short lesson title"),level:me.describe("Difficulty level: junior | mid | senior"),summary:z$1.string().describe("1-3 sentence summary shown in the lesson card"),body:z$1.string().nullish().describe("Optional full lesson markdown"),task_context:z$1.string().nullish().describe("What the user was doing when taught"),project:z$1.string().nullish().describe("Project name (auto-detected from git if omitted)"),repository:z$1.string().nullish().describe("org/repo or path (auto-detected if omitted)"),branch:z$1.string().nullish().describe("git branch (auto-detected if omitted)"),commit_hash:z$1.string().nullish().describe("git commit hash (auto-detected if omitted)"),folder:z$1.string().nullish().describe("workspace folder (auto-detected if omitted)"),repository_platform:fe.nullish().describe("github | gitlab | bitbucket | local (auto-detected if omitted)")},outputSchema:Wo,annotations:{title:"Log Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=Q(),n={};try{n=m(a=>Ue(a));}catch{n={};}let s=re({id:o.id,timestamp:o.timestamp,topic_id:o.topic_id,categories:o.categories,title:o.title,level:o.level,summary:o.summary,body:o.body??null,task_context:o.task_context??null,project:o.project??r.project??n.project??null,repository:o.repository??r.repository??n.repository??null,branch:o.branch??r.branch??n.branch??null,commit_hash:o.commit_hash??r.commit_hash??null,folder:o.folder??r.folder??null,repository_platform:o.repository_platform??r.repository_platform??n.repository_platform??null});m(a=>pt(a,s));try{if(t.server.getClientCapabilities()?.elicitation){let l=await t.server.elicitInput({message:"Did that land?",requestedSchema:{type:"object",properties:{feedback:{type:"string",title:"Did that land?",description:"know = got it \xB7 dont_know = need to revisit \xB7 skip = no feedback",enum:["know","dont_know","skip"]}},required:[]}}),d=l.action==="accept"?l.content?.feedback:void 0;d&&d!=="skip"&&(m(f=>{ne(f,o.id,d),q(f,o.topic_id,d==="know"?1:-1);}),s={...s,feedback:d});}}catch{}return We(s)}catch(r){return L(`log_lesson failed: ${r}`)}}),t.registerTool("update_knowledge",{title:"Update Knowledge",description:"Adjust the confidence score for a topic by delta (e.g. +1 or -1). Returns the new confidence (0-10). Creates the topic at confidence 5 if it does not exist.",inputSchema:{topic:z$1.string().describe("Topic id, e.g. 'python'"),delta:z$1.number().int().describe("Signed change to apply, e.g. +1 or -1")},annotations:{title:"Update Knowledge",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>q(n,o.topic,o.delta));return {content:[I(String(r))]}}catch(r){return L(`update_knowledge failed for '${o.topic}': ${r}`)}}),t.registerTool("get_lessons",{title:"Get Lessons",description:"Query the coaching lesson history. All filters combine. period defaults to all; date_from/date_to override period; limit caps results newest-first (0 = all).",inputSchema:{period:z$1.enum(["today","week","month","year","all"]).nullish().describe("Time window"),category:z$1.string().nullish().describe("Filter by a category tag, e.g. 'python'"),level:z$1.enum(["junior","mid","senior"]).nullish().describe("Filter by difficulty level"),project:z$1.string().nullish().describe("Fuzzy match on git project"),repository:z$1.string().nullish().describe("Fuzzy match on git repository"),branch:z$1.string().nullish().describe("Fuzzy match on git branch"),commit:z$1.string().nullish().describe("Fuzzy match on commit hash"),starred:z$1.boolean().nullish().describe("True to return only starred lessons"),feedback:z$1.enum(["know","dont_know","none"]).nullish().describe("Filter by feedback ('none' = no response)"),search:z$1.string().nullish().describe("Full-text search over title, topic_id, summary, body"),date_from:z$1.string().nullish().describe("ISO date/datetime lower bound"),date_to:z$1.string().nullish().describe("ISO date/datetime upper bound (date-only = end-of-day)"),limit:z$1.number().int().default(10).describe("Max lessons, newest first. Pass 0 for all.")},annotations:{title:"Get Lessons",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}},o=>{try{let r=m(n=>B(n,{period:o.period,category:o.category,level:o.level,project:o.project,repository:o.repository,branch:o.branch,commit:o.commit,starred:o.starred,feedback:o.feedback,search:o.search,date_from:o.date_from,date_to:o.date_to,page:o.limit>0?1:null,per_page:o.limit}));return _t(r)}catch{return _t([])}}),t.registerTool("star_lesson",{title:"Star Lesson",description:"Set the starred (favourite) flag on a lesson. Returns true if found and updated. Idempotent.",inputSchema:{lesson_id:z$1.string().describe("Lesson id"),starred:z$1.boolean().describe("true to favourite, false to unmark")},annotations:{title:"Star Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>Y(n,o.lesson_id,o.starred));return {content:[I(String(r))]}}catch(r){return L(`star_lesson failed for '${o.lesson_id}': ${r}`)}}),t.registerTool("delete_lesson",{title:"Delete Lesson",description:"Permanently delete a lesson by id. Returns true if found and deleted.",inputSchema:{lesson_id:z$1.string().describe("Lesson id to delete")},annotations:{title:"Delete Lesson",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>he(n,o.lesson_id));return {content:[I(String(r))]}}catch(r){return L(`delete_lesson failed for '${o.lesson_id}': ${r}`)}}),t.registerTool("submit_feedback",{title:"Submit Feedback",description:"Record comprehension feedback for a lesson and adjust knowledge confidence. know = +1, dont_know = -1, clear = remove feedback (no confidence change). Idempotent \u2014 the same feedback twice adjusts confidence only once.",inputSchema:{lesson_id:z$1.string().describe("Lesson id"),feedback:z$1.enum(["know","dont_know","clear"]).describe("know | dont_know | clear")},annotations:{title:"Submit Feedback",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=o.feedback==="clear"?null:o.feedback,n=m(s=>{let a=s.prepare("SELECT feedback, topic_id FROM lessons WHERE id = ?").get(o.lesson_id);return a?(a.feedback===r||(ne(s,o.lesson_id,r),(r==="know"||r==="dont_know")&&a.topic_id&&q(s,a.topic_id,r==="know"?1:-1)),!0):!1});return {content:[I(String(n))]}}catch(r){return L(`submit_feedback failed for '${o.lesson_id}': ${r}`)}}),t.registerTool("add_topic",{title:"Add Topic",description:"Add a topic to the knowledge map, or update its confidence if it exists. Optionally assign to a group (auto-created). Prefer a single-word topic id. Idempotent.",inputSchema:{topic:z$1.string().describe("Topic id \u2014 prefer a single word, max 3 words"),confidence:Le.default(5).describe("Initial confidence 0-10 (default 5)"),group:z$1.string().nullish().describe("Optional group name; 'Other' if omitted")},annotations:{title:"Add Topic",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{return m(r=>{O(r,o.topic,o.confidence),o.group&&o.group!=="Other"&&N(r,o.topic,o.group);}),{content:[I("true")]}}catch(r){return L(`add_topic failed for '${o.topic}': ${r}`)}}),t.registerTool("remove_topic",{title:"Remove Topic",description:"Remove a topic from the knowledge map entirely. Returns true if it existed.",inputSchema:{topic:z$1.string().describe("Topic id to remove")},annotations:{title:"Remove Topic",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>J(n,o.topic));return {content:[I(String(r))]}}catch(r){return L(`remove_topic failed for '${o.topic}': ${r}`)}}),t.registerTool("add_group",{title:"Add Group",description:"Create a new (initially empty) knowledge group. Idempotent \u2014 returns true either way.",inputSchema:{name:z$1.string().describe("Group name, e.g. 'Machine Learning'")},annotations:{title:"Add Group",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{return m(r=>P(r,o.name.trim())),{content:[I("true")]}}catch(r){return L(`add_group failed for '${o.name}': ${r}`)}}),t.registerTool("remove_group",{title:"Remove Group",description:"Delete a knowledge group. Its topics move to Other. Returns true if it existed.",inputSchema:{name:z$1.string().describe("Group name to delete")},annotations:{title:"Remove Group",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>V(n,o.name));return {content:[I(String(r))]}}catch(r){return L(`remove_group failed for '${o.name}': ${r}`)}}),t.registerTool("update_settings",{title:"Update Settings",description:"Update a coaching setting. max_per_day: integer 1-20. min_gap_minutes: integer 0-1440 (0 = no cooldown). Returns the full updated Settings.",inputSchema:{key:z$1.enum(["max_per_day","min_gap_minutes"]).describe("Setting key"),value:z$1.string().describe("New value as an integer string")},outputSchema:Yo,annotations:{title:"Update Settings",destructiveHint:false,idempotentHint:true,openWorldHint:false}},o=>{let r=Number.parseInt(o.value,10);if(Number.isNaN(r))return L(`Value must be an integer, got '${o.value}'`);if(o.key==="max_per_day"&&!(r>=1&&r<=20))return L("max_per_day must be between 1 and 20");if(o.key==="min_gap_minutes"&&!(r>=0&&r<=1440))return L("min_gap_minutes must be between 0 and 1440");let n=m(s=>(R(s,o.key,String(r)),E(s)));return We(n)}),t.registerTool("open_ui",{title:"Open UI",description:"Launch the devcoach web dashboard in the background. port must be 1024-65535.",inputSchema:{port:z$1.number().int().default(7860).describe("Port (default 7860)")},annotations:{title:"Open UI",destructiveHint:false,idempotentHint:false,openWorldHint:true}},o=>{let r=o.port;return r>=1024&&r<=65535?(spawn(process.execPath,[process.argv[1]??"","ui","--port",String(r)],{detached:true,stdio:"ignore"}).unref(),{content:[I(`devcoach UI starting at http://localhost:${r}`)]}):{content:[I(`error: port ${r} is out of valid range (1024-65535)`)]}}),t.registerTool("complete_onboarding",{title:"Complete Onboarding",description:"Save the user's initial knowledge profile and mark onboarding complete. Wipes any default-seeded profile. topics: {topic_id: confidence_0_to_10}. groups: {group_name: [topic_id,...]} (topics not in any group go to 'Other'). Returns the updated Profile.",inputSchema:{topics:z$1.record(z$1.string(),Le).describe("{topic_id: confidence 0-10}"),groups:z$1.record(z$1.string(),z$1.array(z$1.string())).optional().describe("{group_name: [topic_id, ...]}")},outputSchema:Xo,annotations:{title:"Complete Onboarding",destructiveHint:true,idempotentHint:false,openWorldHint:false}},async o=>{try{let r=m(n=>{n.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[s,a]of Object.entries(o.topics))O(n,s,Math.max(0,Math.min(10,a)));if(o.groups)for(let[s,a]of Object.entries(o.groups))for(let l of a)l in o.topics&&N(n,l,s);return M(n)});return We(r)}catch(r){return L(`complete_onboarding failed: ${r}`)}});let e=(o,r)=>({title:o,description:r,mimeType:"application/json"});return t.registerResource("profile","devcoach://profile",e("Knowledge Profile","Current knowledge map \u2014 topics, confidence scores, and groups."),o=>{try{return _(o,m(r=>M(r)))}catch(r){return _(o,{error:String(r)})}}),t.registerResource("settings","devcoach://settings",e("Settings","Current coaching settings (rate limits, UI theme)."),o=>{try{return _(o,m(r=>E(r)))}catch(r){return _(o,{error:String(r)})}}),t.registerResource("recent-lessons","devcoach://lessons/recent",e("Recent Lessons","Last 10 lessons from the current week."),o=>{try{return _(o,m(r=>B(r,{period:"week",page:1,per_page:10})))}catch(r){return _(o,[{error:String(r)}])}}),t.registerResource("stats","devcoach://stats",e("Stats","Aggregate coaching statistics: counts and weakest/strongest topics."),o=>{try{return _(o,m(r=>Z(r)))}catch(r){return _(o,{error:String(r)})}}),t.registerResource("taught-topics","devcoach://taught-topics",e("Taught Topics","All topic_ids already taught \u2014 read before selecting a new lesson topic."),o=>{try{return _(o,m(r=>bt(r)))}catch{return _(o,[])}}),t.registerResource("rate-limit","devcoach://rate-limit",e("Rate Limit","Current rate-limit status and running lesson total."),o=>{try{let{r,total:n}=m(a=>({r:z(a),total:X(a)})),s={allowed:r.allowed,total_lessons:n};return r.reason!=null&&(s.reason=r.reason),_(o,s)}catch(r){return _(o,{allowed:false,reason:`Rate limit check unavailable: ${r}`})}}),t.registerResource("context","devcoach://context",e("Workspace Context","Auto-detected git context and most-used lesson metadata defaults."),o=>{try{let r=Q(),n=m(s=>Ue(s));return _(o,{git:r,usage_defaults:n})}catch(r){return _(o,{error:String(r)})}}),t.registerResource("onboarding","devcoach://onboarding",e("Onboarding","Onboarding status, auto-detected stack, and project topic defaults."),o=>{try{let n=m(d=>de(d)).knowledge_ready,s=existsSync(w)&&statSync(w).size>0,a=Q(),l=_e(a.folder??process.cwd());return _(o,{knowledge_ready:n,notebook_ready:s,needs_onboarding:!(n&&s),detected_stack:l,default_topics:at,context_ready:a.branch!==null})}catch(r){return _(o,{error:String(r)})}}),t.registerResource("lesson",new ResourceTemplate("devcoach://lessons/{lesson_id}",{list:void 0}),e("Lesson","A single lesson by id."),(o,r)=>{try{let n=String(r.lesson_id),s=m(a=>K(a,n));return s?_(o,s):_(o,{error:`Lesson '${n}' not found`})}catch(n){return _(o,{error:String(n)})}}),t.registerPrompt("devcoach_instructions",{title:"devcoach coaching instructions",description:"Full coaching instructions for the devcoach skill (content of SKILL.md)."},()=>({messages:[{role:"user",content:{type:"text",text:vt()}}]})),t}async function $t(){let t=Jo(),e=new StdioServerTransport;await t.connect(e);}function ee(t){return JSON.stringify(t??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function $e(t){let e=(o,r,n)=>html`<a href="${o}" class="text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition ${n?"text-gray-900 dark:text-white font-semibold":""}">${r}</a>`;return html`<!DOCTYPE html>
|
|
50
50
|
<html lang="en">
|
|
51
51
|
<head>
|
|
52
52
|
<meta charset="UTF-8" />
|