devcoach 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/bin.js +78 -55
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -457,13 +457,13 @@ For Claude Desktop, delete the `devcoach` key from the platform config file (pat
457
457
 
458
458
  **Full documentation:** The README is a quick start. For detailed guides, visit the **[official docs](https://ultimaphoenix.github.io/dev-coach/)**:
459
459
 
460
- - **[Why devcoach exists](docs/why.md)** — the philosophy and market opportunity
460
+ - **[Why devcoach exists](docs/why.md)** — the philosophy
461
461
  - **[How it works](docs/how-it-works.md)** — session startup, coaching loop, lesson selection
462
462
  - **[Using the web dashboard](docs/usage/web-ui.md)** — personalise your knowledge map, filter lessons, jump to code context
463
463
  - **[CLI reference](docs/usage/cli.md)** — all commands for querying and managing data
464
464
  - **[Configuration](docs/reference/configuration.md)** — rate limits, data location, schema
465
465
  - **[Privacy & security](docs/reference/privacy.md)** — local-first architecture, what we collect
466
- - **[Vision & roadmap](docs/vision.md)** — free tier today, courses and team SaaS planned
466
+ - **[Vision & roadmap](docs/vision.md)** — free, local, not commercialized; ideas we're exploring next
467
467
  - **[Plugin marketplace](docs/install/claude-code-plugin.md#about-the-personal-marketplace)** — UltimaPhoenix plugin collection
468
468
 
469
469
  ---
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"]),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(`
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 be=z$1.enum(["junior","mid","senior"]),fe=z$1.enum(["github","gitlab","bitbucket","local"]),Le=z$1.enum(["know","dont_know"]),Ne=z$1.number().int().min(0).max(10);function to(t){return t.toISOString().replace(/\.\d{3}Z$/,"Z")}function oo(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 ro(t){let e=oo(t),o=new Date;return e.getTime()>o.getTime()&&(e=o),to(e)}var no=z$1.union([z$1.string(),z$1.date()]).transform((t,e)=>{try{return ro(t)}catch(o){return e.addIssue({code:"custom",message:o.message}),z$1.NEVER}}),G=z$1.string().nullish().transform(t=>t??null),so=z$1.object({id:z$1.string(),timestamp:no,topic_id:z$1.string(),categories:z$1.array(z$1.string()),title:z$1.string(),level:be,summary:z$1.string(),body:G,task_context:G,project:G,repository:G,branch:G,commit_hash:G,folder:G,repository_platform:fe.nullish().transform(t=>t??null),starred:z$1.boolean().default(false),feedback:Le.nullish().transform(t=>t??null)}),io=z$1.object({topic:z$1.string(),confidence:z$1.number().int()}),ao=z$1.object({name:z$1.string(),topics:z$1.array(z$1.string())});z$1.object({knowledge:z$1.array(io),groups:z$1.array(ao)});var Oe=z$1.enum(["system","dark","light"]),Ie=z$1.enum(["session","global"]);z$1.object({max_per_day:z$1.number().int().default(2),min_gap_minutes:z$1.number().int().default(240),ui_theme:Oe.default("system"),nudge_every:z$1.number().int().min(0).default(10),nudge_scope:Ie.default("session")});function ne(t){return so.parse(t)}var{DatabaseSync:fo}=createRequire(import.meta.url)("node:sqlite"),dt=join(homedir(),".devcoach"),Y=join(dt,"coaching.db"),w=join(dt,"learning-state.md"),De="settings.json",Ae="lessons.json",je="knowledge.json",Pe="learning-state.md",ct={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},yo={max_per_day:"2",min_gap_minutes:"240",ui_theme:"system",nudge_every:"10",nudge_scope:"session"},ho=50;function A(t,e,...o){return t.prepare(e).all(...o)}function P(t,e,...o){return t.prepare(e).get(...o)}function _(t,e,...o){return Number(t.prepare(e).run(...o).changes)}function xo(t=Y){return t!==":memory:"&&mkdirSync(dirname(t),{recursive:true}),new fo(t)}function ko(t=Y){let e=xo(t);return vo(e),e}function m(t,e=Y){let o=ko(e);try{return t(o)}finally{o.close();}}function vo(t){t.exec(`
3
3
  CREATE TABLE IF NOT EXISTS lessons (
4
4
  id TEXT PRIMARY KEY,
5
5
  timestamp TEXT NOT NULL,
@@ -41,17 +41,25 @@ import {spawn,spawnSync,execFileSync}from'child_process';import {existsSync,writ
41
41
  PRIMARY KEY (group_name, topic)
42
42
  );
43
43
 
44
+ -- Runtime-only: per-session interaction counter for lesson-cue pacing.
45
+ -- Never exported/imported (backup carries config, not this state).
46
+ CREATE TABLE IF NOT EXISTS nudge_state (
47
+ session_id TEXT PRIMARY KEY,
48
+ interactions INTEGER NOT NULL DEFAULT 0,
49
+ updated_at TEXT NOT NULL
50
+ );
51
+
44
52
  CREATE INDEX IF NOT EXISTS idx_lessons_timestamp ON lessons (timestamp);
45
53
  CREATE INDEX IF NOT EXISTS idx_lessons_starred_ts ON lessons (starred, timestamp);
46
54
  CREATE INDEX IF NOT EXISTS idx_lessons_feedback ON lessons (feedback);
47
55
  CREATE INDEX IF NOT EXISTS idx_lessons_topic_id ON lessons (topic_id);
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.6.0"};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(`
56
+ `),wo(t),_o(t);}function wo(t){try{t.exec("ALTER TABLE lessons ADD COLUMN body TEXT");}catch{}}function _o(t){for(let[e,o]of Object.entries(yo))_(t,"INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)",e,o);}var lt="(id, timestamp, topic_id, categories, title, level, summary, body, task_context, project, repository, branch, commit_hash, folder, repository_platform, starred, feedback)",pt="(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";function gt(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 ut(t,e){_(t,`INSERT OR REPLACE INTO lessons ${lt} VALUES ${pt}`,...gt(e));}var So=[["project","project"],["repository","repository"],["branch","branch"],["commit","commit_hash"]];function $o(t,e){if(t.date_from==null&&t.date_to==null){let o=Oo(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 mt(t){let e=[],o=[],r=(s,...a)=>{e.push(s),o.push(...a);};$o(t,r),t.category!=null&&r("categories LIKE ?",`%"${t.category}"%`),t.level!=null&&r("level = ?",t.level);for(let[s,a]of So){let c=t[s];c!=null&&r(`${a} LIKE ?`,`%${c}%`);}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}=mt(e),n=P(t,`SELECT COUNT(*) AS n FROM lessons ${o}`,...r);return Number(n?.n??0)}var To=new Set(["timestamp","level","topic_id","title","feedback"]),Eo=new Set(["project","repository","branch","commit_hash","repository_platform"]);function B(t,e={}){let{where:o,params:r}=mt(e),n=e.sort&&To.has(e.sort)?e.sort:"timestamp",s=(e.order??"desc").toLowerCase()==="asc"?"ASC":"DESC",a=`SELECT * FROM lessons ${o} ORDER BY ${n} ${s}`,c=[...r];if(e.page!=null){let d=e.per_page??25;a+=" LIMIT ? OFFSET ?",c.push(d,(e.page-1)*d);}return A(t,a,...c).map(Be)}function xe(t,e){return _(t,"DELETE FROM lessons WHERE id = ?",e)>0}function V(t,e,o){return _(t,"UPDATE lessons SET starred = ? WHERE id = ?",o?1:0,e)>0}function se(t,e,o){_(t,"UPDATE lessons SET feedback = ? WHERE id = ?",o||null,e);let r=P(t,"SELECT topic_id FROM lessons WHERE id = ?",e);return r?r.topic_id:null}function ie(t){return A(t,"SELECT * FROM lessons ORDER BY timestamp DESC").map(Be)}function Fe(t,e){let o=0,r=0,n=0;for(let s of e){let a;try{a=ne(s);}catch{n+=1;continue}_(t,`INSERT OR IGNORE INTO lessons ${lt} VALUES ${pt}`,...gt(a))>0?o+=1:r+=1;}return {inserted:o,duplicated:r,invalid:n}}function ae(t,e){if(!Eo.has(e))throw new Error(`Column not allowed: ${JSON.stringify(e)}`);return A(t,`SELECT DISTINCT ${e} FROM lessons WHERE ${e} IS NOT NULL ORDER BY ${e}`).map(r=>r[e])}function K(t,e){let o=P(t,"SELECT * FROM lessons WHERE id = ?",e);return o?Be(o):null}function bt(t){return A(t,"SELECT DISTINCT value FROM lessons, json_each(lessons.categories) ORDER BY value").map(o=>o.value)}function ft(t){return A(t,"SELECT DISTINCT topic_id FROM lessons").map(e=>e.topic_id)}function ke(t,e){let o=P(t,"SELECT COUNT(*) AS n FROM lessons WHERE timestamp >= ?",e);return Number(o?.n??0)}function yt(t){let e=P(t,"SELECT timestamp FROM lessons ORDER BY timestamp DESC LIMIT 1");return e?e.timestamp:null}function He(t){let e={};for(let o of A(t,"SELECT topic, confidence FROM knowledge"))e[o.topic]=Number(o.confidence);return e}function de(t){return A(t,"SELECT topic, confidence FROM knowledge ORDER BY topic").map(e=>({topic:e.topic,confidence:Number(e.confidence)}))}function Ue(t){let e=A(t,"SELECT group_name FROM knowledge_group_names ORDER BY group_name").map(r=>r.group_name),o=new Map;for(let r of A(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 O(t,e,o){let r=Math.max(0,Math.min(10,o)),n=new Date().toISOString();_(t,`INSERT INTO knowledge (topic, confidence, updated_at) VALUES (?, ?, ?)
57
+ ON CONFLICT(topic) DO UPDATE SET confidence = excluded.confidence, updated_at = excluded.updated_at`,e,r,n);}function Z(t,e){let o=_(t,"DELETE FROM knowledge WHERE topic = ?",e);return _(t,"DELETE FROM knowledge_groups WHERE topic = ?",e),o>0}function M(t,e){let o=e.trim();if(!o)throw new Error("Group name must not be empty");return _(t,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",o)>0}function Q(t,e){return _(t,"DELETE FROM knowledge_groups WHERE group_name = ?",e),_(t,"DELETE FROM knowledge_group_names WHERE group_name = ?",e)>0}function D(t,e,o){_(t,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",o),_(t,"DELETE FROM knowledge_groups WHERE topic = ?",e),_(t,"INSERT OR IGNORE INTO knowledge_groups (group_name, topic) VALUES (?, ?)",o,e);}function ve(t,e){_(t,"DELETE FROM knowledge_groups WHERE topic = ?",e);}function L(t){let e={};for(let y of A(t,"SELECT key, value FROM settings"))e[y.key]=y.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",c=a==="dark"||a==="light"||a==="system"?a:"system",d=e.nudge_every,b=d!==void 0?Number.parseInt(d,10):10,f=e.nudge_scope==="global"?"global":"session";return {max_per_day:s!==void 0?Number.parseInt(s,10):2,min_gap_minutes:n,ui_theme:c,nudge_every:Number.isFinite(b)&&b>=0?b:10,nudge_scope:f}}function T(t,e,o){_(t,"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",e,o);}function ht(t,e,o){let r=new Date().toISOString();_(t,"INSERT INTO nudge_state (session_id, interactions, updated_at) VALUES (?, 1, ?) ON CONFLICT(session_id) DO UPDATE SET interactions = interactions + 1, updated_at = excluded.updated_at",e,r),_(t,"DELETE FROM nudge_state WHERE session_id NOT IN (SELECT session_id FROM nudge_state ORDER BY updated_at DESC LIMIT ?)",ho);let n=o==="global"?P(t,"SELECT COALESCE(SUM(interactions), 0) AS n FROM nudge_state"):P(t,"SELECT interactions AS n FROM nudge_state WHERE session_id = ?",e);return Number(n?.n??0)}function xt(t){_(t,"DELETE FROM nudge_state");}function ce(t){let e=P(t,"SELECT COUNT(*) AS n FROM knowledge");return {knowledge_ready:Number(e?.n??0)>0}}function Ge(t){let e={};for(let o of ["project","repository","branch","repository_platform"]){let r=P(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=L(t),o=ie(t),r=de(t),n=Ue(t),s=new Map;for(let d of n)for(let b of d.topics)s.set(b,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}))},c={[De]:strToU8(JSON.stringify(e,null,2)),[Ae]:strToU8(JSON.stringify(o,null,2)),[je]:strToU8(JSON.stringify(a,null,2))};return existsSync(w)&&(c[Pe]=strToU8(readFileSync(w,"utf8"))),zipSync(c)}function Ro(t,e,o){if(!(De in e))return;let r=JSON.parse(strFromU8(e[De]));"max_per_day"in r&&T(t,"max_per_day",String(r.max_per_day)),"min_gap_minutes"in r&&T(t,"min_gap_minutes",String(r.min_gap_minutes)),(r.ui_theme==="system"||r.ui_theme==="dark"||r.ui_theme==="light")&&T(t,"ui_theme",r.ui_theme),"nudge_every"in r&&T(t,"nudge_every",String(r.nudge_every)),(r.nudge_scope==="session"||r.nudge_scope==="global")&&T(t,"nudge_scope",r.nudge_scope),o.settings=1;}function Co(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))O(t,s,a);o.topics=Object.keys(r).length;return}let n=0;for(let s of r.groups??[])M(t,s)&&(n+=1);for(let s of r.topics){O(t,s.topic,s.confidence);let a=s.group;a&&a!=="Other"&&(M(t,a)&&(n+=1),D(t,s.topic,a));}o.topics=r.topics.length,o.groups=n;}function Lo(t,e,o){if(!(Ae in e))return;let r=JSON.parse(strFromU8(e[Ae])),{inserted:n,duplicated:s,invalid:a}=Fe(t,r);o.lessons=n,o.skipped=s,o.invalid=a;}function No(t,e){Pe in t&&(mkdirSync(dirname(w),{recursive:true}),writeFileSync(w,strFromU8(t[Pe]),"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 Ro(t,r,o),Co(t,r,o),Lo(t,r,o),No(r,o),o}function Oo(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 Be(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=L(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=yt(t);if(s!=null){let a=new Date(s),c=(o.getTime()-a.getTime())/6e4;if(c<e.min_gap_minutes){let d=e.min_gap_minutes-c,b=Math.floor(e.min_gap_minutes/60),f=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 ${c<0?`in ${Math.round(-c)}m (future timestamp)`:`${Math.round(c)}m ago`}, minimum interval is ${b}h ${f}m (${k}h ${v}m remaining)`}}}return {allowed:!0}}catch(e){return {allowed:true,reason:`Rate limit check failed: ${e}`}}}function F(t){try{return {knowledge:de(t),groups:Ue(t)}}catch{return {knowledge:[],groups:[]}}}function q(t,e,o){let r=He(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=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),c=He(t),d=Object.entries(c).sort((y,k)=>y[1]-k[1]),b=d.slice(0,5).map(([y,k])=>({topic:y,confidence:k})),f=d.slice(-5).reverse().map(([y,k])=>({topic:y,confidence:k}));return {total_lessons:o,lessons_today:s,lessons_this_week:a,weakest_topics:b,strongest_topics:f}}catch(e){return {error:String(e)}}}function kt(t){try{return ft(t)}catch{return []}}var jo=[["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]],Po={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"},Mo={django:"django",flask:"flask",fastapi:"fastapi",starlette:"fastapi",tornado:"python",aiohttp:"python",sqlalchemy:"postgresql",celery:"python"};function Fo(t){let e="";for(let o of t)o==="*"?e+="[^/]*":o==="?"?e+="[^/]":e+=o.replace(/[.+^${}()|[\]\\]/g,String.raw`\$&`);return new RegExp(`^${e}$`)}function Ho(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=Fo(n);return s.some(c=>a.test(c))}function vt(t){try{return existsSync(t)?readFileSync(t,"utf8"):null}catch{return null}}function Uo(t,e){let o=vt(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(Po))n.has(s)&&e(a,6);}function Go(t,e){let o=["requirements.txt","requirements-dev.txt","requirements/base.txt","pyproject.toml"];for(let r of o){let n=vt(join(t,r))?.toLowerCase();if(n)for(let[s,a]of Object.entries(Mo))n.includes(s)&&e(a,6);}}function Se(t){let e={},o=(r,n)=>{(e[r]??-1)<n&&(e[r]=n);};for(let[r,n,s]of jo)Ho(t,r)&&o(n,s);return Uo(t,o),Go(t,o),e}function We(...t){try{return execFileSync("git",t,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}}var wt={"github.com":"github","gitlab.com":"gitlab","bitbucket.org":"bitbucket"};function zo(t){if(!t)return [null,null];let e=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(t);if(e){let r=(e[1]??"").toLowerCase();return [e[2]??t,wt[r]??"local"]}let o=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(t);if(o){let r=(o[1]??"").toLowerCase();return [o[2]??t,wt[r]??"local"]}return [t,"local"]}function ee(){let t=process.cwd(),e=We("rev-parse","--abbrev-ref","HEAD"),o=We("rev-parse","HEAD"),r=We("remote","get-url","origin"),[n,s]=zo(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 Jo=dirname(fileURLToPath(import.meta.url));function Vo(t,e){for(let o of ["../assets","../../assets"])try{return readFileSync(join(Jo,o,t),"utf8")}catch{}return e}var _t=()=>Vo("SKILL.md","devcoach: coaching instructions unavailable (SKILL.md not found in package).");var St={version:"0.7.1"};var pe=St.version;var N=t=>({type:"text",text:t}),Et=t=>({content:[N(JSON.stringify(t))]}),R=t=>({isError:true,content:[N(t)]}),Je=t=>({content:[N(JSON.stringify(t))],structuredContent:t}),sr={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:be,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:fe.nullable(),starred:z$1.boolean(),feedback:Le.nullable()},ir={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())}))},ar={max_per_day:z$1.number().int(),min_gap_minutes:z$1.number().int(),ui_theme:Oe,nudge_every:z$1.number().int(),nudge_scope:Ie};function S(t,e){return {contents:[{uri:t.href,mimeType:"application/json",text:JSON.stringify(e)}]}}function dr(){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:be.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:fe.nullish().describe("github | gitlab | bitbucket | local (auto-detected if omitted)")},outputSchema:sr,annotations:{title:"Log Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=ee(),n={};try{n=m(a=>Ge(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=>{ut(a,s),xt(a);});try{if(t.server.getClientCapabilities()?.elicitation){let c=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=c.action==="accept"?c.content?.feedback:void 0;d&&d!=="skip"&&(m(b=>{se(b,o.id,d),q(b,o.topic_id,d==="know"?1:-1);}),s={...s,feedback:d});}}catch{}return Je(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:[N(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 Et(r)}catch{return Et([])}}),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:[N(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:[N(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:[N(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:Ne.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:[N("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:[N(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=>M(r,o.name.trim())),{content:[N("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:[N(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). nudge_every: integer 0-1000 interactions between lesson cues (0 = cue every turn). nudge_scope: 'session' | 'global'. Returns the full updated Settings.",inputSchema:{key:z$1.enum(["max_per_day","min_gap_minutes","nudge_every","nudge_scope"]).describe("Setting key"),value:z$1.string().describe("New value (integer string; or 'session'|'global' for nudge_scope)")},outputSchema:ar,annotations:{title:"Update Settings",destructiveHint:false,idempotentHint:true,openWorldHint:false}},o=>{let r=s=>Je(m(a=>(T(a,o.key,s),L(a))));if(o.key==="nudge_scope")return o.value!=="session"&&o.value!=="global"?R("nudge_scope must be 'session' or 'global'"):r(o.value);let n=Number.parseInt(o.value,10);return Number.isNaN(n)?R(`Value must be an integer, got '${o.value}'`):o.key==="max_per_day"&&!(n>=1&&n<=20)?R("max_per_day must be between 1 and 20"):o.key==="min_gap_minutes"&&!(n>=0&&n<=1440)?R("min_gap_minutes must be between 0 and 1440"):o.key==="nudge_every"&&!(n>=0&&n<=1e3)?R("nudge_every must be between 0 and 1000"):r(String(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:[N(`devcoach UI starting at http://localhost:${r}`)]}):{content:[N(`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(),Ne).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:ir,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,c]of Object.entries(o.topics))O(s,a,Math.max(0,Math.min(10,c)));if(o.groups)for(let[a,c]of Object.entries(o.groups))for(let d of c)d in o.topics&&D(s,d,a);return F(s)}),n=o.notebook?.trim();return mkdirSync(dirname(w),{recursive:!0}),n?writeFileSync(w,n.endsWith(`
50
58
  `)?n:`${n}
51
59
  `):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(`
60
+ `),Je(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
61
  `)?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>
62
+ `),{content:[N("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 S(o,m(r=>F(r)))}catch(r){return S(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 S(o,m(r=>L(r)))}catch(r){return S(o,{error:String(r)})}}),t.registerResource("recent-lessons","devcoach://lessons/recent",e("Recent Lessons","Last 10 lessons from the current week."),o=>{try{return S(o,m(r=>B(r,{period:"week",page:1,per_page:10})))}catch(r){return S(o,[{error:String(r)}])}}),t.registerResource("stats","devcoach://stats",e("Stats","Aggregate coaching statistics: counts and weakest/strongest topics."),o=>{try{return S(o,m(r=>W(r)))}catch(r){return S(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 S(o,m(r=>kt(r)))}catch{return S(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),S(o,s)}catch(r){return S(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=>Ge(s));return S(o,{git:r,usage_defaults:n})}catch(r){return S(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(),c=Se(a.folder??process.cwd());return S(o,{knowledge_ready:n,notebook_ready:s,needs_onboarding:!(n&&s),detected_stack:c,default_topics:ct,context_ready:a.branch!==null})}catch(r){return S(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?S(o,s):S(o,{error:`Lesson '${n}' not found`})}catch(n){return S(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:_t()}}]})),t}async function Rt(){let t=dr(),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 $e(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>
55
63
  <html lang="en">
56
64
  <head>
57
65
  <meta charset="UTF-8" />
@@ -108,7 +116,7 @@ import {spawn,spawnSync,execFileSync}from'child_process';import {existsSync,writ
108
116
  </script>
109
117
  ${t.scripts??""}
110
118
  </body>
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`
119
+ </html>`}function Ot(t){let e=r=>t.allGroups.filter(n=>n!=="Other").map(n=>html`<option value="${n}" ${n===r?"selected":""}>${n}</option>`),o=html`
112
120
  <div x-data="{ editMode: JSON.parse(localStorage.getItem('km-edit-mode') || 'false'), toggle() { this.editMode = !this.editMode; localStorage.setItem('km-edit-mode', this.editMode); } }">
113
121
 
114
122
  <div class="flex items-center justify-between mb-6">
@@ -192,7 +200,7 @@ ${Object.entries(t.categorised).map(([r,n])=>html`
192
200
  </div>
193
201
  </div>
194
202
  <div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-x-8 gap-y-0">
195
- ${n.map(s=>{let a=s.confidence,l=Math.trunc(a/10*100),d=a>=7?"bg-green-500":a>=4?"bg-yellow-500":"bg-red-500",f=a>=7?"text-green-600 dark:text-green-400":a>=4?"text-yellow-600 dark:text-yellow-400":"text-red-600 dark:text-red-400";return html`
203
+ ${n.map(s=>{let a=s.confidence,c=Math.trunc(a/10*100),d=a>=7?"bg-green-500":a>=4?"bg-yellow-500":"bg-red-500",b=a>=7?"text-green-600 dark:text-green-400":a>=4?"text-yellow-600 dark:text-yellow-400":"text-red-600 dark:text-red-400";return html`
196
204
  <div class="flex items-center gap-2 py-1.5 border-b border-gray-100 dark:border-gray-800/50 group/row">
197
205
  <div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative shrink-0 flex items-center">
198
206
  <button type="button" @click="open = !open" class="text-xs text-gray-200 dark:text-gray-700 hover:text-indigo-500 dark:hover:text-indigo-400 transition w-4 text-center opacity-0 group-hover/row:opacity-100" title="Move to group">⇄</button>
@@ -217,20 +225,20 @@ ${Object.entries(t.categorised).map(([r,n])=>html`
217
225
  <button class="text-xs text-gray-300 dark:text-gray-600 hover:text-red-500 dark:hover:text-red-400 transition w-5 text-center leading-none">−</button>
218
226
  </form>
219
227
  <div class="w-20 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden shrink-0">
220
- <div class="${d} h-full rounded-full transition-all" style="width: ${l}%"></div>
228
+ <div class="${d} h-full rounded-full transition-all" style="width: ${c}%"></div>
221
229
  </div>
222
230
  <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(s.topic)}" class="shrink-0 flex items-center">
223
231
  <input type="hidden" name="delta" value="1" />
224
232
  <button class="text-xs text-gray-300 dark:text-gray-600 hover:text-green-600 dark:hover:text-green-400 transition w-5 text-center leading-none">+</button>
225
233
  </form>
226
- <span class="text-xs font-bold ${f} w-5 text-right shrink-0">${a}</span>
234
+ <span class="text-xs font-bold ${b} w-5 text-right shrink-0">${a}</span>
227
235
  </div>`})}
228
236
  </div>
229
237
  </section>`)}
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}">
238
+ </div>`;return $e({title:"Profile \u2014 devcoach",currentPath:"/",uiTheme:t.uiTheme,body:o})}var Ct={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},Lt={know:"\u2713 Known",dont_know:"\u2717 Don't know",none:"\u2014 No response"},Nt={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 It(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}`:Ct[e.period]??"All time",s=e.feedback?Lt[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"},c=(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,E="")=>{let I=e.sort===h,me=I&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap ${E}">
231
239
  <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}
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>`}
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" />
240
+ ${I?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>`}
241
+ </a></th>`},b=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>`,f=html`<link rel="stylesheet" href="/static/vendor/flatpickr.min.css" />
234
242
  <style>
235
243
  .dark .flatpickr-calendar { background:#1f2937; border-color:#374151; }
236
244
  .dark .flatpickr-day { color:#e5e7eb; }
@@ -270,11 +278,11 @@ ${Object.entries(t.categorised).map(([r,n])=>html`
270
278
 
271
279
  <div class="relative" x-data="periodPicker()" @keydown.escape="close()">
272
280
  <button type="button" @click="toggle()" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${o||e.period!=="all"?"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"}">
273
- <span>📅</span><span x-text="label">${n}</span>${f}
281
+ <span>📅</span><span x-text="label">${n}</span>${b}
274
282
  </button>
275
283
  <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">
276
284
  <div class="p-1">
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>`})}
285
+ ${Object.entries(Ct).map(([g,h])=>{let E=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 ${E?"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?html`<span class="text-indigo-500">✓</span>`:""}</button>`})}
278
286
  </div>
279
287
  <div class="border-t border-gray-100 dark:border-gray-800 p-1">
280
288
  <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"}">
@@ -289,21 +297,21 @@ ${Object.entries(t.categorised).map(([r,n])=>html`
289
297
  </div>
290
298
 
291
299
  <div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
292
- <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.feedback?"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"}">${s}${f}</button>
300
+ <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.feedback?"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"}">${s}${b}</button>
293
301
  <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-44 p-1 overflow-hidden" style="display:none">
294
302
  ${[["","All feedback"],["know","\u2713 Known"],["dont_know","\u2717 Don't know"],["none","\u2014 No response"]].map(([g,h])=>html`<button type="button" onclick="document.getElementById('h-feedback').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.feedback===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.feedback===g?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
295
303
  </div>
296
304
  </div>
297
305
 
298
306
  <div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
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>
307
+ <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?`${Nt[e.level]??""} ${e.level}`:"Level"}${b}</button>
300
308
  <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">
301
309
  ${[["","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>`)}
302
310
  </div>
303
311
  </div>
304
312
 
305
313
  ${t.allCategories.length||t.allProjects.length||t.allRepositories.length||t.allBranches.length||t.allCommits.length?html`<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
306
- <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.category||e.project||e.repository||e.branch||e.commit?"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"}">Filters${f}</button>
314
+ <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.category||e.project||e.repository||e.branch||e.commit?"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"}">Filters${b}</button>
307
315
  <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-64 p-3 space-y-2.5" style="display:none">
308
316
  ${t.allCategories.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Category</label><select name="category" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All categories</option>${t.allCategories.map(g=>html`<option value="${g}" ${e.category===g?"selected":""}>${g}</option>`)}</select></div>`:""}
309
317
  ${t.allProjects.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Project</label><select name="project" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All projects</option>${t.allProjects.map(g=>html`<option value="${g}" ${e.project===g?"selected":""}>${g}</option>`)}</select></div>`:""}
@@ -318,17 +326,17 @@ ${Object.entries(t.categorised).map(([r,n])=>html`
318
326
  </div>
319
327
 
320
328
  ${r?html`<div class="flex flex-wrap gap-1.5 mb-4">
321
- ${o?l(`\u{1F4C5} ${n}`,$(e,{date_from:"",date_to:""})):""}
322
- ${e.period!=="all"&&!o?l(`\u{1F550} ${n}`,$(e,{period:"all"})):""}
323
- ${e.feedback?l(Et[e.feedback]??e.feedback,$(e,{feedback:""})):""}
329
+ ${o?c(`\u{1F4C5} ${n}`,$(e,{date_from:"",date_to:""})):""}
330
+ ${e.period!=="all"&&!o?c(`\u{1F550} ${n}`,$(e,{period:"all"})):""}
331
+ ${e.feedback?c(Lt[e.feedback]??e.feedback,$(e,{feedback:""})):""}
324
332
  ${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>`:""}
325
- ${e.search?l(`\u{1F50D} "${e.search}"`,$(e,{search:""})):""}
326
- ${e.category?l(e.category,$(e,{category:""})):""}
327
- ${e.level?l(`${Rt[e.level]??""} ${e.level}`,$(e,{level:""})):""}
328
- ${e.project?l(`\u{1F4C1} ${e.project}`,$(e,{project:""})):""}
329
- ${e.repository?l(`\u2387 ${e.repository}`,$(e,{repository:""})):""}
330
- ${e.branch?l(`\u2387 ${e.branch}`,$(e,{branch:""})):""}
331
- ${e.commit?l(e.commit.slice(0,7),$(e,{commit:""})):""}
333
+ ${e.search?c(`\u{1F50D} "${e.search}"`,$(e,{search:""})):""}
334
+ ${e.category?c(e.category,$(e,{category:""})):""}
335
+ ${e.level?c(`${Nt[e.level]??""} ${e.level}`,$(e,{level:""})):""}
336
+ ${e.project?c(`\u{1F4C1} ${e.project}`,$(e,{project:""})):""}
337
+ ${e.repository?c(`\u2387 ${e.repository}`,$(e,{repository:""})):""}
338
+ ${e.branch?c(`\u2387 ${e.branch}`,$(e,{branch:""})):""}
339
+ ${e.commit?c(e.commit.slice(0,7),$(e,{commit:""})):""}
332
340
  </div>`:""}
333
341
  </form>
334
342
 
@@ -346,7 +354,7 @@ ${t.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
346
354
  </tr>
347
355
  </thead>
348
356
  <tbody class="divide-y divide-gray-100 dark:divide-gray-800">
349
- ${t.lessons.map(g=>{let h=g.timestamp.slice(0,10),T=g.timestamp.slice(0,16).replace("T"," ");return html`<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors group cursor-pointer" tabindex="0" onclick="window.location='/lessons/${encodeURIComponent(g.id)}'" onkeydown="if(event.key==='Enter')window.location='/lessons/${encodeURIComponent(g.id)}'" role="link">
357
+ ${t.lessons.map(g=>{let h=g.timestamp.slice(0,10),E=g.timestamp.slice(0,16).replace("T"," ");return html`<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors group cursor-pointer" tabindex="0" onclick="window.location='/lessons/${encodeURIComponent(g.id)}'" onkeydown="if(event.key==='Enter')window.location='/lessons/${encodeURIComponent(g.id)}'" role="link">
350
358
  <td class="px-3 py-3" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">
351
359
  <form method="post" action="/lessons/${encodeURIComponent(g.id)}/star">
352
360
  <input type="hidden" name="starred" value="${g.starred?"0":"1"}" />
@@ -356,12 +364,12 @@ ${t.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
356
364
  </td>
357
365
  <td class="px-3 py-3 whitespace-nowrap tabular-nums relative group/date">
358
366
  <span class="text-gray-400 dark:text-gray-500 cursor-default" data-ts="${g.timestamp}">${h}</span>
359
- <div class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${T}</div>
367
+ <div class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${E}</div>
360
368
  </td>
361
369
  <td class="px-3 py-3 hidden sm:table-cell"><span class="text-xs font-mono text-cyan-600 dark:text-cyan-400">${g.topic_id}</span></td>
362
370
  <td class="px-3 py-3 max-w-xs"><a href="/lessons/${encodeURIComponent(g.id)}" class="font-medium text-gray-800 dark:text-gray-100 hover:text-indigo-600 dark:hover:text-indigo-400 transition line-clamp-2">${g.title}</a></td>
363
371
  <td class="px-3 py-3" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><a href="${$(e,{level:g.level})}" class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${a[g.level]??""} hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${g.level}</a></td>
364
- <td class="px-3 py-3 hidden lg:table-cell" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><div class="flex flex-wrap gap-1">${g.categories.map(N=>html`<a href="${$(e,{category:N})}" class="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 hover:text-indigo-700 dark:hover:text-indigo-300 transition-colors">${N}</a>`)}</div></td>
372
+ <td class="px-3 py-3 hidden lg:table-cell" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><div class="flex flex-wrap gap-1">${g.categories.map(I=>html`<a href="${$(e,{category:I})}" class="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 hover:text-indigo-700 dark:hover:text-indigo-300 transition-colors">${I}</a>`)}</div></td>
365
373
  <td class="px-3 py-3 hidden xl:table-cell" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">${g.feedback==="know"?html`<span class="text-xs text-emerald-600 dark:text-emerald-400 font-medium">✓ Known</span>`:g.feedback==="dont_know"?html`<span class="text-xs text-rose-500 dark:text-rose-400 font-medium">✗ Unknown</span>`:""}</td>
366
374
  </tr>`})}
367
375
  </tbody>
@@ -371,7 +379,7 @@ ${t.totalPages>1?html`<div class="flex items-center justify-between mt-4">
371
379
  <p class="text-xs text-gray-400 dark:text-gray-500">Page ${t.page} of ${t.totalPages}</p>
372
380
  <div class="flex items-center gap-1">
373
381
  ${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>`}
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>`)}
382
+ ${cr(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>`)}
375
383
  ${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>`}
376
384
  </div>
377
385
  </div>`:""}`:html`<div class="flex flex-col items-center justify-center py-16 text-center">
@@ -416,7 +424,7 @@ function periodPicker() {
416
424
  },
417
425
  };
418
426
  }
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`
427
+ </script>`;return $e({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:f,body:k,scripts:v})}function cr(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 Ve={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function Dt(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&&Ve[e.repository_platform]&&e.repository&&(n=`https://${Ve[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),c=e.timestamp.slice(0,10),d=e.timestamp.slice(0,16).replace("T"," "),b=html`
420
428
  <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>
421
429
  <div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
422
430
  <div class="flex flex-wrap items-center gap-3 mb-2">
@@ -429,7 +437,7 @@ function periodPicker() {
429
437
  <a href="/lessons?level=${e.level}" class="text-xs font-semibold px-2 py-0.5 rounded-full border ${o} shrink-0 hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${e.level}</a>
430
438
  </div>
431
439
  <div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400 mb-5">
432
- <span class="relative group/date cursor-default">🗓 <span data-ts="${e.timestamp}">${l}</span>
440
+ <span class="relative group/date cursor-default">🗓 <span data-ts="${e.timestamp}">${c}</span>
433
441
  <span class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${d}</span>
434
442
  </span>
435
443
  <span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.topic_id}</span></span>
@@ -445,7 +453,7 @@ function periodPicker() {
445
453
  ${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>`:""}
446
454
  ${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">
447
455
  ${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>`:""}
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>`:""}
456
+ ${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&&Ve[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>`:""}
449
457
  ${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>`:""}
450
458
  ${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>`:""}
451
459
  ${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>`:""}
@@ -455,7 +463,7 @@ function periodPicker() {
455
463
  <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback"><input type="hidden" name="feedback" value="dont_know" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="px-3 py-1 rounded text-sm font-medium transition bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-red-100 dark:hover:bg-red-900 hover:text-red-700 dark:hover:text-white">✗ I don't know this</button></form>
456
464
  </div>`}
457
465
  <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800 text-xs text-gray-300 dark:text-gray-600">ID: ${e.id}</div>
458
- </div>`,b=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,y=html`<script src="/static/vendor/highlight.min.js"></script>
466
+ </div>`,f=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,y=html`<script src="/static/vendor/highlight.min.js"></script>
459
467
  <script src="/static/vendor/marked.min.js"></script>
460
468
  <script src="/static/relative-time.js"></script>
461
469
  <script>
@@ -469,7 +477,7 @@ function periodPicker() {
469
477
  });
470
478
  document.getElementById('summary-content').innerHTML = marked.parse(${raw(te(e.summary))});
471
479
  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`
480
+ </script>`;return $e({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:t.uiTheme,head:f,body:b,scripts:y})}var lr=[[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 At(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`
473
481
  <h1 class="text-2xl font-bold mb-6 text-indigo-600 dark:text-indigo-400">Settings</h1>
474
482
  ${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"}">
475
483
  ${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.":""}
@@ -487,17 +495,32 @@ ${r?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${r.invalid?"bg-ye
487
495
  <div>
488
496
  <label for="min-gap-minutes" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Minimum gap between lessons</label>
489
497
  <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">
490
- ${ir.map(([a,l])=>html`<option value="${a}" ${e===a?"selected":""}>${l}</option>`)}
498
+ ${lr.map(([a,c])=>html`<option value="${a}" ${e===a?"selected":""}>${c}</option>`)}
491
499
  </select>
492
500
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Minimum gap between lessons.</p>
493
501
  </div>
494
502
  </div>
503
+ <div class="grid grid-cols-2 gap-4">
504
+ <div>
505
+ <label for="nudge-every" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Interactions between lessons</label>
506
+ <input id="nudge-every" type="number" name="nudge_every" min="0" max="1000" value="${t.settings.nudge_every}" 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" />
507
+ <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Cue at most once every N (0 = every turn).</p>
508
+ </div>
509
+ <div>
510
+ <label for="nudge-scope" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Count interactions</label>
511
+ <select id="nudge-scope" name="nudge_scope" 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">
512
+ <option value="session" ${t.settings.nudge_scope==="session"?"selected":""}>Per chat session</option>
513
+ <option value="global" ${t.settings.nudge_scope==="global"?"selected":""}>Globally</option>
514
+ </select>
515
+ <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Per-chat or across all sessions.</p>
516
+ </div>
517
+ </div>
495
518
  <fieldset class="border-0 p-0 m-0">
496
519
  <legend class="block text-sm text-gray-600 dark:text-gray-400 mb-1">UI theme</legend>
497
520
  <div class="flex gap-2">
498
- ${o.map(([a,l,d])=>html`<label class="flex-1 cursor-pointer">
521
+ ${o.map(([a,c,d])=>html`<label class="flex-1 cursor-pointer">
499
522
  <input type="radio" name="ui_theme" value="${a}" ${t.settings.ui_theme===a?"checked":""} class="sr-only peer" />
500
- <span class="flex flex-col items-center justify-center gap-1 px-3 py-2 rounded border border-gray-300 dark:border-gray-700 peer-checked:border-indigo-500 peer-checked:bg-indigo-50 dark:peer-checked:bg-indigo-900/30 peer-checked:text-indigo-700 dark:peer-checked:text-indigo-300 text-gray-500 dark:text-gray-400 text-xs font-medium transition select-none"><span class="text-base leading-none">${d}</span>${l}</span>
523
+ <span class="flex flex-col items-center justify-center gap-1 px-3 py-2 rounded border border-gray-300 dark:border-gray-700 peer-checked:border-indigo-500 peer-checked:bg-indigo-50 dark:peer-checked:bg-indigo-900/30 peer-checked:text-indigo-700 dark:peer-checked:text-indigo-300 text-gray-500 dark:text-gray-400 text-xs font-medium transition select-none"><span class="text-base leading-none">${d}</span>${c}</span>
501
524
  </label>`)}
502
525
  </div>
503
526
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">System follows your OS preference.</p>
@@ -564,22 +587,22 @@ function updateLabel(inputId, labelId, submitId) {
564
587
  }
565
588
  marked.setOptions({ breaks: true, gfm: true });
566
589
  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}&notebook=${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.
590
+ </script>`;return $e({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:t.uiTheme,body:n,scripts:s})}var Qe=25;function et(t,e="/lessons"){return t?.startsWith("/")&&!t.startsWith("//")?t:e}function C(t,e,o=""){let r=t[e];return typeof r=="string"?r:o}var tt=dirname(fileURLToPath(import.meta.url)),jt=[join(tt,"../assets/static"),join(tt,"../../assets/static")].find(t=>existsSync(t))??join(tt,"../assets/static"),yr={".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 hr(){try{return m(t=>L(t).ui_theme)}catch{return "system"}}function xr(){let t=new Hono;return t.get("/static/*",e=>{let o=decodeURIComponent(e.req.path.slice(8)),r=join(jt,o);if(!r.startsWith(jt))return e.notFound();try{let n=readFileSync(r);return new Response(new Uint8Array(n),{headers:{"content-type":yr[extname(r)]??"application/octet-stream"}})}catch{return e.notFound()}}),t.get("/",e=>{let{profile:o,stats:r,rateLimit:n,settings:s}=m(d=>({profile:F(d),stats:W(d),rateLimit:z(d),settings:L(d)})),a=new Map;for(let d of o.groups)for(let b of d.topics)a.set(b,d.name);let c={};for(let d of o.groups)c[d.name]=[];for(let d of o.knowledge){let b=a.get(d.topic)??"Other",f=c[b]??[];f.push(d),c[b]=f;}return e.html(Ot({categorised:c,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=C(o,"topic").trim();if(r){let n=Number.parseInt(C(o,"confidence","5"),10)||5,s=C(o,"group").trim();m(a=>{O(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=C(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(C(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=C(await e.req.parseBody(),"group_name").trim();return o&&o!=="Other"&&m(r=>M(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=>Fe(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||"",c=o.starred==="1",d=o.sort||"timestamp",b=o.order==="asc"?"asc":"desc",f=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:c?true:null,search:o.search||null,feedback:o.feedback||null,date_from:s||null,date_to:a||null},v=m(h=>{let E=J(h,k),I=Math.max(1,Math.ceil(E/Qe));return f=Math.min(f,I),{total:E,totalPages:I,lessons:B(h,{...k,sort:d,order:b,page:f,per_page:Qe}),allCategories:bt(h),allProjects:ae(h,"project"),allRepositories:ae(h,"repository"),allBranches:ae(h,"branch"),allCommits:ae(h,"commit_hash"),theme:L(h).ui_theme}}),g={period:y,category:o.category||"",level:n,project:o.project||"",repository:o.repository||"",branch:o.branch||"",commit:o.commit||"",starred:c,search:o.search||"",feedback:o.feedback||"",date_from:s,date_to:a,sort:d,order:b};return e.html(It({lessons:v.lessons,allCategories:v.allCategories,allProjects:v.allProjects,allRepositories:v.allRepositories,allBranches:v.allBranches,allCommits:v.allCommits,s:g,page:f,perPage:Qe,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(et(C(o,"next")||void 0),303)}),t.post("/lessons/:lesson_id/feedback",async e=>{let o=await e.req.parseBody(),r=C(o,"feedback"),n=r===""||r==="clear"?null:r;return m(s=>_e(s,e.req.param("lesson_id"),n)),e.redirect(et(C(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(Dt({lesson:o,uiTheme:hr()})):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}&notebook=${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,C(o,"content"),"utf8"),e.redirect(et(C(o,"next")||void 0,"/settings?notebook_saved=1"),303)}),t.post("/settings",async e=>{let o=await e.req.parseBody(),r=Number.parseInt(C(o,"max_per_day","2"),10)||2,n=Number.parseInt(C(o,"min_gap_minutes","240"),10),s=C(o,"ui_theme","system");["system","dark","light"].includes(s)||(s="system");let a=Math.max(0,Number.parseInt(C(o,"nudge_every","10"),10)||0),c=C(o,"nudge_scope","session");return c!=="session"&&c!=="global"&&(c="session"),m(d=>{T(d,"max_per_day",String(r)),T(d,"min_gap_minutes",String(Number.isNaN(n)?240:n)),T(d,"ui_theme",s),T(d,"nudge_every",String(a)),T(d,"nudge_scope",c);}),e.redirect("/settings",303)}),t.get("/settings",e=>{let o=m(a=>L(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(At({settings:o,notebookContent:r,notebookPath:w,uiTheme:o.ui_theme,flash:s}))}),t}function Mt(t){let e=xr();serve({fetch:e.fetch,port:t,hostname:"127.0.0.1"},o=>{console.log(`devcoach UI running at http://localhost:${o.port}`);});}var vr=!!process.stdout.isTTY&&!process.env.NO_COLOR,X=(t,e)=>vr?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 rt(t,e){return t==="green"?i.green(e):t==="yellow"?i.yellow(e):i.red(e)}var wr=/\[[0-9;]*m/g,_r=t=>t.replace(wr,""),Ee=t=>[..._r(t)].length;function Ft(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((b,f)=>Math.max(r?Ee(b.header):0,...o.map(y=>Ee(y[f]??"")))),s=(b,f,y)=>b+n.map(k=>"\u2500".repeat(k+2)).join(f)+y,a=b=>`\u2502${b.map((f,y)=>` ${Ft(f??"",n[y]??0,e[y]?.justify??"left")} `).join("\u2502")}\u2502`,c=n.reduce((b,f)=>b+f+3,1),d=[];t&&d.push(Ft(i.bold(t),c,"center")),d.push(s("\u256D","\u252C","\u256E")),r&&d.push(a(e.map(b=>i.bold(b.header))),s("\u251C","\u253C","\u2524"));for(let b of o)d.push(a(b));return d.push(s("\u2570","\u2534","\u256F")),d.join(`
591
+ `)}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 Ht=t=>{let e=Math.max(0,Math.min(10,Math.round(t)));return "\u2588".repeat(e)+"\u2591".repeat(10-e)},Ut=t=>t>=7?"green":t>=4?"yellow":"red";var u=(t="")=>{console.log(t);},Wt=t=>t==="junior"?i.green(t):t==="mid"?i.yellow(t):t==="senior"?i.red(t):t,Lr=t=>t==="know"?` ${i.green("\u2713")}`:t==="dont_know"?` ${i.red("\u2717")}`:"",Nr=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 Xt(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=Ut(n.confidence);return [n.topic,o.get(n.topic)??"Other",rt(s,`${n.confidence}/10`),rt(s,Ht(n.confidence))]});return oe(e,[{header:"Topic"},{header:"Group"},{header:"Confidence",justify:"right"},{header:"Bar"}],r)}function Or(){let t=m(e=>F(e));u(Xt(t,"Knowledge Map"));}function Ir(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+Lr(s.feedback),Wt(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 Dr(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:")} ${Wt(e.level)}`);let o=e.starred?i.yellow("\u2605 starred"):i.dim("\u2606 not starred");if(u(`${i.dim("Star:")} ${o} ${i.dim("Feedback:")} ${Nr(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 Bt(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 Ar(t){m(o=>xe(o,t))||(u(i.red(`Lesson '${t}' not found.`)),process.exit(1)),u(`Lesson ${i.cyan(t)} deleted.`);}function jr(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 c=_e(a,t,o);if(c===null)return null;let d=a.prepare("SELECT confidence FROM knowledge WHERE topic = ?").get(c);return {topicId:c,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 Pr(){let t=m(n=>L(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})`],["nudge_every",t.nudge_every===0?"0 (off)":String(t.nudge_every)],["nudge_scope",t.nudge_scope]]));}function Mr(){let{stats:t,rateLimit:e,settings:o}=m(c=>({stats:W(c),rateLimit:z(c),settings:L(c)})),r=c=>Number(t[c]??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 c=[];for(let d=0;d<Math.max(s.length,a.length);d++){let b=s[d],f=a[d];c.push([b?`${i.red(b.topic)} ${i.dim(`(${b.confidence})`)}`:"",f?`${i.green(f.topic)} ${i.dim(`(${f.confidence})`)}`:""]);}u(oe(void 0,[{header:"Weakest topics"},{header:"Strongest topics"}],c));}}function Fr(t,e){let o=["max_per_day","min_gap_minutes","nudge_every","nudge_scope"];if(o.includes(t)||(u(i.red(`Unknown key '${t}'. Valid keys: ${o.join(", ")}`)),process.exit(1)),t==="nudge_scope"&&e!=="session"&&e!=="global"&&(u(i.red(`Invalid nudge_scope '${e}'. Use: session | global`)),process.exit(1)),t==="nudge_every"){let r=Number.parseInt(e,10);(!Number.isInteger(r)||r<0)&&(u(i.red(`Invalid nudge_every '${e}'. Use a non-negative integer (0 = off).`)),process.exit(1));}m(r=>T(r,t,e)),u(i.green(`Set ${t} = ${e}`));}function Hr(t,e,o){let r=t.trim();r||(u(i.red("Topic name must not be empty.")),process.exit(1)),m(s=>{O(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 Ur(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 Gr(t){let e=t.trim();(!e||e==="Other")&&(u(i.red("Invalid group name.")),process.exit(1)),m(o=>M(o,e)),u(`${i.green(`Group '${i.cyan(e)}' ready.`)} Assign topics with: devcoach group-assign <topic> "${e}"`);}function Br(t){let e=m(o=>Q(o,t));u(e?`${i.green(`Removed group '${i.cyan(t)}'`)}`:i.yellow(`Group '${t}' not found.`));}function Kr(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 zr(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 qr(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 Wr(){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 Xr=join(homedir(),".claude","settings.json");function Yt(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 st(){return Yt("devcoach")?{command:"devcoach",args:["mcp"]}:{command:"npx",args:["-y","devcoach","mcp"]}}function Yr(){return st().command==="devcoach"?"devcoach":"npx -y devcoach"}function Jr(t,e){if(!Yt("claude"))return "";e&&spawnSync("claude",["mcp","remove","--scope",t,"devcoach"],{encoding:"utf8"});let o=st(),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 Jt(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 Kt(t,e,o){let r=Jt(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)}
592
+ `),`${i.green("\u2713")} Installed into ${t}`)}function Vr(t){let e=Yr(),o=Xr,r=Jt(o);if(!r.ok)return r.error;let n=r.data;n.hooks??={};let s=n.hooks;s.Stop??=[];let a=s.Stop,c=a.map((d,b)=>[d,b]).filter(([d])=>(d.hooks??[]).some(b=>(b.command??"").includes("devcoach"))).map(([,d])=>d);if(c.length){if(!t)return `${i.yellow("Stop hooks already installed")} in ${o} (use --force to overwrite)`;for(let d of c.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)}
593
+ `),`${i.green("\u2713")} Stop hooks installed into ${o}`}function Zr(t){let e=t.claudeCode||!t.claudeDesktop,o=t.claudeDesktop||!t.claudeCode,r=st(),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=Jr("user",t.force);if(!s){let a=join(homedir(),".claude.json");s=Kt(a,{type:"stdio",env:{},...r},t.force),n=true;}u(` MCP server\u2026 ${s}`),t.skipHook||u(` Stop hooks\u2026 ${Vr(t.force)}`),u();}o&&(u(i.bold("Claude Desktop")),u(` MCP server\u2026 ${Kt(Wr(),r,t.force)}`),n=true,u()),n&&u(`${i.yellow("\u2192")} Restart Claude Desktop to pick up the new server.
571
594
  `),u(i.dim(`Tip: run devcoach backup to export your profile, lessons and settings.
572
595
  run devcoach restore <file> to import a backup on a new machine.
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(`
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(`
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(`
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(`
596
+ The coaching skill is served automatically via the MCP prompt \u2014 always up to date.`));}async function Qr(){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 c=await e(r,String(n)),d=Number.parseInt(c,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(`
597
+ ${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 f=m(y=>le(y,readFileSync(r)));u(`${i.green("\u2713")} Restored: ${f.topics} topics, ${f.lessons} lessons${f.learning_state?", notebook":""}`),u(i.green("Setup complete!"));return}u(`
598
+ ${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=Se(y),v=Object.keys(k).sort((h,E)=>h.localeCompare(E));if(v.length){u(`
599
+ ${i.dim(`Detected from ${i.cyan(y)}:`)}`);for(let h of v){let E=k[h]??5,I=await e(` ${i.cyan(h)} (Enter=keep, 0-10=override, s=skip)`,String(E));if(I.toLowerCase()==="s")continue;let me=Number.parseInt(I,10);s[h]=Number.isNaN(me)?E:Math.max(0,Math.min(10,me));}}else u(i.dim("No technology files detected in current directory."));let g=await e(`
600
+ Add additional topics (comma-separated, or Enter to skip)`,"");for(let h of g.split(",").map(E=>E.trim()).filter(Boolean))s[h]=await o(` Confidence for ${i.cyan(h)}`,5,0,10);}else for(u(i.dim(`
601
+ Enter topics: 'topic_id confidence' (e.g. python 7). Blank line when done.`));;){let f=(await e("Topic (Enter when done)","")).trim();if(!f)break;let y=f.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(`
579
602
  ${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(`
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.
603
+ ${i.bold("Step 4")} \u2014 Rate-limit settings`);let c=await o("Max lessons per day",2,1,20),d=await o("Min gap between lessons (minutes)",240,0,1440),b=m(f=>{f.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[y,k]of Object.entries(s))O(f,y,k);for(let[y,k]of Object.entries(a))for(let v of k)D(f,v,y);return T(f,"max_per_day",String(c)),T(f,"min_gap_minutes",String(d)),F(f)});u(Xt(b,"Knowledge Profile")),u(`
604
+ ${i.green("Setup complete!")} ${Object.keys(s).length} topics saved.`);}finally{t.close();}}function en(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,session_id:typeof e?.session_id=="string"?e.session_id:null}}catch{return {stop_hook_active:false,permission_mode:null,session_id:null}}}function Vt(){let t={stop_hook_active:false,permission_mode:null,session_id:null};try{let e=fstatSync(0);return !e.isFIFO()&&!e.isFile()&&!e.isSocket()?t:en(readFileSync(0,"utf8"))}catch{return t}}function Zt(t){return t.stop_hook_active||t.permission_mode==="plan"}function Qt(t){process.stdout.write(`${JSON.stringify({decision:"block",reason:t})}
605
+ `),process.exit(0);}function tn(){if(Zt(Vt())&&process.exit(0),existsSync(Y)){let t;try{t=m(e=>ce(e).knowledge_ready);}catch{process.exit(0);}t&&process.exit(0);}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.");}var ge=10;function on(){let t=Vt();Zt(t)&&process.exit(0),existsSync(Y)||process.exit(0);let e;try{e=m(a=>{if(!ce(a).knowledge_ready)return {cue:!1,nextLessonNumber:0};if(!z(a).allowed)return {cue:!1,nextLessonNumber:0};let c=L(a);return c.nudge_every>0&&ht(a,t.session_id??"__nosession__",c.nudge_scope)<c.nudge_every?{cue:!1,nextLessonNumber:0}:{cue:!0,nextLessonNumber:Number(W(a).total_lessons??0)+1}});}catch{process.exit(0);}e.cue||process.exit(0);let{nextLessonNumber:o}=e,r=o%ge===0,n=Math.ceil(o/ge)*ge,s=r?`6. This is lesson #${o} \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 ${o} lessons)".`:`6. Do NOT call update_notebook this time \u2014 the notebook is refreshed only every ${ge} lessons (next checkpoint at lesson #${n}). Just deliver the card and call log_lesson.`;Qt(`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.
583
606
 
584
607
  1. Read the MCP resources devcoach://profile, devcoach://taught-topics, and devcoach://notebook.
585
608
  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.
@@ -595,9 +618,9 @@ ${i.green("Setup complete!")} ${Object.keys(s).length} topics saved.`);}finally{
595
618
  ### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 [topic] \xB7 [level] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
596
619
 
597
620
  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}
621
+ ${s}
599
622
 
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(`
623
+ 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 rn(){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
624
  ${i.bold(`devcoach ${i.dim(`v${pe}`)}`)}`);for(let[e,o]of t)u(` ${i.cyan(`devcoach ${e}`.padEnd(32))} ${i.dim(o)}`);u(`
602
625
  ${i.dim("Run")} ${i.bold("devcoach <command> --help")} ${i.dim("for per-command options.")}
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);}
626
+ `);}function j(t){return t??null}function nn(){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(()=>rn()),t.command("mcp").description("Start the MCP server (stdio transport)").action(Rt),t.command("profile").description("Show the knowledge map").action(Or),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=>Ir({period:e.period??"all",category:j(e.category),project:j(e.project),repository:j(e.repository),branch:j(e.branch),commit:j(e.commit),starred:!!e.starred,feedback:j(e.feedback),level:j(e.level),dateFrom:j(e.dateFrom),dateTo:j(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=>Dr(e)),t.command("star").description("Mark a lesson as starred").argument("<id>","Lesson ID").action(e=>Bt(e,true)),t.command("unstar").description("Remove the starred mark from a lesson").argument("<id>","Lesson ID").action(e=>Bt(e,false)),t.command("delete").description("Permanently delete a lesson").argument("<id>","Lesson ID").action(e=>Ar(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)=>jr(e,o)),t.command("settings").description("Show current settings").action(Pr),t.command("stats").description("Coaching statistics and rate-limit status").action(Mr),t.command("set").description("Update a setting (max_per_day | min_gap_minutes)").argument("<key>","Setting key").argument("<value>","New value").action((e,o)=>Fr(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)=>Hr(e,Math.max(0,Math.min(10,Number.parseInt(o.confidence??"5",10)||5)),j(o.group))),t.command("knowledge-remove").description("Remove a topic from the knowledge map").argument("<topic>","Topic ID").action(e=>Ur(e)),t.command("group-add").description("Register a new knowledge group").argument("<name>","Group name").action(e=>Gr(e)),t.command("group-remove").description("Delete a knowledge group (topics move to Other)").argument("<name>","Group name").action(e=>Br(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)=>Kr(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=>zr(e)),t.command("restore").description("Restore from a backup zip file").argument("<file>","Path to backup zip").action(e=>qr(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=>Zr({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=>Mt(Number.parseInt(e.port??"7860",10)||7860)),t.command("setup").description("First-run wizard: import backup or build your knowledge profile").action(Qr),t.command("onboard-hook",{hidden:true}).description("Claude Code Stop hook: cue onboarding").action(tn),t.command("lesson-ready",{hidden:true}).description("Claude Code Stop hook: signal a lesson is due").action(on),t}async function eo(){let t=nn();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 eo();}catch(t){console.error(t),process.exit(1);}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devcoach",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Stay sharp while your AI does the work — learn from every task with in-context coaching, all local, all free",
5
5
  "license": "AGPL-3.0-only",
6
6
  "keywords": [