devcoach 0.9.6 → 1.0.1
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
|
@@ -36,21 +36,21 @@ AI agents now write much of our code — which makes it easy to ship more while
|
|
|
36
36
|
|
|
37
37
|
```mermaid
|
|
38
38
|
flowchart TD
|
|
39
|
-
A([Task completed]) --> B
|
|
40
|
-
B -->|
|
|
41
|
-
B -->|
|
|
42
|
-
|
|
43
|
-
subgraph
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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]
|
|
47
49
|
end
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
G --> F([Done])
|
|
53
|
-
G -.->|prompts| U(["You: ✅ ❌ ⏭"])
|
|
51
|
+
S --> Y(["Silent — pacing re-armed"])
|
|
52
|
+
F --> H(["Done — counter reset"])
|
|
53
|
+
F -.->|prompts| U(["You: ✅ / ❌"])
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
→ [Full decision flow: session startup · lesson selection · depth calibration](docs/how-it-works.md)
|
|
@@ -75,6 +75,7 @@ devcoach runs **locally** — a stdio MCP server that stores everything in `~/.d
|
|
|
75
75
|
|
|
76
76
|
- **Developer, comfortable in a terminal → [Homebrew](#homebrew-recommended-for-developers)** — one `brew install`, and you get the `devcoach` CLI too.
|
|
77
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.
|
|
78
79
|
- Anything else (npx, manual config, other agents, claude.ai web) is under **[Other install methods](#other-install-methods)**.
|
|
79
80
|
|
|
80
81
|
### Homebrew (recommended for developers)
|
|
@@ -128,15 +129,17 @@ See [Claude Code plugin](docs/install/claude-code-plugin.md) for how it works.
|
|
|
128
129
|
|
|
129
130
|
### Claude Desktop extension (`.mcpb`) (recommended for Claude Desktop) — **Beta**
|
|
130
131
|
|
|
131
|
-
A single bundle that runs on Claude Desktop's built-in runtime — no Node or terminal needed:
|
|
132
|
+
A single bundle that runs on Claude Desktop's built-in runtime — no Node or terminal needed. Every GitHub Release ships one:
|
|
133
|
+
|
|
134
|
+
1. Download `devcoach-<version>.mcpb` from the [latest release](https://github.com/UltimaPhoenix/dev-coach/releases/latest)
|
|
135
|
+
2. Claude Desktop → **Settings → Extensions → Install Extension…** → pick the `.mcpb`
|
|
136
|
+
|
|
137
|
+
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?
|
|
132
138
|
|
|
133
139
|
```bash
|
|
134
|
-
npm run mcpb # → dist-mcpb/devcoach-<version>.mcpb
|
|
135
|
-
# Claude Desktop → Settings → Extensions → Install Extension… → pick the .mcpb
|
|
140
|
+
npm run mcpb # → dist-mcpb/devcoach-<version>.mcpb (npm run mcpb:sign self-signs it)
|
|
136
141
|
```
|
|
137
142
|
|
|
138
|
-
`npm run mcpb:sign` self-signs it (installs as an *unverified publisher*; a real code-signing cert is needed for a verified signature). Prebuilt `.mcpb` releases and a Desktop directory listing are planned.
|
|
139
|
-
|
|
140
143
|
### Other install methods — **Beta**
|
|
141
144
|
|
|
142
145
|
<details>
|
|
@@ -220,7 +223,7 @@ Codex asks you to **trust** the new hooks once on the next run — approve them
|
|
|
220
223
|
<details>
|
|
221
224
|
<summary><strong>Other MCP agents</strong> (Cursor, Windsurf, Cline, Continue, Zed…)</summary>
|
|
222
225
|
|
|
223
|
-
Add this to your agent's MCP config file:
|
|
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):
|
|
224
227
|
|
|
225
228
|
```json
|
|
226
229
|
{
|
|
@@ -264,64 +267,62 @@ This gives claude.ai the coaching behaviour without the MCP tools (lesson loggin
|
|
|
264
267
|
|
|
265
268
|
## Onboarding
|
|
266
269
|
|
|
267
|
-
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.
|
|
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.)
|
|
268
271
|
|
|
269
272
|
→ [Full onboarding walkthrough](docs/usage/coaching.md)
|
|
270
273
|
|
|
271
274
|
### Phase 1 — Choose how to set up your profile
|
|
272
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
|
+
|
|
273
278
|
```
|
|
274
|
-
devcoach: Your knowledge profile isn't set up yet.
|
|
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).
|
|
275
281
|
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
278
286
|
```
|
|
279
287
|
|
|
280
|
-
|
|
288
|
+
### Phase 2 — Build your profile
|
|
281
289
|
|
|
282
|
-
|
|
290
|
+
#### Automatic (strongly recommended)
|
|
283
291
|
|
|
284
|
-
|
|
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).
|
|
285
293
|
|
|
286
|
-
#### Automatic (
|
|
294
|
+
#### Automatic (Deep)
|
|
287
295
|
|
|
288
|
-
|
|
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.
|
|
289
297
|
|
|
290
|
-
|
|
291
|
-
I detected these technologies in your project:
|
|
298
|
+
#### Guided
|
|
292
299
|
|
|
293
|
-
|
|
294
|
-
docker → confidence 7 (keep? or enter 0–10 to adjust)
|
|
295
|
-
github_actions → confidence 6 (keep? or enter 0–10 to adjust)
|
|
296
|
-
react → confidence 5 (keep? or enter 0–10 to adjust)
|
|
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.
|
|
297
301
|
|
|
298
|
-
|
|
299
|
-
```
|
|
302
|
+
#### Import backup
|
|
300
303
|
|
|
301
|
-
|
|
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.
|
|
302
305
|
|
|
303
|
-
|
|
304
|
-
Here's how I'd organise these:
|
|
306
|
+
### Phase 3 — Profile saved, coaching begins
|
|
305
307
|
|
|
306
|
-
|
|
307
|
-
Frontend → react, html_css
|
|
308
|
-
DevOps → docker, github_actions
|
|
309
|
-
Databases → postgresql, redis
|
|
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:
|
|
310
309
|
|
|
311
|
-
Does this look right? Any changes?
|
|
312
310
|
```
|
|
311
|
+
✓ Profile saved — 24 topics across 6 groups.
|
|
313
312
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
313
|
+
### Languages
|
|
314
|
+
- **typescript** — 7/10
|
|
315
|
+
- **java** — 5/10
|
|
317
316
|
|
|
318
|
-
###
|
|
317
|
+
### DevOps
|
|
318
|
+
- **docker** — 7/10
|
|
319
|
+
- **github_actions** — 6/10
|
|
319
320
|
|
|
320
|
-
|
|
321
|
-
|
|
321
|
+
### Other
|
|
322
|
+
- **regex** — 4/10
|
|
322
323
|
|
|
323
|
-
|
|
324
|
-
|
|
324
|
+
Change any of this later in chat ("set my Java confidence to 7"), with the
|
|
325
|
+
`devcoach` CLI, or in `devcoach ui`.
|
|
325
326
|
```
|
|
326
327
|
|
|
327
328
|
That's it. You go back to work. Coaching happens silently in the background.
|
|
@@ -337,10 +338,8 @@ You: Refactor this endpoint to handle concurrent requests properly.
|
|
|
337
338
|
|
|
338
339
|
Agent: [refactors the code, explains the changes]
|
|
339
340
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
**Promise.allSettled vs Promise.all — don't let one failure sink the batch**
|
|
341
|
+
### ──────── 🎓 devcoach ────────
|
|
342
|
+
**Promise.allSettled vs Promise.all — don't let one failure sink the batch** · TypeScript · Mid
|
|
344
343
|
|
|
345
344
|
Promise.all rejects the moment any promise rejects, and you lose the results of
|
|
346
345
|
the ones that already succeeded. For independent work (fan-out fetches, batch
|
|
@@ -354,14 +353,15 @@ Promise.allSettled always resolves, giving you a status for every promise:
|
|
|
354
353
|
Use Promise.all when the tasks are genuinely all-or-nothing; reach for
|
|
355
354
|
allSettled when partial success is meaningful and you want to report failures.
|
|
356
355
|
|
|
357
|
-
💡 Senior tip
|
|
358
|
-
|
|
359
|
-
|
|
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
360
|
|
|
361
|
-
Did that land?
|
|
361
|
+
Did that land? ✅ know (y) · ❌ don't know (n)
|
|
362
362
|
```
|
|
363
363
|
|
|
364
|
-
|
|
364
|
+
Reply `y` or `n` (or ✅ / ❌) to adjust your confidence on that topic and shape future lessons — or just move on: no reply means no change.
|
|
365
365
|
|
|
366
366
|
---
|
|
367
367
|
|
|
@@ -377,7 +377,7 @@ Responding adjusts your confidence on that topic and shapes future lessons.
|
|
|
377
377
|
|
|
378
378
|
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.
|
|
379
379
|
|
|
380
|
-
→ [
|
|
380
|
+
→ [The coaching notebook](docs/index.mdx#the-coaching-notebook) · [Web dashboard guide](docs/usage/web-ui.md)
|
|
381
381
|
|
|
382
382
|
---
|
|
383
383
|
|
|
@@ -414,12 +414,12 @@ The CLI is a secondary interface for querying and managing your coaching data. E
|
|
|
414
414
|
| `devcoach feedback <id> <know\|dont_know\|clear>` | Record comprehension |
|
|
415
415
|
| `devcoach set max_per_day <n>` | Max lessons per day (default 2) |
|
|
416
416
|
| `devcoach set min_gap_minutes <n>` | Minutes between lessons (default 240) |
|
|
417
|
-
| `devcoach backup [file.zip]` | Export knowledge + lessons + settings |
|
|
417
|
+
| `devcoach backup [file.zip]` | Export knowledge + lessons + settings + notebook |
|
|
418
418
|
| `devcoach restore <file.zip>` | Restore from a backup |
|
|
419
419
|
| `devcoach setup` | Run the onboarding wizard in the terminal |
|
|
420
420
|
| `devcoach ui` | Open the web dashboard |
|
|
421
421
|
|
|
422
|
-
(Prefix with `npx -y` if you haven't installed globally.) Full reference: [docs/cli.md](docs/usage/cli.md)
|
|
422
|
+
(Prefix with `npx -y` if you haven't installed globally.) Full reference: [docs/usage/cli.md](docs/usage/cli.md)
|
|
423
423
|
|
|
424
424
|
---
|
|
425
425
|
|
|
@@ -432,7 +432,7 @@ devcoach set max_per_day 5 # more lessons per day
|
|
|
432
432
|
devcoach set min_gap_minutes 60 # lessons closer together
|
|
433
433
|
```
|
|
434
434
|
|
|
435
|
-
Or use the [web dashboard](docs/usage/web-ui.md) Settings page. See [docs/configuration.md](docs/reference/configuration.md) for all options.
|
|
435
|
+
Or use the [web dashboard](docs/usage/web-ui.md) Settings page. See [docs/reference/configuration.md](docs/reference/configuration.md) for all options.
|
|
436
436
|
|
|
437
437
|
---
|
|
438
438
|
|
|
@@ -488,7 +488,7 @@ claude mcp remove --scope user devcoach # remove from Claude Code (install use
|
|
|
488
488
|
rm -rf ~/.devcoach # delete all coaching data (back up first: devcoach backup)
|
|
489
489
|
```
|
|
490
490
|
|
|
491
|
-
For Claude Desktop, delete the `devcoach` key from the platform config file (paths in **Manual setup** above). Also remove the two hook entries from `~/.claude/settings.json`
|
|
491
|
+
For Claude Desktop, delete the `devcoach` key from the platform config file (paths in **Manual setup** above). Also remove the two hook entries from `~/.claude/settings.json` — one under `hooks.Stop` (`devcoach stop-hook`) and one under `hooks.UserPromptSubmit` (`devcoach prompt-hook`).
|
|
492
492
|
|
|
493
493
|
---
|
|
494
494
|
|
|
@@ -497,11 +497,13 @@ For Claude Desktop, delete the `devcoach` key from the platform config file (pat
|
|
|
497
497
|
**Full documentation:** The README is a quick start. For detailed guides, visit the **[official docs](https://ultimaphoenix.github.io/dev-coach/)**:
|
|
498
498
|
|
|
499
499
|
- **[Why devcoach exists](docs/why.md)** — the philosophy
|
|
500
|
-
- **[
|
|
500
|
+
- **[Coaching in your agent](docs/usage/coaching.md)** — onboarding, your first lesson, the feedback loop
|
|
501
501
|
- **[Using the web dashboard](docs/usage/web-ui.md)** — personalise your knowledge map, filter lessons, jump to code context
|
|
502
502
|
- **[CLI reference](docs/usage/cli.md)** — all commands for querying and managing data
|
|
503
|
-
- **[
|
|
504
|
-
- **[
|
|
503
|
+
- **[Installation](docs/install/index.md)** — every install method, agent by agent
|
|
504
|
+
- **[Configuration](docs/reference/configuration.md)** — rate limits, pacing, data location, schema
|
|
505
|
+
- **[Privacy & security](docs/reference/privacy.md)** — local-first architecture, what is read and stored
|
|
506
|
+
- **[How it works](docs/how-it-works.md)** — session startup, coaching loop, lesson selection
|
|
505
507
|
- **[Vision & roadmap](docs/vision.md)** — free, local, not commercialized; ideas we're exploring next
|
|
506
508
|
- **[Plugin marketplace](docs/install/claude-code-plugin.md#about-the-personal-marketplace)** — UltimaPhoenix plugin collection
|
|
507
509
|
|
|
@@ -510,7 +512,7 @@ For Claude Desktop, delete the `devcoach` key from the platform config file (pat
|
|
|
510
512
|
## Community
|
|
511
513
|
|
|
512
514
|
- **Star the repo** — help others discover it
|
|
513
|
-
- **[GitHub
|
|
515
|
+
- **[GitHub Issues](https://github.com/UltimaPhoenix/dev-coach/issues)** — feature requests, feedback and questions: open an issue
|
|
514
516
|
|
|
515
517
|
---
|
|
516
518
|
|
|
@@ -555,9 +557,9 @@ Copyright (C) 2026 [UltimaPhoenix](https://github.com/UltimaPhoenix)
|
|
|
555
557
|
|
|
556
558
|
devcoach is licensed under the [GNU Affero General Public License v3.0](LICENSE) (`AGPL-3.0-only`).
|
|
557
559
|
You may use, modify, and distribute it — **provided that any modified version you distribute, or run
|
|
558
|
-
as a network service, is also released as open source under the AGPL**.
|
|
559
|
-
license
|
|
560
|
-
[open an issue](https://github.com/UltimaPhoenix/dev-coach/issues)
|
|
560
|
+
as a network service, is also released as open source under the AGPL**. There is **no commercial
|
|
561
|
+
license on offer today**; if you need to embed devcoach in a closed-source product,
|
|
562
|
+
[open an issue](https://github.com/UltimaPhoenix/dev-coach/issues) and ask.
|
|
561
563
|
|
|
562
564
|
### Licensing FAQ
|
|
563
565
|
|
|
@@ -572,6 +574,6 @@ like querying an AGPL-licensed database. Your own projects keep whatever license
|
|
|
572
574
|
- ✅ *"I use devcoach inside Claude Code while building my closed-source startup app."* — Free. Your app stays proprietary, zero obligations.
|
|
573
575
|
- ✅ *"My whole team installs devcoach to get coaching on our internal/commercial repos."* — Free. Using the unmodified tool at a company is fine.
|
|
574
576
|
- ⚠️ *"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.
|
|
575
|
-
- 💼 *"I want to embed devcoach in my paid product and keep my changes closed."* —
|
|
577
|
+
- 💼 *"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.
|
|
576
578
|
|
|
577
579
|
Keep using devcoach as a tool/executable (not as an imported library) and the AGPL never reaches your own code.
|
package/dist/bin.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var t=process.argv[2]??"",r=new Set(["stop-hook","prompt-hook","gemini-stop-hook","gemini-prompt-hook","codex-stop-hook","codex-prompt-hook","onboard-hook","lesson-ready"]);try{if(r.has(t)){let{runHook:o}=await import('./hooks-5PEDPGZD.js');o(t);}else {let{runCli:o}=await import('./commands-
|
|
2
|
+
var t=process.argv[2]??"",r=new Set(["stop-hook","prompt-hook","gemini-stop-hook","gemini-prompt-hook","codex-stop-hook","codex-prompt-hook","onboard-hook","lesson-ready"]);try{if(r.has(t)){let{runHook:o}=await import('./hooks-5PEDPGZD.js');o(t);}else {let{runCli:o}=await import('./commands-BGJEIPG5.js');await o();}}catch(o){console.error(o),process.exit(1);}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {readFileSync,readdirSync,statSync,existsSync,realpathSync}from'fs';import {dirname,basename,join}from'path';import {homedir,tmpdir}from'os';import {execFileSync}from'child_process';import {fileURLToPath}from'url';var H=[["package.json","javascript",6],["tsconfig*.json","typescript",6],["requirements*.txt","python",6],["pyproject.toml","python",6],["setup.py","python",6],["go.mod","go",6],["Cargo.toml","rust",6],["pom.xml","java",6],["build.gradle","java",6],["build.gradle.kts","java",6],["*.csproj","csharp",6],["Gemfile","ruby",6],["composer.json","php",6],["Dockerfile","docker",7],["Dockerfile.*","docker",7],["docker-compose.yml","docker_compose",7],["docker-compose.yaml","docker_compose",7],["docker-compose.*.yml","docker_compose",7],[".github/workflows/*.yml","github_actions",6],[".github/workflows/*.yaml","github_actions",6],[".gitlab-ci.yml","gitlab_ci",6],["Jenkinsfile","ci_cd",6],["*.tf","terraform",6],["*.sql","sql",5],[".git","git",7],["kubernetes/*.yaml","kubernetes",6],["kubernetes/*.yml","kubernetes",6],["k8s/*.yaml","kubernetes",6],["*.bicep","azure",6],["serverless.yml","aws",6],["serverless.yaml","aws",6],["template.yaml","aws",5],["angular.json","angular",6],["svelte.config.*","svelte",6],["nuxt.config.*","vue",6],["vite.config.*","javascript",6],["*.xcodeproj","swift",6],["*.xcworkspace","swift",6],["Package.swift","swift",6]],G={react:"react","react-dom":"react",vue:"vue","@angular/core":"angular",svelte:"svelte",next:"nextjs",nuxt:"vue",fastify:"fastify",express:"express",koa:"express","@nestjs/core":"node_js"},J={django:"django",flask:"flask",fastapi:"fastapi",starlette:"fastapi",tornado:"python",aiohttp:"python",sqlalchemy:"postgresql",celery:"python"};function $(e){let t="";for(let r of e)r==="*"?t+="[^/]*":r==="?"?t+="[^/]":t+=r.replace(/[.+^${}()|[\]\\]/g,String.raw`\$&`);return new RegExp(`^${t}$`)}function U(e,t){let r=t.lastIndexOf("/"),n=r>=0?join(e,t.slice(0,r)):e,o=r>=0?t.slice(r+1):t,i;try{i=readdirSync(n);}catch{return false}let c=$(o);return i.some(l=>c.test(l))}function k(e){try{return existsSync(e)?readFileSync(e,"utf8"):null}catch{return null}}function X(e,t){let r=k(join(e,"package.json"));if(!r)return;let n;try{n=JSON.parse(r);}catch{return}let o=new Set([...Object.keys(n.dependencies??{}),...Object.keys(n.devDependencies??{})]);for(let[i,c]of Object.entries(G))o.has(i)&&t(c,6);}function z(e,t){let r=["requirements.txt","requirements-dev.txt","requirements/base.txt","pyproject.toml"];for(let n of r){let o=k(join(e,n))?.toLowerCase();if(o)for(let[i,c]of Object.entries(J))o.includes(i)&&t(c,6);}}function S(e){let t={},r=(n,o)=>{(t[n]??-1)<o&&(t[n]=o);};for(let[n,o,i]of H)U(e,n)&&r(o,i);return X(e,r),z(e,r),t}function f(...e){let t={};for(let r of e)for(let[n,o]of Object.entries(r))(t[n]??-1)<o&&(t[n]=o);return t}var B=15,Q=2,tt=40,et=400,x=1200,nt=new Set(["node_modules","dist","build","out","coverage","vendor","venv","__pycache__","Pods","DerivedData","target","tmp"]),rt={".ts":"typescript",".tsx":"typescript",".js":"javascript",".jsx":"javascript",".mjs":"javascript",".py":"python",".swift":"swift",".java":"java",".kt":"java",".go":"go",".rs":"rust",".rb":"ruby",".php":"php",".cs":"csharp",".sql":"sql",".tf":"terraform"},w={detected_stack:{},projects:[],scanned_projects:0};function A(){let e=process.env.DEVCOACH_CLAUDE_DIR??process.env.CLAUDE_CONFIG_DIR,t=e??join(homedir(),".claude");return {configPath:e?join(t,".claude.json"):join(homedir(),".claude.json"),historyPath:join(t,"history.jsonl"),projectsDir:join(t,"projects")}}function b(e){try{return realpathSync(e)}catch{return e}}function E(e){let t=b(e);return t.startsWith(b(homedir()))?false:t.startsWith(b(tmpdir()))||t.startsWith("/private/")}function v(e){try{return statSync(e).isDirectory()}catch{return false}}function st(e,t){let r=[],n=[{dir:e,depth:0}],o=0;for(;n.length>0&&o<tt&&t.left>0;){let{dir:i,depth:c}=n.shift();if(o++,t.left--,r.push(S(i)),c>=Q)continue;let l;try{l=readdirSync(i);}catch{continue}for(let u of l){if(u.startsWith(".")||nt.has(u))continue;let s=join(i,u);v(s)&&n.push({dir:s,depth:c+1});}}return f(...r)}function ot(e){let t={};if(!Array.isArray(e))return t;for(let r of e){if(typeof r!="string")continue;let n=r.lastIndexOf("."),o=n>=0?rt[r.slice(n).toLowerCase()]:void 0;o&&(t[o]=Math.max(t[o]??0,5));}return t}function O(e){let t=new Map,r;try{r=readFileSync(e,"utf8");}catch{return t}for(let n of r.split(`
|
|
3
|
-
`))if(n)try{let o=JSON.parse(n);if(typeof o.project!="string")continue;let i=typeof o.timestamp=="number"?o.timestamp:0,c=t.get(o.project)??{count:0,last:0};t.set(o.project,{count:c.count+1,last:Math.max(c.last,i)});}catch{}return t}function it(e,t){let r=t.replace(/[^a-zA-Z0-9]/g,"-"),n=(()=>{try{return readFileSync(join(e,r,"memory","MEMORY.md"),"utf8").trim()}catch{return null}})();return n?n.length>x?`${n.slice(0,x)}\u2026`:n:null}function xt(){try{let{configPath:e,historyPath:t,projectsDir:r}=A(),n;try{n=JSON.parse(readFileSync(e,"utf8"))?.projects??{};}catch{return w}let o=O(t),i=Object.keys(n).filter(s=>!E(s)&&v(s)).sort((s,a)=>(o.get(a)?.last??0)-(o.get(s)?.last??0)).slice(0,B),c={left:et},l=[];for(let s of i){let a=f(st(s,c),ot(n[s]?.exampleFiles)),h=it(r,s);if(Object.keys(a).length===0&&h===null)continue;let d=o.get(s);l.push({stack:a,signal:{name:basename(s),path:s,topics:Object.keys(a).sort((N,L)=>N.localeCompare(L)),prompt_count:d?.count??0,last_activity:d?.last?new Date(d.last).toISOString():null,memory:h}});}let u=f(...l.map(s=>s.stack));for(let[s,a]of Object.entries(u))l.filter(d=>d.stack[s]!==void 0).length>=3&&a<8&&(u[s]=a+1);return {detected_stack:u,projects:l.map(s=>s.signal).sort((s,a)=>a.prompt_count-s.prompt_count).slice(0,20),scanned_projects:i.length}}catch{return w}}var ct=8,at=50,R=(e,t)=>({window_months:e,cutoff:t,candidate_count:0,over_soft_limit:false,candidates:[]});function wt(e){let t=Number.isFinite(e)&&e>0?e:3,r=Date.now()-t*30*864e5,n=new Date(r).toISOString();try{let{configPath:o,historyPath:i}=A(),c;try{c=JSON.parse(readFileSync(o,"utf8"))?.projects??{};}catch{return R(t,n)}let l=O(i),u=Object.keys(c).filter(s=>!E(s)&&v(s)).map(s=>({path:s,seen:l.get(s)})).filter(s=>(s.seen?.last??0)>=r).sort((s,a)=>(a.seen?.last??0)-(s.seen?.last??0));return {window_months:t,cutoff:n,candidate_count:u.length,over_soft_limit:u.length>ct,candidates:u.slice(0,at).map(s=>({name:basename(s.path),path:s.path,last_activity:s.seen?.last?new Date(s.seen.last).toISOString():null,prompt_count:s.seen?.count??0}))}}catch{return R(t,n)}}function _(...e){try{return execFileSync("git",e,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}}var D={"github.com":"github","gitlab.com":"gitlab","bitbucket.org":"bitbucket"};function pt(e){if(!e)return [null,null];let t=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(e);if(t){let n=(t[1]??"").toLowerCase();return [t[2]??e,D[n]??"local"]}let r=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(e);if(r){let n=(r[1]??"").toLowerCase();return [r[2]??e,D[n]??"local"]}return [e,"local"]}function Et(){let e=process.cwd(),t=_("rev-parse","--abbrev-ref","HEAD"),r=_("rev-parse","HEAD"),n=_("remote","get-url","origin"),[o,i]=pt(n),c;return o?c=o.replace(/\/+$/,"").split("/").pop()??null:c=basename(e)||null,t==="HEAD"&&(t=null),{project:c,repository:o,branch:t,commit_hash:r,folder:e,repository_platform:i}}var C={version:"0.
|
|
3
|
+
`))if(n)try{let o=JSON.parse(n);if(typeof o.project!="string")continue;let i=typeof o.timestamp=="number"?o.timestamp:0,c=t.get(o.project)??{count:0,last:0};t.set(o.project,{count:c.count+1,last:Math.max(c.last,i)});}catch{}return t}function it(e,t){let r=t.replace(/[^a-zA-Z0-9]/g,"-"),n=(()=>{try{return readFileSync(join(e,r,"memory","MEMORY.md"),"utf8").trim()}catch{return null}})();return n?n.length>x?`${n.slice(0,x)}\u2026`:n:null}function xt(){try{let{configPath:e,historyPath:t,projectsDir:r}=A(),n;try{n=JSON.parse(readFileSync(e,"utf8"))?.projects??{};}catch{return w}let o=O(t),i=Object.keys(n).filter(s=>!E(s)&&v(s)).sort((s,a)=>(o.get(a)?.last??0)-(o.get(s)?.last??0)).slice(0,B),c={left:et},l=[];for(let s of i){let a=f(st(s,c),ot(n[s]?.exampleFiles)),h=it(r,s);if(Object.keys(a).length===0&&h===null)continue;let d=o.get(s);l.push({stack:a,signal:{name:basename(s),path:s,topics:Object.keys(a).sort((N,L)=>N.localeCompare(L)),prompt_count:d?.count??0,last_activity:d?.last?new Date(d.last).toISOString():null,memory:h}});}let u=f(...l.map(s=>s.stack));for(let[s,a]of Object.entries(u))l.filter(d=>d.stack[s]!==void 0).length>=3&&a<8&&(u[s]=a+1);return {detected_stack:u,projects:l.map(s=>s.signal).sort((s,a)=>a.prompt_count-s.prompt_count).slice(0,20),scanned_projects:i.length}}catch{return w}}var ct=8,at=50,R=(e,t)=>({window_months:e,cutoff:t,candidate_count:0,over_soft_limit:false,candidates:[]});function wt(e){let t=Number.isFinite(e)&&e>0?e:3,r=Date.now()-t*30*864e5,n=new Date(r).toISOString();try{let{configPath:o,historyPath:i}=A(),c;try{c=JSON.parse(readFileSync(o,"utf8"))?.projects??{};}catch{return R(t,n)}let l=O(i),u=Object.keys(c).filter(s=>!E(s)&&v(s)).map(s=>({path:s,seen:l.get(s)})).filter(s=>(s.seen?.last??0)>=r).sort((s,a)=>(a.seen?.last??0)-(s.seen?.last??0));return {window_months:t,cutoff:n,candidate_count:u.length,over_soft_limit:u.length>ct,candidates:u.slice(0,at).map(s=>({name:basename(s.path),path:s.path,last_activity:s.seen?.last?new Date(s.seen.last).toISOString():null,prompt_count:s.seen?.count??0}))}}catch{return R(t,n)}}function _(...e){try{return execFileSync("git",e,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}}var D={"github.com":"github","gitlab.com":"gitlab","bitbucket.org":"bitbucket"};function pt(e){if(!e)return [null,null];let t=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(e);if(t){let n=(t[1]??"").toLowerCase();return [t[2]??e,D[n]??"local"]}let r=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(e);if(r){let n=(r[1]??"").toLowerCase();return [r[2]??e,D[n]??"local"]}return [e,"local"]}function Et(){let e=process.cwd(),t=_("rev-parse","--abbrev-ref","HEAD"),r=_("rev-parse","HEAD"),n=_("remote","get-url","origin"),[o,i]=pt(n),c;return o?c=o.replace(/\/+$/,"").split("/").pop()??null:c=basename(e)||null,t==="HEAD"&&(t=null),{project:c,repository:o,branch:t,commit_hash:r,folder:e,repository_platform:i}}var C={version:"1.0.1"};var Mt=C.version;var M=dirname(fileURLToPath(import.meta.url));function T(){for(let e of ["../assets","../../assets"])if(existsSync(join(M,e,"SKILL.md")))return join(M,e);return null}var Ft=()=>{let e=T();return e===null?"devcoach: coaching instructions unavailable (SKILL.md not found in package).":readFileSync(join(e,"SKILL.md"),"utf8")};function Wt(){let e=T();if(e===null)return [];try{return readdirSync(join(e,"references")).filter(t=>t.endsWith(".md")).sort().map(t=>({name:t,content:readFileSync(join(e,"references",t),"utf8")}))}catch{return []}}export{S as a,f as b,xt as c,wt as d,Et as e,Mt as f,Ft as g,Wt as h};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {e,g,b,d}from'./chunk-OSA5I6L7.js';export{c as buildLessonCue,d as cmdLessonReady,b as cmdOnboardHook,g as cmdPromptHook,e as cmdStopHook,a as parseHookPayload}from'./chunk-OSA5I6L7.js';import {f,e as e$1,c,b as b$1,a as a$1,g as g$1,h}from'./chunk-
|
|
2
|
+
import {e,g,b,d}from'./chunk-OSA5I6L7.js';export{c as buildLessonCue,d as cmdLessonReady,b as cmdOnboardHook,g as cmdPromptHook,e as cmdStopHook,a as parseHookPayload}from'./chunk-OSA5I6L7.js';import {f,e as e$1,c,b as b$1,a as a$1,g as g$1,h}from'./chunk-TIK4UYBY.js';import {m,Q as Q$1,p,w,r,q as q$1,S as S$1,F,P,T as T$1,G,z as z$1,D as D$1,A,B,C,E as E$1,N,y,t,j,O as O$1,i,L,H as H$1,J,W}from'./chunk-ECN4MNGM.js';import {writeFileSync,existsSync,readFileSync,mkdirSync,accessSync,constants}from'fs';import {createInterface}from'readline/promises';import {Command}from'commander';import {spawnSync}from'child_process';import {homedir,platform}from'os';import {join,dirname}from'path';import {styleText}from'util';var io=!!process.stdout.isTTY&&!process.env.NO_COLOR,_=(e,o)=>io?styleText(e,o):o,n={green:e=>_("green",e),red:e=>_("red",e),yellow:e=>_("yellow",e),cyan:e=>_("cyan",e),magenta:e=>_("magenta",e),dim:e=>_("dim",e),bold:e=>_("bold",e)};function Z(e,o){return e==="green"?n.green(o):e==="yellow"?n.yellow(o):n.red(o)}var so=/\[[0-9;]*m/g,ao=e=>e.replace(so,""),U=e=>[...ao(e)].length;function Be(e,o,t){let r=Math.max(0,o-U(e));if(t==="right")return " ".repeat(r)+e;if(t==="center"){let i=Math.floor(r/2);return " ".repeat(i)+e+" ".repeat(r-i)}return e+" ".repeat(r)}function I(e,o,t,r=true){let i=o.map((f,m)=>Math.max(r?U(f.header):0,...t.map(d=>U(d[m]??"")))),s=(f,m,d)=>f+i.map(g=>"\u2500".repeat(g+2)).join(m)+d,c=f=>`\u2502${f.map((m,d)=>` ${Be(m??"",i[d]??0,o[d]?.justify??"left")} `).join("\u2502")}\u2502`,p=i.reduce((f,m)=>f+m+3,1),l=[];e&&l.push(Be(n.bold(e),p,"center")),l.push(s("\u256D","\u252C","\u256E")),r&&l.push(c(o.map(f=>n.bold(f.header))),s("\u251C","\u253C","\u2524"));for(let f of t)l.push(c(f));return l.push(s("\u2570","\u2534","\u256F")),l.join(`
|
|
3
3
|
`)}function z(e=""){if(!e)return "\u2500".repeat(60);let t=` ${e} `,r=Math.max(2,60-U(t)),i=Math.floor(r/2);return "\u2500".repeat(i)+t+"\u2500".repeat(r-i)}var Ue=e=>{let o=Math.max(0,Math.min(10,Math.round(e)));return "\u2588".repeat(o)+"\u2591".repeat(10-o)},ze=e=>e>=7?"green":e>=4?"yellow":"red";var u=(e="")=>{console.log(e);};function go(){let e=platform();if(e==="darwin")return join(homedir(),"Library","Application Support","Claude","claude_desktop_config.json");if(e==="win32"){let t=process.env.APPDATA??join(homedir(),"AppData","Roaming");return join(t,"Claude","claude_desktop_config.json")}let o=process.env.XDG_CONFIG_HOME??join(homedir(),".config");return join(o,"Claude","claude_desktop_config.json")}var O=join(homedir(),".claude","settings.json"),te=join(homedir(),".claude","skills","devcoach"),re=join(homedir(),".gemini"),T=join(re,"settings.json"),fo=join(re,"extensions","devcoach","gemini-extension.json"),ie=join(homedir(),".codex"),H=join(ie,"hooks.json"),ee=join(ie,"config.toml"),D=join(homedir(),".agents","skills","devcoach");function We(e){for(let o of (process.env.PATH??"").split(":"))if(o)try{return accessSync(join(o,e),constants.X_OK),join(o,e)}catch{}return null}var se=e=>We(e)!==null;function Ve(){return se("devcoach")?{command:"devcoach",args:["mcp"]}:{command:"npx",args:["-y","devcoach","mcp"]}}function ae(){let e=We("devcoach");return e?e.includes("fnm_multishells")?"devcoach":e:"npx -y devcoach"}function Ke(e,o,t){if(!se(e))return "";t&&spawnSync(e,t,{encoding:"utf8"});let r=spawnSync(e,o,{encoding:"utf8"});return r.status===0?`${n.green("\u2713")} Registered via \`${e} mcp add\``:`${r.stderr??""}${r.stdout??""}`.toLowerCase().includes("already")?`${n.yellow("Already registered")} in ${e} (use --force to overwrite)`:`${n.red(`${e} mcp add failed:`)} ${(r.stderr||r.stdout||"").trim()}`}function ho(e,o){if(!se("claude"))return "";o&&spawnSync("claude",["mcp","remove","--scope",e,"devcoach"],{encoding:"utf8"});let t=Ve(),r=spawnSync("claude",["mcp","add","--scope",e,"devcoach",t.command,"--",...t.args],{encoding:"utf8"});return r.status===0?`${n.green("\u2713")} Registered via \`claude mcp add\` (scope: ${e})`:`${r.stderr??""}${r.stdout??""}`.toLowerCase().includes("already")?`${n.yellow("Already registered")} in Claude Code (use --force to overwrite)`:`${n.red("claude mcp add failed:")} ${(r.stderr||r.stdout||"").trim()}`}function S(e){if(!existsSync(e))return {ok:true,data:{}};try{return {ok:!0,data:JSON.parse(readFileSync(e,"utf8"))??{}}}catch{return {ok:false,error:`${n.red("\u2717")} ${e} is not valid JSON \u2014 fix it and re-run (left unchanged)`}}}function q(e,o,t){let r=S(e);if(!r.ok)return r.error;let i=r.data;i.mcpServers??={};let s=i.mcpServers;return s.devcoach&&!t?`${n.yellow("Already registered")} in ${e} (use --force to overwrite)`:(s.devcoach=o,mkdirSync(dirname(e),{recursive:true}),writeFileSync(e,`${JSON.stringify(i,null,2)}
|
|
4
4
|
`),`${n.green("\u2713")} Installed into ${e}`)}function ko(){let e=ae();return {Stop:{hooks:[{type:"command",command:`${e} stop-hook`,timeout:60}]},UserPromptSubmit:{hooks:[{type:"command",command:`${e} prompt-hook`,timeout:30}]}}}function bo(){let e=ae();return {AfterAgent:{hooks:[{type:"command",command:`${e} gemini-stop-hook`,timeout:6e4}]},BeforeAgent:{hooks:[{type:"command",command:`${e} gemini-prompt-hook`,timeout:3e4}]}}}function $o(){let e=ae();return {Stop:{hooks:[{type:"command",command:`${e} codex-stop-hook`,timeout:60}]},UserPromptSubmit:{hooks:[{type:"command",command:`${e} codex-prompt-hook`,timeout:30}]}}}function Ye(e){return Object.entries(e.enabledPlugins??{}).some(([o,t])=>t&&o.startsWith("devcoach@"))}function ce(){return existsSync(fo)}function de(e){return /dev-?coach/i.test(e)&&/\b(?:stop-hook|prompt-hook|onboard-hook|lesson-ready)\b/.test(e)}function le(e,o){let t=S(e);if(!t.ok)return t.error;let r=t.data;r.hooks??={};let i=r.hooks,s=false;for(let[c,p]of Object.entries(o)){i[c]??=[];let l=i[c],f=l.map((d,g)=>[d,g]).filter(([d])=>(d.hooks??[]).some(g=>de(g.command??""))).map(([,d])=>d),[m]=f;if(!(f.length===1&&m!==void 0&&JSON.stringify(l[m])===JSON.stringify(p))){for(let d of f.toReversed())l.splice(d,1);l.push(p),s=true;}}return s?(mkdirSync(dirname(e),{recursive:true}),writeFileSync(e,`${JSON.stringify(r,null,2)}
|
|
5
5
|
`),`${n.green("\u2713")} Hooks installed into ${e} (${Object.keys(o).join(" + ")})`):`${n.yellow("Already installed")} in ${e} (current layout)`}function wo(){let e=S(O);return e.ok?Ye(e.data)?`${n.yellow("Skipped")} \u2014 the devcoach plugin already provides the hooks (installing both would double-count interactions)`:le(O,ko()):e.error}function yo(){return ce()?`${n.yellow("Skipped")} \u2014 the devcoach Gemini extension already provides the hooks (installing both would double-count interactions)`:le(T,bo())}function E(e){if(!existsSync(join(e,"SKILL.md")))return "missing";try{if(readFileSync(join(e,".devcoach-version"),"utf8").trim()===f)return "current"}catch{}return "outdated"}function oe(e,o){if(!o&&E(e)==="current")return `${n.yellow("Already installed")} in ${e} (current version)`;mkdirSync(e,{recursive:true}),writeFileSync(join(e,"SKILL.md"),g$1());let t=h();if(t.length){let r=join(e,"references");mkdirSync(r,{recursive:true});for(let i of t)writeFileSync(join(r,i.name),i.content);}return writeFileSync(join(e,".devcoach-version"),`${f}
|
|
@@ -30,4 +30,4 @@ ${n.green("Setup complete!")} ${Object.keys(s).length} topics saved.`);}finally{
|
|
|
30
30
|
${n.bold(`devcoach ${n.dim(`v${f}`)}`)}`);for(let[t,r]of e)a(` ${n.cyan(`devcoach ${t}`.padEnd(32))} ${n.dim(r)}`);a(`
|
|
31
31
|
${n.dim("Run")} ${n.bold("devcoach <command> --help")} ${n.dim("for per-command options.")}
|
|
32
32
|
`);let o=ue();o&&a(`${o}
|
|
33
|
-
`);}function v(e){return e??null}function Wo(){let e$1=new Command;return e$1.exitOverride(),e$1.name("devcoach").description("devcoach \u2014 progressive technical coaching").version(`devcoach ${f}`,"-v, --version","output the version number").allowExcessArguments(false).showHelpAfterError("(add --help for usage)").action(()=>Jo()),e$1.command("mcp").description("Start the MCP server (stdio transport)").action(async()=>{let{runStdio:o}=await import('./server-
|
|
33
|
+
`);}function v(e){return e??null}function Wo(){let e$1=new Command;return e$1.exitOverride(),e$1.name("devcoach").description("devcoach \u2014 progressive technical coaching").version(`devcoach ${f}`,"-v, --version","output the version number").allowExcessArguments(false).showHelpAfterError("(add --help for usage)").action(()=>Jo()),e$1.command("mcp").description("Start the MCP server (stdio transport)").action(async()=>{let{runStdio:o}=await import('./server-2WMGCXWK.js');await o();}),e$1.command("profile").description("Show the knowledge map").action(To),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("--feedback <feedback>","know | 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=>jo({period:o.period??"all",category:v(o.category),project:v(o.project),repository:v(o.repository),branch:v(o.branch),commit:v(o.commit),starred:!!o.starred,feedback:v(o.feedback),level:v(o.level),dateFrom:v(o.dateFrom),dateTo:v(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=>Mo(o)),e$1.command("star").description("Mark a lesson as starred").argument("<id>","Lesson ID").action(o=>qe(o,true)),e$1.command("unstar").description("Remove the starred mark from a lesson").argument("<id>","Lesson ID").action(o=>qe(o,false)),e$1.command("delete").description("Permanently delete a lesson").argument("<id>","Lesson ID").action(o=>Ho(o)),e$1.command("feedback").description("Record know/dont_know feedback for a lesson").argument("<id>","Lesson ID").argument("<value>","know | dont_know | clear").action((o,t)=>Do(o,t)),e$1.command("settings").description("Show current settings").action(Eo),e$1.command("stats").description("Coaching statistics and rate-limit status").action(Ro),e$1.command("set").description("Update a setting (max_per_day | min_gap_minutes)").argument("<key>","Setting key").argument("<value>","New value").action((o,t)=>Po(o,t)),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,t)=>Ao(o,Math.max(0,Math.min(10,Number.parseInt(t.confidence??"5",10)||5)),v(t.group))),e$1.command("knowledge-remove").description("Remove a topic from the knowledge map").argument("<topic>","Topic ID").action(o=>No(o)),e$1.command("group-add").description("Register a new knowledge group").argument("<name>","Group name").action(o=>Go(o)),e$1.command("group-remove").description("Delete a knowledge group (topics move to Other)").argument("<name>","Group name").action(o=>Fo(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,t)=>Bo(o,t)),e$1.command("backup").description("Export a full backup (settings + knowledge + lessons) as zip").argument("[file]","Output zip path","devcoach-backup.zip").action(o=>Uo(o)),e$1.command("restore").description("Restore from a backup zip file").argument("<file>","Path to backup zip").action(o=>zo(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=>Xe({claudeCode:!!o.claudeCode,claudeDesktop:!!o.claudeDesktop,gemini:!!o.gemini,codex:!!o.codex,force:!!o.force,skipHook:!!o.skipHook})),e$1.command("doctor").description("Diagnose the devcoach \u21C4 agent wiring and the lesson pacing state").action(Ze),e$1.command("ui").description("Launch the web dashboard").option("--port <port>","Port","7860").action(async o=>{let{startUi:t}=await import('./app-GO3HPOM6.js');t(Number.parseInt(o.port??"7860",10)||7860);}),e$1.command("setup").description("First-run wizard: import backup or build your knowledge profile").action(Ko),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()),e$1}async function fn(){let e=Wo();try{await e.parseAsync(process.argv);}catch(o){let t=o.code??"";if(!t.startsWith("commander."))throw o;if(t==="commander.helpDisplayed"||t==="commander.help"||t==="commander.version")return;process.exit(2);}}export{fn as runCli};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {f,e,d as d$2,c as c$2,b as b$1,a as a$1,h,g as g$1}from'./chunk-EEKM4EY4.js';import {c as c$1,b,a,m,M,g,n,I,R,p,r,q,s,K,d as d$1,z as z$1,D,A,B,C,f as f$1,e as e$1,Q,j,F,T,U,o,P,L,k,w,G}from'./chunk-ECN4MNGM.js';import {spawn}from'child_process';import {mkdirSync,existsSync,statSync,writeFileSync,readFileSync}from'fs';import {dirname}from'path';import {McpServer,ResourceTemplate}from'@modelcontextprotocol/sdk/server/mcp.js';import {StdioServerTransport}from'@modelcontextprotocol/sdk/server/stdio.js';import {z}from'zod';var l=r=>({type:"text",text:r}),ie=r=>({content:[l(JSON.stringify(r))]}),d=r=>({isError:true,content:[l(r)]}),x=r=>({content:[l(JSON.stringify(r))],structuredContent:r}),me={id:z.string(),timestamp:z.string(),topic_id:z.string(),categories:z.array(z.string()),title:z.string(),level:a,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:b.nullable(),starred:z.boolean(),feedback:c$1.nullable()},ge={...me,reply_check:z.string()},se="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.",be={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())}))},fe={max_per_day:z.number().int(),min_gap_minutes:z.number().int(),ui_theme:e$1,nudge_every:z.number().int(),nudge_scope:f$1},ye={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 c(r,u){return {contents:[{uri:r.href,mimeType:"application/json",text:JSON.stringify(u)}]}}function _e(){let r$1=new McpServer({name:"devcoach",version:f},{instructions:"Progressive technical coaching server. Use the devcoach_instructions prompt for full coaching behaviour guidelines."});r$1.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:{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:a.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:b.nullish().describe("github | gitlab | bitbucket | local (auto-detected if omitted)")},outputSchema:ge,annotations:{title:"Log Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e$1=>{try{let t=e(),o={};try{o=m(s=>M(s));}catch{o={};}let i=g({id:e$1.id,timestamp:new Date,topic_id:e$1.topic_id,categories:e$1.categories,title:e$1.title,level:e$1.level,summary:e$1.summary,body:e$1.body,task_context:e$1.task_context??null,project:e$1.project??t.project??o.project??null,repository:e$1.repository??t.repository??o.repository??null,branch:e$1.branch??t.branch??o.branch??null,commit_hash:e$1.commit_hash??t.commit_hash??null,folder:e$1.folder??t.folder??null,repository_platform:e$1.repository_platform??t.repository_platform??o.repository_platform??null});return m(s=>{n(s,i),I(s);}),{content:[l(`Lesson saved. ${se}`),l(JSON.stringify(i))],structuredContent:{...i,reply_check:se}}}catch(t){return d(`log_lesson failed: ${t}`)}}),r$1.registerTool("update_knowledge",{title:"Update Knowledge",description:"Adjust the confidence score for a topic by delta (e.g. +1 or -1). Returns the new confidence (0-10). Creates the topic at confidence 5 if it does not exist.",inputSchema:{topic:z.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 e=>{try{let t=m(o=>R(o,e.topic,e.delta));return {content:[l(String(t))]}}catch(t){return d(`update_knowledge failed for '${e.topic}': ${t}`)}}),r$1.registerTool("get_lessons",{title:"Get Lessons",description:"Query the coaching lesson history. All filters combine. period defaults to all; date_from/date_to override period; limit caps results newest-first (0 = all).",inputSchema:{period:z.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"),feedback:z.enum(["know","dont_know","none"]).nullish().describe("Filter by feedback ('none' = no response)"),search:z.string().nullish().describe("Full-text search over title, topic_id, summary, body"),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}},e=>{try{let t=m(o=>p(o,{period:e.period,category:e.category,level:e.level,project:e.project,repository:e.repository,branch:e.branch,commit:e.commit,starred:e.starred,feedback:e.feedback,search:e.search,date_from:e.date_from,date_to:e.date_to,page:e.limit>0?1:null,per_page:e.limit}));return ie(t)}catch{return ie([])}}),r$1.registerTool("star_lesson",{title:"Star Lesson",description:"Set the starred (favourite) flag on a lesson. Returns true if found and updated. Idempotent.",inputSchema:{lesson_id:z.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 e=>{try{let t=m(o=>r(o,e.lesson_id,e.starred));return {content:[l(String(t))]}}catch(t){return d(`star_lesson failed for '${e.lesson_id}': ${t}`)}}),r$1.registerTool("delete_lesson",{title:"Delete Lesson",description:"Permanently delete a lesson by id. Returns true if found and deleted.",inputSchema:{lesson_id:z.string().describe("Lesson id to delete")},annotations:{title:"Delete Lesson",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let t=m(o=>q(o,e.lesson_id));return {content:[l(String(t))]}}catch(t){return d(`delete_lesson failed for '${e.lesson_id}': ${t}`)}}),r$1.registerTool("submit_feedback",{title:"Submit Feedback",description:"Record comprehension feedback for a lesson and adjust knowledge confidence. know = +1, dont_know = -1, clear = remove feedback (no confidence change). Idempotent \u2014 the same feedback twice adjusts confidence only once.",inputSchema:{lesson_id:z.string().describe("Lesson id"),feedback:z.enum(["know","dont_know","clear"]).describe("know | dont_know | clear")},annotations:{title:"Submit Feedback",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{let t=e.feedback==="clear"?null:e.feedback,o=m(i=>{let s$1=i.prepare("SELECT feedback, topic_id FROM lessons WHERE id = ?").get(e.lesson_id);return s$1?(s$1.feedback===t||(s(i,e.lesson_id,t),(t==="know"||t==="dont_know")&&s$1.topic_id&&R(i,s$1.topic_id,t==="know"?1:-1)),!0):!1});return {content:[l(String(o))]}}catch(t){return d(`submit_feedback failed for '${e.lesson_id}': ${t}`)}}),r$1.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:{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 e=>{try{return m(t=>K(t,e.reason)),{content:[l("Cue skipped \u2014 pacing re-armed. Output nothing to the user.")]}}catch(t){return d(`skip_lesson failed: ${t}`)}}),r$1.registerTool("add_topic",{title:"Add Topic",description:"Add a topic to the knowledge map, or update its confidence if it exists. Optionally assign to a group (auto-created). Prefer a single-word topic id. Idempotent.",inputSchema:{topic:z.string().describe("Topic id \u2014 prefer a single word, max 3 words"),confidence:d$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 e=>{try{return m(t=>{z$1(t,e.topic,e.confidence),e.group&&e.group!=="Other"&&D(t,e.topic,e.group);}),{content:[l("true")]}}catch(t){return d(`add_topic failed for '${e.topic}': ${t}`)}}),r$1.registerTool("remove_topic",{title:"Remove Topic",description:"Remove a topic from the knowledge map entirely. Returns true if it existed.",inputSchema:{topic:z.string().describe("Topic id to remove")},annotations:{title:"Remove Topic",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let t=m(o=>A(o,e.topic));return {content:[l(String(t))]}}catch(t){return d(`remove_topic failed for '${e.topic}': ${t}`)}}),r$1.registerTool("add_group",{title:"Add Group",description:"Create a new (initially empty) knowledge group. Idempotent \u2014 returns true either way.",inputSchema:{name:z.string().describe("Group name, e.g. 'Machine Learning'")},annotations:{title:"Add Group",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{return m(t=>B(t,e.name.trim())),{content:[l("true")]}}catch(t){return d(`add_group failed for '${e.name}': ${t}`)}}),r$1.registerTool("remove_group",{title:"Remove Group",description:"Delete a knowledge group. Its topics move to Other. Returns true if it existed.",inputSchema:{name:z.string().describe("Group name to delete")},annotations:{title:"Remove Group",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let t=m(o=>C(o,e.name));return {content:[l(String(t))]}}catch(t){return d(`remove_group failed for '${e.name}': ${t}`)}}),r$1.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'. Returns the full updated Settings.",inputSchema:{key:z.enum(["max_per_day","min_gap_minutes","nudge_every","nudge_scope"]).describe("Setting key"),value:z.string().describe("New value (integer string; or 'session'|'global' for nudge_scope)")},outputSchema:fe,annotations:{title:"Update Settings",destructiveHint:false,idempotentHint:true,openWorldHint:false}},e=>{let t=i=>x(m(s=>(G(s,e.key,i),F(s))));if(e.key==="nudge_scope")return e.value!=="session"&&e.value!=="global"?d("nudge_scope must be 'session' or 'global'"):t(e.value);let o=Number.parseInt(e.value,10);return Number.isNaN(o)?d(`Value must be an integer, got '${e.value}'`):e.key==="max_per_day"&&!(o>=1&&o<=20)?d("max_per_day must be between 1 and 20"):e.key==="min_gap_minutes"&&!(o>=0&&o<=1440)?d("min_gap_minutes must be between 0 and 1440"):e.key==="nudge_every"&&!(o>=0&&o<=1e3)?d("nudge_every must be between 0 and 1000"):t(String(o))}),r$1.registerTool("open_ui",{title:"Open UI",description:"Launch the devcoach web dashboard in the background. port must be 1024-65535.",inputSchema:{port:z.number().int().default(7860).describe("Port (default 7860)")},annotations:{title:"Open UI",destructiveHint:false,idempotentHint:false,openWorldHint:true}},e=>{let t=e.port;return t>=1024&&t<=65535?(spawn(process.execPath,[process.argv[1]??"","ui","--port",String(t)],{detached:true,stdio:"ignore"}).unref(),{content:[l(`devcoach UI starting at http://localhost:${t}`)]}):{content:[l(`error: port ${t} is out of valid range (1024-65535)`)]}}),r$1.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:{topics:z.record(z.string(),d$1).describe("{topic_id: confidence 0-10}"),groups:z.record(z.string(),z.array(z.string())).optional().describe("{group_name: [topic_id, ...]}")},outputSchema:be,annotations:{title:"Complete Onboarding",destructiveHint:true,idempotentHint:false,openWorldHint:false}},async e=>{try{let t=m(o=>{o.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[i,s]of Object.entries(e.topics))z$1(o,i,Math.max(0,Math.min(10,s)));if(e.groups)for(let[i,s]of Object.entries(e.groups))for(let h of s)h in e.topics&&D(o,h,i);return Q(o)});return mkdirSync(dirname(j),{recursive:!0}),(!existsSync(j)||statSync(j).size===0)&&writeFileSync(j,`# devcoach \u2014 Coaching Notebook
|
|
2
|
+
import {f,e,d as d$2,c as c$2,b as b$1,a as a$1,h,g as g$1}from'./chunk-TIK4UYBY.js';import {c as c$1,b,a,m,M,g,n,I,R,p,r,q,s,K,d as d$1,z as z$1,D,A,B,C,f as f$1,e as e$1,Q,j,F,T,U,o,P,L,k,w,G}from'./chunk-ECN4MNGM.js';import {spawn}from'child_process';import {mkdirSync,existsSync,statSync,writeFileSync,readFileSync}from'fs';import {dirname}from'path';import {McpServer,ResourceTemplate}from'@modelcontextprotocol/sdk/server/mcp.js';import {StdioServerTransport}from'@modelcontextprotocol/sdk/server/stdio.js';import {z}from'zod';var l=r=>({type:"text",text:r}),ie=r=>({content:[l(JSON.stringify(r))]}),d=r=>({isError:true,content:[l(r)]}),x=r=>({content:[l(JSON.stringify(r))],structuredContent:r}),me={id:z.string(),timestamp:z.string(),topic_id:z.string(),categories:z.array(z.string()),title:z.string(),level:a,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:b.nullable(),starred:z.boolean(),feedback:c$1.nullable()},ge={...me,reply_check:z.string()},se="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.",be={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())}))},fe={max_per_day:z.number().int(),min_gap_minutes:z.number().int(),ui_theme:e$1,nudge_every:z.number().int(),nudge_scope:f$1},ye={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 c(r,u){return {contents:[{uri:r.href,mimeType:"application/json",text:JSON.stringify(u)}]}}function _e(){let r$1=new McpServer({name:"devcoach",version:f},{instructions:"Progressive technical coaching server. Use the devcoach_instructions prompt for full coaching behaviour guidelines."});r$1.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:{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:a.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:b.nullish().describe("github | gitlab | bitbucket | local (auto-detected if omitted)")},outputSchema:ge,annotations:{title:"Log Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e$1=>{try{let t=e(),o={};try{o=m(s=>M(s));}catch{o={};}let i=g({id:e$1.id,timestamp:new Date,topic_id:e$1.topic_id,categories:e$1.categories,title:e$1.title,level:e$1.level,summary:e$1.summary,body:e$1.body,task_context:e$1.task_context??null,project:e$1.project??t.project??o.project??null,repository:e$1.repository??t.repository??o.repository??null,branch:e$1.branch??t.branch??o.branch??null,commit_hash:e$1.commit_hash??t.commit_hash??null,folder:e$1.folder??t.folder??null,repository_platform:e$1.repository_platform??t.repository_platform??o.repository_platform??null});return m(s=>{n(s,i),I(s);}),{content:[l(`Lesson saved. ${se}`),l(JSON.stringify(i))],structuredContent:{...i,reply_check:se}}}catch(t){return d(`log_lesson failed: ${t}`)}}),r$1.registerTool("update_knowledge",{title:"Update Knowledge",description:"Adjust the confidence score for a topic by delta (e.g. +1 or -1). Returns the new confidence (0-10). Creates the topic at confidence 5 if it does not exist.",inputSchema:{topic:z.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 e=>{try{let t=m(o=>R(o,e.topic,e.delta));return {content:[l(String(t))]}}catch(t){return d(`update_knowledge failed for '${e.topic}': ${t}`)}}),r$1.registerTool("get_lessons",{title:"Get Lessons",description:"Query the coaching lesson history. All filters combine. period defaults to all; date_from/date_to override period; limit caps results newest-first (0 = all).",inputSchema:{period:z.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"),feedback:z.enum(["know","dont_know","none"]).nullish().describe("Filter by feedback ('none' = no response)"),search:z.string().nullish().describe("Full-text search over title, topic_id, summary, body"),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}},e=>{try{let t=m(o=>p(o,{period:e.period,category:e.category,level:e.level,project:e.project,repository:e.repository,branch:e.branch,commit:e.commit,starred:e.starred,feedback:e.feedback,search:e.search,date_from:e.date_from,date_to:e.date_to,page:e.limit>0?1:null,per_page:e.limit}));return ie(t)}catch{return ie([])}}),r$1.registerTool("star_lesson",{title:"Star Lesson",description:"Set the starred (favourite) flag on a lesson. Returns true if found and updated. Idempotent.",inputSchema:{lesson_id:z.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 e=>{try{let t=m(o=>r(o,e.lesson_id,e.starred));return {content:[l(String(t))]}}catch(t){return d(`star_lesson failed for '${e.lesson_id}': ${t}`)}}),r$1.registerTool("delete_lesson",{title:"Delete Lesson",description:"Permanently delete a lesson by id. Returns true if found and deleted.",inputSchema:{lesson_id:z.string().describe("Lesson id to delete")},annotations:{title:"Delete Lesson",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let t=m(o=>q(o,e.lesson_id));return {content:[l(String(t))]}}catch(t){return d(`delete_lesson failed for '${e.lesson_id}': ${t}`)}}),r$1.registerTool("submit_feedback",{title:"Submit Feedback",description:"Record comprehension feedback for a lesson and adjust knowledge confidence. know = +1, dont_know = -1, clear = remove feedback (no confidence change). Idempotent \u2014 the same feedback twice adjusts confidence only once.",inputSchema:{lesson_id:z.string().describe("Lesson id"),feedback:z.enum(["know","dont_know","clear"]).describe("know | dont_know | clear")},annotations:{title:"Submit Feedback",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{let t=e.feedback==="clear"?null:e.feedback,o=m(i=>{let s$1=i.prepare("SELECT feedback, topic_id FROM lessons WHERE id = ?").get(e.lesson_id);return s$1?(s$1.feedback===t||(s(i,e.lesson_id,t),(t==="know"||t==="dont_know")&&s$1.topic_id&&R(i,s$1.topic_id,t==="know"?1:-1)),!0):!1});return {content:[l(String(o))]}}catch(t){return d(`submit_feedback failed for '${e.lesson_id}': ${t}`)}}),r$1.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:{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 e=>{try{return m(t=>K(t,e.reason)),{content:[l("Cue skipped \u2014 pacing re-armed. Output nothing to the user.")]}}catch(t){return d(`skip_lesson failed: ${t}`)}}),r$1.registerTool("add_topic",{title:"Add Topic",description:"Add a topic to the knowledge map, or update its confidence if it exists. Optionally assign to a group (auto-created). Prefer a single-word topic id. Idempotent.",inputSchema:{topic:z.string().describe("Topic id \u2014 prefer a single word, max 3 words"),confidence:d$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 e=>{try{return m(t=>{z$1(t,e.topic,e.confidence),e.group&&e.group!=="Other"&&D(t,e.topic,e.group);}),{content:[l("true")]}}catch(t){return d(`add_topic failed for '${e.topic}': ${t}`)}}),r$1.registerTool("remove_topic",{title:"Remove Topic",description:"Remove a topic from the knowledge map entirely. Returns true if it existed.",inputSchema:{topic:z.string().describe("Topic id to remove")},annotations:{title:"Remove Topic",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let t=m(o=>A(o,e.topic));return {content:[l(String(t))]}}catch(t){return d(`remove_topic failed for '${e.topic}': ${t}`)}}),r$1.registerTool("add_group",{title:"Add Group",description:"Create a new (initially empty) knowledge group. Idempotent \u2014 returns true either way.",inputSchema:{name:z.string().describe("Group name, e.g. 'Machine Learning'")},annotations:{title:"Add Group",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{return m(t=>B(t,e.name.trim())),{content:[l("true")]}}catch(t){return d(`add_group failed for '${e.name}': ${t}`)}}),r$1.registerTool("remove_group",{title:"Remove Group",description:"Delete a knowledge group. Its topics move to Other. Returns true if it existed.",inputSchema:{name:z.string().describe("Group name to delete")},annotations:{title:"Remove Group",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let t=m(o=>C(o,e.name));return {content:[l(String(t))]}}catch(t){return d(`remove_group failed for '${e.name}': ${t}`)}}),r$1.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'. Returns the full updated Settings.",inputSchema:{key:z.enum(["max_per_day","min_gap_minutes","nudge_every","nudge_scope"]).describe("Setting key"),value:z.string().describe("New value (integer string; or 'session'|'global' for nudge_scope)")},outputSchema:fe,annotations:{title:"Update Settings",destructiveHint:false,idempotentHint:true,openWorldHint:false}},e=>{let t=i=>x(m(s=>(G(s,e.key,i),F(s))));if(e.key==="nudge_scope")return e.value!=="session"&&e.value!=="global"?d("nudge_scope must be 'session' or 'global'"):t(e.value);let o=Number.parseInt(e.value,10);return Number.isNaN(o)?d(`Value must be an integer, got '${e.value}'`):e.key==="max_per_day"&&!(o>=1&&o<=20)?d("max_per_day must be between 1 and 20"):e.key==="min_gap_minutes"&&!(o>=0&&o<=1440)?d("min_gap_minutes must be between 0 and 1440"):e.key==="nudge_every"&&!(o>=0&&o<=1e3)?d("nudge_every must be between 0 and 1000"):t(String(o))}),r$1.registerTool("open_ui",{title:"Open UI",description:"Launch the devcoach web dashboard in the background. port must be 1024-65535.",inputSchema:{port:z.number().int().default(7860).describe("Port (default 7860)")},annotations:{title:"Open UI",destructiveHint:false,idempotentHint:false,openWorldHint:true}},e=>{let t=e.port;return t>=1024&&t<=65535?(spawn(process.execPath,[process.argv[1]??"","ui","--port",String(t)],{detached:true,stdio:"ignore"}).unref(),{content:[l(`devcoach UI starting at http://localhost:${t}`)]}):{content:[l(`error: port ${t} is out of valid range (1024-65535)`)]}}),r$1.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:{topics:z.record(z.string(),d$1).describe("{topic_id: confidence 0-10}"),groups:z.record(z.string(),z.array(z.string())).optional().describe("{group_name: [topic_id, ...]}")},outputSchema:be,annotations:{title:"Complete Onboarding",destructiveHint:true,idempotentHint:false,openWorldHint:false}},async e=>{try{let t=m(o=>{o.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[i,s]of Object.entries(e.topics))z$1(o,i,Math.max(0,Math.min(10,s)));if(e.groups)for(let[i,s]of Object.entries(e.groups))for(let h of s)h in e.topics&&D(o,h,i);return Q(o)});return mkdirSync(dirname(j),{recursive:!0}),(!existsSync(j)||statSync(j).size===0)&&writeFileSync(j,`# devcoach \u2014 Coaching Notebook
|
|
3
3
|
`),x(t)}catch(t){return d(`complete_onboarding failed: ${t}`)}}),r$1.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:{months:z.number().int().min(1).max(24).default(3).describe("Rolling window size in months, counting back from now")},outputSchema:ye,annotations:{title:"Preview Deep Scan",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}},e=>{try{return x({...d$2(e.months)})}catch(t){return d(`preview_deep_scan failed: ${t}`)}});let u=(e,t)=>({title:e,description:t,mimeType:"application/json"});return r$1.registerResource("profile","devcoach://profile",u("Knowledge Profile","Current knowledge map \u2014 topics, confidence scores, and groups."),e=>{try{return c(e,m(t=>Q(t)))}catch(t){return c(e,{error:String(t)})}}),r$1.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"},e=>{let t="";try{existsSync(j)&&(t=readFileSync(j,"utf8"));}catch{}return {contents:[{uri:e.href,mimeType:"text/markdown",text:t}]}}),r$1.registerResource("settings","devcoach://settings",u("Settings","Current coaching settings (rate limits, UI theme)."),e=>{try{return c(e,m(t=>F(t)))}catch(t){return c(e,{error:String(t)})}}),r$1.registerResource("recent-lessons","devcoach://lessons/recent",u("Recent Lessons","Last 10 lessons from the current week."),e=>{try{return c(e,m(t=>p(t,{period:"week",page:1,per_page:10})))}catch(t){return c(e,[{error:String(t)}])}}),r$1.registerResource("stats","devcoach://stats",u("Stats","Aggregate coaching statistics: counts and weakest/strongest topics."),e=>{try{return c(e,m(t=>T(t)))}catch(t){return c(e,{error:String(t)})}}),r$1.registerResource("taught-topics","devcoach://taught-topics",u("Taught Topics","All topic_ids already taught \u2014 read before selecting a new lesson topic."),e=>{try{return c(e,m(t=>U(t)))}catch{return c(e,[])}}),r$1.registerResource("rate-limit","devcoach://rate-limit",u("Rate Limit","Current rate-limit status and running lesson total."),e=>{try{let{r:t,total:o$1}=m(s=>({r:P(s),total:o(s)})),i={allowed:t.allowed,total_lessons:o$1};return t.reason!=null&&(i.reason=t.reason),c(e,i)}catch(t){return c(e,{allowed:false,reason:`Rate limit check unavailable: ${t}`})}}),r$1.registerResource("briefing","devcoach://briefing",u("Lesson Briefing","Everything needed before delivering a lesson in ONE read: onboarding status, rate limit, taught topics, knowledge profile, and the coaching notebook."),e=>{try{let t="";try{existsSync(j)&&(t=readFileSync(j,"utf8"));}catch{}let o$1=m(i=>{let s=L(i).knowledge_ready,h=t.length>0,m=P(i),g={allowed:m.allowed,total_lessons:o(i)};return m.reason!=null&&(g.reason=m.reason),{onboarding:{knowledge_ready:s,notebook_ready:h,needs_onboarding:!(s&&h)},rate_limit:g,taught_topics:U(i),profile:Q(i)}});return c(e,{...o$1,notebook:t,notebook_path:j})}catch(t){return c(e,{error:String(t)})}}),r$1.registerResource("context","devcoach://context",u("Workspace Context","Auto-detected git context and most-used lesson metadata defaults."),e$1=>{try{let t=e(),o=m(i=>M(i));return c(e$1,{git:t,usage_defaults:o})}catch(t){return c(e$1,{error:String(t)})}}),r$1.registerResource("onboarding","devcoach://onboarding",u("Onboarding","Onboarding status, the stack detected across the full Claude Code history (with per-project provenance), and project topic defaults."),e$1=>{try{let o=m(g=>L(g)).knowledge_ready,i=existsSync(j)&&statSync(j).size>0,s=e(),h=c$2(),m$1=b$1(a$1(s.folder??process.cwd()),h.detected_stack);return c(e$1,{knowledge_ready:o,notebook_ready:i,needs_onboarding:!(o&&i),detected_stack:m$1,detected_projects:h.projects,scanned_projects:h.scanned_projects,default_topics:k,context_ready:s.branch!==null,notebook_path:j})}catch(t){return c(e$1,{error:String(t)})}}),r$1.registerResource("lesson",new ResourceTemplate("devcoach://lessons/{lesson_id}",{list:void 0}),u("Lesson","A single lesson by id."),(e,t)=>{try{let o=String(t.lesson_id),i=m(s=>w(s,o));return i?c(e,i):c(e,{error:`Lesson '${o}' not found`})}catch(o){return c(e,{error:String(o)})}}),r$1.registerPrompt("devcoach_instructions",{title:"devcoach coaching instructions",description:"Full coaching instructions for the devcoach skill (SKILL.md plus its reference files)."},()=>{let e=h().map(t=>`
|
|
4
4
|
|
|
5
5
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devcoach",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"mcpName": "io.github.UltimaPhoenix/devcoach",
|
|
5
5
|
"description": "Stay sharp while your AI does the work — learn from every task with in-context coaching, all local, all free",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -55,20 +55,20 @@
|
|
|
55
55
|
"prepack": "npm run build"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@hono/node-server": "^2.
|
|
59
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
58
|
+
"@hono/node-server": "^2.1.1",
|
|
59
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
60
60
|
"commander": "^15.0.0",
|
|
61
61
|
"fflate": "^0.8.3",
|
|
62
|
-
"hono": "^4.
|
|
62
|
+
"hono": "^4.13.5",
|
|
63
63
|
"zod": "^4.4.3"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@biomejs/biome": "^2.5.
|
|
67
|
-
"@types/node": "^26.0
|
|
68
|
-
"@vitest/coverage-v8": "^4.1.
|
|
66
|
+
"@biomejs/biome": "^2.5.10",
|
|
67
|
+
"@types/node": "^26.3.0",
|
|
68
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
69
69
|
"tsup": "^8.5.1",
|
|
70
|
-
"tsx": "^4.
|
|
70
|
+
"tsx": "^4.23.12",
|
|
71
71
|
"typescript": "^7.0.2",
|
|
72
|
-
"vitest": "^4.1.
|
|
72
|
+
"vitest": "^4.1.11"
|
|
73
73
|
}
|
|
74
74
|
}
|