devcoach 0.3.58 → 0.3.60
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 +59 -59
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {spawn,spawnSync,execFileSync}from'child_process';import {existsSync,
|
|
2
|
+
import {spawn,spawnSync,execFileSync}from'child_process';import {existsSync,writeFileSync,readFileSync,mkdirSync,statSync,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 me=z$1.enum(["junior","mid","senior"]),fe=z$1.enum(["github","gitlab","bitbucket","local"]),Re=z$1.enum(["know","dont_know"]),Le=z$1.number().int().min(0).max(10);function qt(t){return t.toISOString().replace(/\.\d{3}Z$/,"Z")}function Wt(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 Xt(t){let e=Wt(t),o=new Date;return e.getTime()>o.getTime()&&(e=o),qt(e)}var Yt=z$1.union([z$1.string(),z$1.date()]).transform((t,e)=>{try{return Xt(t)}catch(o){return e.addIssue({code:"custom",message:o.message}),z$1.NEVER}}),H=z$1.string().nullish().transform(t=>t??null),Jt=z$1.object({id:z$1.string(),timestamp:Yt,topic_id:z$1.string(),categories:z$1.array(z$1.string()),title:z$1.string(),level:me,summary:z$1.string(),body:H,task_context:H,project:H,repository:H,branch:H,commit_hash:H,folder:H,repository_platform:fe.nullish().transform(t=>t??null),starred:z$1.boolean().default(false),feedback:Re.nullish().transform(t=>t??null)}),Vt=z$1.object({topic:z$1.string(),confidence:z$1.number().int()}),Zt=z$1.object({name:z$1.string(),topics:z$1.array(z$1.string())});z$1.object({knowledge:z$1.array(Vt),groups:z$1.array(Zt)});var Ie=z$1.enum(["system","dark","light"]);z$1.object({max_per_day:z$1.number().int().default(2),min_gap_minutes:z$1.number().int().default(240),ui_theme:Ie.default("system")});function re(t){return Jt.parse(t)}var{DatabaseSync:io}=createRequire(import.meta.url)("node:sqlite"),it=join(homedir(),".devcoach"),Pe=join(it,"coaching.db"),w=join(it,"learning-state.md"),Oe="settings.json",De="lessons.json",Ne="knowledge.json",je="learning-state.md",at={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},ao={max_per_day:"2",min_gap_minutes:"240",ui_theme:"system"};function j(t,e,...o){return t.prepare(e).all(...o)}function G(t,e,...o){return t.prepare(e).get(...o)}function S(t,e,...o){return Number(t.prepare(e).run(...o).changes)}function co(t=Pe){return t!==":memory:"&&mkdirSync(dirname(t),{recursive:true}),new io(t)}function lo(t=Pe){let e=co(t);return po(e),e}function m(t,e=Pe){let o=lo(e);try{return t(o)}finally{o.close();}}function po(t){t.exec(`
|
|
3
3
|
CREATE TABLE IF NOT EXISTS lessons (
|
|
4
4
|
id TEXT PRIMARY KEY,
|
|
5
5
|
timestamp TEXT NOT NULL,
|
|
@@ -45,8 +45,8 @@ import {spawn,spawnSync,execFileSync}from'child_process';import {existsSync,stat
|
|
|
45
45
|
CREATE INDEX IF NOT EXISTS idx_lessons_starred_ts ON lessons (starred, timestamp);
|
|
46
46
|
CREATE INDEX IF NOT EXISTS idx_lessons_feedback ON lessons (feedback);
|
|
47
47
|
CREATE INDEX IF NOT EXISTS idx_lessons_topic_id ON lessons (topic_id);
|
|
48
|
-
`),
|
|
49
|
-
ON CONFLICT(topic) DO UPDATE SET confidence = excluded.confidence, updated_at = excluded.updated_at`,e,r,n);}function Q(t,e){let o=E(t,"DELETE FROM knowledge WHERE topic = ?",e);return E(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 E(t,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",o)>0}function ee(t,e){return E(t,"DELETE FROM knowledge_groups WHERE group_name = ?",e),E(t,"DELETE FROM knowledge_group_names WHERE group_name = ?",e)>0}function N(t,e,o){E(t,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",o),E(t,"DELETE FROM knowledge_groups WHERE topic = ?",e),E(t,"INSERT OR IGNORE INTO knowledge_groups (group_name, topic) VALUES (?, ?)",o,e);}function ve(t,e){E(t,"DELETE FROM knowledge_groups WHERE topic = ?",e);}function C(t){let e={};for(let d of j(t,"SELECT key, value FROM settings"))e[d.key]=d.value;let o=e.min_gap_minutes,r=e.min_hours_between,n;o!==void 0?n=Number.parseInt(o,10):r!==void 0?n=Number.parseInt(r,10)*60:n=240;let s=e.max_per_day,a=e.ui_theme??"system",l=a==="dark"||a==="light"||a==="system"?a:"system";return {max_per_day:s!==void 0?Number.parseInt(s,10):2,min_gap_minutes:n,ui_theme:l}}function L(t,e,o){E(t,"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",e,o);}function ce(t){let e=B(t,"SELECT COUNT(*) AS n FROM knowledge");return {knowledge_ready:Number(e?.n??0)>0}}function Ke(t){let e={};for(let o of ["project","repository","branch","repository_platform"]){let r=B(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=C(t),o=ie(t),r=de(t),n=Be(t),s=new Map;for(let d of n)for(let f of d.topics)s.set(f,d.name);let a={groups:n.map(d=>d.name),topics:r.map(d=>({topic:d.topic,confidence:d.confidence,group:s.get(d.topic)??null}))},l={[je]:strToU8(JSON.stringify(e,null,2)),[Ae]:strToU8(JSON.stringify(o,null,2)),[Pe]:strToU8(JSON.stringify(a,null,2))};return existsSync(w)&&(l[Me]=strToU8(readFileSync(w,"utf8"))),zipSync(l)}function ko(t,e,o){if(!(je in e))return;let r=JSON.parse(strFromU8(e[je]));"max_per_day"in r&&L(t,"max_per_day",String(r.max_per_day)),"min_gap_minutes"in r&&L(t,"min_gap_minutes",String(r.min_gap_minutes)),(r.ui_theme==="system"||r.ui_theme==="dark"||r.ui_theme==="light")&&L(t,"ui_theme",r.ui_theme),o.settings=1;}function vo(t,e,o){if(!(Pe in e))return;let r=JSON.parse(strFromU8(e[Pe]));if(!(r&&typeof r=="object"&&Array.isArray(r.topics))){for(let[s,a]of Object.entries(r))D(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){D(t,s.topic,s.confidence);let a=s.group;a&&a!=="Other"&&(M(t,a)&&(n+=1),N(t,s.topic,a));}o.topics=r.topics.length,o.groups=n;}function wo(t,e,o){if(!(Ae in e))return;let r=JSON.parse(strFromU8(e[Ae])),{inserted:n,duplicated:s,invalid:a}=He(t,r);o.lessons=n,o.skipped=s,o.invalid=a;}function _o(t,e){Me in t&&(mkdirSync(dirname(w),{recursive:true}),writeFileSync(w,strFromU8(t[Me]),"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 ko(t,r,o),vo(t,r,o),wo(t,r,o),_o(r,o),o}function $o(t){let e=new Date,o;if(t==="today")o=new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),0,0,0,0));else if(t==="week")o=new Date(e.getTime()-7*864e5);else if(t==="month")o=new Date(e.getTime()-30*864e5);else if(t==="year")o=new Date(e.getTime()-365*864e5);else return null;return o.toISOString()}function ze(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 q(t){try{let e=C(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=xt(t);if(s!=null){let a=new Date(s),l=(o.getTime()-a.getTime())/6e4;if(l<e.min_gap_minutes){let d=e.min_gap_minutes-l,f=Math.floor(e.min_gap_minutes/60),k=e.min_gap_minutes%60,v=Math.trunc(d),b=Math.floor(v/60),y=v%60;return {allowed:!1,reason:`Too soon: last lesson ${l<0?`in ${Math.round(-l)}m (future timestamp)`:`${Math.round(l)}m ago`}, minimum interval is ${f}h ${k}m (${b}h ${y}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:Be(t)}}catch{return {knowledge:[],groups:[]}}}function W(t,e,o){let r=Ge(t)[e]??5,n=Math.max(0,Math.min(10,r+o));return D(t,e,n),n}function _e(t,e,o){let r=se(t,e,o);return r&&(o==="know"||o==="dont_know")&&W(t,r,o==="know"?1:-1),r}function te(t){try{let e=new Date,o=V(t),r=new Date(e.getTime()-24*36e5).toISOString(),n=new Date(e.getTime()-7*864e5).toISOString(),s=ke(t,r),a=ke(t,n),l=Ge(t),d=Object.entries(l).sort((v,b)=>v[1]-b[1]),f=d.slice(0,5).map(([v,b])=>({topic:v,confidence:b})),k=d.slice(-5).reverse().map(([v,b])=>({topic:v,confidence:b}));return {total_lessons:o,lessons_today:s,lessons_this_week:a,weakest_topics:f,strongest_topics:k}}catch(e){return {error:String(e)}}}function kt(t){try{return ht(t)}catch{return []}}var Co=[["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]],Ro={react:"react","react-dom":"react",vue:"vue","@angular/core":"angular",svelte:"svelte",next:"nextjs",nuxt:"vue",fastify:"fastify",express:"express",koa:"express","@nestjs/core":"node_js"},Lo={django:"django",flask:"flask",fastapi:"fastapi",starlette:"fastapi",tornado:"python",aiohttp:"python",sqlalchemy:"postgresql",celery:"python"};function Io(t){let e="";for(let o of t)o==="*"?e+="[^/]*":o==="?"?e+="[^/]":e+=o.replace(/[.+^${}()|[\]\\]/g,String.raw`\$&`);return new RegExp(`^${e}$`)}function Oo(t,e){let o=e.lastIndexOf("/"),r=o>=0?join(t,e.slice(0,o)):t,n=o>=0?e.slice(o+1):e,s;try{s=readdirSync(r);}catch{return false}let a=Io(n);return s.some(l=>a.test(l))}function vt(t){try{return existsSync(t)?readFileSync(t,"utf8"):null}catch{return null}}function Do(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(Ro))n.has(s)&&e(a,6);}function No(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(Lo))n.includes(s)&&e(a,6);}}function $e(t){let e={},o=(r,n)=>{(e[r]??-1)<n&&(e[r]=n);};for(let[r,n,s]of Co)Oo(t,r)&&o(n,s);return Do(t,o),No(t,o),e}function Ye(...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 Po(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 oe(){let t=process.cwd(),e=Ye("rev-parse","--abbrev-ref","HEAD"),o=Ye("rev-parse","HEAD"),r=Ye("remote","get-url","origin"),[n,s]=Po(r),a;return n?a=n.replace(/\/+$/,"").split("/").pop()??null:a=basename(t)||null,e==="HEAD"&&(e=null),{project:a,repository:n,branch:e,commit_hash:o,folder:t,repository_platform:s}}var $t=dirname(fileURLToPath(import.meta.url)),Ho=[join($t,"../assets/SKILL.md"),join($t,"../../assets/SKILL.md")];function St(){for(let t of Ho)try{return readFileSync(t,"utf8")}catch{}return "devcoach: coaching instructions unavailable (SKILL.md not found in package)."}var Tt={version:"0.3.58"};var pe=Tt.version;var O=t=>({type:"text",text:t}),Et=t=>({content:[O(JSON.stringify(t))]}),I=t=>({isError:true,content:[O(t)]}),Je=t=>({content:[O(JSON.stringify(t))],structuredContent:t}),Yo={id:z$1.string(),timestamp:z$1.string(),topic_id:z$1.string(),categories:z$1.array(z$1.string()),title:z$1.string(),level:fe,summary:z$1.string(),body:z$1.string().nullable(),task_context:z$1.string().nullable(),project:z$1.string().nullable(),repository:z$1.string().nullable(),branch:z$1.string().nullable(),commit_hash:z$1.string().nullable(),folder:z$1.string().nullable(),repository_platform:be.nullable(),starred:z$1.boolean(),feedback:Oe.nullable()},Jo={knowledge:z$1.array(z$1.object({topic:z$1.string(),confidence:z$1.number().int()})),groups:z$1.array(z$1.object({name:z$1.string(),topics:z$1.array(z$1.string())}))},Vo={max_per_day:z$1.number().int(),min_gap_minutes:z$1.number().int(),ui_theme:Ne};function S(t,e){return {contents:[{uri:t.href,mimeType:"application/json",text:JSON.stringify(e)}]}}function Zo(){let t=new McpServer({name:"devcoach",version:pe},{instructions:"Progressive technical coaching server. Use the devcoach_instructions prompt for full coaching behaviour guidelines."});t.registerTool("log_lesson",{title:"Log Lesson",description:"Save a delivered lesson to the coaching log. Git metadata (project, repository, branch, commit_hash, folder, repository_platform) is auto-detected from the workspace when omitted. Returns the saved Lesson with all resolved fields.",inputSchema:{id:z$1.string().describe("Unique lesson id (uuid or random string)"),timestamp:z$1.string().describe("ISO 8601 timestamp; normalized to UTC and clamped to now"),topic_id:z$1.string().describe("Primary topic id, e.g. 'python'"),categories:z$1.array(z$1.string()).describe("Category tags, e.g. ['python','performance']"),title:z$1.string().describe("Short lesson title"),level:fe.describe("Difficulty level: junior | mid | senior"),summary:z$1.string().describe("1-3 sentence summary shown in the lesson card"),body:z$1.string().nullish().describe("Optional full lesson markdown"),task_context:z$1.string().nullish().describe("What the user was doing when taught"),project:z$1.string().nullish().describe("Project name (auto-detected from git if omitted)"),repository:z$1.string().nullish().describe("org/repo or path (auto-detected if omitted)"),branch:z$1.string().nullish().describe("git branch (auto-detected if omitted)"),commit_hash:z$1.string().nullish().describe("git commit hash (auto-detected if omitted)"),folder:z$1.string().nullish().describe("workspace folder (auto-detected if omitted)"),repository_platform:be.nullish().describe("github | gitlab | bitbucket | local (auto-detected if omitted)")},outputSchema:Yo,annotations:{title:"Log Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=oe(),n={};try{n=m(a=>Ke(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??null,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=>ft(a,s));try{if(t.server.getClientCapabilities()?.elicitation){let l=await t.server.elicitInput({message:"Did that land?",requestedSchema:{type:"object",properties:{feedback:{type:"string",title:"Did that land?",description:"know = got it \xB7 dont_know = need to revisit \xB7 skip = no feedback",enum:["know","dont_know","skip"]}},required:[]}}),d=l.action==="accept"?l.content?.feedback:void 0;d&&d!=="skip"&&(m(f=>{se(f,o.id,d),W(f,o.topic_id,d==="know"?1:-1);}),s={...s,feedback:d});}}catch{}return Je(s)}catch(r){return I(`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=>W(n,o.topic,o.delta));return {content:[O(String(r))]}}catch(r){return I(`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=>K(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=>Z(n,o.lesson_id,o.starred));return {content:[O(String(r))]}}catch(r){return I(`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:[O(String(r))]}}catch(r){return I(`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&&W(s,a.topic_id,r==="know"?1:-1)),!0):!1});return {content:[O(String(n))]}}catch(r){return I(`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:De.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=>{D(r,o.topic,o.confidence),o.group&&o.group!=="Other"&&N(r,o.topic,o.group);}),{content:[O("true")]}}catch(r){return I(`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=>Q(n,o.topic));return {content:[O(String(r))]}}catch(r){return I(`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:[O("true")]}}catch(r){return I(`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=>ee(n,o.name));return {content:[O(String(r))]}}catch(r){return I(`remove_group failed for '${o.name}': ${r}`)}}),t.registerTool("update_settings",{title:"Update Settings",description:"Update a coaching setting. max_per_day: integer 1-20. min_gap_minutes: integer 0-1440 (0 = no cooldown). Returns the full updated Settings.",inputSchema:{key:z$1.enum(["max_per_day","min_gap_minutes"]).describe("Setting key"),value:z$1.string().describe("New value as an integer string")},outputSchema:Vo,annotations:{title:"Update Settings",destructiveHint:false,idempotentHint:true,openWorldHint:false}},o=>{let r=Number.parseInt(o.value,10);if(Number.isNaN(r))return I(`Value must be an integer, got '${o.value}'`);if(o.key==="max_per_day"&&!(r>=1&&r<=20))return I("max_per_day must be between 1 and 20");if(o.key==="min_gap_minutes"&&!(r>=0&&r<=1440))return I("min_gap_minutes must be between 0 and 1440");let n=m(s=>(L(s,o.key,String(r)),C(s)));return Je(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:[O(`devcoach UI starting at http://localhost:${r}`)]}):{content:[O(`error: port ${r} is out of valid range (1024-65535)`)]}}),t.registerTool("complete_onboarding",{title:"Complete Onboarding",description:"Save the user's initial knowledge profile and mark onboarding complete. Wipes any default-seeded profile. topics: {topic_id: confidence_0_to_10}. groups: {group_name: [topic_id,...]} (topics not in any group go to 'Other'). Returns the updated Profile.",inputSchema:{topics:z$1.record(z$1.string(),De).describe("{topic_id: confidence 0-10}"),groups:z$1.record(z$1.string(),z$1.array(z$1.string())).optional().describe("{group_name: [topic_id, ...]}")},outputSchema:Jo,annotations:{title:"Complete Onboarding",destructiveHint:true,idempotentHint:false,openWorldHint:false}},async o=>{try{let r=m(n=>{n.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[s,a]of Object.entries(o.topics))D(n,s,Math.max(0,Math.min(10,a)));if(o.groups)for(let[s,a]of Object.entries(o.groups))for(let l of a)l in o.topics&&N(n,l,s);return F(n)});return Je(r)}catch(r){return I(`complete_onboarding failed: ${r}`)}});let e=(o,r)=>({title:o,description:r,mimeType:"application/json"});return t.registerResource("profile","devcoach://profile",e("Knowledge Profile","Current knowledge map \u2014 topics, confidence scores, and groups."),o=>{try{return S(o,m(r=>F(r)))}catch(r){return S(o,{error:String(r)})}}),t.registerResource("settings","devcoach://settings",e("Settings","Current coaching settings (rate limits, UI theme)."),o=>{try{return S(o,m(r=>C(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=>K(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=>te(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:q(a),total:V(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=oe(),n=m(s=>Ke(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=oe(),l=$e(a.folder??process.cwd());return S(o,{knowledge_ready:n,notebook_ready:s,needs_onboarding:!(n&&s),detected_stack:l,default_topics:pt,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=>z(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:St()}}]})),t}async function Ct(){let t=Zo(),e=new StdioServerTransport;await t.connect(e);}function re(t){return JSON.stringify(t??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function Se(t){let e=(o,r,n)=>html`<a href="${o}" class="text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition ${n?"text-gray-900 dark:text-white font-semibold":""}">${r}</a>`;return html`<!DOCTYPE html>
|
|
48
|
+
`),go(t),uo(t);}function go(t){try{t.exec("ALTER TABLE lessons ADD COLUMN body TEXT");}catch{}}function uo(t){for(let[e,o]of Object.entries(ao))S(t,"INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)",e,o);}var dt="(id, timestamp, topic_id, categories, title, level, summary, body, task_context, project, repository, branch, commit_hash, folder, repository_platform, starred, feedback)",ct="(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";function lt(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 pt(t,e){S(t,`INSERT OR REPLACE INTO lessons ${dt} VALUES ${ct}`,...lt(e));}var mo=[["project","project"],["repository","repository"],["branch","branch"],["commit","commit_hash"]];function fo(t,e){if(t.date_from==null&&t.date_to==null){let o=wo(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 gt(t){let e=[],o=[],r=(s,...a)=>{e.push(s),o.push(...a);};fo(t,r),t.category!=null&&r("categories LIKE ?",`%"${t.category}"%`),t.level!=null&&r("level = ?",t.level);for(let[s,a]of mo){let l=t[s];l!=null&&r(`${a} LIKE ?`,`%${l}%`);}if(t.starred!=null&&r("starred = ?",t.starred?1:0),t.search!=null){let s=`%${t.search}%`;r("(title LIKE ? OR topic_id LIKE ? OR summary LIKE ? OR body LIKE ?)",s,s,s,s);}return t.feedback==="none"?r("feedback IS NULL"):t.feedback!=null&&r("feedback = ?",t.feedback),{where:e.length?`WHERE ${e.join(" AND ")}`:"",params:o}}function X(t,e={}){let{where:o,params:r}=gt(e),n=G(t,`SELECT COUNT(*) AS n FROM lessons ${o}`,...r);return Number(n?.n??0)}var bo=new Set(["timestamp","level","topic_id","title","feedback"]),yo=new Set(["project","repository","branch","commit_hash","repository_platform"]);function B(t,e={}){let{where:o,params:r}=gt(e),n=e.sort&&bo.has(e.sort)?e.sort:"timestamp",s=(e.order??"desc").toLowerCase()==="asc"?"ASC":"DESC",a=`SELECT * FROM lessons ${o} ORDER BY ${n} ${s}`,l=[...r];if(e.page!=null){let d=e.per_page??25;a+=" LIMIT ? OFFSET ?",l.push(d,(e.page-1)*d);}return j(t,a,...l).map(Ge)}function he(t,e){return S(t,"DELETE FROM lessons WHERE id = ?",e)>0}function Y(t,e,o){return S(t,"UPDATE lessons SET starred = ? WHERE id = ?",o?1:0,e)>0}function ne(t,e,o){S(t,"UPDATE lessons SET feedback = ? WHERE id = ?",o||null,e);let r=G(t,"SELECT topic_id FROM lessons WHERE id = ?",e);return r?r.topic_id:null}function se(t){return j(t,"SELECT * FROM lessons ORDER BY timestamp DESC").map(Ge)}function Me(t,e){let o=0,r=0,n=0;for(let s of e){let a;try{a=re(s);}catch{n+=1;continue}S(t,`INSERT OR IGNORE INTO lessons ${dt} VALUES ${ct}`,...lt(a))>0?o+=1:r+=1;}return {inserted:o,duplicated:r,invalid:n}}function ie(t,e){if(!yo.has(e))throw new Error(`Column not allowed: ${JSON.stringify(e)}`);return j(t,`SELECT DISTINCT ${e} FROM lessons WHERE ${e} IS NOT NULL ORDER BY ${e}`).map(r=>r[e])}function K(t,e){let o=G(t,"SELECT * FROM lessons WHERE id = ?",e);return o?Ge(o):null}function ut(t){return j(t,"SELECT DISTINCT value FROM lessons, json_each(lessons.categories) ORDER BY value").map(o=>o.value)}function mt(t){return j(t,"SELECT DISTINCT topic_id FROM lessons").map(e=>e.topic_id)}function xe(t,e){let o=G(t,"SELECT COUNT(*) AS n FROM lessons WHERE timestamp >= ?",e);return Number(o?.n??0)}function ft(t){let e=G(t,"SELECT timestamp FROM lessons ORDER BY timestamp DESC LIMIT 1");return e?e.timestamp:null}function Fe(t){let e={};for(let o of j(t,"SELECT topic, confidence FROM knowledge"))e[o.topic]=Number(o.confidence);return e}function ae(t){return j(t,"SELECT topic, confidence FROM knowledge ORDER BY topic").map(e=>({topic:e.topic,confidence:Number(e.confidence)}))}function Ue(t){let e=j(t,"SELECT group_name FROM knowledge_group_names ORDER BY group_name").map(r=>r.group_name),o=new Map;for(let r of j(t,"SELECT group_name, topic FROM knowledge_groups ORDER BY group_name, topic")){let n=r.group_name,s=o.get(n)??[];s.push(r.topic),o.set(n,s);}return e.map(r=>({name:r,topics:o.get(r)??[]}))}function O(t,e,o){let r=Math.max(0,Math.min(10,o)),n=new Date().toISOString();S(t,`INSERT INTO knowledge (topic, confidence, updated_at) VALUES (?, ?, ?)
|
|
49
|
+
ON CONFLICT(topic) DO UPDATE SET confidence = excluded.confidence, updated_at = excluded.updated_at`,e,r,n);}function J(t,e){let o=S(t,"DELETE FROM knowledge WHERE topic = ?",e);return S(t,"DELETE FROM knowledge_groups WHERE topic = ?",e),o>0}function P(t,e){let o=e.trim();if(!o)throw new Error("Group name must not be empty");return S(t,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",o)>0}function V(t,e){return S(t,"DELETE FROM knowledge_groups WHERE group_name = ?",e),S(t,"DELETE FROM knowledge_group_names WHERE group_name = ?",e)>0}function N(t,e,o){S(t,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",o),S(t,"DELETE FROM knowledge_groups WHERE topic = ?",e),S(t,"INSERT OR IGNORE INTO knowledge_groups (group_name, topic) VALUES (?, ?)",o,e);}function ke(t,e){S(t,"DELETE FROM knowledge_groups WHERE topic = ?",e);}function E(t){let e={};for(let d of j(t,"SELECT key, value FROM settings"))e[d.key]=d.value;let o=e.min_gap_minutes,r=e.min_hours_between,n;o!==void 0?n=Number.parseInt(o,10):r!==void 0?n=Number.parseInt(r,10)*60:n=240;let s=e.max_per_day,a=e.ui_theme??"system",l=a==="dark"||a==="light"||a==="system"?a:"system";return {max_per_day:s!==void 0?Number.parseInt(s,10):2,min_gap_minutes:n,ui_theme:l}}function R(t,e,o){S(t,"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",e,o);}function de(t){let e=G(t,"SELECT COUNT(*) AS n FROM knowledge");return {knowledge_ready:Number(e?.n??0)>0}}function He(t){let e={};for(let o of ["project","repository","branch","repository_platform"]){let r=G(t,`SELECT ${o} AS v, COUNT(*) AS c FROM lessons WHERE ${o} IS NOT NULL GROUP BY ${o} ORDER BY c DESC LIMIT 1`);e[o]=r?r.v:null;}return e}function ve(t){let e=E(t),o=se(t),r=ae(t),n=Ue(t),s=new Map;for(let d of n)for(let f of d.topics)s.set(f,d.name);let a={groups:n.map(d=>d.name),topics:r.map(d=>({topic:d.topic,confidence:d.confidence,group:s.get(d.topic)??null}))},l={[Oe]:strToU8(JSON.stringify(e,null,2)),[De]:strToU8(JSON.stringify(o,null,2)),[Ne]:strToU8(JSON.stringify(a,null,2))};return existsSync(w)&&(l[je]=strToU8(readFileSync(w,"utf8"))),zipSync(l)}function ho(t,e,o){if(!(Oe in e))return;let r=JSON.parse(strFromU8(e[Oe]));"max_per_day"in r&&R(t,"max_per_day",String(r.max_per_day)),"min_gap_minutes"in r&&R(t,"min_gap_minutes",String(r.min_gap_minutes)),(r.ui_theme==="system"||r.ui_theme==="dark"||r.ui_theme==="light")&&R(t,"ui_theme",r.ui_theme),o.settings=1;}function xo(t,e,o){if(!(Ne in e))return;let r=JSON.parse(strFromU8(e[Ne]));if(!(r&&typeof r=="object"&&Array.isArray(r.topics))){for(let[s,a]of Object.entries(r))O(t,s,a);o.topics=Object.keys(r).length;return}let n=0;for(let s of r.groups??[])P(t,s)&&(n+=1);for(let s of r.topics){O(t,s.topic,s.confidence);let a=s.group;a&&a!=="Other"&&(P(t,a)&&(n+=1),N(t,s.topic,a));}o.topics=r.topics.length,o.groups=n;}function ko(t,e,o){if(!(De in e))return;let r=JSON.parse(strFromU8(e[De])),{inserted:n,duplicated:s,invalid:a}=Me(t,r);o.lessons=n,o.skipped=s,o.invalid=a;}function vo(t,e){je in t&&(mkdirSync(dirname(w),{recursive:true}),writeFileSync(w,strFromU8(t[je]),"utf8"),e.learning_state=1);}function ce(t,e){let o={settings:0,topics:0,groups:0,lessons:0,skipped:0,invalid:0,learning_state:0},r=unzipSync(e);return ho(t,r,o),xo(t,r,o),ko(t,r,o),vo(r,o),o}function wo(t){let e=new Date,o;if(t==="today")o=new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),0,0,0,0));else if(t==="week")o=new Date(e.getTime()-7*864e5);else if(t==="month")o=new Date(e.getTime()-30*864e5);else if(t==="year")o=new Date(e.getTime()-365*864e5);else return null;return o.toISOString()}function Ge(t){let e=[];try{let o=JSON.parse(t.categories);Array.isArray(o)&&(e=o);}catch{e=[];}return re({...t,categories:e,starred:!!t.starred})}function z(t){try{let e=E(t),o=new Date,r=new Date(o.getTime()-24*36e5).toISOString(),n=xe(t,r);if(n>=e.max_per_day)return {allowed:!1,reason:`Daily limit reached (${n}/${e.max_per_day} lessons in the last 24h)`};let s=ft(t);if(s!=null){let a=new Date(s),l=(o.getTime()-a.getTime())/6e4;if(l<e.min_gap_minutes){let d=e.min_gap_minutes-l,f=Math.floor(e.min_gap_minutes/60),b=e.min_gap_minutes%60,y=Math.trunc(d),k=Math.floor(y/60),v=y%60;return {allowed:!1,reason:`Too soon: last lesson ${l<0?`in ${Math.round(-l)}m (future timestamp)`:`${Math.round(l)}m ago`}, minimum interval is ${f}h ${b}m (${k}h ${v}m remaining)`}}}return {allowed:!0}}catch(e){return {allowed:true,reason:`Rate limit check failed: ${e}`}}}function M(t){try{return {knowledge:ae(t),groups:Ue(t)}}catch{return {knowledge:[],groups:[]}}}function q(t,e,o){let r=Fe(t)[e]??5,n=Math.max(0,Math.min(10,r+o));return O(t,e,n),n}function we(t,e,o){let r=ne(t,e,o);return r&&(o==="know"||o==="dont_know")&&q(t,r,o==="know"?1:-1),r}function Z(t){try{let e=new Date,o=X(t),r=new Date(e.getTime()-24*36e5).toISOString(),n=new Date(e.getTime()-7*864e5).toISOString(),s=xe(t,r),a=xe(t,n),l=Fe(t),d=Object.entries(l).sort((y,k)=>y[1]-k[1]),f=d.slice(0,5).map(([y,k])=>({topic:y,confidence:k})),b=d.slice(-5).reverse().map(([y,k])=>({topic:y,confidence:k}));return {total_lessons:o,lessons_today:s,lessons_this_week:a,weakest_topics:f,strongest_topics:b}}catch(e){return {error:String(e)}}}function bt(t){try{return mt(t)}catch{return []}}var To=[["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]],Eo={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"},Co={django:"django",flask:"flask",fastapi:"fastapi",starlette:"fastapi",tornado:"python",aiohttp:"python",sqlalchemy:"postgresql",celery:"python"};function Ro(t){let e="";for(let o of t)o==="*"?e+="[^/]*":o==="?"?e+="[^/]":e+=o.replace(/[.+^${}()|[\]\\]/g,String.raw`\$&`);return new RegExp(`^${e}$`)}function Lo(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=Ro(n);return s.some(l=>a.test(l))}function yt(t){try{return existsSync(t)?readFileSync(t,"utf8"):null}catch{return null}}function Io(t,e){let o=yt(join(t,"package.json"));if(!o)return;let r;try{r=JSON.parse(o);}catch{return}let n=new Set([...Object.keys(r.dependencies??{}),...Object.keys(r.devDependencies??{})]);for(let[s,a]of Object.entries(Eo))n.has(s)&&e(a,6);}function Oo(t,e){let o=["requirements.txt","requirements-dev.txt","requirements/base.txt","pyproject.toml"];for(let r of o){let n=yt(join(t,r))?.toLowerCase();if(n)for(let[s,a]of Object.entries(Co))n.includes(s)&&e(a,6);}}function _e(t){let e={},o=(r,n)=>{(e[r]??-1)<n&&(e[r]=n);};for(let[r,n,s]of To)Lo(t,r)&&o(n,s);return Io(t,o),Oo(t,o),e}function qe(...t){try{return execFileSync("git",t,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}}var ht={"github.com":"github","gitlab.com":"gitlab","bitbucket.org":"bitbucket"};function jo(t){if(!t)return [null,null];let e=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(t);if(e){let r=(e[1]??"").toLowerCase();return [e[2]??t,ht[r]??"local"]}let o=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(t);if(o){let r=(o[1]??"").toLowerCase();return [o[2]??t,ht[r]??"local"]}return [t,"local"]}function Q(){let t=process.cwd(),e=qe("rev-parse","--abbrev-ref","HEAD"),o=qe("rev-parse","HEAD"),r=qe("remote","get-url","origin"),[n,s]=jo(r),a;return n?a=n.replace(/\/+$/,"").split("/").pop()??null:a=basename(t)||null,e==="HEAD"&&(e=null),{project:a,repository:n,branch:e,commit_hash:o,folder:t,repository_platform:s}}var kt=dirname(fileURLToPath(import.meta.url)),Fo=[join(kt,"../assets/SKILL.md"),join(kt,"../../assets/SKILL.md")];function vt(){for(let t of Fo)try{return readFileSync(t,"utf8")}catch{}return "devcoach: coaching instructions unavailable (SKILL.md not found in package)."}var wt={version:"0.3.60"};var le=wt.version;var I=t=>({type:"text",text:t}),_t=t=>({content:[I(JSON.stringify(t))]}),L=t=>({isError:true,content:[I(t)]}),We=t=>({content:[I(JSON.stringify(t))],structuredContent:t}),Wo={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:me,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:Re.nullable()},Xo={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())}))},Yo={max_per_day:z$1.number().int(),min_gap_minutes:z$1.number().int(),ui_theme:Ie};function _(t,e){return {contents:[{uri:t.href,mimeType:"application/json",text:JSON.stringify(e)}]}}function Jo(){let t=new McpServer({name:"devcoach",version:le},{instructions:"Progressive technical coaching server. Use the devcoach_instructions prompt for full coaching behaviour guidelines."});t.registerTool("log_lesson",{title:"Log Lesson",description:"Save a delivered lesson to the coaching log. Git metadata (project, repository, branch, commit_hash, folder, repository_platform) is auto-detected from the workspace when omitted. Returns the saved Lesson with all resolved fields.",inputSchema:{id:z$1.string().describe("Unique lesson id (uuid or random string)"),timestamp:z$1.string().describe("ISO 8601 timestamp; normalized to UTC and clamped to now"),topic_id:z$1.string().describe("Primary topic id, e.g. 'python'"),categories:z$1.array(z$1.string()).describe("Category tags, e.g. ['python','performance']"),title:z$1.string().describe("Short lesson title"),level:me.describe("Difficulty level: junior | mid | senior"),summary:z$1.string().describe("1-3 sentence summary shown in the lesson card"),body:z$1.string().nullish().describe("Optional full lesson markdown"),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:Wo,annotations:{title:"Log Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=Q(),n={};try{n=m(a=>He(a));}catch{n={};}let s=re({id:o.id,timestamp:o.timestamp,topic_id:o.topic_id,categories:o.categories,title:o.title,level:o.level,summary:o.summary,body:o.body??null,task_context:o.task_context??null,project:o.project??r.project??n.project??null,repository:o.repository??r.repository??n.repository??null,branch:o.branch??r.branch??n.branch??null,commit_hash:o.commit_hash??r.commit_hash??null,folder:o.folder??r.folder??null,repository_platform:o.repository_platform??r.repository_platform??n.repository_platform??null});m(a=>pt(a,s));try{if(t.server.getClientCapabilities()?.elicitation){let l=await t.server.elicitInput({message:"Did that land?",requestedSchema:{type:"object",properties:{feedback:{type:"string",title:"Did that land?",description:"know = got it \xB7 dont_know = need to revisit \xB7 skip = no feedback",enum:["know","dont_know","skip"]}},required:[]}}),d=l.action==="accept"?l.content?.feedback:void 0;d&&d!=="skip"&&(m(f=>{ne(f,o.id,d),q(f,o.topic_id,d==="know"?1:-1);}),s={...s,feedback:d});}}catch{}return We(s)}catch(r){return L(`log_lesson failed: ${r}`)}}),t.registerTool("update_knowledge",{title:"Update Knowledge",description:"Adjust the confidence score for a topic by delta (e.g. +1 or -1). Returns the new confidence (0-10). Creates the topic at confidence 5 if it does not exist.",inputSchema:{topic:z$1.string().describe("Topic id, e.g. 'python'"),delta:z$1.number().int().describe("Signed change to apply, e.g. +1 or -1")},annotations:{title:"Update Knowledge",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>q(n,o.topic,o.delta));return {content:[I(String(r))]}}catch(r){return L(`update_knowledge failed for '${o.topic}': ${r}`)}}),t.registerTool("get_lessons",{title:"Get Lessons",description:"Query the coaching lesson history. All filters combine. period defaults to all; date_from/date_to override period; limit caps results newest-first (0 = all).",inputSchema:{period:z$1.enum(["today","week","month","year","all"]).nullish().describe("Time window"),category:z$1.string().nullish().describe("Filter by a category tag, e.g. 'python'"),level:z$1.enum(["junior","mid","senior"]).nullish().describe("Filter by difficulty level"),project:z$1.string().nullish().describe("Fuzzy match on git project"),repository:z$1.string().nullish().describe("Fuzzy match on git repository"),branch:z$1.string().nullish().describe("Fuzzy match on git branch"),commit:z$1.string().nullish().describe("Fuzzy match on commit hash"),starred:z$1.boolean().nullish().describe("True to return only starred lessons"),feedback:z$1.enum(["know","dont_know","none"]).nullish().describe("Filter by feedback ('none' = no response)"),search:z$1.string().nullish().describe("Full-text search over title, topic_id, summary, body"),date_from:z$1.string().nullish().describe("ISO date/datetime lower bound"),date_to:z$1.string().nullish().describe("ISO date/datetime upper bound (date-only = end-of-day)"),limit:z$1.number().int().default(10).describe("Max lessons, newest first. Pass 0 for all.")},annotations:{title:"Get Lessons",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}},o=>{try{let r=m(n=>B(n,{period:o.period,category:o.category,level:o.level,project:o.project,repository:o.repository,branch:o.branch,commit:o.commit,starred:o.starred,feedback:o.feedback,search:o.search,date_from:o.date_from,date_to:o.date_to,page:o.limit>0?1:null,per_page:o.limit}));return _t(r)}catch{return _t([])}}),t.registerTool("star_lesson",{title:"Star Lesson",description:"Set the starred (favourite) flag on a lesson. Returns true if found and updated. Idempotent.",inputSchema:{lesson_id:z$1.string().describe("Lesson id"),starred:z$1.boolean().describe("true to favourite, false to unmark")},annotations:{title:"Star Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>Y(n,o.lesson_id,o.starred));return {content:[I(String(r))]}}catch(r){return L(`star_lesson failed for '${o.lesson_id}': ${r}`)}}),t.registerTool("delete_lesson",{title:"Delete Lesson",description:"Permanently delete a lesson by id. Returns true if found and deleted.",inputSchema:{lesson_id:z$1.string().describe("Lesson id to delete")},annotations:{title:"Delete Lesson",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>he(n,o.lesson_id));return {content:[I(String(r))]}}catch(r){return L(`delete_lesson failed for '${o.lesson_id}': ${r}`)}}),t.registerTool("submit_feedback",{title:"Submit Feedback",description:"Record comprehension feedback for a lesson and adjust knowledge confidence. know = +1, dont_know = -1, clear = remove feedback (no confidence change). Idempotent \u2014 the same feedback twice adjusts confidence only once.",inputSchema:{lesson_id:z$1.string().describe("Lesson id"),feedback:z$1.enum(["know","dont_know","clear"]).describe("know | dont_know | clear")},annotations:{title:"Submit Feedback",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=o.feedback==="clear"?null:o.feedback,n=m(s=>{let a=s.prepare("SELECT feedback, topic_id FROM lessons WHERE id = ?").get(o.lesson_id);return a?(a.feedback===r||(ne(s,o.lesson_id,r),(r==="know"||r==="dont_know")&&a.topic_id&&q(s,a.topic_id,r==="know"?1:-1)),!0):!1});return {content:[I(String(n))]}}catch(r){return L(`submit_feedback failed for '${o.lesson_id}': ${r}`)}}),t.registerTool("add_topic",{title:"Add Topic",description:"Add a topic to the knowledge map, or update its confidence if it exists. Optionally assign to a group (auto-created). Prefer a single-word topic id. Idempotent.",inputSchema:{topic:z$1.string().describe("Topic id \u2014 prefer a single word, max 3 words"),confidence:Le.default(5).describe("Initial confidence 0-10 (default 5)"),group:z$1.string().nullish().describe("Optional group name; 'Other' if omitted")},annotations:{title:"Add Topic",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{return m(r=>{O(r,o.topic,o.confidence),o.group&&o.group!=="Other"&&N(r,o.topic,o.group);}),{content:[I("true")]}}catch(r){return L(`add_topic failed for '${o.topic}': ${r}`)}}),t.registerTool("remove_topic",{title:"Remove Topic",description:"Remove a topic from the knowledge map entirely. Returns true if it existed.",inputSchema:{topic:z$1.string().describe("Topic id to remove")},annotations:{title:"Remove Topic",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>J(n,o.topic));return {content:[I(String(r))]}}catch(r){return L(`remove_topic failed for '${o.topic}': ${r}`)}}),t.registerTool("add_group",{title:"Add Group",description:"Create a new (initially empty) knowledge group. Idempotent \u2014 returns true either way.",inputSchema:{name:z$1.string().describe("Group name, e.g. 'Machine Learning'")},annotations:{title:"Add Group",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async o=>{try{return m(r=>P(r,o.name.trim())),{content:[I("true")]}}catch(r){return L(`add_group failed for '${o.name}': ${r}`)}}),t.registerTool("remove_group",{title:"Remove Group",description:"Delete a knowledge group. Its topics move to Other. Returns true if it existed.",inputSchema:{name:z$1.string().describe("Group name to delete")},annotations:{title:"Remove Group",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async o=>{try{let r=m(n=>V(n,o.name));return {content:[I(String(r))]}}catch(r){return L(`remove_group failed for '${o.name}': ${r}`)}}),t.registerTool("update_settings",{title:"Update Settings",description:"Update a coaching setting. max_per_day: integer 1-20. min_gap_minutes: integer 0-1440 (0 = no cooldown). Returns the full updated Settings.",inputSchema:{key:z$1.enum(["max_per_day","min_gap_minutes"]).describe("Setting key"),value:z$1.string().describe("New value as an integer string")},outputSchema:Yo,annotations:{title:"Update Settings",destructiveHint:false,idempotentHint:true,openWorldHint:false}},o=>{let r=Number.parseInt(o.value,10);if(Number.isNaN(r))return L(`Value must be an integer, got '${o.value}'`);if(o.key==="max_per_day"&&!(r>=1&&r<=20))return L("max_per_day must be between 1 and 20");if(o.key==="min_gap_minutes"&&!(r>=0&&r<=1440))return L("min_gap_minutes must be between 0 and 1440");let n=m(s=>(R(s,o.key,String(r)),E(s)));return We(n)}),t.registerTool("open_ui",{title:"Open UI",description:"Launch the devcoach web dashboard in the background. port must be 1024-65535.",inputSchema:{port:z$1.number().int().default(7860).describe("Port (default 7860)")},annotations:{title:"Open UI",destructiveHint:false,idempotentHint:false,openWorldHint:true}},o=>{let r=o.port;return r>=1024&&r<=65535?(spawn(process.execPath,[process.argv[1]??"","ui","--port",String(r)],{detached:true,stdio:"ignore"}).unref(),{content:[I(`devcoach UI starting at http://localhost:${r}`)]}):{content:[I(`error: port ${r} is out of valid range (1024-65535)`)]}}),t.registerTool("complete_onboarding",{title:"Complete Onboarding",description:"Save the user's initial knowledge profile and mark onboarding complete. Wipes any default-seeded profile. topics: {topic_id: confidence_0_to_10}. groups: {group_name: [topic_id,...]} (topics not in any group go to 'Other'). Returns the updated Profile.",inputSchema:{topics:z$1.record(z$1.string(),Le).describe("{topic_id: confidence 0-10}"),groups:z$1.record(z$1.string(),z$1.array(z$1.string())).optional().describe("{group_name: [topic_id, ...]}")},outputSchema:Xo,annotations:{title:"Complete Onboarding",destructiveHint:true,idempotentHint:false,openWorldHint:false}},async o=>{try{let r=m(n=>{n.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[s,a]of Object.entries(o.topics))O(n,s,Math.max(0,Math.min(10,a)));if(o.groups)for(let[s,a]of Object.entries(o.groups))for(let l of a)l in o.topics&&N(n,l,s);return M(n)});return We(r)}catch(r){return L(`complete_onboarding failed: ${r}`)}});let e=(o,r)=>({title:o,description:r,mimeType:"application/json"});return t.registerResource("profile","devcoach://profile",e("Knowledge Profile","Current knowledge map \u2014 topics, confidence scores, and groups."),o=>{try{return _(o,m(r=>M(r)))}catch(r){return _(o,{error:String(r)})}}),t.registerResource("settings","devcoach://settings",e("Settings","Current coaching settings (rate limits, UI theme)."),o=>{try{return _(o,m(r=>E(r)))}catch(r){return _(o,{error:String(r)})}}),t.registerResource("recent-lessons","devcoach://lessons/recent",e("Recent Lessons","Last 10 lessons from the current week."),o=>{try{return _(o,m(r=>B(r,{period:"week",page:1,per_page:10})))}catch(r){return _(o,[{error:String(r)}])}}),t.registerResource("stats","devcoach://stats",e("Stats","Aggregate coaching statistics: counts and weakest/strongest topics."),o=>{try{return _(o,m(r=>Z(r)))}catch(r){return _(o,{error:String(r)})}}),t.registerResource("taught-topics","devcoach://taught-topics",e("Taught Topics","All topic_ids already taught \u2014 read before selecting a new lesson topic."),o=>{try{return _(o,m(r=>bt(r)))}catch{return _(o,[])}}),t.registerResource("rate-limit","devcoach://rate-limit",e("Rate Limit","Current rate-limit status and running lesson total."),o=>{try{let{r,total:n}=m(a=>({r:z(a),total:X(a)})),s={allowed:r.allowed,total_lessons:n};return r.reason!=null&&(s.reason=r.reason),_(o,s)}catch(r){return _(o,{allowed:false,reason:`Rate limit check unavailable: ${r}`})}}),t.registerResource("context","devcoach://context",e("Workspace Context","Auto-detected git context and most-used lesson metadata defaults."),o=>{try{let r=Q(),n=m(s=>He(s));return _(o,{git:r,usage_defaults:n})}catch(r){return _(o,{error:String(r)})}}),t.registerResource("onboarding","devcoach://onboarding",e("Onboarding","Onboarding status, auto-detected stack, and project topic defaults."),o=>{try{let n=m(d=>de(d)).knowledge_ready,s=existsSync(w)&&statSync(w).size>0,a=Q(),l=_e(a.folder??process.cwd());return _(o,{knowledge_ready:n,notebook_ready:s,needs_onboarding:!(n&&s),detected_stack:l,default_topics:at,context_ready:a.branch!==null})}catch(r){return _(o,{error:String(r)})}}),t.registerResource("lesson",new ResourceTemplate("devcoach://lessons/{lesson_id}",{list:void 0}),e("Lesson","A single lesson by id."),(o,r)=>{try{let n=String(r.lesson_id),s=m(a=>K(a,n));return s?_(o,s):_(o,{error:`Lesson '${n}' not found`})}catch(n){return _(o,{error:String(n)})}}),t.registerPrompt("devcoach_instructions",{title:"devcoach coaching instructions",description:"Full coaching instructions for the devcoach skill (content of SKILL.md)."},()=>({messages:[{role:"user",content:{type:"text",text:vt()}}]})),t}async function $t(){let t=Jo(),e=new StdioServerTransport;await t.connect(e);}function ee(t){return JSON.stringify(t??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function $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>
|
|
50
50
|
<html lang="en">
|
|
51
51
|
<head>
|
|
52
52
|
<meta charset="UTF-8" />
|
|
@@ -103,7 +103,7 @@ import {spawn,spawnSync,execFileSync}from'child_process';import {existsSync,stat
|
|
|
103
103
|
</script>
|
|
104
104
|
${t.scripts??""}
|
|
105
105
|
</body>
|
|
106
|
-
</html>`}function
|
|
106
|
+
</html>`}function Ct(t){let e=r=>t.allGroups.filter(n=>n!=="Other").map(n=>html`<option value="${n}" ${n===r?"selected":""}>${n}</option>`),o=html`
|
|
107
107
|
<div x-data="{ editMode: JSON.parse(localStorage.getItem('km-edit-mode') || 'false'), toggle() { this.editMode = !this.editMode; localStorage.setItem('km-edit-mode', this.editMode); } }">
|
|
108
108
|
|
|
109
109
|
<div class="flex items-center justify-between mb-6">
|
|
@@ -222,10 +222,10 @@ ${Object.entries(t.categorised).map(([r,n])=>html`
|
|
|
222
222
|
</div>`})}
|
|
223
223
|
</div>
|
|
224
224
|
</section>`)}
|
|
225
|
-
</div>`;return
|
|
226
|
-
<a href="${
|
|
227
|
-
${
|
|
228
|
-
</a></th>`},f=html`<svg class="w-3 h-3 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"/></svg>`,
|
|
225
|
+
</div>`;return $e({title:"Profile \u2014 devcoach",currentPath:"/",uiTheme:t.uiTheme,body:o})}var St={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},Tt={know:"\u2713 Known",dont_know:"\u2717 Don't know",none:"\u2014 No response"},Et={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 Rt(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}`:St[e.period]??"All time",s=e.feedback?Tt[e.feedback]??"Feedback":"Feedback",a={junior:"text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-900/20",mid:"text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20",senior:"text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-900/20"},l=(g,h)=>html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-indigo-50 dark:bg-indigo-900/20 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800">${g}<a href="${h}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-indigo-200 dark:hover:bg-indigo-700 transition">×</a></span>`,d=(g,h,T="")=>{let D=e.sort===h,ue=D&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap ${T}">
|
|
226
|
+
<a href="${$(e,{sort:h,order:ue})}" class="inline-flex items-center gap-1 hover:text-gray-700 dark:hover:text-gray-200 transition group/sort">${g}
|
|
227
|
+
${D?html`<span class="text-indigo-400">${e.order==="asc"?"\u2191":"\u2193"}</span>`:html`<span class="text-gray-200 dark:text-gray-700 group-hover/sort:text-gray-400 dark:group-hover/sort:text-gray-500 transition">↕</span>`}
|
|
228
|
+
</a></th>`},f=html`<svg class="w-3 h-3 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"/></svg>`,b=html`<link rel="stylesheet" href="/static/vendor/flatpickr.min.css" />
|
|
229
229
|
<style>
|
|
230
230
|
.dark .flatpickr-calendar { background:#1f2937; border-color:#374151; }
|
|
231
231
|
.dark .flatpickr-day { color:#e5e7eb; }
|
|
@@ -236,7 +236,7 @@ ${Object.entries(t.categorised).map(([r,n])=>html`
|
|
|
236
236
|
.dark .flatpickr-current-month, .dark .flatpickr-monthDropdown-months { color:#e5e7eb; background:#1f2937; }
|
|
237
237
|
.dark .flatpickr-weekday { color:#9ca3af; background:#1f2937; }
|
|
238
238
|
.dark .flatpickr-prev-month svg, .dark .flatpickr-next-month svg { fill:#9ca3af; }
|
|
239
|
-
</style>`,
|
|
239
|
+
</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`
|
|
240
240
|
<form id="filter-form" method="get" action="/lessons">
|
|
241
241
|
<input type="hidden" name="period" id="h-period" value="${e.period}">
|
|
242
242
|
<input type="hidden" name="date_from" id="h-date-from" value="${e.date_from}">
|
|
@@ -256,7 +256,7 @@ ${Object.entries(t.categorised).map(([r,n])=>html`
|
|
|
256
256
|
<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" />
|
|
257
257
|
${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>`:""}
|
|
258
258
|
</div>
|
|
259
|
-
<p class="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap shrink-0">${
|
|
259
|
+
<p class="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap shrink-0">${y}</p>
|
|
260
260
|
</div>
|
|
261
261
|
|
|
262
262
|
<div class="flex flex-wrap items-center gap-2 mb-3">
|
|
@@ -269,7 +269,7 @@ ${Object.entries(t.categorised).map(([r,n])=>html`
|
|
|
269
269
|
</button>
|
|
270
270
|
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95" @click.outside="close()" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-64 overflow-hidden" style="display:none">
|
|
271
271
|
<div class="p-1">
|
|
272
|
-
${Object.entries(
|
|
272
|
+
${Object.entries(St).map(([g,h])=>{let T=e.period===g&&!o;return html`<button type="button" @click="selectPreset('${g}', '${h}')" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${T?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${h}${T?html`<span class="text-indigo-500">✓</span>`:""}</button>`})}
|
|
273
273
|
</div>
|
|
274
274
|
<div class="border-t border-gray-100 dark:border-gray-800 p-1">
|
|
275
275
|
<button type="button" @click="showCustom = !showCustom" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${o?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">
|
|
@@ -291,7 +291,7 @@ ${Object.entries(t.categorised).map(([r,n])=>html`
|
|
|
291
291
|
</div>
|
|
292
292
|
|
|
293
293
|
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
|
|
294
|
-
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.level?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${e.level?`${
|
|
294
|
+
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.level?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${e.level?`${Et[e.level]??""} ${e.level}`:"Level"}${f}</button>
|
|
295
295
|
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-40 p-1 overflow-hidden" style="display:none">
|
|
296
296
|
${[["","All levels"],["junior","\u{1F7E2} Junior"],["mid","\u{1F7E1} Mid"],["senior","\u{1F534} Senior"]].map(([g,h])=>html`<button type="button" onclick="document.getElementById('h-level').value='${g}'; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${e.level===g?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${h}${e.level===g?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
|
|
297
297
|
</div>
|
|
@@ -313,17 +313,17 @@ ${Object.entries(t.categorised).map(([r,n])=>html`
|
|
|
313
313
|
</div>
|
|
314
314
|
|
|
315
315
|
${r?html`<div class="flex flex-wrap gap-1.5 mb-4">
|
|
316
|
-
${o?l(`\u{1F4C5} ${n}
|
|
317
|
-
${e.period!=="all"&&!o?l(`\u{1F550} ${n}
|
|
318
|
-
${e.feedback?l(
|
|
319
|
-
${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="${
|
|
320
|
-
${e.search?l(`\u{1F50D} "${e.search}"
|
|
321
|
-
${e.category?l(e.category
|
|
322
|
-
${e.level?l(`${
|
|
323
|
-
${e.project?l(`\u{1F4C1} ${e.project}
|
|
324
|
-
${e.repository?l(`\u2387 ${e.repository}
|
|
325
|
-
${e.branch?l(`\u2387 ${e.branch}
|
|
326
|
-
${e.commit?l(e.commit.slice(0,7)
|
|
316
|
+
${o?l(`\u{1F4C5} ${n}`,$(e,{date_from:"",date_to:""})):""}
|
|
317
|
+
${e.period!=="all"&&!o?l(`\u{1F550} ${n}`,$(e,{period:"all"})):""}
|
|
318
|
+
${e.feedback?l(Tt[e.feedback]??e.feedback,$(e,{feedback:""})):""}
|
|
319
|
+
${e.starred?html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300 border border-yellow-200 dark:border-yellow-800">★ Starred<a href="${$({...e,starred:false})}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-yellow-200 dark:hover:bg-yellow-700 transition">×</a></span>`:""}
|
|
320
|
+
${e.search?l(`\u{1F50D} "${e.search}"`,$(e,{search:""})):""}
|
|
321
|
+
${e.category?l(e.category,$(e,{category:""})):""}
|
|
322
|
+
${e.level?l(`${Et[e.level]??""} ${e.level}`,$(e,{level:""})):""}
|
|
323
|
+
${e.project?l(`\u{1F4C1} ${e.project}`,$(e,{project:""})):""}
|
|
324
|
+
${e.repository?l(`\u2387 ${e.repository}`,$(e,{repository:""})):""}
|
|
325
|
+
${e.branch?l(`\u2387 ${e.branch}`,$(e,{branch:""})):""}
|
|
326
|
+
${e.commit?l(e.commit.slice(0,7),$(e,{commit:""})):""}
|
|
327
327
|
</div>`:""}
|
|
328
328
|
</form>
|
|
329
329
|
|
|
@@ -341,22 +341,22 @@ ${t.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
|
|
|
341
341
|
</tr>
|
|
342
342
|
</thead>
|
|
343
343
|
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
|
|
344
|
-
${t.lessons.map(g=>{let h=g.timestamp.slice(0,10),
|
|
344
|
+
${t.lessons.map(g=>{let h=g.timestamp.slice(0,10),T=g.timestamp.slice(0,16).replace("T"," ");return html`<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors group cursor-pointer" tabindex="0" onclick="window.location='/lessons/${encodeURIComponent(g.id)}'" onkeydown="if(event.key==='Enter')window.location='/lessons/${encodeURIComponent(g.id)}'" role="link">
|
|
345
345
|
<td class="px-3 py-3" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">
|
|
346
346
|
<form method="post" action="/lessons/${encodeURIComponent(g.id)}/star">
|
|
347
347
|
<input type="hidden" name="starred" value="${g.starred?"0":"1"}" />
|
|
348
|
-
<input type="hidden" name="next" value="/lessons${
|
|
348
|
+
<input type="hidden" name="next" value="/lessons${$(e)}" />
|
|
349
349
|
<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>
|
|
350
350
|
</form>
|
|
351
351
|
</td>
|
|
352
352
|
<td class="px-3 py-3 whitespace-nowrap tabular-nums relative group/date">
|
|
353
353
|
<span class="text-gray-400 dark:text-gray-500 cursor-default" data-ts="${g.timestamp}">${h}</span>
|
|
354
|
-
<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">${
|
|
354
|
+
<div class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${T}</div>
|
|
355
355
|
</td>
|
|
356
356
|
<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>
|
|
357
357
|
<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>
|
|
358
|
-
<td class="px-3 py-3" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><a href="${
|
|
359
|
-
<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(
|
|
358
|
+
<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>
|
|
359
|
+
<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(D=>html`<a href="${$(e,{category:D})}" 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">${D}</a>`)}</div></td>
|
|
360
360
|
<td class="px-3 py-3 hidden xl:table-cell" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">${g.feedback==="know"?html`<span class="text-xs text-emerald-600 dark:text-emerald-400 font-medium">✓ Known</span>`:g.feedback==="dont_know"?html`<span class="text-xs text-rose-500 dark:text-rose-400 font-medium">✗ Unknown</span>`:""}</td>
|
|
361
361
|
</tr>`})}
|
|
362
362
|
</tbody>
|
|
@@ -365,22 +365,22 @@ ${t.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
|
|
|
365
365
|
${t.totalPages>1?html`<div class="flex items-center justify-between mt-4">
|
|
366
366
|
<p class="text-xs text-gray-400 dark:text-gray-500">Page ${t.page} of ${t.totalPages}</p>
|
|
367
367
|
<div class="flex items-center gap-1">
|
|
368
|
-
${t.page>1?html`<a href="${
|
|
369
|
-
${
|
|
370
|
-
${t.page<t.totalPages?html`<a href="${
|
|
368
|
+
${t.page>1?html`<a href="${$(e,{page:String(t.page-1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">← Prev</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">← Prev</span>`}
|
|
369
|
+
${Vo(t.page,t.totalPages).map(g=>g===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:g===t.page?html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-indigo-600 text-white border border-indigo-600">${g}</span>`:html`<a href="${$(e,{page:String(g)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">${g}</a>`)}
|
|
370
|
+
${t.page<t.totalPages?html`<a href="${$(e,{page:String(t.page+1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">Next →</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">Next →</span>`}
|
|
371
371
|
</div>
|
|
372
372
|
</div>`:""}`:html`<div class="flex flex-col items-center justify-center py-16 text-center">
|
|
373
373
|
<p class="text-3xl mb-3">📭</p>
|
|
374
374
|
<p class="text-gray-500 dark:text-gray-400 text-sm">No lessons match the current filters.</p>
|
|
375
375
|
${r?html`<a href="/lessons" class="mt-2 text-indigo-500 hover:text-indigo-400 text-sm transition">Clear all filters</a>`:""}
|
|
376
|
-
</div>`}`,
|
|
376
|
+
</div>`}`,v=html`<script src="/static/vendor/flatpickr.min.js"></script>
|
|
377
377
|
<script src="/static/relative-time.js"></script>
|
|
378
378
|
<script>
|
|
379
379
|
function periodPicker() {
|
|
380
380
|
return {
|
|
381
381
|
open: false,
|
|
382
382
|
showCustom: ${raw(o?"true":"false")},
|
|
383
|
-
label: ${raw(
|
|
383
|
+
label: ${raw(ee(n))},
|
|
384
384
|
fp: null,
|
|
385
385
|
init() {
|
|
386
386
|
const self = this;
|
|
@@ -388,7 +388,7 @@ function periodPicker() {
|
|
|
388
388
|
if (val && !this.fp) {
|
|
389
389
|
this.fp = flatpickr(this.$refs.fp, {
|
|
390
390
|
mode: 'range', dateFormat: 'Y-m-d', inline: false,
|
|
391
|
-
${raw(e.date_from?`defaultDate: [${
|
|
391
|
+
${raw(e.date_from?`defaultDate: [${ee(e.date_from)}${e.date_to?`, ${ee(e.date_to)}`:""}],`:"")}
|
|
392
392
|
onChange(dates) {
|
|
393
393
|
if (dates.length === 2) {
|
|
394
394
|
document.getElementById('h-date-from').value = self.fp.formatDate(dates[0], 'Y-m-d');
|
|
@@ -411,7 +411,7 @@ function periodPicker() {
|
|
|
411
411
|
},
|
|
412
412
|
};
|
|
413
413
|
}
|
|
414
|
-
</script>`;return
|
|
414
|
+
</script>`;return $e({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:b,body:k,scripts:v})}function Vo(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 Xe={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function Lt(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&&Xe[e.repository_platform]&&e.repository&&(n=`https://${Xe[e.repository_platform]}/${e.repository}`);let s=null;n&&e.commit_hash&&!r&&(s=e.repository_platform==="gitlab"?`${n}/-/commit/${e.commit_hash}`:e.repository_platform==="bitbucket"?`${n}/commits/${e.commit_hash}`:`${n}/commit/${e.commit_hash}`);let a=!!(e.project||e.repository||e.branch||e.commit_hash||e.folder),l=e.timestamp.slice(0,10),d=e.timestamp.slice(0,16).replace("T"," "),f=html`
|
|
415
415
|
<div class="mb-4"><a href="/lessons" class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white text-sm transition">← Back to lessons</a></div>
|
|
416
416
|
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
|
|
417
417
|
<div class="flex flex-wrap items-center gap-3 mb-2">
|
|
@@ -428,7 +428,7 @@ function periodPicker() {
|
|
|
428
428
|
<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>
|
|
429
429
|
</span>
|
|
430
430
|
<span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.topic_id}</span></span>
|
|
431
|
-
${e.categories.map(
|
|
431
|
+
${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>`)}
|
|
432
432
|
${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>`}
|
|
433
433
|
<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>`:""}
|
|
434
434
|
</div>
|
|
@@ -440,7 +440,7 @@ function periodPicker() {
|
|
|
440
440
|
${e.task_context?html`<div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 text-sm text-gray-500 dark:text-gray-400"><span class="text-gray-400 dark:text-gray-500">Context:</span> ${e.task_context}</div>`:""}
|
|
441
441
|
${a?html`<div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800"><div class="flex flex-wrap gap-x-5 gap-y-1.5 text-xs text-gray-400 dark:text-gray-500 font-mono">
|
|
442
442
|
${e.project?html`<span><span class="text-gray-400 dark:text-gray-600">project</span> ${n?html`<a href="${n}" ${r?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.project}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.project}</span>`}</span>`:""}
|
|
443
|
-
${e.repository?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">repo</span>${e.repository_platform&&
|
|
443
|
+
${e.repository?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">repo</span>${e.repository_platform&&Xe[e.repository_platform]?html`<img src="/static/vendor/icons/${e.repository_platform}.svg" class="w-3 h-3 dark:invert opacity-60 shrink-0" alt="" />`:""}${n?html`<a href="${n}" ${r?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.repository}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.repository}</span>`}</span>`:""}
|
|
444
444
|
${e.branch?html`<span><span class="text-gray-400 dark:text-gray-600">branch</span> <span class="text-indigo-600 dark:text-indigo-400">${e.branch}</span></span>`:""}
|
|
445
445
|
${e.commit_hash?html`<span><span class="text-gray-400 dark:text-gray-600">commit</span> ${s?html`<a href="${s}" target="_blank" rel="noopener" class="text-cyan-600 dark:text-cyan-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.commit_hash.slice(0,7)}</a>`:html`<span class="text-cyan-600 dark:text-cyan-400">${e.commit_hash.slice(0,7)}</span>`}</span>`:""}
|
|
446
446
|
${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>`:""}
|
|
@@ -450,7 +450,7 @@ function periodPicker() {
|
|
|
450
450
|
<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>
|
|
451
451
|
</div>`}
|
|
452
452
|
<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>
|
|
453
|
-
</div>`,
|
|
453
|
+
</div>`,b=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,y=html`<script src="/static/vendor/highlight.min.js"></script>
|
|
454
454
|
<script src="/static/vendor/marked.min.js"></script>
|
|
455
455
|
<script src="/static/relative-time.js"></script>
|
|
456
456
|
<script>
|
|
@@ -462,9 +462,9 @@ function periodPicker() {
|
|
|
462
462
|
},
|
|
463
463
|
breaks: true, gfm: true,
|
|
464
464
|
});
|
|
465
|
-
document.getElementById('summary-content').innerHTML = marked.parse(${raw(
|
|
466
|
-
document.getElementById('body-content').innerHTML = marked.parse(${raw(
|
|
467
|
-
</script>`;return
|
|
465
|
+
document.getElementById('summary-content').innerHTML = marked.parse(${raw(ee(e.summary))});
|
|
466
|
+
document.getElementById('body-content').innerHTML = marked.parse(${raw(ee(e.body))});
|
|
467
|
+
</script>`;return $e({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:t.uiTheme,head:b,body:f,scripts:y})}var Zo=[[0,"No cooldown"],[15,"15 minutes"],[30,"30 minutes"],[60,"1 hour"],[90,"1 hour 30 min"],[120,"2 hours"],[180,"3 hours"],[240,"4 hours"],[360,"6 hours"],[480,"8 hours"],[720,"12 hours"],[1440,"24 hours"]];function It(t){let e=t.settings.min_gap_minutes,o=[["system","System","\u{1F313}"],["light","Light","\u2600\uFE0F"],["dark","Dark","\u{1F319}"]],r=t.flash,n=html`
|
|
468
468
|
<h1 class="text-2xl font-bold mb-6 text-indigo-600 dark:text-indigo-400">Settings</h1>
|
|
469
469
|
${r?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${r.invalid?"bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-700 text-yellow-800 dark:text-yellow-300":"bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-700 text-green-700 dark:text-green-300"}">
|
|
470
470
|
${r.imported} lesson${r.imported!==1?"s":""} imported.${r.skipped?` ${r.skipped} skipped (already in DB).`:""}${r.invalid?` ${r.invalid} rejected (failed validation).`:""}${r.groups?` ${r.groups} group${r.groups!==1?"s":""} added.`:""}${r.notebook?" Notebook restored.":""}
|
|
@@ -482,7 +482,7 @@ ${r?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${r.invalid?"bg-ye
|
|
|
482
482
|
<div>
|
|
483
483
|
<label for="min-gap-minutes" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Minimum gap between lessons</label>
|
|
484
484
|
<select id="min-gap-minutes" name="min_gap_minutes" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500">
|
|
485
|
-
${
|
|
485
|
+
${Zo.map(([a,l])=>html`<option value="${a}" ${e===a?"selected":""}>${l}</option>`)}
|
|
486
486
|
</select>
|
|
487
487
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Minimum gap between lessons.</p>
|
|
488
488
|
</div>
|
|
@@ -558,23 +558,23 @@ function updateLabel(inputId, labelId, submitId) {
|
|
|
558
558
|
document.getElementById(submitId).disabled = !input.files.length;
|
|
559
559
|
}
|
|
560
560
|
marked.setOptions({ breaks: true, gfm: true });
|
|
561
|
-
document.getElementById('notebook-preview-settings').innerHTML = marked.parse(${raw(
|
|
562
|
-
</script>`;return
|
|
563
|
-
`)}function
|
|
564
|
-
`),`${i.green("\u2713")} Installed into ${t}`)}function
|
|
565
|
-
`),`${i.green("\u2713")} Stop hooks installed into ${o}`}function
|
|
561
|
+
document.getElementById('notebook-preview-settings').innerHTML = marked.parse(${raw(ee(t.notebookContent))});
|
|
562
|
+
</script>`;return $e({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:t.uiTheme,body:n,scripts:s})}var Je=25;function Ve(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 Ze=dirname(fileURLToPath(import.meta.url)),Ot=[join(Ze,"../assets/static"),join(Ze,"../../assets/static")].find(t=>existsSync(t))??join(Ze,"../assets/static"),sr={".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 ir(){try{return m(t=>E(t).ui_theme)}catch{return "system"}}function ar(){let t=new Hono;return t.get("/static/*",e=>{let o=decodeURIComponent(e.req.path.slice(8)),r=join(Ot,o);if(!r.startsWith(Ot))return e.notFound();try{let n=readFileSync(r);return new Response(new Uint8Array(n),{headers:{"content-type":sr[extname(r)]??"application/octet-stream"}})}catch{return e.notFound()}}),t.get("/",e=>{let{profile:o,stats:r,rateLimit:n,settings:s}=m(d=>({profile:M(d),stats:Z(d),rateLimit:z(d),settings:E(d)})),a=new Map;for(let d of o.groups)for(let f of d.topics)a.set(f,d.name);let l={};for(let d of o.groups)l[d.name]=[];for(let d of o.knowledge){let f=a.get(d.topic)??"Other",b=l[f]??[];b.push(d),l[f]=b;}return e.html(Ct({categorised:l,allGroups:o.groups.map(d=>d.name),stats:r,rateLimit:n,maxPerDay:s.max_per_day,uiTheme:s.ui_theme}))}),t.post("/knowledge",async e=>{let o=await e.req.parseBody(),r=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"&&N(a,r,s);});}return e.redirect("/",303)}),t.post("/knowledge/:topic/delete",e=>(m(o=>J(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"?N(n,r,o):ke(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=>P(r,o)),e.redirect("/",303)}),t.post("/groups/:group_name/delete",e=>(m(o=>V(o,e.req.param("group_name"))),e.redirect("/",303))),t.get("/lessons/export",()=>{let e=m(o=>se(o));return new Response(JSON.stringify(e,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),t.post("/lessons/import",async e=>{let o=(await e.req.parseBody()).file,r;try{r=JSON.parse(o instanceof File?await o.text():String(o));}catch{return e.redirect("/settings?imported=0&skipped=0&invalid=1",303)}if(!Array.isArray(r))return e.redirect("/settings?imported=0&skipped=0&invalid=1",303);let n=m(s=>Me(s,r));return e.redirect(`/settings?imported=${n.inserted}&skipped=${n.duplicated}&invalid=${n.invalid}`,303)}),t.get("/lessons",e=>{let o=e.req.query(),r=new Set(["junior","mid","senior"]),n=o.level&&r.has(o.level)?o.level:"",s=o.date_from||"",a=o.date_to||"",l=o.starred==="1",d=o.sort||"timestamp",f=o.order==="asc"?"asc":"desc",b=Math.max(1,Number.parseInt(o.page??"1",10)||1),y=o.period||"all",k={period:s||a?null:y!=="all"?y:null,category:o.category||null,level:n||null,project:o.project||null,repository:o.repository||null,branch:o.branch||null,commit:o.commit||null,starred:l?true:null,search:o.search||null,feedback:o.feedback||null,date_from:s||null,date_to:a||null},v=m(h=>{let T=X(h,k),D=Math.max(1,Math.ceil(T/Je));return b=Math.min(b,D),{total:T,totalPages:D,lessons:B(h,{...k,sort:d,order:f,page:b,per_page:Je}),allCategories:ut(h),allProjects:ie(h,"project"),allRepositories:ie(h,"repository"),allBranches:ie(h,"branch"),allCommits:ie(h,"commit_hash"),theme:E(h).ui_theme}}),g={period:y,category:o.category||"",level:n,project:o.project||"",repository:o.repository||"",branch:o.branch||"",commit:o.commit||"",starred:l,search:o.search||"",feedback:o.feedback||"",date_from:s,date_to:a,sort:d,order:f};return e.html(Rt({lessons:v.lessons,allCategories:v.allCategories,allProjects:v.allProjects,allRepositories:v.allRepositories,allBranches:v.allBranches,allCommits:v.allCommits,s:g,page:b,perPage:Je,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=>Y(r,e.req.param("lesson_id"),o.starred==="1")),e.redirect(Ve(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=>we(s,e.req.param("lesson_id"),n)),e.redirect(Ve(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(Lt({lesson:o,uiTheme:ir()})):e.html("<h1>Lesson not found</h1>",404)}),t.get("/settings/export",()=>{let e=m(o=>ve(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=>ce(s,r));return e.redirect(`/settings?imported=${n.lessons}&skipped=${n.skipped}&invalid=${n.invalid}&groups=${n.groups}¬ebook=${n.learning_state}`,303)}),t.get("/settings/notebook/download",()=>{let e=existsSync(w)?readFileSync(w,"utf8"):"";return new Response(e,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),t.post("/settings/notebook",async e=>{let o=await e.req.parseBody();return mkdirSync(dirname(w),{recursive:true}),writeFileSync(w,C(o,"content"),"utf8"),e.redirect(Ve(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");return ["system","dark","light"].includes(s)||(s="system"),m(a=>{R(a,"max_per_day",String(r)),R(a,"min_gap_minutes",String(Number.isNaN(n)?240:n)),R(a,"ui_theme",s);}),e.redirect("/settings",303)}),t.get("/settings",e=>{let o=m(a=>E(a)),r=existsSync(w)?readFileSync(w,"utf8"):"",n=e.req.query(),s=n.imported!==void 0?{imported:Number(n.imported??0),skipped:Number(n.skipped??0),invalid:Number(n.invalid??0),groups:Number(n.groups??0),notebook:Number(n.notebook??0)}:null;return e.html(It({settings:o,notebookContent:r,notebookPath:w,uiTheme:o.ui_theme,flash:s}))}),t}function Nt(t){let e=ar();serve({fetch:e.fetch,port:t,hostname:"127.0.0.1"},o=>{console.log(`devcoach UI running at http://localhost:${o.port}`);});}var cr=!!process.stdout.isTTY&&!process.env.NO_COLOR,W=(t,e)=>cr?styleText(t,e):e,i={green:t=>W("green",t),red:t=>W("red",t),yellow:t=>W("yellow",t),cyan:t=>W("cyan",t),magenta:t=>W("magenta",t),dim:t=>W("dim",t),bold:t=>W("bold",t)};function et(t,e){return t==="green"?i.green(e):t==="yellow"?i.yellow(e):i.red(e)}var lr=/\[[0-9;]*m/g,pr=t=>t.replace(lr,""),Te=t=>[...pr(t)].length;function jt(t,e,o){let r=Math.max(0,e-Te(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 te(t,e,o,r=true){let n=e.map((f,b)=>Math.max(r?Te(f.header):0,...o.map(y=>Te(y[b]??"")))),s=(f,b,y)=>f+n.map(k=>"\u2500".repeat(k+2)).join(b)+y,a=f=>`\u2502${f.map((b,y)=>` ${jt(b??"",n[y]??0,e[y]?.justify??"left")} `).join("\u2502")}\u2502`,l=n.reduce((f,b)=>f+b+3,1),d=[];t&&d.push(jt(i.bold(t),l,"center")),d.push(s("\u256D","\u252C","\u256E")),r&&d.push(a(e.map(f=>i.bold(f.header))),s("\u251C","\u253C","\u2524"));for(let f of o)d.push(a(f));return d.push(s("\u2570","\u2534","\u256F")),d.join(`
|
|
563
|
+
`)}function Ee(t=""){if(!t)return "\u2500".repeat(60);let o=` ${t} `,r=Math.max(2,60-Te(o)),n=Math.floor(r/2);return "\u2500".repeat(n)+o+"\u2500".repeat(r-n)}var At=t=>{let e=Math.max(0,Math.min(10,Math.round(t)));return "\u2588".repeat(e)+"\u2591".repeat(10-e)},Pt=t=>t>=7?"green":t>=4?"yellow":"red";var u=(t="")=>{console.log(t);},Ht=t=>t==="junior"?i.green(t):t==="mid"?i.yellow(t):t==="senior"?i.red(t):t,hr=t=>t==="know"?` ${i.green("\u2713")}`:t==="dont_know"?` ${i.red("\u2717")}`:"",xr=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 Gt(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=Pt(n.confidence);return [n.topic,o.get(n.topic)??"Other",et(s,`${n.confidence}/10`),et(s,At(n.confidence))]});return te(e,[{header:"Topic"},{header:"Group"},{header:"Confidence",justify:"right"},{header:"Bar"}],r)}function kr(){let t=m(e=>M(e));u(Gt(t,"Knowledge Map"));}function vr(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+hr(s.feedback),Ht(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(te("Lessons",r,n));}function wr(t){let e=m(r=>K(r,t));e===null&&(u(i.red(`Lesson '${t}' not found.`)),process.exit(1)),u(Ee(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:")} ${Ht(e.level)}`);let o=e.starred?i.yellow("\u2605 starred"):i.dim("\u2606 not starred");if(u(`${i.dim("Star:")} ${o} ${i.dim("Feedback:")} ${xr(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(Ee()),u(e.summary);}function Ft(t,e){m(r=>K(r,t)===null?!1:(Y(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 _r(t){m(o=>he(o,t))||(u(i.red(`Lesson '${t}' not found.`)),process.exit(1)),u(`Lesson ${i.cyan(t)} deleted.`);}function $r(t,e){["know","dont_know","clear"].includes(e)||(u(i.red(`Invalid feedback '${e}'. Use: know | dont_know | clear`)),process.exit(1));let o=e==="clear"?null:e,r=m(a=>{let l=we(a,t,o);if(l===null)return null;let d=a.prepare("SELECT confidence FROM knowledge WHERE topic = ?").get(l);return {topicId:l,newConf:d?d.confidence:5}});r===null&&(u(i.red(`Lesson '${t}' not found.`)),process.exit(1));let n;if(o==="know"||o==="dont_know"){let a=r.newConf+(o==="know"?-1:1);n=`${i.cyan(r.topicId)} confidence: ${a} \u2192 ${i.bold(String(r.newConf))}`;}else n="feedback cleared";let s=o==="know"?i.green("\u2713 I know this"):o==="dont_know"?i.red("\u2717 I don't know this"):i.dim("cleared");u(`Lesson ${i.cyan(t)} \u2192 ${s} (${n})`);}function Sr(){let t=m(n=>E(n)),e=Math.floor(t.min_gap_minutes/60),o=t.min_gap_minutes%60,r=e?`${e}h ${o}m`:`${o}m`;u(te("Settings",[{header:"Key"},{header:"Value",justify:"right"}],[["max_per_day",String(t.max_per_day)],["min_gap_minutes",`${t.min_gap_minutes} (${r})`]]));}function Tr(){let{stats:t,rateLimit:e,settings:o}=m(l=>({stats:Z(l),rateLimit:z(l),settings:E(l)})),r=l=>Number(t[l]??0),n=e.allowed?i.green("Available now"):i.yellow(e.reason??"");u(te("Coaching Stats",[{header:"Metric"},{header:"Value",justify:"right"}],[["Total lessons",String(r("total_lessons"))],["Lessons today (24h)",`${r("lessons_today")} / ${o.max_per_day}`],["Lessons this week",String(r("lessons_this_week"))],["Next lesson",n]],false));let s=t.weakest_topics??[],a=t.strongest_topics??[];if(s.length||a.length){let l=[];for(let d=0;d<Math.max(s.length,a.length);d++){let f=s[d],b=a[d];l.push([f?`${i.red(f.topic)} ${i.dim(`(${f.confidence})`)}`:"",b?`${i.green(b.topic)} ${i.dim(`(${b.confidence})`)}`:""]);}u(te(void 0,[{header:"Weakest topics"},{header:"Strongest topics"}],l));}}function Er(t,e){["max_per_day","min_gap_minutes"].includes(t)||(u(i.red(`Unknown key '${t}'. Valid keys: max_per_day, min_gap_minutes`)),process.exit(1)),m(o=>R(o,t,e)),u(i.green(`Set ${t} = ${e}`));}function Cr(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"&&N(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 Rr(t){let e=m(o=>J(o,t));u(e?`${i.green("Removed")} ${i.bold(t)} from knowledge map`:i.yellow(`Topic '${t}' not found.`));}function Lr(t){let e=t.trim();(!e||e==="Other")&&(u(i.red("Invalid group name.")),process.exit(1)),m(o=>P(o,e)),u(`${i.green(`Group '${i.cyan(e)}' ready.`)} Assign topics with: devcoach group-assign <topic> "${e}"`);}function Ir(t){let e=m(o=>V(o,t));u(e?`${i.green(`Removed group '${i.cyan(t)}'`)}`:i.yellow(`Group '${t}' not found.`));}function Or(t,e){m(r=>r.prepare("SELECT topic FROM knowledge WHERE topic = ?").get(t)===void 0?!1:(e==="Other"?ke(r,t):N(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 Dr(t){let{lessonsCount:e,knowledgeCount:o,data:r}=m(s=>({lessonsCount:se(s).length,knowledgeCount:ae(s).length,data:ve(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 Nr(t){existsSync(t)||(u(i.red(`File not found: ${t}`)),process.exit(1));let e=m(r=>ce(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 jr(){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 Ar=join(homedir(),".claude","settings.json");function Bt(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 rt(){return Bt("devcoach")?{command:"devcoach",args:["mcp"]}:{command:"npx",args:["-y","devcoach","mcp"]}}function Pr(){return rt().command==="devcoach"?"devcoach":"npx -y devcoach"}function Mr(t,e){if(!Bt("claude"))return "";e&&spawnSync("claude",["mcp","remove","--scope",t,"devcoach"],{encoding:"utf8"});let o=rt(),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 Kt(t){if(!existsSync(t))return {ok:true,data:{}};try{return {ok:!0,data:JSON.parse(readFileSync(t,"utf8"))??{}}}catch{return {ok:false,error:`${i.red("\u2717")} ${t} is not valid JSON \u2014 fix it and re-run (left unchanged)`}}}function Ut(t,e,o){let r=Kt(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)}
|
|
564
|
+
`),`${i.green("\u2713")} Installed into ${t}`)}function Fr(t){let e=Pr(),o=Ar,r=Kt(o);if(!r.ok)return r.error;let n=r.data;n.hooks??={};let s=n.hooks;s.Stop??=[];let a=s.Stop,l=a.map((d,f)=>[d,f]).filter(([d])=>(d.hooks??[]).some(f=>(f.command??"").includes("devcoach"))).map(([,d])=>d);if(l.length){if(!t)return `${i.yellow("Stop hooks already installed")} in ${o} (use --force to overwrite)`;for(let d of l.toReversed())a.splice(d,1);}return a.push({hooks:[{type:"command",command:`${e} onboard-hook`}]},{hooks:[{type:"command",command:`${e} lesson-ready`}]}),mkdirSync(dirname(o),{recursive:true}),writeFileSync(o,`${JSON.stringify(n,null,2)}
|
|
565
|
+
`),`${i.green("\u2713")} Stop hooks installed into ${o}`}function Ur(t){let e=t.claudeCode||!t.claudeDesktop,o=t.claudeDesktop||!t.claudeCode,r=rt(),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=Mr("user",t.force);if(!s){let a=join(homedir(),".claude.json");s=Ut(a,{type:"stdio",env:{},...r},t.force),n=true;}u(` MCP server\u2026 ${s}`),t.skipHook||u(` Stop hooks\u2026 ${Fr(t.force)}`),u();}o&&(u(i.bold("Claude Desktop")),u(` MCP server\u2026 ${Ut(jr(),r,t.force)}`),n=true,u()),n&&u(`${i.yellow("\u2192")} Restart Claude Desktop to pick up the new server.
|
|
566
566
|
`),u(i.dim(`Tip: run devcoach backup to export your profile, lessons and settings.
|
|
567
567
|
run devcoach restore <file> to import a backup on a new machine.
|
|
568
|
-
The coaching skill is served automatically via the MCP prompt \u2014 always up to date.`));}async function
|
|
569
|
-
${i.bold("Step 1")} \u2014 Restore from backup`);let r=await e("Path to existing backup zip (Enter to skip)","");if(r){existsSync(r)||(u(i.red(`File not found: ${r}`)),process.exit(1));let b=m(y=>
|
|
570
|
-
${i.bold("Step 2")} \u2014 Build your knowledge profile`);let n=(await e("Mode: [a]utomatic (detect from files) / [m]anual (type your stack)","a")).toLowerCase(),s={};if(n.startsWith("a")){let y=
|
|
571
|
-
${i.dim(`Detected from ${i.cyan(y)}:`)}`);for(let
|
|
572
|
-
Add additional topics (comma-separated, or Enter to skip)`,"");for(let
|
|
573
|
-
Enter topics: 'topic_id confidence' (e.g. python 7). Blank line when done.`));;){let b=(await e("Topic (Enter when done)","")).trim();if(!b)break;let y=b.split(/\s+/),
|
|
574
|
-
${i.bold("Step 3")} \u2014 Organise into groups`),(await e("Organise topics into groups? [y/N]","n")).toLowerCase().startsWith("y"))){let y=[];for(let
|
|
575
|
-
${i.bold("Step 4")} \u2014 Rate-limit settings`);let l=await o("Max lessons per day",2,1,20),d=await o("Min gap between lessons (minutes)",240,0,1440),f=m(b=>{b.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[y,
|
|
576
|
-
${i.green("Setup complete!")} ${Object.keys(s).length} topics saved.`);}finally{t.close();}}var
|
|
577
|
-
`),process.stderr.write("devcoach: no coaching profile yet. Run onboarding now \u2014 ask the user how they want to set up their profile: Automatic (detect this project's tech stack), Guided (a short conversation to map topics and confidence levels), or Import (restore from a backup). 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.\n"),process.exit(2);}function
|
|
578
|
-
${i.bold(`devcoach ${i.dim(`v${
|
|
568
|
+
The coaching skill is served automatically via the MCP prompt \u2014 always up to date.`));}async function Hr(){let t=createInterface({input:process.stdin,output:process.stdout}),e=async(r,n="")=>{let s=n?` [${n}]`:"";return (await t.question(`${r}${s}: `)).trim()||n},o=async(r,n,s,a)=>{for(;;){let l=await e(r,String(n)),d=Number.parseInt(l,10);if(!Number.isNaN(d)&&d>=s&&d<=a)return d;u(i.red(`Must be ${s}\u2013${a}.`));}};try{u(Ee(i.bold("devcoach setup"))),u(`
|
|
569
|
+
${i.bold("Step 1")} \u2014 Restore from backup`);let r=await e("Path to existing backup zip (Enter to skip)","");if(r){existsSync(r)||(u(i.red(`File not found: ${r}`)),process.exit(1));let b=m(y=>ce(y,readFileSync(r)));u(`${i.green("\u2713")} Restored: ${b.topics} topics, ${b.lessons} lessons${b.learning_state?", notebook":""}`),u(i.green("Setup complete!"));return}u(`
|
|
570
|
+
${i.bold("Step 2")} \u2014 Build your knowledge profile`);let n=(await e("Mode: [a]utomatic (detect from files) / [m]anual (type your stack)","a")).toLowerCase(),s={};if(n.startsWith("a")){let y=Q().folder??process.cwd(),k=_e(y),v=Object.keys(k).sort((h,T)=>h.localeCompare(T));if(v.length){u(`
|
|
571
|
+
${i.dim(`Detected from ${i.cyan(y)}:`)}`);for(let h of v){let T=k[h]??5,D=await e(` ${i.cyan(h)} (Enter=keep, 0-10=override, s=skip)`,String(T));if(D.toLowerCase()==="s")continue;let ue=Number.parseInt(D,10);s[h]=Number.isNaN(ue)?T:Math.max(0,Math.min(10,ue));}}else u(i.dim("No technology files detected in current directory."));let g=await e(`
|
|
572
|
+
Add additional topics (comma-separated, or Enter to skip)`,"");for(let h of g.split(",").map(T=>T.trim()).filter(Boolean))s[h]=await o(` Confidence for ${i.cyan(h)}`,5,0,10);}else for(u(i.dim(`
|
|
573
|
+
Enter topics: 'topic_id confidence' (e.g. python 7). Blank line when done.`));;){let b=(await e("Topic (Enter when done)","")).trim();if(!b)break;let y=b.split(/\s+/),k=y[0]??"",v=Number.parseInt(y[1]??"",10),g=Number.isNaN(v)?5:Math.max(0,Math.min(10,v));s[k]=g,u(` ${i.green("+")} ${i.cyan(k)} \u2192 ${g}`);}let a={};if(Object.keys(s).length&&(u(`
|
|
574
|
+
${i.bold("Step 3")} \u2014 Organise into groups`),(await e("Organise topics into groups? [y/N]","n")).toLowerCase().startsWith("y"))){let y=[];for(let k of Object.keys(s).sort((v,g)=>v.localeCompare(g))){let v=y.length?y.join(", "):"(none yet)",g=await e(` Group for ${i.cyan(k)} existing: ${i.dim(v)} (Enter=Other)`,"");if(g&&g!=="Other"){let h=a[g]??[];h.push(k),a[g]=h,y.includes(g)||y.push(g);}}}u(`
|
|
575
|
+
${i.bold("Step 4")} \u2014 Rate-limit settings`);let l=await o("Max lessons per day",2,1,20),d=await o("Min gap between lessons (minutes)",240,0,1440),f=m(b=>{b.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[y,k]of Object.entries(s))O(b,y,k);for(let[y,k]of Object.entries(a))for(let v of k)N(b,v,y);return R(b,"max_per_day",String(l)),R(b,"min_gap_minutes",String(d)),M(b)});u(Gt(f,"Knowledge Profile")),u(`
|
|
576
|
+
${i.green("Setup complete!")} ${Object.keys(s).length} topics saved.`);}finally{t.close();}}var Gr=24;function Br(t){return t?true:existsSync(w)?(Date.now()-statSync(w).mtimeMs)/36e5<Gr:false}function Kr(){let t;try{t=m(e=>de(e).knowledge_ready);}catch{process.exit(0);}Br(t)&&process.exit(0),mkdirSync(dirname(w),{recursive:true}),existsSync(w)?writeFileSync(w,readFileSync(w)):writeFileSync(w,`# devcoach \u2014 Coaching Notebook
|
|
577
|
+
`),process.stderr.write("devcoach: no coaching profile yet. Run onboarding now \u2014 ask the user how they want to set up their profile: Automatic (detect this project's tech stack), Guided (a short conversation to map topics and confidence levels), or Import (restore from a backup). 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.\n"),process.exit(2);}function zr(){let t;try{t=m(e=>de(e).knowledge_ready?z(e).allowed:!1);}catch{process.exit(0);}t||process.exit(0),process.stderr.write("Deliver a devcoach lesson based on the work in this conversation, then record it by calling the devcoach `log_lesson` MCP tool (not a shell/CLI command \u2014 that is required, or this hook will fire again). Ignore the devcoach://rate-limit check \u2014 it is already verified by this hook.\n"),process.exit(2);}function qr(){let t=[["mcp","Start the MCP server (stdio) for Claude Code / Claude Desktop"],["ui [--port N]","Launch the web dashboard (default port: 7860)"],["setup","First-run wizard: import backup or build your knowledge profile"],["install","Register the MCP server + Stop hook in Claude Code / Claude Desktop"],["onboard-hook","Claude Code Stop hook: cue onboarding (exit 2 when profile needed)"],["lesson-ready","Claude Code Stop hook: exit 2 when a lesson 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(`
|
|
578
|
+
${i.bold(`devcoach ${i.dim(`v${le}`)}`)}`);for(let[e,o]of t)u(` ${i.cyan(`devcoach ${e}`.padEnd(32))} ${i.dim(o)}`);u(`
|
|
579
579
|
${i.dim("Run")} ${i.bold("devcoach <command> --help")} ${i.dim("for per-command options.")}
|
|
580
|
-
`);}function A(t){return t??null}function
|
|
580
|
+
`);}function A(t){return t??null}function Wr(){let t=new Command;return t.exitOverride(),t.name("devcoach").description("devcoach \u2014 progressive technical coaching").version(`devcoach ${le}`,"-v, --version","output the version number").allowExcessArguments(false).showHelpAfterError("(add --help for usage)").action(()=>qr()),t.command("mcp").description("Start the MCP server (stdio transport)").action($t),t.command("profile").description("Show the knowledge map").action(kr),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=>vr({period:e.period??"all",category:A(e.category),project:A(e.project),repository:A(e.repository),branch:A(e.branch),commit:A(e.commit),starred:!!e.starred,feedback:A(e.feedback),level:A(e.level),dateFrom:A(e.dateFrom),dateTo:A(e.dateTo),sort:e.sort??"timestamp",order:e.order??"desc"})),t.command("lesson").description("Show a single lesson in detail").argument("<id>","Lesson ID").action(e=>wr(e)),t.command("star").description("Mark a lesson as starred").argument("<id>","Lesson ID").action(e=>Ft(e,true)),t.command("unstar").description("Remove the starred mark from a lesson").argument("<id>","Lesson ID").action(e=>Ft(e,false)),t.command("delete").description("Permanently delete a lesson").argument("<id>","Lesson ID").action(e=>_r(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)=>$r(e,o)),t.command("settings").description("Show current settings").action(Sr),t.command("stats").description("Coaching statistics and rate-limit status").action(Tr),t.command("set").description("Update a setting (max_per_day | min_gap_minutes)").argument("<key>","Setting key").argument("<value>","New value").action((e,o)=>Er(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)=>Cr(e,Math.max(0,Math.min(10,Number.parseInt(o.confidence??"5",10)||5)),A(o.group))),t.command("knowledge-remove").description("Remove a topic from the knowledge map").argument("<topic>","Topic ID").action(e=>Rr(e)),t.command("group-add").description("Register a new knowledge group").argument("<name>","Group name").action(e=>Lr(e)),t.command("group-remove").description("Delete a knowledge group (topics move to Other)").argument("<name>","Group name").action(e=>Ir(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)=>Or(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=>Dr(e)),t.command("restore").description("Restore from a backup zip file").argument("<file>","Path to backup zip").action(e=>Nr(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=>Ur({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=>Nt(Number.parseInt(e.port??"7860",10)||7860)),t.command("setup").description("First-run wizard: import backup or build your knowledge profile").action(Hr),t.command("onboard-hook",{hidden:true}).description("Claude Code Stop hook: cue onboarding").action(Kr),t.command("lesson-ready",{hidden:true}).description("Claude Code Stop hook: signal a lesson is due").action(zr),t}async function zt(){let t=Wr();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 zt();}catch(t){console.error(t),process.exit(1);}
|