devcoach 0.9.2 → 0.9.3

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.
@@ -31,8 +31,10 @@ For each lesson whose `topic_id` is not in `devcoach://profile`:
31
31
 
32
32
  ## Step 4 — Update the coaching notebook
33
33
 
34
- Read `devcoach://notebook`. Merge findings into the relevant sections and save the
35
- complete revised markdown via `update_notebook`:
34
+ Read `devcoach://briefing` for the current notebook text and its `notebook_path` — one
35
+ resource covers both. Merge findings into the relevant sections and write the complete
36
+ revised markdown directly to `notebook_path` (your own Write/Edit tool), overwriting the
37
+ file in one go:
36
38
 
37
39
  - **Recurring patterns** — append any `topic_id` that appeared 3+ times, with its count
38
40
  and whether feedback was positive, negative, or absent.
@@ -12,30 +12,39 @@ Check `knowledge_ready` and `notebook_ready` from `devcoach://onboarding` indepe
12
12
  — each step can run alone:
13
13
 
14
14
  ```
15
- knowledge_ready = false → run Steps 1–3 (topic collection + complete_onboarding)
16
- notebook_ready = false → run Step 4 (notebook initialisation)
15
+ knowledge_ready = false → run Steps 1–4 (topic collection + save)
16
+ notebook_ready = false → run the notebook-composition part of Step 4 only
17
17
  both ready → proceed normally
18
18
  ```
19
19
 
20
20
  This means a user who restores from backup has their knowledge automatically, so
21
- only Step 4 runs. On-demand re-setup ("redo onboarding", "reset my topics") always
22
- re-runs Steps 1–3 regardless of `knowledge_ready`.
21
+ only the notebook step runs. On-demand re-setup ("redo onboarding", "reset my topics")
22
+ always re-runs Steps 1–4 regardless of `knowledge_ready`.
23
23
 
24
24
  ## Step 1 — Ask how to set up (strongly recommend Automatic; never pick for the user)
25
25
 
26
- Read `devcoach://onboarding` FIRST — its `detected_stack`, `detected_projects`, and
27
- `scanned_projects` come from a scan of the user's **full Claude Code history** (every
28
- project Claude has worked in, ranked by recent activity), not just the current folder.
26
+ Read `devcoach://onboarding` FIRST — its `detected_stack`, `detected_projects`,
27
+ `scanned_projects`, and `notebook_path` come from a scan of the user's **full Claude
28
+ Code history** (every project Claude has worked in, ranked by recent activity), not
29
+ just the current folder. Keep `notebook_path` in scope — Step 4 needs it and there is
30
+ no reason to re-read this resource later in the same flow.
29
31
 
30
32
  Then present a single choice using the client's question UI (e.g. AskUserQuestion in
31
- Claude Code). Offer exactly three options, and make **Automatic** the first option,
32
- **strongly recommended**, with its description citing the real evidence — name the
33
- project count and 2–3 detected topics with where they were seen, e.g. *"I scanned 9
34
- of your Claude Code projects and found TypeScript (dev-coach), Java (discordbot,
35
- over-night-runner), Swift (blueprince) — I can build your profile from that."*:
33
+ Claude Code). Offer exactly four options, in this order:
36
34
 
37
35
  - **Automatic (Strongly recommended)** — build the profile from the detected
38
- history-wide stack; the user only confirms and adjusts confidences.
36
+ history-wide stack; cites the real evidence — name the project count and 2–3
37
+ detected topics with where they were seen, e.g. *"I scanned 9 of your Claude Code
38
+ projects and found TypeScript (dev-coach), Java (discordbot, over-night-runner),
39
+ Swift (blueprince) — I can build your profile from that."* Builds and saves
40
+ everything in one pass — you review the result afterward, not before.
41
+ - **Automatic (Deep)** — like Automatic, but also reads the real conversation content
42
+ from your local Claude Code session history (not just file/activity metadata), for
43
+ the most accurate, personalized profile and notebook it can produce. Be transparent
44
+ about what this means, don't bury it: *"This reads actual conversation text from your
45
+ local sessions, not just file and activity metadata — more accurate, but it means
46
+ sharing more of your own local history with the model, as a one-time setup step on
47
+ this machine. Everything else works the same."*
39
48
  - **Guided** — a step-by-step conversation to map knowledge, confidence levels, and
40
49
  topic groups (thorough, interactive).
41
50
  - **Import backup** — restore knowledge, lessons, and settings from an existing
@@ -43,35 +52,30 @@ over-night-runner), Swift (blueprince) — I can build your profile from that."*
43
52
 
44
53
  When the scan found nothing (`scanned_projects` 0 or an empty `detected_stack`),
45
54
  present Automatic without the evidence sentence — it falls back to the current
46
- project's files. Do **not** default to Automatic silently — surface the choice and
47
- wait for the user's answer before proceeding.
55
+ project's files (Automatic (Deep) stays available regardless — its value doesn't
56
+ depend on `scanClaudeHistory`'s metadata scan). Do **not** default to Automatic
57
+ silently — surface the choice and wait for the user's answer before proceeding.
48
58
 
49
59
  **If Import backup:** ask for the file path and call `restore` (CLI) with it. Restore
50
60
  brings back knowledge entries automatically — no further DB steps needed. Re-read
51
- `devcoach://onboarding` after restore; if `knowledge_ready` is now true, skip to
52
- Step 4. If `notebook_ready` is also true, proceed normally.
53
-
54
- ## Step 2 — Collect topics for the chosen mode
55
-
56
- **Automatic mode:**
57
- - Present a merged topic list: `detected_stack` (history-wide, already merged with
58
- the current project) enriched with relevant entries from `default_topics` (the
59
- project's default knowledge map).
60
- - Show each topic with its suggested confidence **and its provenance** from
61
- `detected_projects`, as one plain-markdown bullet per topic (never a table, never
62
- prose paragraphs — this list is scanned, not read):
63
- ```markdown
64
- - **java** — 6/10 — seen in discordbot, over-night-runner
65
- - **typescript** — 7/10 — seen in dev-coach
66
- ```
67
- Weight what you emphasise by `prompt_count` and `last_activity`: a stack the user
68
- works in daily deserves more topics than a one-off experiment. After the list, ask
69
- the user to confirm, adjust, or remove each: *"Looks right? Or enter 1–10 to
70
- change it."*
71
- - After the list, ask: *"Anything else I missed? List any tools, languages,
72
- frameworks, or practices you work with regularly."* — add each with a confidence.
73
-
74
- **Guided mode:**
61
+ `devcoach://onboarding` after restore; if `knowledge_ready` is now true, skip to the
62
+ notebook-composition part of Step 4. If `notebook_ready` is also true, proceed normally.
63
+
64
+ ## Step 2 — Build the topic list for the chosen mode
65
+
66
+ **Automatic mode:** build the merged topic list yourself, in one pass, no questions
67
+ asked — `detected_stack` (history-wide, already merged with the current project)
68
+ enriched with relevant entries from `default_topics` (the project's default knowledge
69
+ map), weighted by `prompt_count` and `last_activity` from `detected_projects` (a stack
70
+ the user works in daily deserves more topics than a one-off experiment). Do not ask the
71
+ user to confirm, adjust, or list anything else first — that happens after saving, in
72
+ Step 5.
73
+
74
+ **Automatic (Deep) mode:** run the "Automatic (Deep) procedure" below to get an
75
+ evidence-based `topics`/`groups`/`notebook` result, then skip straight to Step 4 (Deep's
76
+ subagent already decides groups and composes the notebook — don't redo Step 3 for it).
77
+
78
+ **Guided mode (unchanged — stays fully interactive):**
75
79
  - Have a free-form conversation: *"Tell me about the technologies you work with
76
80
  day-to-day. For each one I'll ask how confident you are:
77
81
  1–3 = still learning · 4–6 = comfortable · 7–9 = strong · 10 = expert."*
@@ -79,67 +83,93 @@ Step 4. If `notebook_ready` is also true, proceed normally.
79
83
  (languages, frameworks, databases, infrastructure, version control, CI/CD, testing,
80
84
  architecture patterns, etc.). Keep probing until the user says they're done.
81
85
 
82
- ## Step 3 — Propose groups and save
83
-
84
- Once the full topic list is agreed:
85
- - **Suggest logical groups** based on what was collected. Examples:
86
- Languages, Backend, Frontend, Databases, DevOps, Version Control, Testing.
87
- These names emerge from the conversation — there is no fixed list.
86
+ ### Automatic (Deep) procedure
87
+
88
+ 1. Call `preview_deep_scan({months: 3})` — cheap, metadata-only, no prompt text read.
89
+ 2. If `over_soft_limit` is false, go straight to step 4 below — no question asked.
90
+ 3. If `over_soft_limit` is true, surface `candidate_count` and ask (three options):
91
+ *narrow the window* (re-call `preview_deep_scan` with a smaller `months`, loop back
92
+ to step 2); *proceed with all of them anyway* (cap what you hand the subagent at the
93
+ 25 most-recently-active `candidates`, and say so if you truncated); *pick specific
94
+ projects* (show `candidates` — name, last_activity, prompt_count — as a plain bullet
95
+ list and ask which to include by name, since an arbitrary-length list doesn't fit a
96
+ multiple-choice UI).
97
+ 4. Spawn a subagent (the `Agent` tool, a general-purpose subagent) with the confirmed
98
+ project paths. Give it a fully self-contained prompt — it has its own Bash/Read/Glob
99
+ tools and does not inherit devcoach's MCP tools, so instruct it explicitly:
100
+ - For each project path, compute its escaped directory name
101
+ (`path.replace(/[^a-zA-Z0-9]/g, "-")` — the same rule devcoach uses for its own
102
+ memory index) and Glob `~/.claude/projects/<escaped>/*.jsonl` for that project's
103
+ session transcripts.
104
+ - Read a bounded sample per project (at most the 5 most-recent sessions — this is a
105
+ sample for signal, not an exhaustive audit) — each line is JSON with a `type`
106
+ (`user`/`assistant`) and a `message.content` field holding real conversation text.
107
+ Cross-reference `memory/MEMORY.md` in the same project directory if present.
108
+ - This is a deliberate, user-approved, one-time exception to devcoach's normal
109
+ metadata-only rule — make that explicit in the subagent's instructions so it
110
+ doesn't second-guess reading the content. Even so, tell it to favor synthesized
111
+ observations over verbatim quotes in what it writes back, and to keep the raw
112
+ transcript content out of its final answer — only a compact result crosses back to
113
+ the parent conversation, never full transcript dumps.
114
+ - Instruct it to return **only** a single fenced ` ```json ` block, nothing else,
115
+ shaped:
116
+ ```json
117
+ {
118
+ "topics": { "topic_id": 7 },
119
+ "groups": { "Group Name": ["topic_id"] },
120
+ "provenance": { "topic_id": "short evidence phrase, e.g. seen in discordbot + a retry-logic debugging session" },
121
+ "notebook": "# devcoach — Coaching Notebook\n...full markdown per the template below..."
122
+ }
123
+ ```
124
+ 5. Parse the subagent's JSON. If it's missing, malformed, or the subagent failed, fall
125
+ back to plain Automatic mode's topic list (`detected_stack`/`default_topics`) instead
126
+ of blocking onboarding — say nothing about the fallback, just proceed.
127
+
128
+ ## Step 3 — Groups
129
+
130
+ **Automatic / Automatic (Deep):** groups are already decided as part of Step 2's
131
+ one-pass build (or came from the Deep subagent's `groups` field) — no confirmation gate
132
+ here. Pick logical group names the same way Guided does (Languages, Backend, Frontend,
133
+ Databases, DevOps, Version Control, Testing, or whatever fits what was actually found).
134
+
135
+ **Guided mode (unchanged — stays interactive):** once the full topic list is agreed —
136
+ - **Suggest logical groups** based on what was collected. These names emerge from the
137
+ conversation — there is no fixed list.
88
138
  - Show the proposed grouping: *"Here's how I'd organise these — does this look
89
139
  right? Any changes?"*
90
- - When confirmed, call the MCP tool `complete_onboarding`, including the personalized
91
- `notebook` markdown you compose in Step 4 (so the profile and notebook are saved
92
- together):
93
- ```json
94
- {
95
- "topics": { "lang_a": 7, "tool_b": 8, "practice_c": 7 },
96
- "groups": { "Languages": ["lang_a"], "DevOps": ["tool_b"], "Version Control": ["practice_c"] },
97
- "notebook": "# devcoach — Coaching Notebook\n_Last updated: …_\n\n## Observations\n…"
98
- }
99
- ```
100
- - Do not deliver a lesson in this turn — the rate-limit clock starts after onboarding.
101
-
102
- **Rule:** Never ask about groups during topic collection. Propose them only in
103
- Step 3 after all topics are known.
104
-
105
- ### Step 3b — Show the setup summary
106
-
107
- After saving (or after a restore), show the user a concise summary of what was set up:
108
- the topics with their confidences, organised under their groups, as one `###` heading
109
- per group with a plain bullet list underneath (no table, no prose paragraph):
110
- ```markdown
111
- ### Languages
112
- - **python** — 7/10
113
- - **typescript** — 5/10
114
-
115
- ### DevOps
116
- - **docker** — 8/10
117
- ```
118
- Topics with no group go under a trailing `### Other` heading, same bullet shape.
119
-
120
- Then tell the user **how to change any of it later**, across all three surfaces:
121
- - **In chat** — just ask, e.g. *"set my Python confidence to 7"*, *"add Rust at 4"*,
122
- or *"redo onboarding"*. (Backed by the `update_knowledge`, `add_topic`, and
123
- `add_group` tools.)
124
- - **CLI** — `devcoach profile` to view; `devcoach knowledge-add <topic> <0-10>
125
- [--group <Group>]` to add/update; `devcoach knowledge-remove <topic>` to remove;
126
- `devcoach group-add` / `devcoach group-assign` to organise groups.
127
- - **UI** — `devcoach ui` (web dashboard at http://localhost:7860), or ask me to open
128
- it (the `open_ui` tool).
129
-
130
- ## Step 4 — Compose the coaching notebook (pass it to `complete_onboarding`)
131
-
132
- Do **not** write `learning-state.md` yourself — compose its full markdown and pass it
133
- as the `notebook` field of the `complete_onboarding` call in Step 3. The tool saves it
134
- to `~/.devcoach/learning-state.md` atomically with the profile (so it is never empty
135
- and never created before the user finishes).
136
140
 
137
- Make it **personalized**: draw on everything you know about *this* user — this
138
- onboarding conversation, plus the real cross-project data in `detected_projects`
139
- (per-project stacks, activity volume and recency, and each project's auto-`memory`
140
- excerpt — distilled facts about how the user actually works). Cite project names and
141
- observed habits; never quote prompt text (it is not in the data, by design).
142
- Generic boilerplate is a failure; write specific, real notes. Use this structure:
141
+ **Rule (all modes):** never ask about groups during topic collection in Guided mode —
142
+ propose them only after all topics are known.
143
+
144
+ ## Step 4 — Save everything, then write the notebook directly
145
+
146
+ For **all three modes**, once topics and groups are decided (immediately for both
147
+ Automatic tiers; after the user confirms in Guided mode):
148
+
149
+ 1. Call the MCP tool `complete_onboarding` with `topics` and `groups` only — there is no
150
+ `notebook` argument. It guarantees `learning-state.md` exists and is non-empty (a
151
+ placeholder if needed) the instant it saves the profile.
152
+ ```json
153
+ { "topics": { "lang_a": 7, "tool_b": 8 }, "groups": { "Languages": ["lang_a"], "DevOps": ["tool_b"] } }
154
+ ```
155
+ 2. **Immediately after**, write the real personalized notebook markdown directly to
156
+ `notebook_path` (from Step 1's `devcoach://onboarding` read) using your own file
157
+ tools — overwrite the placeholder right away, don't leave it standing. If
158
+ `notebook_path` already has real content (re-onboarding, or restoring), read it first
159
+ and fold prior notes in rather than discarding them.
160
+ 3. Do not deliver a lesson in this turn — the rate-limit clock starts after onboarding.
161
+
162
+ ### Notebook composition
163
+
164
+ Make it **personalized**: draw on everything you know about *this* user — the
165
+ onboarding conversation (Guided) or the real cross-project data in `detected_projects`
166
+ (Automatic) or the subagent's synthesis (Automatic (Deep)) — per-project stacks,
167
+ activity volume and recency, and each project's auto-`memory` excerpt (distilled facts
168
+ about how the user actually works). Cite project names and observed habits. Generic
169
+ boilerplate is a failure; write specific, real notes. In Automatic/Guided modes, never
170
+ quote prompt text (it is not in the data, by design) — Automatic (Deep) is the one
171
+ exception, and even there prefer synthesized observations over verbatim quotes. Use
172
+ this structure:
143
173
 
144
174
  ```markdown
145
175
  # devcoach — Coaching Notebook
@@ -159,5 +189,29 @@ projects, confidence style, gaps. Personalized — never generic.]
159
189
  [Things to watch for and confirm in future sessions.]
160
190
  ```
161
191
 
162
- If re-onboarding or returning, fold prior notes into the markdown you pass —
163
- `complete_onboarding` overwrites the file with exactly what you provide.
192
+ ## Step 5 — Show the setup summary
193
+
194
+ After saving (or after a restore), show the user a concise summary of what was set up
195
+ — for the Automatic tiers, this is the FIRST time they see it, so this is also their
196
+ chance to ask for changes, not a formality: the topics with their confidences,
197
+ organised under their groups, as one `###` heading per group with a plain bullet list
198
+ underneath (no table, no prose paragraph):
199
+ ```markdown
200
+ ### Languages
201
+ - **python** — 7/10
202
+ - **typescript** — 5/10
203
+
204
+ ### DevOps
205
+ - **docker** — 8/10
206
+ ```
207
+ Topics with no group go under a trailing `### Other` heading, same bullet shape.
208
+
209
+ Then tell the user **how to change any of it later**, across all three surfaces:
210
+ - **In chat** — just ask, e.g. *"set my Python confidence to 7"*, *"add Rust at 4"*,
211
+ or *"redo onboarding"*. (Backed by the `update_knowledge`, `add_topic`, and
212
+ `add_group` tools.)
213
+ - **CLI** — `devcoach profile` to view; `devcoach knowledge-add <topic> <0-10>
214
+ [--group <Group>]` to add/update; `devcoach knowledge-remove <topic>` to remove;
215
+ `devcoach group-add` / `devcoach group-assign` to organise groups.
216
+ - **UI** — `devcoach ui` (web dashboard at http://localhost:7860), or ask me to open
217
+ it (the `open_ui` tool).
@@ -12,21 +12,21 @@ Route by intent:
12
12
  review my profile / notebook → Review (below)
13
13
  rebuild / refresh my notebook → Rebuild notebook (below)
14
14
  new tech to track? → New tech check (below)
15
- redo onboarding / reset topics → references/onboarding.md, Steps 1–3 (confirm first — it wipes)
15
+ redo onboarding / reset topics → references/onboarding.md, Steps 1–4 (confirm first — it wipes)
16
16
  ```
17
17
 
18
18
  ## Review
19
19
 
20
20
  1. Read `devcoach://briefing` — one silent read returns the profile, the taught
21
- topics, and the notebook.
21
+ topics, the notebook, and its `notebook_path`.
22
22
  2. Walk the user through the profile **grouped, not topic-by-topic**: show each
23
23
  group with its topics and confidences, and ask what looks wrong. Apply
24
24
  changes as they come: `update_knowledge` (confidence), `add_topic` /
25
25
  `remove_topic` (membership).
26
26
  3. Then show the notebook section by section (Observations, Recurring patterns,
27
27
  Recommended focus, Open hypotheses). Ask what is stale or wrong. Fold the
28
- edits into a revised full markdown and save it with ONE `update_notebook`
29
- call at the end — never one call per edit.
28
+ edits into a revised full markdown and save it with ONE direct Write to
29
+ `notebook_path` (from step 1) at the end — never one write per edit.
30
30
  4. Close with the New tech check below.
31
31
 
32
32
  ## Rebuild notebook
@@ -35,15 +35,15 @@ Re-derive the notebook from real data; the knowledge map is untouched.
35
35
 
36
36
  1. Read `devcoach://onboarding` — `detected_projects` carries the history-wide
37
37
  evidence: per-project stacks, activity volume and recency, and auto-memory
38
- excerpts. Read `devcoach://briefing` for the current notebook and profile,
39
- and `get_lessons({period: "all"})` for feedback history (`dont_know` lessons
40
- are open gaps).
41
- 2. Compose a fresh notebook (structure in `references/onboarding.md`, Step 4):
42
- cite real cross-project observations — project names, stacks, what recurs,
43
- what the memories reveal about how the user works. Preserve prior notes that
44
- are still true; drop the stale ones. Never quote prompt text — it is not in
45
- the data, by design.
46
- 3. Show the draft, adjust to taste, save with `update_notebook`.
38
+ excerpts (and its `notebook_path`). Read `devcoach://briefing` for the
39
+ current notebook and profile, and `get_lessons({period: "all"})` for
40
+ feedback history (`dont_know` lessons are open gaps).
41
+ 2. Compose a fresh notebook (structure in `references/onboarding.md`'s notebook
42
+ composition section): cite real cross-project observations — project
43
+ names, stacks, what recurs, what the memories reveal about how the user
44
+ works. Preserve prior notes that are still true; drop the stale ones. Never
45
+ quote prompt text — it is not in the data, by design.
46
+ 3. Show the draft, adjust to taste, save with a direct Write to `notebook_path`.
47
47
 
48
48
  ## New tech check (keep the profile curated)
49
49
 
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-PBBMDC5B.js');o(t);}else {let{runCli:o}=await import('./commands-AJ6T6MVV.js');await o();}}catch(o){console.error(o),process.exit(1);}
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-HVBFXBK6.js');o(t);}else {let{runCli:o}=await import('./commands-NY3UYI54.js');await o();}}catch(o){console.error(o),process.exit(1);}
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {i,m as m$1,L,W,h as h$1,l,V as V$1}from'./chunk-ECN4MNGM.js';import {existsSync,mkdirSync,statSync,writeFileSync,appendFileSync,fstatSync,readFileSync}from'fs';import {join}from'path';function D(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 s(){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:D(readFileSync(0,"utf8"))}catch{return e}}function N(e){return e.stop_hook_active||e.permission_mode==="plan"}function r(e,o,t){if(process.env.DEVCOACH_HOOK_DEBUG==="1")try{mkdirSync(h$1,{recursive:!0});let n=join(h$1,"hook.log");existsSync(n)&&statSync(n).size>256*1024&&writeFileSync(n,""),appendFileSync(n,`${new Date().toISOString()} ${e} ${(o??"-").slice(0,8)} ${t}
3
- `);}catch{}}var T={claude:"block",gemini:"deny",codex:"block"};function h(e,o,t){let n={decision:T[e],reason:o};t&&(n.systemMessage=t),process.stdout.write(`${JSON.stringify(n)}
4
- `),process.exit(0);}var m="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 (read devcoach://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 A(e=s()){if(N(e)&&process.exit(0),existsSync(i)){let o;try{o=m$1(t=>L(t).knowledge_ready);}catch(t){r("onboard-hook",e.session_id,`error: ${t}`),process.exit(0);}o&&process.exit(0);}r("onboard-hook",e.session_id,"onboarding cue"),h("claude",m);}var c=10,v={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 E(e,o="claude"){let t=e%c===0,n=Math.ceil(e/c)*c,i=t?`This is lesson #${e} \u2014 a notebook checkpoint (every ${c} lessons): after log_lesson returns the feedback, also call update_notebook with the revised notebook markdown and record "(updated after ${e} lessons)".`:`Do NOT call update_notebook this time \u2014 the notebook is refreshed only every ${c} 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.
3
+ `);}catch{}}var A={claude:"block",gemini:"deny",codex:"block"};function h(e,o,t){let n={decision:A[e],reason:o};t&&(n.systemMessage=t),process.stdout.write(`${JSON.stringify(n)}
4
+ `),process.exit(0);}var b="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 (read devcoach://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 E(e=s()){if(N(e)&&process.exit(0),existsSync(i)){let o;try{o=m$1(t=>L(t).knowledge_ready);}catch(t){r("onboard-hook",e.session_id,`error: ${t}`),process.exit(0);}o&&process.exit(0);}r("onboard-hook",e.session_id,"onboarding cue"),h("claude",b);}var c=10,v={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%c===0,n=Math.ceil(e/c)*c,i=t?`This is lesson #${e} \u2014 a notebook checkpoint (every ${c} 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 ${c} 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
 
6
6
  ${v[o].cue} NOW and follow it to deliver ONE lesson. Three hard rules:
7
7
  - The lesson card MUST be printed as your visible reply BEFORE calling log_lesson.
@@ -11,8 +11,8 @@ ${i}
11
11
 
12
12
  If the devcoach skill is not available, fall back to: read devcoach://briefing (one read: profile, taught topics, notebook); pick ONE untaught profile topic at or above the user's confidence band; write the card as plain markdown (no '>' blockquote) between two band headings \`### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u{1F393} devcoach \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\` and \`### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 [topic] \xB7 [level] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\`, with \`**[Title]** \xB7 [Category] \xB7 [Level]\`, 3\u20136 short paragraphs tied to the task, and a \`\u{1F4A1} *Senior tip:*\` line; THEN call log_lesson (body = clean markdown without bands or the title line).
13
13
 
14
- The rate-limit check is already done by this hook \u2014 skip it. Work in silence: never narrate devcoach tool calls or resource reads ('checking your profile\u2026'). Output only the lesson card (or nothing). No preamble, no meta-commentary.`}function $(e,o,t){let n=o.permission_mode==="plan";if(o.stop_hook_active)return {kind:"silent",note:"hook-forced continuation"};if(!L(e).knowledge_ready)return !t.onboardCue||n?{kind:"silent",note:"onboarding not complete"}:{kind:"onboard",note:"onboarding cue (empty knowledge)"};let i=V$1(e,o.session_id,{planMode:n});return i.cue?{kind:"cue",nextLessonNumber:i.nextLessonNumber,note:i.reason}:{kind:"silent",note:i.reason}}function y(e,o,t){let n;try{n=m$1(i=>l(i,()=>$(i,o,t)));}catch(i){r(e,o.session_id,`error: ${i}`),process.exit(0);}switch(r(e,o.session_id,n.note),n.kind){case "onboard":h(t.client,m);break;case "cue":h(t.client,E(n.nextLessonNumber,t.client),"\u{1F393} devcoach: preparing a lesson\u2026");break;}process.exit(0);}function I(e=s()){existsSync(i)||process.exit(0),y("lesson-ready",e,{onboardCue:false,client:"claude"});}function k(e=s(),o="claude"){let t=o==="claude"?"stop-hook":`${o}-stop-hook`;existsSync(i)||((e.stop_hook_active||e.permission_mode==="plan")&&process.exit(0),r(t,e.session_id,"onboarding cue (no DB)"),h(o,m)),y(t,e,{onboardCue:true,client:o});}function R(e="claude"){return `devcoach: the lesson pacing threshold will be reached at the end of this turn. Resolve it before finishing:
14
+ The rate-limit check is already done by this hook \u2014 skip it. Work in silence: never narrate devcoach tool calls or resource reads ('checking your profile\u2026'). Output only the lesson card (or nothing). No preamble, no meta-commentary.`}function $(e,o,t){let n=o.permission_mode==="plan";if(o.stop_hook_active)return {kind:"silent",note:"hook-forced continuation"};if(!L(e).knowledge_ready)return !t.onboardCue||n?{kind:"silent",note:"onboarding not complete"}:{kind:"onboard",note:"onboarding cue (empty knowledge)"};let i=V$1(e,o.session_id,{planMode:n});return i.cue?{kind:"cue",nextLessonNumber:i.nextLessonNumber,note:i.reason}:{kind:"silent",note:i.reason}}function y(e,o,t){let n;try{n=m$1(i=>l(i,()=>$(i,o,t)));}catch(i){r(e,o.session_id,`error: ${i}`),process.exit(0);}switch(r(e,o.session_id,n.note),n.kind){case "onboard":h(t.client,b);break;case "cue":h(t.client,T(n.nextLessonNumber,t.client),"\u{1F393} devcoach: preparing a lesson\u2026");break;}process.exit(0);}function I(e=s()){existsSync(i)||process.exit(0),y("lesson-ready",e,{onboardCue:false,client:"claude"});}function k(e=s(),o="claude"){let t=o==="claude"?"stop-hook":`${o}-stop-hook`;existsSync(i)||((e.stop_hook_active||e.permission_mode==="plan")&&process.exit(0),r(t,e.session_id,"onboarding cue (no DB)"),h(o,b)),y(t,e,{onboardCue:true,client:o});}function R(e="claude"){return `devcoach: the lesson pacing threshold will be reached at the end of this turn. Resolve it before finishing:
15
15
  - If this task is technical: ${v[e].prime}, then end your reply with ONE lesson card (the \`### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u{1F393} devcoach \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\` block) followed by a log_lesson call. The card must be the LAST visible text \u2014 after log_lesson returns, output nothing else.
16
16
  - If it is not technical: silently call the devcoach \`skip_lesson\` tool with a one-line reason and output nothing about it.
17
- - Either way, work in silence: never narrate devcoach tool calls or resource reads ('checking your profile\u2026').`}var B={claude:"UserPromptSubmit",gemini:"BeforeAgent",codex:"UserPromptSubmit"};function b(e=s(),o="claude"){let t=o==="claude"?"prompt-hook":`${o}-prompt-hook`;e.permission_mode==="plan"&&process.exit(0),existsSync(i)||process.exit(0);let n;try{n=m$1(i=>W(i,e.session_id).wouldCue);}catch(i){r(t,e.session_id,`error: ${i}`),process.exit(0);}r(t,e.session_id,n?"priming":"no prime"),n||process.exit(0),process.stdout.write(`${JSON.stringify({hookSpecificOutput:{hookEventName:B[o],additionalContext:R(o)}})}
18
- `),process.exit(0);}function V(e){switch(e){case "stop-hook":k();break;case "gemini-stop-hook":k(s(),"gemini");break;case "codex-stop-hook":k(s(),"codex");break;case "prompt-hook":b();break;case "gemini-prompt-hook":b(s(),"gemini");break;case "codex-prompt-hook":b(s(),"codex");break;case "onboard-hook":A();break;case "lesson-ready":I();break;default:process.exit(0);}}export{D as a,A as b,E as c,I as d,k as e,R as f,b as g,V as h};
17
+ - Either way, work in silence: never narrate devcoach tool calls or resource reads ('checking your profile\u2026').`}var B={claude:"UserPromptSubmit",gemini:"BeforeAgent",codex:"UserPromptSubmit"};function m(e=s(),o="claude"){let t=o==="claude"?"prompt-hook":`${o}-prompt-hook`;e.permission_mode==="plan"&&process.exit(0),existsSync(i)||process.exit(0);let n;try{n=m$1(i=>W(i,e.session_id).wouldCue);}catch(i){r(t,e.session_id,`error: ${i}`),process.exit(0);}r(t,e.session_id,n?"priming":"no prime"),n||process.exit(0),process.stdout.write(`${JSON.stringify({hookSpecificOutput:{hookEventName:B[o],additionalContext:R(o)}})}
18
+ `),process.exit(0);}function V(e){switch(e){case "stop-hook":k();break;case "gemini-stop-hook":k(s(),"gemini");break;case "codex-stop-hook":k(s(),"codex");break;case "prompt-hook":m();break;case "gemini-prompt-hook":m(s(),"gemini");break;case "codex-prompt-hook":m(s(),"codex");break;case "onboard-hook":E();break;case "lesson-ready":I();break;default:process.exit(0);}}export{D as a,E as b,T as c,I as d,k as e,R as f,m as g,V as h};
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
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.9.3"};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,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import {e as e$1,g,b,d}from'./chunk-C42T6YUU.js';export{c as buildLessonCue,d as cmdLessonReady,b as cmdOnboardHook,g as cmdPromptHook,e as cmdStopHook,a as parseHookPayload}from'./chunk-C42T6YUU.js';import {e,d as d$1,c,b as b$1,a as a$1,f,g as g$1}from'./chunk-M5FUYHRM.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(`
2
+ import {e,g,b,d}from'./chunk-4HRCOGG7.js';export{c as buildLessonCue,d as cmdLessonReady,b as cmdOnboardHook,g as cmdPromptHook,e as cmdStopHook,a as parseHookPayload}from'./chunk-4HRCOGG7.js';import {f,e as e$1,c,b as b$1,a as a$1,g as g$1,h}from'./chunk-X5LCD7AM.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
- `),`${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$1){if(!existsSync(join(e$1,"SKILL.md")))return "missing";try{if(readFileSync(join(e$1,".devcoach-version"),"utf8").trim()===e)return "current"}catch{}return "outdated"}function oe(e$1,o){if(!o&&E(e$1)==="current")return `${n.yellow("Already installed")} in ${e$1} (current version)`;mkdirSync(e$1,{recursive:true}),writeFileSync(join(e$1,"SKILL.md"),f());let t=g$1();if(t.length){let r=join(e$1,"references");mkdirSync(r,{recursive:true});for(let i of t)writeFileSync(join(r,i.name),i.content);}return writeFileSync(join(e$1,".devcoach-version"),`${e}
6
- `),`${n.green("\u2713")} Installed into ${e$1}`}function vo(e){return ce()?`${n.yellow("Skipped")} \u2014 bundled with the devcoach Gemini extension`:oe(D,e)}function Q(e,o){let t=S(e);return t.ok&&(t.data.hooks?.[o]??[]).some(r=>(r.hooks??[]).some(i=>de(i.command??"")))}function ue(){let e=[{name:"Claude Code",dir:te,cmd:"devcoach install",hooked:()=>Q(O,"Stop")},{name:"Gemini/Codex",dir:D,cmd:"devcoach install --gemini (or --codex)",hooked:()=>Q(T,"AfterAgent")||Q(H,"Stop")}];for(let o of e){let t=E(o.dir);if(t==="outdated")return `${n.yellow("\u2192")} The devcoach ${o.name} skill is out of date \u2014 run ${n.bold(o.cmd)} to refresh it.`;if(t==="missing"&&o.hooked())return `${n.yellow("\u2192")} The devcoach ${o.name} skill is not installed \u2014 run ${n.bold(o.cmd)} to add it.`}return ""}function Xe(e){let o=e.claudeCode||e.claudeDesktop||e.gemini||e.codex,t=e.claudeCode||!o,r=e.claudeDesktop||!o,i=Ve(),s=false;if(u(n.bold("Setting up devcoach")+n.dim(` (${i.command} ${i.args.join(" ")})`)),u(),t){u(n.bold("Claude Code"));let c=ho("user",e.force);if(!c){let p=join(homedir(),".claude.json");c=q(p,{type:"stdio",env:{},...i},e.force),s=true;}u(` MCP server\u2026 ${c}`),e.skipHook||u(` Hooks\u2026 ${wo()}`),u(` Skill\u2026 ${oe(te,e.force)}`),u();}if(r&&(u(n.bold("Claude Desktop")),u(` MCP server\u2026 ${q(go(),i,e.force)}`),s=true,u()),e.gemini){u(n.bold("Gemini CLI (beta)"));let c=Ke("gemini",["mcp","add","-s","user","devcoach",i.command,...i.args],e.force?["mcp","remove","-s","user","devcoach"]:null);c||(c=q(T,i,e.force)),u(` MCP server\u2026 ${c}`),e.skipHook||u(` Hooks\u2026 ${yo()}`),u(` Skill\u2026 ${vo(e.force)}`),u();}if(e.codex){u(n.bold("Codex CLI (beta)"));let c=Ke("codex",["mcp","add","devcoach","--",i.command,...i.args],e.force?["mcp","remove","devcoach"]:null);c||(c=`${n.yellow("Manual step")} \u2014 codex CLI not found. Add to ${ee}:
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}
6
+ `),`${n.green("\u2713")} Installed into ${e}`}function vo(e){return ce()?`${n.yellow("Skipped")} \u2014 bundled with the devcoach Gemini extension`:oe(D,e)}function Q(e,o){let t=S(e);return t.ok&&(t.data.hooks?.[o]??[]).some(r=>(r.hooks??[]).some(i=>de(i.command??"")))}function ue(){let e=[{name:"Claude Code",dir:te,cmd:"devcoach install",hooked:()=>Q(O,"Stop")},{name:"Gemini/Codex",dir:D,cmd:"devcoach install --gemini (or --codex)",hooked:()=>Q(T,"AfterAgent")||Q(H,"Stop")}];for(let o of e){let t=E(o.dir);if(t==="outdated")return `${n.yellow("\u2192")} The devcoach ${o.name} skill is out of date \u2014 run ${n.bold(o.cmd)} to refresh it.`;if(t==="missing"&&o.hooked())return `${n.yellow("\u2192")} The devcoach ${o.name} skill is not installed \u2014 run ${n.bold(o.cmd)} to add it.`}return ""}function Xe(e){let o=e.claudeCode||e.claudeDesktop||e.gemini||e.codex,t=e.claudeCode||!o,r=e.claudeDesktop||!o,i=Ve(),s=false;if(u(n.bold("Setting up devcoach")+n.dim(` (${i.command} ${i.args.join(" ")})`)),u(),t){u(n.bold("Claude Code"));let c=ho("user",e.force);if(!c){let p=join(homedir(),".claude.json");c=q(p,{type:"stdio",env:{},...i},e.force),s=true;}u(` MCP server\u2026 ${c}`),e.skipHook||u(` Hooks\u2026 ${wo()}`),u(` Skill\u2026 ${oe(te,e.force)}`),u();}if(r&&(u(n.bold("Claude Desktop")),u(` MCP server\u2026 ${q(go(),i,e.force)}`),s=true,u()),e.gemini){u(n.bold("Gemini CLI (beta)"));let c=Ke("gemini",["mcp","add","-s","user","devcoach",i.command,...i.args],e.force?["mcp","remove","-s","user","devcoach"]:null);c||(c=q(T,i,e.force)),u(` MCP server\u2026 ${c}`),e.skipHook||u(` Hooks\u2026 ${yo()}`),u(` Skill\u2026 ${vo(e.force)}`),u();}if(e.codex){u(n.bold("Codex CLI (beta)"));let c=Ke("codex",["mcp","add","devcoach","--",i.command,...i.args],e.force?["mcp","remove","devcoach"]:null);c||(c=`${n.yellow("Manual step")} \u2014 codex CLI not found. Add to ${ee}:
7
7
  [mcp_servers.devcoach]
8
8
  command = "${i.command}"
9
9
  args = [${i.args.map(p=>`"${p}"`).join(", ")}]`),u(` MCP server\u2026 ${c}`),e.skipHook||u(` Hooks\u2026 ${le(H,$o())}`),u(` Skill\u2026 ${oe(D,e.force)}`),u(` ${n.yellow("\u2192")} Codex asks once to trust new hooks \u2014 approve devcoach on the next run.`),u();}s&&u(`${n.yellow("\u2192")} Restart Claude Desktop to pick up the new server.
@@ -11,23 +11,23 @@ import {e as e$1,g,b,d}from'./chunk-C42T6YUU.js';export{c as buildLessonCue,d as
11
11
  run devcoach restore <file> to import a backup on a new machine.
12
12
  After upgrading devcoach (npm, Homebrew), re-run devcoach install to refresh the skill.`));}function me(e){let o=[];for(let[t,r]of Object.entries(e.hooks??{}))for(let i of r??[])for(let s of i.hooks??[])de(s.command??"")&&o.push({event:t,cmd:s});return o}function pe(e,o,t){for(let{cmd:r}of o){let i=r.command.split(" ")[0]??"";i.startsWith("/")&&!existsSync(i)&&e.bad(`hook command not found: ${i} \u2014 re-run ${n.bold(t)}`),r.command.startsWith("npx ")&&e.warn("hook runs via npx \u2014 needs the npx cache (or network) on every stop"),r.timeout==null&&e.warn(`hook entry has no timeout (${r.command})`);}}function Co(e){u(n.bold(`
13
13
  Gemini CLI wiring (beta)`));let o=S(T),t=ce();if(!o.ok){e.bad(`${T} is not valid JSON \u2014 hooks cannot run`);return}let r=me(o.data);t&&r.length?e.bad("devcoach hooks are registered TWICE (extension + settings.json) \u2014 interactions are double-counted. Uninstall the extension or remove the settings.json entries."):t?e.ok("hooks provided by the devcoach Gemini extension"):r.length?(r.some(s=>s.event==="AfterAgent")?e.ok("AfterAgent stop hook wired"):e.warn(`no AfterAgent stop hook \u2014 run ${n.bold("devcoach install --gemini")}`),r.some(s=>s.event==="BeforeAgent")?e.ok("BeforeAgent priming hook wired"):e.warn(`no BeforeAgent priming hook \u2014 run ${n.bold("devcoach install --gemini")} (lessons land more reliably)`),pe(e,r,"devcoach install --gemini")):e.warn(`no devcoach hooks in ${T} \u2014 run ${n.bold("devcoach install --gemini")}`);let i=E(D);t?e.ok("skill bundled with the devcoach Gemini extension"):i==="current"?e.ok("skill installed (~/.agents/skills, current version)"):i==="outdated"?e.warn(`skill is out of date \u2014 run ${n.bold("devcoach install --gemini")}`):e.warn(`skill not installed \u2014 run ${n.bold("devcoach install --gemini")}`),o.data.mcpServers?.devcoach?e.ok("MCP server registered (settings.json)"):e.warn(`MCP server not found in settings.json \u2014 it may be registered elsewhere (check with ${n.bold("gemini mcp list")})`);}function So(e){u(n.bold(`
14
- Codex CLI wiring (beta)`));let o=S(H);if(!o.ok){e.bad(`${H} is not valid JSON \u2014 hooks cannot run`);return}let t=me(o.data);t.length?(t.some(s=>s.event==="Stop")?e.ok("Stop hook wired"):e.warn(`no Stop hook \u2014 run ${n.bold("devcoach install --codex")}`),t.some(s=>s.event==="UserPromptSubmit")?e.ok("UserPromptSubmit priming hook wired"):e.warn(`no UserPromptSubmit priming hook \u2014 run ${n.bold("devcoach install --codex")} (lessons land more reliably)`),pe(e,t,"devcoach install --codex"),e.warn("Codex runs hooks only after you trust them once \u2014 check the prompt on the next run")):e.warn(`no devcoach hooks in ${H} \u2014 run ${n.bold("devcoach install --codex")}`);let r=E(D);r==="current"?e.ok("skill installed (~/.agents/skills, current version)"):r==="outdated"?e.warn(`skill is out of date \u2014 run ${n.bold("devcoach install --codex")}`):e.warn(`skill not installed \u2014 run ${n.bold("devcoach install --codex")}`);let i=existsSync(ee)?readFileSync(ee,"utf8"):"";/\[mcp_servers\.devcoach\]/.test(i)?e.ok("MCP server registered (config.toml)"):e.warn(`MCP server not found in config.toml \u2014 it may be registered elsewhere (check with ${n.bold("codex mcp list")})`);}function Ze(){let e$1={ok:c=>u(` ${n.green("\u2713")} ${c}`),warn:c=>u(` ${n.yellow("\u2192")} ${c}`),bad:c=>u(` ${n.red("\u2717")} ${c}`)},{ok:o,warn:t,bad:r}=e$1;u(`
15
- ${n.bold("devcoach doctor")} ${n.dim(`v${e}`)}
14
+ Codex CLI wiring (beta)`));let o=S(H);if(!o.ok){e.bad(`${H} is not valid JSON \u2014 hooks cannot run`);return}let t=me(o.data);t.length?(t.some(s=>s.event==="Stop")?e.ok("Stop hook wired"):e.warn(`no Stop hook \u2014 run ${n.bold("devcoach install --codex")}`),t.some(s=>s.event==="UserPromptSubmit")?e.ok("UserPromptSubmit priming hook wired"):e.warn(`no UserPromptSubmit priming hook \u2014 run ${n.bold("devcoach install --codex")} (lessons land more reliably)`),pe(e,t,"devcoach install --codex"),e.warn("Codex runs hooks only after you trust them once \u2014 check the prompt on the next run")):e.warn(`no devcoach hooks in ${H} \u2014 run ${n.bold("devcoach install --codex")}`);let r=E(D);r==="current"?e.ok("skill installed (~/.agents/skills, current version)"):r==="outdated"?e.warn(`skill is out of date \u2014 run ${n.bold("devcoach install --codex")}`):e.warn(`skill not installed \u2014 run ${n.bold("devcoach install --codex")}`);let i=existsSync(ee)?readFileSync(ee,"utf8"):"";/\[mcp_servers\.devcoach\]/.test(i)?e.ok("MCP server registered (config.toml)"):e.warn(`MCP server not found in config.toml \u2014 it may be registered elsewhere (check with ${n.bold("codex mcp list")})`);}function Ze(){let e={ok:c=>u(` ${n.green("\u2713")} ${c}`),warn:c=>u(` ${n.yellow("\u2192")} ${c}`),bad:c=>u(` ${n.red("\u2717")} ${c}`)},{ok:o,warn:t,bad:r}=e;u(`
15
+ ${n.bold("devcoach doctor")} ${n.dim(`v${f}`)}
16
16
  `),u(n.bold("Environment")),Number.parseInt(process.versions.node.split(".")[0]??"0",10)>=24?o(`Node ${process.versions.node} (\u2265 24)`):r(`Node ${process.versions.node} \u2014 devcoach needs Node \u2265 24 (embedded node:sqlite)`),u(n.bold(`
17
- Claude Code wiring`));let s=S(O);if(!s.ok)r(`${O} is not valid JSON \u2014 hooks cannot run`);else {let c=Ye(s.data),p=me(s.data);if(c&&p.length)r("devcoach hooks are registered TWICE (plugin + settings.json) \u2014 interactions are double-counted. Disable the plugin or remove the settings.json entries.");else if(c)o("hooks provided by the devcoach plugin");else if(!p.length)r(`no devcoach hooks in ${O} \u2014 run ${n.bold("devcoach install")}`);else {let m=p.filter(({cmd:g})=>g.command.includes("onboard-hook")||g.command.includes("lesson-ready")),d=p.filter(g=>g.event==="Stop").length;m.length?t(`legacy two-entry Stop layout \u2014 run ${n.bold("devcoach install")} to merge into one stop-hook entry (fewer spawns per stop)`):d>1?t(`${d} devcoach Stop entries \u2014 interactions are counted ${d}\xD7 per stop; run ${n.bold("devcoach install")} to keep only the current one`):o(`Stop hook wired (${d} entry)`),p.some(g=>g.event==="UserPromptSubmit")?o("UserPromptSubmit priming hook wired"):t(`no UserPromptSubmit priming hook \u2014 run ${n.bold("devcoach install")} to add it (lessons land more reliably)`),pe(e$1,p,"devcoach install");}let l=E(te);l==="current"?o("Claude Code skill installed (current version)"):c?o("Claude Code skill bundled with the plugin"):t(l==="outdated"?`Claude Code skill is out of date \u2014 run ${n.bold("devcoach install")}`:`Claude Code skill not installed \u2014 run ${n.bold("devcoach install")}`);let f=S(join(homedir(),".claude.json"));f.ok&&f.data.mcpServers?.devcoach?o("MCP server registered (user scope)"):t(`MCP server not found in ~/.claude.json \u2014 it may be registered elsewhere (check with ${n.bold("claude mcp get devcoach")})`);}if(existsSync(re)&&Co(e$1),existsSync(ie)&&So(e$1),u(n.bold(`
17
+ Claude Code wiring`));let s=S(O);if(!s.ok)r(`${O} is not valid JSON \u2014 hooks cannot run`);else {let c=Ye(s.data),p=me(s.data);if(c&&p.length)r("devcoach hooks are registered TWICE (plugin + settings.json) \u2014 interactions are double-counted. Disable the plugin or remove the settings.json entries.");else if(c)o("hooks provided by the devcoach plugin");else if(!p.length)r(`no devcoach hooks in ${O} \u2014 run ${n.bold("devcoach install")}`);else {let m=p.filter(({cmd:g})=>g.command.includes("onboard-hook")||g.command.includes("lesson-ready")),d=p.filter(g=>g.event==="Stop").length;m.length?t(`legacy two-entry Stop layout \u2014 run ${n.bold("devcoach install")} to merge into one stop-hook entry (fewer spawns per stop)`):d>1?t(`${d} devcoach Stop entries \u2014 interactions are counted ${d}\xD7 per stop; run ${n.bold("devcoach install")} to keep only the current one`):o(`Stop hook wired (${d} entry)`),p.some(g=>g.event==="UserPromptSubmit")?o("UserPromptSubmit priming hook wired"):t(`no UserPromptSubmit priming hook \u2014 run ${n.bold("devcoach install")} to add it (lessons land more reliably)`),pe(e,p,"devcoach install");}let l=E(te);l==="current"?o("Claude Code skill installed (current version)"):c?o("Claude Code skill bundled with the plugin"):t(l==="outdated"?`Claude Code skill is out of date \u2014 run ${n.bold("devcoach install")}`:`Claude Code skill not installed \u2014 run ${n.bold("devcoach install")}`);let f=S(join(homedir(),".claude.json"));f.ok&&f.data.mcpServers?.devcoach?o("MCP server registered (user scope)"):t(`MCP server not found in ~/.claude.json \u2014 it may be registered elsewhere (check with ${n.bold("claude mcp get devcoach")})`);}if(existsSync(re)&&Co(e),existsSync(ie)&&So(e),u(n.bold(`
18
18
  Database & pacing`)),!existsSync(i)){t(`no database yet (${i}) \u2014 onboarding runs on the first technical task`),u();return}try{m(c=>{o(`database opens (${i})`),L(c).knowledge_ready?o("onboarding complete"):t("onboarding not complete \u2014 the next stop cues it");let p=F(c);u(` settings: max_per_day=${p.max_per_day} \xB7 min_gap_minutes=${p.min_gap_minutes} \xB7 nudge_every=${p.nudge_every} \xB7 nudge_scope=${p.nudge_scope}`);let l=H$1(c),f=l.reduce(($,b)=>$+b.interactions,0);u(` pacing: ${l.length} session(s) counted, ${f} interaction(s) total`+(l[0]?` \u2014 latest ${l[0].session_id.slice(0,8)}\u2026 at ${l[0].interactions}`:""));let m=J(c);m.pending?t(`a cue is pending since ${m.last_cue_at} (retry threshold armed)`):m.last_skip_reason&&u(` last skip: "${m.last_skip_reason}"`);let d=P(c);d.allowed?o("rate limit: allowed"):t(`rate limit: ${d.reason}`),u(n.bold(`
19
19
  Verdict`));let g=W(c,l[0]?.session_id??null);g.wouldCue?o("the next eligible stop WOULD cue a lesson"):t("the next eligible stop would NOT cue a lesson:");for(let $ of g.reasons)u(` \xB7 ${$}`);});}catch(c){r(`database check failed: ${c}`);}u();}var a=(e="")=>{console.log(e);},eo=e=>e==="junior"?n.green(e):e==="mid"?n.yellow(e):e==="senior"?n.red(e):e,Io=e=>e==="know"?` ${n.green("\u2713")}`:e==="dont_know"?` ${n.red("\u2717")}`:"",Oo=e=>e==="know"?n.green("\u2713 I know this"):e==="dont_know"?n.red("\u2717 I don't know this"):n.dim("no feedback");function oo(e,o){let t=new Map;for(let i of e.groups)for(let s of i.topics)t.set(s,i.name);let r=[...e.knowledge].sort((i,s)=>s.confidence-i.confidence).map(i=>{let s=ze(i.confidence);return [i.topic,t.get(i.topic)??"Other",Z(s,`${i.confidence}/10`),Z(s,Ue(i.confidence))]});return I(o,[{header:"Topic"},{header:"Group"},{header:"Confidence",justify:"right"},{header:"Bar"}],r)}function To(){let e=m(o=>Q$1(o));a(oo(e,"Knowledge Map"));}function jo(e){let o=m(s=>p(s,{period:e.period==="all"?null:e.period,category:e.category,level:e.level,project:e.project,repository:e.repository,branch:e.branch,commit:e.commit,starred:e.starred?true:null,feedback:e.feedback,date_from:e.dateFrom,date_to:e.dateTo,sort:e.sort,order:e.order}));if(o.length===0){a(n.dim("No lessons found."));return}let t=o.some(s=>s.project||s.branch||s.commit_hash),r=[{header:""},{header:"Date"},{header:"Topic"},{header:"Title"},{header:"Level",justify:"center"},{header:"Categories"}];t&&r.push({header:"Project"},{header:"Branch"},{header:"Commit"});let i=o.map(s=>{let c=[s.starred?n.yellow("\u2605"):n.dim("\xB7"),s.timestamp.slice(0,10),n.cyan(s.topic_id),s.title+Io(s.feedback),eo(s.level),s.categories.join(", ")];return t&&c.push(s.project??"",s.branch?n.magenta(s.branch):"",s.commit_hash?n.cyan(s.commit_hash.slice(0,7)):""),c});a(I("Lessons",r,i));}function Mo(e){let o=m(r=>w(r,e));o===null&&(a(n.red(`Lesson '${e}' not found.`)),process.exit(1)),a(z(n.bold(o.title))),a(`${n.dim("ID:")} ${o.id}`),a(`${n.dim("Date:")} ${o.timestamp.slice(0,19).replace("T"," ")}`),a(`${n.dim("Topic:")} ${o.topic_id}`),a(`${n.dim("Categories:")} ${o.categories.join(", ")}`),a(`${n.dim("Level:")} ${eo(o.level)}`);let t=o.starred?n.yellow("\u2605 starred"):n.dim("\u2606 not starred");if(a(`${n.dim("Star:")} ${t} ${n.dim("Feedback:")} ${Oo(o.feedback)}`),o.task_context&&a(`${n.dim("Context:")} ${o.task_context}`),o.project||o.repository||o.branch||o.commit_hash||o.folder){let r=[];o.project&&r.push(`project=${o.project}`),o.repository&&r.push(`repo=${o.repository}`),o.branch&&r.push(`branch=${n.magenta(o.branch)}`),o.commit_hash&&r.push(`commit=${n.cyan(o.commit_hash.slice(0,7))}`),o.folder&&r.push(`folder=${o.folder}`),a(`${n.dim("Git:")} ${r.join(" \xB7 ")}`);}a(z()),a(o.summary);}function qe(e,o){m(r$1=>w(r$1,e)===null?false:(r(r$1,e,o),true))||(a(n.red(`Lesson '${e}' not found.`)),process.exit(1)),a(o?`Lesson ${n.cyan(e)} \u2192 ${n.yellow("\u2605 starred")}`:`Lesson ${n.cyan(e)} \u2192 ${n.dim("\u2606 unstarred")}`);}function Ho(e){m(t=>q$1(t,e))||(a(n.red(`Lesson '${e}' not found.`)),process.exit(1)),a(`Lesson ${n.cyan(e)} deleted.`);}function Do(e,o){["know","dont_know","clear"].includes(o)||(a(n.red(`Invalid feedback '${o}'. Use: know | dont_know | clear`)),process.exit(1));let t=o==="clear"?null:o,r=m(c=>{let p=S$1(c,e,t);if(p===null)return null;let l=c.prepare("SELECT confidence FROM knowledge WHERE topic = ?").get(p);return {topicId:p,newConf:l?l.confidence:5}});r===null&&(a(n.red(`Lesson '${e}' not found.`)),process.exit(1));let i;if(t==="know"||t==="dont_know"){let c=r.newConf+(t==="know"?-1:1);i=`${n.cyan(r.topicId)} confidence: ${c} \u2192 ${n.bold(String(r.newConf))}`;}else i="feedback cleared";let s=t==="know"?n.green("\u2713 I know this"):t==="dont_know"?n.red("\u2717 I don't know this"):n.dim("cleared");a(`Lesson ${n.cyan(e)} \u2192 ${s} (${i})`);}function Eo(){let e=m(i=>F(i)),o=Math.floor(e.min_gap_minutes/60),t=e.min_gap_minutes%60,r=o?`${o}h ${t}m`:`${t}m`;a(I("Settings",[{header:"Key"},{header:"Value",justify:"right"}],[["max_per_day",String(e.max_per_day)],["min_gap_minutes",`${e.min_gap_minutes} (${r})`],["nudge_every",e.nudge_every===0?"0 (off)":String(e.nudge_every)],["nudge_scope",e.nudge_scope]]));}function Ro(){let{stats:e,rateLimit:o,settings:t}=m(l=>({stats:T$1(l),rateLimit:P(l),settings:F(l)})),r=l=>Number(e[l]??0),i=o.allowed?n.green("Available now"):n.yellow(o.reason??"");a(I("Coaching Stats",[{header:"Metric"},{header:"Value",justify:"right"}],[["Total lessons",String(r("total_lessons"))],["Lessons today (24h)",`${r("lessons_today")} / ${t.max_per_day}`],["Lessons this week",String(r("lessons_this_week"))],["Next lesson",i]],false));let s=e.weakest_topics??[],c=e.strongest_topics??[];if(s.length||c.length){let l=[];for(let f=0;f<Math.max(s.length,c.length);f++){let m=s[f],d=c[f];l.push([m?`${n.red(m.topic)} ${n.dim(`(${m.confidence})`)}`:"",d?`${n.green(d.topic)} ${n.dim(`(${d.confidence})`)}`:""]);}a(I(void 0,[{header:"Weakest topics"},{header:"Strongest topics"}],l));}let p=ue();p&&a(`
20
20
  ${p}`);}function Po(e,o){let t=["max_per_day","min_gap_minutes","nudge_every","nudge_scope"];if(t.includes(e)||(a(n.red(`Unknown key '${e}'. Valid keys: ${t.join(", ")}`)),process.exit(1)),e==="nudge_scope"&&o!=="session"&&o!=="global"&&(a(n.red(`Invalid nudge_scope '${o}'. Use: session | global`)),process.exit(1)),e==="nudge_every"){let r=Number.parseInt(o,10);(!Number.isInteger(r)||r<0)&&(a(n.red(`Invalid nudge_every '${o}'. Use a non-negative integer (0 = off).`)),process.exit(1));}m(r=>G(r,e,o)),a(n.green(`Set ${e} = ${o}`));}function Ao(e,o,t){let r=e.trim();r||(a(n.red("Topic name must not be empty.")),process.exit(1)),m(s=>{z$1(s,r,o),t&&t!=="Other"&&D$1(s,r,t);});let i=t&&t!=="Other"?` \u2192 ${n.cyan(t)}`:"";a(`${n.green("Added")} ${n.bold(r)} (confidence ${n.cyan(String(o))})${i}`);}function No(e){let o=m(t=>A(t,e));a(o?`${n.green("Removed")} ${n.bold(e)} from knowledge map`:n.yellow(`Topic '${e}' not found.`));}function Go(e){let o=e.trim();(!o||o==="Other")&&(a(n.red("Invalid group name.")),process.exit(1)),m(t=>B(t,o)),a(`${n.green(`Group '${n.cyan(o)}' ready.`)} Assign topics with: devcoach group-assign <topic> "${o}"`);}function Fo(e){let o=m(t=>C(t,e));a(o?`${n.green(`Removed group '${n.cyan(e)}'`)}`:n.yellow(`Group '${e}' not found.`));}function Bo(e,o){m(r=>r.prepare("SELECT topic FROM knowledge WHERE topic = ?").get(e)===void 0?false:(o==="Other"?E$1(r,e):D$1(r,e,o),true))||(a(n.red(`Topic '${e}' not in knowledge map. Add it first.`)),process.exit(1)),a(o==="Other"?`${n.green("Moved")} ${n.bold(e)} \u2192 Other (ungrouped)`:`${n.green("Moved")} ${n.bold(e)} \u2192 ${n.cyan(o)}`);}function Uo(e){let{lessonsCount:o,knowledgeCount:t$1,data:r}=m(s=>({lessonsCount:t(s).length,knowledgeCount:y(s).length,data:N(s)}));writeFileSync(e,r);let i=existsSync(j)?" + notebook":"";a(`${n.green("Backup saved:")} ${e} (${n.cyan(String(o))} lessons, ${n.cyan(String(t$1))} topics${i})`);}function zo(e){existsSync(e)||(a(n.red(`File not found: ${e}`)),process.exit(1));let o=m(r=>O$1(r,readFileSync(e)));o.settings&&a(`${n.green("\u2713")} Settings restored`),o.topics&&a(`${n.green("\u2713")} Knowledge map restored (${n.cyan(String(o.topics))} topics)`);let t=[`${n.cyan(String(o.lessons))} imported`];o.skipped&&t.push(`${n.yellow(String(o.skipped))} duplicates skipped`),o.invalid&&t.push(`${n.red(String(o.invalid))} rejected (invalid)`),a(`${n.green("\u2713")} Lessons: ${t.join(", ")}`),o.learning_state&&a(`${n.green("\u2713")} Notebook restored`);}async function Ko(){let e=createInterface({input:process.stdin,output:process.stdout}),o=async(r,i="")=>{let s=i?` [${i}]`:"";return (await e.question(`${r}${s}: `)).trim()||i},t=async(r,i,s,c)=>{for(;;){let p=await o(r,String(i)),l=Number.parseInt(p,10);if(!Number.isNaN(l)&&l>=s&&l<=c)return l;a(n.red(`Must be ${s}\u2013${c}.`));}};try{a(z(n.bold("devcoach setup"))),a(`
21
21
  ${n.bold("Step 1")} \u2014 Restore from backup`);let r=await o("Path to existing backup zip (Enter to skip)","");if(r){existsSync(r)||(a(n.red(`File not found: ${r}`)),process.exit(1));let m$1=m(d=>O$1(d,readFileSync(r)));a(`${n.green("\u2713")} Restored: ${m$1.topics} topics, ${m$1.lessons} lessons${m$1.learning_state?", notebook":""}`),a(n.green("Setup complete!"));return}a(`
22
- ${n.bold("Step 2")} \u2014 Build your knowledge profile`);let i=(await o("Mode: [a]utomatic (detect from files) / [m]anual (type your stack)","a")).toLowerCase(),s={};if(i.startsWith("a")){let d=d$1().folder??process.cwd(),g=c(),$=b$1(a$1(d),g.detected_stack),b=Object.keys($).sort((w,L)=>w.localeCompare(L));if(g.scanned_projects>0){a(`
22
+ ${n.bold("Step 2")} \u2014 Build your knowledge profile`);let i=(await o("Mode: [a]utomatic (detect from files) / [m]anual (type your stack)","a")).toLowerCase(),s={};if(i.startsWith("a")){let d=e$1().folder??process.cwd(),g=c(),$=b$1(a$1(d),g.detected_stack),b=Object.keys($).sort((w,L)=>w.localeCompare(L));if(g.scanned_projects>0){a(`
23
23
  ${n.dim(`Scanned ${g.scanned_projects} Claude Code project(s) from your history:`)}`);for(let w of g.projects.slice(0,8))a(n.dim(` ${w.name} \u2014 ${w.topics.join(", ")||"no stack signals"}`));}if(b.length){a(`
24
24
  ${n.dim(`Detected (history + ${n.cyan(d)}):`)}`);for(let w of b){let L=$[w]??5,fe=await o(` ${n.cyan(w)} (Enter=keep, 0-10=override, s=skip)`,String(L));if(fe.toLowerCase()==="s")continue;let he=Number.parseInt(fe,10);s[w]=Number.isNaN(he)?L:Math.max(0,Math.min(10,he));}}else a(n.dim("No technology files detected in current directory."));let R=await o(`
25
25
  Add additional topics (comma-separated, or Enter to skip)`,"");for(let w of R.split(",").map(L=>L.trim()).filter(Boolean))s[w]=await t(` Confidence for ${n.cyan(w)}`,5,0,10);}else for(a(n.dim(`
26
26
  Enter topics: 'topic_id confidence' (e.g. python 7). Blank line when done.`));;){let m=(await o("Topic (Enter when done)","")).trim();if(!m)break;let d=m.split(/\s+/),g=d[0]??"",$=Number.parseInt(d[1]??"",10),b=Number.isNaN($)?5:Math.max(0,Math.min(10,$));s[g]=b,a(` ${n.green("+")} ${n.cyan(g)} \u2192 ${b}`);}let c$1={};if(Object.keys(s).length&&(a(`
27
27
  ${n.bold("Step 3")} \u2014 Organise into groups`),(await o("Organise topics into groups? [y/N]","n")).toLowerCase().startsWith("y"))){let d=[];for(let g of Object.keys(s).sort(($,b)=>$.localeCompare(b))){let $=d.length?d.join(", "):"(none yet)",b=await o(` Group for ${n.cyan(g)} existing: ${n.dim($)} (Enter=Other)`,"");if(b&&b!=="Other"){let R=c$1[b]??[];R.push(g),c$1[b]=R,d.includes(b)||d.push(b);}}}a(`
28
28
  ${n.bold("Step 4")} \u2014 Rate-limit settings`);let p=await t("Max lessons per day",2,1,20),l=await t("Min gap between lessons (minutes)",240,0,1440),f=m(m=>{m.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[d,g]of Object.entries(s))z$1(m,d,g);for(let[d,g]of Object.entries(c$1))for(let $ of g)D$1(m,$,d);return G(m,"max_per_day",String(p)),G(m,"min_gap_minutes",String(l)),Q$1(m)});a(oo(f,"Knowledge Profile")),a(`
29
- ${n.green("Setup complete!")} ${Object.keys(s).length} topics saved.`);}finally{e.close();}}function Jo(){let e$1=[["mcp","Start the MCP server (stdio) for Claude Code / Claude Desktop"],["ui [--port N]","Launch the web dashboard (default port: 7860)"],["setup","First-run wizard: import backup or build your knowledge profile"],["install","Register MCP server + hooks + skill (Claude default; --gemini/--codex beta)"],["onboard-hook","Claude Code Stop hook: cue onboarding when no profile exists"],["lesson-ready","Claude Code Stop hook: cue a lesson when one is due"],["doctor","Diagnose the agent wiring \u2014 explains why a lesson would(n't) fire"],["profile","Show the knowledge map"],["stats","Coaching statistics and rate-limit status"],["settings / set","Show / update settings (max_per_day | min_gap_minutes)"],["lessons / lesson","List past lessons / show one in detail"],["star / unstar / delete","Manage a lesson"],["feedback <id>","Record know / dont_know feedback"],["knowledge-add / -remove","Add / remove a topic"],["group-add / -remove / -assign","Manage knowledge groups"],["backup / restore","Export / import a full backup zip"]];a(`
30
- ${n.bold(`devcoach ${n.dim(`v${e}`)}`)}`);for(let[t,r]of e$1)a(` ${n.cyan(`devcoach ${t}`.padEnd(32))} ${n.dim(r)}`);a(`
29
+ ${n.green("Setup complete!")} ${Object.keys(s).length} topics saved.`);}finally{e.close();}}function Jo(){let e=[["mcp","Start the MCP server (stdio) for Claude Code / Claude Desktop"],["ui [--port N]","Launch the web dashboard (default port: 7860)"],["setup","First-run wizard: import backup or build your knowledge profile"],["install","Register MCP server + hooks + skill (Claude default; --gemini/--codex beta)"],["onboard-hook","Claude Code Stop hook: cue onboarding when no profile exists"],["lesson-ready","Claude Code Stop hook: cue a lesson when one is due"],["doctor","Diagnose the agent wiring \u2014 explains why a lesson would(n't) fire"],["profile","Show the knowledge map"],["stats","Coaching statistics and rate-limit status"],["settings / set","Show / update settings (max_per_day | min_gap_minutes)"],["lessons / lesson","List past lessons / show one in detail"],["star / unstar / delete","Manage a lesson"],["feedback <id>","Record know / dont_know feedback"],["knowledge-add / -remove","Add / remove a topic"],["group-add / -remove / -assign","Manage knowledge groups"],["backup / restore","Export / import a full backup zip"]];a(`
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$2=new Command;return e$2.exitOverride(),e$2.name("devcoach").description("devcoach \u2014 progressive technical coaching").version(`devcoach ${e}`,"-v, --version","output the version number").allowExcessArguments(false).showHelpAfterError("(add --help for usage)").action(()=>Jo()),e$2.command("mcp").description("Start the MCP server (stdio transport)").action(async()=>{let{runStdio:o}=await import('./server-PBW7MYCW.js');await o();}),e$2.command("profile").description("Show the knowledge map").action(To),e$2.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$2.command("lesson").description("Show a single lesson in detail").argument("<id>","Lesson ID").action(o=>Mo(o)),e$2.command("star").description("Mark a lesson as starred").argument("<id>","Lesson ID").action(o=>qe(o,true)),e$2.command("unstar").description("Remove the starred mark from a lesson").argument("<id>","Lesson ID").action(o=>qe(o,false)),e$2.command("delete").description("Permanently delete a lesson").argument("<id>","Lesson ID").action(o=>Ho(o)),e$2.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$2.command("settings").description("Show current settings").action(Eo),e$2.command("stats").description("Coaching statistics and rate-limit status").action(Ro),e$2.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$2.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$2.command("knowledge-remove").description("Remove a topic from the knowledge map").argument("<topic>","Topic ID").action(o=>No(o)),e$2.command("group-add").description("Register a new knowledge group").argument("<name>","Group name").action(o=>Go(o)),e$2.command("group-remove").description("Delete a knowledge group (topics move to Other)").argument("<name>","Group name").action(o=>Fo(o)),e$2.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$2.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$2.command("restore").description("Restore from a backup zip file").argument("<file>","Path to backup zip").action(o=>zo(o)),e$2.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$2.command("doctor").description("Diagnose the devcoach \u21C4 agent wiring and the lesson pacing state").action(Ze),e$2.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$2.command("setup").description("First-run wizard: import backup or build your knowledge profile").action(Ko),e$2.command("stop-hook",{hidden:true}).description("Claude Code Stop hook: onboarding check + lesson cue in one spawn").action(()=>e$1()),e$2.command("prompt-hook",{hidden:true}).description("Claude Code UserPromptSubmit hook: prime the model when a lesson is due").action(()=>g()),e$2.command("gemini-stop-hook",{hidden:true}).description("Gemini CLI AfterAgent hook: onboarding check + lesson cue in one spawn").action(()=>e$1(void 0,"gemini")),e$2.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$2.command("codex-stop-hook",{hidden:true}).description("Codex CLI Stop hook: onboarding check + lesson cue in one spawn").action(()=>e$1(void 0,"codex")),e$2.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$2.command("onboard-hook",{hidden:true}).description("Claude Code Stop hook: cue onboarding (legacy two-entry layout)").action(()=>b()),e$2.command("lesson-ready",{hidden:true}).description("Claude Code Stop hook: signal a lesson is due (legacy two-entry layout)").action(()=>d()),e$2}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};
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-AOCD7LE2.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,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-C42T6YUU.js';import'./chunk-ECN4MNGM.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-4HRCOGG7.js';import'./chunk-ECN4MNGM.js';
@@ -0,0 +1,9 @@
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-X5LCD7AM.js';import {c as c$1,b,a,m,M,g,n,I,s,R,p,r,q,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)]}),O=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});m(s=>{n(s,i),I(s);});try{if(r$1.server.getClientCapabilities()?.elicitation){let h=await r$1.server.elicitInput({message:"Did that land?",requestedSchema:{type:"object",properties:{feedback:{type:"string",title:"Did that land?",description:"know = got it \xB7 dont_know = need to revisit \xB7 skip = no feedback",enum:["know","dont_know","skip"]}},required:[]}}),m$1=h.action==="accept"?h.content?.feedback:void 0;m$1&&m$1!=="skip"&&(m(g=>{s(g,e$1.id,m$1),R(g,e$1.topic_id,m$1==="know"?1:-1);}),i={...i,feedback:m$1});}}catch{}return {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=>O(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
+ `),O(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 O({...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
+
5
+ ---
6
+
7
+ <!-- reference: ${t.name} -->
8
+
9
+ ${t.content}`).join("");return {messages:[{role:"user",content:{type:"text",text:g$1()+e}}]}}),r$1}async function je(){let r=_e(),u=new StdioServerTransport;await r.connect(u);}export{_e as createServer,je as runStdio};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devcoach",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
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",
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
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 L=[["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]],N={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"},F={django:"django",flask:"flask",fastapi:"fastapi",starlette:"fastapi",tornado:"python",aiohttp:"python",sqlalchemy:"postgresql",celery:"python"};function q(t){let e="";for(let r of t)r==="*"?e+="[^/]*":r==="?"?e+="[^/]":e+=r.replace(/[.+^${}()|[\]\\]/g,String.raw`\$&`);return new RegExp(`^${e}$`)}function H(t,e){let r=e.lastIndexOf("/"),n=r>=0?join(t,e.slice(0,r)):t,s=r>=0?e.slice(r+1):e,i;try{i=readdirSync(n);}catch{return false}let c=q(s);return i.some(l=>c.test(l))}function v(t){try{return existsSync(t)?readFileSync(t,"utf8"):null}catch{return null}}function G(t,e){let r=v(join(t,"package.json"));if(!r)return;let n;try{n=JSON.parse(r);}catch{return}let s=new Set([...Object.keys(n.dependencies??{}),...Object.keys(n.devDependencies??{})]);for(let[i,c]of Object.entries(N))s.has(i)&&e(c,6);}function J(t,e){let r=["requirements.txt","requirements-dev.txt","requirements/base.txt","pyproject.toml"];for(let n of r){let s=v(join(t,n))?.toLowerCase();if(s)for(let[i,c]of Object.entries(F))s.includes(i)&&e(c,6);}}function x(t){let e={},r=(n,s)=>{(e[n]??-1)<s&&(e[n]=s);};for(let[n,s,i]of L)H(t,n)&&r(s,i);return G(t,r),J(t,r),e}function g(...t){let e={};for(let r of t)for(let[n,s]of Object.entries(r))(e[n]??-1)<s&&(e[n]=s);return e}var X=15,Y=2,V=40,Z=400,S=1200,B=new Set(["node_modules","dist","build","out","coverage","vendor","venv","__pycache__","Pods","DerivedData","target","tmp"]),Q={".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"},_={detected_stack:{},projects:[],scanned_projects:0};function tt(){let t=process.env.DEVCOACH_CLAUDE_DIR??process.env.CLAUDE_CONFIG_DIR,e=t??join(homedir(),".claude");return {configPath:t?join(e,".claude.json"):join(homedir(),".claude.json"),historyPath:join(e,"history.jsonl"),projectsDir:join(e,"projects")}}function y(t){try{return realpathSync(t)}catch{return t}}function et(t){let e=y(t);return e.startsWith(y(homedir()))?false:e.startsWith(y(tmpdir()))||e.startsWith("/private/")}function R(t){try{return statSync(t).isDirectory()}catch{return false}}function nt(t,e){let r=[],n=[{dir:t,depth:0}],s=0;for(;n.length>0&&s<V&&e.left>0;){let{dir:i,depth:c}=n.shift();if(s++,e.left--,r.push(x(i)),c>=Y)continue;let l;try{l=readdirSync(i);}catch{continue}for(let u of l){if(u.startsWith(".")||B.has(u))continue;let o=join(i,u);R(o)&&n.push({dir:o,depth:c+1});}}return g(...r)}function rt(t){let e={};if(!Array.isArray(t))return e;for(let r of t){if(typeof r!="string")continue;let n=r.lastIndexOf("."),s=n>=0?Q[r.slice(n).toLowerCase()]:void 0;s&&(e[s]=Math.max(e[s]??0,5));}return e}function st(t){let e=new Map,r;try{r=readFileSync(t,"utf8");}catch{return e}for(let n of r.split(`
3
- `))if(n)try{let s=JSON.parse(n);if(typeof s.project!="string")continue;let i=typeof s.timestamp=="number"?s.timestamp:0,c=e.get(s.project)??{count:0,last:0};e.set(s.project,{count:c.count+1,last:Math.max(c.last,i)});}catch{}return e}function ot(t,e){let r=e.replace(/[^a-zA-Z0-9]/g,"-"),n=(()=>{try{return readFileSync(join(t,r,"memory","MEMORY.md"),"utf8").trim()}catch{return null}})();return n?n.length>S?`${n.slice(0,S)}\u2026`:n:null}function vt(){try{let{configPath:t,historyPath:e,projectsDir:r}=tt(),n;try{n=JSON.parse(readFileSync(t,"utf8"))?.projects??{};}catch{return _}let s=st(e),i=Object.keys(n).filter(o=>!et(o)&&R(o)).sort((o,a)=>(s.get(a)?.last??0)-(s.get(o)?.last??0)).slice(0,X),c={left:Z},l=[];for(let o of i){let a=g(nt(o,c),rt(n[o]?.exampleFiles)),f=ot(r,o);if(Object.keys(a).length===0&&f===null)continue;let d=s.get(o);l.push({stack:a,signal:{name:basename(o),path:o,topics:Object.keys(a).sort((O,D)=>O.localeCompare(D)),prompt_count:d?.count??0,last_activity:d?.last?new Date(d.last).toISOString():null,memory:f}});}let u=g(...l.map(o=>o.stack));for(let[o,a]of Object.entries(u))l.filter(d=>d.stack[o]!==void 0).length>=3&&a<8&&(u[o]=a+1);return {detected_stack:u,projects:l.map(o=>o.signal).sort((o,a)=>a.prompt_count-o.prompt_count).slice(0,20),scanned_projects:i.length}}catch{return _}}function k(...t){try{return execFileSync("git",t,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}}var w={"github.com":"github","gitlab.com":"gitlab","bitbucket.org":"bitbucket"};function at(t){if(!t)return [null,null];let e=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(t);if(e){let n=(e[1]??"").toLowerCase();return [e[2]??t,w[n]??"local"]}let r=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(t);if(r){let n=(r[1]??"").toLowerCase();return [r[2]??t,w[n]??"local"]}return [t,"local"]}function Rt(){let t=process.cwd(),e=k("rev-parse","--abbrev-ref","HEAD"),r=k("rev-parse","HEAD"),n=k("remote","get-url","origin"),[s,i]=at(n),c;return s?c=s.replace(/\/+$/,"").split("/").pop()??null:c=basename(t)||null,e==="HEAD"&&(e=null),{project:c,repository:s,branch:e,commit_hash:r,folder:t,repository_platform:i}}var A={version:"0.9.2"};var Et=A.version;var P=dirname(fileURLToPath(import.meta.url));function C(){for(let t of ["../assets","../../assets"])if(existsSync(join(P,t,"SKILL.md")))return join(P,t);return null}var It=()=>{let t=C();return t===null?"devcoach: coaching instructions unavailable (SKILL.md not found in package).":readFileSync(join(t,"SKILL.md"),"utf8")};function Tt(){let t=C();if(t===null)return [];try{return readdirSync(join(t,"references")).filter(e=>e.endsWith(".md")).sort().map(e=>({name:e,content:readFileSync(join(t,"references",e),"utf8")}))}catch{return []}}export{x as a,g as b,vt as c,Rt as d,Et as e,It as f,Tt as g};
@@ -1,13 +0,0 @@
1
- #!/usr/bin/env node
2
- import {e,d as d$1,c as c$2,b as b$1,a as a$1,g as g$1,f as f$1}from'./chunk-M5FUYHRM.js';import {c as c$1,b,a,m,M,g,n,I,s,R,p,r,q,K,d as d$2,z as z$1,D,A,B,C,f,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,writeFileSync,existsSync,readFileSync,statSync}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)]}),se=r=>({content:[l(JSON.stringify(r))],structuredContent:r}),he={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={...he,reply_check:z.string()},ae="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.",me={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())}))},be={max_per_day:z.number().int(),min_gap_minutes:z.number().int(),ui_theme:e$1,nudge_every:z.number().int(),nudge_scope:f};function c(r,u){return {contents:[{uri:r.href,mimeType:"application/json",text:JSON.stringify(u)}]}}function fe(){let r$1=new McpServer({name:"devcoach",version:e},{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=>{try{let t=d$1(),o={};try{o=m(s=>M(s));}catch{o={};}let i=g({id:e.id,timestamp:new Date,topic_id:e.topic_id,categories:e.categories,title:e.title,level:e.level,summary:e.summary,body:e.body,task_context:e.task_context??null,project:e.project??t.project??o.project??null,repository:e.repository??t.repository??o.repository??null,branch:e.branch??t.branch??o.branch??null,commit_hash:e.commit_hash??t.commit_hash??null,folder:e.folder??t.folder??null,repository_platform:e.repository_platform??t.repository_platform??o.repository_platform??null});m(s=>{n(s,i),I(s);});try{if(r$1.server.getClientCapabilities()?.elicitation){let h=await r$1.server.elicitInput({message:"Did that land?",requestedSchema:{type:"object",properties:{feedback:{type:"string",title:"Did that land?",description:"know = got it \xB7 dont_know = need to revisit \xB7 skip = no feedback",enum:["know","dont_know","skip"]}},required:[]}}),p=h.action==="accept"?h.content?.feedback:void 0;p&&p!=="skip"&&(m(m=>{s(m,e.id,p),R(m,e.topic_id,p==="know"?1:-1);}),i={...i,feedback:p});}}catch{}return {content:[l(`Lesson saved. ${ae}`),l(JSON.stringify(i))],structuredContent:{...i,reply_check:ae}}}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$2.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:be,annotations:{title:"Update Settings",destructiveHint:false,idempotentHint:true,openWorldHint:false}},e=>{let t=i=>se(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'). Pass the personalized coaching notebook as `notebook`; it is saved to learning-state.md atomically with the profile. Returns the updated Profile.",inputSchema:{topics:z.record(z.string(),d$2).describe("{topic_id: confidence 0-10}"),groups:z.record(z.string(),z.array(z.string())).optional().describe("{group_name: [topic_id, ...]}"),notebook:z.string().optional().describe("Full personalized coaching notebook (markdown) saved verbatim to learning-state.md. Write real observations about THIS user \u2014 background, strengths, gaps, and how they work across ALL their projects (not just this one), per the template in the devcoach skill. If omitted, a minimal placeholder is written so the file is never empty.")},outputSchema:me,annotations:{title:"Complete Onboarding",destructiveHint:true,idempotentHint:false,openWorldHint:false}},async e=>{try{let t=m(i=>{i.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[s,h]of Object.entries(e.topics))z$1(i,s,Math.max(0,Math.min(10,h)));if(e.groups)for(let[s,h]of Object.entries(e.groups))for(let p of h)p in e.topics&&D(i,p,s);return Q(i)}),o=e.notebook?.trim();return mkdirSync(dirname(j),{recursive:!0}),o?writeFileSync(j,o.endsWith(`
3
- `)?o:`${o}
4
- `):existsSync(j)||writeFileSync(j,`# devcoach \u2014 Coaching Notebook
5
- `),se(t)}catch(t){return d(`complete_onboarding failed: ${t}`)}}),r$1.registerTool("update_notebook",{title:"Update Coaching Notebook",description:"Overwrite the coaching notebook (~/.devcoach/learning-state.md) with revised markdown. Use it after a lesson to fold in what the user absorbed or struggled with, new recurring patterns, and updated hypotheses \u2014 keeping the prior notes. Pass the full notebook content.",inputSchema:{notebook:z.string().min(1).describe("Full notebook markdown saved verbatim to learning-state.md (overwrites).")},annotations:{title:"Update Coaching Notebook",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{return mkdirSync(dirname(j),{recursive:!0}),writeFileSync(j,e.notebook.endsWith(`
6
- `)?e.notebook:`${e.notebook}
7
- `),{content:[l("notebook updated")]}}catch(t){return d(`update_notebook 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,p=P(i),m={allowed:p.allowed,total_lessons:o(i)};return p.reason!=null&&(m.reason=p.reason),{onboarding:{knowledge_ready:s,notebook_ready:h,needs_onboarding:!(s&&h)},rate_limit:m,taught_topics:U(i),profile:Q(i)}});return c(e,{...o$1,notebook:t})}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=>{try{let t=d$1(),o=m(i=>M(i));return c(e,{git:t,usage_defaults:o})}catch(t){return c(e,{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=>{try{let o=m(m=>L(m)).knowledge_ready,i=existsSync(j)&&statSync(j).size>0,s=d$1(),h=c$2(),p=b$1(a$1(s.folder??process.cwd()),h.detected_stack);return c(e,{knowledge_ready:o,notebook_ready:i,needs_onboarding:!(o&&i),detected_stack:p,detected_projects:h.projects,scanned_projects:h.scanned_projects,default_topics:k,context_ready:s.branch!==null})}catch(t){return c(e,{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=g$1().map(t=>`
8
-
9
- ---
10
-
11
- <!-- reference: ${t.name} -->
12
-
13
- ${t.content}`).join("");return {messages:[{role:"user",content:{type:"text",text:f$1()+e}}]}}),r$1}async function xe(){let r=fe(),u=new StdioServerTransport;await r.connect(u);}export{fe as createServer,xe as runStdio};