devcoach 0.7.4 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -1,626 +1,2 @@
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 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
- CREATE TABLE IF NOT EXISTS lessons (
4
- id TEXT PRIMARY KEY,
5
- timestamp TEXT NOT NULL,
6
- topic_id TEXT NOT NULL,
7
- categories TEXT NOT NULL,
8
- title TEXT NOT NULL,
9
- level TEXT NOT NULL,
10
- summary TEXT NOT NULL,
11
- body TEXT,
12
- task_context TEXT,
13
- project TEXT,
14
- repository TEXT,
15
- branch TEXT,
16
- commit_hash TEXT,
17
- folder TEXT,
18
- feedback TEXT,
19
- repository_platform TEXT,
20
- starred INTEGER NOT NULL DEFAULT 0
21
- );
22
-
23
- CREATE TABLE IF NOT EXISTS knowledge (
24
- topic TEXT PRIMARY KEY,
25
- confidence INTEGER NOT NULL DEFAULT 5,
26
- updated_at TEXT NOT NULL
27
- );
28
-
29
- CREATE TABLE IF NOT EXISTS settings (
30
- key TEXT PRIMARY KEY,
31
- value TEXT NOT NULL
32
- );
33
-
34
- CREATE TABLE IF NOT EXISTS knowledge_group_names (
35
- group_name TEXT PRIMARY KEY
36
- );
37
-
38
- CREATE TABLE IF NOT EXISTS knowledge_groups (
39
- group_name TEXT NOT NULL,
40
- topic TEXT NOT NULL,
41
- PRIMARY KEY (group_name, topic)
42
- );
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
-
52
- CREATE INDEX IF NOT EXISTS idx_lessons_timestamp ON lessons (timestamp);
53
- CREATE INDEX IF NOT EXISTS idx_lessons_starred_ts ON lessons (starred, timestamp);
54
- CREATE INDEX IF NOT EXISTS idx_lessons_feedback ON lessons (feedback);
55
- CREATE INDEX IF NOT EXISTS idx_lessons_topic_id ON lessons (topic_id);
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.4"};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(`
58
- `)?n:`${n}
59
- `):existsSync(w)||writeFileSync(w,`# devcoach \u2014 Coaching Notebook
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(`
61
- `)?o.notebook:`${o.notebook}
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>
63
- <html lang="en">
64
- <head>
65
- <meta charset="UTF-8" />
66
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
67
- <title>${t.title}</title>
68
- <link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
69
- <meta name="devcoach-theme" content="${t.uiTheme}" />
70
- <script>
71
- (function () {
72
- var serverTheme = document.querySelector('meta[name="devcoach-theme"]').content;
73
- var session = localStorage.getItem('theme-override');
74
- var active = session || serverTheme;
75
- var dark = active === 'dark' || (active === 'system' && globalThis.matchMedia('(prefers-color-scheme: dark)').matches);
76
- if (dark) document.documentElement.classList.add('dark');
77
- })();
78
- </script>
79
- <script src="/static/vendor/tailwind.js"></script>
80
- <script>tailwind.config = { darkMode: 'class' }</script>
81
- <script src="/static/vendor/htmx.min.js"></script>
82
- <script src="/static/vendor/alpinejs.min.js" defer></script>
83
- <link rel="stylesheet" href="/static/style.css" />
84
- ${t.head??""}
85
- </head>
86
- <body class="bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 min-h-screen font-mono transition-colors duration-200">
87
- <nav class="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 px-6 py-3 flex items-center gap-6">
88
- <span class="text-indigo-600 dark:text-indigo-400 font-bold text-lg">🎓 devcoach</span>
89
- ${e("/","Profile",t.currentPath==="/")}
90
- ${e("/lessons","Lessons",t.currentPath.includes("/lessons"))}
91
- ${e("/settings","Settings",t.currentPath==="/settings")}
92
- <div class="ml-auto">
93
- <button id="theme-toggle" onclick="toggleTheme()"
94
- class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-200 transition text-lg leading-none px-1"
95
- title="Toggle theme"></button>
96
- </div>
97
- </nav>
98
- <main class="max-w-7xl mx-auto px-4 sm:px-6 py-8">${t.body}</main>
99
- <script>
100
- function isDark() { return document.documentElement.classList.contains('dark'); }
101
- function updateThemeIcon() { var b = document.getElementById('theme-toggle'); if (b) b.textContent = isDark() ? '☀️' : '🌙'; }
102
- function updateHljsTheme() {
103
- var link = document.getElementById('hljs-theme'); if (!link) return;
104
- link.href = isDark() ? '/static/vendor/hljs-dark.min.css' : '/static/vendor/hljs-light.min.css';
105
- }
106
- function toggleTheme() {
107
- document.documentElement.classList.toggle('dark');
108
- localStorage.setItem('theme-override', isDark() ? 'dark' : 'light');
109
- updateThemeIcon(); updateHljsTheme();
110
- }
111
- (function () {
112
- var serverTheme = document.querySelector('meta[name="devcoach-theme"]').content;
113
- if (serverTheme !== 'system') localStorage.removeItem('theme-override');
114
- })();
115
- updateThemeIcon(); updateHljsTheme();
116
- </script>
117
- ${t.scripts??""}
118
- </body>
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`
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); } }">
121
-
122
- <div class="flex items-center justify-between mb-6">
123
- <h1 class="text-2xl font-bold text-indigo-600 dark:text-indigo-400">Knowledge Map</h1>
124
- <div class="flex items-center gap-2">
125
- <div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative">
126
- <button type="button" @click="open = !open"
127
- class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">+ Add group</button>
128
- <div x-show="open" @click.outside="open = false" class="absolute right-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 p-3 w-56" style="display:none">
129
- <form method="post" action="/groups" class="flex gap-2">
130
- <input type="text" name="group_name" placeholder="Group name…" required class="flex-1 text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
131
- <button type="submit" class="px-3 py-1.5 rounded-lg text-sm font-medium bg-indigo-600 text-white hover:bg-indigo-500 transition">Add</button>
132
- </form>
133
- </div>
134
- </div>
135
- <div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative">
136
- <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 bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">+ Add topic</button>
137
- <div x-show="open" @click.outside="open = false" class="absolute right-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 p-4 w-72" style="display:none">
138
- <form method="post" action="/knowledge" class="space-y-3">
139
- <div>
140
- <label for="add-topic-id" class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Topic ID</label>
141
- <input id="add-topic-id" type="text" name="topic" placeholder="e.g. rust_lifetimes" required class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
142
- </div>
143
- <div class="flex gap-2">
144
- <div class="w-24">
145
- <label for="add-topic-confidence" class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Confidence</label>
146
- <input id="add-topic-confidence" type="number" name="confidence" value="5" min="0" max="10" class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
147
- </div>
148
- <div class="flex-1">
149
- <label for="add-topic-group" class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Group</label>
150
- <select id="add-topic-group" name="group" class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500">
151
- <option value="">Other (ungrouped)</option>
152
- ${e("")}
153
- </select>
154
- </div>
155
- </div>
156
- <button type="submit" class="w-full px-3 py-1.5 rounded-lg text-sm font-medium bg-indigo-600 text-white hover:bg-indigo-500 transition">Add</button>
157
- </form>
158
- </div>
159
- </div>
160
- <button type="button" @click="toggle()"
161
- :class="editMode ? 'bg-indigo-600 text-white border-indigo-600 hover:bg-indigo-500' : 'bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400'"
162
- class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition">
163
- <span x-text="editMode ? '✓ Done' : '✎ Edit'">✎ Edit</span>
164
- </button>
165
- </div>
166
- </div>
167
-
168
- <div class="mb-6 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400">
169
- <span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(t.stats.total_lessons??0)}</span> lessons total</span>
170
- <span class="text-gray-300 dark:text-gray-600">·</span>
171
- <span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(t.stats.lessons_today??0)}</span> / ${t.maxPerDay} today</span>
172
- <span class="text-gray-300 dark:text-gray-600">·</span>
173
- <span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(t.stats.lessons_this_week??0)}</span> this week</span>
174
- <span class="text-gray-300 dark:text-gray-600">·</span>
175
- ${t.rateLimit.allowed?html`<span class="text-green-600 dark:text-green-400 font-medium">Available now</span>`:html`<span class="text-yellow-600 dark:text-yellow-400">${t.rateLimit.reason}</span>`}
176
- </div>
177
-
178
- ${Object.entries(t.categorised).map(([r,n])=>html`
179
- <section class="mb-6 group/section">
180
- <div class="flex items-center gap-2 mb-2 border-b border-gray-200 dark:border-gray-800 pb-1">
181
- <div class="flex items-center gap-1 flex-1 min-w-0">
182
- <h2 class="text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500 shrink-0">${r}</h2>
183
- ${r!=="Other"?html`<form x-show="editMode" style="display:none" method="post" action="/groups/${encodeURIComponent(r)}/delete" onsubmit="return confirm('Remove group &quot;${r}&quot;? Topics will move to Other.')">
184
- <button type="submit" class="text-xs text-gray-300 dark:text-gray-500 hover:text-red-500 dark:hover:text-red-400 transition w-4 text-center opacity-0 group-hover/section:opacity-100" title="Delete group">×</button>
185
- </form>`:""}
186
- </div>
187
- <div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative">
188
- <button type="button" @click="open = !open" class="text-xs text-gray-300 dark:text-gray-600 hover:text-indigo-500 dark:hover:text-indigo-400 transition px-1">+ topic</button>
189
- <div x-show="open" @click.outside="open = false" class="absolute right-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 p-3 w-64" style="display:none">
190
- <form method="post" action="/knowledge" class="space-y-2">
191
- <input type="hidden" name="group" value="${r!=="Other"?r:""}" />
192
- <input type="text" name="topic" placeholder="topic_id (e.g. rust_lifetimes)" required class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
193
- <div class="flex items-center gap-2">
194
- <label class="text-xs text-gray-400 dark:text-gray-500 shrink-0">Confidence</label>
195
- <input type="number" name="confidence" value="5" min="0" max="10" class="w-16 text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2 py-1 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
196
- <button type="submit" class="flex-1 px-3 py-1.5 rounded-lg text-sm font-medium bg-indigo-600 text-white hover:bg-indigo-500 transition">Add</button>
197
- </div>
198
- </form>
199
- </div>
200
- </div>
201
- </div>
202
- <div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-x-8 gap-y-0">
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`
204
- <div class="flex items-center gap-2 py-1.5 border-b border-gray-100 dark:border-gray-800/50 group/row">
205
- <div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative shrink-0 flex items-center">
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>
207
- <div x-show="open" @click.outside="open = false" 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 p-1 w-40" style="display:none">
208
- <form method="post" action="/knowledge/${encodeURIComponent(s.topic)}/group">
209
- <select name="group" onchange="this.form.submit()" class="w-full text-xs bg-transparent text-gray-700 dark:text-gray-200 px-2 py-1 focus:outline-none">
210
- <option value="Other" ${r==="Other"?"selected":""}>Other (ungrouped)</option>
211
- ${e(r)}
212
- </select>
213
- </form>
214
- </div>
215
- </div>
216
- <div class="flex items-center gap-1 flex-1 min-w-0">
217
- <a x-show="!editMode" href="/lessons?search=${encodeURIComponent(s.topic)}" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate hover:text-indigo-600 dark:hover:text-indigo-400 hover:underline transition" title="${s.topic}">${s.topic}</a>
218
- <span x-show="editMode" style="display:none" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate" title="${s.topic}">${s.topic}</span>
219
- <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(s.topic)}/delete" onsubmit="return confirm('Remove ${s.topic} from your knowledge map?')" class="shrink-0 flex items-center">
220
- <button type="submit" class="text-xs text-gray-200 dark:text-gray-700 hover:text-red-500 dark:hover:text-red-400 transition w-4 text-center opacity-0 group-hover/row:opacity-100" title="Remove topic">×</button>
221
- </form>
222
- </div>
223
- <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(s.topic)}" class="shrink-0 flex items-center">
224
- <input type="hidden" name="delta" value="-1" />
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>
226
- </form>
227
- <div class="w-20 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden shrink-0">
228
- <div class="${d} h-full rounded-full transition-all" style="width: ${c}%"></div>
229
- </div>
230
- <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(s.topic)}" class="shrink-0 flex items-center">
231
- <input type="hidden" name="delta" value="1" />
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>
233
- </form>
234
- <span class="text-xs font-bold ${b} w-5 text-right shrink-0">${a}</span>
235
- </div>`})}
236
- </div>
237
- </section>`)}
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-teal-600 dark:text-teal-400 bg-teal-50 dark:bg-teal-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}">
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}
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" />
242
- <style>
243
- .dark .flatpickr-calendar { background:#1f2937; border-color:#374151; }
244
- .dark .flatpickr-day { color:#e5e7eb; }
245
- .dark .flatpickr-day:hover { background:#374151; }
246
- .dark .flatpickr-day.selected, .dark .flatpickr-day.startRange, .dark .flatpickr-day.endRange, .dark .flatpickr-day.inRange { background:#4f46e5; border-color:#4f46e5; color:#fff; }
247
- .dark .flatpickr-day.today { border-color:#6366f1; }
248
- .dark .flatpickr-months, .dark .flatpickr-month { background:#1f2937; color:#e5e7eb; fill:#e5e7eb; }
249
- .dark .flatpickr-current-month, .dark .flatpickr-monthDropdown-months { color:#e5e7eb; background:#1f2937; }
250
- .dark .flatpickr-weekday { color:#9ca3af; background:#1f2937; }
251
- .dark .flatpickr-prev-month svg, .dark .flatpickr-next-month svg { fill:#9ca3af; }
252
- </style>`,y=t.total===0?"No lessons":t.totalPages===1?`${t.total} lesson${t.total!==1?"s":""}`:`${(t.page-1)*t.perPage+1}\u2013${Math.min(t.page*t.perPage,t.total)} of ${t.total}`,k=html`
253
- <form id="filter-form" method="get" action="/lessons">
254
- <input type="hidden" name="period" id="h-period" value="${e.period}">
255
- <input type="hidden" name="date_from" id="h-date-from" value="${e.date_from}">
256
- <input type="hidden" name="date_to" id="h-date-to" value="${e.date_to}">
257
- <input type="hidden" name="feedback" id="h-feedback" value="${e.feedback}">
258
- <input type="hidden" name="starred" id="h-starred" value="${e.starred?"1":""}">
259
- <input type="hidden" name="category" value="${e.category}">
260
- <input type="hidden" name="level" id="h-level" value="${e.level}">
261
- <input type="hidden" name="project" value="${e.project}">
262
- <input type="hidden" name="repository" value="${e.repository}">
263
- <input type="hidden" name="branch" value="${e.branch}">
264
- <input type="hidden" name="commit" value="${e.commit}">
265
-
266
- <div class="flex items-center gap-3 mb-4">
267
- <div class="relative flex-1">
268
- <span class="absolute inset-y-0 left-3.5 flex items-center text-gray-400 pointer-events-none text-sm">🔍</span>
269
- <input type="text" name="search" value="${e.search}" placeholder="Search lessons…" autocomplete="off" class="w-full pl-9 pr-10 py-3 rounded-xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-800 dark:text-gray-100 placeholder-gray-400 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" />
270
- ${e.search?html`<button type="submit" name="search" value="" class="absolute inset-y-0 right-3 flex items-center text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 text-xl leading-none">×</button>`:""}
271
- </div>
272
- <p class="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap shrink-0">${y}</p>
273
- </div>
274
-
275
- <div class="flex flex-wrap items-center gap-2 mb-3">
276
- <button type="button" onclick="var h=document.getElementById('h-starred'); h.value=h.value?'':'1'; document.getElementById('filter-form').submit()"
277
- class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.starred?"bg-yellow-400 text-yellow-900 border-yellow-400":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-yellow-400 hover:text-yellow-500"}">★ Starred</button>
278
-
279
- <div class="relative" x-data="periodPicker()" @keydown.escape="close()">
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"}">
281
- <span>📅</span><span x-text="label">${n}</span>${b}
282
- </button>
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">
284
- <div class="p-1">
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>`})}
286
- </div>
287
- <div class="border-t border-gray-100 dark:border-gray-800 p-1">
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
- <span>Custom range</span>
290
- <svg class="w-3 h-3 opacity-60 transition-transform" :class="showCustom ? 'rotate-180' : ''" 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>
291
- </button>
292
- <div x-show="showCustom" class="px-2 pb-2 pt-1">
293
- <input type="text" x-ref="fp" placeholder="Select date range…" class="w-full text-xs bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2 py-1.5 text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 cursor-pointer" />
294
- </div>
295
- </div>
296
- </div>
297
- </div>
298
-
299
- <div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
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>
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">
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>`)}
303
- </div>
304
- </div>
305
-
306
- <div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
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>
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">
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>`)}
310
- </div>
311
- </div>
312
-
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">
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>
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">
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>`:""}
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
- ${t.allRepositories.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Repository</label><select name="repository" 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 repositories</option>${t.allRepositories.map(g=>html`<option value="${g}" ${e.repository===g?"selected":""}>${g}</option>`)}</select></div>`:""}
319
- ${t.allBranches.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Branch</label><input type="text" name="branch" value="${e.branch}" placeholder="e.g. main" list="branch-list" autocomplete="off" 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" /><datalist id="branch-list">${t.allBranches.map(g=>html`<option value="${g}">`)}</datalist></div>`:""}
320
- ${t.allCommits.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Commit</label><input type="text" name="commit" value="${e.commit}" placeholder="hash prefix…" list="commit-list" autocomplete="off" 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 font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500" /><datalist id="commit-list">${t.allCommits.map(g=>html`<option value="${g.slice(0,7)}">`)}</datalist></div>`:""}
321
- <button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg px-3 py-1.5 text-xs font-medium transition">Apply</button>
322
- </div>
323
- </div>`:""}
324
-
325
- ${r?html`<a href="/lessons" class="ml-auto text-xs text-gray-400 hover:text-gray-700 dark:hover:text-white transition">Clear all</a>`:""}
326
- </div>
327
-
328
- ${r?html`<div class="flex flex-wrap gap-1.5 mb-4">
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:""})):""}
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>`:""}
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:""})):""}
340
- </div>`:""}
341
- </form>
342
-
343
- ${t.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden">
344
- <table class="w-full text-sm">
345
- <thead>
346
- <tr class="bg-gray-50 dark:bg-gray-800/60 border-b border-gray-200 dark:border-gray-800 text-left text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
347
- <th class="w-8 px-3 py-3"></th>
348
- ${d("Date","timestamp")}
349
- ${d("Topic","topic_id","hidden sm:table-cell")}
350
- ${d("Title","title")}
351
- ${d("Level","level")}
352
- <th class="px-3 py-3 hidden lg:table-cell">Categories</th>
353
- ${d("Feedback","feedback","hidden xl:table-cell")}
354
- </tr>
355
- </thead>
356
- <tbody class="divide-y divide-gray-100 dark:divide-gray-800">
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">
358
- <td class="px-3 py-3" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">
359
- <form method="post" action="/lessons/${encodeURIComponent(g.id)}/star">
360
- <input type="hidden" name="starred" value="${g.starred?"0":"1"}" />
361
- <input type="hidden" name="next" value="/lessons${$(e)}" />
362
- <button type="submit" title="${g.starred?"Unstar":"Star"}" class="w-6 text-lg text-center leading-none transition ${g.starred?"text-yellow-400 hover:text-yellow-300":"text-gray-300 dark:text-gray-600 hover:text-yellow-400"}">${g.starred?"\u2605":"\u2606"}</button>
363
- </form>
364
- </td>
365
- <td class="px-3 py-3 whitespace-nowrap tabular-nums relative group/date">
366
- <span class="text-gray-400 dark:text-gray-500 cursor-default" data-ts="${g.timestamp}">${h}</span>
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>
368
- </td>
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>
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>
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>
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>
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-teal-600 dark:text-teal-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>
374
- </tr>`})}
375
- </tbody>
376
- </table>
377
- </div>
378
- ${t.totalPages>1?html`<div class="flex items-center justify-between mt-4">
379
- <p class="text-xs text-gray-400 dark:text-gray-500">Page ${t.page} of ${t.totalPages}</p>
380
- <div class="flex items-center gap-1">
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>`}
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>`)}
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>`}
384
- </div>
385
- </div>`:""}`:html`<div class="flex flex-col items-center justify-center py-16 text-center">
386
- <p class="text-3xl mb-3">📭</p>
387
- <p class="text-gray-500 dark:text-gray-400 text-sm">No lessons match the current filters.</p>
388
- ${r?html`<a href="/lessons" class="mt-2 text-indigo-500 hover:text-indigo-400 text-sm transition">Clear all filters</a>`:""}
389
- </div>`}`,v=html`<script src="/static/vendor/flatpickr.min.js"></script>
390
- <script src="/static/relative-time.js"></script>
391
- <script>
392
- function periodPicker() {
393
- return {
394
- open: false,
395
- showCustom: ${raw(o?"true":"false")},
396
- label: ${raw(te(n))},
397
- fp: null,
398
- init() {
399
- const self = this;
400
- this.$watch('showCustom', val => {
401
- if (val && !this.fp) {
402
- this.fp = flatpickr(this.$refs.fp, {
403
- mode: 'range', dateFormat: 'Y-m-d', inline: false,
404
- ${raw(e.date_from?`defaultDate: [${te(e.date_from)}${e.date_to?`, ${te(e.date_to)}`:""}],`:"")}
405
- onChange(dates) {
406
- if (dates.length === 2) {
407
- document.getElementById('h-date-from').value = self.fp.formatDate(dates[0], 'Y-m-d');
408
- document.getElementById('h-date-to').value = self.fp.formatDate(dates[1], 'Y-m-d');
409
- document.getElementById('h-period').value = '';
410
- document.getElementById('filter-form').submit();
411
- }
412
- }
413
- });
414
- }
415
- });
416
- },
417
- toggle() { this.open = !this.open; },
418
- close() { this.open = false; },
419
- selectPreset(val) {
420
- document.getElementById('h-period').value = val;
421
- document.getElementById('h-date-from').value = '';
422
- document.getElementById('h-date-to').value = '';
423
- document.getElementById('filter-form').submit();
424
- },
425
- };
426
- }
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`
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>
429
- <div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
430
- <div class="flex flex-wrap items-center gap-3 mb-2">
431
- <form method="post" action="/lessons/${encodeURIComponent(e.id)}/star">
432
- <input type="hidden" name="starred" value="${e.starred?"0":"1"}" />
433
- <input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" />
434
- <button type="submit" title="${e.starred?"Unstar":"Star"}" class="w-6 text-center text-xl leading-none transition ${e.starred?"text-yellow-400 hover:text-yellow-300":"text-gray-300 dark:text-gray-600 hover:text-yellow-400"}">${e.starred?"\u2605":"\u2606"}</button>
435
- </form>
436
- <h1 class="text-xl font-bold text-gray-900 dark:text-white flex-1 min-w-0">${e.title}</h1>
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>
438
- </div>
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">
440
- <span class="relative group/date cursor-default">🗓 <span data-ts="${e.timestamp}">${c}</span>
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>
442
- </span>
443
- <span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.topic_id}</span></span>
444
- ${e.categories.map(k=>html`<a href="/lessons?category=${encodeURIComponent(k)}" class="inline-block bg-gray-100 dark:bg-gray-800 hover:bg-indigo-50 dark:hover:bg-indigo-900/50 text-gray-600 dark:text-gray-300 text-xs rounded px-2 py-0.5 transition border border-gray-200 dark:border-gray-700 hover:border-indigo-400 dark:hover:border-indigo-600">${k}</a>`)}
445
- ${e.feedback?html`${e.feedback==="know"?html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 border border-green-200 dark:border-green-700">✓ I know this</span>`:html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 border border-red-200 dark:border-red-700">✗ I don't know this</span>`}
446
- <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback"><input type="hidden" name="feedback" value="clear" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="text-xs text-gray-400 dark:text-gray-600 hover:text-gray-600 dark:hover:text-gray-400 transition">Clear</button></form>`:""}
447
- </div>
448
- <div class="my-5 pl-4 border-l-4 border-indigo-400 dark:border-indigo-500 bg-indigo-50 dark:bg-indigo-950/40 rounded-r-lg py-3 pr-4">
449
- <p class="text-xs font-semibold uppercase tracking-wide text-indigo-500 dark:text-indigo-400 mb-1">TL;DR</p>
450
- <div id="summary-content" class="markdown-body text-sm text-indigo-900 dark:text-indigo-100"></div>
451
- </div>
452
- <div id="body-content" class="markdown-body"></div>
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>`:""}
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">
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>`:""}
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>`:""}
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>`:""}
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>`:""}
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>`:""}
460
- </div></div>`:""}
461
- ${e.feedback?"":html`<div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 flex flex-wrap gap-2">
462
- <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback"><input type="hidden" name="feedback" value="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-green-100 dark:hover:bg-green-800 hover:text-green-700 dark:hover:text-white">✓ I know this</button></form>
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>
464
- </div>`}
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>
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>
467
- <script src="/static/vendor/marked.min.js"></script>
468
- <script src="/static/relative-time.js"></script>
469
- <script>
470
- updateHljsTheme();
471
- marked.setOptions({
472
- highlight: function(code, lang) {
473
- const language = lang && hljs.getLanguage(lang) ? lang : 'plaintext';
474
- return hljs.highlight(code, { language }).value;
475
- },
476
- breaks: true, gfm: true,
477
- });
478
- document.getElementById('summary-content').innerHTML = marked.parse(${raw(te(e.summary))});
479
- document.getElementById('body-content').innerHTML = marked.parse(${raw(te(e.body))});
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`
481
- <h1 class="text-2xl font-bold mb-6 text-indigo-600 dark:text-indigo-400">Settings</h1>
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"}">
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.":""}
484
- </div>`:""}
485
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
486
- <div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
487
- <p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 mb-5">Coaching</p>
488
- <form method="post" action="/settings" class="space-y-4">
489
- <div class="grid grid-cols-2 gap-4">
490
- <div>
491
- <label for="max-per-day" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Max lessons per day</label>
492
- <input id="max-per-day" type="number" name="max_per_day" min="1" max="20" value="${t.settings.max_per_day}" 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" />
493
- <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Lessons in a 24h window.</p>
494
- </div>
495
- <div>
496
- <label for="min-gap-minutes" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Minimum gap between lessons</label>
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">
498
- ${lr.map(([a,c])=>html`<option value="${a}" ${e===a?"selected":""}>${c}</option>`)}
499
- </select>
500
- <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Minimum gap between lessons.</p>
501
- </div>
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>
518
- <fieldset class="border-0 p-0 m-0">
519
- <legend class="block text-sm text-gray-600 dark:text-gray-400 mb-1">UI theme</legend>
520
- <div class="flex gap-2">
521
- ${o.map(([a,c,d])=>html`<label class="flex-1 cursor-pointer">
522
- <input type="radio" name="ui_theme" value="${a}" ${t.settings.ui_theme===a?"checked":""} class="sr-only peer" />
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>
524
- </label>`)}
525
- </div>
526
- <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">System follows your OS preference.</p>
527
- </fieldset>
528
- <button type="submit" class="bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded px-4 py-2 text-sm transition">Save settings</button>
529
- </form>
530
- </div>
531
-
532
- <div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
533
- <p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 mb-5">Backup &amp; Restore</p>
534
- <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
535
- <div class="flex flex-col gap-1">
536
- <p class="text-sm font-medium text-gray-700 dark:text-gray-200">Full backup</p>
537
- <p class="text-xs text-gray-400 dark:text-gray-500">Settings + knowledge map + lessons + coaching notebook as a zip file.</p>
538
- <a href="/settings/export" class="mt-auto inline-flex items-center justify-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-4 py-2 text-sm transition">↓ Download backup</a>
539
- </div>
540
- <div class="flex flex-col gap-1">
541
- <p class="text-sm font-medium text-gray-700 dark:text-gray-200">Restore backup</p>
542
- <p class="text-xs text-gray-400 dark:text-gray-500">Overwrites settings, knowledge map &amp; coaching notebook; duplicate lessons are skipped.</p>
543
- <form method="post" action="/settings/import" enctype="multipart/form-data" class="mt-auto flex flex-col gap-2">
544
- <input id="file-restore" type="file" name="file" accept=".zip,.json" class="hidden" onchange="updateLabel('file-restore','label-restore','restore-submit')" />
545
- <label id="label-restore" for="file-restore" class="cursor-pointer inline-flex items-center justify-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-4 py-2 text-sm transition truncate">Choose file…</label>
546
- <button id="restore-submit" type="submit" disabled class="bg-indigo-600 hover:bg-indigo-500 disabled:opacity-40 disabled:cursor-not-allowed text-white font-medium rounded px-4 py-2 text-sm transition">Restore</button>
547
- </form>
548
- </div>
549
- <div class="flex flex-col gap-1">
550
- <p class="text-sm font-medium text-gray-700 dark:text-gray-200">Lessons only</p>
551
- <p class="text-xs text-gray-400 dark:text-gray-500">Export just the lesson log as JSON (no settings).</p>
552
- <a href="/lessons/export" class="mt-auto inline-flex items-center justify-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-4 py-2 text-sm transition">↓ Download lessons</a>
553
- </div>
554
- <div class="flex flex-col gap-1">
555
- <p class="text-sm font-medium text-gray-700 dark:text-gray-200">Import lessons</p>
556
- <p class="text-xs text-gray-400 dark:text-gray-500">Merge a lessons JSON file; duplicates are skipped.</p>
557
- <form method="post" action="/lessons/import" enctype="multipart/form-data" class="mt-auto flex flex-col gap-2">
558
- <input id="file-import" type="file" name="file" accept=".json" class="hidden" onchange="updateLabel('file-import','label-import','import-submit')" />
559
- <label id="label-import" for="file-import" class="cursor-pointer inline-flex items-center justify-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-4 py-2 text-sm transition truncate">Choose file…</label>
560
- <button id="import-submit" type="submit" disabled class="bg-indigo-600 hover:bg-indigo-500 disabled:opacity-40 disabled:cursor-not-allowed text-white font-medium rounded px-4 py-2 text-sm transition">Import</button>
561
- </form>
562
- </div>
563
- </div>
564
- </div>
565
-
566
- <div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6 lg:col-span-2" x-data="{ nbMode: 'preview' }">
567
- <div class="flex flex-wrap items-center justify-between gap-2 mb-4">
568
- <p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Coaching Notebook</p>
569
- <div class="flex items-center gap-2">
570
- <div class="flex rounded border border-gray-200 dark:border-gray-700 overflow-hidden text-xs font-medium">
571
- <button type="button" @click="nbMode = 'preview'" :class="nbMode === 'preview' ? 'bg-indigo-600 text-white' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'" class="px-3 py-1.5 transition">Preview</button>
572
- <button type="button" @click="nbMode = 'source'" :class="nbMode === 'source' ? 'bg-indigo-600 text-white' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'" class="px-3 py-1.5 transition border-l border-gray-200 dark:border-gray-700">Source</button>
573
- </div>
574
- <a href="/settings/notebook/download" class="inline-flex items-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-3 py-1.5 text-xs transition">↓ Download</a>
575
- <a href="vscode://file/${t.notebookPath}" title="Open in VS Code" class="inline-flex items-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-3 py-1.5 text-xs transition"><img src="/static/vscode.svg" class="w-4 h-4" alt="VS Code" />Open in VS Code</a>
576
- </div>
577
- </div>
578
- <div x-show="nbMode === 'preview'" id="notebook-preview-settings" class="markdown-body min-h-24"></div>
579
- <pre x-show="nbMode === 'source'" style="display:none" class="text-xs font-mono bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded p-3 overflow-x-auto whitespace-pre-wrap text-gray-700 dark:text-gray-200 min-h-24">${t.notebookContent}</pre>
580
- </div>
581
- </div>`,s=html`<script src="/static/vendor/marked.min.js"></script>
582
- <script>
583
- function updateLabel(inputId, labelId, submitId) {
584
- var input = document.getElementById(inputId);
585
- document.getElementById(labelId).textContent = input.files.length ? input.files[0].name : 'Choose file…';
586
- document.getElementById(submitId).disabled = !input.files.length;
587
- }
588
- marked.setOptions({ breaks: true, gfm: true });
589
- document.getElementById('notebook-preview-settings').innerHTML = marked.parse(${raw(te(t.notebookContent))});
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.
594
- `),u(i.dim(`Tip: run devcoach backup to export your profile, lessons and settings.
595
- run devcoach restore <file> to import a backup on a new machine.
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(`
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(`
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.
606
-
607
- 1. Read the MCP resources devcoach://profile, devcoach://taught-topics, and devcoach://notebook.
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.
609
- 3. Use the notebook to choose WHAT to teach: skip topics it marks as absorbed, prefer its 'Recommended focus', calibrate depth from 'Recurring patterns', and watch its 'Open hypotheses'. Pick ONE devcoach://profile topic NOT already in devcoach://taught-topics, pitched at or above the user's confidence band.
610
- 4. FIRST write the lesson as your visible reply \u2014 in EXACTLY this format. It is plain markdown between two band headings; do NOT prefix lines with '>' (no blockquote):
611
-
612
- ### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u{1F393} devcoach \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
613
- **[Title]** \xB7 [Category] \xB7 [Junior|Mid|Senior]
614
-
615
- [3\u20136 short paragraphs: explain the why, tie it to the task; fenced code if useful]
616
-
617
- \u{1F4A1} *Senior tip:* [one line]
618
- ### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 [topic] \xB7 [level] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
619
-
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.
621
- ${s}
622
-
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(`
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(`
625
- ${i.dim("Run")} ${i.bold("devcoach <command> --help")} ${i.dim("for per-command options.")}
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);}
2
+ var r=process.argv[2]??"",t=new Set(["stop-hook","prompt-hook","onboard-hook","lesson-ready"]);try{if(t.has(r)){let{runHook:o}=await import('./hooks-7QPFYLPI.js');o(r);}else {let{runCli:o}=await import('./commands-NL54QLEU.js');await o();}}catch(o){console.error(o),process.exit(1);}