devcoach 0.4.3 → 0.4.4
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 +52 -40
- package/dist/bin.js +47 -40
- package/package.json +1 -1
package/assets/SKILL.md
CHANGED
|
@@ -22,14 +22,6 @@ by teaching one thing at a time, at the right moment, based on what they actuall
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
## Plan-mode suppression
|
|
26
|
-
|
|
27
|
-
If you are currently in **plan mode** (plan mode is active and `ExitPlanMode` has not
|
|
28
|
-
yet been called in this session), **exit this skill immediately** — do not read any
|
|
29
|
-
devcoach MCP resources, do not run onboarding, do not deliver or log any lesson.
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
25
|
## Onboarding flow
|
|
34
26
|
|
|
35
27
|
The onboarding flow runs inline, immediately before delivering a lesson, whenever
|
|
@@ -94,15 +86,17 @@ Once the full topic list is agreed:
|
|
|
94
86
|
These names emerge from the conversation — there is no fixed list.
|
|
95
87
|
- Show the proposed grouping: *"Here's how I'd organise these — does this look
|
|
96
88
|
right? Any changes?"*
|
|
97
|
-
- When confirmed, call the MCP tool `complete_onboarding
|
|
89
|
+
- When confirmed, call the MCP tool `complete_onboarding`, including the personalized
|
|
90
|
+
`notebook` markdown you compose in Step 4 (so the profile and notebook are saved
|
|
91
|
+
together):
|
|
98
92
|
```json
|
|
99
93
|
{
|
|
100
94
|
"topics": { "lang_a": 7, "tool_b": 8, "practice_c": 7 },
|
|
101
|
-
"groups": { "Languages": ["lang_a"], "DevOps": ["tool_b"], "Version Control": ["practice_c"] }
|
|
95
|
+
"groups": { "Languages": ["lang_a"], "DevOps": ["tool_b"], "Version Control": ["practice_c"] },
|
|
96
|
+
"notebook": "# devcoach — Coaching Notebook\n_Last updated: …_\n\n## Observations\n…"
|
|
102
97
|
}
|
|
103
98
|
```
|
|
104
|
-
-
|
|
105
|
-
starts after onboarding.
|
|
99
|
+
- Do not deliver a lesson in this turn — the rate-limit clock starts after onboarding.
|
|
106
100
|
|
|
107
101
|
**Rule:** Never ask about groups during topic collection. Propose them only in
|
|
108
102
|
Step 3 after all topics are known.
|
|
@@ -121,30 +115,39 @@ Then tell the user **how to change any of it later**, across all three surfaces:
|
|
|
121
115
|
- **UI** — `devcoach ui` (web dashboard at http://localhost:7860), or ask me to open
|
|
122
116
|
it (the `open_ui` tool).
|
|
123
117
|
|
|
124
|
-
### Step 4 —
|
|
118
|
+
### Step 4 — Compose the coaching notebook (pass it to `complete_onboarding`)
|
|
125
119
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
120
|
+
Do **not** write `learning-state.md` yourself — compose its full markdown and pass it
|
|
121
|
+
as the `notebook` field of the `complete_onboarding` call in Step 3. The tool saves it
|
|
122
|
+
to `~/.devcoach/learning-state.md` atomically with the profile (so it is never empty
|
|
123
|
+
and never created before the user finishes).
|
|
124
|
+
|
|
125
|
+
Make it **personalized**: draw on everything you know about *this* user — not only this
|
|
126
|
+
onboarding conversation, but how they work across **all their projects** (languages and
|
|
127
|
+
tools they reach for, recurring habits, strengths, and gaps you have observed elsewhere).
|
|
128
|
+
Generic boilerplate is a failure; write specific, real notes. Use this structure:
|
|
129
129
|
|
|
130
130
|
```markdown
|
|
131
131
|
# devcoach — Coaching Notebook
|
|
132
132
|
_Last updated: [ISO timestamp]_
|
|
133
133
|
|
|
134
134
|
## Observations
|
|
135
|
-
[
|
|
136
|
-
|
|
135
|
+
[Specific, real notes about this user: background, how they work across their
|
|
136
|
+
projects, confidence style, gaps. Personalized — never generic.]
|
|
137
137
|
|
|
138
138
|
## Recurring patterns
|
|
139
|
-
[
|
|
139
|
+
[Patterns you have noticed in how they build, debug, or structure work.]
|
|
140
140
|
|
|
141
141
|
## Recommended focus
|
|
142
142
|
[Topics the user flagged as priorities or areas of uncertainty.]
|
|
143
143
|
|
|
144
144
|
## Open hypotheses
|
|
145
|
-
[
|
|
145
|
+
[Things to watch for and confirm in future sessions.]
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
+
If re-onboarding or returning, fold prior notes into the markdown you pass —
|
|
149
|
+
`complete_onboarding` overwrites the file with exactly what you provide.
|
|
150
|
+
|
|
148
151
|
---
|
|
149
152
|
|
|
150
153
|
## Session context
|
|
@@ -342,21 +345,17 @@ it is central to the task and no applicable profile topic exists.
|
|
|
342
345
|
|
|
343
346
|
## 4. Lesson format
|
|
344
347
|
|
|
345
|
-
Append the lesson **at the bottom of the response**, rendered as a card: a titled
|
|
346
|
-
|
|
347
|
-
topic and level.
|
|
348
|
+
Append the lesson **at the bottom of the response**, rendered as a card: a titled top
|
|
349
|
+
band, the lesson as **plain markdown**, and a bottom band echoing the topic and level.
|
|
348
350
|
|
|
349
351
|
```
|
|
350
352
|
### ──────── 🎓 devcoach ────────
|
|
351
|
-
|
|
352
|
-
>
|
|
353
|
-
> **[Lesson title]**
|
|
354
|
-
>
|
|
355
|
-
> [Body: 3–6 paragraphs. Concise, practical, with a code example if useful.
|
|
356
|
-
> Explain the WHY, not just the what. Connect it to the task just completed.]
|
|
357
|
-
>
|
|
358
|
-
> 💡 *Senior tip:* [One sentence a senior would say to a junior on this topic]
|
|
353
|
+
**[Lesson title]** · [Category] · [Junior|Mid|Senior]
|
|
359
354
|
|
|
355
|
+
[Body: 3–6 paragraphs. Concise, practical, with a code example if useful.
|
|
356
|
+
Explain the WHY, not just the what. Connect it to the task just completed.]
|
|
357
|
+
|
|
358
|
+
💡 *Senior tip:* [One sentence a senior would say to a junior on this topic]
|
|
360
359
|
### ──────── [topic] · [level] ────────
|
|
361
360
|
```
|
|
362
361
|
|
|
@@ -365,16 +364,16 @@ Card rules:
|
|
|
365
364
|
U+2500) around a centered title — the heading makes Claude Code colour the whole
|
|
366
365
|
line. Do **not** use `---` (a real rule can't carry a title). Aim for a band
|
|
367
366
|
width of ~34 characters and keep the top and bottom bands the same width.
|
|
368
|
-
-
|
|
369
|
-
-
|
|
367
|
+
- The body is **plain markdown** between the bands — do **not** prefix lines with `> `
|
|
368
|
+
(a per-line blockquote breaks on multi-paragraph bodies and fenced code blocks).
|
|
370
369
|
|
|
371
|
-
|
|
372
|
-
|
|
370
|
+
**Display vs storage:** the card above is for chat only. When you call `log_lesson`,
|
|
371
|
+
the `body` is **clean markdown** — no bands, no `> ` blockquote, and no title/`Category ·
|
|
372
|
+
Level` line (those go in their own fields). See Step 1 below.
|
|
373
373
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
- ⏭️ skip
|
|
374
|
+
**Feedback is handled by the MCP protocol** — `log_lesson` runs an elicitation
|
|
375
|
+
(*"Did that land?"* → know / dont_know / skip) when the client supports it. Do **not**
|
|
376
|
+
render a feedback question yourself; just deliver the card and call `log_lesson`.
|
|
378
377
|
|
|
379
378
|
**Tone:** direct, like a senior colleague explaining during a code review.
|
|
380
379
|
Not academic, not verbose. Gets straight to the point.
|
|
@@ -396,7 +395,7 @@ Call `log_lesson` right after delivering the lesson, without waiting for feedbac
|
|
|
396
395
|
"title": "Lesson title",
|
|
397
396
|
"level": "junior|mid|senior",
|
|
398
397
|
"summary": "One line — what was taught",
|
|
399
|
-
"body": "
|
|
398
|
+
"body": "ONLY the lesson prose + the 💡 Senior tip, as CLEAN markdown — no card bands, no '>' blockquote, and do NOT repeat the title or the 'Category · Level' line (those are their own fields).",
|
|
400
399
|
"task_context": "Brief description of the task that triggered it"
|
|
401
400
|
}
|
|
402
401
|
```
|
|
@@ -435,6 +434,19 @@ Did that land?
|
|
|
435
434
|
|
|
436
435
|
---
|
|
437
436
|
|
|
437
|
+
### Step 1c — Update the coaching notebook (only at every-10-lesson checkpoints)
|
|
438
|
+
|
|
439
|
+
The notebook's observations are refreshed only at **checkpoints — every 10 delivered
|
|
440
|
+
lessons**. The `lesson-ready` hook owns the count and tells you whether the current lesson
|
|
441
|
+
is a checkpoint; do not count yourself. **At a checkpoint**, after feedback is known, call
|
|
442
|
+
`update_notebook` with the full revised `learning-state.md` markdown — note what the user
|
|
443
|
+
absorbed (`know`) or should revisit (`dont_know`), any new recurring pattern, and updated
|
|
444
|
+
open hypotheses, keep the prior notes, and record `(updated after N lessons)`. **Between
|
|
445
|
+
checkpoints, do not touch the notebook.** This keeps `devcoach://notebook` driving future
|
|
446
|
+
lesson selection without churning it on every lesson.
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
438
450
|
### Step 2 — Record feedback and conditionally adjust confidence (fallback only)
|
|
439
451
|
|
|
440
452
|
When `log_lesson` returned `feedback: null` (elicitation declined or not supported),
|
package/dist/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {spawn,spawnSync,execFileSync}from'child_process';import {existsSync,writeFileSync,readFileSync,mkdirSync,statSync,fstatSync,accessSync,constants,readdirSync}from'fs';import {homedir,platform}from'os';import {join,dirname,extname,basename}from'path';import {createInterface}from'readline/promises';import {Command}from'commander';import {createRequire}from'module';import {unzipSync,strToU8,zipSync,strFromU8}from'fflate';import {z as z$1}from'zod';import {McpServer,ResourceTemplate}from'@modelcontextprotocol/sdk/server/mcp.js';import {StdioServerTransport}from'@modelcontextprotocol/sdk/server/stdio.js';import {fileURLToPath}from'url';import {serve}from'@hono/node-server';import {Hono}from'hono';import {html,raw}from'hono/html';import {styleText}from'util';var fe=z$1.enum(["junior","mid","senior"]),be=z$1.enum(["github","gitlab","bitbucket","local"]),
|
|
2
|
+
import {spawn,spawnSync,execFileSync}from'child_process';import {existsSync,writeFileSync,readFileSync,mkdirSync,statSync,fstatSync,accessSync,constants,readdirSync}from'fs';import {homedir,platform}from'os';import {join,dirname,extname,basename}from'path';import {createInterface}from'readline/promises';import {Command}from'commander';import {createRequire}from'module';import {unzipSync,strToU8,zipSync,strFromU8}from'fflate';import {z as z$1}from'zod';import {McpServer,ResourceTemplate}from'@modelcontextprotocol/sdk/server/mcp.js';import {StdioServerTransport}from'@modelcontextprotocol/sdk/server/stdio.js';import {fileURLToPath}from'url';import {serve}from'@hono/node-server';import {Hono}from'hono';import {html,raw}from'hono/html';import {styleText}from'util';var fe=z$1.enum(["junior","mid","senior"]),be=z$1.enum(["github","gitlab","bitbucket","local"]),Le=z$1.enum(["know","dont_know"]),Oe=z$1.number().int().min(0).max(10);function Zt(t){return t.toISOString().replace(/\.\d{3}Z$/,"Z")}function Qt(t){if(t instanceof Date){if(Number.isNaN(t.getTime()))throw new TypeError("Invalid Date");return t}let e=t.trim();/[zZ]$|[+-]\d{2}:?\d{2}$/.test(e)||(e=e.includes("T")||e.includes(" ")?`${e.replace(" ","T")}Z`:`${e}T00:00:00Z`);let r=new Date(e);if(Number.isNaN(r.getTime()))throw new TypeError(`Cannot parse timestamp ${JSON.stringify(t)} \u2014 expected ISO 8601`);return r}function eo(t){let e=Qt(t),o=new Date;return e.getTime()>o.getTime()&&(e=o),Zt(e)}var to=z$1.union([z$1.string(),z$1.date()]).transform((t,e)=>{try{return eo(t)}catch(o){return e.addIssue({code:"custom",message:o.message}),z$1.NEVER}}),U=z$1.string().nullish().transform(t=>t??null),oo=z$1.object({id:z$1.string(),timestamp:to,topic_id:z$1.string(),categories:z$1.array(z$1.string()),title:z$1.string(),level:fe,summary:z$1.string(),body:U,task_context:U,project:U,repository:U,branch:U,commit_hash:U,folder:U,repository_platform:be.nullish().transform(t=>t??null),starred:z$1.boolean().default(false),feedback:Le.nullish().transform(t=>t??null)}),ro=z$1.object({topic:z$1.string(),confidence:z$1.number().int()}),no=z$1.object({name:z$1.string(),topics:z$1.array(z$1.string())});z$1.object({knowledge:z$1.array(ro),groups:z$1.array(no)});var Ie=z$1.enum(["system","dark","light"]);z$1.object({max_per_day:z$1.number().int().default(2),min_gap_minutes:z$1.number().int().default(240),ui_theme:Ie.default("system")});function ne(t){return oo.parse(t)}var{DatabaseSync:uo}=createRequire(import.meta.url)("node:sqlite"),at=join(homedir(),".devcoach"),Y=join(at,"coaching.db"),w=join(at,"learning-state.md"),Ne="settings.json",De="lessons.json",je="knowledge.json",Ae="learning-state.md",dt={engineering:8,architecture:8,patterns:7,debugging:8,node:7,javascript:7,typescript:6,python:4,django:3,fastapi:4,docker:8,traefik:7,coolify:7,postgresql:6,redis:6,git:7,ci_cd:6,security:5,performance:6,testing:5,linux:7,networking:6,react:5,frontend:5},mo={max_per_day:"2",min_gap_minutes:"240",ui_theme:"system"};function j(t,e,...o){return t.prepare(e).all(...o)}function G(t,e,...o){return t.prepare(e).get(...o)}function S(t,e,...o){return Number(t.prepare(e).run(...o).changes)}function fo(t=Y){return t!==":memory:"&&mkdirSync(dirname(t),{recursive:true}),new uo(t)}function bo(t=Y){let e=fo(t);return yo(e),e}function m(t,e=Y){let o=bo(e);try{return t(o)}finally{o.close();}}function yo(t){t.exec(`
|
|
3
3
|
CREATE TABLE IF NOT EXISTS lessons (
|
|
4
4
|
id TEXT PRIMARY KEY,
|
|
5
5
|
timestamp TEXT NOT NULL,
|
|
@@ -45,8 +45,13 @@ import {spawn,spawnSync,execFileSync}from'child_process';import {existsSync,writ
|
|
|
45
45
|
CREATE INDEX IF NOT EXISTS idx_lessons_starred_ts ON lessons (starred, timestamp);
|
|
46
46
|
CREATE INDEX IF NOT EXISTS idx_lessons_feedback ON lessons (feedback);
|
|
47
47
|
CREATE INDEX IF NOT EXISTS idx_lessons_topic_id ON lessons (topic_id);
|
|
48
|
-
`),
|
|
49
|
-
ON CONFLICT(topic) DO UPDATE SET confidence = excluded.confidence, updated_at = excluded.updated_at`,e,r,n);}function J(t,e){let o=S(t,"DELETE FROM knowledge WHERE topic = ?",e);return S(t,"DELETE FROM knowledge_groups WHERE topic = ?",e),o>0}function P(t,e){let o=e.trim();if(!o)throw new Error("Group name must not be empty");return S(t,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",o)>0}function V(t,e){return S(t,"DELETE FROM knowledge_groups WHERE group_name = ?",e),S(t,"DELETE FROM knowledge_group_names WHERE group_name = ?",e)>0}function D(t,e,o){S(t,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",o),S(t,"DELETE FROM knowledge_groups WHERE topic = ?",e),S(t,"INSERT OR IGNORE INTO knowledge_groups (group_name, topic) VALUES (?, ?)",o,e);}function ve(t,e){S(t,"DELETE FROM knowledge_groups WHERE topic = ?",e);}function E(t){let e={};for(let d of j(t,"SELECT key, value FROM settings"))e[d.key]=d.value;let o=e.min_gap_minutes,r=e.min_hours_between,n;o!==void 0?n=Number.parseInt(o,10):r!==void 0?n=Number.parseInt(r,10)*60:n=240;let s=e.max_per_day,a=e.ui_theme??"system",l=a==="dark"||a==="light"||a==="system"?a:"system";return {max_per_day:s!==void 0?Number.parseInt(s,10):2,min_gap_minutes:n,ui_theme:l}}function C(t,e,o){S(t,"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",e,o);}function de(t){let e=G(t,"SELECT COUNT(*) AS n FROM knowledge");return {knowledge_ready:Number(e?.n??0)>0}}function Ue(t){let e={};for(let o of ["project","repository","branch","repository_platform"]){let r=G(t,`SELECT ${o} AS v, COUNT(*) AS c FROM lessons WHERE ${o} IS NOT NULL GROUP BY ${o} ORDER BY c DESC LIMIT 1`);e[o]=r?r.v:null;}return e}function we(t){let e=E(t),o=se(t),r=ae(t),n=He(t),s=new Map;for(let d of n)for(let f of d.topics)s.set(f,d.name);let a={groups:n.map(d=>d.name),topics:r.map(d=>({topic:d.topic,confidence:d.confidence,group:s.get(d.topic)??null}))},l={[Oe]:strToU8(JSON.stringify(e,null,2)),[Ne]:strToU8(JSON.stringify(o,null,2)),[De]:strToU8(JSON.stringify(a,null,2))};return existsSync(w)&&(l[je]=strToU8(readFileSync(w,"utf8"))),zipSync(l)}function ko(t,e,o){if(!(Oe in e))return;let r=JSON.parse(strFromU8(e[Oe]));"max_per_day"in r&&C(t,"max_per_day",String(r.max_per_day)),"min_gap_minutes"in r&&C(t,"min_gap_minutes",String(r.min_gap_minutes)),(r.ui_theme==="system"||r.ui_theme==="dark"||r.ui_theme==="light")&&C(t,"ui_theme",r.ui_theme),o.settings=1;}function vo(t,e,o){if(!(De in e))return;let r=JSON.parse(strFromU8(e[De]));if(!(r&&typeof r=="object"&&Array.isArray(r.topics))){for(let[s,a]of Object.entries(r))O(t,s,a);o.topics=Object.keys(r).length;return}let n=0;for(let s of r.groups??[])P(t,s)&&(n+=1);for(let s of r.topics){O(t,s.topic,s.confidence);let a=s.group;a&&a!=="Other"&&(P(t,a)&&(n+=1),D(t,s.topic,a));}o.topics=r.topics.length,o.groups=n;}function wo(t,e,o){if(!(Ne in e))return;let r=JSON.parse(strFromU8(e[Ne])),{inserted:n,duplicated:s,invalid:a}=Me(t,r);o.lessons=n,o.skipped=s,o.invalid=a;}function _o(t,e){je in t&&(mkdirSync(dirname(w),{recursive:true}),writeFileSync(w,strFromU8(t[je]),"utf8"),e.learning_state=1);}function ce(t,e){let o={settings:0,topics:0,groups:0,lessons:0,skipped:0,invalid:0,learning_state:0},r=unzipSync(e);return ko(t,r,o),vo(t,r,o),wo(t,r,o),_o(r,o),o}function $o(t){let e=new Date,o;if(t==="today")o=new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),0,0,0,0));else if(t==="week")o=new Date(e.getTime()-7*864e5);else if(t==="month")o=new Date(e.getTime()-30*864e5);else if(t==="year")o=new Date(e.getTime()-365*864e5);else return null;return o.toISOString()}function Ge(t){let e=[];try{let o=JSON.parse(t.categories);Array.isArray(o)&&(e=o);}catch{e=[];}return re({...t,categories:e,starred:!!t.starred})}function z(t){try{let e=E(t),o=new Date,r=new Date(o.getTime()-24*36e5).toISOString(),n=ke(t,r);if(n>=e.max_per_day)return {allowed:!1,reason:`Daily limit reached (${n}/${e.max_per_day} lessons in the last 24h)`};let s=ft(t);if(s!=null){let a=new Date(s),l=(o.getTime()-a.getTime())/6e4;if(l<e.min_gap_minutes){let d=e.min_gap_minutes-l,f=Math.floor(e.min_gap_minutes/60),b=e.min_gap_minutes%60,y=Math.trunc(d),k=Math.floor(y/60),v=y%60;return {allowed:!1,reason:`Too soon: last lesson ${l<0?`in ${Math.round(-l)}m (future timestamp)`:`${Math.round(l)}m ago`}, minimum interval is ${f}h ${b}m (${k}h ${v}m remaining)`}}}return {allowed:!0}}catch(e){return {allowed:true,reason:`Rate limit check failed: ${e}`}}}function M(t){try{return {knowledge:ae(t),groups:He(t)}}catch{return {knowledge:[],groups:[]}}}function q(t,e,o){let r=Fe(t)[e]??5,n=Math.max(0,Math.min(10,r+o));return O(t,e,n),n}function _e(t,e,o){let r=ne(t,e,o);return r&&(o==="know"||o==="dont_know")&&q(t,r,o==="know"?1:-1),r}function Z(t){try{let e=new Date,o=X(t),r=new Date(e.getTime()-24*36e5).toISOString(),n=new Date(e.getTime()-7*864e5).toISOString(),s=ke(t,r),a=ke(t,n),l=Fe(t),d=Object.entries(l).sort((y,k)=>y[1]-k[1]),f=d.slice(0,5).map(([y,k])=>({topic:y,confidence:k})),b=d.slice(-5).reverse().map(([y,k])=>({topic:y,confidence:k}));return {total_lessons:o,lessons_today:s,lessons_this_week:a,weakest_topics:f,strongest_topics:b}}catch(e){return {error:String(e)}}}function bt(t){try{return mt(t)}catch{return []}}var Ro=[["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]],Co={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"},Lo={django:"django",flask:"flask",fastapi:"fastapi",starlette:"fastapi",tornado:"python",aiohttp:"python",sqlalchemy:"postgresql",celery:"python"};function Io(t){let e="";for(let o of t)o==="*"?e+="[^/]*":o==="?"?e+="[^/]":e+=o.replace(/[.+^${}()|[\]\\]/g,String.raw`\$&`);return new RegExp(`^${e}$`)}function Oo(t,e){let o=e.lastIndexOf("/"),r=o>=0?join(t,e.slice(0,o)):t,n=o>=0?e.slice(o+1):e,s;try{s=readdirSync(r);}catch{return false}let a=Io(n);return s.some(l=>a.test(l))}function yt(t){try{return existsSync(t)?readFileSync(t,"utf8"):null}catch{return null}}function No(t,e){let o=yt(join(t,"package.json"));if(!o)return;let r;try{r=JSON.parse(o);}catch{return}let n=new Set([...Object.keys(r.dependencies??{}),...Object.keys(r.devDependencies??{})]);for(let[s,a]of Object.entries(Co))n.has(s)&&e(a,6);}function Do(t,e){let o=["requirements.txt","requirements-dev.txt","requirements/base.txt","pyproject.toml"];for(let r of o){let n=yt(join(t,r))?.toLowerCase();if(n)for(let[s,a]of Object.entries(Lo))n.includes(s)&&e(a,6);}}function $e(t){let e={},o=(r,n)=>{(e[r]??-1)<n&&(e[r]=n);};for(let[r,n,s]of Ro)Oo(t,r)&&o(n,s);return No(t,o),Do(t,o),e}function qe(...t){try{return execFileSync("git",t,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}}var ht={"github.com":"github","gitlab.com":"gitlab","bitbucket.org":"bitbucket"};function Po(t){if(!t)return [null,null];let e=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(t);if(e){let r=(e[1]??"").toLowerCase();return [e[2]??t,ht[r]??"local"]}let o=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(t);if(o){let r=(o[1]??"").toLowerCase();return [o[2]??t,ht[r]??"local"]}return [t,"local"]}function Q(){let t=process.cwd(),e=qe("rev-parse","--abbrev-ref","HEAD"),o=qe("rev-parse","HEAD"),r=qe("remote","get-url","origin"),[n,s]=Po(r),a;return n?a=n.replace(/\/+$/,"").split("/").pop()??null:a=basename(t)||null,e==="HEAD"&&(e=null),{project:a,repository:n,branch:e,commit_hash:o,folder:t,repository_platform:s}}var kt=dirname(fileURLToPath(import.meta.url)),Uo=[join(kt,"../assets/SKILL.md"),join(kt,"../../assets/SKILL.md")];function vt(){for(let t of Uo)try{return readFileSync(t,"utf8")}catch{}return "devcoach: coaching instructions unavailable (SKILL.md not found in package)."}var wt={version:"0.4.3"};var le=wt.version;var I=t=>({type:"text",text:t}),_t=t=>({content:[I(JSON.stringify(t))]}),L=t=>({isError:true,content:[I(t)]}),We=t=>({content:[I(JSON.stringify(t))],structuredContent:t}),Yo={id:z$1.string(),timestamp:z$1.string(),topic_id:z$1.string(),categories:z$1.array(z$1.string()),title:z$1.string(),level:fe,summary:z$1.string(),body:z$1.string().nullable(),task_context:z$1.string().nullable(),project:z$1.string().nullable(),repository:z$1.string().nullable(),branch:z$1.string().nullable(),commit_hash:z$1.string().nullable(),folder:z$1.string().nullable(),repository_platform:be.nullable(),starred:z$1.boolean(),feedback:Ce.nullable()},Jo={knowledge:z$1.array(z$1.object({topic:z$1.string(),confidence:z$1.number().int()})),groups:z$1.array(z$1.object({name:z$1.string(),topics:z$1.array(z$1.string())}))},Vo={max_per_day:z$1.number().int(),min_gap_minutes:z$1.number().int(),ui_theme:Ie};function _(t,e){return {contents:[{uri:t.href,mimeType:"application/json",text:JSON.stringify(e)}]}}function Zo(){let t=new McpServer({name:"devcoach",version:le},{instructions:"Progressive technical coaching server. Use the devcoach_instructions prompt for full coaching behaviour guidelines."});t.registerTool("log_lesson",{title:"Log Lesson",description:"Save a delivered lesson to the coaching log. Git metadata (project, repository, branch, commit_hash, folder, repository_platform) is auto-detected from the workspace when omitted. Returns the saved Lesson with all resolved fields.",inputSchema:{id:z$1.string().describe("Unique lesson id (uuid or random string)"),timestamp:z$1.string().describe("ISO 8601 timestamp; normalized to UTC and clamped to now"),topic_id:z$1.string().describe("Primary topic id, e.g. 'python'"),categories:z$1.array(z$1.string()).describe("Category tags, e.g. ['python','performance']"),title:z$1.string().describe("Short lesson title"),level:fe.describe("Difficulty level: junior | mid | senior"),summary:z$1.string().describe("1-3 sentence summary shown in the lesson card"),body:z$1.string().min(1).describe("Full lesson markdown \u2014 the exact card text shown to the user (required, non-empty)"),task_context:z$1.string().nullish().describe("What the user was doing when taught"),project:z$1.string().nullish().describe("Project name (auto-detected from git if omitted)"),repository:z$1.string().nullish().describe("org/repo or path (auto-detected if omitted)"),branch:z$1.string().nullish().describe("git branch (auto-detected if omitted)"),commit_hash:z$1.string().nullish().describe("git commit hash (auto-detected if omitted)"),folder:z$1.string().nullish().describe("workspace folder (auto-detected if omitted)"),repository_platform:be.nullish().describe("github | gitlab | bitbucket | local (auto-detected if omitted)")},outputSchema:Yo,annotations:{title:"Log Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=Q(),n={};try{n=m(a=>Ue(a));}catch{n={};}let s=re({id:o.id,timestamp:o.timestamp,topic_id:o.topic_id,categories:o.categories,title:o.title,level:o.level,summary:o.summary,body:o.body,task_context:o.task_context??null,project:o.project??r.project??n.project??null,repository:o.repository??r.repository??n.repository??null,branch:o.branch??r.branch??n.branch??null,commit_hash:o.commit_hash??r.commit_hash??null,folder:o.folder??r.folder??null,repository_platform:o.repository_platform??r.repository_platform??n.repository_platform??null});m(a=>pt(a,s));try{if(t.server.getClientCapabilities()?.elicitation){let l=await t.server.elicitInput({message:"Did that land?",requestedSchema:{type:"object",properties:{feedback:{type:"string",title:"Did that land?",description:"know = got it \xB7 dont_know = need to revisit \xB7 skip = no feedback",enum:["know","dont_know","skip"]}},required:[]}}),d=l.action==="accept"?l.content?.feedback:void 0;d&&d!=="skip"&&(m(f=>{ne(f,o.id,d),q(f,o.topic_id,d==="know"?1:-1);}),s={...s,feedback:d});}}catch{}return We(s)}catch(r){return L(`log_lesson failed: ${r}`)}}),t.registerTool("update_knowledge",{title:"Update Knowledge",description:"Adjust the confidence score for a topic by delta (e.g. +1 or -1). Returns the new confidence (0-10). Creates the topic at confidence 5 if it does not exist.",inputSchema:{topic:z$1.string().describe("Topic id, e.g. 'python'"),delta:z$1.number().int().describe("Signed change to apply, e.g. +1 or -1")},annotations:{title:"Update Knowledge",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>q(n,o.topic,o.delta));return {content:[I(String(r))]}}catch(r){return L(`update_knowledge failed for '${o.topic}': ${r}`)}}),t.registerTool("get_lessons",{title:"Get Lessons",description:"Query the coaching lesson history. All filters combine. period defaults to all; date_from/date_to override period; limit caps results newest-first (0 = all).",inputSchema:{period:z$1.enum(["today","week","month","year","all"]).nullish().describe("Time window"),category:z$1.string().nullish().describe("Filter by a category tag, e.g. 'python'"),level:z$1.enum(["junior","mid","senior"]).nullish().describe("Filter by difficulty level"),project:z$1.string().nullish().describe("Fuzzy match on git project"),repository:z$1.string().nullish().describe("Fuzzy match on git repository"),branch:z$1.string().nullish().describe("Fuzzy match on git branch"),commit:z$1.string().nullish().describe("Fuzzy match on commit hash"),starred:z$1.boolean().nullish().describe("True to return only starred lessons"),feedback:z$1.enum(["know","dont_know","none"]).nullish().describe("Filter by feedback ('none' = no response)"),search:z$1.string().nullish().describe("Full-text search over title, topic_id, summary, body"),date_from:z$1.string().nullish().describe("ISO date/datetime lower bound"),date_to:z$1.string().nullish().describe("ISO date/datetime upper bound (date-only = end-of-day)"),limit:z$1.number().int().default(10).describe("Max lessons, newest first. Pass 0 for all.")},annotations:{title:"Get Lessons",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}},o=>{try{let r=m(n=>B(n,{period:o.period,category:o.category,level:o.level,project:o.project,repository:o.repository,branch:o.branch,commit:o.commit,starred:o.starred,feedback:o.feedback,search:o.search,date_from:o.date_from,date_to:o.date_to,page:o.limit>0?1:null,per_page:o.limit}));return _t(r)}catch{return _t([])}}),t.registerTool("star_lesson",{title:"Star Lesson",description:"Set the starred (favourite) flag on a lesson. Returns true if found and updated. Idempotent.",inputSchema:{lesson_id:z$1.string().describe("Lesson id"),starred:z$1.boolean().describe("true to favourite, false to unmark")},annotations:{title:"Star Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>Y(n,o.lesson_id,o.starred));return {content:[I(String(r))]}}catch(r){return L(`star_lesson failed for '${o.lesson_id}': ${r}`)}}),t.registerTool("delete_lesson",{title:"Delete Lesson",description:"Permanently delete a lesson by id. Returns true if found and deleted.",inputSchema:{lesson_id:z$1.string().describe("Lesson id to delete")},annotations:{title:"Delete Lesson",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>xe(n,o.lesson_id));return {content:[I(String(r))]}}catch(r){return L(`delete_lesson failed for '${o.lesson_id}': ${r}`)}}),t.registerTool("submit_feedback",{title:"Submit Feedback",description:"Record comprehension feedback for a lesson and adjust knowledge confidence. know = +1, dont_know = -1, clear = remove feedback (no confidence change). Idempotent \u2014 the same feedback twice adjusts confidence only once.",inputSchema:{lesson_id:z$1.string().describe("Lesson id"),feedback:z$1.enum(["know","dont_know","clear"]).describe("know | dont_know | clear")},annotations:{title:"Submit Feedback",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=o.feedback==="clear"?null:o.feedback,n=m(s=>{let a=s.prepare("SELECT feedback, topic_id FROM lessons WHERE id = ?").get(o.lesson_id);return a?(a.feedback===r||(ne(s,o.lesson_id,r),(r==="know"||r==="dont_know")&&a.topic_id&&q(s,a.topic_id,r==="know"?1:-1)),!0):!1});return {content:[I(String(n))]}}catch(r){return L(`submit_feedback failed for '${o.lesson_id}': ${r}`)}}),t.registerTool("add_topic",{title:"Add Topic",description:"Add a topic to the knowledge map, or update its confidence if it exists. Optionally assign to a group (auto-created). Prefer a single-word topic id. Idempotent.",inputSchema:{topic:z$1.string().describe("Topic id \u2014 prefer a single word, max 3 words"),confidence:Le.default(5).describe("Initial confidence 0-10 (default 5)"),group:z$1.string().nullish().describe("Optional group name; 'Other' if omitted")},annotations:{title:"Add Topic",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{return m(r=>{O(r,o.topic,o.confidence),o.group&&o.group!=="Other"&&D(r,o.topic,o.group);}),{content:[I("true")]}}catch(r){return L(`add_topic failed for '${o.topic}': ${r}`)}}),t.registerTool("remove_topic",{title:"Remove Topic",description:"Remove a topic from the knowledge map entirely. Returns true if it existed.",inputSchema:{topic:z$1.string().describe("Topic id to remove")},annotations:{title:"Remove Topic",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>J(n,o.topic));return {content:[I(String(r))]}}catch(r){return L(`remove_topic failed for '${o.topic}': ${r}`)}}),t.registerTool("add_group",{title:"Add Group",description:"Create a new (initially empty) knowledge group. Idempotent \u2014 returns true either way.",inputSchema:{name:z$1.string().describe("Group name, e.g. 'Machine Learning'")},annotations:{title:"Add Group",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{return m(r=>P(r,o.name.trim())),{content:[I("true")]}}catch(r){return L(`add_group failed for '${o.name}': ${r}`)}}),t.registerTool("remove_group",{title:"Remove Group",description:"Delete a knowledge group. Its topics move to Other. Returns true if it existed.",inputSchema:{name:z$1.string().describe("Group name to delete")},annotations:{title:"Remove Group",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>V(n,o.name));return {content:[I(String(r))]}}catch(r){return L(`remove_group failed for '${o.name}': ${r}`)}}),t.registerTool("update_settings",{title:"Update Settings",description:"Update a coaching setting. max_per_day: integer 1-20. min_gap_minutes: integer 0-1440 (0 = no cooldown). Returns the full updated Settings.",inputSchema:{key:z$1.enum(["max_per_day","min_gap_minutes"]).describe("Setting key"),value:z$1.string().describe("New value as an integer string")},outputSchema:Vo,annotations:{title:"Update Settings",destructiveHint:false,idempotentHint:true,openWorldHint:false}},o=>{let r=Number.parseInt(o.value,10);if(Number.isNaN(r))return L(`Value must be an integer, got '${o.value}'`);if(o.key==="max_per_day"&&!(r>=1&&r<=20))return L("max_per_day must be between 1 and 20");if(o.key==="min_gap_minutes"&&!(r>=0&&r<=1440))return L("min_gap_minutes must be between 0 and 1440");let n=m(s=>(C(s,o.key,String(r)),E(s)));return We(n)}),t.registerTool("open_ui",{title:"Open UI",description:"Launch the devcoach web dashboard in the background. port must be 1024-65535.",inputSchema:{port:z$1.number().int().default(7860).describe("Port (default 7860)")},annotations:{title:"Open UI",destructiveHint:false,idempotentHint:false,openWorldHint:true}},o=>{let r=o.port;return r>=1024&&r<=65535?(spawn(process.execPath,[process.argv[1]??"","ui","--port",String(r)],{detached:true,stdio:"ignore"}).unref(),{content:[I(`devcoach UI starting at http://localhost:${r}`)]}):{content:[I(`error: port ${r} is out of valid range (1024-65535)`)]}}),t.registerTool("complete_onboarding",{title:"Complete Onboarding",description:"Save the user's initial knowledge profile and mark onboarding complete. Wipes any default-seeded profile. topics: {topic_id: confidence_0_to_10}. groups: {group_name: [topic_id,...]} (topics not in any group go to 'Other'). Returns the updated Profile.",inputSchema:{topics:z$1.record(z$1.string(),Le).describe("{topic_id: confidence 0-10}"),groups:z$1.record(z$1.string(),z$1.array(z$1.string())).optional().describe("{group_name: [topic_id, ...]}")},outputSchema:Jo,annotations:{title:"Complete Onboarding",destructiveHint:true,idempotentHint:false,openWorldHint:false}},async o=>{try{let r=m(n=>{n.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[s,a]of Object.entries(o.topics))O(n,s,Math.max(0,Math.min(10,a)));if(o.groups)for(let[s,a]of Object.entries(o.groups))for(let l of a)l in o.topics&&D(n,l,s);return M(n)});return We(r)}catch(r){return L(`complete_onboarding failed: ${r}`)}});let e=(o,r)=>({title:o,description:r,mimeType:"application/json"});return t.registerResource("profile","devcoach://profile",e("Knowledge Profile","Current knowledge map \u2014 topics, confidence scores, and groups."),o=>{try{return _(o,m(r=>M(r)))}catch(r){return _(o,{error:String(r)})}}),t.registerResource("settings","devcoach://settings",e("Settings","Current coaching settings (rate limits, UI theme)."),o=>{try{return _(o,m(r=>E(r)))}catch(r){return _(o,{error:String(r)})}}),t.registerResource("recent-lessons","devcoach://lessons/recent",e("Recent Lessons","Last 10 lessons from the current week."),o=>{try{return _(o,m(r=>B(r,{period:"week",page:1,per_page:10})))}catch(r){return _(o,[{error:String(r)}])}}),t.registerResource("stats","devcoach://stats",e("Stats","Aggregate coaching statistics: counts and weakest/strongest topics."),o=>{try{return _(o,m(r=>Z(r)))}catch(r){return _(o,{error:String(r)})}}),t.registerResource("taught-topics","devcoach://taught-topics",e("Taught Topics","All topic_ids already taught \u2014 read before selecting a new lesson topic."),o=>{try{return _(o,m(r=>bt(r)))}catch{return _(o,[])}}),t.registerResource("rate-limit","devcoach://rate-limit",e("Rate Limit","Current rate-limit status and running lesson total."),o=>{try{let{r,total:n}=m(a=>({r:z(a),total:X(a)})),s={allowed:r.allowed,total_lessons:n};return r.reason!=null&&(s.reason=r.reason),_(o,s)}catch(r){return _(o,{allowed:false,reason:`Rate limit check unavailable: ${r}`})}}),t.registerResource("context","devcoach://context",e("Workspace Context","Auto-detected git context and most-used lesson metadata defaults."),o=>{try{let r=Q(),n=m(s=>Ue(s));return _(o,{git:r,usage_defaults:n})}catch(r){return _(o,{error:String(r)})}}),t.registerResource("onboarding","devcoach://onboarding",e("Onboarding","Onboarding status, auto-detected stack, and project topic defaults."),o=>{try{let n=m(d=>de(d)).knowledge_ready,s=existsSync(w)&&statSync(w).size>0,a=Q(),l=$e(a.folder??process.cwd());return _(o,{knowledge_ready:n,notebook_ready:s,needs_onboarding:!(n&&s),detected_stack:l,default_topics:at,context_ready:a.branch!==null})}catch(r){return _(o,{error:String(r)})}}),t.registerResource("lesson",new ResourceTemplate("devcoach://lessons/{lesson_id}",{list:void 0}),e("Lesson","A single lesson by id."),(o,r)=>{try{let n=String(r.lesson_id),s=m(a=>K(a,n));return s?_(o,s):_(o,{error:`Lesson '${n}' not found`})}catch(n){return _(o,{error:String(n)})}}),t.registerPrompt("devcoach_instructions",{title:"devcoach coaching instructions",description:"Full coaching instructions for the devcoach skill (content of SKILL.md)."},()=>({messages:[{role:"user",content:{type:"text",text:vt()}}]})),t}async function $t(){let t=Zo(),e=new StdioServerTransport;await t.connect(e);}function ee(t){return JSON.stringify(t??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function Se(t){let e=(o,r,n)=>html`<a href="${o}" class="text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition ${n?"text-gray-900 dark:text-white font-semibold":""}">${r}</a>`;return html`<!DOCTYPE html>
|
|
48
|
+
`),ho(t),xo(t);}function ho(t){try{t.exec("ALTER TABLE lessons ADD COLUMN body TEXT");}catch{}}function xo(t){for(let[e,o]of Object.entries(mo))S(t,"INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)",e,o);}var ct="(id, timestamp, topic_id, categories, title, level, summary, body, task_context, project, repository, branch, commit_hash, folder, repository_platform, starred, feedback)",lt="(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";function pt(t){return [t.id,t.timestamp,t.topic_id,JSON.stringify(t.categories),t.title,t.level,t.summary,t.body,t.task_context,t.project,t.repository,t.branch,t.commit_hash,t.folder,t.repository_platform,t.starred?1:0,t.feedback]}function gt(t,e){S(t,`INSERT OR REPLACE INTO lessons ${ct} VALUES ${lt}`,...pt(e));}var ko=[["project","project"],["repository","repository"],["branch","branch"],["commit","commit_hash"]];function vo(t,e){if(t.date_from==null&&t.date_to==null){let o=Ro(t.period??null);o!=null&&e("timestamp >= ?",o);return}if(t.date_from!=null&&e("timestamp >= ?",t.date_from),t.date_to!=null){let o=t.date_to.includes("T")||t.date_to.includes(" ");e("timestamp <= ?",o?t.date_to:`${t.date_to}T23:59:59`);}}function ut(t){let e=[],o=[],r=(s,...a)=>{e.push(s),o.push(...a);};vo(t,r),t.category!=null&&r("categories LIKE ?",`%"${t.category}"%`),t.level!=null&&r("level = ?",t.level);for(let[s,a]of ko){let l=t[s];l!=null&&r(`${a} LIKE ?`,`%${l}%`);}if(t.starred!=null&&r("starred = ?",t.starred?1:0),t.search!=null){let s=`%${t.search}%`;r("(title LIKE ? OR topic_id LIKE ? OR summary LIKE ? OR body LIKE ?)",s,s,s,s);}return t.feedback==="none"?r("feedback IS NULL"):t.feedback!=null&&r("feedback = ?",t.feedback),{where:e.length?`WHERE ${e.join(" AND ")}`:"",params:o}}function J(t,e={}){let{where:o,params:r}=ut(e),n=G(t,`SELECT COUNT(*) AS n FROM lessons ${o}`,...r);return Number(n?.n??0)}var wo=new Set(["timestamp","level","topic_id","title","feedback"]),_o=new Set(["project","repository","branch","commit_hash","repository_platform"]);function B(t,e={}){let{where:o,params:r}=ut(e),n=e.sort&&wo.has(e.sort)?e.sort:"timestamp",s=(e.order??"desc").toLowerCase()==="asc"?"ASC":"DESC",a=`SELECT * FROM lessons ${o} ORDER BY ${n} ${s}`,l=[...r];if(e.page!=null){let d=e.per_page??25;a+=" LIMIT ? OFFSET ?",l.push(d,(e.page-1)*d);}return j(t,a,...l).map(Ge)}function xe(t,e){return S(t,"DELETE FROM lessons WHERE id = ?",e)>0}function V(t,e,o){return S(t,"UPDATE lessons SET starred = ? WHERE id = ?",o?1:0,e)>0}function se(t,e,o){S(t,"UPDATE lessons SET feedback = ? WHERE id = ?",o||null,e);let r=G(t,"SELECT topic_id FROM lessons WHERE id = ?",e);return r?r.topic_id:null}function ie(t){return j(t,"SELECT * FROM lessons ORDER BY timestamp DESC").map(Ge)}function Me(t,e){let o=0,r=0,n=0;for(let s of e){let a;try{a=ne(s);}catch{n+=1;continue}S(t,`INSERT OR IGNORE INTO lessons ${ct} VALUES ${lt}`,...pt(a))>0?o+=1:r+=1;}return {inserted:o,duplicated:r,invalid:n}}function ae(t,e){if(!_o.has(e))throw new Error(`Column not allowed: ${JSON.stringify(e)}`);return j(t,`SELECT DISTINCT ${e} FROM lessons WHERE ${e} IS NOT NULL ORDER BY ${e}`).map(r=>r[e])}function K(t,e){let o=G(t,"SELECT * FROM lessons WHERE id = ?",e);return o?Ge(o):null}function mt(t){return j(t,"SELECT DISTINCT value FROM lessons, json_each(lessons.categories) ORDER BY value").map(o=>o.value)}function ft(t){return j(t,"SELECT DISTINCT topic_id FROM lessons").map(e=>e.topic_id)}function ke(t,e){let o=G(t,"SELECT COUNT(*) AS n FROM lessons WHERE timestamp >= ?",e);return Number(o?.n??0)}function bt(t){let e=G(t,"SELECT timestamp FROM lessons ORDER BY timestamp DESC LIMIT 1");return e?e.timestamp:null}function Fe(t){let e={};for(let o of j(t,"SELECT topic, confidence FROM knowledge"))e[o.topic]=Number(o.confidence);return e}function de(t){return j(t,"SELECT topic, confidence FROM knowledge ORDER BY topic").map(e=>({topic:e.topic,confidence:Number(e.confidence)}))}function He(t){let e=j(t,"SELECT group_name FROM knowledge_group_names ORDER BY group_name").map(r=>r.group_name),o=new Map;for(let r of j(t,"SELECT group_name, topic FROM knowledge_groups ORDER BY group_name, topic")){let n=r.group_name,s=o.get(n)??[];s.push(r.topic),o.set(n,s);}return e.map(r=>({name:r,topics:o.get(r)??[]}))}function I(t,e,o){let r=Math.max(0,Math.min(10,o)),n=new Date().toISOString();S(t,`INSERT INTO knowledge (topic, confidence, updated_at) VALUES (?, ?, ?)
|
|
49
|
+
ON CONFLICT(topic) DO UPDATE SET confidence = excluded.confidence, updated_at = excluded.updated_at`,e,r,n);}function Z(t,e){let o=S(t,"DELETE FROM knowledge WHERE topic = ?",e);return S(t,"DELETE FROM knowledge_groups WHERE topic = ?",e),o>0}function P(t,e){let o=e.trim();if(!o)throw new Error("Group name must not be empty");return S(t,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",o)>0}function Q(t,e){return S(t,"DELETE FROM knowledge_groups WHERE group_name = ?",e),S(t,"DELETE FROM knowledge_group_names WHERE group_name = ?",e)>0}function D(t,e,o){S(t,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",o),S(t,"DELETE FROM knowledge_groups WHERE topic = ?",e),S(t,"INSERT OR IGNORE INTO knowledge_groups (group_name, topic) VALUES (?, ?)",o,e);}function ve(t,e){S(t,"DELETE FROM knowledge_groups WHERE topic = ?",e);}function E(t){let e={};for(let d of j(t,"SELECT key, value FROM settings"))e[d.key]=d.value;let o=e.min_gap_minutes,r=e.min_hours_between,n;o!==void 0?n=Number.parseInt(o,10):r!==void 0?n=Number.parseInt(r,10)*60:n=240;let s=e.max_per_day,a=e.ui_theme??"system",l=a==="dark"||a==="light"||a==="system"?a:"system";return {max_per_day:s!==void 0?Number.parseInt(s,10):2,min_gap_minutes:n,ui_theme:l}}function O(t,e,o){S(t,"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",e,o);}function ce(t){let e=G(t,"SELECT COUNT(*) AS n FROM knowledge");return {knowledge_ready:Number(e?.n??0)>0}}function Ue(t){let e={};for(let o of ["project","repository","branch","repository_platform"]){let r=G(t,`SELECT ${o} AS v, COUNT(*) AS c FROM lessons WHERE ${o} IS NOT NULL GROUP BY ${o} ORDER BY c DESC LIMIT 1`);e[o]=r?r.v:null;}return e}function we(t){let e=E(t),o=ie(t),r=de(t),n=He(t),s=new Map;for(let d of n)for(let f of d.topics)s.set(f,d.name);let a={groups:n.map(d=>d.name),topics:r.map(d=>({topic:d.topic,confidence:d.confidence,group:s.get(d.topic)??null}))},l={[Ne]:strToU8(JSON.stringify(e,null,2)),[De]:strToU8(JSON.stringify(o,null,2)),[je]:strToU8(JSON.stringify(a,null,2))};return existsSync(w)&&(l[Ae]=strToU8(readFileSync(w,"utf8"))),zipSync(l)}function $o(t,e,o){if(!(Ne in e))return;let r=JSON.parse(strFromU8(e[Ne]));"max_per_day"in r&&O(t,"max_per_day",String(r.max_per_day)),"min_gap_minutes"in r&&O(t,"min_gap_minutes",String(r.min_gap_minutes)),(r.ui_theme==="system"||r.ui_theme==="dark"||r.ui_theme==="light")&&O(t,"ui_theme",r.ui_theme),o.settings=1;}function So(t,e,o){if(!(je in e))return;let r=JSON.parse(strFromU8(e[je]));if(!(r&&typeof r=="object"&&Array.isArray(r.topics))){for(let[s,a]of Object.entries(r))I(t,s,a);o.topics=Object.keys(r).length;return}let n=0;for(let s of r.groups??[])P(t,s)&&(n+=1);for(let s of r.topics){I(t,s.topic,s.confidence);let a=s.group;a&&a!=="Other"&&(P(t,a)&&(n+=1),D(t,s.topic,a));}o.topics=r.topics.length,o.groups=n;}function To(t,e,o){if(!(De in e))return;let r=JSON.parse(strFromU8(e[De])),{inserted:n,duplicated:s,invalid:a}=Me(t,r);o.lessons=n,o.skipped=s,o.invalid=a;}function Eo(t,e){Ae in t&&(mkdirSync(dirname(w),{recursive:true}),writeFileSync(w,strFromU8(t[Ae]),"utf8"),e.learning_state=1);}function le(t,e){let o={settings:0,topics:0,groups:0,lessons:0,skipped:0,invalid:0,learning_state:0},r=unzipSync(e);return $o(t,r,o),So(t,r,o),To(t,r,o),Eo(r,o),o}function Ro(t){let e=new Date,o;if(t==="today")o=new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),0,0,0,0));else if(t==="week")o=new Date(e.getTime()-7*864e5);else if(t==="month")o=new Date(e.getTime()-30*864e5);else if(t==="year")o=new Date(e.getTime()-365*864e5);else return null;return o.toISOString()}function Ge(t){let e=[];try{let o=JSON.parse(t.categories);Array.isArray(o)&&(e=o);}catch{e=[];}return ne({...t,categories:e,starred:!!t.starred})}function z(t){try{let e=E(t),o=new Date,r=new Date(o.getTime()-24*36e5).toISOString(),n=ke(t,r);if(n>=e.max_per_day)return {allowed:!1,reason:`Daily limit reached (${n}/${e.max_per_day} lessons in the last 24h)`};let s=bt(t);if(s!=null){let a=new Date(s),l=(o.getTime()-a.getTime())/6e4;if(l<e.min_gap_minutes){let d=e.min_gap_minutes-l,f=Math.floor(e.min_gap_minutes/60),b=e.min_gap_minutes%60,y=Math.trunc(d),k=Math.floor(y/60),v=y%60;return {allowed:!1,reason:`Too soon: last lesson ${l<0?`in ${Math.round(-l)}m (future timestamp)`:`${Math.round(l)}m ago`}, minimum interval is ${f}h ${b}m (${k}h ${v}m remaining)`}}}return {allowed:!0}}catch(e){return {allowed:true,reason:`Rate limit check failed: ${e}`}}}function M(t){try{return {knowledge:de(t),groups:He(t)}}catch{return {knowledge:[],groups:[]}}}function q(t,e,o){let r=Fe(t)[e]??5,n=Math.max(0,Math.min(10,r+o));return I(t,e,n),n}function _e(t,e,o){let r=se(t,e,o);return r&&(o==="know"||o==="dont_know")&&q(t,r,o==="know"?1:-1),r}function W(t){try{let e=new Date,o=J(t),r=new Date(e.getTime()-24*36e5).toISOString(),n=new Date(e.getTime()-7*864e5).toISOString(),s=ke(t,r),a=ke(t,n),l=Fe(t),d=Object.entries(l).sort((y,k)=>y[1]-k[1]),f=d.slice(0,5).map(([y,k])=>({topic:y,confidence:k})),b=d.slice(-5).reverse().map(([y,k])=>({topic:y,confidence:k}));return {total_lessons:o,lessons_today:s,lessons_this_week:a,weakest_topics:f,strongest_topics:b}}catch(e){return {error:String(e)}}}function yt(t){try{return ft(t)}catch{return []}}var Io=[["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]],No={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"},Do={django:"django",flask:"flask",fastapi:"fastapi",starlette:"fastapi",tornado:"python",aiohttp:"python",sqlalchemy:"postgresql",celery:"python"};function jo(t){let e="";for(let o of t)o==="*"?e+="[^/]*":o==="?"?e+="[^/]":e+=o.replace(/[.+^${}()|[\]\\]/g,String.raw`\$&`);return new RegExp(`^${e}$`)}function Ao(t,e){let o=e.lastIndexOf("/"),r=o>=0?join(t,e.slice(0,o)):t,n=o>=0?e.slice(o+1):e,s;try{s=readdirSync(r);}catch{return false}let a=jo(n);return s.some(l=>a.test(l))}function ht(t){try{return existsSync(t)?readFileSync(t,"utf8"):null}catch{return null}}function Po(t,e){let o=ht(join(t,"package.json"));if(!o)return;let r;try{r=JSON.parse(o);}catch{return}let n=new Set([...Object.keys(r.dependencies??{}),...Object.keys(r.devDependencies??{})]);for(let[s,a]of Object.entries(No))n.has(s)&&e(a,6);}function Mo(t,e){let o=["requirements.txt","requirements-dev.txt","requirements/base.txt","pyproject.toml"];for(let r of o){let n=ht(join(t,r))?.toLowerCase();if(n)for(let[s,a]of Object.entries(Do))n.includes(s)&&e(a,6);}}function $e(t){let e={},o=(r,n)=>{(e[r]??-1)<n&&(e[r]=n);};for(let[r,n,s]of Io)Ao(t,r)&&o(n,s);return Po(t,o),Mo(t,o),e}function qe(...t){try{return execFileSync("git",t,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}}var xt={"github.com":"github","gitlab.com":"gitlab","bitbucket.org":"bitbucket"};function Uo(t){if(!t)return [null,null];let e=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(t);if(e){let r=(e[1]??"").toLowerCase();return [e[2]??t,xt[r]??"local"]}let o=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(t);if(o){let r=(o[1]??"").toLowerCase();return [o[2]??t,xt[r]??"local"]}return [t,"local"]}function ee(){let t=process.cwd(),e=qe("rev-parse","--abbrev-ref","HEAD"),o=qe("rev-parse","HEAD"),r=qe("remote","get-url","origin"),[n,s]=Uo(r),a;return n?a=n.replace(/\/+$/,"").split("/").pop()??null:a=basename(t)||null,e==="HEAD"&&(e=null),{project:a,repository:n,branch:e,commit_hash:o,folder:t,repository_platform:s}}var qo=dirname(fileURLToPath(import.meta.url));function Wo(t,e){for(let o of ["../assets","../../assets"])try{return readFileSync(join(qo,o,t),"utf8")}catch{}return e}var kt=()=>Wo("SKILL.md","devcoach: coaching instructions unavailable (SKILL.md not found in package).");var vt={version:"0.4.4"};var pe=vt.version;var C=t=>({type:"text",text:t}),$t=t=>({content:[C(JSON.stringify(t))]}),R=t=>({isError:true,content:[C(t)]}),Ye=t=>({content:[C(JSON.stringify(t))],structuredContent:t}),tr={id:z$1.string(),timestamp:z$1.string(),topic_id:z$1.string(),categories:z$1.array(z$1.string()),title:z$1.string(),level:fe,summary:z$1.string(),body:z$1.string().nullable(),task_context:z$1.string().nullable(),project:z$1.string().nullable(),repository:z$1.string().nullable(),branch:z$1.string().nullable(),commit_hash:z$1.string().nullable(),folder:z$1.string().nullable(),repository_platform:be.nullable(),starred:z$1.boolean(),feedback:Le.nullable()},or={knowledge:z$1.array(z$1.object({topic:z$1.string(),confidence:z$1.number().int()})),groups:z$1.array(z$1.object({name:z$1.string(),topics:z$1.array(z$1.string())}))},rr={max_per_day:z$1.number().int(),min_gap_minutes:z$1.number().int(),ui_theme:Ie};function _(t,e){return {contents:[{uri:t.href,mimeType:"application/json",text:JSON.stringify(e)}]}}function nr(){let t=new McpServer({name:"devcoach",version:pe},{instructions:"Progressive technical coaching server. Use the devcoach_instructions prompt for full coaching behaviour guidelines."});t.registerTool("log_lesson",{title:"Log Lesson",description:"Save a delivered lesson to the coaching log. Git metadata (project, repository, branch, commit_hash, folder, repository_platform) is auto-detected from the workspace when omitted. Returns the saved Lesson with all resolved fields.",inputSchema:{id:z$1.string().describe("Unique lesson id (uuid or random string)"),timestamp:z$1.string().describe("ISO 8601 timestamp; normalized to UTC and clamped to now"),topic_id:z$1.string().describe("Primary topic id, e.g. 'python'"),categories:z$1.array(z$1.string()).describe("Category tags, e.g. ['python','performance']"),title:z$1.string().describe("Short lesson title"),level:fe.describe("Difficulty level: junior | mid | senior"),summary:z$1.string().describe("1-3 sentence summary shown in the lesson card"),body:z$1.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$1.string().nullish().describe("What the user was doing when taught"),project:z$1.string().nullish().describe("Project name (auto-detected from git if omitted)"),repository:z$1.string().nullish().describe("org/repo or path (auto-detected if omitted)"),branch:z$1.string().nullish().describe("git branch (auto-detected if omitted)"),commit_hash:z$1.string().nullish().describe("git commit hash (auto-detected if omitted)"),folder:z$1.string().nullish().describe("workspace folder (auto-detected if omitted)"),repository_platform:be.nullish().describe("github | gitlab | bitbucket | local (auto-detected if omitted)")},outputSchema:tr,annotations:{title:"Log Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=ee(),n={};try{n=m(a=>Ue(a));}catch{n={};}let s=ne({id:o.id,timestamp:o.timestamp,topic_id:o.topic_id,categories:o.categories,title:o.title,level:o.level,summary:o.summary,body:o.body,task_context:o.task_context??null,project:o.project??r.project??n.project??null,repository:o.repository??r.repository??n.repository??null,branch:o.branch??r.branch??n.branch??null,commit_hash:o.commit_hash??r.commit_hash??null,folder:o.folder??r.folder??null,repository_platform:o.repository_platform??r.repository_platform??n.repository_platform??null});m(a=>gt(a,s));try{if(t.server.getClientCapabilities()?.elicitation){let l=await t.server.elicitInput({message:"Did that land?",requestedSchema:{type:"object",properties:{feedback:{type:"string",title:"Did that land?",description:"know = got it \xB7 dont_know = need to revisit \xB7 skip = no feedback",enum:["know","dont_know","skip"]}},required:[]}}),d=l.action==="accept"?l.content?.feedback:void 0;d&&d!=="skip"&&(m(f=>{se(f,o.id,d),q(f,o.topic_id,d==="know"?1:-1);}),s={...s,feedback:d});}}catch{}return Ye(s)}catch(r){return R(`log_lesson failed: ${r}`)}}),t.registerTool("update_knowledge",{title:"Update Knowledge",description:"Adjust the confidence score for a topic by delta (e.g. +1 or -1). Returns the new confidence (0-10). Creates the topic at confidence 5 if it does not exist.",inputSchema:{topic:z$1.string().describe("Topic id, e.g. 'python'"),delta:z$1.number().int().describe("Signed change to apply, e.g. +1 or -1")},annotations:{title:"Update Knowledge",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>q(n,o.topic,o.delta));return {content:[C(String(r))]}}catch(r){return R(`update_knowledge failed for '${o.topic}': ${r}`)}}),t.registerTool("get_lessons",{title:"Get Lessons",description:"Query the coaching lesson history. All filters combine. period defaults to all; date_from/date_to override period; limit caps results newest-first (0 = all).",inputSchema:{period:z$1.enum(["today","week","month","year","all"]).nullish().describe("Time window"),category:z$1.string().nullish().describe("Filter by a category tag, e.g. 'python'"),level:z$1.enum(["junior","mid","senior"]).nullish().describe("Filter by difficulty level"),project:z$1.string().nullish().describe("Fuzzy match on git project"),repository:z$1.string().nullish().describe("Fuzzy match on git repository"),branch:z$1.string().nullish().describe("Fuzzy match on git branch"),commit:z$1.string().nullish().describe("Fuzzy match on commit hash"),starred:z$1.boolean().nullish().describe("True to return only starred lessons"),feedback:z$1.enum(["know","dont_know","none"]).nullish().describe("Filter by feedback ('none' = no response)"),search:z$1.string().nullish().describe("Full-text search over title, topic_id, summary, body"),date_from:z$1.string().nullish().describe("ISO date/datetime lower bound"),date_to:z$1.string().nullish().describe("ISO date/datetime upper bound (date-only = end-of-day)"),limit:z$1.number().int().default(10).describe("Max lessons, newest first. Pass 0 for all.")},annotations:{title:"Get Lessons",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}},o=>{try{let r=m(n=>B(n,{period:o.period,category:o.category,level:o.level,project:o.project,repository:o.repository,branch:o.branch,commit:o.commit,starred:o.starred,feedback:o.feedback,search:o.search,date_from:o.date_from,date_to:o.date_to,page:o.limit>0?1:null,per_page:o.limit}));return $t(r)}catch{return $t([])}}),t.registerTool("star_lesson",{title:"Star Lesson",description:"Set the starred (favourite) flag on a lesson. Returns true if found and updated. Idempotent.",inputSchema:{lesson_id:z$1.string().describe("Lesson id"),starred:z$1.boolean().describe("true to favourite, false to unmark")},annotations:{title:"Star Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>V(n,o.lesson_id,o.starred));return {content:[C(String(r))]}}catch(r){return R(`star_lesson failed for '${o.lesson_id}': ${r}`)}}),t.registerTool("delete_lesson",{title:"Delete Lesson",description:"Permanently delete a lesson by id. Returns true if found and deleted.",inputSchema:{lesson_id:z$1.string().describe("Lesson id to delete")},annotations:{title:"Delete Lesson",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>xe(n,o.lesson_id));return {content:[C(String(r))]}}catch(r){return R(`delete_lesson failed for '${o.lesson_id}': ${r}`)}}),t.registerTool("submit_feedback",{title:"Submit Feedback",description:"Record comprehension feedback for a lesson and adjust knowledge confidence. know = +1, dont_know = -1, clear = remove feedback (no confidence change). Idempotent \u2014 the same feedback twice adjusts confidence only once.",inputSchema:{lesson_id:z$1.string().describe("Lesson id"),feedback:z$1.enum(["know","dont_know","clear"]).describe("know | dont_know | clear")},annotations:{title:"Submit Feedback",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=o.feedback==="clear"?null:o.feedback,n=m(s=>{let a=s.prepare("SELECT feedback, topic_id FROM lessons WHERE id = ?").get(o.lesson_id);return a?(a.feedback===r||(se(s,o.lesson_id,r),(r==="know"||r==="dont_know")&&a.topic_id&&q(s,a.topic_id,r==="know"?1:-1)),!0):!1});return {content:[C(String(n))]}}catch(r){return R(`submit_feedback failed for '${o.lesson_id}': ${r}`)}}),t.registerTool("add_topic",{title:"Add Topic",description:"Add a topic to the knowledge map, or update its confidence if it exists. Optionally assign to a group (auto-created). Prefer a single-word topic id. Idempotent.",inputSchema:{topic:z$1.string().describe("Topic id \u2014 prefer a single word, max 3 words"),confidence:Oe.default(5).describe("Initial confidence 0-10 (default 5)"),group:z$1.string().nullish().describe("Optional group name; 'Other' if omitted")},annotations:{title:"Add Topic",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{return m(r=>{I(r,o.topic,o.confidence),o.group&&o.group!=="Other"&&D(r,o.topic,o.group);}),{content:[C("true")]}}catch(r){return R(`add_topic failed for '${o.topic}': ${r}`)}}),t.registerTool("remove_topic",{title:"Remove Topic",description:"Remove a topic from the knowledge map entirely. Returns true if it existed.",inputSchema:{topic:z$1.string().describe("Topic id to remove")},annotations:{title:"Remove Topic",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>Z(n,o.topic));return {content:[C(String(r))]}}catch(r){return R(`remove_topic failed for '${o.topic}': ${r}`)}}),t.registerTool("add_group",{title:"Add Group",description:"Create a new (initially empty) knowledge group. Idempotent \u2014 returns true either way.",inputSchema:{name:z$1.string().describe("Group name, e.g. 'Machine Learning'")},annotations:{title:"Add Group",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{return m(r=>P(r,o.name.trim())),{content:[C("true")]}}catch(r){return R(`add_group failed for '${o.name}': ${r}`)}}),t.registerTool("remove_group",{title:"Remove Group",description:"Delete a knowledge group. Its topics move to Other. Returns true if it existed.",inputSchema:{name:z$1.string().describe("Group name to delete")},annotations:{title:"Remove Group",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>Q(n,o.name));return {content:[C(String(r))]}}catch(r){return R(`remove_group failed for '${o.name}': ${r}`)}}),t.registerTool("update_settings",{title:"Update Settings",description:"Update a coaching setting. max_per_day: integer 1-20. min_gap_minutes: integer 0-1440 (0 = no cooldown). Returns the full updated Settings.",inputSchema:{key:z$1.enum(["max_per_day","min_gap_minutes"]).describe("Setting key"),value:z$1.string().describe("New value as an integer string")},outputSchema:rr,annotations:{title:"Update Settings",destructiveHint:false,idempotentHint:true,openWorldHint:false}},o=>{let r=Number.parseInt(o.value,10);if(Number.isNaN(r))return R(`Value must be an integer, got '${o.value}'`);if(o.key==="max_per_day"&&!(r>=1&&r<=20))return R("max_per_day must be between 1 and 20");if(o.key==="min_gap_minutes"&&!(r>=0&&r<=1440))return R("min_gap_minutes must be between 0 and 1440");let n=m(s=>(O(s,o.key,String(r)),E(s)));return Ye(n)}),t.registerTool("open_ui",{title:"Open UI",description:"Launch the devcoach web dashboard in the background. port must be 1024-65535.",inputSchema:{port:z$1.number().int().default(7860).describe("Port (default 7860)")},annotations:{title:"Open UI",destructiveHint:false,idempotentHint:false,openWorldHint:true}},o=>{let r=o.port;return r>=1024&&r<=65535?(spawn(process.execPath,[process.argv[1]??"","ui","--port",String(r)],{detached:true,stdio:"ignore"}).unref(),{content:[C(`devcoach UI starting at http://localhost:${r}`)]}):{content:[C(`error: port ${r} is out of valid range (1024-65535)`)]}}),t.registerTool("complete_onboarding",{title:"Complete Onboarding",description:"Save the user's initial knowledge profile and mark onboarding complete. Wipes any default-seeded profile. topics: {topic_id: confidence_0_to_10}. groups: {group_name: [topic_id,...]} (topics not in any group go to 'Other'). Pass the personalized coaching notebook as `notebook`; it is saved to learning-state.md atomically with the profile. Returns the updated Profile.",inputSchema:{topics:z$1.record(z$1.string(),Oe).describe("{topic_id: confidence 0-10}"),groups:z$1.record(z$1.string(),z$1.array(z$1.string())).optional().describe("{group_name: [topic_id, ...]}"),notebook:z$1.string().optional().describe("Full personalized coaching notebook (markdown) saved verbatim to learning-state.md. Write real observations about THIS user \u2014 background, strengths, gaps, and how they work across ALL their projects (not just this one), per the template in the devcoach skill. If omitted, a minimal placeholder is written so the file is never empty.")},outputSchema:or,annotations:{title:"Complete Onboarding",destructiveHint:true,idempotentHint:false,openWorldHint:false}},async o=>{try{let r=m(s=>{s.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[a,l]of Object.entries(o.topics))I(s,a,Math.max(0,Math.min(10,l)));if(o.groups)for(let[a,l]of Object.entries(o.groups))for(let d of l)d in o.topics&&D(s,d,a);return M(s)}),n=o.notebook?.trim();return mkdirSync(dirname(w),{recursive:!0}),n?writeFileSync(w,n.endsWith(`
|
|
50
|
+
`)?n:`${n}
|
|
51
|
+
`):existsSync(w)||writeFileSync(w,`# devcoach \u2014 Coaching Notebook
|
|
52
|
+
`),Ye(r)}catch(r){return R(`complete_onboarding failed: ${r}`)}}),t.registerTool("update_notebook",{title:"Update Coaching Notebook",description:"Overwrite the coaching notebook (~/.devcoach/learning-state.md) with revised markdown. Use it after a lesson to fold in what the user absorbed or struggled with, new recurring patterns, and updated hypotheses \u2014 keeping the prior notes. Pass the full notebook content.",inputSchema:{notebook:z$1.string().min(1).describe("Full notebook markdown saved verbatim to learning-state.md (overwrites).")},annotations:{title:"Update Coaching Notebook",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{return mkdirSync(dirname(w),{recursive:!0}),writeFileSync(w,o.notebook.endsWith(`
|
|
53
|
+
`)?o.notebook:`${o.notebook}
|
|
54
|
+
`),{content:[C("notebook updated")]}}catch(r){return R(`update_notebook failed: ${r}`)}});let e=(o,r)=>({title:o,description:r,mimeType:"application/json"});return t.registerResource("profile","devcoach://profile",e("Knowledge Profile","Current knowledge map \u2014 topics, confidence scores, and groups."),o=>{try{return _(o,m(r=>M(r)))}catch(r){return _(o,{error:String(r)})}}),t.registerResource("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"},o=>{let r="";try{existsSync(w)&&(r=readFileSync(w,"utf8"));}catch{}return {contents:[{uri:o.href,mimeType:"text/markdown",text:r}]}}),t.registerResource("settings","devcoach://settings",e("Settings","Current coaching settings (rate limits, UI theme)."),o=>{try{return _(o,m(r=>E(r)))}catch(r){return _(o,{error:String(r)})}}),t.registerResource("recent-lessons","devcoach://lessons/recent",e("Recent Lessons","Last 10 lessons from the current week."),o=>{try{return _(o,m(r=>B(r,{period:"week",page:1,per_page:10})))}catch(r){return _(o,[{error:String(r)}])}}),t.registerResource("stats","devcoach://stats",e("Stats","Aggregate coaching statistics: counts and weakest/strongest topics."),o=>{try{return _(o,m(r=>W(r)))}catch(r){return _(o,{error:String(r)})}}),t.registerResource("taught-topics","devcoach://taught-topics",e("Taught Topics","All topic_ids already taught \u2014 read before selecting a new lesson topic."),o=>{try{return _(o,m(r=>yt(r)))}catch{return _(o,[])}}),t.registerResource("rate-limit","devcoach://rate-limit",e("Rate Limit","Current rate-limit status and running lesson total."),o=>{try{let{r,total:n}=m(a=>({r:z(a),total:J(a)})),s={allowed:r.allowed,total_lessons:n};return r.reason!=null&&(s.reason=r.reason),_(o,s)}catch(r){return _(o,{allowed:false,reason:`Rate limit check unavailable: ${r}`})}}),t.registerResource("context","devcoach://context",e("Workspace Context","Auto-detected git context and most-used lesson metadata defaults."),o=>{try{let r=ee(),n=m(s=>Ue(s));return _(o,{git:r,usage_defaults:n})}catch(r){return _(o,{error:String(r)})}}),t.registerResource("onboarding","devcoach://onboarding",e("Onboarding","Onboarding status, auto-detected stack, and project topic defaults."),o=>{try{let n=m(d=>ce(d)).knowledge_ready,s=existsSync(w)&&statSync(w).size>0,a=ee(),l=$e(a.folder??process.cwd());return _(o,{knowledge_ready:n,notebook_ready:s,needs_onboarding:!(n&&s),detected_stack:l,default_topics:dt,context_ready:a.branch!==null})}catch(r){return _(o,{error:String(r)})}}),t.registerResource("lesson",new ResourceTemplate("devcoach://lessons/{lesson_id}",{list:void 0}),e("Lesson","A single lesson by id."),(o,r)=>{try{let n=String(r.lesson_id),s=m(a=>K(a,n));return s?_(o,s):_(o,{error:`Lesson '${n}' not found`})}catch(n){return _(o,{error:String(n)})}}),t.registerPrompt("devcoach_instructions",{title:"devcoach coaching instructions",description:"Full coaching instructions for the devcoach skill (content of SKILL.md)."},()=>({messages:[{role:"user",content:{type:"text",text:kt()}}]})),t}async function St(){let t=nr(),e=new StdioServerTransport;await t.connect(e);}function te(t){return JSON.stringify(t??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function Se(t){let e=(o,r,n)=>html`<a href="${o}" class="text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition ${n?"text-gray-900 dark:text-white font-semibold":""}">${r}</a>`;return html`<!DOCTYPE html>
|
|
50
55
|
<html lang="en">
|
|
51
56
|
<head>
|
|
52
57
|
<meta charset="UTF-8" />
|
|
@@ -103,7 +108,7 @@ import {spawn,spawnSync,execFileSync}from'child_process';import {existsSync,writ
|
|
|
103
108
|
</script>
|
|
104
109
|
${t.scripts??""}
|
|
105
110
|
</body>
|
|
106
|
-
</html>`}function
|
|
111
|
+
</html>`}function Ct(t){let e=r=>t.allGroups.filter(n=>n!=="Other").map(n=>html`<option value="${n}" ${n===r?"selected":""}>${n}</option>`),o=html`
|
|
107
112
|
<div x-data="{ editMode: JSON.parse(localStorage.getItem('km-edit-mode') || 'false'), toggle() { this.editMode = !this.editMode; localStorage.setItem('km-edit-mode', this.editMode); } }">
|
|
108
113
|
|
|
109
114
|
<div class="flex items-center justify-between mb-6">
|
|
@@ -222,7 +227,7 @@ ${Object.entries(t.categorised).map(([r,n])=>html`
|
|
|
222
227
|
</div>`})}
|
|
223
228
|
</div>
|
|
224
229
|
</section>`)}
|
|
225
|
-
</div>`;return Se({title:"Profile \u2014 devcoach",currentPath:"/",uiTheme:t.uiTheme,body:o})}var
|
|
230
|
+
</div>`;return Se({title:"Profile \u2014 devcoach",currentPath:"/",uiTheme:t.uiTheme,body:o})}var Tt={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},Et={know:"\u2713 Known",dont_know:"\u2717 Don't know",none:"\u2014 No response"},Rt={junior:"\u{1F7E2}",mid:"\u{1F7E1}",senior:"\u{1F534}"};function $(t,e={}){let o={period:t.period,category:t.category,level:t.level,project:t.project,repository:t.repository,branch:t.branch,commit:t.commit,feedback:t.feedback,search:t.search,date_from:t.date_from,date_to:t.date_to,sort:t.sort,order:t.order};t.starred&&(o.starred="1"),Object.assign(o,e);let r=new URLSearchParams;for(let[n,s]of Object.entries(o))s&&r.set(n,s);return `?${r.toString()}`}function Lt(t){let e=t.s,o=!!(e.date_from||e.date_to),r=e.period!=="all"&&!o||o||!!(e.category||e.level||e.project||e.repository||e.branch||e.commit||e.starred||e.search||e.feedback),n=o?e.date_from&&e.date_to?`${e.date_from} \u2192 ${e.date_to}`:e.date_from?`From ${e.date_from}`:`Until ${e.date_to}`:Tt[e.period]??"All time",s=e.feedback?Et[e.feedback]??"Feedback":"Feedback",a={junior:"text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-900/20",mid:"text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20",senior:"text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-900/20"},l=(g,h)=>html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-indigo-50 dark:bg-indigo-900/20 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800">${g}<a href="${h}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-indigo-200 dark:hover:bg-indigo-700 transition">×</a></span>`,d=(g,h,T="")=>{let N=e.sort===h,me=N&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap ${T}">
|
|
226
231
|
<a href="${$(e,{sort:h,order:me})}" class="inline-flex items-center gap-1 hover:text-gray-700 dark:hover:text-gray-200 transition group/sort">${g}
|
|
227
232
|
${N?html`<span class="text-indigo-400">${e.order==="asc"?"\u2191":"\u2193"}</span>`:html`<span class="text-gray-200 dark:text-gray-700 group-hover/sort:text-gray-400 dark:group-hover/sort:text-gray-500 transition">↕</span>`}
|
|
228
233
|
</a></th>`},f=html`<svg class="w-3 h-3 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"/></svg>`,b=html`<link rel="stylesheet" href="/static/vendor/flatpickr.min.css" />
|
|
@@ -269,7 +274,7 @@ ${Object.entries(t.categorised).map(([r,n])=>html`
|
|
|
269
274
|
</button>
|
|
270
275
|
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95" @click.outside="close()" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-64 overflow-hidden" style="display:none">
|
|
271
276
|
<div class="p-1">
|
|
272
|
-
${Object.entries(
|
|
277
|
+
${Object.entries(Tt).map(([g,h])=>{let T=e.period===g&&!o;return html`<button type="button" @click="selectPreset('${g}', '${h}')" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${T?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${h}${T?html`<span class="text-indigo-500">✓</span>`:""}</button>`})}
|
|
273
278
|
</div>
|
|
274
279
|
<div class="border-t border-gray-100 dark:border-gray-800 p-1">
|
|
275
280
|
<button type="button" @click="showCustom = !showCustom" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${o?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">
|
|
@@ -291,7 +296,7 @@ ${Object.entries(t.categorised).map(([r,n])=>html`
|
|
|
291
296
|
</div>
|
|
292
297
|
|
|
293
298
|
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
|
|
294
|
-
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.level?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${e.level?`${
|
|
299
|
+
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.level?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${e.level?`${Rt[e.level]??""} ${e.level}`:"Level"}${f}</button>
|
|
295
300
|
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-40 p-1 overflow-hidden" style="display:none">
|
|
296
301
|
${[["","All levels"],["junior","\u{1F7E2} Junior"],["mid","\u{1F7E1} Mid"],["senior","\u{1F534} Senior"]].map(([g,h])=>html`<button type="button" onclick="document.getElementById('h-level').value='${g}'; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${e.level===g?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${h}${e.level===g?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
|
|
297
302
|
</div>
|
|
@@ -315,11 +320,11 @@ ${Object.entries(t.categorised).map(([r,n])=>html`
|
|
|
315
320
|
${r?html`<div class="flex flex-wrap gap-1.5 mb-4">
|
|
316
321
|
${o?l(`\u{1F4C5} ${n}`,$(e,{date_from:"",date_to:""})):""}
|
|
317
322
|
${e.period!=="all"&&!o?l(`\u{1F550} ${n}`,$(e,{period:"all"})):""}
|
|
318
|
-
${e.feedback?l(
|
|
323
|
+
${e.feedback?l(Et[e.feedback]??e.feedback,$(e,{feedback:""})):""}
|
|
319
324
|
${e.starred?html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300 border border-yellow-200 dark:border-yellow-800">★ Starred<a href="${$({...e,starred:false})}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-yellow-200 dark:hover:bg-yellow-700 transition">×</a></span>`:""}
|
|
320
325
|
${e.search?l(`\u{1F50D} "${e.search}"`,$(e,{search:""})):""}
|
|
321
326
|
${e.category?l(e.category,$(e,{category:""})):""}
|
|
322
|
-
${e.level?l(`${
|
|
327
|
+
${e.level?l(`${Rt[e.level]??""} ${e.level}`,$(e,{level:""})):""}
|
|
323
328
|
${e.project?l(`\u{1F4C1} ${e.project}`,$(e,{project:""})):""}
|
|
324
329
|
${e.repository?l(`\u2387 ${e.repository}`,$(e,{repository:""})):""}
|
|
325
330
|
${e.branch?l(`\u2387 ${e.branch}`,$(e,{branch:""})):""}
|
|
@@ -366,7 +371,7 @@ ${t.totalPages>1?html`<div class="flex items-center justify-between mt-4">
|
|
|
366
371
|
<p class="text-xs text-gray-400 dark:text-gray-500">Page ${t.page} of ${t.totalPages}</p>
|
|
367
372
|
<div class="flex items-center gap-1">
|
|
368
373
|
${t.page>1?html`<a href="${$(e,{page:String(t.page-1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">← Prev</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">← Prev</span>`}
|
|
369
|
-
${
|
|
374
|
+
${sr(t.page,t.totalPages).map(g=>g===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:g===t.page?html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-indigo-600 text-white border border-indigo-600">${g}</span>`:html`<a href="${$(e,{page:String(g)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">${g}</a>`)}
|
|
370
375
|
${t.page<t.totalPages?html`<a href="${$(e,{page:String(t.page+1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">Next →</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">Next →</span>`}
|
|
371
376
|
</div>
|
|
372
377
|
</div>`:""}`:html`<div class="flex flex-col items-center justify-center py-16 text-center">
|
|
@@ -380,7 +385,7 @@ function periodPicker() {
|
|
|
380
385
|
return {
|
|
381
386
|
open: false,
|
|
382
387
|
showCustom: ${raw(o?"true":"false")},
|
|
383
|
-
label: ${raw(
|
|
388
|
+
label: ${raw(te(n))},
|
|
384
389
|
fp: null,
|
|
385
390
|
init() {
|
|
386
391
|
const self = this;
|
|
@@ -388,7 +393,7 @@ function periodPicker() {
|
|
|
388
393
|
if (val && !this.fp) {
|
|
389
394
|
this.fp = flatpickr(this.$refs.fp, {
|
|
390
395
|
mode: 'range', dateFormat: 'Y-m-d', inline: false,
|
|
391
|
-
${raw(e.date_from?`defaultDate: [${
|
|
396
|
+
${raw(e.date_from?`defaultDate: [${te(e.date_from)}${e.date_to?`, ${te(e.date_to)}`:""}],`:"")}
|
|
392
397
|
onChange(dates) {
|
|
393
398
|
if (dates.length === 2) {
|
|
394
399
|
document.getElementById('h-date-from').value = self.fp.formatDate(dates[0], 'Y-m-d');
|
|
@@ -411,7 +416,7 @@ function periodPicker() {
|
|
|
411
416
|
},
|
|
412
417
|
};
|
|
413
418
|
}
|
|
414
|
-
</script>`;return Se({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:b,body:k,scripts:v})}function
|
|
419
|
+
</script>`;return Se({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:b,body:k,scripts:v})}function sr(t,e){let o=[],r=0;for(let n=1;n<=e;n++)(n===1||n===e||n>=t-2&&n<=t+2)&&(r&&n-r>1&&o.push(0),o.push(n),r=n);return o}var Je={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function Ot(t){let e=t.lesson,o=e.level==="junior"?"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700":e.level==="mid"?"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700":"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700",r=e.repository_platform==="local",n=null;r&&e.repository?n=`vscode://file/${e.repository}`:e.repository_platform&&Je[e.repository_platform]&&e.repository&&(n=`https://${Je[e.repository_platform]}/${e.repository}`);let s=null;n&&e.commit_hash&&!r&&(s=e.repository_platform==="gitlab"?`${n}/-/commit/${e.commit_hash}`:e.repository_platform==="bitbucket"?`${n}/commits/${e.commit_hash}`:`${n}/commit/${e.commit_hash}`);let a=!!(e.project||e.repository||e.branch||e.commit_hash||e.folder),l=e.timestamp.slice(0,10),d=e.timestamp.slice(0,16).replace("T"," "),f=html`
|
|
415
420
|
<div class="mb-4"><a href="/lessons" class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white text-sm transition">← Back to lessons</a></div>
|
|
416
421
|
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
|
|
417
422
|
<div class="flex flex-wrap items-center gap-3 mb-2">
|
|
@@ -440,7 +445,7 @@ function periodPicker() {
|
|
|
440
445
|
${e.task_context?html`<div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 text-sm text-gray-500 dark:text-gray-400"><span class="text-gray-400 dark:text-gray-500">Context:</span> ${e.task_context}</div>`:""}
|
|
441
446
|
${a?html`<div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800"><div class="flex flex-wrap gap-x-5 gap-y-1.5 text-xs text-gray-400 dark:text-gray-500 font-mono">
|
|
442
447
|
${e.project?html`<span><span class="text-gray-400 dark:text-gray-600">project</span> ${n?html`<a href="${n}" ${r?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.project}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.project}</span>`}</span>`:""}
|
|
443
|
-
${e.repository?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">repo</span>${e.repository_platform&&
|
|
448
|
+
${e.repository?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">repo</span>${e.repository_platform&&Je[e.repository_platform]?html`<img src="/static/vendor/icons/${e.repository_platform}.svg" class="w-3 h-3 dark:invert opacity-60 shrink-0" alt="" />`:""}${n?html`<a href="${n}" ${r?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.repository}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.repository}</span>`}</span>`:""}
|
|
444
449
|
${e.branch?html`<span><span class="text-gray-400 dark:text-gray-600">branch</span> <span class="text-indigo-600 dark:text-indigo-400">${e.branch}</span></span>`:""}
|
|
445
450
|
${e.commit_hash?html`<span><span class="text-gray-400 dark:text-gray-600">commit</span> ${s?html`<a href="${s}" target="_blank" rel="noopener" class="text-cyan-600 dark:text-cyan-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.commit_hash.slice(0,7)}</a>`:html`<span class="text-cyan-600 dark:text-cyan-400">${e.commit_hash.slice(0,7)}</span>`}</span>`:""}
|
|
446
451
|
${e.folder?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">folder</span><a href="vscode://file/${e.folder}" class="inline-flex items-center gap-1 text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition"><img src="/static/vendor/icons/vscode.svg" class="w-3.5 h-3.5 shrink-0" alt="" />${e.folder}</a></span>`:""}
|
|
@@ -462,9 +467,9 @@ function periodPicker() {
|
|
|
462
467
|
},
|
|
463
468
|
breaks: true, gfm: true,
|
|
464
469
|
});
|
|
465
|
-
document.getElementById('summary-content').innerHTML = marked.parse(${raw(
|
|
466
|
-
document.getElementById('body-content').innerHTML = marked.parse(${raw(
|
|
467
|
-
</script>`;return Se({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:t.uiTheme,head:b,body:f,scripts:y})}var
|
|
470
|
+
document.getElementById('summary-content').innerHTML = marked.parse(${raw(te(e.summary))});
|
|
471
|
+
document.getElementById('body-content').innerHTML = marked.parse(${raw(te(e.body))});
|
|
472
|
+
</script>`;return Se({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:t.uiTheme,head:b,body:f,scripts:y})}var ir=[[0,"No cooldown"],[15,"15 minutes"],[30,"30 minutes"],[60,"1 hour"],[90,"1 hour 30 min"],[120,"2 hours"],[180,"3 hours"],[240,"4 hours"],[360,"6 hours"],[480,"8 hours"],[720,"12 hours"],[1440,"24 hours"]];function It(t){let e=t.settings.min_gap_minutes,o=[["system","System","\u{1F313}"],["light","Light","\u2600\uFE0F"],["dark","Dark","\u{1F319}"]],r=t.flash,n=html`
|
|
468
473
|
<h1 class="text-2xl font-bold mb-6 text-indigo-600 dark:text-indigo-400">Settings</h1>
|
|
469
474
|
${r?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${r.invalid?"bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-700 text-yellow-800 dark:text-yellow-300":"bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-700 text-green-700 dark:text-green-300"}">
|
|
470
475
|
${r.imported} lesson${r.imported!==1?"s":""} imported.${r.skipped?` ${r.skipped} skipped (already in DB).`:""}${r.invalid?` ${r.invalid} rejected (failed validation).`:""}${r.groups?` ${r.groups} group${r.groups!==1?"s":""} added.`:""}${r.notebook?" Notebook restored.":""}
|
|
@@ -482,7 +487,7 @@ ${r?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${r.invalid?"bg-ye
|
|
|
482
487
|
<div>
|
|
483
488
|
<label for="min-gap-minutes" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Minimum gap between lessons</label>
|
|
484
489
|
<select id="min-gap-minutes" name="min_gap_minutes" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500">
|
|
485
|
-
${
|
|
490
|
+
${ir.map(([a,l])=>html`<option value="${a}" ${e===a?"selected":""}>${l}</option>`)}
|
|
486
491
|
</select>
|
|
487
492
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Minimum gap between lessons.</p>
|
|
488
493
|
</div>
|
|
@@ -558,39 +563,41 @@ function updateLabel(inputId, labelId, submitId) {
|
|
|
558
563
|
document.getElementById(submitId).disabled = !input.files.length;
|
|
559
564
|
}
|
|
560
565
|
marked.setOptions({ breaks: true, gfm: true });
|
|
561
|
-
document.getElementById('notebook-preview-settings').innerHTML = marked.parse(${raw(
|
|
562
|
-
</script>`;return Se({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:t.uiTheme,body:n,scripts:s})}var
|
|
563
|
-
`)}function Re(t=""){if(!t)return "\u2500".repeat(60);let o=` ${t} `,r=Math.max(2,60-Ee(o)),n=Math.floor(r/2);return "\u2500".repeat(n)+o+"\u2500".repeat(r-n)}var
|
|
564
|
-
`),`${i.green("\u2713")} Installed into ${t}`)}function
|
|
565
|
-
`),`${i.green("\u2713")} Stop hooks installed into ${o}`}function
|
|
566
|
+
document.getElementById('notebook-preview-settings').innerHTML = marked.parse(${raw(te(t.notebookContent))});
|
|
567
|
+
</script>`;return Se({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:t.uiTheme,body:n,scripts:s})}var Ze=25;function Qe(t,e="/lessons"){return t?.startsWith("/")&&!t.startsWith("//")?t:e}function L(t,e,o=""){let r=t[e];return typeof r=="string"?r:o}var et=dirname(fileURLToPath(import.meta.url)),Nt=[join(et,"../assets/static"),join(et,"../../assets/static")].find(t=>existsSync(t))??join(et,"../assets/static"),ur={".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".woff2":"font/woff2",".woff":"font/woff",".ico":"image/x-icon"};function mr(){try{return m(t=>E(t).ui_theme)}catch{return "system"}}function fr(){let t=new Hono;return t.get("/static/*",e=>{let o=decodeURIComponent(e.req.path.slice(8)),r=join(Nt,o);if(!r.startsWith(Nt))return e.notFound();try{let n=readFileSync(r);return new Response(new Uint8Array(n),{headers:{"content-type":ur[extname(r)]??"application/octet-stream"}})}catch{return e.notFound()}}),t.get("/",e=>{let{profile:o,stats:r,rateLimit:n,settings:s}=m(d=>({profile:M(d),stats:W(d),rateLimit:z(d),settings:E(d)})),a=new Map;for(let d of o.groups)for(let f of d.topics)a.set(f,d.name);let l={};for(let d of o.groups)l[d.name]=[];for(let d of o.knowledge){let f=a.get(d.topic)??"Other",b=l[f]??[];b.push(d),l[f]=b;}return e.html(Ct({categorised:l,allGroups:o.groups.map(d=>d.name),stats:r,rateLimit:n,maxPerDay:s.max_per_day,uiTheme:s.ui_theme}))}),t.post("/knowledge",async e=>{let o=await e.req.parseBody(),r=L(o,"topic").trim();if(r){let n=Number.parseInt(L(o,"confidence","5"),10)||5,s=L(o,"group").trim();m(a=>{I(a,r,n),s&&s!=="Other"&&D(a,r,s);});}return e.redirect("/",303)}),t.post("/knowledge/:topic/delete",e=>(m(o=>Z(o,e.req.param("topic"))),e.redirect("/",303))),t.post("/knowledge/:topic/group",async e=>{let o=L(await e.req.parseBody(),"group").trim(),r=e.req.param("topic");return m(n=>{o&&o!=="Other"?D(n,r,o):ve(n,r);}),e.redirect("/",303)}),t.post("/knowledge/:topic",async e=>{let o=Number.parseInt(L(await e.req.parseBody(),"delta","0"),10)||0;return m(r=>q(r,e.req.param("topic"),o)),e.redirect("/",303)}),t.post("/groups",async e=>{let o=L(await e.req.parseBody(),"group_name").trim();return o&&o!=="Other"&&m(r=>P(r,o)),e.redirect("/",303)}),t.post("/groups/:group_name/delete",e=>(m(o=>Q(o,e.req.param("group_name"))),e.redirect("/",303))),t.get("/lessons/export",()=>{let e=m(o=>ie(o));return new Response(JSON.stringify(e,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),t.post("/lessons/import",async e=>{let o=(await e.req.parseBody()).file,r;try{r=JSON.parse(o instanceof File?await o.text():String(o));}catch{return e.redirect("/settings?imported=0&skipped=0&invalid=1",303)}if(!Array.isArray(r))return e.redirect("/settings?imported=0&skipped=0&invalid=1",303);let n=m(s=>Me(s,r));return e.redirect(`/settings?imported=${n.inserted}&skipped=${n.duplicated}&invalid=${n.invalid}`,303)}),t.get("/lessons",e=>{let o=e.req.query(),r=new Set(["junior","mid","senior"]),n=o.level&&r.has(o.level)?o.level:"",s=o.date_from||"",a=o.date_to||"",l=o.starred==="1",d=o.sort||"timestamp",f=o.order==="asc"?"asc":"desc",b=Math.max(1,Number.parseInt(o.page??"1",10)||1),y=o.period||"all",k={period:s||a?null:y!=="all"?y:null,category:o.category||null,level:n||null,project:o.project||null,repository:o.repository||null,branch:o.branch||null,commit:o.commit||null,starred:l?true:null,search:o.search||null,feedback:o.feedback||null,date_from:s||null,date_to:a||null},v=m(h=>{let T=J(h,k),N=Math.max(1,Math.ceil(T/Ze));return b=Math.min(b,N),{total:T,totalPages:N,lessons:B(h,{...k,sort:d,order:f,page:b,per_page:Ze}),allCategories:mt(h),allProjects:ae(h,"project"),allRepositories:ae(h,"repository"),allBranches:ae(h,"branch"),allCommits:ae(h,"commit_hash"),theme:E(h).ui_theme}}),g={period:y,category:o.category||"",level:n,project:o.project||"",repository:o.repository||"",branch:o.branch||"",commit:o.commit||"",starred:l,search:o.search||"",feedback:o.feedback||"",date_from:s,date_to:a,sort:d,order:f};return e.html(Lt({lessons:v.lessons,allCategories:v.allCategories,allProjects:v.allProjects,allRepositories:v.allRepositories,allBranches:v.allBranches,allCommits:v.allCommits,s:g,page:b,perPage:Ze,total:v.total,totalPages:v.totalPages,uiTheme:v.theme}))}),t.post("/lessons/:lesson_id/star",async e=>{let o=await e.req.parseBody();return m(r=>V(r,e.req.param("lesson_id"),o.starred==="1")),e.redirect(Qe(L(o,"next")||void 0),303)}),t.post("/lessons/:lesson_id/feedback",async e=>{let o=await e.req.parseBody(),r=L(o,"feedback"),n=r===""||r==="clear"?null:r;return m(s=>_e(s,e.req.param("lesson_id"),n)),e.redirect(Qe(L(o,"next")||void 0),303)}),t.get("/lessons/:lesson_id",e=>{let o=m(r=>K(r,e.req.param("lesson_id")));return o?e.html(Ot({lesson:o,uiTheme:mr()})):e.html("<h1>Lesson not found</h1>",404)}),t.get("/settings/export",()=>{let e=m(o=>we(o));return new Response(new Uint8Array(e),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),t.post("/settings/import",async e=>{let o=(await e.req.parseBody()).file;if(!(o instanceof File))return e.redirect("/settings?imported=0&skipped=0&invalid=1",303);let r=new Uint8Array(await o.arrayBuffer()),n=m(s=>le(s,r));return e.redirect(`/settings?imported=${n.lessons}&skipped=${n.skipped}&invalid=${n.invalid}&groups=${n.groups}¬ebook=${n.learning_state}`,303)}),t.get("/settings/notebook/download",()=>{let e=existsSync(w)?readFileSync(w,"utf8"):"";return new Response(e,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),t.post("/settings/notebook",async e=>{let o=await e.req.parseBody();return mkdirSync(dirname(w),{recursive:true}),writeFileSync(w,L(o,"content"),"utf8"),e.redirect(Qe(L(o,"next")||void 0,"/settings?notebook_saved=1"),303)}),t.post("/settings",async e=>{let o=await e.req.parseBody(),r=Number.parseInt(L(o,"max_per_day","2"),10)||2,n=Number.parseInt(L(o,"min_gap_minutes","240"),10),s=L(o,"ui_theme","system");return ["system","dark","light"].includes(s)||(s="system"),m(a=>{O(a,"max_per_day",String(r)),O(a,"min_gap_minutes",String(Number.isNaN(n)?240:n)),O(a,"ui_theme",s);}),e.redirect("/settings",303)}),t.get("/settings",e=>{let o=m(a=>E(a)),r=existsSync(w)?readFileSync(w,"utf8"):"",n=e.req.query(),s=n.imported!==void 0?{imported:Number(n.imported??0),skipped:Number(n.skipped??0),invalid:Number(n.invalid??0),groups:Number(n.groups??0),notebook:Number(n.notebook??0)}:null;return e.html(It({settings:o,notebookContent:r,notebookPath:w,uiTheme:o.ui_theme,flash:s}))}),t}function jt(t){let e=fr();serve({fetch:e.fetch,port:t,hostname:"127.0.0.1"},o=>{console.log(`devcoach UI running at http://localhost:${o.port}`);});}var yr=!!process.stdout.isTTY&&!process.env.NO_COLOR,X=(t,e)=>yr?styleText(t,e):e,i={green:t=>X("green",t),red:t=>X("red",t),yellow:t=>X("yellow",t),cyan:t=>X("cyan",t),magenta:t=>X("magenta",t),dim:t=>X("dim",t),bold:t=>X("bold",t)};function ot(t,e){return t==="green"?i.green(e):t==="yellow"?i.yellow(e):i.red(e)}var hr=/\[[0-9;]*m/g,xr=t=>t.replace(hr,""),Ee=t=>[...xr(t)].length;function At(t,e,o){let r=Math.max(0,e-Ee(t));if(o==="right")return " ".repeat(r)+t;if(o==="center"){let n=Math.floor(r/2);return " ".repeat(n)+t+" ".repeat(r-n)}return t+" ".repeat(r)}function oe(t,e,o,r=true){let n=e.map((f,b)=>Math.max(r?Ee(f.header):0,...o.map(y=>Ee(y[b]??"")))),s=(f,b,y)=>f+n.map(k=>"\u2500".repeat(k+2)).join(b)+y,a=f=>`\u2502${f.map((b,y)=>` ${At(b??"",n[y]??0,e[y]?.justify??"left")} `).join("\u2502")}\u2502`,l=n.reduce((f,b)=>f+b+3,1),d=[];t&&d.push(At(i.bold(t),l,"center")),d.push(s("\u256D","\u252C","\u256E")),r&&d.push(a(e.map(f=>i.bold(f.header))),s("\u251C","\u253C","\u2524"));for(let f of o)d.push(a(f));return d.push(s("\u2570","\u2534","\u256F")),d.join(`
|
|
568
|
+
`)}function Re(t=""){if(!t)return "\u2500".repeat(60);let o=` ${t} `,r=Math.max(2,60-Ee(o)),n=Math.floor(r/2);return "\u2500".repeat(n)+o+"\u2500".repeat(r-n)}var Pt=t=>{let e=Math.max(0,Math.min(10,Math.round(t)));return "\u2588".repeat(e)+"\u2591".repeat(10-e)},Mt=t=>t>=7?"green":t>=4?"yellow":"red";var u=(t="")=>{console.log(t);},Kt=t=>t==="junior"?i.green(t):t==="mid"?i.yellow(t):t==="senior"?i.red(t):t,Tr=t=>t==="know"?` ${i.green("\u2713")}`:t==="dont_know"?` ${i.red("\u2717")}`:"",Er=t=>t==="know"?i.green("\u2713 I know this"):t==="dont_know"?i.red("\u2717 I don't know this"):i.dim("no feedback");function zt(t,e){let o=new Map;for(let n of t.groups)for(let s of n.topics)o.set(s,n.name);let r=[...t.knowledge].sort((n,s)=>s.confidence-n.confidence).map(n=>{let s=Mt(n.confidence);return [n.topic,o.get(n.topic)??"Other",ot(s,`${n.confidence}/10`),ot(s,Pt(n.confidence))]});return oe(e,[{header:"Topic"},{header:"Group"},{header:"Confidence",justify:"right"},{header:"Bar"}],r)}function Rr(){let t=m(e=>M(e));u(zt(t,"Knowledge Map"));}function Cr(t){let e=m(s=>B(s,{period:t.period==="all"?null:t.period,category:t.category,level:t.level,project:t.project,repository:t.repository,branch:t.branch,commit:t.commit,starred:t.starred?!0:null,feedback:t.feedback,date_from:t.dateFrom,date_to:t.dateTo,sort:t.sort,order:t.order}));if(e.length===0){u(i.dim("No lessons found."));return}let o=e.some(s=>s.project||s.branch||s.commit_hash),r=[{header:""},{header:"Date"},{header:"Topic"},{header:"Title"},{header:"Level",justify:"center"},{header:"Categories"}];o&&r.push({header:"Project"},{header:"Branch"},{header:"Commit"});let n=e.map(s=>{let a=[s.starred?i.yellow("\u2605"):i.dim("\xB7"),s.timestamp.slice(0,10),i.cyan(s.topic_id),s.title+Tr(s.feedback),Kt(s.level),s.categories.join(", ")];return o&&a.push(s.project??"",s.branch?i.magenta(s.branch):"",s.commit_hash?i.cyan(s.commit_hash.slice(0,7)):""),a});u(oe("Lessons",r,n));}function Lr(t){let e=m(r=>K(r,t));e===null&&(u(i.red(`Lesson '${t}' not found.`)),process.exit(1)),u(Re(i.bold(e.title))),u(`${i.dim("ID:")} ${e.id}`),u(`${i.dim("Date:")} ${e.timestamp.slice(0,19).replace("T"," ")}`),u(`${i.dim("Topic:")} ${e.topic_id}`),u(`${i.dim("Categories:")} ${e.categories.join(", ")}`),u(`${i.dim("Level:")} ${Kt(e.level)}`);let o=e.starred?i.yellow("\u2605 starred"):i.dim("\u2606 not starred");if(u(`${i.dim("Star:")} ${o} ${i.dim("Feedback:")} ${Er(e.feedback)}`),e.task_context&&u(`${i.dim("Context:")} ${e.task_context}`),e.project||e.repository||e.branch||e.commit_hash||e.folder){let r=[];e.project&&r.push(`project=${e.project}`),e.repository&&r.push(`repo=${e.repository}`),e.branch&&r.push(`branch=${i.magenta(e.branch)}`),e.commit_hash&&r.push(`commit=${i.cyan(e.commit_hash.slice(0,7))}`),e.folder&&r.push(`folder=${e.folder}`),u(`${i.dim("Git:")} ${r.join(" \xB7 ")}`);}u(Re()),u(e.summary);}function Ht(t,e){m(r=>K(r,t)===null?!1:(V(r,t,e),!0))||(u(i.red(`Lesson '${t}' not found.`)),process.exit(1)),u(e?`Lesson ${i.cyan(t)} \u2192 ${i.yellow("\u2605 starred")}`:`Lesson ${i.cyan(t)} \u2192 ${i.dim("\u2606 unstarred")}`);}function Or(t){m(o=>xe(o,t))||(u(i.red(`Lesson '${t}' not found.`)),process.exit(1)),u(`Lesson ${i.cyan(t)} deleted.`);}function Ir(t,e){["know","dont_know","clear"].includes(e)||(u(i.red(`Invalid feedback '${e}'. Use: know | dont_know | clear`)),process.exit(1));let o=e==="clear"?null:e,r=m(a=>{let l=_e(a,t,o);if(l===null)return null;let d=a.prepare("SELECT confidence FROM knowledge WHERE topic = ?").get(l);return {topicId:l,newConf:d?d.confidence:5}});r===null&&(u(i.red(`Lesson '${t}' not found.`)),process.exit(1));let n;if(o==="know"||o==="dont_know"){let a=r.newConf+(o==="know"?-1:1);n=`${i.cyan(r.topicId)} confidence: ${a} \u2192 ${i.bold(String(r.newConf))}`;}else n="feedback cleared";let s=o==="know"?i.green("\u2713 I know this"):o==="dont_know"?i.red("\u2717 I don't know this"):i.dim("cleared");u(`Lesson ${i.cyan(t)} \u2192 ${s} (${n})`);}function Nr(){let t=m(n=>E(n)),e=Math.floor(t.min_gap_minutes/60),o=t.min_gap_minutes%60,r=e?`${e}h ${o}m`:`${o}m`;u(oe("Settings",[{header:"Key"},{header:"Value",justify:"right"}],[["max_per_day",String(t.max_per_day)],["min_gap_minutes",`${t.min_gap_minutes} (${r})`]]));}function Dr(){let{stats:t,rateLimit:e,settings:o}=m(l=>({stats:W(l),rateLimit:z(l),settings:E(l)})),r=l=>Number(t[l]??0),n=e.allowed?i.green("Available now"):i.yellow(e.reason??"");u(oe("Coaching Stats",[{header:"Metric"},{header:"Value",justify:"right"}],[["Total lessons",String(r("total_lessons"))],["Lessons today (24h)",`${r("lessons_today")} / ${o.max_per_day}`],["Lessons this week",String(r("lessons_this_week"))],["Next lesson",n]],false));let s=t.weakest_topics??[],a=t.strongest_topics??[];if(s.length||a.length){let l=[];for(let d=0;d<Math.max(s.length,a.length);d++){let f=s[d],b=a[d];l.push([f?`${i.red(f.topic)} ${i.dim(`(${f.confidence})`)}`:"",b?`${i.green(b.topic)} ${i.dim(`(${b.confidence})`)}`:""]);}u(oe(void 0,[{header:"Weakest topics"},{header:"Strongest topics"}],l));}}function jr(t,e){["max_per_day","min_gap_minutes"].includes(t)||(u(i.red(`Unknown key '${t}'. Valid keys: max_per_day, min_gap_minutes`)),process.exit(1)),m(o=>O(o,t,e)),u(i.green(`Set ${t} = ${e}`));}function Ar(t,e,o){let r=t.trim();r||(u(i.red("Topic name must not be empty.")),process.exit(1)),m(s=>{I(s,r,e),o&&o!=="Other"&&D(s,r,o);});let n=o&&o!=="Other"?` \u2192 ${i.cyan(o)}`:"";u(`${i.green("Added")} ${i.bold(r)} (confidence ${i.cyan(String(e))})${n}`);}function Pr(t){let e=m(o=>Z(o,t));u(e?`${i.green("Removed")} ${i.bold(t)} from knowledge map`:i.yellow(`Topic '${t}' not found.`));}function Mr(t){let e=t.trim();(!e||e==="Other")&&(u(i.red("Invalid group name.")),process.exit(1)),m(o=>P(o,e)),u(`${i.green(`Group '${i.cyan(e)}' ready.`)} Assign topics with: devcoach group-assign <topic> "${e}"`);}function Fr(t){let e=m(o=>Q(o,t));u(e?`${i.green(`Removed group '${i.cyan(t)}'`)}`:i.yellow(`Group '${t}' not found.`));}function Hr(t,e){m(r=>r.prepare("SELECT topic FROM knowledge WHERE topic = ?").get(t)===void 0?!1:(e==="Other"?ve(r,t):D(r,t,e),!0))||(u(i.red(`Topic '${t}' not in knowledge map. Add it first.`)),process.exit(1)),u(e==="Other"?`${i.green("Moved")} ${i.bold(t)} \u2192 Other (ungrouped)`:`${i.green("Moved")} ${i.bold(t)} \u2192 ${i.cyan(e)}`);}function Ur(t){let{lessonsCount:e,knowledgeCount:o,data:r}=m(s=>({lessonsCount:ie(s).length,knowledgeCount:de(s).length,data:we(s)}));writeFileSync(t,r);let n=existsSync(w)?" + notebook":"";u(`${i.green("Backup saved:")} ${t} (${i.cyan(String(e))} lessons, ${i.cyan(String(o))} topics${n})`);}function Gr(t){existsSync(t)||(u(i.red(`File not found: ${t}`)),process.exit(1));let e=m(r=>le(r,readFileSync(t)));e.settings&&u(`${i.green("\u2713")} Settings restored`),e.topics&&u(`${i.green("\u2713")} Knowledge map restored (${i.cyan(String(e.topics))} topics)`);let o=[`${i.cyan(String(e.lessons))} imported`];e.skipped&&o.push(`${i.yellow(String(e.skipped))} duplicates skipped`),e.invalid&&o.push(`${i.red(String(e.invalid))} rejected (invalid)`),u(`${i.green("\u2713")} Lessons: ${o.join(", ")}`),e.learning_state&&u(`${i.green("\u2713")} Notebook restored`);}function Br(){let t=platform();if(t==="darwin")return join(homedir(),"Library","Application Support","Claude","claude_desktop_config.json");if(t==="win32"){let o=process.env.APPDATA??join(homedir(),"AppData","Roaming");return join(o,"Claude","claude_desktop_config.json")}let e=process.env.XDG_CONFIG_HOME??join(homedir(),".config");return join(e,"Claude","claude_desktop_config.json")}var Kr=join(homedir(),".claude","settings.json");function qt(t){for(let e of (process.env.PATH??"").split(":"))if(e)try{return accessSync(join(e,t),constants.X_OK),!0}catch{}return false}function nt(){return qt("devcoach")?{command:"devcoach",args:["mcp"]}:{command:"npx",args:["-y","devcoach","mcp"]}}function zr(){return nt().command==="devcoach"?"devcoach":"npx -y devcoach"}function qr(t,e){if(!qt("claude"))return "";e&&spawnSync("claude",["mcp","remove","--scope",t,"devcoach"],{encoding:"utf8"});let o=nt(),r=spawnSync("claude",["mcp","add","--scope",t,"devcoach",o.command,"--",...o.args],{encoding:"utf8"});return r.status===0?`${i.green("\u2713")} Registered via \`claude mcp add\` (scope: ${t})`:`${r.stderr??""}${r.stdout??""}`.toLowerCase().includes("already")?`${i.yellow("Already registered")} in Claude Code (use --force to overwrite)`:`${i.red("claude mcp add failed:")} ${(r.stderr||r.stdout||"").trim()}`}function Wt(t){if(!existsSync(t))return {ok:true,data:{}};try{return {ok:!0,data:JSON.parse(readFileSync(t,"utf8"))??{}}}catch{return {ok:false,error:`${i.red("\u2717")} ${t} is not valid JSON \u2014 fix it and re-run (left unchanged)`}}}function Ut(t,e,o){let r=Wt(t);if(!r.ok)return r.error;let n=r.data;n.mcpServers??={};let s=n.mcpServers;return s.devcoach&&!o?`${i.yellow("Already registered")} in ${t} (use --force to overwrite)`:(s.devcoach=e,mkdirSync(dirname(t),{recursive:true}),writeFileSync(t,`${JSON.stringify(n,null,2)}
|
|
569
|
+
`),`${i.green("\u2713")} Installed into ${t}`)}function Wr(t){let e=zr(),o=Kr,r=Wt(o);if(!r.ok)return r.error;let n=r.data;n.hooks??={};let s=n.hooks;s.Stop??=[];let a=s.Stop,l=a.map((d,f)=>[d,f]).filter(([d])=>(d.hooks??[]).some(f=>(f.command??"").includes("devcoach"))).map(([,d])=>d);if(l.length){if(!t)return `${i.yellow("Stop hooks already installed")} in ${o} (use --force to overwrite)`;for(let d of l.toReversed())a.splice(d,1);}return a.push({hooks:[{type:"command",command:`${e} onboard-hook`}]},{hooks:[{type:"command",command:`${e} lesson-ready`}]}),mkdirSync(dirname(o),{recursive:true}),writeFileSync(o,`${JSON.stringify(n,null,2)}
|
|
570
|
+
`),`${i.green("\u2713")} Stop hooks installed into ${o}`}function Xr(t){let e=t.claudeCode||!t.claudeDesktop,o=t.claudeDesktop||!t.claudeCode,r=nt(),n=false;if(u(i.bold("Setting up devcoach")+i.dim(` (${r.command} ${r.args.join(" ")})`)),u(),e){u(i.bold("Claude Code"));let s=qr("user",t.force);if(!s){let a=join(homedir(),".claude.json");s=Ut(a,{type:"stdio",env:{},...r},t.force),n=true;}u(` MCP server\u2026 ${s}`),t.skipHook||u(` Stop hooks\u2026 ${Wr(t.force)}`),u();}o&&(u(i.bold("Claude Desktop")),u(` MCP server\u2026 ${Ut(Br(),r,t.force)}`),n=true,u()),n&&u(`${i.yellow("\u2192")} Restart Claude Desktop to pick up the new server.
|
|
566
571
|
`),u(i.dim(`Tip: run devcoach backup to export your profile, lessons and settings.
|
|
567
572
|
run devcoach restore <file> to import a backup on a new machine.
|
|
568
|
-
The coaching skill is served automatically via the MCP prompt \u2014 always up to date.`));}async function
|
|
569
|
-
${i.bold("Step 1")} \u2014 Restore from backup`);let r=await e("Path to existing backup zip (Enter to skip)","");if(r){existsSync(r)||(u(i.red(`File not found: ${r}`)),process.exit(1));let b=m(y=>
|
|
570
|
-
${i.bold("Step 2")} \u2014 Build your knowledge profile`);let n=(await e("Mode: [a]utomatic (detect from files) / [m]anual (type your stack)","a")).toLowerCase(),s={};if(n.startsWith("a")){let y=
|
|
573
|
+
The coaching skill is served automatically via the MCP prompt \u2014 always up to date.`));}async function Yr(){let t=createInterface({input:process.stdin,output:process.stdout}),e=async(r,n="")=>{let s=n?` [${n}]`:"";return (await t.question(`${r}${s}: `)).trim()||n},o=async(r,n,s,a)=>{for(;;){let l=await e(r,String(n)),d=Number.parseInt(l,10);if(!Number.isNaN(d)&&d>=s&&d<=a)return d;u(i.red(`Must be ${s}\u2013${a}.`));}};try{u(Re(i.bold("devcoach setup"))),u(`
|
|
574
|
+
${i.bold("Step 1")} \u2014 Restore from backup`);let r=await e("Path to existing backup zip (Enter to skip)","");if(r){existsSync(r)||(u(i.red(`File not found: ${r}`)),process.exit(1));let b=m(y=>le(y,readFileSync(r)));u(`${i.green("\u2713")} Restored: ${b.topics} topics, ${b.lessons} lessons${b.learning_state?", notebook":""}`),u(i.green("Setup complete!"));return}u(`
|
|
575
|
+
${i.bold("Step 2")} \u2014 Build your knowledge profile`);let n=(await e("Mode: [a]utomatic (detect from files) / [m]anual (type your stack)","a")).toLowerCase(),s={};if(n.startsWith("a")){let y=ee().folder??process.cwd(),k=$e(y),v=Object.keys(k).sort((h,T)=>h.localeCompare(T));if(v.length){u(`
|
|
571
576
|
${i.dim(`Detected from ${i.cyan(y)}:`)}`);for(let h of v){let T=k[h]??5,N=await e(` ${i.cyan(h)} (Enter=keep, 0-10=override, s=skip)`,String(T));if(N.toLowerCase()==="s")continue;let me=Number.parseInt(N,10);s[h]=Number.isNaN(me)?T:Math.max(0,Math.min(10,me));}}else u(i.dim("No technology files detected in current directory."));let g=await e(`
|
|
572
577
|
Add additional topics (comma-separated, or Enter to skip)`,"");for(let h of g.split(",").map(T=>T.trim()).filter(Boolean))s[h]=await o(` Confidence for ${i.cyan(h)}`,5,0,10);}else for(u(i.dim(`
|
|
573
578
|
Enter topics: 'topic_id confidence' (e.g. python 7). Blank line when done.`));;){let b=(await e("Topic (Enter when done)","")).trim();if(!b)break;let y=b.split(/\s+/),k=y[0]??"",v=Number.parseInt(y[1]??"",10),g=Number.isNaN(v)?5:Math.max(0,Math.min(10,v));s[k]=g,u(` ${i.green("+")} ${i.cyan(k)} \u2192 ${g}`);}let a={};if(Object.keys(s).length&&(u(`
|
|
574
579
|
${i.bold("Step 3")} \u2014 Organise into groups`),(await e("Organise topics into groups? [y/N]","n")).toLowerCase().startsWith("y"))){let y=[];for(let k of Object.keys(s).sort((v,g)=>v.localeCompare(g))){let v=y.length?y.join(", "):"(none yet)",g=await e(` Group for ${i.cyan(k)} existing: ${i.dim(v)} (Enter=Other)`,"");if(g&&g!=="Other"){let h=a[g]??[];h.push(k),a[g]=h,y.includes(g)||y.push(g);}}}u(`
|
|
575
|
-
${i.bold("Step 4")} \u2014 Rate-limit settings`);let l=await o("Max lessons per day",2,1,20),d=await o("Min gap between lessons (minutes)",240,0,1440),f=m(b=>{b.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[y,k]of Object.entries(s))
|
|
576
|
-
${i.green("Setup complete!")} ${Object.keys(s).length} topics saved.`);}finally{t.close();}}function
|
|
577
|
-
`),process.exit(0);}
|
|
578
|
-
`),qt("devcoach: the user has no coaching profile yet. Ask them how they want to set it up \u2014 Automatic (detect this project's tech stack), Guided (a short conversation to map topics and confidence levels), or Import (restore from a backup) \u2014 and 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 Yr(){zt()&&process.exit(0);let t;try{t=m(e=>de(e).knowledge_ready?z(e).allowed:!1);}catch{process.exit(0);}t||process.exit(0),qt(`devcoach: a lesson is due for the technical work just completed. Act now \u2014 do not acknowledge this message and do not explain what you are about to do.
|
|
580
|
+
${i.bold("Step 4")} \u2014 Rate-limit settings`);let l=await o("Max lessons per day",2,1,20),d=await o("Min gap between lessons (minutes)",240,0,1440),f=m(b=>{b.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[y,k]of Object.entries(s))I(b,y,k);for(let[y,k]of Object.entries(a))for(let v of k)D(b,v,y);return O(b,"max_per_day",String(l)),O(b,"min_gap_minutes",String(d)),M(b)});u(zt(f,"Knowledge Profile")),u(`
|
|
581
|
+
${i.green("Setup complete!")} ${Object.keys(s).length} topics saved.`);}finally{t.close();}}function Jr(t){try{let e=JSON.parse(t);return {stop_hook_active:e?.stop_hook_active===!0,permission_mode:typeof e?.permission_mode=="string"?e.permission_mode:null}}catch{return {stop_hook_active:false,permission_mode:null}}}function Xt(){let t={stop_hook_active:false,permission_mode:null};try{let e=fstatSync(0);return !e.isFIFO()&&!e.isFile()&&!e.isSocket()?t:Jr(readFileSync(0,"utf8"))}catch{return t}}function Yt(t){return t.stop_hook_active||t.permission_mode==="plan"}function Jt(t){process.stdout.write(`${JSON.stringify({decision:"block",reason:t})}
|
|
582
|
+
`),process.exit(0);}function Vr(){if(Yt(Xt())&&process.exit(0),existsSync(Y)){let t;try{t=m(e=>ce(e).knowledge_ready);}catch{process.exit(0);}t&&process.exit(0);}Jt("devcoach: the user has no coaching profile yet. Ask them how they want to set it up \u2014 Automatic (detect this project's tech stack), Guided (a short conversation to map topics and confidence levels), or Import (restore from a backup) \u2014 and 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.");}var ge=10;function Zr(){Yt(Xt())&&process.exit(0),existsSync(Y)||process.exit(0);let t;try{t=m(s=>ce(s).knowledge_ready?{allowed:z(s).allowed,nextLessonNumber:Number(W(s).total_lessons??0)+1}:{allowed:!1,nextLessonNumber:0});}catch{process.exit(0);}t.allowed||process.exit(0);let{nextLessonNumber:e}=t,o=e%ge===0,r=Math.ceil(e/ge)*ge,n=o?`6. This is lesson #${e} \u2014 a notebook checkpoint (every ${ge} lessons). After log_lesson returns the feedback, call update_notebook with the revised notebook markdown: fold in what the user absorbed or struggled with and any new pattern, keep the prior notes, and record "(updated after ${e} lessons)".`:`6. Do NOT call update_notebook this time \u2014 the notebook is refreshed only every ${ge} lessons (next checkpoint at lesson #${r}). Just deliver the card and call log_lesson.`;Jt(`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.
|
|
579
583
|
|
|
580
|
-
1. Read the MCP resources devcoach://profile
|
|
584
|
+
1. Read the MCP resources devcoach://profile, devcoach://taught-topics, and devcoach://notebook.
|
|
581
585
|
2. Decide whether the work just completed warrants ONE lesson: writing or reviewing code, commits, debugging, config, queries, or infra = yes; pure questions, chat, or non-technical replies = no. If no, output nothing further and stop.
|
|
582
|
-
3.
|
|
586
|
+
3. Use the notebook to choose WHAT to teach: skip topics it marks as absorbed, prefer its 'Recommended focus', calibrate depth from 'Recurring patterns', and watch its 'Open hypotheses'. Pick ONE devcoach://profile topic NOT already in devcoach://taught-topics, pitched at or above the user's confidence band.
|
|
587
|
+
4. FIRST write the lesson as your visible reply \u2014 in EXACTLY this format. It is plain markdown between two band headings; do NOT prefix lines with '>' (no blockquote):
|
|
583
588
|
|
|
584
589
|
### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u{1F393} devcoach \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
590
|
+
**[Title]** \xB7 [Category] \xB7 [Junior|Mid|Senior]
|
|
591
|
+
|
|
592
|
+
[3\u20136 short paragraphs: explain the why, tie it to the task; fenced code if useful]
|
|
593
|
+
|
|
594
|
+
\u{1F4A1} *Senior tip:* [one line]
|
|
589
595
|
### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 [topic] \xB7 [level] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
590
596
|
|
|
591
|
-
|
|
597
|
+
5. ONLY AFTER the card is written, call log_lesson. Put title/topic_id/categories/level/summary in their own fields; body = ONLY the prose + the \u{1F4A1} tip as CLEAN markdown (no bands, no '>' quote, no title or 'Category \xB7 Level' line). log_lesson then asks the user 'Did that land?', so the card MUST be printed first.
|
|
598
|
+
${n}
|
|
592
599
|
|
|
593
|
-
The rate-limit check is already done by this hook \u2014 skip it. Output only the lesson card (or nothing). No preamble, no meta-commentary.`);}function
|
|
594
|
-
${i.bold(`devcoach ${i.dim(`v${
|
|
600
|
+
The rate-limit check is already done by this hook \u2014 skip it. Output only the lesson card (or nothing). No preamble, no meta-commentary.`);}function Qr(){let t=[["mcp","Start the MCP server (stdio) for Claude Code / Claude Desktop"],["ui [--port N]","Launch the web dashboard (default port: 7860)"],["setup","First-run wizard: import backup or build your knowledge profile"],["install","Register the MCP server + Stop hook in Claude Code / Claude Desktop"],["onboard-hook","Claude Code Stop hook: cue onboarding when no profile exists"],["lesson-ready","Claude Code Stop hook: cue a lesson when one is due"],["profile","Show the knowledge map"],["stats","Coaching statistics and rate-limit status"],["settings / set","Show / update settings (max_per_day | min_gap_minutes)"],["lessons / lesson","List past lessons / show one in detail"],["star / unstar / delete","Manage a lesson"],["feedback <id>","Record know / dont_know feedback"],["knowledge-add / -remove","Add / remove a topic"],["group-add / -remove / -assign","Manage knowledge groups"],["backup / restore","Export / import a full backup zip"]];u(`
|
|
601
|
+
${i.bold(`devcoach ${i.dim(`v${pe}`)}`)}`);for(let[e,o]of t)u(` ${i.cyan(`devcoach ${e}`.padEnd(32))} ${i.dim(o)}`);u(`
|
|
595
602
|
${i.dim("Run")} ${i.bold("devcoach <command> --help")} ${i.dim("for per-command options.")}
|
|
596
|
-
`);}function A(t){return t??null}function
|
|
603
|
+
`);}function A(t){return t??null}function en(){let t=new Command;return t.exitOverride(),t.name("devcoach").description("devcoach \u2014 progressive technical coaching").version(`devcoach ${pe}`,"-v, --version","output the version number").allowExcessArguments(false).showHelpAfterError("(add --help for usage)").action(()=>Qr()),t.command("mcp").description("Start the MCP server (stdio transport)").action(St),t.command("profile").description("Show the knowledge map").action(Rr),t.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(e=>Cr({period:e.period??"all",category:A(e.category),project:A(e.project),repository:A(e.repository),branch:A(e.branch),commit:A(e.commit),starred:!!e.starred,feedback:A(e.feedback),level:A(e.level),dateFrom:A(e.dateFrom),dateTo:A(e.dateTo),sort:e.sort??"timestamp",order:e.order??"desc"})),t.command("lesson").description("Show a single lesson in detail").argument("<id>","Lesson ID").action(e=>Lr(e)),t.command("star").description("Mark a lesson as starred").argument("<id>","Lesson ID").action(e=>Ht(e,true)),t.command("unstar").description("Remove the starred mark from a lesson").argument("<id>","Lesson ID").action(e=>Ht(e,false)),t.command("delete").description("Permanently delete a lesson").argument("<id>","Lesson ID").action(e=>Or(e)),t.command("feedback").description("Record know/dont_know feedback for a lesson").argument("<id>","Lesson ID").argument("<value>","know | dont_know | clear").action((e,o)=>Ir(e,o)),t.command("settings").description("Show current settings").action(Nr),t.command("stats").description("Coaching statistics and rate-limit status").action(Dr),t.command("set").description("Update a setting (max_per_day | min_gap_minutes)").argument("<key>","Setting key").argument("<value>","New value").action((e,o)=>jr(e,o)),t.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((e,o)=>Ar(e,Math.max(0,Math.min(10,Number.parseInt(o.confidence??"5",10)||5)),A(o.group))),t.command("knowledge-remove").description("Remove a topic from the knowledge map").argument("<topic>","Topic ID").action(e=>Pr(e)),t.command("group-add").description("Register a new knowledge group").argument("<name>","Group name").action(e=>Mr(e)),t.command("group-remove").description("Delete a knowledge group (topics move to Other)").argument("<name>","Group name").action(e=>Fr(e)),t.command("group-assign").description("Move a topic to a group").argument("<topic>","Topic ID").argument("<group>","Group name (use 'Other' to ungroup)").action((e,o)=>Hr(e,o)),t.command("backup").description("Export a full backup (settings + knowledge + lessons) as zip").argument("[file]","Output zip path","devcoach-backup.zip").action(e=>Ur(e)),t.command("restore").description("Restore from a backup zip file").argument("<file>","Path to backup zip").action(e=>Gr(e)),t.command("install").description("Register the MCP server + Stop hooks in Claude Code / Claude Desktop").option("--claude-code","Target Claude Code only").option("--claude-desktop","Target Claude Desktop only").option("--force","Overwrite existing devcoach entry").option("--skip-hook","Register MCP server only \u2014 skip the Stop hooks").action(e=>Xr({claudeCode:!!e.claudeCode,claudeDesktop:!!e.claudeDesktop,force:!!e.force,skipHook:!!e.skipHook})),t.command("ui").description("Launch the web dashboard").option("--port <port>","Port","7860").action(e=>jt(Number.parseInt(e.port??"7860",10)||7860)),t.command("setup").description("First-run wizard: import backup or build your knowledge profile").action(Yr),t.command("onboard-hook",{hidden:true}).description("Claude Code Stop hook: cue onboarding").action(Vr),t.command("lesson-ready",{hidden:true}).description("Claude Code Stop hook: signal a lesson is due").action(Zr),t}async function Vt(){let t=en();try{await t.parseAsync(process.argv);}catch(e){let o=e.code??"";if(!o.startsWith("commander."))throw e;if(o==="commander.helpDisplayed"||o==="commander.help"||o==="commander.version")return;process.exit(2);}}try{await Vt();}catch(t){console.error(t),process.exit(1);}
|
package/package.json
CHANGED