devcoach 2.8.0-next.671.gdec5426 → 2.8.0-next.673.g14f8bb5

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 CHANGED
@@ -18,572 +18,96 @@
18
18
 
19
19
  devcoach connects to Claude Code, Claude Desktop, Gemini CLI *(beta)*, Codex CLI *(beta)*, Cursor, Windsurf, and other MCP-compatible tools. When your agent finishes a task, devcoach teaches you one thing from it **that you don't know yet**: it keeps a map of what you already know precisely so it can skip it, and pitches each lesson at your level — no generic tutorials, no repeated topics, nothing to open.
20
20
 
21
- Everything runs **locally**. No data leaves your machine. One SQLite file at `~/.devcoach/coaching.db`.
22
-
23
- > Built on the official [Model Context Protocol TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk), Node's embedded `node:sqlite`, [Hono](https://hono.dev) (web dashboard), and [Commander](https://github.com/tj/commander.js) (CLI). Requires **Node.js ≥ 24**.
24
-
25
- ---
26
-
27
- ## Why devcoach?
28
-
29
- AI agents now write much of our code — which makes it easy to ship more while understanding less. devcoach turns the tasks your agent finishes into short, in-context lessons on what you don't know yet, so you keep learning as you go. The deeper bet: as AI grows more capable, the scarce skill becomes *validating* what it produces — and that ability only survives if you keep practicing it.
30
-
31
- → [Why devcoach exists](docs/why.md)
32
-
33
- ---
34
-
35
- ## How it works
36
-
37
- ```mermaid
38
- flowchart TD
39
- A([Task completed]) --> B{"stop-hook:<br/>paced + rate limit ok?"}
40
- B -->|not yet| Z(["Silent — counter +1"])
41
- B -->|due| C["Cue: activate the devcoach skill"]
42
-
43
- subgraph skill["devcoach skill"]
44
- C --> D{"Technical work?"}
45
- D -->|no| S[skip_lesson]
46
- D -->|yes| E[Select topic & depth]
47
- E --> G[log_lesson]
48
- G --> F[Print lesson card]
49
- end
50
-
51
- S --> Y(["Silent — pacing re-armed"])
52
- F --> H(["Done — counter reset"])
53
- F -.->|prompts| U(["You: ✅ / 💡 / ❌"])
54
- ```
55
-
56
- → [Full decision flow: session startup · lesson selection · depth calibration](docs/how-it-works.md)
57
-
58
- ---
59
-
60
- ## Privacy by design
61
-
62
- Everything stays on your machine. No telemetry, no accounts, no calls home. Just one SQLite file.
21
+ <p align="center">
22
+ <img src="docs/screenshots/lesson-loop.gif" alt="A Claude Code session: the agent adds a Redis cache, then devcoach appends a short lesson on cache stampedes and the developer answers with one key" width="720">
23
+ </p>
63
24
 
64
- → [Privacy & security](docs/reference/privacy.md)
25
+ Everything runs **locally**. No data leaves your machine. One SQLite file at `~/.devcoach/coaching.db`.
65
26
 
66
27
  ---
67
28
 
68
- ## Installation
69
-
70
- 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**, **Claude Desktop**, **Gemini CLI** *(beta)*, and **Codex CLI** *(beta)*, but **not** on claude.ai web (which only supports hosted/remote connectors).
71
-
72
- **Requires Node.js ≥ 24** (devcoach uses the embedded `node:sqlite` module, available only from Node 24 onward).
73
-
74
- **Pick by how you like to work** — each section is self-contained (install **and** connect):
29
+ ## Install
75
30
 
76
- - **Developer, comfortable in a terminal → [Homebrew](#homebrew-recommended-for-developers)** — one `brew install`, and you get the `devcoach` CLI too.
77
- - **Prefer a one-click, no-terminal setup → the [Claude Code plugin](#claude-code-plugin-recommended-for-claude-code) or the [`.mcpb` extension](#claude-desktop-extension-mcpb-recommended-for-claude-desktop)**.
78
- - **Registry-aware MCP client?** devcoach is listed in the official [MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.UltimaPhoenix/devcoach` — install it straight from there.
79
- - Anything else (npx, manual config, other agents, claude.ai web) is under **[Other install methods](#other-install-methods)**.
31
+ Pick your agent. Each command wires up everything: the MCP server, the coaching hooks and the skill. Requires **Node.js ≥ 24**.
80
32
 
81
- ### Homebrew (recommended for developers)
82
-
83
- macOS / Linux. Add and trust the tap once, install, then connect — you also get the **`devcoach` CLI** (so [`devcoach ui`](#web-dashboard) and the [CLI](docs/usage/cli.md) work without an `npx` prefix):
33
+ **Claude Code** — the plugin (recommended):
84
34
 
85
35
  ```bash
86
- # 1. Add the tap — registers github.com/UltimaPhoenix/homebrew-tap with Homebrew
87
- brew tap UltimaPhoenix/tap
88
-
89
- # 2. Trust the whole tap — required when Homebrew enforces HOMEBREW_REQUIRE_TAP_TRUST
90
- brew trust --tap UltimaPhoenix/tap
91
-
92
- # 3. Install
93
- brew install devcoach
94
-
95
- # 4. Connect (Homebrew puts `devcoach` on your PATH — no `npx -y` prefix needed)
96
- devcoach install
97
- ```
98
-
99
- `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`, then `devcoach install` again — Homebrew updates only the binary, the hooks and the skill it wired into your agents are refreshed by that second command (the [Claude Code plugin](#claude-code-plugin-recommended-for-claude-code) does this automatically, which is why **plugin + Homebrew** is the nicest combination for Claude Code users: the plugin owns the coaching wiring, Homebrew adds the CLI — and then you skip `devcoach install`). 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.
100
-
101
- ### Claude Code plugin (recommended for Claude Code)
102
-
103
- The simplest way to add devcoach to **Claude Code** — one click, nothing to configure. It bundles **everything** (MCP server + automatic-coaching Stop hooks + skill), so **don't also run `devcoach install`** (or the Stop hooks get registered twice).
104
-
105
- ```bash
106
- # Add the marketplace once, then install (you can install any UltimaPhoenix plugin from it later)
107
36
  /plugin marketplace add UltimaPhoenix/claude-plugins-marketplace
108
37
  /plugin install devcoach@ultimaphoenix
109
38
  ```
110
39
 
111
- The MCP server, hooks, and skill activate on install — no restart needed. Open the [web dashboard](#web-dashboard) right from Claude Code with `/devcoach:ui` (or just ask — the `open_ui` tool); `/devcoach:share` and `/devcoach:import` hand lessons around; `/devcoach:course` turns a lesson you couldn't follow into a step-by-step course. Update later with `/plugin marketplace update ultimaphoenix`. Want what's coming next? The [beta channel](https://ultimaphoenix.github.io/dev-coach/install/claude-code-plugin#beta-channel) installs the latest canary as `devcoach@ultimaphoenix-beta`.
112
-
113
- The plugin **does not** add the `devcoach` CLI, but the CLI stays a handy companion when you want the dashboard or your data without going through Claude — `devcoach ui`, `stats`, `lessons`, `share` / `import`, `backup`: run them with `npx -y devcoach …`, or install Homebrew / `npm i -g devcoach` for a bare `devcoach`. Just never run `devcoach install` next to the plugin — it owns the hooks.
114
-
115
- <details>
116
- <summary><strong>Offline install</strong> (no marketplace access)</summary>
40
+ **Homebrew** — macOS / Linux, adds the `devcoach` CLI:
117
41
 
118
42
  ```bash
119
- # Download devcoach-plugin-<version>.zip from a GitHub Release, unzip, then:
120
- /plugin marketplace add /path/to/unzipped-folder
121
- /plugin install devcoach@devcoach
43
+ brew tap UltimaPhoenix/tap && brew trust --tap UltimaPhoenix/tap
44
+ brew install devcoach && devcoach install
122
45
  ```
123
46
 
124
- </details>
125
-
126
- See [Claude Code plugin](docs/install/claude-code-plugin.md) for how it works.
127
-
128
- ### Claude Desktop extension (`.mcpb`) (recommended for Claude Desktop) — **Beta**
129
-
130
- A single bundle that runs on Claude Desktop's built-in runtime — no Node or terminal needed. Every GitHub Release ships one:
131
-
132
- 1. Download `devcoach-<version>.mcpb` from the [latest release](https://github.com/UltimaPhoenix/dev-coach/releases/latest)
133
- 2. Claude Desktop → **Settings → Extensions → Install Extension…** → pick the `.mcpb`
134
-
135
- The bundle is **self-signed**, so Claude Desktop installs it as an *unverified publisher* (a real code-signing cert would be needed for a verified signature). Prefer to build it from source?
136
-
137
- ```bash
138
- npm run mcpb # → dist-mcpb/devcoach-<version>.mcpb (npm run mcpb:sign self-signs it)
139
- ```
140
-
141
- ### Other install methods — **Beta**
142
-
143
- <details>
144
- <summary><strong>npx / npm CLI</strong> (any MCP agent — no install)</summary>
145
-
146
- No install required — `npx` runs devcoach on demand. For **Claude Code** and **Claude Desktop**, one command registers the MCP server, wires up automatic lesson delivery (Stop hooks), and installs the coaching **skill** into `~/.claude/skills/devcoach/`:
47
+ **npx** — any OS, for Claude Code and Claude Desktop:
147
48
 
148
49
  ```bash
149
50
  npx -y devcoach install
150
51
  ```
151
52
 
152
- Restart your agent afterward. Prefer a global binary? `npm install -g devcoach`, then run `devcoach install` (and drop the `npx -y` prefix everywhere). After upgrading devcoach, re-run `devcoach install` to refresh the skill — `devcoach stats` reminds you when it's out of date.
153
-
154
- **Canary builds:** every change merged into the `develop` branch is published to npm under the `next` tag — `npx -y devcoach@next` (or `npm install -g devcoach@next`) runs the unreleased version, documented at [ultimaphoenix.github.io/dev-coach/next](https://ultimaphoenix.github.io/dev-coach/next/). `latest` stays the last release.
155
-
156
- </details>
157
-
158
- <details>
159
- <summary><strong>Manual MCP config for Claude Code</strong> (if <code>devcoach install</code> isn't available)</summary>
160
-
161
- **Option A — via the `claude mcp` CLI (recommended):**
162
-
163
- ```bash
164
- claude mcp add devcoach npx -- -y devcoach mcp
165
-
166
- # all projects (user scope)
167
- claude mcp add --scope user devcoach npx -- -y devcoach mcp
168
- ```
169
-
170
- **Option B — edit `~/.claude.json` directly:**
171
-
172
- ```json
173
- { "mcpServers": { "devcoach": { "type": "stdio", "command": "npx", "args": ["-y", "devcoach", "mcp"] } } }
174
- ```
175
-
176
- Then add the hooks to `~/.claude/settings.json` for automatic lesson delivery — `stop-hook` decides
177
- after each turn whether a lesson is due, `prompt-hook` primes the model up front when one is:
178
-
179
- ```json
180
- {
181
- "hooks": {
182
- "Stop": [
183
- { "hooks": [{ "type": "command", "command": "npx -y devcoach stop-hook", "timeout": 60 }] }
184
- ],
185
- "UserPromptSubmit": [
186
- { "hooks": [{ "type": "command", "command": "npx -y devcoach prompt-hook", "timeout": 30 }] }
187
- ]
188
- }
189
- }
190
- ```
191
-
192
- > Tip: a global install puts `devcoach` on your `PATH`, so you can drop the `npx -y` prefix. `devcoach install` detects this automatically.
193
- > **Using the Claude Code plugin (above)?** Skip the hooks here — the plugin already provides them.
194
-
195
- </details>
196
-
197
- <details>
198
- <summary><strong>Gemini CLI</strong> — full coaching loop (hooks + skill) — <strong>Beta</strong></summary>
199
-
200
- One command registers the MCP server, the `AfterAgent`/`BeforeAgent` lesson hooks, and the coaching skill:
201
-
202
- ```bash
203
- npx -y devcoach install --gemini
204
- ```
205
-
206
- Or install the bundled **Gemini extension** instead: download `devcoach-gemini-extension-<version>.zip` from the [latest release](https://github.com/UltimaPhoenix/dev-coach/releases/latest), unzip, then `gemini extensions install <folder>`. Pick **one** of the two (both provide the same hooks; devcoach detects the extension and skips duplicates). Details: [docs/install/gemini-cli.md](docs/install/gemini-cli.md).
207
-
208
- </details>
209
-
210
- <details>
211
- <summary><strong>Codex CLI</strong> — full coaching loop (hooks + skill) — <strong>Beta</strong></summary>
212
-
213
- One command registers the MCP server (`codex mcp add`), the `Stop`/`UserPromptSubmit` lesson hooks (`~/.codex/hooks.json`), and the coaching skill (`~/.agents/skills/`):
214
-
215
- ```bash
216
- npx -y devcoach install --codex
217
- ```
218
-
219
- Codex asks you to **trust** the new hooks once on the next run — approve them or coaching stays inert. Details: [docs/install/codex.md](docs/install/codex.md).
220
-
221
- </details>
222
-
223
- <details>
224
- <summary><strong>Other MCP agents</strong> (Cursor, Windsurf, Cline, Continue, Zed…)</summary>
225
-
226
- Add this to your agent's MCP config file (or, if your client browses the [MCP Registry](https://registry.modelcontextprotocol.io), install `io.github.UltimaPhoenix/devcoach` from there):
227
-
228
- ```json
229
- {
230
- "mcpServers": {
231
- "devcoach": {
232
- "command": "npx",
233
- "args": ["-y", "devcoach", "mcp"]
234
- }
235
- }
236
- }
237
- ```
238
-
239
- | Agent | Config file |
240
- |-------|-------------|
241
- | **Cursor** | `~/.cursor/mcp.json` |
242
- | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` |
243
- | **Cline** (VS Code) | VS Code Settings → `cline.mcpServers` |
244
- | **Continue.dev** | `~/.continue/config.json` → `mcpServers` |
245
- | **Zed** | `.zed/settings.json` → `context_servers` |
246
-
247
- > Stop hooks (automatic lesson delivery after each task) exist for Claude Code, Gemini CLI, and Codex CLI. The agents in this table have full access to all MCP tools and resources — coaching can be triggered manually or by prompting your agent.
53
+ | Also on | How |
54
+ |---|---|
55
+ | **Claude Desktop** *(beta)* | Download `devcoach-<version>.mcpb` from the [latest release](https://github.com/UltimaPhoenix/dev-coach/releases/latest) → Settings → Extensions → Install Extension… |
56
+ | **Gemini CLI** *(beta)* | `npx -y devcoach install --gemini` |
57
+ | **Codex CLI** *(beta)* | `npx -y devcoach install --codex` (approve the hooks once on the next run) |
58
+ | **Cursor, Windsurf, Cline, Continue, Zed** | Add `{ "mcpServers": { "devcoach": { "command": "npx", "args": ["-y", "devcoach", "mcp"] } } }` to the agent's MCP config, or install `io.github.UltimaPhoenix/devcoach` from the [MCP Registry](https://registry.modelcontextprotocol.io) |
59
+ | **claude.ai web** | No MCP there: paste [`assets/SKILL.md`](assets/SKILL.md) as custom instructions (coaching only, nothing saved) |
248
60
 
249
- </details>
250
-
251
- <details>
252
- <summary><strong>claude.ai web</strong> (skill copy — no MCP)</summary>
253
-
254
- Claude.ai does not support MCP servers. Install the coaching instructions as a skill instead:
255
-
256
- 1. Copy the content of [`assets/SKILL.md`](assets/SKILL.md)
257
- 2. Go to **claude.ai → Settings → Custom instructions** (or Skills, depending on your plan)
258
- 3. Paste the content and save
259
-
260
- This gives claude.ai the coaching behaviour without the MCP tools (lesson logging and profile tracking will not work).
261
-
262
- > **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.
263
-
264
- </details>
61
+ Two rules: with the plugin, **never also run `devcoach install`** (the hooks would count twice); after `brew upgrade` or an npm update, **re-run `devcoach install`** to refresh the hooks and the skill. Every method, step by step: **[Installation](docs/install/index.md)**.
265
62
 
266
63
  ---
267
64
 
268
- ## Onboarding
269
-
270
- The first time your agent connects to devcoach it detects that your profile isn't set up and walks you through it inline — no separate command needed. (Prefer the terminal? `devcoach setup` runs the same wizard.)
271
-
272
- → [Full onboarding walkthrough](docs/usage/coaching.md)
273
-
274
- ### Phase 1 — Choose how to set up your profile
275
-
276
- devcoach scans your **full local Claude Code history** (every project the agent has worked in, ranked by recent activity — file and activity metadata only, never prompt text) and offers four ways to build the profile:
277
-
278
- ```
279
- devcoach: Your knowledge profile isn't set up yet. I scanned 9 of your Claude Code
280
- projects and found TypeScript (dev-coach), Java (discordbot), Swift (blueprince).
281
-
282
- 1. Automatic (strongly recommended) — build the profile from that, no questions asked
283
- 2. Automatic (Deep) — also read my local session transcripts for a sharper profile
284
- 3. Guided — a step-by-step conversation about my stack
285
- 4. Import backup — restore a devcoach backup zip
286
- ```
287
-
288
- ### Phase 2 — Build your profile
289
-
290
- #### Automatic (strongly recommended)
291
-
292
- Builds the whole profile — topics, confidence scores, groups, and the coaching notebook — in **one pass** from the stack detected across your history. No questions: you review and adjust the result afterwards (in chat, with the CLI, or in the dashboard).
293
-
294
- #### Automatic (Deep)
295
-
296
- Opt-in. Everything Automatic does, plus a separate subagent reads the **real conversation text** of your local Claude Code session transcripts (`~/.claude/projects/`) for the most accurate profile and notebook it can produce. A metadata-only pre-check runs first over a rolling 3-month window; if it finds more than 8 projects, devcoach asks whether to narrow the window, proceed with the 25 most recent, or pick specific projects. The agent says so up front: this shares more of your own local history with the model than the metadata scan does — once, on this machine, as a setup step.
297
-
298
- #### Guided
299
-
300
- Prefer to describe your stack yourself? devcoach asks about each technology and your confidence level (1–3 still learning · 4–6 comfortable · 7–9 strong · 10 expert), agrees the topic groups with you, then saves the profile.
301
-
302
- #### Import backup
303
-
304
- On a new machine or reinstalling? Provide the path to your backup zip and your full profile — knowledge map, lessons, settings, and notebook — is restored instantly.
305
-
306
- ### Phase 3 — Profile saved, coaching begins
307
-
308
- After saving, your agent shows what was set up, grouped the way your knowledge map is organised — for the Automatic modes this is your first look at it, so it's the moment to ask for changes:
309
-
310
- ```
311
- ✓ Profile saved — 24 topics across 6 groups.
65
+ ## What happens next
312
66
 
313
- ### Languages
314
- - **typescript** — 7/10
315
- - **java** — 5/10
67
+ 1. **Onboarding in one pass.** The first time your agent connects, devcoach reads your local Claude Code history (file and activity metadata only, never prompt text) and builds your knowledge map, with no questions. Prefer to describe your stack yourself, or restore a backup? You can. → [Onboarding](docs/usage/coaching.md#onboarding)
68
+ 2. **A lesson when one is due.** At most **2 a day, 4 hours apart**, one cue every ~10 interactions per chat session, never in plan mode. Every number is a setting. → [Configuration](docs/reference/configuration.md)
69
+ 3. **One key to answer.** `y` knew it (the next lessons on that topic go deeper) · `u` understood · `n` couldn't follow (kept as the seed of a step-by-step [course](docs/usage/courses.md)). → [The feedback loop](docs/usage/coaching.md#the-feedback-loop)
316
70
 
317
- ### DevOps
318
- - **docker** — 7/10
319
- - **github_actions** — 6/10
71
+ Look back at all of it in the local dashboard (`devcoach ui`, or `/devcoach:ui` in Claude Code) or the [CLI](docs/usage/cli.md), and [share a lesson](docs/usage/sharing.md) with a teammate as text, a link or a file.
320
72
 
321
- ### Other
322
- - **regex** — 4/10
323
-
324
- Change any of this later in chat ("set my Java confidence to 7"), with the
325
- `devcoach` CLI, or in `devcoach ui`.
326
- ```
327
-
328
- That's it. You go back to work. Coaching happens silently in the background.
329
-
330
- ---
331
-
332
- ## Your first lesson
333
-
334
- You work on a task as normal. After your agent responds, devcoach appends a lesson:
335
-
336
- ```
337
- You: Refactor this endpoint to handle concurrent requests properly.
338
-
339
- Agent: [refactors the code, explains the changes]
340
-
341
- ### ──────── 🎓 devcoach ────────
342
- **Promise.allSettled vs Promise.all — don't let one failure sink the batch** · TypeScript · Mid
343
-
344
- Promise.all rejects the moment any promise rejects, and you lose the results of
345
- the ones that already succeeded. For independent work (fan-out fetches, batch
346
- writes) that's usually the wrong default.
347
-
348
- Promise.allSettled always resolves, giving you a status for every promise:
349
-
350
- const results = await Promise.allSettled(ids.map(fetchUser));
351
- const ok = results.filter(r => r.status === "fulfilled").map(r => r.value);
352
-
353
- Use Promise.all when the tasks are genuinely all-or-nothing; reach for
354
- allSettled when partial success is meaningful and you want to report failures.
355
-
356
- 💡 *Senior tip:* for coordinated work that *should* cancel siblings on failure,
357
- an AbortController shared across the requests gives you all-or-nothing with
358
- prompt cancellation — the structured-concurrency middle ground.
359
- ### ──────── typescript · mid ────────
360
-
361
- Did that land? ✅ knew it (y) · 💡 understood (u) · ❌ couldn't follow (n)
362
- ```
363
-
364
- Reply `y` (already knew it — confidence goes up), `u` (new and clear) or `n` (too dense — kept as a seed for a step-by-step course) to shape future lessons — or just move on: no reply means no change.
365
-
366
- ---
367
-
368
- ## Screenshots
369
-
370
- | Knowledge map (personalize here) | Lesson history | Settings |
371
- |:---------------------------------------------------------:|:---:|:---:|
372
- | ![Knowledge map](docs/screenshots/knowledge-map-dark.png) | ![Lessons](docs/screenshots/lessons-dark.png) | ![Settings](docs/screenshots/settings-dark.png) |
373
-
374
- | A course, one step at a time |
375
- |:---:|
376
- | ![A course page](docs/screenshots/course-cache-stampede-dark.png) |
73
+ | Knowledge map | Lesson history | A course, one step at a time |
74
+ |:---:|:---:|:---:|
75
+ | ![Knowledge map](docs/screenshots/knowledge-map-dark.png) | ![Lessons](docs/screenshots/lessons-dark.png) | ![A course page](docs/screenshots/course-cache-stampede-dark.png) |
377
76
 
378
77
  ---
379
78
 
380
- ## Context & personalization
381
-
382
- Every lesson stores **where it happened** — your project folder, repository, branch, commit hash, editor. You can also personalize your coaching with a **learning notebook** at `~/.devcoach/learning-state.md` that shapes which topics devcoach prioritises and how deep the lessons go.
383
-
384
- → [The coaching notebook](docs/index.mdx#the-coaching-notebook) · [Web dashboard guide](docs/usage/web-ui.md)
385
-
386
- ---
387
-
388
- ## Web dashboard
389
-
390
- Open the dashboard at any time to review your progress, edit your profile, or manage settings:
391
-
392
- ```bash
393
- npx -y devcoach ui # → http://localhost:7860 (printed as a clickable link; Ctrl+C stops it)
394
- npx -y devcoach ui --open # …and open it in your browser
395
- npx -y devcoach ui --stop # stop a running dashboard, e.g. one your agent started
396
- ```
397
-
398
- Using the Claude Code plugin? Type `/devcoach:ui` in Claude Code instead — no install needed (`/devcoach:ui stop` closes it).
399
-
400
- | Page | What you can do |
401
- |------|-----------------|
402
- | **Knowledge map** | See all topics with confidence bars; adjust scores directly |
403
- | **Lessons** | Browse and filter your full lesson history; star lessons to revisit; share a lesson with a teammate or import theirs |
404
- | **Settings** | Change rate limits, import/export your profile, take a backup |
405
-
406
- Full reference: [docs/usage/web-ui.md](docs/usage/web-ui.md)
407
-
408
- ---
409
-
410
- ## Sharing lessons
411
-
412
- A lesson that landed for you is one action away from a teammate — as copyable text (the card plus one `devcoach:lesson:1:…` line), a server-less link, or a `.devcoach.md` file. The receiver pastes, clicks or drops it into their own devcoach, where it joins the log as *shared by you* and never touches their daily limit. Only the lesson travels unless you include where it happened; a local path never does.
413
-
414
- ```bash
415
- devcoach share --last # or --link / --file, --with-context, --by "Ada" / --anonymous
416
- devcoach import # reads the clipboard; or pass a code, link, URL, file, or - for stdin
417
- ```
418
-
419
- From the dashboard: **↗ Share** on a lesson and **+ Import** on the Lessons page (drop a `.devcoach.md` anywhere). From your agent: *"share the last lesson"*, *"import this devcoach lesson: …"* — with the plugin, `/devcoach:share` and `/devcoach:import`. Full guide: [docs/usage/sharing.md](docs/usage/sharing.md)
420
-
421
- ---
422
-
423
- ## CLI reference
424
-
425
- The CLI is a secondary interface for querying and managing your coaching data. Everything is also available in the [web dashboard](#web-dashboard). Run `devcoach --help` or `devcoach <command> --help` for full usage.
426
-
427
- | Command | Description |
428
- |---------|-------------|
429
- | `devcoach install` | Register with Claude Code (MCP + hooks + skill) / Claude Desktop |
430
- | `devcoach profile` | Show your knowledge map with confidence bars |
431
- | `devcoach stats` | Overview: lesson counts, weakest/strongest topics |
432
- | `devcoach lessons` | Browse lesson history with filters |
433
- | `devcoach lesson <id>` | Show a single lesson in full |
434
- | `devcoach star <id>` | Mark a lesson as starred |
435
- | `devcoach feedback <id> <know\|understood\|dont_know\|clear>` | Record your answer under a lesson |
436
- | `devcoach share [id] [--last] [--link] [--file]` | Hand a lesson to a teammate (copyable text, link or `.devcoach.md`) |
437
- | `devcoach import [source]` | Add a shared lesson — code, link, URL or file; no argument reads the clipboard |
438
- | `devcoach set max_per_day <n>` | Max lessons per day (default 2) |
439
- | `devcoach set min_gap_minutes <n>` | Minutes between lessons (default 240) |
440
- | `devcoach backup [file.zip]` | Export knowledge + lessons + settings + notebook |
441
- | `devcoach restore <file.zip>` | Restore from a backup |
442
- | `devcoach setup` | Run the onboarding wizard in the terminal |
443
- | `devcoach ui` | Open the web dashboard |
444
-
445
- (Prefix with `npx -y` if you haven't installed globally.) Full reference: [docs/usage/cli.md](docs/usage/cli.md)
446
-
447
- ---
448
-
449
- ## Configuration
450
-
451
- By default: **up to 2 lessons/day, at least 4 hours apart.** Want more coaching? Want less? Change it.
452
-
453
- ```bash
454
- devcoach set max_per_day 5 # more lessons per day
455
- devcoach set min_gap_minutes 60 # lessons closer together
456
- ```
457
-
458
- Or use the [web dashboard](docs/usage/web-ui.md) Settings page. See [docs/reference/configuration.md](docs/reference/configuration.md) for all options.
459
-
460
- ---
461
-
462
- ## Expected outcomes
463
-
464
- Over a typical work week with 1–2 tasks per day, you'll receive **2–4 lessons aligned to your stack and confidence level.** Each lesson takes 30 seconds to read. After a month of normal AI-assisted development, you'll have built a coaching notebook specific to your gaps — exact patterns you struggle with, edge cases you tend to miss, the reasoning behind tools you reach for instinctively.
465
-
466
- **Too intense?** Dial back the frequency: `devcoach set max_per_day 1`. **Want more coaching?** Crank it up: `devcoach set max_per_day 5`. Everything is tunable.
467
-
468
- The compounding effect: developers who keep learning while tools get stronger stay in control of the result.
469
-
470
- ---
471
-
472
- ## Troubleshooting
473
-
474
- **"Node version error"**
475
-
476
- devcoach requires Node.js ≥ 24. Check your version: `node --version`. If you're below 24, upgrade: `brew upgrade node` (Homebrew) or `nvm install 24` (if using nvm).
477
-
478
- **"Failed to install plugin devcoach@ultimaphoenix: … invalid manifest … repository: expected string"**
479
-
480
- Plugin releases up to 1.0.1 shipped a manifest that current Claude Code rejects, so the install stopped before copying anything. Refresh the marketplace so it points at the latest release, then install again:
481
-
482
- ```bash
483
- /plugin marketplace update ultimaphoenix
484
- /plugin install devcoach@ultimaphoenix
485
- ```
486
-
487
- **"MCP server not connecting"**
488
-
489
- *With the Claude Code plugin* (`plugin:devcoach:devcoach failed to connect`): run **`/devcoach:setup`** — its first step prints the reason (Node older than 24, `npm` not on the PATH, no network on the first run) and the fix; then reconnect from `/mcp`. Do **not** run `devcoach install` for a plugin. Details: [plugin troubleshooting](https://ultimaphoenix.github.io/dev-coach/install/claude-code-plugin#troubleshooting).
490
-
491
- *Without the plugin* (npm / Homebrew, Claude Desktop): run **`devcoach doctor`**: it checks the command each agent starts (Claude Desktop's from `claude_desktop_config.json`, Claude Code's from `~/.claude.json`) — that it exists on the PATH that agent uses, that the Node it names exists and is ≥ 24, and which devcoach version it runs. Then `devcoach install --force` re-registers it with an absolute command, and you restart the agent. In Claude Desktop's log (`~/Library/Logs/Claude/mcp-server-devcoach.log`), `Failed to spawn process: No such file or directory` means devcoach never ran: the command, or the Node behind it, is missing — see [When devcoach does not start](https://ultimaphoenix.github.io/dev-coach/install/claude-desktop#when-devcoach-does-not-start). Every devcoach start writes one line to that log with its version, Node and install location.
492
-
493
- **"Coaching isn't firing / no lessons appear"**
494
-
495
- Run **`devcoach doctor`** — it checks the whole wiring (Node version, hook entries in `~/.claude/settings.json` — plus `~/.gemini/settings.json` and `~/.codex/hooks.json` when present — skill, MCP registration, database, pacing counters, rate limit) and ends with a verdict explaining exactly why the next stop would or wouldn't cue a lesson. Remember that pacing is intentionally quiet: with the defaults a lesson is cued only after `nudge_every` interactions in a session and within the rate limits — that's by design, not a bug. For a live trace, set `DEVCOACH_HOOK_DEBUG=1` and watch `~/.devcoach/hook.log`: every hook run appends one line with its decision (`paced (3/10)`, `rate limited: …`, `cue`). Hooks exist for Claude Code, Gemini CLI *(beta)*, and Codex CLI *(beta)*; other agents (Cursor, Windsurf, Cline) don't support them — coaching is available on demand via MCP tools or manual prompting.
496
-
497
- **"Stop hook error occurred" next to a lesson**
498
-
499
- That notice is Claude Code's current labeling of *any* Stop hook that blocks — including devcoach's lesson cue (the accompanying "🎓 devcoach…" line tells you it's coaching, not a failure). It's harmless, and by design it's also rare: the UserPromptSubmit priming hook delivers most lessons within the turn, so the blocking fallback only fires when that didn't happen.
500
-
501
- **"SQLite permission error"**
502
-
503
- devcoach writes to `~/.devcoach/coaching.db`. If you get a permission error, check the directory exists and you have write access: `ls -ld ~/.devcoach`. If missing, run `devcoach install` or `devcoach setup` to initialize it.
504
-
505
- ---
506
-
507
- ## Known limitations
508
-
509
- - **claude.ai web:** MCP servers are not supported. You can use the [skill copy](docs/install/claude-ai.md) (coaching behaviour only, no data storage).
510
- - **Ephemeral sandboxes:** If your agent runs in a fresh sandbox on each restart (like GitHub Codespaces), devcoach cannot persist data across sessions. It works fine for the current session, but lessons won't carry over.
511
- - **Windows:** devcoach is tested on macOS and Linux. Windows support depends on Node.js ≥ 24 and `node:sqlite` availability (generally solid, but report issues).
512
- - **Multi-user machines:** devcoach writes to `~/.devcoach/`, so each user gets their own coaching database. Profiles are not shared.
513
-
514
- ---
515
-
516
- ## Uninstallation
79
+ ## Why devcoach?
517
80
 
518
- One command undoes everything `devcoach install` wired up — the MCP registration, the hook entries and the skill — for Claude Code and Claude Desktop (add `--gemini`, `--codex` or `--all` for the beta agents). Your coaching data stays unless you ask for it to go:
81
+ AI agents now write much of our code — which makes it easy to ship more while understanding less. devcoach turns the tasks your agent finishes into short, in-context lessons on what you don't know yet, so you keep learning as you go. The deeper bet: as AI grows more capable, the scarce skill becomes *validating* what it produces — and that ability only survives if you keep practicing it.
519
82
 
520
- ```bash
521
- devcoach uninstall # remove MCP server + hooks + skill (Claude Code + Claude Desktop)
522
- devcoach uninstall --all --data # every agent, and delete ~/.devcoach (asks first; devcoach backup before)
523
- npm uninstall -g devcoach # then remove the program (npx: nothing to remove)
524
- brew uninstall devcoach # or, via Homebrew (brew untap UltimaPhoenix/tap to drop the tap)
525
- ```
83
+ → [Why devcoach exists](docs/why.md)
526
84
 
527
- Run `devcoach uninstall` **before** `brew uninstall` / `npm uninstall`: Homebrew formulae have no uninstall hook, and once the binary is gone the hooks left in `~/.claude/settings.json` would error on every turn. Using the Claude Code plugin instead? `/plugin uninstall devcoach` removes its hooks, skill and server in one go.
85
+ **Privacy by design.** Everything stays on your machine. No telemetry, no accounts, no calls home. Just one SQLite file. → [Privacy & security](docs/reference/privacy.md)
528
86
 
529
87
  ---
530
88
 
531
- ## Learn more
89
+ ## Documentation
532
90
 
533
- **Full documentation:** The README is a quick start. For detailed guides, visit the **[official docs](https://ultimaphoenix.github.io/dev-coach/)**:
91
+ The full guides live at **[ultimaphoenix.github.io/dev-coach](https://ultimaphoenix.github.io/dev-coach/)**:
534
92
 
535
- - **[Why devcoach exists](docs/why.md)** — the philosophy
536
- - **[Coaching in your agent](docs/usage/coaching.md)** — onboarding, your first lesson, the feedback loop
537
- - **[Using the web dashboard](docs/usage/web-ui.md)** — personalise your knowledge map, filter lessons, jump to code context
538
- - **[CLI reference](docs/usage/cli.md)** — all commands for querying and managing data
93
+ - **[Coaching in your agent](docs/usage/coaching.md)** — onboarding, your first lesson, the feedback loop, what to expect
94
+ - **[Courses](docs/usage/courses.md)** · **[Sharing lessons](docs/usage/sharing.md)** · **[Web dashboard](docs/usage/web-ui.md)** · **[CLI reference](docs/usage/cli.md)**
539
95
  - **[Installation](docs/install/index.md)** — every install method, agent by agent
540
96
  - **[Configuration](docs/reference/configuration.md)** — rate limits, pacing, data location, schema
541
- - **[Privacy & security](docs/reference/privacy.md)** — local-first architecture, what is read and stored
542
97
  - **[How it works](docs/how-it-works.md)** — session startup, coaching loop, lesson selection
98
+ - **[Troubleshooting](docs/troubleshooting.md)** — `devcoach doctor`, common errors, known limitations, uninstalling
99
+ - **[MCP server reference](docs/reference/mcp-server.md)** · **[Privacy & security](docs/reference/privacy.md)** · **[FAQ](docs/faq.mdx)**
543
100
  - **[Vision & roadmap](docs/vision.md)** — free, local, not commercialized; ideas we're exploring next
544
- - **[Plugin marketplace](docs/install/claude-code-plugin.md#about-the-personal-marketplace)** — UltimaPhoenix plugin collection
545
101
 
546
102
  ---
547
103
 
548
- ## Community
104
+ ## Community & contributing
549
105
 
550
106
  - **Star the repo** — help others discover it
551
- - **[GitHub Issues](https://github.com/UltimaPhoenix/dev-coach/issues)** — feature requests, feedback and questions: open an issue
552
-
553
- ---
554
-
555
- ## Contributing
107
+ - **[GitHub Issues](https://github.com/UltimaPhoenix/dev-coach/issues)** — feature requests, feedback and questions
108
+ - **[CONTRIBUTING.md](CONTRIBUTING.md)** — quick start, architecture rules, testing, branches & releases
556
109
 
557
- Help welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) to get started.
558
-
559
- ---
560
-
561
- ## Development
562
-
563
- ```bash
564
- git clone https://github.com/UltimaPhoenix/dev-coach && cd dev-coach
565
- npm install
566
- npm run dev -- mcp # run the MCP server from source
567
- npm run dev -- ui # run the web dashboard from source
568
- npm run lint && npm run typecheck && npm test
569
- npm run build # tsup → dist/bin.js
570
- npm run mcpb # build the Claude Desktop .mcpb (npm run mcpb:sign to self-sign)
571
- ```
572
-
573
- - **MCP Inspector:** `npx @modelcontextprotocol/inspector node dist/bin.js mcp`
574
- - **Stack:** `@modelcontextprotocol/server` (MCP SDK v2) · `node:sqlite` · Hono · Commander · Zod · Biome · Vitest · tsup
575
-
576
- ### Branches & releases
577
-
578
- - `develop` is the integration branch: every PR targets it, and each merge publishes a **canary**
579
- (`npx -y devcoach@next`, docs at `/next/`).
580
- - `main` only receives releases and is always the last published version (the Claude Code
581
- self-marketplace installs from it).
582
- - A release is one click: run the **CI** workflow (*Run workflow* → patch / minor / major) on
583
- `develop`. CI bumps the version on `develop`, tags it, fast-forwards `main`, then publishes to npm via
584
- **OIDC provenance**, attaches the `.mcpb` + plugin zips to a GitHub Release, and updates the Homebrew
585
- tap, the plugin marketplace and the MCP Registry. First-time setup: configure a Trusted Publisher on
586
- npmjs.com for the `devcoach` package (GitHub Actions, repo `UltimaPhoenix/dev-coach`, workflow `ci.yml`).
110
+ Built on the official [Model Context Protocol TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk), Node's embedded `node:sqlite`, [Hono](https://hono.dev) (web dashboard), and [Commander](https://github.com/tj/commander.js) (CLI).
587
111
 
588
112
  ---
589
113
 
@@ -597,19 +121,7 @@ as a network service, is also released as open source under the AGPL**. There is
597
121
  license on offer today**; if you need to embed devcoach in a closed-source product,
598
122
  [open an issue](https://github.com/UltimaPhoenix/dev-coach/issues) and ask.
599
123
 
600
- ### Licensing FAQ
601
-
602
124
  > **devcoach is and stays 100% free** to download, install, and use — for everyone, forever, including
603
- > at work and on commercial projects. AGPL is *not* a price tag: you only ever pay if you want to ship
604
- > a **proprietary/closed derivative of devcoach itself** without complying with the AGPL.
605
-
606
- **Using devcoach does not put your code under the AGPL.** It runs as a separate process over
607
- stdio/MCP (`npx -y devcoach mcp`); talking to it at arm's length is not a derivative work — exactly
608
- like querying an AGPL-licensed database. Your own projects keep whatever license you choose.
609
-
610
- - ✅ *"I use devcoach inside Claude Code while building my closed-source startup app."* — Free. Your app stays proprietary, zero obligations.
611
- - ✅ *"My whole team installs devcoach to get coaching on our internal/commercial repos."* — Free. Using the unmodified tool at a company is fine.
612
- - ⚠️ *"I fork devcoach, add a feature, and publish the package or host its dashboard as a public service."* — You must release **your modified devcoach** source under the AGPL.
613
- - 💼 *"I want to embed devcoach in my paid product and keep my changes closed."* — Not covered by the AGPL — ask first by opening an issue.
614
-
615
- Keep using devcoach as a tool/executable (not as an imported library) and the AGPL never reaches your own code.
125
+ > at work and on commercial projects. **Using devcoach does not put your code under the AGPL**: it runs
126
+ > as a separate process over stdio/MCP, so your own projects keep whatever license you choose.
127
+ > Examples and the details: **[License & commercial use](docs/reference/license.md)**.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import {a as a$3,c as c$1}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g,j as j$1,l,n}from'./chunk-M3XX2DV7.js';import {z,E as E$1,A,y,G,X as X$1,Ba,Fa,Ca,R,V as V$1,S,W as W$1,Da,T,U,L,Ga,ra,va,N,M,P as P$1,H,K as K$1,Ea,J as J$1,O,Y as Y$1,w,Aa,za,b as b$1,c,a as a$2,oa,qa,r,ta,sa,ua,xa,wa}from'./chunk-ZFCI6ROC.js';import {a}from'./chunk-FL5OOXCF.js';import {existsSync,readFileSync,mkdirSync,writeFileSync}from'fs';import {dirname,join,extname}from'path';import {fileURLToPath}from'url';import {serve}from'@hono/node-server';import {Hono}from'hono';import {html,raw}from'hono/html';function j(r){return JSON.stringify(r??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function We(r,e){return r+e.replace(/[^A-Za-z0-9_-]/g,g=>`_${(g.codePointAt(0)??0).toString(16)}`)}var lt=html`<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><circle cx="4" cy="10" r="1.75"/><circle cx="10" cy="10" r="1.75"/><circle cx="16" cy="10" r="1.75"/></svg>`;function Y(r,e="",g=""){return html`<div class="relative ${e}" ${raw(g)} x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
2
+ import {a as a$3,c as c$1}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g,j as j$1,l,n}from'./chunk-IHT2QFRV.js';import {z,E as E$1,A,y,G,X as X$1,Ba,Fa,Ca,R,V as V$1,S,W as W$1,Da,T,U,L,Ga,ra,va,N,M,P as P$1,H,K as K$1,Ea,J as J$1,O,Y as Y$1,w,Aa,za,b as b$1,c,a as a$2,oa,qa,r,ta,sa,ua,xa,wa}from'./chunk-O53WOENG.js';import {a}from'./chunk-ACFVSHDR.js';import {existsSync,readFileSync,mkdirSync,writeFileSync}from'fs';import {dirname,join,extname}from'path';import {fileURLToPath}from'url';import {serve}from'@hono/node-server';import {Hono}from'hono';import {html,raw}from'hono/html';function j(r){return JSON.stringify(r??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function We(r,e){return r+e.replace(/[^A-Za-z0-9_-]/g,g=>`_${(g.codePointAt(0)??0).toString(16)}`)}var lt=html`<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><circle cx="4" cy="10" r="1.75"/><circle cx="10" cy="10" r="1.75"/><circle cx="16" cy="10" r="1.75"/></svg>`;function Y(r,e="",g=""){return html`<div class="relative ${e}" ${raw(g)} x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
3
3
  <button type="button" @click="open = !open" :aria-expanded="open" aria-label="More actions" title="More actions" class="w-8 h-8 inline-flex items-center justify-center rounded-lg border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-accent-400 hover:text-gray-800 dark:hover:text-gray-100">${lt}</button>
4
4
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-56 p-1" style="display:none">
5
5
  ${r}
package/dist/bin.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import {c as c$1,e as e$1,f}from'./chunk-WRY2DLSB.js';import'./chunk-FL5OOXCF.js';var e=process.argv[2]??"",i=new Set(["stop-hook","prompt-hook","gemini-stop-hook","gemini-prompt-hook","codex-stop-hook","codex-prompt-hook","onboard-hook","lesson-ready"]);function c(){let o=e$1();return `devcoach ${o.version} (${f(o.where)}): `}var n=c$1(process.versions.node);n&&(i.has(e)&&process.exit(0),console.error(`${n} at ${process.execPath}. Upgrade Node, then restart.`),process.exit(1));try{if(i.has(e)){let{runHook:o}=await import('./hooks-BPQRRUI7.js');o(e);}else {let{runCli:o}=await import('./commands-ZSLAPMTJ.js');await o();}}catch(o){o?.name==="SchemaTooNewError"&&(console.error(o.message),process.exit(3)),console.error(c(),o),process.exit(1);}
2
+ import {c as c$1,e as e$1,f}from'./chunk-QRNH4GKU.js';import'./chunk-ACFVSHDR.js';var e=process.argv[2]??"",i=new Set(["stop-hook","prompt-hook","gemini-stop-hook","gemini-prompt-hook","codex-stop-hook","codex-prompt-hook","onboard-hook","lesson-ready"]);function c(){let o=e$1();return `devcoach ${o.version} (${f(o.where)}): `}var n=c$1(process.versions.node);n&&(i.has(e)&&process.exit(0),console.error(`${n} at ${process.execPath}. Upgrade Node, then restart.`),process.exit(1));try{if(i.has(e)){let{runHook:o}=await import('./hooks-XM34NW5N.js');o(e);}else {let{runCli:o}=await import('./commands-RJLBPNY7.js');await o();}}catch(o){o?.name==="SchemaTooNewError"&&(console.error(o.message),process.exit(3)),console.error(c(),o),process.exit(1);}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ var e={version:"2.8.0-next.673.g14f8bb5"};var s=e.version;export{s as a};
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import {s,t,pa,da,ea,ga,fa,D,ha,ia,la,ja,ka,ma,ra,ya,d}from'./chunk-ZFCI6ROC.js';import {a}from'./chunk-FL5OOXCF.js';import {lstatSync,mkdirSync,readFileSync,rmSync,existsSync,writeFileSync}from'fs';import {join,sep,basename}from'path';import {execFileSync}from'child_process';import st from'dns';import it from'http';import at from'https';import {isIP}from'net';var w=/^[a-z0-9-]+$/,I=t,h=class extends Error{};function tt(e){if(!w.test(e))throw new h(`Invalid course id '${e}'`)}function y(e){return tt(e),join(s,e)}function W(e){return join(y(e),I)}function B(e){let t=W(e);if(!t.startsWith(y(e)+sep))return null;try{let r=lstatSync(t);return !r.isFile()||r.size>pa?null:t}catch{return null}}function H(e){try{let t=B(e);if(t===null)return !1;let r=d(readFileSync(t,"utf8"));return r===null||Buffer.byteLength(r)>pa?!1:(writeFileSync(t,r),!0)}catch{return false}}function et(e,t){let r=ya(t,"course"),n=r;for(let o=2;ea(e,n)!==null||existsSync(join(s,n));o++)n=`${r}-${o}`;return n}function Ct(e,t){let r=new Date().toISOString(),n={id:et(e,t.title),lesson_id:t.lesson_id??null,topic_id:t.topic_id,title:t.title,goal:t.goal??null,prerequisites:t.prerequisites??[],status:"active",created_at:r,updated_at:r,completed_at:null};return da(e,n),mkdirSync(y(n.id),{recursive:true}),{...n,steps:[]}}function E(e,t){if(!w.test(t))return null;let r=ea(e,t);return r?{...r,steps:ga(e,t)}:null}function bt(e,t={}){return fa(e,t).map(r=>({...r,steps:ga(e,r.id)}))}function xt(e,t,r){return D(e,()=>{let n=E(e,t);if(!n)throw new h(`Course '${t}' not found`);let o=n.steps.length,i=r.after??o;if(!Number.isInteger(i)||i<0||i>o)throw new h(`'after' must be between 0 and ${o} (the course has ${o} steps)`);let a=B(t);if(!a)throw new h(`Course '${t}' has no document yet \u2014 write ${W(t)} first`);let f=readFileSync(a,"utf8");if(!new RegExp(`\\sid=["']${r.anchor}["']`).test(f))throw new h(`No element with id="${r.anchor}" in ${a}`);if(n.steps.some(s=>s.anchor===r.anchor))throw new h(`Step anchor '${r.anchor}' is already registered`);let l={course_id:t,position:i+1,title:r.title,kind:r.kind,anchor:r.anchor,status:"todo",done_at:null};return i<o&&ha(e,t,i+1),ia(e,l),la(e,t,new Date().toISOString()),H(t),l})}function Rt(e,t,r,n){let o=new Date().toISOString();return H(t),D(e,()=>{if(!ja(e,t,r,n,n==="done"?o:null))return null;let a=ga(e,t),f=a.length>0&&a.every(s=>s.status!=="todo"),l=ea(e,t);return l&&f&&l.status==="active"?ka(e,t,"completed",o):l&&!f&&l.status==="completed"?ka(e,t,"active",o):la(e,t,o),E(e,t)})}function _t(e,t,r){return w.test(t)&&ka(e,t,r,new Date().toISOString())?E(e,t):null}function Lt(e,t){if(!w.test(t))return false;let r=ma(e,t);return rmSync(y(t),{recursive:true,force:true}),r}function Dt(e){return {done:e.steps.filter(t=>t.status!=="todo").length,total:e.steps.length}}function C(...e){try{return execFileSync("git",e,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}}var G={"github.com":"github","gitlab.com":"gitlab","bitbucket.org":"bitbucket"};function ot(e){if(!e)return [null,null];let t=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(e);if(t){let n=(t[1]??"").toLowerCase();return [t[2]??e,G[n]??"local"]}let r=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(e);if(r){let n=(r[1]??"").toLowerCase();return [r[2]??e,G[n]??"local"]}return [e,"local"]}function Tt(){return C("config","user.name")}function vt(){let e=process.cwd(),t=C("rev-parse","--abbrev-ref","HEAD"),r=C("rev-parse","HEAD"),n=C("remote","get-url","origin"),[o,i]=ot(n),a;return o?a=o.replace(/\/+$/,"").split("/").pop()??null:a=basename(e)||null,t==="HEAD"&&(t=null),{project:a,repository:o,branch:t,commit_hash:r,folder:e,repository_platform:i}}var j="That URL is too large to be a lesson.",b="Only public http(s) URLs can be imported.",ut=new Set([301,302,303,307,308]);function lt(e){return /^https?:\/\/\S+$/i.test(e.trim())}function z(e){let[t=0,r=0]=e.split(".").map(Number);return t===0||t===10||t===127||t===169&&r===254||t===172&&r>=16&&r<=31||t===192&&r===168||t===100&&r>=64&&r<=127||t>=224}function K(e){let t=e.replace(/^\[|\]$/g,"").replace(/%.*$/,"").toLowerCase();if(isIP(t)===4)return z(t);if(isIP(t)!==6)return true;let r=/^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(t);if(r?.[1])return z(r[1]);if(t==="::"||t==="::1")return true;let n=t.split(":")[0]??"";return !!(/^f[cd]/.test(n)||/^fe[89ab]/.test(n))}async function ct(e){return (await st.promises.lookup(e,{all:true})).map(r=>r.address)}async function pt(e,t){let r=e.hostname.replace(/^\[|\]$/g,"").toLowerCase();if(r==="localhost"||r.endsWith(".localhost"))throw new ra(b);if(isIP(r)){if(K(r))throw new ra(b);return r}let n;try{n=await t(r);}catch{throw new ra("The URL could not be reached \u2014 copy the lesson text instead.")}let o=n[0];if(!o||n.some(i=>K(i)))throw new ra(b);return o}var ft=(e,t,r)=>new Promise((n,o)=>{let i=isIP(t)===6?6:4,a$1=(s,c,m)=>{c.all?m(null,[{address:t,family:i}]):m(null,t,i);},l=(e.protocol==="https:"?at:it).request(e,{method:"GET",lookup:a$1,signal:r,headers:{accept:"text/plain, text/markdown, application/json;q=0.9, */*;q=0.5","user-agent":`devcoach/${a}`}},s=>{let c=s.headers["content-length"];n({status:s.statusCode??0,location:s.headers.location??null,contentLength:c===void 0?null:Number(c),body:s,abort:()=>s.destroy()});});l.on("error",o),l.end();});async function ht(e,t){if(e.contentLength!==null&&e.contentLength>t)throw e.abort(),new ra(j);let r=[],n=0;for await(let o of e.body){if(n+=o.byteLength,n>t)throw e.abort(),new ra(j);r.push(o);}return Buffer.concat(r).toString("utf8")}async function Ft(e,t={}){let r=t.timeoutMs??5e3,n=t.maxBytes??256*1024,o=t.maxRedirects??3,i=t.lookup??ct,a=t.transport??ft;if(!lt(e))throw new ra("Only http(s) URLs can be imported.");let f=new AbortController,l=setTimeout(()=>f.abort(),r);try{let s=new URL(e.trim());for(let c=0;;c++){let m=await pt(s,i),p=await a(s,m,f.signal);if(ut.has(p.status)){if(p.abort(),!p.location||c>=o)throw new ra("The URL redirects too many times \u2014 copy the lesson text instead.");if(s=new URL(p.location,s),!/^https?:$/.test(s.protocol))throw new ra(b);continue}if(p.status<200||p.status>=300)throw p.abort(),new ra(`The URL answered ${p.status} \u2014 nothing to import there.`);return await ht(p,n)}}catch(s){if(s instanceof ra)throw s;let c=s?.name==="AbortError"?"timed out":"could not be reached";throw new ra(`The URL ${c} \u2014 copy the lesson text instead.`)}finally{clearTimeout(l);}}export{w as a,h as b,y as c,W as d,B as e,Ct as f,E as g,bt as h,xt as i,Rt as j,_t as k,Lt as l,Dt as m,Tt as n,vt as o,lt as p,Ft as q};
2
+ import {s,t,pa,da,ea,ga,fa,D,ha,ia,la,ja,ka,ma,ra,ya,d}from'./chunk-O53WOENG.js';import {a}from'./chunk-ACFVSHDR.js';import {lstatSync,mkdirSync,readFileSync,rmSync,existsSync,writeFileSync}from'fs';import {join,sep,basename}from'path';import {execFileSync}from'child_process';import st from'dns';import it from'http';import at from'https';import {isIP}from'net';var w=/^[a-z0-9-]+$/,I=t,h=class extends Error{};function tt(e){if(!w.test(e))throw new h(`Invalid course id '${e}'`)}function y(e){return tt(e),join(s,e)}function W(e){return join(y(e),I)}function B(e){let t=W(e);if(!t.startsWith(y(e)+sep))return null;try{let r=lstatSync(t);return !r.isFile()||r.size>pa?null:t}catch{return null}}function H(e){try{let t=B(e);if(t===null)return !1;let r=d(readFileSync(t,"utf8"));return r===null||Buffer.byteLength(r)>pa?!1:(writeFileSync(t,r),!0)}catch{return false}}function et(e,t){let r=ya(t,"course"),n=r;for(let o=2;ea(e,n)!==null||existsSync(join(s,n));o++)n=`${r}-${o}`;return n}function Ct(e,t){let r=new Date().toISOString(),n={id:et(e,t.title),lesson_id:t.lesson_id??null,topic_id:t.topic_id,title:t.title,goal:t.goal??null,prerequisites:t.prerequisites??[],status:"active",created_at:r,updated_at:r,completed_at:null};return da(e,n),mkdirSync(y(n.id),{recursive:true}),{...n,steps:[]}}function E(e,t){if(!w.test(t))return null;let r=ea(e,t);return r?{...r,steps:ga(e,t)}:null}function bt(e,t={}){return fa(e,t).map(r=>({...r,steps:ga(e,r.id)}))}function xt(e,t,r){return D(e,()=>{let n=E(e,t);if(!n)throw new h(`Course '${t}' not found`);let o=n.steps.length,i=r.after??o;if(!Number.isInteger(i)||i<0||i>o)throw new h(`'after' must be between 0 and ${o} (the course has ${o} steps)`);let a=B(t);if(!a)throw new h(`Course '${t}' has no document yet \u2014 write ${W(t)} first`);let f=readFileSync(a,"utf8");if(!new RegExp(`\\sid=["']${r.anchor}["']`).test(f))throw new h(`No element with id="${r.anchor}" in ${a}`);if(n.steps.some(s=>s.anchor===r.anchor))throw new h(`Step anchor '${r.anchor}' is already registered`);let l={course_id:t,position:i+1,title:r.title,kind:r.kind,anchor:r.anchor,status:"todo",done_at:null};return i<o&&ha(e,t,i+1),ia(e,l),la(e,t,new Date().toISOString()),H(t),l})}function Rt(e,t,r,n){let o=new Date().toISOString();return H(t),D(e,()=>{if(!ja(e,t,r,n,n==="done"?o:null))return null;let a=ga(e,t),f=a.length>0&&a.every(s=>s.status!=="todo"),l=ea(e,t);return l&&f&&l.status==="active"?ka(e,t,"completed",o):l&&!f&&l.status==="completed"?ka(e,t,"active",o):la(e,t,o),E(e,t)})}function _t(e,t,r){return w.test(t)&&ka(e,t,r,new Date().toISOString())?E(e,t):null}function Lt(e,t){if(!w.test(t))return false;let r=ma(e,t);return rmSync(y(t),{recursive:true,force:true}),r}function Dt(e){return {done:e.steps.filter(t=>t.status!=="todo").length,total:e.steps.length}}function C(...e){try{return execFileSync("git",e,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}}var G={"github.com":"github","gitlab.com":"gitlab","bitbucket.org":"bitbucket"};function ot(e){if(!e)return [null,null];let t=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(e);if(t){let n=(t[1]??"").toLowerCase();return [t[2]??e,G[n]??"local"]}let r=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(e);if(r){let n=(r[1]??"").toLowerCase();return [r[2]??e,G[n]??"local"]}return [e,"local"]}function Tt(){return C("config","user.name")}function vt(){let e=process.cwd(),t=C("rev-parse","--abbrev-ref","HEAD"),r=C("rev-parse","HEAD"),n=C("remote","get-url","origin"),[o,i]=ot(n),a;return o?a=o.replace(/\/+$/,"").split("/").pop()??null:a=basename(e)||null,t==="HEAD"&&(t=null),{project:a,repository:o,branch:t,commit_hash:r,folder:e,repository_platform:i}}var j="That URL is too large to be a lesson.",b="Only public http(s) URLs can be imported.",ut=new Set([301,302,303,307,308]);function lt(e){return /^https?:\/\/\S+$/i.test(e.trim())}function z(e){let[t=0,r=0]=e.split(".").map(Number);return t===0||t===10||t===127||t===169&&r===254||t===172&&r>=16&&r<=31||t===192&&r===168||t===100&&r>=64&&r<=127||t>=224}function K(e){let t=e.replace(/^\[|\]$/g,"").replace(/%.*$/,"").toLowerCase();if(isIP(t)===4)return z(t);if(isIP(t)!==6)return true;let r=/^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(t);if(r?.[1])return z(r[1]);if(t==="::"||t==="::1")return true;let n=t.split(":")[0]??"";return !!(/^f[cd]/.test(n)||/^fe[89ab]/.test(n))}async function ct(e){return (await st.promises.lookup(e,{all:true})).map(r=>r.address)}async function pt(e,t){let r=e.hostname.replace(/^\[|\]$/g,"").toLowerCase();if(r==="localhost"||r.endsWith(".localhost"))throw new ra(b);if(isIP(r)){if(K(r))throw new ra(b);return r}let n;try{n=await t(r);}catch{throw new ra("The URL could not be reached \u2014 copy the lesson text instead.")}let o=n[0];if(!o||n.some(i=>K(i)))throw new ra(b);return o}var ft=(e,t,r)=>new Promise((n,o)=>{let i=isIP(t)===6?6:4,a$1=(s,c,m)=>{c.all?m(null,[{address:t,family:i}]):m(null,t,i);},l=(e.protocol==="https:"?at:it).request(e,{method:"GET",lookup:a$1,signal:r,headers:{accept:"text/plain, text/markdown, application/json;q=0.9, */*;q=0.5","user-agent":`devcoach/${a}`}},s=>{let c=s.headers["content-length"];n({status:s.statusCode??0,location:s.headers.location??null,contentLength:c===void 0?null:Number(c),body:s,abort:()=>s.destroy()});});l.on("error",o),l.end();});async function ht(e,t){if(e.contentLength!==null&&e.contentLength>t)throw e.abort(),new ra(j);let r=[],n=0;for await(let o of e.body){if(n+=o.byteLength,n>t)throw e.abort(),new ra(j);r.push(o);}return Buffer.concat(r).toString("utf8")}async function Ft(e,t={}){let r=t.timeoutMs??5e3,n=t.maxBytes??256*1024,o=t.maxRedirects??3,i=t.lookup??ct,a=t.transport??ft;if(!lt(e))throw new ra("Only http(s) URLs can be imported.");let f=new AbortController,l=setTimeout(()=>f.abort(),r);try{let s=new URL(e.trim());for(let c=0;;c++){let m=await pt(s,i),p=await a(s,m,f.signal);if(ut.has(p.status)){if(p.abort(),!p.location||c>=o)throw new ra("The URL redirects too many times \u2014 copy the lesson text instead.");if(s=new URL(p.location,s),!/^https?:$/.test(s.protocol))throw new ra(b);continue}if(p.status<200||p.status>=300)throw p.abort(),new ra(`The URL answered ${p.status} \u2014 nothing to import there.`);return await ht(p,n)}}catch(s){if(s instanceof ra)throw s;let c=s?.name==="AbortError"?"timed out":"could not be reached";throw new ra(`The URL ${c} \u2014 copy the lesson text instead.`)}finally{clearTimeout(l);}}export{w as a,h as b,y as c,W as d,B as e,Ct as f,E as g,bt as h,xt as i,Rt as j,_t as k,Lt as l,Dt as m,Tt as n,vt as o,lt as p,Ft as q};
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import {a}from'./chunk-FL5OOXCF.js';import {readFileSync,existsSync,lstatSync,mkdirSync,writeFileSync}from'fs';import {dirname,join}from'path';import {fileURLToPath}from'url';import {z as z$1}from'zod';import {createRequire}from'module';import {homedir}from'os';import {strToU8,zipSync,unzipSync,deflateSync,strFromU8,Inflate}from'fflate';var lt=dirname(fileURLToPath(import.meta.url)),ut='<script data-devcoach="highlighter"></script>',Ee=/<script data-devcoach="highlighter">\s*<\/script>/,pt=/<script data-devcoach="highlighter">\s*(?!<\/script>)\S/;function dt(){for(let e of ["../assets","../../assets"]){let t=join(lt,e,"static","vendor");if(existsSync(join(t,"highlight.min.js")))return t}return null}var _e={light:{keyword:"#0033b3",string:"#067d17",comment:"#5f6672",number:"#1750eb",title:"#00627a",type:"#871094",attr:"#374151",meta:"#7a6a00"},dark:{keyword:"#cf8e6d",string:"#6aab73",comment:"#8b8f96",number:"#2aacb8",title:"#56a8f5",type:"#c77dbb",attr:"#bcbec4",meta:"#b3ae60"}};function Se(e){return Object.entries(e).map(([t,n])=>`--syn-${t}:${n}`).join(";")}function mt(){let e=Se(_e.light),t=Se(_e.dark);return [`:where(:root){${e}}`,`@media (prefers-color-scheme: dark){:where(:root:not([data-theme="light"])){${t}}}`,`:where(:root[data-theme="dark"]){${t}}`,"pre code.hljs{display:block;background:transparent;color:inherit;padding:0}",".hljs-comment,.hljs-quote{color:var(--syn-comment);font-style:italic}",".hljs-keyword,.hljs-selector-tag,.hljs-literal,.hljs-doctag{color:var(--syn-keyword)}",".hljs-string,.hljs-regexp,.hljs-addition,.hljs-selector-attr,.hljs-selector-pseudo{color:var(--syn-string)}",".hljs-number,.hljs-symbol,.hljs-bullet,.hljs-link{color:var(--syn-number)}",".hljs-title,.hljs-section,.hljs-name,.hljs-selector-id,.hljs-selector-class{color:var(--syn-title)}",".hljs-type,.hljs-built_in,.hljs-title.class_,.hljs-tag{color:var(--syn-type)}",".hljs-attr,.hljs-attribute,.hljs-variable,.hljs-template-variable,.hljs-property,.hljs-params{color:var(--syn-attr)}",".hljs-meta,.hljs-deletion,.hljs-subst{color:var(--syn-meta)}",".hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold}"].join("")}var gt=';(function(){function run(){hljs.configure({ignoreUnescapedHTML:true});document.querySelectorAll("pre code").forEach(function(el){if(el.dataset.highlighted||el.classList.contains("nohighlight"))return;hljs.highlightElement(el);});}if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",run);else run();})();',T;function ye(){if(T!==void 0)return T;T=null;try{let e=dt();if(e===null)return T;let t=readFileSync(join(e,"highlight.min.js"),"utf8"),n=readFileSync(join(e,"highlight.LICENSE.txt"),"utf8").trim();if(/<\/script/i.test(t)||n.includes("*/"))return T;T=`<style data-devcoach="highlighter">${mt()}</style>
2
+ import {a}from'./chunk-ACFVSHDR.js';import {readFileSync,existsSync,lstatSync,mkdirSync,writeFileSync}from'fs';import {dirname,join}from'path';import {fileURLToPath}from'url';import {z as z$1}from'zod';import {createRequire}from'module';import {homedir}from'os';import {strToU8,zipSync,unzipSync,deflateSync,strFromU8,Inflate}from'fflate';var lt=dirname(fileURLToPath(import.meta.url)),ut='<script data-devcoach="highlighter"></script>',Ee=/<script data-devcoach="highlighter">\s*<\/script>/,pt=/<script data-devcoach="highlighter">\s*(?!<\/script>)\S/;function dt(){for(let e of ["../assets","../../assets"]){let t=join(lt,e,"static","vendor");if(existsSync(join(t,"highlight.min.js")))return t}return null}var _e={light:{keyword:"#0033b3",string:"#067d17",comment:"#5f6672",number:"#1750eb",title:"#00627a",type:"#871094",attr:"#374151",meta:"#7a6a00"},dark:{keyword:"#cf8e6d",string:"#6aab73",comment:"#8b8f96",number:"#2aacb8",title:"#56a8f5",type:"#c77dbb",attr:"#bcbec4",meta:"#b3ae60"}};function Se(e){return Object.entries(e).map(([t,n])=>`--syn-${t}:${n}`).join(";")}function mt(){let e=Se(_e.light),t=Se(_e.dark);return [`:where(:root){${e}}`,`@media (prefers-color-scheme: dark){:where(:root:not([data-theme="light"])){${t}}}`,`:where(:root[data-theme="dark"]){${t}}`,"pre code.hljs{display:block;background:transparent;color:inherit;padding:0}",".hljs-comment,.hljs-quote{color:var(--syn-comment);font-style:italic}",".hljs-keyword,.hljs-selector-tag,.hljs-literal,.hljs-doctag{color:var(--syn-keyword)}",".hljs-string,.hljs-regexp,.hljs-addition,.hljs-selector-attr,.hljs-selector-pseudo{color:var(--syn-string)}",".hljs-number,.hljs-symbol,.hljs-bullet,.hljs-link{color:var(--syn-number)}",".hljs-title,.hljs-section,.hljs-name,.hljs-selector-id,.hljs-selector-class{color:var(--syn-title)}",".hljs-type,.hljs-built_in,.hljs-title.class_,.hljs-tag{color:var(--syn-type)}",".hljs-attr,.hljs-attribute,.hljs-variable,.hljs-template-variable,.hljs-property,.hljs-params{color:var(--syn-attr)}",".hljs-meta,.hljs-deletion,.hljs-subst{color:var(--syn-meta)}",".hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold}"].join("")}var gt=';(function(){function run(){hljs.configure({ignoreUnescapedHTML:true});document.querySelectorAll("pre code").forEach(function(el){if(el.dataset.highlighted||el.classList.contains("nohighlight"))return;hljs.highlightElement(el);});}if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",run);else run();})();',T;function ye(){if(T!==void 0)return T;T=null;try{let e=dt();if(e===null)return T;let t=readFileSync(join(e,"highlight.min.js"),"utf8"),n=readFileSync(join(e,"highlight.LICENSE.txt"),"utf8").trim();if(/<\/script/i.test(t)||n.includes("*/"))return T;T=`<style data-devcoach="highlighter">${mt()}</style>
3
3
  <script data-devcoach="highlighter">/*
4
4
  Syntax highlighting by highlight.js, inlined by devcoach.
5
5
 
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import {a as a$1}from'./chunk-FL5OOXCF.js';import {realpathSync}from'fs';import {homedir}from'os';import {fileURLToPath}from'url';var a=24;function c(n,e=process.platform){let t=n.replace(/\\/g,"/");return e==="win32"?t.toLowerCase():t}var g=[["nvm",/\/\.nvm\/versions\/node\/(v?[\d.]+[^/]*)\//],["nvm-windows",/\/appdata\/roaming\/nvm\/(v?[\d.]+[^/]*)\//],["fnm",/\/fnm\/node-versions\/(v?[\d.]+[^/]*)\//],["volta",/\/\.volta\/tools\/image\/node\/(v?[\d.]+[^/]*)\//],["volta",/\/volta\/tools\/image\/node\/(v?[\d.]+[^/]*)\//],["asdf",/\/\.asdf\/installs\/nodejs\/(v?[\d.]+[^/]*)\//],["mise",/\/mise\/installs\/node\/(v?[\d.]+[^/]*)\//],["n",/\/n\/versions\/node\/(v?[\d.]+[^/]*)\//]];function u(n){for(let[e,t]of g){let r=t.exec(n);if(r?.[1])return {manager:e,boundTo:r[1].startsWith("v")?r[1]:`v${r[1]}`}}return null}function p(n){let e=n.toLowerCase();return e.includes("/.claude/plugins/data/")||e.includes("/.claude/plugins/cache/")?"claude-plugin":e.includes("/.devcoach/gemini-ext/")?"gemini-extension":e.includes("/claude extensions/")?"claude-desktop-extension":e.includes("/_npx/")?"npx":e.includes("/cellar/devcoach/")?"homebrew":e.includes("/node_modules/devcoach/")?"npm-global":/\/(src\/core\/runtime\.ts|src\/bin\.ts|dist\/[^/]+\.js)$/.test(e)?"source":"unknown"}function h(n,e=process.platform){let t=c(n,e),r=u(t),o=t.toLowerCase();return {channel:p(t),manager:r?.manager??(o.includes("/fnm_multishells/")?"fnm":null),boundTo:r?.boundTo??null,ephemeral:o.includes("/_npx/")||o.includes("/fnm_multishells/")}}function v(n,e=process.platform){let t=c(n,e),r=t.toLowerCase(),o=u(t);return o?o.manager:r.includes("/fnm_multishells/")?"fnm":r.includes("/.volta/")||r.includes("/volta/")?"volta":r.includes("/cellar/node")||r.includes("/opt/homebrew/")||r.includes("/linuxbrew/")||r.startsWith("/usr/local/opt/node")?"homebrew":r.startsWith("/usr/bin/")||r.startsWith("/usr/local/bin/")||r.includes("/program files/nodejs/")||r.startsWith("/snap/")?"system":"unknown"}function R(n){return Number.parseInt(n.split(".")[0]??"0",10)>=a?null:`devcoach ${a$1} needs Node ${a} or newer (it uses the built-in node:sqlite); this is Node ${n}`}function x(n,e=homedir(),t=process.platform){if(!e)return n;let r=t==="win32"?"\\":"/",o=t==="win32"?"%USERPROFILE%":"~";return t==="win32"?n.toLowerCase()===e.toLowerCase()?o:n.toLowerCase().startsWith(`${e.toLowerCase()}${r}`)?o+n.slice(e.length):n:n===e?o:n.startsWith(`${e}${r}`)?o+n.slice(e.length):n}function w(n){try{return realpathSync(n)}catch{return n}}function y(n={}){let e=n.platform??process.platform,t=n.execPath??process.execPath,r=n.codePath??w(fileURLToPath(import.meta.url)),o=/^(.*)[/\\](?:dist[/\\][^/\\]+\.js|src[/\\]core[/\\]runtime\.ts)$/.exec(r)?.[1];return {version:a$1,node:n.nodeVersion??process.versions.node,execPath:t,nodeManager:v(t,e),entry:o??r,where:h(r,e)}}function b(n){let e=[n.channel];return n.boundTo&&e.push(`bound to Node ${n.boundTo} (${n.manager})`),n.ephemeral&&e.push("temporary location"),e.join(", ")}function M(n,e,t=[],r=homedir(),o=process.platform){let i=d=>x(d,r,o);return [`devcoach ${e.version} ${n}`,`node ${e.node} via ${e.nodeManager} (${i(e.execPath)})`,`${b(e.where)} (${i(e.entry)})`,...t].join(" \xB7 ")}
2
+ import {a as a$1}from'./chunk-ACFVSHDR.js';import {realpathSync}from'fs';import {homedir}from'os';import {fileURLToPath}from'url';var a=24;function c(n,e=process.platform){let t=n.replace(/\\/g,"/");return e==="win32"?t.toLowerCase():t}var g=[["nvm",/\/\.nvm\/versions\/node\/(v?[\d.]+[^/]*)\//],["nvm-windows",/\/appdata\/roaming\/nvm\/(v?[\d.]+[^/]*)\//],["fnm",/\/fnm\/node-versions\/(v?[\d.]+[^/]*)\//],["volta",/\/\.volta\/tools\/image\/node\/(v?[\d.]+[^/]*)\//],["volta",/\/volta\/tools\/image\/node\/(v?[\d.]+[^/]*)\//],["asdf",/\/\.asdf\/installs\/nodejs\/(v?[\d.]+[^/]*)\//],["mise",/\/mise\/installs\/node\/(v?[\d.]+[^/]*)\//],["n",/\/n\/versions\/node\/(v?[\d.]+[^/]*)\//]];function u(n){for(let[e,t]of g){let r=t.exec(n);if(r?.[1])return {manager:e,boundTo:r[1].startsWith("v")?r[1]:`v${r[1]}`}}return null}function p(n){let e=n.toLowerCase();return e.includes("/.claude/plugins/data/")||e.includes("/.claude/plugins/cache/")?"claude-plugin":e.includes("/.devcoach/gemini-ext/")?"gemini-extension":e.includes("/claude extensions/")?"claude-desktop-extension":e.includes("/_npx/")?"npx":e.includes("/cellar/devcoach/")?"homebrew":e.includes("/node_modules/devcoach/")?"npm-global":/\/(src\/core\/runtime\.ts|src\/bin\.ts|dist\/[^/]+\.js)$/.test(e)?"source":"unknown"}function h(n,e=process.platform){let t=c(n,e),r=u(t),o=t.toLowerCase();return {channel:p(t),manager:r?.manager??(o.includes("/fnm_multishells/")?"fnm":null),boundTo:r?.boundTo??null,ephemeral:o.includes("/_npx/")||o.includes("/fnm_multishells/")}}function v(n,e=process.platform){let t=c(n,e),r=t.toLowerCase(),o=u(t);return o?o.manager:r.includes("/fnm_multishells/")?"fnm":r.includes("/.volta/")||r.includes("/volta/")?"volta":r.includes("/cellar/node")||r.includes("/opt/homebrew/")||r.includes("/linuxbrew/")||r.startsWith("/usr/local/opt/node")?"homebrew":r.startsWith("/usr/bin/")||r.startsWith("/usr/local/bin/")||r.includes("/program files/nodejs/")||r.startsWith("/snap/")?"system":"unknown"}function R(n){return Number.parseInt(n.split(".")[0]??"0",10)>=a?null:`devcoach ${a$1} needs Node ${a} or newer (it uses the built-in node:sqlite); this is Node ${n}`}function x(n,e=homedir(),t=process.platform){if(!e)return n;let r=t==="win32"?"\\":"/",o=t==="win32"?"%USERPROFILE%":"~";return t==="win32"?n.toLowerCase()===e.toLowerCase()?o:n.toLowerCase().startsWith(`${e.toLowerCase()}${r}`)?o+n.slice(e.length):n:n===e?o:n.startsWith(`${e}${r}`)?o+n.slice(e.length):n}function w(n){try{return realpathSync(n)}catch{return n}}function y(n={}){let e=n.platform??process.platform,t=n.execPath??process.execPath,r=n.codePath??w(fileURLToPath(import.meta.url)),o=/^(.*)[/\\](?:dist[/\\][^/\\]+\.js|src[/\\]core[/\\]runtime\.ts)$/.exec(r)?.[1];return {version:a$1,node:n.nodeVersion??process.versions.node,execPath:t,nodeManager:v(t,e),entry:o??r,where:h(r,e)}}function b(n){let e=[n.channel];return n.boundTo&&e.push(`bound to Node ${n.boundTo} (${n.manager})`),n.ephemeral&&e.push("temporary location"),e.join(", ")}function M(n,e,t=[],r=homedir(),o=process.platform){let i=d=>x(d,r,o);return [`devcoach ${e.version} ${n}`,`node ${e.node} via ${e.nodeManager} (${i(e.execPath)})`,`${b(e.where)} (${i(e.entry)})`,...t].join(" \xB7 ")}
3
3
  export{a,h as b,R as c,x as d,y as e,b as f,M as g};
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import {q,E as E$1,A,ba,na,Ja,p,D,Ia}from'./chunk-ZFCI6ROC.js';import {existsSync,mkdirSync,statSync,writeFileSync,appendFileSync,fstatSync,readFileSync}from'fs';import {join}from'path';function N(e){try{let o=JSON.parse(e);return {stop_hook_active:o?.stop_hook_active===!0,permission_mode:typeof o?.permission_mode=="string"?o.permission_mode:null,session_id:typeof o?.session_id=="string"?o.session_id:null}}catch{return {stop_hook_active:false,permission_mode:null,session_id:null}}}function l(){let e={stop_hook_active:false,permission_mode:null,session_id:null};try{let o=fstatSync(0);return !o.isFIFO()&&!o.isFile()&&!o.isSocket()?e:N(readFileSync(0,"utf8"))}catch{return e}}function $(e){return e.stop_hook_active||e.permission_mode==="plan"}function s(e,o,t){if(process.env.DEVCOACH_HOOK_DEBUG==="1")try{mkdirSync(p,{recursive:!0});let n=join(p,"hook.log");existsSync(n)&&statSync(n).size>256*1024&&writeFileSync(n,""),appendFileSync(n,`${new Date().toISOString()} ${e} ${(o??"-").slice(0,8)} ${t}
2
+ import {q,E as E$1,A,ba,na,Ja,p,D,Ia}from'./chunk-O53WOENG.js';import {existsSync,mkdirSync,statSync,writeFileSync,appendFileSync,fstatSync,readFileSync}from'fs';import {join}from'path';function N(e){try{let o=JSON.parse(e);return {stop_hook_active:o?.stop_hook_active===!0,permission_mode:typeof o?.permission_mode=="string"?o.permission_mode:null,session_id:typeof o?.session_id=="string"?o.session_id:null}}catch{return {stop_hook_active:false,permission_mode:null,session_id:null}}}function l(){let e={stop_hook_active:false,permission_mode:null,session_id:null};try{let o=fstatSync(0);return !o.isFIFO()&&!o.isFile()&&!o.isSocket()?e:N(readFileSync(0,"utf8"))}catch{return e}}function $(e){return e.stop_hook_active||e.permission_mode==="plan"}function s(e,o,t){if(process.env.DEVCOACH_HOOK_DEBUG==="1")try{mkdirSync(p,{recursive:!0});let n=join(p,"hook.log");existsSync(n)&&statSync(n).size>256*1024&&writeFileSync(n,""),appendFileSync(n,`${new Date().toISOString()} ${e} ${(o??"-").slice(0,8)} ${t}
3
3
  `);}catch{}}var E={claude:"block",gemini:"deny",codex:"block"};function u(e,o,t){let n={decision:E[e],reason:o};t&&(n.systemMessage=t),process.stdout.write(`${JSON.stringify(n)}
4
4
  `),process.exit(0);}var g="devcoach: the user has no coaching profile yet. Ask them how they want to set it up and STRONGLY recommend Automatic \u2014 devcoach scans their full Claude Code history across all their projects (call get_onboarding for the detected stack and per-project evidence) so the profile reflects what they actually build. Also offer Guided (a short conversation to map topics and confidence levels) and Import (restore from a backup), but lead with Automatic; do not pick for them. After they choose, build the profile and save it by calling the devcoach `complete_onboarding` MCP tool (not a shell command) before ending your turn.";function I(e=l()){if($(e)&&process.exit(0),existsSync(q)){let o;try{o=E$1(t=>A(t)!==null||ba(t).knowledge_ready);}catch(t){s("onboard-hook",e.session_id,`error: ${t}`),process.exit(0);}o&&process.exit(0);}s("onboard-hook",e.session_id,"onboarding cue"),u("claude",g);}var a=10,w={claude:{cue:"Invoke the `devcoach` skill (Skill tool)",prime:"invoke the `devcoach` skill (Skill tool)"},gemini:{cue:"Activate the `devcoach` skill (activate_skill tool)",prime:"activate the `devcoach` skill (activate_skill tool)"},codex:{cue:"Activate the `devcoach` skill",prime:"activate the `devcoach` skill"}};function T(e,o="claude"){let t=e%a===0,n=Math.ceil(e/a)*a,i=t?`This is lesson #${e} \u2014 a notebook checkpoint (every ${a} lessons): after log_lesson returns the feedback, also read references/calibration.md in the devcoach skill and run its notebook-update step (a direct file write), recording "(updated after ${e} lessons)".`:`Skip the notebook update this time \u2014 it is refreshed only every ${a} lessons (next checkpoint at lesson #${n}).`;return `devcoach: a lesson is due for the technical work just completed. Do it now \u2014 do not acknowledge this message and do not explain what you are about to do.
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import {e,g,b,d as d$1}from'./chunk-R44CZZD2.js';export{c as buildLessonCue,d as cmdLessonReady,b as cmdOnboardHook,g as cmdPromptHook,e as cmdStopHook,a as parseHookPayload}from'./chunk-R44CZZD2.js';import {a as a$1,d as d$2,e as e$1,g as g$2,b as b$2,f as f$1}from'./chunk-QU6UQJWC.js';import {c as c$1,b as b$1,a as a$2,e as e$4,f as f$2}from'./chunk-MRABMPXO.js';import {e as e$3,f,b as b$3,a as a$3}from'./chunk-WRY2DLSB.js';import {h,m,g as g$1,e as e$2,d as d$3,n,o,p as p$1,q as q$1}from'./chunk-M3XX2DV7.js';import {E,A as A$1,Ca,H as H$1,O,K,I as I$1,Ea,X,ta,Y,w,sa,za,Aa,xa,wa,ua,Ga,ra,Ba,Fa,v,R,V as V$1,S,T,U as U$1,W,oa,Q,L,r,qa,p,q,y,B as B$1,x,ba,Z,$,Ja}from'./chunk-ZFCI6ROC.js';import {a}from'./chunk-FL5OOXCF.js';import {spawn,spawnSync,execFileSync}from'child_process';import {writeFileSync,existsSync,readFileSync,rmSync,mkdirSync,realpathSync,openSync,readSync,closeSync,statSync,accessSync,constants}from'fs';import {createInterface}from'readline/promises';import {text}from'stream/consumers';import {Command}from'commander';import {homedir,platform}from'os';import {join,dirname}from'path';var j=e=>e==="win32",oe=e=>e.includes("/")||e.includes("\\");function pt(e,o){return e.endsWith("/")||e.endsWith("\\")?e+o:`${e}${e.includes("\\")&&!e.includes("/")?"\\":"/"}${o}`}function Ao(e,o){try{return statSync(e).isFile()?(j(o)||accessSync(e,constants.X_OK),!0):!1}catch{return false}}function B(e,o={}){let n=o.platform??process.platform,r=j(n)?/\.[a-z0-9]+$/i.test(e)?[""]:(o.pathext??process.env.PATHEXT??".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean).map(i=>i.toLowerCase()):[""];if(oe(e)){for(let i of r)if(Ao(e+i,n))return e+i;return null}let s=o.path??process.env.PATH??"";for(let i of s.split(j(n)?";":":"))if(i)for(let l of r){let a=pt(i,e+l);if(Ao(a,n))return a}return null}function gt(e){let o=null;try{o=openSync(e,"r");let n=Buffer.alloc(256),r=readSync(o,n,0,n.length,0),s=n.subarray(0,r).toString("utf8");return s.startsWith("#!")&&s.slice(2).split(/\r?\n/)[0]?.trim()||null}catch{return null}finally{o!==null&&closeSync(o);}}var Ho={command:"npx",args:["-y","devcoach","mcp"]};function Ro(e){let{hit:o,entry:n,execPath:r,platform:s}=e;if(!o)return Ho;let i=b$3(j(s)?n:o,s);if(i.ephemeral||b$3(n,s).ephemeral)return Ho;let l=i.boundTo?`bound to Node ${i.boundTo} (${i.manager}) \u2014 after removing or switching that version, run \`devcoach install --force\` again`:void 0;return j(s)?{command:r,args:[n,"mcp"],note:l}:{command:o,args:["mcp"],note:l}}function Mo(e=process.platform,o=process.env,n=homedir()){if(e==="darwin")return join(n,"Library","Logs","Claude","mcp-server-devcoach.log");if(e==="win32"){let r=o.APPDATA??join(n,"AppData","Roaming");return join(r,"Claude","logs","mcp-server-devcoach.log")}return join(o.XDG_CONFIG_HOME??join(n,".config"),"Claude","logs","mcp-server-devcoach.log")}function To(e,o=process.platform){let n=e.split(/\r?\n/),r=null;for(let i=n.length-1;i>=0&&r===null;i--){if(!/Using MCP server command:/.test(n[i]??""))continue;let l=[];for(let a=i+1;a<n.length&&a<i+80;a++){let u=n[a]??"";if(/\[length\]:/.test(u)||/^\S/.test(u))break;let g=/^\s*'(.+)',?\s*$/.exec(u);g?.[1]&&l.push(g[1].replace(/\\\\/g,"\\"));}l.length&&(r=l.join(j(o)?";":":"));}let s=null;for(let i=n.length-1;i>=0&&s===null;i--){let l=n[i]??"";if(/^Failed to spawn process/.test(l))s=l.trim();else if(/\[error\]/.test(l)){let a=(n[i-1]??"").trim();s=/^Failed to spawn process/.test(a)?a:l.replace(/\s*\{ metadata:.*$/,"").replace(/^\S+Z \[devcoach\] \[error\] /,"").trim();}}return {path:r,lastError:s}}function ft(e,o){let n=spawnSync(e,o,{encoding:"utf8",timeout:5e3,shell:process.platform==="win32"&&/\.(cmd|bat)$/i.test(e)});return n.status===0?(n.stdout??"").trim():null}var ye="`Failed to spawn process: No such file or directory`";function we(e,o){let n=o.platform??process.platform,r=o.run??ft,s=[],{command:i}=e,l=e.args??[];if(i==="npx")return s.push({level:"warn",text:"starts via `npx -y devcoach` \u2014 needs the npx cache (or the network) at every start"}),s;let a$1=B(i,{...o,platform:n});if(!a$1)return s.push({level:"bad",text:`\`${i}\` is not found ${oe(i)?"":`on ${o.pathSource} `}\u2014 the host fails with ${ye}. Reinstall devcoach, then run \`devcoach install --force\``}),s;s.push({level:"ok",text:`command resolves to ${a$1}${oe(i)?"":` on ${o.pathSource}`}`}),oe(i)||s.push({level:"warn",text:"bare name \u2014 the host resolves it on its own PATH, which can differ from your shell's; `devcoach install --force` writes an absolute path"});let u=/(^|[/\\])node(\.exe)?$/i.test(a$1),g=u?l[0]:a$1;if(u&&(!g||!existsSync(g)))return s.push({level:"bad",text:`devcoach's script ${g??"(missing)"} does not exist`}),s;let p=b$3(g??a$1,n);p.ephemeral?s.push({level:"bad",text:"lives in a temporary location (npx cache or an fnm shell) \u2014 it will disappear; run `devcoach install --force`"}):p.boundTo&&s.push({level:"warn",text:`bound to Node ${p.boundTo} (${p.manager}) \u2014 removing or switching that version breaks it with ${ye}; run \`devcoach install --force\` after`});let m=u?a$1:null;if(!u&&/\.(cmd|bat)$/i.test(a$1)){if(m=B("node",{...o,platform:n}),!m)return s.push({level:"bad",text:`the \`${a$1}\` shim needs \`node\`, not found on ${o.pathSource}`}),s}else if(!u){let f=gt(a$1);if(f){let[k,...y]=f.split(/\s+/),S=/(^|\/)env$/.test(k??""),O=S?y.find(K=>!K.startsWith("-")):k;if(m=O?S?B(O,{...o,platform:n}):O:null,!m||!S&&!existsSync(m))return s.push({level:"bad",text:S?`its \`#!${f}\` finds no \`${O}\` on ${o.pathSource} \u2014 the host fails before devcoach starts`:`its interpreter ${O} does not exist \u2014 the host fails with ${ye}. Reinstall that Node (e.g. \`brew install node\`) or devcoach`}),s}}if(m){let f=r(m,["--version"]),k=f?Number.parseInt(f.replace(/^v/,"").split(".")[0]??"0",10):null;k!==null&&k<a$3?s.push({level:"bad",text:`runs on Node ${f} (${m}) \u2014 devcoach needs Node ${a$3} or newer`}):f&&s.push({level:"ok",text:`runs on Node ${f.replace(/^v/,"")} (${m})`});}let $=(u?r(a$1,[g,"--version"]):r(a$1,["--version"]))?.match(/(\d+\.\d+\.\d+[^\s]*)/)?.[1]??null;return $?$!==a?s.push({level:"warn",text:`it runs devcoach ${$}; this doctor is ${a}`}):s.push({level:"ok",text:`it runs devcoach ${$}`}):s.push({level:"warn",text:"could not run it to read its version"}),s}var c=(e="")=>{console.log(e);};function se(){let e=platform();if(e==="darwin")return join(homedir(),"Library","Application Support","Claude","claude_desktop_config.json");if(e==="win32"){let n=process.env.APPDATA??join(homedir(),"AppData","Roaming");return join(n,"Claude","claude_desktop_config.json")}let o=process.env.XDG_CONFIG_HOME??join(homedir(),".config");return join(o,"Claude","claude_desktop_config.json")}var N=join(homedir(),".claude","settings.json"),ie=join(homedir(),".claude","skills","devcoach"),De=join(homedir(),".gemini"),I=join(De,"settings.json"),yt=join(De,"extensions","devcoach","gemini-extension.json"),Ie=join(homedir(),".codex"),A=join(Ie,"hooks.json"),ne=join(Ie,"config.toml"),H=join(homedir(),".agents","skills","devcoach"),Oe=e=>B(e),Ne=e=>Oe(e)!==null;function wt(){try{return realpathSync(process.argv[1]??"")}catch{return process.argv[1]??""}}function Uo(){return Ro({hit:Oe("devcoach"),entry:wt(),execPath:process.execPath,platform:process.platform})}function Ee(){let e=Oe("devcoach");return !e||e.includes("/_npx/")||e.includes("\\_npx\\")?"npx -y devcoach":e.includes("fnm_multishells")?"devcoach":e.includes(" ")?`"${e}"`:e}function jo(e,o,n){if(!Ne(e))return "";n&&spawnSync(e,n,{encoding:"utf8"});let r=spawnSync(e,o,{encoding:"utf8"});return r.status===0?`${a$1.green("\u2713")} Registered via \`${e} mcp add\``:`${r.stderr??""}${r.stdout??""}`.toLowerCase().includes("already")?`${a$1.yellow("Already registered")} in ${e} (use --force to overwrite)`:`${a$1.red(`${e} mcp add failed:`)} ${(r.stderr||r.stdout||"").trim()}`}function Ct(e,o){if(!Ne("claude"))return "";o&&spawnSync("claude",["mcp","remove","--scope",e,"devcoach"],{encoding:"utf8"});let n=Uo(),r=spawnSync("claude",["mcp","add","--scope",e,"devcoach",n.command,"--",...n.args],{encoding:"utf8"});return r.status===0?`${a$1.green("\u2713")} Registered via \`claude mcp add\` (scope: ${e})`:`${r.stderr??""}${r.stdout??""}`.toLowerCase().includes("already")?`${a$1.yellow("Already registered")} in Claude Code (use --force to overwrite)`:`${a$1.red("claude mcp add failed:")} ${(r.stderr||r.stdout||"").trim()}`}function C(e){if(!existsSync(e))return {ok:true,data:{}};try{return {ok:!0,data:JSON.parse(readFileSync(e,"utf8"))??{}}}catch{return {ok:false,error:`${a$1.red("\u2717")} ${e} is not valid JSON \u2014 fix it and re-run (left unchanged)`}}}function Ce(e,o,n){let r=C(e);if(!r.ok)return r.error;let s=r.data;s.mcpServers??={};let i=s.mcpServers;return i.devcoach&&!n?`${a$1.yellow("Already registered")} in ${e} (use --force to overwrite)`:(i.devcoach=o,mkdirSync(dirname(e),{recursive:true}),writeFileSync(e,`${JSON.stringify(s,null,2)}
2
+ import {e,g,b,d as d$1}from'./chunk-XNGALLYP.js';export{c as buildLessonCue,d as cmdLessonReady,b as cmdOnboardHook,g as cmdPromptHook,e as cmdStopHook,a as parseHookPayload}from'./chunk-XNGALLYP.js';import {a as a$1,d as d$2,e as e$1,g as g$2,b as b$2,f as f$1}from'./chunk-QU6UQJWC.js';import {c as c$1,b as b$1,a as a$2,e as e$4,f as f$2}from'./chunk-MRABMPXO.js';import {e as e$3,f,b as b$3,a as a$3}from'./chunk-QRNH4GKU.js';import {h,m,g as g$1,e as e$2,d as d$3,n,o,p as p$1,q as q$1}from'./chunk-IHT2QFRV.js';import {E,A as A$1,Ca,H as H$1,O,K,I as I$1,Ea,X,ta,Y,w,sa,za,Aa,xa,wa,ua,Ga,ra,Ba,Fa,v,R,V as V$1,S,T,U as U$1,W,oa,Q,L,r,qa,p,q,y,B as B$1,x,ba,Z,$,Ja}from'./chunk-O53WOENG.js';import {a}from'./chunk-ACFVSHDR.js';import {spawn,spawnSync,execFileSync}from'child_process';import {writeFileSync,existsSync,readFileSync,rmSync,mkdirSync,realpathSync,openSync,readSync,closeSync,statSync,accessSync,constants}from'fs';import {createInterface}from'readline/promises';import {text}from'stream/consumers';import {Command}from'commander';import {homedir,platform}from'os';import {join,dirname}from'path';var j=e=>e==="win32",oe=e=>e.includes("/")||e.includes("\\");function pt(e,o){return e.endsWith("/")||e.endsWith("\\")?e+o:`${e}${e.includes("\\")&&!e.includes("/")?"\\":"/"}${o}`}function Ao(e,o){try{return statSync(e).isFile()?(j(o)||accessSync(e,constants.X_OK),!0):!1}catch{return false}}function B(e,o={}){let n=o.platform??process.platform,r=j(n)?/\.[a-z0-9]+$/i.test(e)?[""]:(o.pathext??process.env.PATHEXT??".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean).map(i=>i.toLowerCase()):[""];if(oe(e)){for(let i of r)if(Ao(e+i,n))return e+i;return null}let s=o.path??process.env.PATH??"";for(let i of s.split(j(n)?";":":"))if(i)for(let l of r){let a=pt(i,e+l);if(Ao(a,n))return a}return null}function gt(e){let o=null;try{o=openSync(e,"r");let n=Buffer.alloc(256),r=readSync(o,n,0,n.length,0),s=n.subarray(0,r).toString("utf8");return s.startsWith("#!")&&s.slice(2).split(/\r?\n/)[0]?.trim()||null}catch{return null}finally{o!==null&&closeSync(o);}}var Ho={command:"npx",args:["-y","devcoach","mcp"]};function Ro(e){let{hit:o,entry:n,execPath:r,platform:s}=e;if(!o)return Ho;let i=b$3(j(s)?n:o,s);if(i.ephemeral||b$3(n,s).ephemeral)return Ho;let l=i.boundTo?`bound to Node ${i.boundTo} (${i.manager}) \u2014 after removing or switching that version, run \`devcoach install --force\` again`:void 0;return j(s)?{command:r,args:[n,"mcp"],note:l}:{command:o,args:["mcp"],note:l}}function Mo(e=process.platform,o=process.env,n=homedir()){if(e==="darwin")return join(n,"Library","Logs","Claude","mcp-server-devcoach.log");if(e==="win32"){let r=o.APPDATA??join(n,"AppData","Roaming");return join(r,"Claude","logs","mcp-server-devcoach.log")}return join(o.XDG_CONFIG_HOME??join(n,".config"),"Claude","logs","mcp-server-devcoach.log")}function To(e,o=process.platform){let n=e.split(/\r?\n/),r=null;for(let i=n.length-1;i>=0&&r===null;i--){if(!/Using MCP server command:/.test(n[i]??""))continue;let l=[];for(let a=i+1;a<n.length&&a<i+80;a++){let u=n[a]??"";if(/\[length\]:/.test(u)||/^\S/.test(u))break;let g=/^\s*'(.+)',?\s*$/.exec(u);g?.[1]&&l.push(g[1].replace(/\\\\/g,"\\"));}l.length&&(r=l.join(j(o)?";":":"));}let s=null;for(let i=n.length-1;i>=0&&s===null;i--){let l=n[i]??"";if(/^Failed to spawn process/.test(l))s=l.trim();else if(/\[error\]/.test(l)){let a=(n[i-1]??"").trim();s=/^Failed to spawn process/.test(a)?a:l.replace(/\s*\{ metadata:.*$/,"").replace(/^\S+Z \[devcoach\] \[error\] /,"").trim();}}return {path:r,lastError:s}}function ft(e,o){let n=spawnSync(e,o,{encoding:"utf8",timeout:5e3,shell:process.platform==="win32"&&/\.(cmd|bat)$/i.test(e)});return n.status===0?(n.stdout??"").trim():null}var ye="`Failed to spawn process: No such file or directory`";function we(e,o){let n=o.platform??process.platform,r=o.run??ft,s=[],{command:i}=e,l=e.args??[];if(i==="npx")return s.push({level:"warn",text:"starts via `npx -y devcoach` \u2014 needs the npx cache (or the network) at every start"}),s;let a$1=B(i,{...o,platform:n});if(!a$1)return s.push({level:"bad",text:`\`${i}\` is not found ${oe(i)?"":`on ${o.pathSource} `}\u2014 the host fails with ${ye}. Reinstall devcoach, then run \`devcoach install --force\``}),s;s.push({level:"ok",text:`command resolves to ${a$1}${oe(i)?"":` on ${o.pathSource}`}`}),oe(i)||s.push({level:"warn",text:"bare name \u2014 the host resolves it on its own PATH, which can differ from your shell's; `devcoach install --force` writes an absolute path"});let u=/(^|[/\\])node(\.exe)?$/i.test(a$1),g=u?l[0]:a$1;if(u&&(!g||!existsSync(g)))return s.push({level:"bad",text:`devcoach's script ${g??"(missing)"} does not exist`}),s;let p=b$3(g??a$1,n);p.ephemeral?s.push({level:"bad",text:"lives in a temporary location (npx cache or an fnm shell) \u2014 it will disappear; run `devcoach install --force`"}):p.boundTo&&s.push({level:"warn",text:`bound to Node ${p.boundTo} (${p.manager}) \u2014 removing or switching that version breaks it with ${ye}; run \`devcoach install --force\` after`});let m=u?a$1:null;if(!u&&/\.(cmd|bat)$/i.test(a$1)){if(m=B("node",{...o,platform:n}),!m)return s.push({level:"bad",text:`the \`${a$1}\` shim needs \`node\`, not found on ${o.pathSource}`}),s}else if(!u){let f=gt(a$1);if(f){let[k,...y]=f.split(/\s+/),S=/(^|\/)env$/.test(k??""),O=S?y.find(K=>!K.startsWith("-")):k;if(m=O?S?B(O,{...o,platform:n}):O:null,!m||!S&&!existsSync(m))return s.push({level:"bad",text:S?`its \`#!${f}\` finds no \`${O}\` on ${o.pathSource} \u2014 the host fails before devcoach starts`:`its interpreter ${O} does not exist \u2014 the host fails with ${ye}. Reinstall that Node (e.g. \`brew install node\`) or devcoach`}),s}}if(m){let f=r(m,["--version"]),k=f?Number.parseInt(f.replace(/^v/,"").split(".")[0]??"0",10):null;k!==null&&k<a$3?s.push({level:"bad",text:`runs on Node ${f} (${m}) \u2014 devcoach needs Node ${a$3} or newer`}):f&&s.push({level:"ok",text:`runs on Node ${f.replace(/^v/,"")} (${m})`});}let $=(u?r(a$1,[g,"--version"]):r(a$1,["--version"]))?.match(/(\d+\.\d+\.\d+[^\s]*)/)?.[1]??null;return $?$!==a?s.push({level:"warn",text:`it runs devcoach ${$}; this doctor is ${a}`}):s.push({level:"ok",text:`it runs devcoach ${$}`}):s.push({level:"warn",text:"could not run it to read its version"}),s}var c=(e="")=>{console.log(e);};function se(){let e=platform();if(e==="darwin")return join(homedir(),"Library","Application Support","Claude","claude_desktop_config.json");if(e==="win32"){let n=process.env.APPDATA??join(homedir(),"AppData","Roaming");return join(n,"Claude","claude_desktop_config.json")}let o=process.env.XDG_CONFIG_HOME??join(homedir(),".config");return join(o,"Claude","claude_desktop_config.json")}var N=join(homedir(),".claude","settings.json"),ie=join(homedir(),".claude","skills","devcoach"),De=join(homedir(),".gemini"),I=join(De,"settings.json"),yt=join(De,"extensions","devcoach","gemini-extension.json"),Ie=join(homedir(),".codex"),A=join(Ie,"hooks.json"),ne=join(Ie,"config.toml"),H=join(homedir(),".agents","skills","devcoach"),Oe=e=>B(e),Ne=e=>Oe(e)!==null;function wt(){try{return realpathSync(process.argv[1]??"")}catch{return process.argv[1]??""}}function Uo(){return Ro({hit:Oe("devcoach"),entry:wt(),execPath:process.execPath,platform:process.platform})}function Ee(){let e=Oe("devcoach");return !e||e.includes("/_npx/")||e.includes("\\_npx\\")?"npx -y devcoach":e.includes("fnm_multishells")?"devcoach":e.includes(" ")?`"${e}"`:e}function jo(e,o,n){if(!Ne(e))return "";n&&spawnSync(e,n,{encoding:"utf8"});let r=spawnSync(e,o,{encoding:"utf8"});return r.status===0?`${a$1.green("\u2713")} Registered via \`${e} mcp add\``:`${r.stderr??""}${r.stdout??""}`.toLowerCase().includes("already")?`${a$1.yellow("Already registered")} in ${e} (use --force to overwrite)`:`${a$1.red(`${e} mcp add failed:`)} ${(r.stderr||r.stdout||"").trim()}`}function Ct(e,o){if(!Ne("claude"))return "";o&&spawnSync("claude",["mcp","remove","--scope",e,"devcoach"],{encoding:"utf8"});let n=Uo(),r=spawnSync("claude",["mcp","add","--scope",e,"devcoach",n.command,"--",...n.args],{encoding:"utf8"});return r.status===0?`${a$1.green("\u2713")} Registered via \`claude mcp add\` (scope: ${e})`:`${r.stderr??""}${r.stdout??""}`.toLowerCase().includes("already")?`${a$1.yellow("Already registered")} in Claude Code (use --force to overwrite)`:`${a$1.red("claude mcp add failed:")} ${(r.stderr||r.stdout||"").trim()}`}function C(e){if(!existsSync(e))return {ok:true,data:{}};try{return {ok:!0,data:JSON.parse(readFileSync(e,"utf8"))??{}}}catch{return {ok:false,error:`${a$1.red("\u2717")} ${e} is not valid JSON \u2014 fix it and re-run (left unchanged)`}}}function Ce(e,o,n){let r=C(e);if(!r.ok)return r.error;let s=r.data;s.mcpServers??={};let i=s.mcpServers;return i.devcoach&&!n?`${a$1.yellow("Already registered")} in ${e} (use --force to overwrite)`:(i.devcoach=o,mkdirSync(dirname(e),{recursive:true}),writeFileSync(e,`${JSON.stringify(s,null,2)}
3
3
  `),`${a$1.green("\u2713")} Installed into ${e}`)}function St(){let e=Ee();return {Stop:{hooks:[{type:"command",command:`${e} stop-hook`,timeout:60}]},UserPromptSubmit:{hooks:[{type:"command",command:`${e} prompt-hook`,timeout:30}]}}}function xt(){let e=Ee();return {AfterAgent:{hooks:[{type:"command",command:`${e} gemini-stop-hook`,timeout:6e4}]},BeforeAgent:{hooks:[{type:"command",command:`${e} gemini-prompt-hook`,timeout:3e4}]}}}function _t(){let e=Ee();return {Stop:{hooks:[{type:"command",command:`${e} codex-stop-hook`,timeout:60}]},UserPromptSubmit:{hooks:[{type:"command",command:`${e} codex-prompt-hook`,timeout:30}]}}}function Jo(e){return Object.entries(e.enabledPlugins??{}).filter(([o,n])=>n&&o.startsWith("devcoach@")).map(([o])=>o)}function Pe(e){return Jo(e).length>0}function ae(){return existsSync(yt)}function de(e){return /dev-?coach/i.test(e)&&/\b(?:stop-hook|prompt-hook|onboard-hook|lesson-ready)\b/.test(e)}function Ae(e,o){let n=C(e);if(!n.ok)return n.error;let r=n.data;r.hooks??={};let s=r.hooks,i=false;for(let[l,a]of Object.entries(o)){s[l]??=[];let u=s[l],g=u.map((m,h)=>[m,h]).filter(([m])=>(m.hooks??[]).some(h=>de(h.command??""))).map(([,m])=>m),[p]=g;if(!(g.length===1&&p!==void 0&&JSON.stringify(u[p])===JSON.stringify(a))){for(let m of g.toReversed())u.splice(m,1);u.push(a),i=true;}}return i?(mkdirSync(dirname(e),{recursive:true}),writeFileSync(e,`${JSON.stringify(r,null,2)}
4
4
  `),`${a$1.green("\u2713")} Hooks installed into ${e} (${Object.keys(o).join(" + ")})`):`${a$1.yellow("Already installed")} in ${e} (current layout)`}function Lt(){let e=C(N);return e.ok?Pe(e.data)?`${a$1.yellow("Skipped")} \u2014 the devcoach plugin already provides the hooks (installing both would double-count interactions)`:Ae(N,St()):e.error}function Dt(){return ae()?`${a$1.yellow("Skipped")} \u2014 the devcoach Gemini extension already provides the hooks (installing both would double-count interactions)`:Ae(I,xt())}function V(e){if(!existsSync(join(e,"SKILL.md")))return "missing";try{if(readFileSync(join(e,".devcoach-version"),"utf8").trim()===a)return "current"}catch{}return "outdated"}function Le(e,o){if(!o&&V(e)==="current")return `${a$1.yellow("Already installed")} in ${e} (current version)`;mkdirSync(e,{recursive:true}),writeFileSync(join(e,"SKILL.md"),e$4());let n=f$2();if(n.length){let r=join(e,"references");mkdirSync(r,{recursive:true});for(let s of n)writeFileSync(join(r,s.name),s.content);}return writeFileSync(join(e,".devcoach-version"),`${a}
5
5
  `),`${a$1.green("\u2713")} Installed into ${e}`}function It(e){return ae()?`${a$1.yellow("Skipped")} \u2014 bundled with the devcoach Gemini extension`:Le(H,e)}function U(e,o){let n=C(e);return n.ok&&(n.data.hooks?.[o]??[]).some(r=>(r.hooks??[]).some(s=>de(s.command??"")))}function He(){let e=[{name:"Claude Code",dir:ie,cmd:"devcoach install",hooked:()=>U(N,"Stop")},{name:"Gemini/Codex",dir:H,cmd:"devcoach install --gemini (or --codex)",hooked:()=>U(I,"AfterAgent")||U(A,"Stop")}];for(let o of e){let n=V(o.dir);if(n==="outdated")return `${a$1.yellow("\u2192")} The devcoach ${o.name} skill is out of date \u2014 run ${a$1.bold(o.cmd)} to refresh it.`;if(n==="missing"&&o.hooked())return `${a$1.yellow("\u2192")} The devcoach ${o.name} skill is not installed \u2014 run ${a$1.bold(o.cmd)} to add it.`}return ""}function Vo(e){let o=e.claudeCode||e.claudeDesktop||e.gemini||e.codex,n=e.claudeCode||!o,r=e.claudeDesktop||!o,s=Uo(),i={command:s.command,args:s.args},l=false;if(c(a$1.bold("Setting up devcoach")+a$1.dim(` (${i.command} ${i.args.join(" ")})`)),s.note&&c(a$1.yellow(` ${s.note}`)),c(),n){c(a$1.bold("Claude Code"));let a=Ct("user",e.force);if(!a){let u=join(homedir(),".claude.json");a=Ce(u,{type:"stdio",env:{},...i},e.force),l=true;}c(` MCP server\u2026 ${a}`),e.skipHook||c(` Hooks\u2026 ${Lt()}`),c(` Skill\u2026 ${Le(ie,e.force)}`),c();}if(r&&(c(a$1.bold("Claude Desktop")),c(` MCP server\u2026 ${Ce(se(),i,e.force)}`),l=true,c()),e.gemini){c(a$1.bold("Gemini CLI (beta)"));let a=jo("gemini",["mcp","add","-s","user","devcoach",i.command,...i.args],e.force?["mcp","remove","-s","user","devcoach"]:null);a||(a=Ce(I,i,e.force)),c(` MCP server\u2026 ${a}`),e.skipHook||c(` Hooks\u2026 ${Dt()}`),c(` Skill\u2026 ${It(e.force)}`),c();}if(e.codex){c(a$1.bold("Codex CLI (beta)"));let a=jo("codex",["mcp","add","devcoach","--",i.command,...i.args],e.force?["mcp","remove","devcoach"]:null);a||(a=`${a$1.yellow("Manual step")} \u2014 codex CLI not found. Add to ${ne}:
@@ -34,4 +34,4 @@ ${a$1.green("Setup complete!")} ${Object.keys(i).length} topics saved.`);}finall
34
34
  ${a$1.bold(`devcoach ${a$1.dim(`v${a}`)}`)}`);for(let[n,r]of e)d(` ${a$1.cyan(`devcoach ${n}`.padEnd(32))} ${a$1.dim(r)}`);d(`
35
35
  ${a$1.dim("Run")} ${a$1.bold("devcoach <command> --help")} ${a$1.dim("for per-command options.")}
36
36
  `);let o=He();o&&d(`${o}
37
- `);}function _(e){return e??null}function mn(){let e$1=new Command;return e$1.exitOverride(),e$1.name("devcoach").description("devcoach \u2014 learn what you don't know yet from the code your AI agent writes").version(`devcoach ${a}`,"-v, --version","output the version number").allowExcessArguments(false).showHelpAfterError("(add --help for usage)").action(()=>un()),e$1.command("mcp").description("Start the MCP server (stdio transport)").action(async()=>{let{runStdio:o}=await import('./server-RIRBIWLN.js');await o();}),e$1.command("profile").description("Show the knowledge map").action(Bt),e$1.command("lessons").description("List past lessons").option("--period <period>","today | week | month | year | all","all").option("--category <category>","Filter by category tag").option("--project <project>","Filter by project (fuzzy)").option("--repository <repository>","Filter by repository (fuzzy)").option("--branch <branch>","Filter by branch (fuzzy)").option("--commit <commit>","Filter by commit hash prefix (fuzzy)").option("--starred","Show only starred lessons").option("--imported","Show only lessons shared with you").option("--from <name>","Show only lessons shared by this person").option("--feedback <feedback>","know | understood | dont_know | none").option("--level <level>","junior | mid | senior").option("--date-from <date>","Show lessons on or after this date (YYYY-MM-DD[THH:MM])").option("--date-to <date>","Show lessons on or before this date (YYYY-MM-DD[THH:MM])").option("--sort <column>","timestamp | level | topic_id | title | feedback","timestamp").option("--order <order>","asc | desc","desc").action(o=>Ut({period:o.period??"all",category:_(o.category),project:_(o.project),repository:_(o.repository),branch:_(o.branch),commit:_(o.commit),starred:!!o.starred,imported:!!o.imported,sharedBy:_(o.from),feedback:_(o.feedback),level:_(o.level),dateFrom:_(o.dateFrom),dateTo:_(o.dateTo),sort:o.sort??"timestamp",order:o.order??"desc"})),e$1.command("lesson").description("Show a single lesson in detail").argument("<id>","Lesson ID").action(o=>Jt(o)),e$1.command("courses").description("List your step-by-step courses").action(()=>Kt()),e$1.command("course").description("Show a course: steps, progress, document path").argument("<id>","Course ID").action(o=>zt(o)),e$1.command("star").description("Mark a lesson as starred").argument("<id>","Lesson ID").action(o=>Zo(o,true)),e$1.command("unstar").description("Remove the starred mark from a lesson").argument("<id>","Lesson ID").action(o=>Zo(o,false)),e$1.command("delete").description("Permanently delete a lesson").argument("<id>","Lesson ID").action(o=>Vt(o)),e$1.command("feedback").description("Record your answer under a lesson: know / understood / dont_know").argument("<id>","Lesson ID").argument("<value>","know | understood | dont_know | clear").action((o,n)=>Wt(o,n)),e$1.command("share").description("Share a lesson: copyable text (default), --link, or a .devcoach.md file").argument("[id]","Lesson ID (or use --last)").option("--last","Share the most recent lesson").option("--link","Print the share link only").option("--file [path]","Write a .devcoach.md file (default: <lesson-id>.devcoach.md)").option("--with-context","Include project/branch/commit/task context (off by default)").option("--by <name>","Sender name (remembered as share_name when unset)").option("--anonymous","Share without a sender name").action((o,n)=>sn(o,n)),e$1.command("import").description("Import a shared lesson: a devcoach:lesson code, a share link, a URL, a .devcoach.md or lessons .json file, - for stdin \u2014 or the clipboard when omitted").argument("[source]","Code, link, URL, file path, or - (stdin); omit to read the clipboard").action(o=>cn(o)),e$1.command("settings").description("Show current settings").action(Xt),e$1.command("stats").description("Coaching statistics and rate-limit status").action(Yt),e$1.command("set").description("Update a setting (max_per_day | min_gap_minutes | nudge_every | nudge_scope | share_name)").argument("<key>","Setting key").argument("<value>","New value").action((o,n)=>Zt(o,n)),e$1.command("knowledge-add").description("Add or update a topic in the knowledge map").argument("<topic>","Topic ID").option("--confidence <n>","Initial confidence 0-10","5").option("--group <group>","Assign to a named group").action((o,n)=>qt(o,Math.max(0,Math.min(10,Number.parseInt(n.confidence??"5",10)||5)),_(n.group))),e$1.command("knowledge-remove").description("Remove a topic from the knowledge map").argument("<topic>","Topic ID").action(o=>Qt(o)),e$1.command("group-add").description("Register a new knowledge group").argument("<name>","Group name").action(o=>en(o)),e$1.command("group-remove").description("Delete a knowledge group (topics move to Other)").argument("<name>","Group name").action(o=>on(o)),e$1.command("group-assign").description("Move a topic to a group").argument("<topic>","Topic ID").argument("<group>","Group name (use 'Other' to ungroup)").action((o,n)=>tn(o,n)),e$1.command("backup").description("Export a full backup (settings + knowledge + lessons) as zip").argument("[file]","Output zip path","devcoach-backup.zip").action(o=>nn(o)),e$1.command("restore").description("Restore from a backup zip file").argument("<file>","Path to backup zip").action(o=>rn(o)),e$1.command("install").description("Register the MCP server + hooks + skill in Claude Code / Claude Desktop (default), Gemini CLI (--gemini, beta), or Codex CLI (--codex, beta)").option("--claude-code","Target Claude Code only").option("--claude-desktop","Target Claude Desktop only").option("--gemini","Target Google Gemini CLI (beta)").option("--codex","Target OpenAI Codex CLI (beta)").option("--force","Overwrite existing devcoach entry").option("--skip-hook","Register MCP server only \u2014 skip the hooks").action(o=>Vo({claudeCode:!!o.claudeCode,claudeDesktop:!!o.claudeDesktop,gemini:!!o.gemini,codex:!!o.codex,force:!!o.force,skipHook:!!o.skipHook})),e$1.command("uninstall").description("Remove devcoach's MCP server, hooks and skill from Claude Code / Claude Desktop (default), Gemini CLI (--gemini), Codex CLI (--codex) or every agent (--all). Coaching data is kept unless --data is given. Run it before brew/npm uninstall.").option("--claude-code","Target Claude Code only").option("--claude-desktop","Target Claude Desktop only").option("--gemini","Also remove from Google Gemini CLI (beta)").option("--codex","Also remove from OpenAI Codex CLI (beta)").option("--all","Every agent: Claude Code, Claude Desktop, Gemini CLI, Codex CLI").option("--data","Also delete ~/.devcoach (lessons, profile, notebook) \u2014 asks first").option("--yes","Do not ask before deleting the data (with --data)").action(async o=>Xo({claudeCode:!!o.claudeCode,claudeDesktop:!!o.claudeDesktop,gemini:!!o.gemini,codex:!!o.codex,all:!!o.all,data:!!o.data,yes:!!o.yes})),e$1.command("doctor").description("Diagnose the devcoach \u21C4 agent wiring and the lesson pacing state").action(Wo),e$1.command("ui").description("Launch the web dashboard (Ctrl+C stops it gracefully; --stop stops a running one)").option("--port <port>","Port","7860").option("-o, --open","Open the dashboard in your default browser").option("--stop","Stop the dashboard running on --port (e.g. one started by your agent)").action(async o=>{let n=Number.parseInt(o.port??"7860",10)||7860,{startUi:r,stopUi:s}=await import('./app-FWUAWVD5.js');if(o.stop){d(await s(n)?a$1.green(`\u2713 devcoach UI on port ${n} stopped`):a$1.yellow(`No devcoach UI is running on port ${n}`));return}r(n,{onReady:i=>{o.open&&Te(i);},onListenError:i=>{console.error(i.message),o.open&&i.existingUrl&&Te(i.existingUrl),process.exit(i.exitCode);}});}),e$1.command("setup").description("First-run wizard: import backup or build your knowledge profile").action(ln),e$1.command("stop-hook",{hidden:true}).description("Claude Code Stop hook: onboarding check + lesson cue in one spawn").action(()=>e()),e$1.command("prompt-hook",{hidden:true}).description("Claude Code UserPromptSubmit hook: prime the model when a lesson is due").action(()=>g()),e$1.command("gemini-stop-hook",{hidden:true}).description("Gemini CLI AfterAgent hook: onboarding check + lesson cue in one spawn").action(()=>e(void 0,"gemini")),e$1.command("gemini-prompt-hook",{hidden:true}).description("Gemini CLI BeforeAgent hook: prime the model when a lesson is due").action(()=>g(void 0,"gemini")),e$1.command("codex-stop-hook",{hidden:true}).description("Codex CLI Stop hook: onboarding check + lesson cue in one spawn").action(()=>e(void 0,"codex")),e$1.command("codex-prompt-hook",{hidden:true}).description("Codex CLI UserPromptSubmit hook: prime the model when a lesson is due").action(()=>g(void 0,"codex")),e$1.command("onboard-hook",{hidden:true}).description("Claude Code Stop hook: cue onboarding (legacy two-entry layout)").action(()=>b()),e$1.command("lesson-ready",{hidden:true}).description("Claude Code Stop hook: signal a lesson is due (legacy two-entry layout)").action(()=>d$1()),e$1}async function Xn(){let e=mn();try{await e.parseAsync(process.argv);}catch(o){let n=o.code??"";if(!n.startsWith("commander."))throw o;if(n==="commander.helpDisplayed"||n==="commander.help"||n==="commander.version")return;process.exit(2);}}export{Xn as runCli};
37
+ `);}function _(e){return e??null}function mn(){let e$1=new Command;return e$1.exitOverride(),e$1.name("devcoach").description("devcoach \u2014 learn what you don't know yet from the code your AI agent writes").version(`devcoach ${a}`,"-v, --version","output the version number").allowExcessArguments(false).showHelpAfterError("(add --help for usage)").action(()=>un()),e$1.command("mcp").description("Start the MCP server (stdio transport)").action(async()=>{let{runStdio:o}=await import('./server-R54NX6Z5.js');await o();}),e$1.command("profile").description("Show the knowledge map").action(Bt),e$1.command("lessons").description("List past lessons").option("--period <period>","today | week | month | year | all","all").option("--category <category>","Filter by category tag").option("--project <project>","Filter by project (fuzzy)").option("--repository <repository>","Filter by repository (fuzzy)").option("--branch <branch>","Filter by branch (fuzzy)").option("--commit <commit>","Filter by commit hash prefix (fuzzy)").option("--starred","Show only starred lessons").option("--imported","Show only lessons shared with you").option("--from <name>","Show only lessons shared by this person").option("--feedback <feedback>","know | understood | dont_know | none").option("--level <level>","junior | mid | senior").option("--date-from <date>","Show lessons on or after this date (YYYY-MM-DD[THH:MM])").option("--date-to <date>","Show lessons on or before this date (YYYY-MM-DD[THH:MM])").option("--sort <column>","timestamp | level | topic_id | title | feedback","timestamp").option("--order <order>","asc | desc","desc").action(o=>Ut({period:o.period??"all",category:_(o.category),project:_(o.project),repository:_(o.repository),branch:_(o.branch),commit:_(o.commit),starred:!!o.starred,imported:!!o.imported,sharedBy:_(o.from),feedback:_(o.feedback),level:_(o.level),dateFrom:_(o.dateFrom),dateTo:_(o.dateTo),sort:o.sort??"timestamp",order:o.order??"desc"})),e$1.command("lesson").description("Show a single lesson in detail").argument("<id>","Lesson ID").action(o=>Jt(o)),e$1.command("courses").description("List your step-by-step courses").action(()=>Kt()),e$1.command("course").description("Show a course: steps, progress, document path").argument("<id>","Course ID").action(o=>zt(o)),e$1.command("star").description("Mark a lesson as starred").argument("<id>","Lesson ID").action(o=>Zo(o,true)),e$1.command("unstar").description("Remove the starred mark from a lesson").argument("<id>","Lesson ID").action(o=>Zo(o,false)),e$1.command("delete").description("Permanently delete a lesson").argument("<id>","Lesson ID").action(o=>Vt(o)),e$1.command("feedback").description("Record your answer under a lesson: know / understood / dont_know").argument("<id>","Lesson ID").argument("<value>","know | understood | dont_know | clear").action((o,n)=>Wt(o,n)),e$1.command("share").description("Share a lesson: copyable text (default), --link, or a .devcoach.md file").argument("[id]","Lesson ID (or use --last)").option("--last","Share the most recent lesson").option("--link","Print the share link only").option("--file [path]","Write a .devcoach.md file (default: <lesson-id>.devcoach.md)").option("--with-context","Include project/branch/commit/task context (off by default)").option("--by <name>","Sender name (remembered as share_name when unset)").option("--anonymous","Share without a sender name").action((o,n)=>sn(o,n)),e$1.command("import").description("Import a shared lesson: a devcoach:lesson code, a share link, a URL, a .devcoach.md or lessons .json file, - for stdin \u2014 or the clipboard when omitted").argument("[source]","Code, link, URL, file path, or - (stdin); omit to read the clipboard").action(o=>cn(o)),e$1.command("settings").description("Show current settings").action(Xt),e$1.command("stats").description("Coaching statistics and rate-limit status").action(Yt),e$1.command("set").description("Update a setting (max_per_day | min_gap_minutes | nudge_every | nudge_scope | share_name)").argument("<key>","Setting key").argument("<value>","New value").action((o,n)=>Zt(o,n)),e$1.command("knowledge-add").description("Add or update a topic in the knowledge map").argument("<topic>","Topic ID").option("--confidence <n>","Initial confidence 0-10","5").option("--group <group>","Assign to a named group").action((o,n)=>qt(o,Math.max(0,Math.min(10,Number.parseInt(n.confidence??"5",10)||5)),_(n.group))),e$1.command("knowledge-remove").description("Remove a topic from the knowledge map").argument("<topic>","Topic ID").action(o=>Qt(o)),e$1.command("group-add").description("Register a new knowledge group").argument("<name>","Group name").action(o=>en(o)),e$1.command("group-remove").description("Delete a knowledge group (topics move to Other)").argument("<name>","Group name").action(o=>on(o)),e$1.command("group-assign").description("Move a topic to a group").argument("<topic>","Topic ID").argument("<group>","Group name (use 'Other' to ungroup)").action((o,n)=>tn(o,n)),e$1.command("backup").description("Export a full backup (settings + knowledge + lessons) as zip").argument("[file]","Output zip path","devcoach-backup.zip").action(o=>nn(o)),e$1.command("restore").description("Restore from a backup zip file").argument("<file>","Path to backup zip").action(o=>rn(o)),e$1.command("install").description("Register the MCP server + hooks + skill in Claude Code / Claude Desktop (default), Gemini CLI (--gemini, beta), or Codex CLI (--codex, beta)").option("--claude-code","Target Claude Code only").option("--claude-desktop","Target Claude Desktop only").option("--gemini","Target Google Gemini CLI (beta)").option("--codex","Target OpenAI Codex CLI (beta)").option("--force","Overwrite existing devcoach entry").option("--skip-hook","Register MCP server only \u2014 skip the hooks").action(o=>Vo({claudeCode:!!o.claudeCode,claudeDesktop:!!o.claudeDesktop,gemini:!!o.gemini,codex:!!o.codex,force:!!o.force,skipHook:!!o.skipHook})),e$1.command("uninstall").description("Remove devcoach's MCP server, hooks and skill from Claude Code / Claude Desktop (default), Gemini CLI (--gemini), Codex CLI (--codex) or every agent (--all). Coaching data is kept unless --data is given. Run it before brew/npm uninstall.").option("--claude-code","Target Claude Code only").option("--claude-desktop","Target Claude Desktop only").option("--gemini","Also remove from Google Gemini CLI (beta)").option("--codex","Also remove from OpenAI Codex CLI (beta)").option("--all","Every agent: Claude Code, Claude Desktop, Gemini CLI, Codex CLI").option("--data","Also delete ~/.devcoach (lessons, profile, notebook) \u2014 asks first").option("--yes","Do not ask before deleting the data (with --data)").action(async o=>Xo({claudeCode:!!o.claudeCode,claudeDesktop:!!o.claudeDesktop,gemini:!!o.gemini,codex:!!o.codex,all:!!o.all,data:!!o.data,yes:!!o.yes})),e$1.command("doctor").description("Diagnose the devcoach \u21C4 agent wiring and the lesson pacing state").action(Wo),e$1.command("ui").description("Launch the web dashboard (Ctrl+C stops it gracefully; --stop stops a running one)").option("--port <port>","Port","7860").option("-o, --open","Open the dashboard in your default browser").option("--stop","Stop the dashboard running on --port (e.g. one started by your agent)").action(async o=>{let n=Number.parseInt(o.port??"7860",10)||7860,{startUi:r,stopUi:s}=await import('./app-STPXOHWA.js');if(o.stop){d(await s(n)?a$1.green(`\u2713 devcoach UI on port ${n} stopped`):a$1.yellow(`No devcoach UI is running on port ${n}`));return}r(n,{onReady:i=>{o.open&&Te(i);},onListenError:i=>{console.error(i.message),o.open&&i.existingUrl&&Te(i.existingUrl),process.exit(i.exitCode);}});}),e$1.command("setup").description("First-run wizard: import backup or build your knowledge profile").action(ln),e$1.command("stop-hook",{hidden:true}).description("Claude Code Stop hook: onboarding check + lesson cue in one spawn").action(()=>e()),e$1.command("prompt-hook",{hidden:true}).description("Claude Code UserPromptSubmit hook: prime the model when a lesson is due").action(()=>g()),e$1.command("gemini-stop-hook",{hidden:true}).description("Gemini CLI AfterAgent hook: onboarding check + lesson cue in one spawn").action(()=>e(void 0,"gemini")),e$1.command("gemini-prompt-hook",{hidden:true}).description("Gemini CLI BeforeAgent hook: prime the model when a lesson is due").action(()=>g(void 0,"gemini")),e$1.command("codex-stop-hook",{hidden:true}).description("Codex CLI Stop hook: onboarding check + lesson cue in one spawn").action(()=>e(void 0,"codex")),e$1.command("codex-prompt-hook",{hidden:true}).description("Codex CLI UserPromptSubmit hook: prime the model when a lesson is due").action(()=>g(void 0,"codex")),e$1.command("onboard-hook",{hidden:true}).description("Claude Code Stop hook: cue onboarding (legacy two-entry layout)").action(()=>b()),e$1.command("lesson-ready",{hidden:true}).description("Claude Code Stop hook: signal a lesson is due (legacy two-entry layout)").action(()=>d$1()),e$1}async function Xn(){let e=mn();try{await e.parseAsync(process.argv);}catch(o){let n=o.code??"";if(!n.startsWith("commander."))throw o;if(n==="commander.helpDisplayed"||n==="commander.help"||n==="commander.version")return;process.exit(2);}}export{Xn as runCli};
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export{c as buildLessonCue,f as buildPromptPrime,d as cmdLessonReady,b as cmdOnboardHook,g as cmdPromptHook,e as cmdStopHook,a as parseHookPayload,h as runHook}from'./chunk-R44CZZD2.js';import'./chunk-ZFCI6ROC.js';import'./chunk-FL5OOXCF.js';
2
+ export{c as buildLessonCue,f as buildPromptPrime,d as cmdLessonReady,b as cmdOnboardHook,g as cmdPromptHook,e as cmdStopHook,a as parseHookPayload,h as runHook}from'./chunk-XNGALLYP.js';import'./chunk-O53WOENG.js';import'./chunk-ACFVSHDR.js';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import {d,f as f$2,e as e$1,c as c$1,b as b$1,a as a$2}from'./chunk-MRABMPXO.js';import {g as g$3,e as e$2,d as d$2}from'./chunk-WRY2DLSB.js';import {o,n,p,q,b,f as f$1,d as d$1,c,i as i$1,g as g$2,j as j$1,k as k$1,h as h$2}from'./chunk-M3XX2DV7.js';import {e,g as g$1,f,k,j,i,E,ca,o as o$1,F,_,Da,H,K,I,Ea,aa,h as h$1,R,V,S,T,U,v,w,Ca,r,O,ta,X,sa,za,Aa,xa,wa,ua,Ga,ra,n as n$1,m,l,Fa,Ha,G,Ba,C,x,p as p$1,Y,ba,A,u as u$1}from'./chunk-ZFCI6ROC.js';import {a as a$1}from'./chunk-FL5OOXCF.js';import {spawn}from'child_process';import {mkdirSync,existsSync,statSync,writeFileSync,readFileSync}from'fs';import {dirname}from'path';import {McpServer,ResourceTemplate}from'@modelcontextprotocol/server';import {StdioServerTransport}from'@modelcontextprotocol/server/stdio';import {z}from'zod';var u=n=>({type:"text",text:n}),We=n=>({content:[u(JSON.stringify(n))]}),a=n=>({isError:true,content:[u(n)]}),g=n=>({content:[u(JSON.stringify(n))],structuredContent:n}),Je=z.object({id:z.string(),timestamp:z.string(),topic_id:z.string(),categories:z.array(z.string()),title:z.string(),level:e,summary:z.string(),body:z.string().nullable(),task_context:z.string().nullable(),project:z.string().nullable(),repository:z.string().nullable(),branch:z.string().nullable(),commit_hash:z.string().nullable(),folder:z.string().nullable(),repository_platform:f.nullable(),starred:z.boolean(),feedback:g$1.nullable(),imported:z.boolean(),shared_by:z.string().nullable()}),ot=Je.extend({reply_check:z.string()}),Fe="Saved \u2014 but saving does NOT display anything. The user sees ONLY plain text you write in your reply; the title/body inside this tool call's ARGUMENTS are invisible to them, and having composed them does not count as having shown the card. Did you already write the full card (both ### band headings + title line + body + tip) as plain reply text, outside any tool call? If yes: output nothing. If no: write the card now, as the final text of your reply. Never write it twice.",nt=z.object({readOnly:z.literal(true),dbSchema:z.number().int(),appSchema:z.number().int(),upgradedBy:z.string().nullable(),minApp:z.string().nullable()}).nullable().optional(),De=z.object({knowledge:z.array(z.object({topic:z.string(),confidence:z.number().int()})),groups:z.array(z.object({name:z.string(),topics:z.array(z.string())})),compat:nt}),rt=z.object({max_per_day:z.number().int(),min_gap_minutes:z.number().int(),ui_theme:i,ui_home:j,nudge_every:z.number().int(),nudge_scope:k,share_name:z.string().nullable()}),it=z.object({transport:z.enum(["text","link","file"]),code:z.string(),text:z.string().nullable(),link:z.string().nullable(),markdown:z.string().nullable(),filename:z.string().nullable(),shared_by:z.string().nullable(),include_context:z.boolean(),reply_check:z.string()}),st=z.object({kind:z.enum(["shared","lessons"]),inserted:z.number().int(),duplicated:z.number().int(),invalid:z.number().int(),lesson:Je.nullable(),topic_tracked:z.boolean().nullable(),reply_check:z.string()}),at={text:"Reply with `text` verbatim as the final message: the card as markdown, then the last line (devcoach:lesson:1:\u2026) inside a fenced code block, character for character \u2014 never paraphrase, wrap, truncate or split it.",link:"Reply with `link` on its own line, unchanged.",file:"Write `markdown` to `filename` (in the workspace unless the user named a path) with your file tool, then confirm the path in one line; do not print the markdown."},ct="Confirm in one line: the title, who shared it, and that it joined the coaching log (feedback works as usual; it does not count against the daily limit). If topic_tracked is false, offer to track the topic with add_topic \u2014 only on confirmation.",dt=z.object({window_months:z.number().int(),cutoff:z.string(),candidate_count:z.number().int(),over_soft_limit:z.boolean(),candidates:z.array(z.object({name:z.string(),path:z.string(),last_activity:z.string().nullable(),prompt_count:z.number().int()}))});function h(n,c){return {contents:[{uri:n.href,mimeType:"application/json",text:JSON.stringify(c)}]}}function Me(){try{return E(n=>({...Ca(n),compat:A(n)}))}catch(n){return {error:String(n)}}}function ze(){try{let n="";try{existsSync(r)&&(n=readFileSync(r,"utf8"));}catch{}return {...E(d=>{let b=ba(d).knowledge_ready,f=n.length>0,p=Ba(d),t={allowed:p.allowed,total_lessons:G(d)};return p.reason!=null&&(t.reason=p.reason),{onboarding:{knowledge_ready:b,notebook_ready:f,needs_onboarding:!(b&&f)},rate_limit:t,taught_topics:Ha(d),profile:Ca(d),compat:A(d)}}),notebook:n,notebook_path:r}}catch(n){return {error:String(n)}}}function lt(n,c,d){return n&&c?null:n?"The profile exists; only the notebook is missing. Compose it and write it to notebook_path \u2014 do not rebuild the topics.":`Let the user choose how to set up (lead with Automatic, never pick for them). ${d?"Automatic = build the topic map yourself, in one pass, from detected_stack enriched with default_topics.":"No Claude Code history was found on this machine (detected_stack is empty). That does NOT block Automatic: use default_topics exactly as returned as the starting map \u2014 it is devcoach's general developer baseline, not a guess of yours \u2014 and tell the user it is a starting point that corrects itself as they work. Do not stop, do not ask again, do not switch to Guided unless they ask."} Then save with complete_onboarding and write the notebook to notebook_path, in the same turn. The devcoach skill's references/onboarding.md has the full flow when the skill is available.`}function Ke(){try{let{status:n,compat:c}=E(o=>({status:ba(o),compat:A(o)})),d=n.knowledge_ready,b=existsSync(r)&&statSync(r).size>0,f=o(),p=c$1(),t=b$1(a$2(f.folder??process.cwd()),p.detected_stack);return {compat:c,knowledge_ready:d,notebook_ready:b,needs_onboarding:!(d&&b),detected_stack:t,detected_projects:p.projects,scanned_projects:p.scanned_projects,default_topics:u$1,context_ready:f.branch!==null,notebook_path:r,next_step:lt(d,b,Object.keys(t).length>0)}}catch(n){return {error:String(n)}}}var D=(n,c)=>typeof c.error=="string"?a(`${n} failed: ${c.error}`):g(c);function ut(){let n$2=new McpServer({name:"devcoach",version:a$1},{instructions:"Progressive technical coaching server. Use the devcoach_instructions prompt for full coaching behaviour guidelines. Read coaching state with the get_briefing / get_onboarding / get_profile tools; the devcoach:// resources expose the same data for clients that browse resources. share_lesson hands a lesson to another person (text, link or file); import_lesson stores one they were given."});n$2.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. timestamp is always stamped server-side with the current time \u2014 there is no argument for it. Returns the saved Lesson with all resolved fields.",inputSchema:z.object({id:z.string().describe("Unique lesson id (uuid or random string)"),topic_id:z.string().describe("Primary topic id, e.g. 'python'"),categories:z.array(z.string()).describe("Category tags, e.g. ['python','performance']"),title:z.string().describe("Short lesson title"),level:e.describe("Difficulty level: junior | mid | senior"),summary:z.string().describe("1-3 sentence summary shown in the lesson card"),body:z.string().min(1).describe("Clean lesson markdown: the prose + \u{1F4A1} Senior tip only \u2014 NO card bands, NO '>' blockquote, and do not repeat title/category/level (those are their own fields). Required, non-empty."),task_context:z.string().nullish().describe("What the user was doing when taught"),project:z.string().nullish().describe("Project name (auto-detected from git if omitted)"),repository:z.string().nullish().describe("org/repo or path (auto-detected if omitted)"),branch:z.string().nullish().describe("git branch (auto-detected if omitted)"),commit_hash:z.string().nullish().describe("git commit hash (auto-detected if omitted)"),folder:z.string().nullish().describe("workspace folder (auto-detected if omitted)"),repository_platform:f.nullish().describe("github | gitlab | bitbucket | local (auto-detected if omitted)")}),outputSchema:ot,annotations:{title:"Log Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async t=>{try{let o$2=o(),r={};try{r=E(l=>ca(l));}catch{r={};}let i=o$1({id:t.id,timestamp:new Date,topic_id:t.topic_id,categories:t.categories,title:t.title,level:t.level,summary:t.summary,body:t.body,task_context:t.task_context??null,project:t.project??o$2.project??r.project??null,repository:t.repository??o$2.repository??r.repository??null,branch:t.branch??o$2.branch??r.branch??null,commit_hash:t.commit_hash??o$2.commit_hash??null,folder:t.folder??o$2.folder??null,repository_platform:t.repository_platform??o$2.repository_platform??r.repository_platform??null});return E(l=>{F(l,i),_(l);}),{content:[u(`Lesson saved. ${Fe}`),u(JSON.stringify(i))],structuredContent:{...i,reply_check:Fe}}}catch(o){return a(`log_lesson failed: ${o}`)}}),n$2.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:z.object({topic:z.string().describe("Topic id, e.g. 'python'"),delta:z.number().int().describe("Signed change to apply, e.g. +1 or -1")}),annotations:{title:"Update Knowledge",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async t=>{try{let o=E(r=>Da(r,t.topic,t.delta));return {content:[u(String(o))]}}catch(o){return a(`update_knowledge failed for '${t.topic}': ${o}`)}}),n$2.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:z.object({period:z.enum(["today","week","month","year","all"]).nullish().describe("Time window"),category:z.string().nullish().describe("Filter by a category tag, e.g. 'python'"),level:z.enum(["junior","mid","senior"]).nullish().describe("Filter by difficulty level"),project:z.string().nullish().describe("Fuzzy match on git project"),repository:z.string().nullish().describe("Fuzzy match on git repository"),branch:z.string().nullish().describe("Fuzzy match on git branch"),commit:z.string().nullish().describe("Fuzzy match on commit hash"),starred:z.boolean().nullish().describe("True to return only starred lessons"),imported:z.boolean().nullish().describe("True = only lessons shared with you, false = only your own"),shared_by:z.string().nullish().describe("Only lessons shared by this person (exact sender name; implies imported)"),feedback:z.enum(["know","understood","dont_know","none"]).nullish().describe("Filter by feedback: know (already knew), understood (new, now clear), dont_know (couldn't follow \u2014 a course seed), none (no response)"),search:z.string().nullish().describe("Full-text search over title, topic_id, summary, body and the sender's name"),date_from:z.string().nullish().describe("ISO date/datetime lower bound"),date_to:z.string().nullish().describe("ISO date/datetime upper bound (date-only = end-of-day)"),limit:z.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}},t=>{try{let o=E(r=>H(r,{period:t.period,category:t.category,level:t.level,project:t.project,repository:t.repository,branch:t.branch,commit:t.commit,starred:t.starred,imported:t.imported,shared_by:t.shared_by,feedback:t.feedback,search:t.search,date_from:t.date_from,date_to:t.date_to,page:t.limit>0?1:null,per_page:t.limit}));return We(o)}catch{return We([])}}),n$2.registerTool("star_lesson",{title:"Star Lesson",description:"Set the starred (favourite) flag on a lesson. Returns true if found and updated. Idempotent.",inputSchema:z.object({lesson_id:z.string().describe("Lesson id"),starred:z.boolean().describe("true to favourite, false to unmark")}),annotations:{title:"Star Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async t=>{try{let o=E(r=>K(r,t.lesson_id,t.starred));return {content:[u(String(o))]}}catch(o){return a(`star_lesson failed for '${t.lesson_id}': ${o}`)}}),n$2.registerTool("delete_lesson",{title:"Delete Lesson",description:"Permanently delete a lesson by id. Returns true if found and deleted.",inputSchema:z.object({lesson_id:z.string().describe("Lesson id to delete")}),annotations:{title:"Delete Lesson",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async t=>{try{let o=E(r=>I(r,t.lesson_id));return {content:[u(String(o))]}}catch(o){return a(`delete_lesson failed for '${t.lesson_id}': ${o}`)}}),n$2.registerTool("submit_feedback",{title:"Submit Feedback",description:"Record the user's answer under a lesson card. know = already knew it (confidence +1); understood = new and now clear (no confidence change); dont_know = couldn't follow this session (no confidence change; the lesson becomes a course seed); clear = remove the answer. Idempotent \u2014 repeating the stored answer changes nothing.",inputSchema:z.object({lesson_id:z.string().describe("Lesson id"),feedback:z.enum(["know","understood","dont_know","clear"]).describe("know | understood | dont_know | clear")}),annotations:{title:"Submit Feedback",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async t=>{try{let o=t.feedback==="clear"?null:t.feedback,r=E(i=>Ea(i,t.lesson_id,o));return {content:[u(String(r!==null))]}}catch(o){return a(`submit_feedback failed for '${t.lesson_id}': ${o}`)}}),n$2.registerTool("skip_lesson",{title:"Skip Lesson",description:"Decline a lesson cue: call this when the coaching hook asked for a lesson but the completed work does not warrant one (pure questions, chat, nothing technical). Re-arms the pacing counter so the cue is not repeated immediately. Never call it after delivering a lesson \u2014 log_lesson already resolves the cue.",inputSchema:z.object({reason:z.string().min(1).describe("One line: why no lesson was warranted (shown in `devcoach doctor`)")}),annotations:{title:"Skip Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async t=>{try{return E(o=>aa(o,t.reason)),{content:[u("Cue skipped \u2014 pacing re-armed. Output nothing to the user.")]}}catch(o){return a(`skip_lesson failed: ${o}`)}}),n$2.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:z.object({topic:z.string().describe("Topic id \u2014 prefer a single word, max 3 words"),confidence:h$1.default(5).describe("Initial confidence 0-10 (default 5)"),group:z.string().nullish().describe("Optional group name; 'Other' if omitted")}),annotations:{title:"Add Topic",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async t=>{try{return E(o=>{R(o,t.topic,t.confidence),t.group&&t.group!=="Other"&&V(o,t.topic,t.group);}),{content:[u("true")]}}catch(o){return a(`add_topic failed for '${t.topic}': ${o}`)}}),n$2.registerTool("remove_topic",{title:"Remove Topic",description:"Remove a topic from the knowledge map entirely. Returns true if it existed.",inputSchema:z.object({topic:z.string().describe("Topic id to remove")}),annotations:{title:"Remove Topic",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async t=>{try{let o=E(r=>S(r,t.topic));return {content:[u(String(o))]}}catch(o){return a(`remove_topic failed for '${t.topic}': ${o}`)}}),n$2.registerTool("add_group",{title:"Add Group",description:"Create a new (initially empty) knowledge group. Idempotent \u2014 returns true either way.",inputSchema:z.object({name:z.string().describe("Group name, e.g. 'Machine Learning'")}),annotations:{title:"Add Group",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async t=>{try{return E(o=>T(o,t.name.trim())),{content:[u("true")]}}catch(o){return a(`add_group failed for '${t.name}': ${o}`)}}),n$2.registerTool("remove_group",{title:"Remove Group",description:"Delete a knowledge group. Its topics move to Other. Returns true if it existed.",inputSchema:z.object({name:z.string().describe("Group name to delete")}),annotations:{title:"Remove Group",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async t=>{try{let o=E(r=>U(r,t.name));return {content:[u(String(o))]}}catch(o){return a(`remove_group failed for '${t.name}': ${o}`)}}),n$2.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). nudge_every: integer 0-1000 interactions between lesson cues (0 = cue every turn). nudge_scope: 'session' | 'global'. share_name: the sender name proposed when sharing a lesson (max 80 chars, empty clears). Returns the full updated Settings.",inputSchema:z.object({key:z.enum(["max_per_day","min_gap_minutes","nudge_every","nudge_scope","share_name"]).describe("Setting key"),value:z.string().describe("New value (integer string; 'session'|'global' for nudge_scope; free text for share_name)")}),outputSchema:rt,annotations:{title:"Update Settings",destructiveHint:false,idempotentHint:true,openWorldHint:false}},t=>{let o=i=>g(E(l=>(Y(l,t.key,i),X(l))));if(t.key==="share_name")return t.value.trim().length>v?a(`share_name must be at most ${v} characters`):o(w(t.value));if(t.key==="nudge_scope")return t.value!=="session"&&t.value!=="global"?a("nudge_scope must be 'session' or 'global'"):o(t.value);let r=Number.parseInt(t.value,10);return Number.isNaN(r)?a(`Value must be an integer, got '${t.value}'`):t.key==="max_per_day"&&!(r>=1&&r<=20)?a("max_per_day must be between 1 and 20"):t.key==="min_gap_minutes"&&!(r>=0&&r<=1440)?a("min_gap_minutes must be between 0 and 1440"):t.key==="nudge_every"&&!(r>=0&&r<=1e3)?a("nudge_every must be between 0 and 1000"):o(String(r))}),n$2.registerTool("open_ui",{title:"Open UI",description:"Launch the devcoach web dashboard in the background. port must be 1024-65535.",inputSchema:z.object({port:z.number().int().default(7860).describe("Port (default 7860)")}),annotations:{title:"Open UI",destructiveHint:false,idempotentHint:false,openWorldHint:true}},async t=>{let o=t.port;if(!(o>=1024&&o<=65535))return {content:[u(`error: port ${o} is out of valid range (1024-65535)`)]};let{pingUi:r}=await import('./app-FWUAWVD5.js');return await r(o)?{content:[u(`devcoach UI is already running at http://localhost:${o}`)]}:(spawn(process.execPath,[process.argv[1]??"","ui","--port",String(o)],{detached:true,stdio:"ignore"}).unref(),{content:[u(`devcoach UI starting at http://localhost:${o}`)]})}),n$2.registerTool("stop_ui",{title:"Stop UI",description:"Stop the devcoach web dashboard listening on port (whoever started it \u2014 open_ui or a terminal). Graceful: in-flight requests finish first. Reports stopped=false when nothing is running there. port must be 1024-65535.",inputSchema:z.object({port:z.number().int().default(7860).describe("Port (default 7860)")}),annotations:{title:"Stop UI",destructiveHint:false,idempotentHint:true,openWorldHint:true}},async t=>{let o=t.port;if(!(o>=1024&&o<=65535))return {content:[u(`error: port ${o} is out of valid range (1024-65535)`)]};let{stopUi:r}=await import('./app-FWUAWVD5.js'),i=await r(o);return {content:[u(i?`devcoach UI on port ${o} stopped`:`No devcoach UI is running on port ${o}`)],structuredContent:{stopped:i,port:o}}}),n$2.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'). Ensures learning-state.md exists and is non-empty (a placeholder if needed) \u2014 there is no notebook argument here. Write the real personalized notebook yourself right after, directly to the path in devcoach://onboarding's notebook_path field. Returns the updated Profile.",inputSchema:z.object({topics:z.record(z.string(),h$1).describe("{topic_id: confidence 0-10}"),groups:z.record(z.string(),z.array(z.string())).optional().describe("{group_name: [topic_id, ...]}")}),outputSchema:De,annotations:{title:"Complete Onboarding",destructiveHint:true,idempotentHint:false,openWorldHint:false}},async t=>{try{let o=E(r=>{r.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[i,l]of Object.entries(t.topics))R(r,i,Math.max(0,Math.min(10,l)));if(t.groups)for(let[i,l]of Object.entries(t.groups))for(let M of l)M in t.topics&&V(r,M,i);return Ca(r)});return mkdirSync(dirname(r),{recursive:!0}),(!existsSync(r)||statSync(r).size===0)&&writeFileSync(r,`# devcoach \u2014 Coaching Notebook
2
+ import {d,f as f$2,e as e$1,c as c$1,b as b$1,a as a$2}from'./chunk-MRABMPXO.js';import {g as g$3,e as e$2,d as d$2}from'./chunk-QRNH4GKU.js';import {o,n,p,q,b,f as f$1,d as d$1,c,i as i$1,g as g$2,j as j$1,k as k$1,h as h$2}from'./chunk-IHT2QFRV.js';import {e,g as g$1,f,k,j,i,E,ca,o as o$1,F,_,Da,H,K,I,Ea,aa,h as h$1,R,V,S,T,U,v,w,Ca,r,O,ta,X,sa,za,Aa,xa,wa,ua,Ga,ra,n as n$1,m,l,Fa,Ha,G,Ba,C,x,p as p$1,Y,ba,A,u as u$1}from'./chunk-O53WOENG.js';import {a as a$1}from'./chunk-ACFVSHDR.js';import {spawn}from'child_process';import {mkdirSync,existsSync,statSync,writeFileSync,readFileSync}from'fs';import {dirname}from'path';import {McpServer,ResourceTemplate}from'@modelcontextprotocol/server';import {StdioServerTransport}from'@modelcontextprotocol/server/stdio';import {z}from'zod';var u=n=>({type:"text",text:n}),We=n=>({content:[u(JSON.stringify(n))]}),a=n=>({isError:true,content:[u(n)]}),g=n=>({content:[u(JSON.stringify(n))],structuredContent:n}),Je=z.object({id:z.string(),timestamp:z.string(),topic_id:z.string(),categories:z.array(z.string()),title:z.string(),level:e,summary:z.string(),body:z.string().nullable(),task_context:z.string().nullable(),project:z.string().nullable(),repository:z.string().nullable(),branch:z.string().nullable(),commit_hash:z.string().nullable(),folder:z.string().nullable(),repository_platform:f.nullable(),starred:z.boolean(),feedback:g$1.nullable(),imported:z.boolean(),shared_by:z.string().nullable()}),ot=Je.extend({reply_check:z.string()}),Fe="Saved \u2014 but saving does NOT display anything. The user sees ONLY plain text you write in your reply; the title/body inside this tool call's ARGUMENTS are invisible to them, and having composed them does not count as having shown the card. Did you already write the full card (both ### band headings + title line + body + tip) as plain reply text, outside any tool call? If yes: output nothing. If no: write the card now, as the final text of your reply. Never write it twice.",nt=z.object({readOnly:z.literal(true),dbSchema:z.number().int(),appSchema:z.number().int(),upgradedBy:z.string().nullable(),minApp:z.string().nullable()}).nullable().optional(),De=z.object({knowledge:z.array(z.object({topic:z.string(),confidence:z.number().int()})),groups:z.array(z.object({name:z.string(),topics:z.array(z.string())})),compat:nt}),rt=z.object({max_per_day:z.number().int(),min_gap_minutes:z.number().int(),ui_theme:i,ui_home:j,nudge_every:z.number().int(),nudge_scope:k,share_name:z.string().nullable()}),it=z.object({transport:z.enum(["text","link","file"]),code:z.string(),text:z.string().nullable(),link:z.string().nullable(),markdown:z.string().nullable(),filename:z.string().nullable(),shared_by:z.string().nullable(),include_context:z.boolean(),reply_check:z.string()}),st=z.object({kind:z.enum(["shared","lessons"]),inserted:z.number().int(),duplicated:z.number().int(),invalid:z.number().int(),lesson:Je.nullable(),topic_tracked:z.boolean().nullable(),reply_check:z.string()}),at={text:"Reply with `text` verbatim as the final message: the card as markdown, then the last line (devcoach:lesson:1:\u2026) inside a fenced code block, character for character \u2014 never paraphrase, wrap, truncate or split it.",link:"Reply with `link` on its own line, unchanged.",file:"Write `markdown` to `filename` (in the workspace unless the user named a path) with your file tool, then confirm the path in one line; do not print the markdown."},ct="Confirm in one line: the title, who shared it, and that it joined the coaching log (feedback works as usual; it does not count against the daily limit). If topic_tracked is false, offer to track the topic with add_topic \u2014 only on confirmation.",dt=z.object({window_months:z.number().int(),cutoff:z.string(),candidate_count:z.number().int(),over_soft_limit:z.boolean(),candidates:z.array(z.object({name:z.string(),path:z.string(),last_activity:z.string().nullable(),prompt_count:z.number().int()}))});function h(n,c){return {contents:[{uri:n.href,mimeType:"application/json",text:JSON.stringify(c)}]}}function Me(){try{return E(n=>({...Ca(n),compat:A(n)}))}catch(n){return {error:String(n)}}}function ze(){try{let n="";try{existsSync(r)&&(n=readFileSync(r,"utf8"));}catch{}return {...E(d=>{let b=ba(d).knowledge_ready,f=n.length>0,p=Ba(d),t={allowed:p.allowed,total_lessons:G(d)};return p.reason!=null&&(t.reason=p.reason),{onboarding:{knowledge_ready:b,notebook_ready:f,needs_onboarding:!(b&&f)},rate_limit:t,taught_topics:Ha(d),profile:Ca(d),compat:A(d)}}),notebook:n,notebook_path:r}}catch(n){return {error:String(n)}}}function lt(n,c,d){return n&&c?null:n?"The profile exists; only the notebook is missing. Compose it and write it to notebook_path \u2014 do not rebuild the topics.":`Let the user choose how to set up (lead with Automatic, never pick for them). ${d?"Automatic = build the topic map yourself, in one pass, from detected_stack enriched with default_topics.":"No Claude Code history was found on this machine (detected_stack is empty). That does NOT block Automatic: use default_topics exactly as returned as the starting map \u2014 it is devcoach's general developer baseline, not a guess of yours \u2014 and tell the user it is a starting point that corrects itself as they work. Do not stop, do not ask again, do not switch to Guided unless they ask."} Then save with complete_onboarding and write the notebook to notebook_path, in the same turn. The devcoach skill's references/onboarding.md has the full flow when the skill is available.`}function Ke(){try{let{status:n,compat:c}=E(o=>({status:ba(o),compat:A(o)})),d=n.knowledge_ready,b=existsSync(r)&&statSync(r).size>0,f=o(),p=c$1(),t=b$1(a$2(f.folder??process.cwd()),p.detected_stack);return {compat:c,knowledge_ready:d,notebook_ready:b,needs_onboarding:!(d&&b),detected_stack:t,detected_projects:p.projects,scanned_projects:p.scanned_projects,default_topics:u$1,context_ready:f.branch!==null,notebook_path:r,next_step:lt(d,b,Object.keys(t).length>0)}}catch(n){return {error:String(n)}}}var D=(n,c)=>typeof c.error=="string"?a(`${n} failed: ${c.error}`):g(c);function ut(){let n$2=new McpServer({name:"devcoach",version:a$1},{instructions:"Progressive technical coaching server. Use the devcoach_instructions prompt for full coaching behaviour guidelines. Read coaching state with the get_briefing / get_onboarding / get_profile tools; the devcoach:// resources expose the same data for clients that browse resources. share_lesson hands a lesson to another person (text, link or file); import_lesson stores one they were given."});n$2.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. timestamp is always stamped server-side with the current time \u2014 there is no argument for it. Returns the saved Lesson with all resolved fields.",inputSchema:z.object({id:z.string().describe("Unique lesson id (uuid or random string)"),topic_id:z.string().describe("Primary topic id, e.g. 'python'"),categories:z.array(z.string()).describe("Category tags, e.g. ['python','performance']"),title:z.string().describe("Short lesson title"),level:e.describe("Difficulty level: junior | mid | senior"),summary:z.string().describe("1-3 sentence summary shown in the lesson card"),body:z.string().min(1).describe("Clean lesson markdown: the prose + \u{1F4A1} Senior tip only \u2014 NO card bands, NO '>' blockquote, and do not repeat title/category/level (those are their own fields). Required, non-empty."),task_context:z.string().nullish().describe("What the user was doing when taught"),project:z.string().nullish().describe("Project name (auto-detected from git if omitted)"),repository:z.string().nullish().describe("org/repo or path (auto-detected if omitted)"),branch:z.string().nullish().describe("git branch (auto-detected if omitted)"),commit_hash:z.string().nullish().describe("git commit hash (auto-detected if omitted)"),folder:z.string().nullish().describe("workspace folder (auto-detected if omitted)"),repository_platform:f.nullish().describe("github | gitlab | bitbucket | local (auto-detected if omitted)")}),outputSchema:ot,annotations:{title:"Log Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async t=>{try{let o$2=o(),r={};try{r=E(l=>ca(l));}catch{r={};}let i=o$1({id:t.id,timestamp:new Date,topic_id:t.topic_id,categories:t.categories,title:t.title,level:t.level,summary:t.summary,body:t.body,task_context:t.task_context??null,project:t.project??o$2.project??r.project??null,repository:t.repository??o$2.repository??r.repository??null,branch:t.branch??o$2.branch??r.branch??null,commit_hash:t.commit_hash??o$2.commit_hash??null,folder:t.folder??o$2.folder??null,repository_platform:t.repository_platform??o$2.repository_platform??r.repository_platform??null});return E(l=>{F(l,i),_(l);}),{content:[u(`Lesson saved. ${Fe}`),u(JSON.stringify(i))],structuredContent:{...i,reply_check:Fe}}}catch(o){return a(`log_lesson failed: ${o}`)}}),n$2.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:z.object({topic:z.string().describe("Topic id, e.g. 'python'"),delta:z.number().int().describe("Signed change to apply, e.g. +1 or -1")}),annotations:{title:"Update Knowledge",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async t=>{try{let o=E(r=>Da(r,t.topic,t.delta));return {content:[u(String(o))]}}catch(o){return a(`update_knowledge failed for '${t.topic}': ${o}`)}}),n$2.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:z.object({period:z.enum(["today","week","month","year","all"]).nullish().describe("Time window"),category:z.string().nullish().describe("Filter by a category tag, e.g. 'python'"),level:z.enum(["junior","mid","senior"]).nullish().describe("Filter by difficulty level"),project:z.string().nullish().describe("Fuzzy match on git project"),repository:z.string().nullish().describe("Fuzzy match on git repository"),branch:z.string().nullish().describe("Fuzzy match on git branch"),commit:z.string().nullish().describe("Fuzzy match on commit hash"),starred:z.boolean().nullish().describe("True to return only starred lessons"),imported:z.boolean().nullish().describe("True = only lessons shared with you, false = only your own"),shared_by:z.string().nullish().describe("Only lessons shared by this person (exact sender name; implies imported)"),feedback:z.enum(["know","understood","dont_know","none"]).nullish().describe("Filter by feedback: know (already knew), understood (new, now clear), dont_know (couldn't follow \u2014 a course seed), none (no response)"),search:z.string().nullish().describe("Full-text search over title, topic_id, summary, body and the sender's name"),date_from:z.string().nullish().describe("ISO date/datetime lower bound"),date_to:z.string().nullish().describe("ISO date/datetime upper bound (date-only = end-of-day)"),limit:z.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}},t=>{try{let o=E(r=>H(r,{period:t.period,category:t.category,level:t.level,project:t.project,repository:t.repository,branch:t.branch,commit:t.commit,starred:t.starred,imported:t.imported,shared_by:t.shared_by,feedback:t.feedback,search:t.search,date_from:t.date_from,date_to:t.date_to,page:t.limit>0?1:null,per_page:t.limit}));return We(o)}catch{return We([])}}),n$2.registerTool("star_lesson",{title:"Star Lesson",description:"Set the starred (favourite) flag on a lesson. Returns true if found and updated. Idempotent.",inputSchema:z.object({lesson_id:z.string().describe("Lesson id"),starred:z.boolean().describe("true to favourite, false to unmark")}),annotations:{title:"Star Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async t=>{try{let o=E(r=>K(r,t.lesson_id,t.starred));return {content:[u(String(o))]}}catch(o){return a(`star_lesson failed for '${t.lesson_id}': ${o}`)}}),n$2.registerTool("delete_lesson",{title:"Delete Lesson",description:"Permanently delete a lesson by id. Returns true if found and deleted.",inputSchema:z.object({lesson_id:z.string().describe("Lesson id to delete")}),annotations:{title:"Delete Lesson",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async t=>{try{let o=E(r=>I(r,t.lesson_id));return {content:[u(String(o))]}}catch(o){return a(`delete_lesson failed for '${t.lesson_id}': ${o}`)}}),n$2.registerTool("submit_feedback",{title:"Submit Feedback",description:"Record the user's answer under a lesson card. know = already knew it (confidence +1); understood = new and now clear (no confidence change); dont_know = couldn't follow this session (no confidence change; the lesson becomes a course seed); clear = remove the answer. Idempotent \u2014 repeating the stored answer changes nothing.",inputSchema:z.object({lesson_id:z.string().describe("Lesson id"),feedback:z.enum(["know","understood","dont_know","clear"]).describe("know | understood | dont_know | clear")}),annotations:{title:"Submit Feedback",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async t=>{try{let o=t.feedback==="clear"?null:t.feedback,r=E(i=>Ea(i,t.lesson_id,o));return {content:[u(String(r!==null))]}}catch(o){return a(`submit_feedback failed for '${t.lesson_id}': ${o}`)}}),n$2.registerTool("skip_lesson",{title:"Skip Lesson",description:"Decline a lesson cue: call this when the coaching hook asked for a lesson but the completed work does not warrant one (pure questions, chat, nothing technical). Re-arms the pacing counter so the cue is not repeated immediately. Never call it after delivering a lesson \u2014 log_lesson already resolves the cue.",inputSchema:z.object({reason:z.string().min(1).describe("One line: why no lesson was warranted (shown in `devcoach doctor`)")}),annotations:{title:"Skip Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async t=>{try{return E(o=>aa(o,t.reason)),{content:[u("Cue skipped \u2014 pacing re-armed. Output nothing to the user.")]}}catch(o){return a(`skip_lesson failed: ${o}`)}}),n$2.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:z.object({topic:z.string().describe("Topic id \u2014 prefer a single word, max 3 words"),confidence:h$1.default(5).describe("Initial confidence 0-10 (default 5)"),group:z.string().nullish().describe("Optional group name; 'Other' if omitted")}),annotations:{title:"Add Topic",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async t=>{try{return E(o=>{R(o,t.topic,t.confidence),t.group&&t.group!=="Other"&&V(o,t.topic,t.group);}),{content:[u("true")]}}catch(o){return a(`add_topic failed for '${t.topic}': ${o}`)}}),n$2.registerTool("remove_topic",{title:"Remove Topic",description:"Remove a topic from the knowledge map entirely. Returns true if it existed.",inputSchema:z.object({topic:z.string().describe("Topic id to remove")}),annotations:{title:"Remove Topic",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async t=>{try{let o=E(r=>S(r,t.topic));return {content:[u(String(o))]}}catch(o){return a(`remove_topic failed for '${t.topic}': ${o}`)}}),n$2.registerTool("add_group",{title:"Add Group",description:"Create a new (initially empty) knowledge group. Idempotent \u2014 returns true either way.",inputSchema:z.object({name:z.string().describe("Group name, e.g. 'Machine Learning'")}),annotations:{title:"Add Group",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async t=>{try{return E(o=>T(o,t.name.trim())),{content:[u("true")]}}catch(o){return a(`add_group failed for '${t.name}': ${o}`)}}),n$2.registerTool("remove_group",{title:"Remove Group",description:"Delete a knowledge group. Its topics move to Other. Returns true if it existed.",inputSchema:z.object({name:z.string().describe("Group name to delete")}),annotations:{title:"Remove Group",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async t=>{try{let o=E(r=>U(r,t.name));return {content:[u(String(o))]}}catch(o){return a(`remove_group failed for '${t.name}': ${o}`)}}),n$2.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). nudge_every: integer 0-1000 interactions between lesson cues (0 = cue every turn). nudge_scope: 'session' | 'global'. share_name: the sender name proposed when sharing a lesson (max 80 chars, empty clears). Returns the full updated Settings.",inputSchema:z.object({key:z.enum(["max_per_day","min_gap_minutes","nudge_every","nudge_scope","share_name"]).describe("Setting key"),value:z.string().describe("New value (integer string; 'session'|'global' for nudge_scope; free text for share_name)")}),outputSchema:rt,annotations:{title:"Update Settings",destructiveHint:false,idempotentHint:true,openWorldHint:false}},t=>{let o=i=>g(E(l=>(Y(l,t.key,i),X(l))));if(t.key==="share_name")return t.value.trim().length>v?a(`share_name must be at most ${v} characters`):o(w(t.value));if(t.key==="nudge_scope")return t.value!=="session"&&t.value!=="global"?a("nudge_scope must be 'session' or 'global'"):o(t.value);let r=Number.parseInt(t.value,10);return Number.isNaN(r)?a(`Value must be an integer, got '${t.value}'`):t.key==="max_per_day"&&!(r>=1&&r<=20)?a("max_per_day must be between 1 and 20"):t.key==="min_gap_minutes"&&!(r>=0&&r<=1440)?a("min_gap_minutes must be between 0 and 1440"):t.key==="nudge_every"&&!(r>=0&&r<=1e3)?a("nudge_every must be between 0 and 1000"):o(String(r))}),n$2.registerTool("open_ui",{title:"Open UI",description:"Launch the devcoach web dashboard in the background. port must be 1024-65535.",inputSchema:z.object({port:z.number().int().default(7860).describe("Port (default 7860)")}),annotations:{title:"Open UI",destructiveHint:false,idempotentHint:false,openWorldHint:true}},async t=>{let o=t.port;if(!(o>=1024&&o<=65535))return {content:[u(`error: port ${o} is out of valid range (1024-65535)`)]};let{pingUi:r}=await import('./app-STPXOHWA.js');return await r(o)?{content:[u(`devcoach UI is already running at http://localhost:${o}`)]}:(spawn(process.execPath,[process.argv[1]??"","ui","--port",String(o)],{detached:true,stdio:"ignore"}).unref(),{content:[u(`devcoach UI starting at http://localhost:${o}`)]})}),n$2.registerTool("stop_ui",{title:"Stop UI",description:"Stop the devcoach web dashboard listening on port (whoever started it \u2014 open_ui or a terminal). Graceful: in-flight requests finish first. Reports stopped=false when nothing is running there. port must be 1024-65535.",inputSchema:z.object({port:z.number().int().default(7860).describe("Port (default 7860)")}),annotations:{title:"Stop UI",destructiveHint:false,idempotentHint:true,openWorldHint:true}},async t=>{let o=t.port;if(!(o>=1024&&o<=65535))return {content:[u(`error: port ${o} is out of valid range (1024-65535)`)]};let{stopUi:r}=await import('./app-STPXOHWA.js'),i=await r(o);return {content:[u(i?`devcoach UI on port ${o} stopped`:`No devcoach UI is running on port ${o}`)],structuredContent:{stopped:i,port:o}}}),n$2.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'). Ensures learning-state.md exists and is non-empty (a placeholder if needed) \u2014 there is no notebook argument here. Write the real personalized notebook yourself right after, directly to the path in devcoach://onboarding's notebook_path field. Returns the updated Profile.",inputSchema:z.object({topics:z.record(z.string(),h$1).describe("{topic_id: confidence 0-10}"),groups:z.record(z.string(),z.array(z.string())).optional().describe("{group_name: [topic_id, ...]}")}),outputSchema:De,annotations:{title:"Complete Onboarding",destructiveHint:true,idempotentHint:false,openWorldHint:false}},async t=>{try{let o=E(r=>{r.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[i,l]of Object.entries(t.topics))R(r,i,Math.max(0,Math.min(10,l)));if(t.groups)for(let[i,l]of Object.entries(t.groups))for(let M of l)M in t.topics&&V(r,M,i);return Ca(r)});return mkdirSync(dirname(r),{recursive:!0}),(!existsSync(r)||statSync(r).size===0)&&writeFileSync(r,`# devcoach \u2014 Coaching Notebook
3
3
  `),g(o)}catch(o){return a(`complete_onboarding failed: ${o}`)}}),n$2.registerTool("preview_deep_scan",{title:"Preview Deep Scan",description:"Cheap, metadata-only pre-check for 'Automatic (Deep)' onboarding. Counts and lists Claude Code projects whose last recorded activity falls within a rolling window (months back from now) \u2014 a real date window, not a fixed top-N. Reads no prompt/conversation text, only project paths and activity timestamps. Call this BEFORE spawning the deep-read subagent: over_soft_limit true means there are enough candidates that the user should be asked whether to narrow the window, proceed anyway, or pick specific projects.",inputSchema:z.object({months:z.number().int().min(1).max(24).default(3).describe("Rolling window size in months, counting back from now")}),outputSchema:dt,annotations:{title:"Preview Deep Scan",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}},t=>{try{return g({...d(t.months)})}catch(o){return a(`preview_deep_scan failed: ${o}`)}});let c$1=t=>({title:t,readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false});n$2.registerTool("get_briefing",{title:"Lesson Briefing",description:"Everything needed before delivering a lesson, in ONE call: onboarding status, rate limit, taught topics, knowledge profile, the coaching notebook text and its notebook_path. Same data as the devcoach://briefing resource \u2014 call the tool, it needs no server name.",inputSchema:z.object({}),annotations:c$1("Lesson Briefing")},()=>D("get_briefing",ze())),n$2.registerTool("get_onboarding",{title:"Onboarding Status & Detected Stack",description:"Onboarding status (knowledge_ready, notebook_ready, needs_onboarding), the stack detected across the full Claude Code history with per-project provenance, the default topics and the notebook_path. Same data as the devcoach://onboarding resource.",inputSchema:z.object({}),annotations:c$1("Onboarding Status & Detected Stack")},()=>D("get_onboarding",Ke())),n$2.registerTool("get_profile",{title:"Knowledge Profile",description:"Current knowledge map: topics with confidence 0\u201310 and their groups. Same data as the devcoach://profile resource.",inputSchema:z.object({}),outputSchema:De,annotations:c$1("Knowledge Profile")},()=>D("get_profile",Me())),n$2.registerTool("share_lesson",{title:"Share Lesson",description:"Hand a lesson to another person. transport 'text' (default) returns the card plus a one-line code to paste anywhere; 'link' a server-less URL that previews the lesson and imports it into their dashboard; 'file' the contents of a .devcoach.md file. Only title, summary, body, topic, categories and level travel unless include_context is true; local paths never do. Pick the lesson with get_lessons first.",inputSchema:z.object({lesson_id:z.string().describe("Lesson id (from get_lessons)"),transport:z.enum(["text","link","file"]).default("text").describe("text = card + code to paste; link = share URL; file = .devcoach.md contents"),include_context:z.boolean().default(false).describe("Include project/branch/commit/task context (off by default \u2014 privacy). Local paths are never included."),shared_by:z.string().nullish().describe("Sender name. Omit \u2192 the share_name setting, then git user.name. Empty string \u2192 anonymous.")}),outputSchema:it,annotations:c$1("Share Lesson")},t=>{try{let o=E(r=>{let i=O(r,t.lesson_id);if(!i)return null;let l=ta({explicit:t.shared_by??null,setting:X(r).share_name,gitUserName:n()});return sa(i,{includeContext:t.include_context,sharedBy:l})});return o?g({transport:t.transport,code:ua(o),text:t.transport==="text"?wa(o):null,link:t.transport==="link"?xa(o):null,markdown:t.transport==="file"?Aa(o):null,filename:t.transport==="file"?za(o):null,shared_by:o.shared_by,include_context:t.include_context,reply_check:at[t.transport]}):a(`share_lesson: lesson '${t.lesson_id}' not found \u2014 call get_lessons to pick one`)}catch(o){return a(`share_lesson failed: ${o}`)}}),n$2.registerTool("import_lesson",{title:"Import Lesson",description:"Store a lesson someone shared. Pass whatever the user handed you, verbatim: the devcoach:lesson:1:\u2026 code, the whole copied card, the share link, a URL to the lesson text, the contents of a .devcoach.md file, or a lessons JSON export. The lesson joins the coaching log as their own (feedback works as usual) but never counts against the daily limit; re-importing the same share is reported as a duplicate, not an error.",inputSchema:z.object({payload:z.string().min(1).describe("The code, link, URL, card text, file text or JSON \u2014 verbatim")}),outputSchema:st,annotations:{title:"Import Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:true}},async t=>{try{let o=p(t.payload)&&!t.payload.includes("#devcoach:lesson:")?await q(t.payload):t.payload,r=E(i=>Ga(i,o));return g({...r,reply_check:ct})}catch(o){return o instanceof ra?a(`import_lesson: ${o.message}`):a(`import_lesson failed: ${o}`)}});let d$2=z.object({course_id:z.string(),position:z.number().int(),title:z.string(),kind:m,anchor:z.string(),status:n$1,done_at:z.string().nullable()}),b$1=z.object({id:z.string(),lesson_id:z.string().nullable(),topic_id:z.string(),title:z.string(),goal:z.string().nullable(),prerequisites:z.array(z.object({concept:z.string(),known:z.boolean()})),status:l,created_at:z.string(),updated_at:z.string(),completed_at:z.string().nullable(),steps:z.array(d$2)}),f$3='Write ONE self-contained HTML document at document_path (inline CSS + JS, no external URLs, no fetch, no <form>, no alert/confirm/prompt) with one <section id="step-N"> per step, using your file tools \u2014 never paste HTML into the chat. Code goes in <pre><code class="language-\u2026"> and the document carries ONE <script data-devcoach="highlighter"></script> placeholder: devcoach fills it with the highlighter when you register steps (never write one yourself, never re-read the whole file afterwards). Then register every section with add_course_step (position order, anchor = the section id). Teach one step per message and tell the user the course is on the dashboard under Courses (/devcoach:ui).';n$2.registerTool("create_course",{title:"Create Course",description:"Start a step-by-step course, usually from a lesson the user couldn't follow. Pass the prerequisite chain you explored ([{concept, known}], top-down). Returns the course row, course_dir and document_path \u2014 write the document there yourself, then call add_course_step for each section.",inputSchema:z.object({title:z.string().min(1).describe("Course title, e.g. 'From sums to logarithms'"),topic_id:z.string().min(1).describe("The lesson-style topic id the course belongs to"),goal:z.string().nullish().describe("One sentence: what the user will be able to do"),lesson_id:z.string().nullish().describe("The seed lesson's id, when the course grows from one"),prerequisites:z.array(z.object({concept:z.string().min(1),known:z.boolean()})).default([]).describe("The explored chain, top-down: [{concept, known}]")}),outputSchema:b$1.extend({course_dir:z.string(),document_path:z.string(),seed_context:z.object({task_context:z.string().nullable(),project:z.string().nullable(),repository:z.string().nullable(),branch:z.string().nullable()}).nullable(),reply_check:z.string()}),annotations:{title:"Create Course",destructiveHint:false,idempotentHint:false,openWorldHint:false}},async t=>{try{let o=E(r=>{let i=t.lesson_id?O(r,t.lesson_id):null;if(t.lesson_id&&!i)throw new b(`lesson '${t.lesson_id}' not found \u2014 call get_lessons to pick one`);return {course:f$1(r,{title:t.title,topic_id:t.topic_id,goal:t.goal??null,lesson_id:t.lesson_id??null,prerequisites:t.prerequisites}),seed:i}});return g({...o.course,course_dir:c(o.course.id),document_path:d$1(o.course.id),seed_context:o.seed?{task_context:o.seed.task_context,project:o.seed.project,repository:o.seed.repository,branch:o.seed.branch}:null,reply_check:f$3})}catch(o){return a(`create_course failed: ${o}`)}}),n$2.registerTool("add_course_step",{title:"Add Course Step",description:"Register a step of a course: the section with id=<anchor> must already exist in the course document. Appended by default; pass `after` to insert it after that step (0 = first) \u2014 the following steps are renumbered.",inputSchema:z.object({course_id:z.string().describe("Course id (from create_course)"),title:z.string().min(1).describe("Step title"),kind:m.describe("concept | example | practice | check"),anchor:z.string().regex(/^[a-z0-9][a-z0-9-]*$/).describe("The section's id in the document, e.g. step-1"),after:z.number().int().min(0).nullish().describe("Insert after this step number (0 = first); omit to append")}),outputSchema:b$1.extend({step:d$2}),annotations:{title:"Add Course Step",destructiveHint:false,idempotentHint:false,openWorldHint:false}},async t=>{try{let o=E(r=>{let i=i$1(r,t.course_id,{title:t.title,kind:t.kind,anchor:t.anchor,after:t.after});return {...g$2(r,t.course_id),step:i}});return g(o)}catch(o){return a(`add_course_step failed: ${o}`)}}),n$2.registerTool("update_course_progress",{title:"Update Course Progress",description:"With position: set that step to todo | done | skipped (the course completes itself once every step is done or skipped). Without position: set the course to active | completed | abandoned.",inputSchema:z.object({course_id:z.string().describe("Course id"),position:z.number().int().min(1).nullish().describe("Step number (1-based); omit for the course"),status:z.string().describe("Step: todo | done | skipped \u2014 course: active | completed | abandoned")}),outputSchema:b$1,annotations:{title:"Update Course Progress",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async t=>{try{let o=E(r=>{if(t.position!=null){let i=n$1.parse(t.status);return j$1(r,t.course_id,t.position,i)}return k$1(r,t.course_id,l.parse(t.status))});return o?g(o):a(`update_course_progress: course '${t.course_id}'${t.position!=null?` step ${t.position}`:""} not found \u2014 call get_courses`)}catch(o){return a(`update_course_progress failed: ${o}`)}}),n$2.registerTool("get_courses",{title:"Get Courses",description:"List courses with their steps and progress; filter by id, seed lesson or status. 'continue the course' = status active.",inputSchema:z.object({course_id:z.string().nullish().describe("One course"),lesson_id:z.string().nullish().describe("Courses seeded by this lesson"),status:l.nullish().describe("active | completed | abandoned")}),outputSchema:z.object({courses:z.array(b$1.extend({course_dir:z.string()}))}),annotations:c$1("Get Courses")},t=>{try{let o=E(r=>{if(t.course_id){let i=g$2(r,t.course_id);return i?[i]:[]}return h$2(r,{lesson_id:t.lesson_id,status:t.status})});return g({courses:o.map(r=>({...r,course_dir:c(r.id)}))})}catch{return g({courses:[]})}});let p$1=(t,o)=>({title:t,description:o,mimeType:"application/json"});return n$2.registerResource("profile","devcoach://profile",p$1("Knowledge Profile","Current knowledge map \u2014 topics, confidence scores, and groups."),t=>h(t,Me())),n$2.registerResource("notebook","devcoach://notebook",{title:"Coaching Notebook",description:"The coaching notebook (learning-state.md): observations, recurring patterns, recommended focus, and open hypotheses about the user. Read it to decide what to teach.",mimeType:"text/markdown"},t=>{let o="";try{existsSync(r)&&(o=readFileSync(r,"utf8"));}catch{}return {contents:[{uri:t.href,mimeType:"text/markdown",text:o}]}}),n$2.registerResource("settings","devcoach://settings",p$1("Settings","Current coaching settings (rate limits, UI theme)."),t=>{try{return h(t,E(o=>X(o)))}catch(o){return h(t,{error:String(o)})}}),n$2.registerResource("recent-lessons","devcoach://lessons/recent",p$1("Recent Lessons","Last 10 lessons from the current week."),t=>{try{return h(t,E(o=>H(o,{period:"week",page:1,per_page:10})))}catch(o){return h(t,[{error:String(o)}])}}),n$2.registerResource("stats","devcoach://stats",p$1("Stats","Aggregate coaching statistics: counts and weakest/strongest topics."),t=>{try{return h(t,E(o=>Fa(o)))}catch(o){return h(t,{error:String(o)})}}),n$2.registerResource("taught-topics","devcoach://taught-topics",p$1("Taught Topics","All topic_ids already taught \u2014 read before selecting a new lesson topic."),t=>{try{return h(t,E(o=>Ha(o)))}catch{return h(t,[])}}),n$2.registerResource("rate-limit","devcoach://rate-limit",p$1("Rate Limit","Current rate-limit status and running lesson total."),t=>{try{let{r:o,total:r}=E(l=>({r:Ba(l),total:G(l)})),i={allowed:o.allowed,total_lessons:r};return o.reason!=null&&(i.reason=o.reason),h(t,i)}catch(o){return h(t,{allowed:false,reason:`Rate limit check unavailable: ${o}`})}}),n$2.registerResource("briefing","devcoach://briefing",p$1("Lesson Briefing","Everything needed before delivering a lesson in ONE read: onboarding status, rate limit, taught topics, knowledge profile, and the coaching notebook."),t=>h(t,ze())),n$2.registerResource("context","devcoach://context",p$1("Workspace Context","Auto-detected git context and most-used lesson metadata defaults."),t=>{try{let o$1=o(),r=E(i=>ca(i));return h(t,{git:o$1,usage_defaults:r})}catch(o){return h(t,{error:String(o)})}}),n$2.registerResource("onboarding","devcoach://onboarding",p$1("Onboarding","Onboarding status, the stack detected across the full Claude Code history (with per-project provenance), and project topic defaults."),t=>h(t,Ke())),n$2.registerResource("lesson",new ResourceTemplate("devcoach://lessons/{lesson_id}",{list:void 0}),p$1("Lesson","A single lesson by id."),(t,o)=>{try{let r=String(o.lesson_id),i=E(l=>O(l,r));return i?h(t,i):h(t,{error:`Lesson '${r}' not found`})}catch(r){return h(t,{error:String(r)})}}),n$2.registerPrompt("devcoach_instructions",{title:"devcoach coaching instructions",description:"Full coaching instructions for the devcoach skill (SKILL.md plus its reference files)."},()=>{let t=f$2().map(o=>`
4
4
 
5
5
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devcoach",
3
- "version": "2.8.0-next.671.gdec5426",
3
+ "version": "2.8.0-next.673.g14f8bb5",
4
4
  "mcpName": "io.github.UltimaPhoenix/devcoach",
5
5
  "description": "Stay sharp while your AI does the work: learn what you don't know yet from the code your AI agent writes, one short lesson at a time. Free and 100% local.",
6
6
  "license": "AGPL-3.0-only",
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- var e={version:"2.8.0-next.671.gdec5426"};var s=e.version;export{s as a};