devcoach 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/SKILL.md CHANGED
@@ -40,7 +40,7 @@ The three hard rules, before anything else:
40
40
 
41
41
  ## Onboarding
42
42
 
43
- Read `devcoach://onboarding`. If `knowledge_ready` or `notebook_ready` is false — or the
43
+ Call `get_onboarding`. If `knowledge_ready` or `notebook_ready` is false — or the
44
44
  user explicitly asks to (re-)initialise their profile — read `references/onboarding.md`
45
45
  in this skill's directory and follow it. Do not deliver a lesson in the same turn as
46
46
  `complete_onboarding`; end the response after confirming setup.
@@ -55,8 +55,11 @@ only an explicit "redo onboarding" goes through `references/onboarding.md`.
55
55
 
56
56
  ## Before delivering a lesson
57
57
 
58
- Read the **`devcoach://briefing`** MCP resource — ONE silent read returns everything
59
- below (never read the underlying resources one by one; each extra call is noise):
58
+ Call the **`get_briefing`** tool — ONE silent call returns everything below (never fetch
59
+ the pieces one by one; each extra call is noise). Read state through tools, never through
60
+ `devcoach://` resources: tool names resolve in every client, a resource read needs the
61
+ server's client-specific name. Older server without `get_briefing` → read the
62
+ `devcoach://briefing` resource instead.
60
63
 
61
64
  - `onboarding` — either flag false → onboarding first (see above)
62
65
  - `rate_limit` — `allowed: false` → skip entirely, say nothing.
@@ -163,7 +166,7 @@ for "know" (within/above band → already calibrated, skip the call). Never call
163
166
  this one? ⭐"* — `star_lesson` only if the user agrees, never silently.
164
167
 
165
168
  **Profile expansion:** if the lesson covered a concept absent from
166
- `devcoach://profile` (or one recurs across tasks), offer to track it with an estimated
169
+ the profile (`get_profile`), or one that recurs across tasks, offer to track it with an estimated
167
170
  confidence (fluent use → 6–7, uncertain → 4–5, first encounter → 2–3); `add_topic`
168
171
  only on confirmation.
169
172
 
@@ -176,7 +179,7 @@ checkpoints, never touch the notebook.
176
179
 
177
180
  - "What did I learn today/this week?" → `get_lessons({period: "today" | "week" | …})`
178
181
  - "Show me lessons about X" → `get_lessons({category: X})` or `({search: keyword})`
179
- - "How good am I at X?" / "Show my profile" → read `devcoach://profile`
182
+ - "How good am I at X?" / "Show my profile" → `get_profile`
180
183
  - "Coaching log" → `get_lessons({period: "all"})`
181
184
  - "Lessons to revisit" → `get_lessons({feedback: "dont_know"})`
182
185
 
@@ -6,7 +6,8 @@ Read this file when the hook cue marks the lesson as a notebook checkpoint (ever
6
6
  ## Step 1 — Fetch the window
7
7
 
8
8
  Call `get_lessons()` (default `limit=10`, newest first) to get the last 10 lessons.
9
- Read `devcoach://profile` for the current knowledge map.
9
+ Call `get_profile` for the current knowledge map (older server without the tool: read the
10
+ `devcoach://profile` resource).
10
11
 
11
12
  ## Step 2 — Per-topic signal analysis
12
13
 
@@ -17,22 +18,22 @@ Group the 10 lessons by `topic_id`. For each topic that appeared:
17
18
  | Consistent mastery | All feedback `know`, no `dont_know` | `update_knowledge(topic, +1)` if confidence < 9 |
18
19
  | Persistent gap | 2+ lessons on same topic, any `dont_know` | `update_knowledge(topic, -1)` if confidence > 1 |
19
20
  | Recurring topic | 3+ lessons on same topic, mixed or no feedback | no confidence change — note in notebook |
20
- | New topic | `topic_id` absent from `devcoach://profile` | `add_topic` — see Step 3 |
21
+ | New topic | `topic_id` absent from `get_profile` | `add_topic` — see Step 3 |
21
22
 
22
23
  Apply at most **one** `update_knowledge` call per topic per calibration run.
23
24
  Never call `update_knowledge` on a topic with confidence 10 (already mastered).
24
25
 
25
26
  ## Step 3 — New topic discovery
26
27
 
27
- For each lesson whose `topic_id` is not in `devcoach://profile`:
28
+ For each lesson whose `topic_id` is not in `get_profile`:
28
29
  - 2+ lessons share this `topic_id` in the window → call `add_topic(topic_id, confidence=5)`;
29
30
  assign it to the same group as the closest related existing topic, or `"Other"` if unclear.
30
31
  - Only 1 lesson on this `topic_id` → note under **Open hypotheses** in the notebook; do not add yet.
31
32
 
32
33
  ## Step 4 — Update the coaching notebook
33
34
 
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
35
+ Call `get_briefing` for the current notebook text and its `notebook_path` — one
36
+ call covers both. Merge findings into the relevant sections and write the complete
36
37
  revised markdown directly to `notebook_path` (your own Write/Edit tool), overwriting the
37
38
  file in one go:
38
39
 
@@ -1,6 +1,6 @@
1
1
  # devcoach — Onboarding flow
2
2
 
3
- Read this file when `devcoach://onboarding` reports `knowledge_ready: false` or
3
+ Read this file when `get_onboarding` reports `knowledge_ready: false` or
4
4
  `notebook_ready: false`, or when the user explicitly asks to (re-)initialise their
5
5
  profile ("setup devcoach", "redo onboarding", "reset my topics").
6
6
 
@@ -8,7 +8,7 @@ The onboarding flow runs inline, immediately before delivering a lesson, wheneve
8
8
  it is needed. There is no separate session startup phase — this makes the skill
9
9
  robust to context compaction and plan-mode transitions.
10
10
 
11
- Check `knowledge_ready` and `notebook_ready` from `devcoach://onboarding` independently
11
+ Check `knowledge_ready` and `notebook_ready` from `get_onboarding` independently
12
12
  — each step can run alone:
13
13
 
14
14
  ```
@@ -23,7 +23,8 @@ 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`,
26
+ Call `get_onboarding` FIRST (older server without the tool: read the `devcoach://onboarding`
27
+ resource) — its `detected_stack`, `detected_projects`,
27
28
  `scanned_projects`, and `notebook_path` come from a scan of the user's **full Claude
28
29
  Code history** (every project Claude has worked in, ranked by recent activity), not
29
30
  just the current folder. Keep `notebook_path` in scope — Step 4 needs it and there is
@@ -57,8 +58,8 @@ depend on `scanClaudeHistory`'s metadata scan). Do **not** default to Automatic
57
58
  silently — surface the choice and wait for the user's answer before proceeding.
58
59
 
59
60
  **If Import backup:** ask for the file path and call `restore` (CLI) with it. Restore
60
- brings back knowledge entries automatically — no further DB steps needed. Re-read
61
- `devcoach://onboarding` after restore; if `knowledge_ready` is now true, skip to the
61
+ brings back knowledge entries automatically — no further DB steps needed. Call
62
+ `get_onboarding` again after restore; if `knowledge_ready` is now true, skip to the
62
63
  notebook-composition part of Step 4. If `notebook_ready` is also true, proceed normally.
63
64
 
64
65
  ## Step 2 — Build the topic list for the chosen mode
@@ -153,7 +154,7 @@ Automatic tiers; after the user confirms in Guided mode):
153
154
  { "topics": { "lang_a": 7, "tool_b": 8 }, "groups": { "Languages": ["lang_a"], "DevOps": ["tool_b"] } }
154
155
  ```
155
156
  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
+ `notebook_path` (from Step 1's `get_onboarding` call) using your own file
157
158
  tools — overwrite the placeholder right away, don't leave it standing. If
158
159
  `notebook_path` already has real content (re-onboarding, or restoring), read it first
159
160
  and fold prior notes in rather than discarding them.
@@ -17,7 +17,8 @@ redo onboarding / reset topics → references/onboarding.md, Steps 1–4 (co
17
17
 
18
18
  ## Review
19
19
 
20
- 1. Read `devcoach://briefing` — one silent read returns the profile, the taught
20
+ 1. Call `get_briefing` (older server without the tool: read the `devcoach://briefing`
21
+ resource) — one silent call returns the profile, the taught
21
22
  topics, the notebook, and its `notebook_path`.
22
23
  2. Walk the user through the profile **grouped, not topic-by-topic**: show each
23
24
  group with its topics and confidences, and ask what looks wrong. Apply
@@ -33,9 +34,9 @@ redo onboarding / reset topics → references/onboarding.md, Steps 1–4 (co
33
34
 
34
35
  Re-derive the notebook from real data; the knowledge map is untouched.
35
36
 
36
- 1. Read `devcoach://onboarding` — `detected_projects` carries the history-wide
37
+ 1. Call `get_onboarding` — `detected_projects` carries the history-wide
37
38
  evidence: per-project stacks, activity volume and recency, and auto-memory
38
- excerpts (and its `notebook_path`). Read `devcoach://briefing` for the
39
+ excerpts (and its `notebook_path`). Call `get_briefing` for the
39
40
  current notebook and profile, and `get_lessons({period: "all"})` for
40
41
  feedback history (`dont_know` lessons are open gaps).
41
42
  2. Compose a fresh notebook (structure in `references/onboarding.md`'s notebook
@@ -47,7 +48,7 @@ Re-derive the notebook from real data; the knowledge map is untouched.
47
48
 
48
49
  ## New tech check (keep the profile curated)
49
50
 
50
- Compare `detected_stack` + `detected_projects` (from `devcoach://onboarding`)
51
+ Compare `detected_stack` + `detected_projects` (from `get_onboarding`)
51
52
  against the current profile:
52
53
 
53
54
  - Candidate = a detected topic **absent from the profile** that appears in **2+
@@ -60,7 +61,7 @@ against the current profile:
60
61
  appear in **no** scanned project. Offer `remove_topic`; keeping them is fine.
61
62
  - Nothing qualifies → say so in one line and stop. An overcrowded topic list
62
63
  dilutes lesson choice — fewer, real topics beat completeness.
63
- - `devcoach://onboarding` unreachable, or `detected_projects` empty → derive
64
+ - `get_onboarding` failing, or `detected_projects` empty → derive
64
65
  candidates from lesson history instead (`get_lessons`: recurring stacks in
65
66
  `task_context` that lack a matching topic), tell the user the evidence is
66
67
  history-only, and defer the retirement pass — never guess retirements
package/dist/bin.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- var t=process.argv[2]??"",r=new Set(["stop-hook","prompt-hook","gemini-stop-hook","gemini-prompt-hook","codex-stop-hook","codex-prompt-hook","onboard-hook","lesson-ready"]);try{if(r.has(t)){let{runHook:o}=await import('./hooks-5PEDPGZD.js');o(t);}else {let{runCli:o}=await import('./commands-DC5NTWGY.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-4E5KRWGP.js');o(t);}else {let{runCli:o}=await import('./commands-QBSCNZVJ.js');await o();}}catch(o){console.error(o),process.exit(1);}
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import {i,m as m$1,L,W,h as h$1,l as l$1,V}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 l(){let e={stop_hook_active:false,permission_mode:null,session_id:null};try{let o=fstatSync(0);return !o.isFIFO()&&!o.isFile()&&!o.isSocket()?e:D(readFileSync(0,"utf8"))}catch{return e}}function A(e){return e.stop_hook_active||e.permission_mode==="plan"}function s(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 N={claude:"block",gemini:"deny",codex:"block"};function h(e,o,t){let n={decision:N[e],reason:o};t&&(n.systemMessage=t),process.stdout.write(`${JSON.stringify(n)}
4
+ `),process.exit(0);}var f="devcoach: the user has no coaching profile yet. Ask them how they want to set it up and STRONGLY recommend Automatic \u2014 devcoach scans their full Claude Code history across all their projects (call get_onboarding for the detected stack and per-project evidence) so the profile reflects what they actually build. Also offer Guided (a short conversation to map topics and confidence levels) and Import (restore from a backup), but lead with Automatic; do not pick for them. After they choose, build the profile and save it by calling the devcoach `complete_onboarding` MCP tool (not a shell command) before ending your turn.";function $(e=l()){if(A(e)&&process.exit(0),existsSync(i)){let o;try{o=m$1(t=>L(t).knowledge_ready);}catch(t){s("onboard-hook",e.session_id,`error: ${t}`),process.exit(0);}o&&process.exit(0);}s("onboard-hook",e.session_id,"onboarding cue"),h("claude",f);}var a=10,y={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%a===0,n=Math.ceil(e/a)*a,i=t?`This is lesson #${e} \u2014 a notebook checkpoint (every ${a} lessons): after log_lesson returns the feedback, also read references/calibration.md in the devcoach skill and run its notebook-update step (a direct file write), recording "(updated after ${e} lessons)".`:`Skip the notebook update this time \u2014 it is refreshed only every ${a} lessons (next checkpoint at lesson #${n}).`;return `devcoach: a lesson is due for the technical work just completed. Do it now \u2014 do not acknowledge this message and do not explain what you are about to do.
5
+
6
+ ${y[o].cue} NOW and follow it exactly \u2014 the skill is the single source of truth for the entire flow, including the explicit no-op when the completed work does not warrant a lesson.
7
+ ${i}
8
+
9
+ If the devcoach skill is not available, fall back to: call get_briefing (one call: profile, taught topics, notebook); pick ONE untaught profile topic at or above the user's confidence band; call log_lesson silently (body = 3\u20136 short paragraphs tied to the task plus a \`\u{1F4A1} *Senior tip:*\` line, clean markdown without bands or a title line); THEN write the card as the FINAL message of the turn: 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]\`, the body you passed, and the tip \u2014 saving is not showing; the card in the final message is the only thing the user sees. If instead the work does not warrant a lesson (pure questions, chat, nothing technical), call the devcoach \`skip_lesson\` tool with a one-line reason.
10
+
11
+ 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 I(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(e,o.session_id,{planMode:n});return i.cue?{kind:"cue",nextLessonNumber:i.nextLessonNumber,note:i.reason}:{kind:"silent",note:i.reason}}function v(e,o,t){let n;try{n=m$1(i=>l$1(i,()=>I(i,o,t)));}catch(i){s(e,o.session_id,`error: ${i}`),process.exit(0);}switch(s(e,o.session_id,n.note),n.kind){case "onboard":h(t.client,f);break;case "cue":h(t.client,E(n.nextLessonNumber,t.client),"\u{1F393} devcoach: checking whether a lesson is due\u2026");break;}process.exit(0);}function T(e=l()){existsSync(i)||process.exit(0),v("lesson-ready",e,{onboardCue:false,client:"claude"});}function k(e=l(),o="claude"){let t=o==="claude"?"stop-hook":`${o}-stop-hook`;existsSync(i)||((e.stop_hook_active||e.permission_mode==="plan")&&process.exit(0),s(t,e.session_id,"onboarding cue (no DB)"),h(o,f)),v(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:
12
+ - If this task is technical: ${y[e].prime} and follow it exactly to close your reply \u2014 the skill is the single source of truth for the entire lesson flow.
13
+ - If it is not technical: silently call the devcoach \`skip_lesson\` tool with a one-line reason and output nothing about it.
14
+ - 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=l(),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){s(t,e.session_id,`error: ${i}`),process.exit(0);}s(t,e.session_id,n?"priming":"no prime"),n||process.exit(0),process.stdout.write(`${JSON.stringify({hookSpecificOutput:{hookEventName:B[o],additionalContext:R(o)}})}
15
+ `),process.exit(0);}function U(e,o){let t=()=>o??l();switch(e){case "stop-hook":k(t());break;case "gemini-stop-hook":k(t(),"gemini");break;case "codex-stop-hook":k(t(),"codex");break;case "prompt-hook":m(t());break;case "gemini-prompt-hook":m(t(),"gemini");break;case "codex-prompt-hook":m(t(),"codex");break;case "onboard-hook":$(t());break;case "lesson-ready":T(t());break;default:process.exit(0);}}export{D as a,$ as b,E as c,T as d,k as e,R as f,m as g,U as h};
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
  import {readFileSync,readdirSync,statSync,existsSync,realpathSync}from'fs';import {dirname,basename,join}from'path';import {homedir,tmpdir}from'os';import {execFileSync}from'child_process';import {fileURLToPath}from'url';var H=[["package.json","javascript",6],["tsconfig*.json","typescript",6],["requirements*.txt","python",6],["pyproject.toml","python",6],["setup.py","python",6],["go.mod","go",6],["Cargo.toml","rust",6],["pom.xml","java",6],["build.gradle","java",6],["build.gradle.kts","java",6],["*.csproj","csharp",6],["Gemfile","ruby",6],["composer.json","php",6],["Dockerfile","docker",7],["Dockerfile.*","docker",7],["docker-compose.yml","docker_compose",7],["docker-compose.yaml","docker_compose",7],["docker-compose.*.yml","docker_compose",7],[".github/workflows/*.yml","github_actions",6],[".github/workflows/*.yaml","github_actions",6],[".gitlab-ci.yml","gitlab_ci",6],["Jenkinsfile","ci_cd",6],["*.tf","terraform",6],["*.sql","sql",5],[".git","git",7],["kubernetes/*.yaml","kubernetes",6],["kubernetes/*.yml","kubernetes",6],["k8s/*.yaml","kubernetes",6],["*.bicep","azure",6],["serverless.yml","aws",6],["serverless.yaml","aws",6],["template.yaml","aws",5],["angular.json","angular",6],["svelte.config.*","svelte",6],["nuxt.config.*","vue",6],["vite.config.*","javascript",6],["*.xcodeproj","swift",6],["*.xcworkspace","swift",6],["Package.swift","swift",6]],G={react:"react","react-dom":"react",vue:"vue","@angular/core":"angular",svelte:"svelte",next:"nextjs",nuxt:"vue",fastify:"fastify",express:"express",koa:"express","@nestjs/core":"node_js"},J={django:"django",flask:"flask",fastapi:"fastapi",starlette:"fastapi",tornado:"python",aiohttp:"python",sqlalchemy:"postgresql",celery:"python"};function $(e){let t="";for(let r of e)r==="*"?t+="[^/]*":r==="?"?t+="[^/]":t+=r.replace(/[.+^${}()|[\]\\]/g,String.raw`\$&`);return new RegExp(`^${t}$`)}function U(e,t){let r=t.lastIndexOf("/"),n=r>=0?join(e,t.slice(0,r)):e,o=r>=0?t.slice(r+1):t,i;try{i=readdirSync(n);}catch{return false}let c=$(o);return i.some(l=>c.test(l))}function k(e){try{return existsSync(e)?readFileSync(e,"utf8"):null}catch{return null}}function X(e,t){let r=k(join(e,"package.json"));if(!r)return;let n;try{n=JSON.parse(r);}catch{return}let o=new Set([...Object.keys(n.dependencies??{}),...Object.keys(n.devDependencies??{})]);for(let[i,c]of Object.entries(G))o.has(i)&&t(c,6);}function z(e,t){let r=["requirements.txt","requirements-dev.txt","requirements/base.txt","pyproject.toml"];for(let n of r){let o=k(join(e,n))?.toLowerCase();if(o)for(let[i,c]of Object.entries(J))o.includes(i)&&t(c,6);}}function S(e){let t={},r=(n,o)=>{(t[n]??-1)<o&&(t[n]=o);};for(let[n,o,i]of H)U(e,n)&&r(o,i);return X(e,r),z(e,r),t}function f(...e){let t={};for(let r of e)for(let[n,o]of Object.entries(r))(t[n]??-1)<o&&(t[n]=o);return t}var B=15,Q=2,tt=40,et=400,x=1200,nt=new Set(["node_modules","dist","build","out","coverage","vendor","venv","__pycache__","Pods","DerivedData","target","tmp"]),rt={".ts":"typescript",".tsx":"typescript",".js":"javascript",".jsx":"javascript",".mjs":"javascript",".py":"python",".swift":"swift",".java":"java",".kt":"java",".go":"go",".rs":"rust",".rb":"ruby",".php":"php",".cs":"csharp",".sql":"sql",".tf":"terraform"},w={detected_stack:{},projects:[],scanned_projects:0};function A(){let e=process.env.DEVCOACH_CLAUDE_DIR??process.env.CLAUDE_CONFIG_DIR,t=e??join(homedir(),".claude");return {configPath:e?join(t,".claude.json"):join(homedir(),".claude.json"),historyPath:join(t,"history.jsonl"),projectsDir:join(t,"projects")}}function b(e){try{return realpathSync(e)}catch{return e}}function E(e){let t=b(e);return t.startsWith(b(homedir()))?false:t.startsWith(b(tmpdir()))||t.startsWith("/private/")}function v(e){try{return statSync(e).isDirectory()}catch{return false}}function st(e,t){let r=[],n=[{dir:e,depth:0}],o=0;for(;n.length>0&&o<tt&&t.left>0;){let{dir:i,depth:c}=n.shift();if(o++,t.left--,r.push(S(i)),c>=Q)continue;let l;try{l=readdirSync(i);}catch{continue}for(let u of l){if(u.startsWith(".")||nt.has(u))continue;let s=join(i,u);v(s)&&n.push({dir:s,depth:c+1});}}return f(...r)}function ot(e){let t={};if(!Array.isArray(e))return t;for(let r of e){if(typeof r!="string")continue;let n=r.lastIndexOf("."),o=n>=0?rt[r.slice(n).toLowerCase()]:void 0;o&&(t[o]=Math.max(t[o]??0,5));}return t}function O(e){let t=new Map,r;try{r=readFileSync(e,"utf8");}catch{return t}for(let n of r.split(`
3
- `))if(n)try{let o=JSON.parse(n);if(typeof o.project!="string")continue;let i=typeof o.timestamp=="number"?o.timestamp:0,c=t.get(o.project)??{count:0,last:0};t.set(o.project,{count:c.count+1,last:Math.max(c.last,i)});}catch{}return t}function it(e,t){let r=t.replace(/[^a-zA-Z0-9]/g,"-"),n=(()=>{try{return readFileSync(join(e,r,"memory","MEMORY.md"),"utf8").trim()}catch{return null}})();return n?n.length>x?`${n.slice(0,x)}\u2026`:n:null}function xt(){try{let{configPath:e,historyPath:t,projectsDir:r}=A(),n;try{n=JSON.parse(readFileSync(e,"utf8"))?.projects??{};}catch{return w}let o=O(t),i=Object.keys(n).filter(s=>!E(s)&&v(s)).sort((s,a)=>(o.get(a)?.last??0)-(o.get(s)?.last??0)).slice(0,B),c={left:et},l=[];for(let s of i){let a=f(st(s,c),ot(n[s]?.exampleFiles)),h=it(r,s);if(Object.keys(a).length===0&&h===null)continue;let d=o.get(s);l.push({stack:a,signal:{name:basename(s),path:s,topics:Object.keys(a).sort((N,L)=>N.localeCompare(L)),prompt_count:d?.count??0,last_activity:d?.last?new Date(d.last).toISOString():null,memory:h}});}let u=f(...l.map(s=>s.stack));for(let[s,a]of Object.entries(u))l.filter(d=>d.stack[s]!==void 0).length>=3&&a<8&&(u[s]=a+1);return {detected_stack:u,projects:l.map(s=>s.signal).sort((s,a)=>a.prompt_count-s.prompt_count).slice(0,20),scanned_projects:i.length}}catch{return w}}var ct=8,at=50,R=(e,t)=>({window_months:e,cutoff:t,candidate_count:0,over_soft_limit:false,candidates:[]});function wt(e){let t=Number.isFinite(e)&&e>0?e:3,r=Date.now()-t*30*864e5,n=new Date(r).toISOString();try{let{configPath:o,historyPath:i}=A(),c;try{c=JSON.parse(readFileSync(o,"utf8"))?.projects??{};}catch{return R(t,n)}let l=O(i),u=Object.keys(c).filter(s=>!E(s)&&v(s)).map(s=>({path:s,seen:l.get(s)})).filter(s=>(s.seen?.last??0)>=r).sort((s,a)=>(a.seen?.last??0)-(s.seen?.last??0));return {window_months:t,cutoff:n,candidate_count:u.length,over_soft_limit:u.length>ct,candidates:u.slice(0,at).map(s=>({name:basename(s.path),path:s.path,last_activity:s.seen?.last?new Date(s.seen.last).toISOString():null,prompt_count:s.seen?.count??0}))}}catch{return R(t,n)}}function _(...e){try{return execFileSync("git",e,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}}var D={"github.com":"github","gitlab.com":"gitlab","bitbucket.org":"bitbucket"};function pt(e){if(!e)return [null,null];let t=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(e);if(t){let n=(t[1]??"").toLowerCase();return [t[2]??e,D[n]??"local"]}let r=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(e);if(r){let n=(r[1]??"").toLowerCase();return [r[2]??e,D[n]??"local"]}return [e,"local"]}function Et(){let e=process.cwd(),t=_("rev-parse","--abbrev-ref","HEAD"),r=_("rev-parse","HEAD"),n=_("remote","get-url","origin"),[o,i]=pt(n),c;return o?c=o.replace(/\/+$/,"").split("/").pop()??null:c=basename(e)||null,t==="HEAD"&&(t=null),{project:c,repository:o,branch:t,commit_hash:r,folder:e,repository_platform:i}}var C={version:"2.0.1"};var Mt=C.version;var M=dirname(fileURLToPath(import.meta.url));function T(){for(let e of ["../assets","../../assets"])if(existsSync(join(M,e,"SKILL.md")))return join(M,e);return null}var Ft=()=>{let e=T();return e===null?"devcoach: coaching instructions unavailable (SKILL.md not found in package).":readFileSync(join(e,"SKILL.md"),"utf8")};function Wt(){let e=T();if(e===null)return [];try{return readdirSync(join(e,"references")).filter(t=>t.endsWith(".md")).sort().map(t=>({name:t,content:readFileSync(join(e,"references",t),"utf8")}))}catch{return []}}export{S as a,f as b,xt as c,wt as d,Et as e,Mt as f,Ft as g,Wt as h};
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:"2.0.2"};var Mt=C.version;var M=dirname(fileURLToPath(import.meta.url));function T(){for(let e of ["../assets","../../assets"])if(existsSync(join(M,e,"SKILL.md")))return join(M,e);return null}var Ft=()=>{let e=T();return e===null?"devcoach: coaching instructions unavailable (SKILL.md not found in package).":readFileSync(join(e,"SKILL.md"),"utf8")};function Wt(){let e=T();if(e===null)return [];try{return readdirSync(join(e,"references")).filter(t=>t.endsWith(".md")).sort().map(t=>({name:t,content:readFileSync(join(e,"references",t),"utf8")}))}catch{return []}}export{S as a,f as b,xt as c,wt as d,Et as e,Mt as f,Ft as g,Wt as h};
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import {e,g,b,d}from'./chunk-OSA5I6L7.js';export{c as buildLessonCue,d as cmdLessonReady,b as cmdOnboardHook,g as cmdPromptHook,e as cmdStopHook,a as parseHookPayload}from'./chunk-OSA5I6L7.js';import {f,e as e$1,c,b as b$1,a as a$1,g as g$1,h}from'./chunk-ZCSBBJ3A.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-QGXVUN7G.js';export{c as buildLessonCue,d as cmdLessonReady,b as cmdOnboardHook,g as cmdPromptHook,e as cmdStopHook,a as parseHookPayload}from'./chunk-QGXVUN7G.js';import {f,e as e$1,c,b as b$1,a as a$1,g as g$1,h}from'./chunk-TGTNEQU4.js';import {m,Q as Q$1,p,w,r,q as q$1,S as S$1,F,P,T as T$1,G,z as z$1,D as D$1,A,B,C,E as E$1,N,y,t,j,O as O$1,i,L,H as H$1,J,W}from'./chunk-ECN4MNGM.js';import {writeFileSync,existsSync,readFileSync,mkdirSync,accessSync,constants}from'fs';import {createInterface}from'readline/promises';import {Command}from'commander';import {spawnSync}from'child_process';import {homedir,platform}from'os';import {join,dirname}from'path';import {styleText}from'util';var io=!!process.stdout.isTTY&&!process.env.NO_COLOR,_=(e,o)=>io?styleText(e,o):o,n={green:e=>_("green",e),red:e=>_("red",e),yellow:e=>_("yellow",e),cyan:e=>_("cyan",e),magenta:e=>_("magenta",e),dim:e=>_("dim",e),bold:e=>_("bold",e)};function Z(e,o){return e==="green"?n.green(o):e==="yellow"?n.yellow(o):n.red(o)}var so=/\[[0-9;]*m/g,ao=e=>e.replace(so,""),U=e=>[...ao(e)].length;function Be(e,o,t){let r=Math.max(0,o-U(e));if(t==="right")return " ".repeat(r)+e;if(t==="center"){let i=Math.floor(r/2);return " ".repeat(i)+e+" ".repeat(r-i)}return e+" ".repeat(r)}function I(e,o,t,r=true){let i=o.map((f,m)=>Math.max(r?U(f.header):0,...t.map(d=>U(d[m]??"")))),s=(f,m,d)=>f+i.map(g=>"\u2500".repeat(g+2)).join(m)+d,c=f=>`\u2502${f.map((m,d)=>` ${Be(m??"",i[d]??0,o[d]?.justify??"left")} `).join("\u2502")}\u2502`,p=i.reduce((f,m)=>f+m+3,1),l=[];e&&l.push(Be(n.bold(e),p,"center")),l.push(s("\u256D","\u252C","\u256E")),r&&l.push(c(o.map(f=>n.bold(f.header))),s("\u251C","\u253C","\u2524"));for(let f of t)l.push(c(f));return l.push(s("\u2570","\u2534","\u256F")),l.join(`
3
3
  `)}function z(e=""){if(!e)return "\u2500".repeat(60);let t=` ${e} `,r=Math.max(2,60-U(t)),i=Math.floor(r/2);return "\u2500".repeat(i)+t+"\u2500".repeat(r-i)}var Ue=e=>{let o=Math.max(0,Math.min(10,Math.round(e)));return "\u2588".repeat(o)+"\u2591".repeat(10-o)},ze=e=>e>=7?"green":e>=4?"yellow":"red";var u=(e="")=>{console.log(e);};function go(){let e=platform();if(e==="darwin")return join(homedir(),"Library","Application Support","Claude","claude_desktop_config.json");if(e==="win32"){let t=process.env.APPDATA??join(homedir(),"AppData","Roaming");return join(t,"Claude","claude_desktop_config.json")}let o=process.env.XDG_CONFIG_HOME??join(homedir(),".config");return join(o,"Claude","claude_desktop_config.json")}var O=join(homedir(),".claude","settings.json"),te=join(homedir(),".claude","skills","devcoach"),re=join(homedir(),".gemini"),T=join(re,"settings.json"),fo=join(re,"extensions","devcoach","gemini-extension.json"),ie=join(homedir(),".codex"),H=join(ie,"hooks.json"),ee=join(ie,"config.toml"),D=join(homedir(),".agents","skills","devcoach");function We(e){for(let o of (process.env.PATH??"").split(":"))if(o)try{return accessSync(join(o,e),constants.X_OK),join(o,e)}catch{}return null}var se=e=>We(e)!==null;function Ve(){return se("devcoach")?{command:"devcoach",args:["mcp"]}:{command:"npx",args:["-y","devcoach","mcp"]}}function ae(){let e=We("devcoach");return e?e.includes("fnm_multishells")?"devcoach":e:"npx -y devcoach"}function Ke(e,o,t){if(!se(e))return "";t&&spawnSync(e,t,{encoding:"utf8"});let r=spawnSync(e,o,{encoding:"utf8"});return r.status===0?`${n.green("\u2713")} Registered via \`${e} mcp add\``:`${r.stderr??""}${r.stdout??""}`.toLowerCase().includes("already")?`${n.yellow("Already registered")} in ${e} (use --force to overwrite)`:`${n.red(`${e} mcp add failed:`)} ${(r.stderr||r.stdout||"").trim()}`}function ho(e,o){if(!se("claude"))return "";o&&spawnSync("claude",["mcp","remove","--scope",e,"devcoach"],{encoding:"utf8"});let t=Ve(),r=spawnSync("claude",["mcp","add","--scope",e,"devcoach",t.command,"--",...t.args],{encoding:"utf8"});return r.status===0?`${n.green("\u2713")} Registered via \`claude mcp add\` (scope: ${e})`:`${r.stderr??""}${r.stdout??""}`.toLowerCase().includes("already")?`${n.yellow("Already registered")} in Claude Code (use --force to overwrite)`:`${n.red("claude mcp add failed:")} ${(r.stderr||r.stdout||"").trim()}`}function S(e){if(!existsSync(e))return {ok:true,data:{}};try{return {ok:!0,data:JSON.parse(readFileSync(e,"utf8"))??{}}}catch{return {ok:false,error:`${n.red("\u2717")} ${e} is not valid JSON \u2014 fix it and re-run (left unchanged)`}}}function q(e,o,t){let r=S(e);if(!r.ok)return r.error;let i=r.data;i.mcpServers??={};let s=i.mcpServers;return s.devcoach&&!t?`${n.yellow("Already registered")} in ${e} (use --force to overwrite)`:(s.devcoach=o,mkdirSync(dirname(e),{recursive:true}),writeFileSync(e,`${JSON.stringify(i,null,2)}
4
4
  `),`${n.green("\u2713")} Installed into ${e}`)}function ko(){let e=ae();return {Stop:{hooks:[{type:"command",command:`${e} stop-hook`,timeout:60}]},UserPromptSubmit:{hooks:[{type:"command",command:`${e} prompt-hook`,timeout:30}]}}}function bo(){let e=ae();return {AfterAgent:{hooks:[{type:"command",command:`${e} gemini-stop-hook`,timeout:6e4}]},BeforeAgent:{hooks:[{type:"command",command:`${e} gemini-prompt-hook`,timeout:3e4}]}}}function $o(){let e=ae();return {Stop:{hooks:[{type:"command",command:`${e} codex-stop-hook`,timeout:60}]},UserPromptSubmit:{hooks:[{type:"command",command:`${e} codex-prompt-hook`,timeout:30}]}}}function Ye(e){return Object.entries(e.enabledPlugins??{}).some(([o,t])=>t&&o.startsWith("devcoach@"))}function ce(){return existsSync(fo)}function de(e){return /dev-?coach/i.test(e)&&/\b(?:stop-hook|prompt-hook|onboard-hook|lesson-ready)\b/.test(e)}function le(e,o){let t=S(e);if(!t.ok)return t.error;let r=t.data;r.hooks??={};let i=r.hooks,s=false;for(let[c,p]of Object.entries(o)){i[c]??=[];let l=i[c],f=l.map((d,g)=>[d,g]).filter(([d])=>(d.hooks??[]).some(g=>de(g.command??""))).map(([,d])=>d),[m]=f;if(!(f.length===1&&m!==void 0&&JSON.stringify(l[m])===JSON.stringify(p))){for(let d of f.toReversed())l.splice(d,1);l.push(p),s=true;}}return s?(mkdirSync(dirname(e),{recursive:true}),writeFileSync(e,`${JSON.stringify(r,null,2)}
5
5
  `),`${n.green("\u2713")} Hooks installed into ${e} (${Object.keys(o).join(" + ")})`):`${n.yellow("Already installed")} in ${e} (current layout)`}function wo(){let e=S(O);return e.ok?Ye(e.data)?`${n.yellow("Skipped")} \u2014 the devcoach plugin already provides the hooks (installing both would double-count interactions)`:le(O,ko()):e.error}function yo(){return ce()?`${n.yellow("Skipped")} \u2014 the devcoach Gemini extension already provides the hooks (installing both would double-count interactions)`:le(T,bo())}function E(e){if(!existsSync(join(e,"SKILL.md")))return "missing";try{if(readFileSync(join(e,".devcoach-version"),"utf8").trim()===f)return "current"}catch{}return "outdated"}function oe(e,o){if(!o&&E(e)==="current")return `${n.yellow("Already installed")} in ${e} (current version)`;mkdirSync(e,{recursive:true}),writeFileSync(join(e,"SKILL.md"),g$1());let t=h();if(t.length){let r=join(e,"references");mkdirSync(r,{recursive:true});for(let i of t)writeFileSync(join(r,i.name),i.content);}return writeFileSync(join(e,".devcoach-version"),`${f}
@@ -30,4 +30,4 @@ ${n.green("Setup complete!")} ${Object.keys(s).length} topics saved.`);}finally{
30
30
  ${n.bold(`devcoach ${n.dim(`v${f}`)}`)}`);for(let[t,r]of e)a(` ${n.cyan(`devcoach ${t}`.padEnd(32))} ${n.dim(r)}`);a(`
31
31
  ${n.dim("Run")} ${n.bold("devcoach <command> --help")} ${n.dim("for per-command options.")}
32
32
  `);let o=ue();o&&a(`${o}
33
- `);}function v(e){return e??null}function Wo(){let e$1=new Command;return e$1.exitOverride(),e$1.name("devcoach").description("devcoach \u2014 progressive technical coaching").version(`devcoach ${f}`,"-v, --version","output the version number").allowExcessArguments(false).showHelpAfterError("(add --help for usage)").action(()=>Jo()),e$1.command("mcp").description("Start the MCP server (stdio transport)").action(async()=>{let{runStdio:o}=await import('./server-4FNXIQ2Q.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};
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-FARS47NV.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-OSA5I6L7.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-QGXVUN7G.js';import'./chunk-ECN4MNGM.js';
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import {f as f$1,e as e$1,d as d$2,h,g as g$1,c as c$1,b as b$2,a as a$1}from'./chunk-TGTNEQU4.js';import {a,c,b as b$1,f,e,m,M,g,n,I,R,p,r,q,s,K,d as d$1,z as z$1,D,A,B,C,Q,j,F,T,U,o,P,w,G,L,k}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/server';import {StdioServerTransport}from'@modelcontextprotocol/server/stdio';import {z}from'zod';var u=o=>({type:"text",text:o}),oe=o=>({content:[u(JSON.stringify(o))]}),l=o=>({isError:true,content:[u(o)]}),b=o=>({content:[u(JSON.stringify(o))],structuredContent:o}),ye=z.object({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$1.nullable(),starred:z.boolean(),feedback:c.nullable()}),_e=ye.extend({reply_check:z.string()}),re="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.",ie=z.object({knowledge:z.array(z.object({topic:z.string(),confidence:z.number().int()})),groups:z.array(z.object({name:z.string(),topics:z.array(z.string())}))}),we=z.object({max_per_day:z.number().int(),min_gap_minutes:z.number().int(),ui_theme:e,nudge_every:z.number().int(),nudge_scope:f}),ke=z.object({window_months:z.number().int(),cutoff:z.string(),candidate_count:z.number().int(),over_soft_limit:z.boolean(),candidates:z.array(z.object({name:z.string(),path:z.string(),last_activity:z.string().nullable(),prompt_count:z.number().int()}))});function d(o,p){return {contents:[{uri:o.href,mimeType:"application/json",text:JSON.stringify(p)}]}}function se(){try{return {...m(o=>Q(o))}}catch(o){return {error:String(o)}}}function ae(){try{let o$1="";try{existsSync(j)&&(o$1=readFileSync(j,"utf8"));}catch{}return {...m(c=>{let e=L(c).knowledge_ready,n=o$1.length>0,r=P(c),i={allowed:r.allowed,total_lessons:o(c)};return r.reason!=null&&(i.reason=r.reason),{onboarding:{knowledge_ready:e,notebook_ready:n,needs_onboarding:!(e&&n)},rate_limit:i,taught_topics:U(c),profile:Q(c)}}),notebook:o$1,notebook_path:j}}catch(o){return {error:String(o)}}}function ce(){try{let p=m(i=>L(i)).knowledge_ready,c=existsSync(j)&&statSync(j).size>0,e=e$1(),n=c$1(),r=b$2(a$1(e.folder??process.cwd()),n.detected_stack);return {knowledge_ready:p,notebook_ready:c,needs_onboarding:!(p&&c),detected_stack:r,detected_projects:n.projects,scanned_projects:n.scanned_projects,default_topics:k,context_ready:e.branch!==null,notebook_path:j}}catch(o){return {error:String(o)}}}var E=(o,p)=>typeof p.error=="string"?l(`${o} failed: ${p.error}`):b(p);function ve(){let o$1=new McpServer({name:"devcoach",version:f$1},{instructions:"Progressive technical coaching server. Use the devcoach_instructions prompt for full coaching behaviour guidelines. Read coaching state with the get_briefing / get_onboarding / get_profile tools; the devcoach:// resources expose the same data for clients that browse resources."});o$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:z.object({id:z.string().describe("Unique lesson id (uuid or random string)"),topic_id:z.string().describe("Primary topic id, e.g. 'python'"),categories:z.array(z.string()).describe("Category tags, e.g. ['python','performance']"),title:z.string().describe("Short lesson title"),level: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$1.nullish().describe("github | gitlab | bitbucket | local (auto-detected if omitted)")}),outputSchema:_e,annotations:{title:"Log Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{let n$1=e$1(),r={};try{r=m(a=>M(a));}catch{r={};}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??n$1.project??r.project??null,repository:e.repository??n$1.repository??r.repository??null,branch:e.branch??n$1.branch??r.branch??null,commit_hash:e.commit_hash??n$1.commit_hash??null,folder:e.folder??n$1.folder??null,repository_platform:e.repository_platform??n$1.repository_platform??r.repository_platform??null});return m(a=>{n(a,i),I(a);}),{content:[u(`Lesson saved. ${re}`),u(JSON.stringify(i))],structuredContent:{...i,reply_check:re}}}catch(n){return l(`log_lesson failed: ${n}`)}}),o$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:z.object({topic:z.string().describe("Topic id, e.g. 'python'"),delta:z.number().int().describe("Signed change to apply, e.g. +1 or -1")}),annotations:{title:"Update Knowledge",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{let n=m(r=>R(r,e.topic,e.delta));return {content:[u(String(n))]}}catch(n){return l(`update_knowledge failed for '${e.topic}': ${n}`)}}),o$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:z.object({period:z.enum(["today","week","month","year","all"]).nullish().describe("Time window"),category:z.string().nullish().describe("Filter by a category tag, e.g. 'python'"),level:z.enum(["junior","mid","senior"]).nullish().describe("Filter by difficulty level"),project:z.string().nullish().describe("Fuzzy match on git project"),repository:z.string().nullish().describe("Fuzzy match on git repository"),branch:z.string().nullish().describe("Fuzzy match on git branch"),commit:z.string().nullish().describe("Fuzzy match on commit hash"),starred:z.boolean().nullish().describe("True to return only starred lessons"),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 n=m(r=>p(r,{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 oe(n)}catch{return oe([])}}),o$1.registerTool("star_lesson",{title:"Star Lesson",description:"Set the starred (favourite) flag on a lesson. Returns true if found and updated. Idempotent.",inputSchema:z.object({lesson_id:z.string().describe("Lesson id"),starred:z.boolean().describe("true to favourite, false to unmark")}),annotations:{title:"Star Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{let n=m(r$1=>r(r$1,e.lesson_id,e.starred));return {content:[u(String(n))]}}catch(n){return l(`star_lesson failed for '${e.lesson_id}': ${n}`)}}),o$1.registerTool("delete_lesson",{title:"Delete Lesson",description:"Permanently delete a lesson by id. Returns true if found and deleted.",inputSchema:z.object({lesson_id:z.string().describe("Lesson id to delete")}),annotations:{title:"Delete Lesson",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let n=m(r=>q(r,e.lesson_id));return {content:[u(String(n))]}}catch(n){return l(`delete_lesson failed for '${e.lesson_id}': ${n}`)}}),o$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:z.object({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 n=e.feedback==="clear"?null:e.feedback,r=m(i=>{let a=i.prepare("SELECT feedback, topic_id FROM lessons WHERE id = ?").get(e.lesson_id);return a?(a.feedback===n||(s(i,e.lesson_id,n),(n==="know"||n==="dont_know")&&a.topic_id&&R(i,a.topic_id,n==="know"?1:-1)),!0):!1});return {content:[u(String(r))]}}catch(n){return l(`submit_feedback failed for '${e.lesson_id}': ${n}`)}}),o$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:z.object({reason:z.string().min(1).describe("One line: why no lesson was warranted (shown in `devcoach doctor`)")}),annotations:{title:"Skip Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{return m(n=>K(n,e.reason)),{content:[u("Cue skipped \u2014 pacing re-armed. Output nothing to the user.")]}}catch(n){return l(`skip_lesson failed: ${n}`)}}),o$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:z.object({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(n=>{z$1(n,e.topic,e.confidence),e.group&&e.group!=="Other"&&D(n,e.topic,e.group);}),{content:[u("true")]}}catch(n){return l(`add_topic failed for '${e.topic}': ${n}`)}}),o$1.registerTool("remove_topic",{title:"Remove Topic",description:"Remove a topic from the knowledge map entirely. Returns true if it existed.",inputSchema:z.object({topic:z.string().describe("Topic id to remove")}),annotations:{title:"Remove Topic",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let n=m(r=>A(r,e.topic));return {content:[u(String(n))]}}catch(n){return l(`remove_topic failed for '${e.topic}': ${n}`)}}),o$1.registerTool("add_group",{title:"Add Group",description:"Create a new (initially empty) knowledge group. Idempotent \u2014 returns true either way.",inputSchema:z.object({name:z.string().describe("Group name, e.g. 'Machine Learning'")}),annotations:{title:"Add Group",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{return m(n=>B(n,e.name.trim())),{content:[u("true")]}}catch(n){return l(`add_group failed for '${e.name}': ${n}`)}}),o$1.registerTool("remove_group",{title:"Remove Group",description:"Delete a knowledge group. Its topics move to Other. Returns true if it existed.",inputSchema:z.object({name:z.string().describe("Group name to delete")}),annotations:{title:"Remove Group",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let n=m(r=>C(r,e.name));return {content:[u(String(n))]}}catch(n){return l(`remove_group failed for '${e.name}': ${n}`)}}),o$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:z.object({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:we,annotations:{title:"Update Settings",destructiveHint:false,idempotentHint:true,openWorldHint:false}},e=>{let n=i=>b(m(a=>(G(a,e.key,i),F(a))));if(e.key==="nudge_scope")return e.value!=="session"&&e.value!=="global"?l("nudge_scope must be 'session' or 'global'"):n(e.value);let r=Number.parseInt(e.value,10);return Number.isNaN(r)?l(`Value must be an integer, got '${e.value}'`):e.key==="max_per_day"&&!(r>=1&&r<=20)?l("max_per_day must be between 1 and 20"):e.key==="min_gap_minutes"&&!(r>=0&&r<=1440)?l("min_gap_minutes must be between 0 and 1440"):e.key==="nudge_every"&&!(r>=0&&r<=1e3)?l("nudge_every must be between 0 and 1000"):n(String(r))}),o$1.registerTool("open_ui",{title:"Open UI",description:"Launch the devcoach web dashboard in the background. port must be 1024-65535.",inputSchema:z.object({port:z.number().int().default(7860).describe("Port (default 7860)")}),annotations:{title:"Open UI",destructiveHint:false,idempotentHint:false,openWorldHint:true}},e=>{let n=e.port;return n>=1024&&n<=65535?(spawn(process.execPath,[process.argv[1]??"","ui","--port",String(n)],{detached:true,stdio:"ignore"}).unref(),{content:[u(`devcoach UI starting at http://localhost:${n}`)]}):{content:[u(`error: port ${n} is out of valid range (1024-65535)`)]}}),o$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:z.object({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:ie,annotations:{title:"Complete Onboarding",destructiveHint:true,idempotentHint:false,openWorldHint:false}},async e=>{try{let n=m(r=>{r.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[i,a]of Object.entries(e.topics))z$1(r,i,Math.max(0,Math.min(10,a)));if(e.groups)for(let[i,a]of Object.entries(e.groups))for(let N of a)N in e.topics&&D(r,N,i);return Q(r)});return mkdirSync(dirname(j),{recursive:!0}),(!existsSync(j)||statSync(j).size===0)&&writeFileSync(j,`# devcoach \u2014 Coaching Notebook
3
+ `),b(n)}catch(n){return l(`complete_onboarding failed: ${n}`)}}),o$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:z.object({months:z.number().int().min(1).max(24).default(3).describe("Rolling window size in months, counting back from now")}),outputSchema:ke,annotations:{title:"Preview Deep Scan",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}},e=>{try{return b({...d$2(e.months)})}catch(n){return l(`preview_deep_scan failed: ${n}`)}});let p$1=e=>({title:e,readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false});o$1.registerTool("get_briefing",{title:"Lesson Briefing",description:"Everything needed before delivering a lesson, in ONE call: onboarding status, rate limit, taught topics, knowledge profile, the coaching notebook text and its notebook_path. Same data as the devcoach://briefing resource \u2014 call the tool, it needs no server name.",inputSchema:z.object({}),annotations:p$1("Lesson Briefing")},()=>E("get_briefing",ae())),o$1.registerTool("get_onboarding",{title:"Onboarding Status & Detected Stack",description:"Onboarding status (knowledge_ready, notebook_ready, needs_onboarding), the stack detected across the full Claude Code history with per-project provenance, the default topics and the notebook_path. Same data as the devcoach://onboarding resource.",inputSchema:z.object({}),annotations:p$1("Onboarding Status & Detected Stack")},()=>E("get_onboarding",ce())),o$1.registerTool("get_profile",{title:"Knowledge Profile",description:"Current knowledge map: topics with confidence 0\u201310 and their groups. Same data as the devcoach://profile resource.",inputSchema:z.object({}),outputSchema:ie,annotations:p$1("Knowledge Profile")},()=>E("get_profile",se()));let c=(e,n)=>({title:e,description:n,mimeType:"application/json"});return o$1.registerResource("profile","devcoach://profile",c("Knowledge Profile","Current knowledge map \u2014 topics, confidence scores, and groups."),e=>d(e,se())),o$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 n="";try{existsSync(j)&&(n=readFileSync(j,"utf8"));}catch{}return {contents:[{uri:e.href,mimeType:"text/markdown",text:n}]}}),o$1.registerResource("settings","devcoach://settings",c("Settings","Current coaching settings (rate limits, UI theme)."),e=>{try{return d(e,m(n=>F(n)))}catch(n){return d(e,{error:String(n)})}}),o$1.registerResource("recent-lessons","devcoach://lessons/recent",c("Recent Lessons","Last 10 lessons from the current week."),e=>{try{return d(e,m(n=>p(n,{period:"week",page:1,per_page:10})))}catch(n){return d(e,[{error:String(n)}])}}),o$1.registerResource("stats","devcoach://stats",c("Stats","Aggregate coaching statistics: counts and weakest/strongest topics."),e=>{try{return d(e,m(n=>T(n)))}catch(n){return d(e,{error:String(n)})}}),o$1.registerResource("taught-topics","devcoach://taught-topics",c("Taught Topics","All topic_ids already taught \u2014 read before selecting a new lesson topic."),e=>{try{return d(e,m(n=>U(n)))}catch{return d(e,[])}}),o$1.registerResource("rate-limit","devcoach://rate-limit",c("Rate Limit","Current rate-limit status and running lesson total."),e=>{try{let{r:n,total:r}=m(a=>({r:P(a),total:o(a)})),i={allowed:n.allowed,total_lessons:r};return n.reason!=null&&(i.reason=n.reason),d(e,i)}catch(n){return d(e,{allowed:false,reason:`Rate limit check unavailable: ${n}`})}}),o$1.registerResource("briefing","devcoach://briefing",c("Lesson Briefing","Everything needed before delivering a lesson in ONE read: onboarding status, rate limit, taught topics, knowledge profile, and the coaching notebook."),e=>d(e,ae())),o$1.registerResource("context","devcoach://context",c("Workspace Context","Auto-detected git context and most-used lesson metadata defaults."),e=>{try{let n=e$1(),r=m(i=>M(i));return d(e,{git:n,usage_defaults:r})}catch(n){return d(e,{error:String(n)})}}),o$1.registerResource("onboarding","devcoach://onboarding",c("Onboarding","Onboarding status, the stack detected across the full Claude Code history (with per-project provenance), and project topic defaults."),e=>d(e,ce())),o$1.registerResource("lesson",new ResourceTemplate("devcoach://lessons/{lesson_id}",{list:void 0}),c("Lesson","A single lesson by id."),(e,n)=>{try{let r=String(n.lesson_id),i=m(a=>w(a,r));return i?d(e,i):d(e,{error:`Lesson '${r}' not found`})}catch(r){return d(e,{error:String(r)})}}),o$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(n=>`
4
+
5
+ ---
6
+
7
+ <!-- reference: ${n.name} -->
8
+
9
+ ${n.content}`).join("");return {messages:[{role:"user",content:{type:"text",text:g$1()+e}}]}}),o$1}async function $e(){let o=ve(),p=new StdioServerTransport;await o.connect(p);}export{ve as createServer,$e as runStdio};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devcoach",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
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",
@@ -63,13 +63,13 @@
63
63
  "zod": "^4.4.3"
64
64
  },
65
65
  "devDependencies": {
66
- "@biomejs/biome": "^2.5.10",
66
+ "@biomejs/biome": "^2.5.12",
67
67
  "@modelcontextprotocol/client": "^2.0.0",
68
- "@types/node": "^26.3.0",
69
- "@vitest/coverage-v8": "^4.1.11",
68
+ "@types/node": "^26.4.1",
69
+ "@vitest/coverage-v8": "^5.0.0",
70
70
  "tsup": "^8.5.1",
71
71
  "tsx": "^4.23.12",
72
72
  "typescript": "^7.0.2",
73
- "vitest": "^4.1.11"
73
+ "vitest": "^5.0.0"
74
74
  }
75
75
  }
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env node
2
- import {i,m as m$1,L,W,h as h$1,l,V}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 A(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 N={claude:"block",gemini:"deny",codex:"block"};function h(e,o,t){let n={decision:N[e],reason:o};t&&(n.systemMessage=t),process.stdout.write(`${JSON.stringify(n)}
4
- `),process.exit(0);}var f="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=s()){if(A(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",f);}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 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
-
6
- ${v[o].cue} NOW and follow it exactly \u2014 the skill is the single source of truth for the entire flow, including the explicit no-op when the completed work does not warrant a lesson.
7
- ${i}
8
-
9
- 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; call log_lesson silently (body = 3\u20136 short paragraphs tied to the task plus a \`\u{1F4A1} *Senior tip:*\` line, clean markdown without bands or a title line); THEN write the card as the FINAL message of the turn: 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]\`, the body you passed, and the tip \u2014 saving is not showing; the card in the final message is the only thing the user sees. If instead the work does not warrant a lesson (pure questions, chat, nothing technical), call the devcoach \`skip_lesson\` tool with a one-line reason.
10
-
11
- 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 I(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(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(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,f);break;case "cue":h(t.client,E(n.nextLessonNumber,t.client),"\u{1F393} devcoach: checking whether a lesson is due\u2026");break;}process.exit(0);}function T(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,f)),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:
12
- - If this task is technical: ${v[e].prime} and follow it exactly to close your reply \u2014 the skill is the single source of truth for the entire lesson flow.
13
- - If it is not technical: silently call the devcoach \`skip_lesson\` tool with a one-line reason and output nothing about it.
14
- - 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)}})}
15
- `),process.exit(0);}function U(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":$();break;case "lesson-ready":T();break;default:process.exit(0);}}export{D as a,$ as b,E as c,T as d,k as e,R as f,m as g,U as h};
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env node
2
- import {f as f$1,e as e$1,d as d$2,c as c$1,b as b$1,a as a$2,h,g as g$1}from'./chunk-ZCSBBJ3A.js';import {a as a$1,c,b,f,e,m,M,g,n,I,R,p,r,q,s,K,d as d$1,z as z$1,D,A,B,C,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/server';import {StdioServerTransport}from'@modelcontextprotocol/server/stdio';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)]}),N=r=>({content:[l(JSON.stringify(r))],structuredContent:r}),me=z.object({id:z.string(),timestamp:z.string(),topic_id:z.string(),categories:z.array(z.string()),title:z.string(),level:a$1,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.nullable()}),ge=me.extend({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=z.object({knowledge:z.array(z.object({topic:z.string(),confidence:z.number().int()})),groups:z.array(z.object({name:z.string(),topics:z.array(z.string())}))}),fe=z.object({max_per_day:z.number().int(),min_gap_minutes:z.number().int(),ui_theme:e,nudge_every:z.number().int(),nudge_scope:f}),ye=z.object({window_months:z.number().int(),cutoff:z.string(),candidate_count:z.number().int(),over_soft_limit:z.boolean(),candidates:z.array(z.object({name:z.string(),path:z.string(),last_activity:z.string().nullable(),prompt_count:z.number().int()}))});function a(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$1},{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:z.object({id:z.string().describe("Unique lesson id (uuid or random string)"),topic_id:z.string().describe("Primary topic id, e.g. 'python'"),categories:z.array(z.string()).describe("Category tags, e.g. ['python','performance']"),title:z.string().describe("Short lesson title"),level:a$1.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=e$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});return m(s=>{n(s,i),I(s);}),{content:[l(`Lesson saved. ${se}`),l(JSON.stringify(i))],structuredContent:{...i,reply_check:se}}}catch(t){return d(`log_lesson failed: ${t}`)}}),r$1.registerTool("update_knowledge",{title:"Update Knowledge",description:"Adjust the confidence score for a topic by delta (e.g. +1 or -1). Returns the new confidence (0-10). Creates the topic at confidence 5 if it does not exist.",inputSchema:z.object({topic:z.string().describe("Topic id, e.g. 'python'"),delta:z.number().int().describe("Signed change to apply, e.g. +1 or -1")}),annotations:{title:"Update Knowledge",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async 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:z.object({period:z.enum(["today","week","month","year","all"]).nullish().describe("Time window"),category:z.string().nullish().describe("Filter by a category tag, e.g. 'python'"),level:z.enum(["junior","mid","senior"]).nullish().describe("Filter by difficulty level"),project:z.string().nullish().describe("Fuzzy match on git project"),repository:z.string().nullish().describe("Fuzzy match on git repository"),branch:z.string().nullish().describe("Fuzzy match on git branch"),commit:z.string().nullish().describe("Fuzzy match on commit hash"),starred:z.boolean().nullish().describe("True to return only starred lessons"),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:z.object({lesson_id:z.string().describe("Lesson id"),starred:z.boolean().describe("true to favourite, false to unmark")}),annotations:{title:"Star Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async 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:z.object({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:z.object({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:z.object({reason:z.string().min(1).describe("One line: why no lesson was warranted (shown in `devcoach doctor`)")}),annotations:{title:"Skip Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async 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:z.object({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:z.object({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:z.object({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:z.object({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:z.object({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=>N(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:z.object({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:z.object({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
- `),N(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:z.object({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 N({...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 a(e,m(t=>Q(t)))}catch(t){return a(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 a(e,m(t=>F(t)))}catch(t){return a(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 a(e,m(t=>p(t,{period:"week",page:1,per_page:10})))}catch(t){return a(e,[{error:String(t)}])}}),r$1.registerResource("stats","devcoach://stats",u("Stats","Aggregate coaching statistics: counts and weakest/strongest topics."),e=>{try{return a(e,m(t=>T(t)))}catch(t){return a(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 a(e,m(t=>U(t)))}catch{return a(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),a(e,i)}catch(t){return a(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 a(e,{...o$1,notebook:t,notebook_path:j})}catch(t){return a(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=e$1(),o=m(i=>M(i));return a(e,{git:t,usage_defaults:o})}catch(t){return a(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(g=>L(g)).knowledge_ready,i=existsSync(j)&&statSync(j).size>0,s=e$1(),h=c$1(),m$1=b$1(a$2(s.folder??process.cwd()),h.detected_stack);return a(e,{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 a(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?a(e,i):a(e,{error:`Lesson '${o}' not found`})}catch(o){return a(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 Oe(){let r=_e(),u=new StdioServerTransport;await r.connect(u);}export{_e as createServer,Oe as runStdio};