devcoach 0.8.2 → 0.9.0

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.
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ import {z as z$1}from'zod';import {existsSync,readFileSync,mkdirSync,writeFileSync}from'fs';import {createRequire}from'module';import {homedir}from'os';import {join,dirname}from'path';import {strToU8,zipSync,unzipSync,strFromU8}from'fflate';var se=z$1.enum(["junior","mid","senior"]),oe=z$1.enum(["github","gitlab","bitbucket","local"]),re=z$1.enum(["know","dont_know"]),He=z$1.number().int().min(0).max(10);function ie(e){return e.toISOString().replace(/\.\d{3}Z$/,"Z")}function ae(e){if(e instanceof Date){if(Number.isNaN(e.getTime()))throw new TypeError("Invalid Date");return e}let t=e.trim();/[zZ]$|[+-]\d{2}:?\d{2}$/.test(t)||(t=t.includes("T")||t.includes(" ")?`${t.replace(" ","T")}Z`:`${t}T00:00:00Z`);let s=new Date(t);if(Number.isNaN(s.getTime()))throw new TypeError(`Cannot parse timestamp ${JSON.stringify(e)} \u2014 expected ISO 8601`);return s}function ce(e){let t=ae(e),n=new Date;return t.getTime()>n.getTime()&&(t=n),ie(t)}var ue=z$1.union([z$1.string(),z$1.date()]).transform((e,t)=>{try{return ce(e)}catch(n){return t.addIssue({code:"custom",message:n.message}),z$1.NEVER}}),T=z$1.string().nullish().transform(e=>e??null),le=z$1.object({id:z$1.string(),timestamp:ue,topic_id:z$1.string(),categories:z$1.array(z$1.string()),title:z$1.string(),level:se,summary:z$1.string(),body:T,task_context:T,project:T,repository:T,branch:T,commit_hash:T,folder:T,repository_platform:oe.nullish().transform(e=>e??null),starred:z$1.boolean().default(false),feedback:re.nullish().transform(e=>e??null)}),pe=z$1.object({topic:z$1.string(),confidence:z$1.number().int()}),ge=z$1.object({name:z$1.string(),topics:z$1.array(z$1.string())});z$1.object({knowledge:z$1.array(pe),groups:z$1.array(ge)});var de=z$1.enum(["system","dark","light"]),me=z$1.enum(["session","global"]);z$1.object({max_per_day:z$1.number().int().default(2),min_gap_minutes:z$1.number().int().default(240),ui_theme:de.default("system"),nudge_every:z$1.number().int().min(0).default(10),nudge_scope:me.default("session")});function D(e){return le.parse(e)}var {DatabaseSync:Ne}=createRequire(import.meta.url)("node:sqlite"),j=process.env.DEVCOACH_DIR??join(homedir(),".devcoach"),C=join(j,"coaching.db"),R=join(j,"learning-state.md"),b="settings.json",h="lessons.json",w="knowledge.json",x="learning-state.md",nt={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},Le={max_per_day:"2",min_gap_minutes:"240",ui_theme:"system",nudge_every:"10",nudge_scope:"session"},Oe=50;function d(e,t,...n){return e.prepare(t).all(...n)}function m(e,t,...n){return e.prepare(t).get(...n)}function l(e,t,...n){return Number(e.prepare(t).run(...n).changes)}function De(e=C){e!==":memory:"&&mkdirSync(dirname(e),{recursive:true});let t=new Ne(e);return t.exec("PRAGMA busy_timeout = 3000"),t}var X=2;function be(e=C){let t=De(e),n=t.prepare("PRAGMA user_version").get();return Number(n?.user_version??0)!==X&&(he(t),t.exec(`PRAGMA user_version = ${X}`)),t}function st(e,t){e.exec("BEGIN IMMEDIATE");try{let n=t();return e.exec("COMMIT"),n}catch(n){try{e.exec("ROLLBACK");}catch{}throw n}}function ot(e,t=C){let n=be(t);try{return e(n)}finally{n.close();}}function he(e){e.exec(`
3
+ CREATE TABLE IF NOT EXISTS lessons (
4
+ id TEXT PRIMARY KEY,
5
+ timestamp TEXT NOT NULL,
6
+ topic_id TEXT NOT NULL,
7
+ categories TEXT NOT NULL,
8
+ title TEXT NOT NULL,
9
+ level TEXT NOT NULL,
10
+ summary TEXT NOT NULL,
11
+ body TEXT,
12
+ task_context TEXT,
13
+ project TEXT,
14
+ repository TEXT,
15
+ branch TEXT,
16
+ commit_hash TEXT,
17
+ folder TEXT,
18
+ feedback TEXT,
19
+ repository_platform TEXT,
20
+ starred INTEGER NOT NULL DEFAULT 0
21
+ );
22
+
23
+ CREATE TABLE IF NOT EXISTS knowledge (
24
+ topic TEXT PRIMARY KEY,
25
+ confidence INTEGER NOT NULL DEFAULT 5,
26
+ updated_at TEXT NOT NULL
27
+ );
28
+
29
+ CREATE TABLE IF NOT EXISTS settings (
30
+ key TEXT PRIMARY KEY,
31
+ value TEXT NOT NULL
32
+ );
33
+
34
+ CREATE TABLE IF NOT EXISTS knowledge_group_names (
35
+ group_name TEXT PRIMARY KEY
36
+ );
37
+
38
+ CREATE TABLE IF NOT EXISTS knowledge_groups (
39
+ group_name TEXT NOT NULL,
40
+ topic TEXT NOT NULL,
41
+ PRIMARY KEY (group_name, topic)
42
+ );
43
+
44
+ -- Runtime-only: per-session interaction counter for lesson-cue pacing.
45
+ -- Never exported/imported (backup carries config, not this state).
46
+ CREATE TABLE IF NOT EXISTS nudge_state (
47
+ session_id TEXT PRIMARY KEY,
48
+ interactions INTEGER NOT NULL DEFAULT 0,
49
+ updated_at TEXT NOT NULL
50
+ );
51
+
52
+ -- Runtime-only, single row: cue lifecycle. pending=1 between an emitted cue and
53
+ -- its resolution (log_lesson or skip_lesson), arming a shorter retry threshold.
54
+ CREATE TABLE IF NOT EXISTS cue_state (
55
+ id INTEGER PRIMARY KEY CHECK (id = 1),
56
+ pending INTEGER NOT NULL DEFAULT 0,
57
+ last_cue_at TEXT,
58
+ last_skip_reason TEXT
59
+ );
60
+
61
+ CREATE INDEX IF NOT EXISTS idx_lessons_timestamp ON lessons (timestamp);
62
+ CREATE INDEX IF NOT EXISTS idx_lessons_starred_ts ON lessons (starred, timestamp);
63
+ CREATE INDEX IF NOT EXISTS idx_lessons_feedback ON lessons (feedback);
64
+ CREATE INDEX IF NOT EXISTS idx_lessons_topic_id ON lessons (topic_id);
65
+ `),we(e),xe(e);}function we(e){try{e.exec("ALTER TABLE lessons ADD COLUMN body TEXT");}catch{}}function xe(e){for(let[t,n]of Object.entries(Le))l(e,"INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)",t,n);}var B="(id, timestamp, topic_id, categories, title, level, summary, body, task_context, project, repository, branch, commit_hash, folder, repository_platform, starred, feedback)",H="(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";function W(e){return [e.id,e.timestamp,e.topic_id,JSON.stringify(e.categories),e.title,e.level,e.summary,e.body,e.task_context,e.project,e.repository,e.branch,e.commit_hash,e.folder,e.repository_platform,e.starred?1:0,e.feedback]}function rt(e,t){l(e,`INSERT OR REPLACE INTO lessons ${B} VALUES ${H}`,...W(t));}var Ie=[["project","project"],["repository","repository"],["branch","branch"],["commit","commit_hash"]];function Ce(e,t){if(e.date_from==null&&e.date_to==null){let n=Ke(e.period??null);n!=null&&t("timestamp >= ?",n);return}if(e.date_from!=null&&t("timestamp >= ?",e.date_from),e.date_to!=null){let n=e.date_to.includes("T")||e.date_to.includes(" ");t("timestamp <= ?",n?e.date_to:`${e.date_to}T23:59:59`);}}function z(e){let t=[],n=[],s=(r,...i)=>{t.push(r),n.push(...i);};Ce(e,s),e.category!=null&&s("categories LIKE ?",`%"${e.category}"%`),e.level!=null&&s("level = ?",e.level);for(let[r,i]of Ie){let c=e[r];c!=null&&s(`${i} LIKE ?`,`%${c}%`);}if(e.starred!=null&&s("starred = ?",e.starred?1:0),e.search!=null){let r=`%${e.search}%`;s("(title LIKE ? OR topic_id LIKE ? OR summary LIKE ? OR body LIKE ?)",r,r,r,r);}return e.feedback==="none"?s("feedback IS NULL"):e.feedback!=null&&s("feedback = ?",e.feedback),{where:t.length?`WHERE ${t.join(" AND ")}`:"",params:n}}function q(e,t={}){let{where:n,params:s}=z(t),o=m(e,`SELECT COUNT(*) AS n FROM lessons ${n}`,...s);return Number(o?.n??0)}var ke=new Set(["timestamp","level","topic_id","title","feedback"]),Ae=new Set(["project","repository","branch","commit_hash","repository_platform"]);function it(e,t={}){let{where:n,params:s}=z(t),o=t.sort&&ke.has(t.sort)?t.sort:"timestamp",r=(t.order??"desc").toLowerCase()==="asc"?"ASC":"DESC",i=`SELECT * FROM lessons ${n} ORDER BY ${o} ${r}`,c=[...s];if(t.page!=null){let u=t.per_page??25;i+=" LIMIT ? OFFSET ?",c.push(u,(t.page-1)*u);}return d(e,i,...c).map(P)}function at(e,t){return l(e,"DELETE FROM lessons WHERE id = ?",t)>0}function ct(e,t,n){return l(e,"UPDATE lessons SET starred = ? WHERE id = ?",n?1:0,t)>0}function V(e,t,n){l(e,"UPDATE lessons SET feedback = ? WHERE id = ?",n||null,t);let s=m(e,"SELECT topic_id FROM lessons WHERE id = ?",t);return s?s.topic_id:null}function Ue(e){return d(e,"SELECT * FROM lessons ORDER BY timestamp DESC").map(P)}function Fe(e,t){let n=0,s=0,o=0;for(let r of t){let i;try{i=D(r);}catch{o+=1;continue}l(e,`INSERT OR IGNORE INTO lessons ${B} VALUES ${H}`,...W(i))>0?n+=1:s+=1;}return {inserted:n,duplicated:s,invalid:o}}function ut(e,t){if(!Ae.has(t))throw new Error(`Column not allowed: ${JSON.stringify(t)}`);return d(e,`SELECT DISTINCT ${t} FROM lessons WHERE ${t} IS NOT NULL ORDER BY ${t}`).map(s=>s[t])}function lt(e,t){let n=m(e,"SELECT * FROM lessons WHERE id = ?",t);return n?P(n):null}function pt(e){return d(e,"SELECT DISTINCT value FROM lessons, json_each(lessons.categories) ORDER BY value").map(n=>n.value)}function Z(e){return d(e,"SELECT DISTINCT topic_id FROM lessons").map(t=>t.topic_id)}function O(e,t){let n=m(e,"SELECT COUNT(*) AS n FROM lessons WHERE timestamp >= ?",t);return Number(n?.n??0)}function J(e){let t=m(e,"SELECT timestamp FROM lessons ORDER BY timestamp DESC LIMIT 1");return t?t.timestamp:null}function k(e){let t={};for(let n of d(e,"SELECT topic, confidence FROM knowledge"))t[n.topic]=Number(n.confidence);return t}function A(e){return d(e,"SELECT topic, confidence FROM knowledge ORDER BY topic").map(t=>({topic:t.topic,confidence:Number(t.confidence)}))}function U(e){let t=d(e,"SELECT group_name FROM knowledge_group_names ORDER BY group_name").map(s=>s.group_name),n=new Map;for(let s of d(e,"SELECT group_name, topic FROM knowledge_groups ORDER BY group_name, topic")){let o=s.group_name,r=n.get(o)??[];r.push(s.topic),n.set(o,r);}return t.map(s=>({name:s,topics:n.get(s)??[]}))}function N(e,t,n){let s=Math.max(0,Math.min(10,n)),o=new Date().toISOString();l(e,`INSERT INTO knowledge (topic, confidence, updated_at) VALUES (?, ?, ?)
66
+ ON CONFLICT(topic) DO UPDATE SET confidence = excluded.confidence, updated_at = excluded.updated_at`,t,s,o);}function gt(e,t){let n=l(e,"DELETE FROM knowledge WHERE topic = ?",t);return l(e,"DELETE FROM knowledge_groups WHERE topic = ?",t),n>0}function K(e,t){let n=t.trim();if(!n)throw new Error("Group name must not be empty");return l(e,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",n)>0}function dt(e,t){return l(e,"DELETE FROM knowledge_groups WHERE group_name = ?",t),l(e,"DELETE FROM knowledge_group_names WHERE group_name = ?",t)>0}function Me(e,t,n){l(e,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",n),l(e,"DELETE FROM knowledge_groups WHERE topic = ?",t),l(e,"INSERT OR IGNORE INTO knowledge_groups (group_name, topic) VALUES (?, ?)",n,t);}function mt(e,t){l(e,"DELETE FROM knowledge_groups WHERE topic = ?",t);}function f(e){let t={};for(let g of d(e,"SELECT key, value FROM settings"))t[g.key]=g.value;let n=t.min_gap_minutes,s=t.min_hours_between,o;n!==void 0?o=Number.parseInt(n,10):s!==void 0?o=Number.parseInt(s,10)*60:o=240;let r=t.max_per_day,i=t.ui_theme??"system",c=i==="dark"||i==="light"||i==="system"?i:"system",u=t.nudge_every,p=u!==void 0?Number.parseInt(u,10):10,_=t.nudge_scope==="global"?"global":"session";return {max_per_day:r!==void 0?Number.parseInt(r,10):2,min_gap_minutes:o,ui_theme:c,nudge_every:Number.isFinite(p)&&p>=0?p:10,nudge_scope:_}}function S(e,t,n){l(e,"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",t,n);}function Q(e,t,n){let s=m(e,"INSERT INTO nudge_state (session_id, interactions, updated_at) VALUES (?, 1, ?) ON CONFLICT(session_id) DO UPDATE SET interactions = interactions + 1, updated_at = excluded.updated_at RETURNING interactions",t,new Date().toISOString()),o=Number(s?.interactions??0);return o===1&&l(e,"DELETE FROM nudge_state WHERE session_id NOT IN (SELECT session_id FROM nudge_state ORDER BY updated_at DESC LIMIT ?)",Oe),n==="global"?F(e,t,n):o}function Et(e){return d(e,"SELECT session_id, interactions, updated_at FROM nudge_state ORDER BY updated_at DESC").map(t=>({session_id:String(t.session_id),interactions:Number(t.interactions),updated_at:String(t.updated_at)}))}function F(e,t,n){let s=n==="global"?m(e,"SELECT COALESCE(SUM(interactions), 0) AS n FROM nudge_state"):m(e,"SELECT interactions AS n FROM nudge_state WHERE session_id = ?",t);return Number(s?.n??0)}function Tt(e){l(e,"DELETE FROM nudge_state"),l(e,"UPDATE cue_state SET pending = 0 WHERE id = 1");}var M=3;function v(e){let t=m(e,"SELECT pending, last_cue_at, last_skip_reason FROM cue_state WHERE id = 1");return {pending:Number(t?.pending??0)===1,last_cue_at:t?.last_cue_at??null,last_skip_reason:t?.last_skip_reason??null}}function ee(e){l(e,"INSERT INTO cue_state (id, pending, last_cue_at) VALUES (1, 1, ?) ON CONFLICT(id) DO UPDATE SET pending = 1, last_cue_at = excluded.last_cue_at",new Date().toISOString()),l(e,"DELETE FROM nudge_state");}function _t(e,t){l(e,"INSERT INTO cue_state (id, pending, last_skip_reason) VALUES (1, 0, ?) ON CONFLICT(id) DO UPDATE SET pending = 0, last_skip_reason = excluded.last_skip_reason",t??null),l(e,"DELETE FROM nudge_state");}function $(e){let t=m(e,"SELECT COUNT(*) AS n FROM knowledge");return {knowledge_ready:Number(t?.n??0)>0}}function St(e){let t={};for(let n of ["project","repository","branch","repository_platform"]){let s=m(e,`SELECT ${n} AS v, COUNT(*) AS c FROM lessons WHERE ${n} IS NOT NULL GROUP BY ${n} ORDER BY c DESC LIMIT 1`);t[n]=s?s.v:null;}return t}function ft(e){let t=f(e),n=Ue(e),s=A(e),o=U(e),r=new Map;for(let u of o)for(let p of u.topics)r.set(p,u.name);let i={groups:o.map(u=>u.name),topics:s.map(u=>({topic:u.topic,confidence:u.confidence,group:r.get(u.topic)??null}))},c={[b]:strToU8(JSON.stringify(t,null,2)),[h]:strToU8(JSON.stringify(n,null,2)),[w]:strToU8(JSON.stringify(i,null,2))};return existsSync(R)&&(c[x]=strToU8(readFileSync(R,"utf8"))),zipSync(c)}function ve(e,t,n){if(!(b in t))return;let s=JSON.parse(strFromU8(t[b]));"max_per_day"in s&&S(e,"max_per_day",String(s.max_per_day)),"min_gap_minutes"in s&&S(e,"min_gap_minutes",String(s.min_gap_minutes)),(s.ui_theme==="system"||s.ui_theme==="dark"||s.ui_theme==="light")&&S(e,"ui_theme",s.ui_theme),"nudge_every"in s&&S(e,"nudge_every",String(s.nudge_every)),(s.nudge_scope==="session"||s.nudge_scope==="global")&&S(e,"nudge_scope",s.nudge_scope),n.settings=1;}function $e(e,t,n){if(!(w in t))return;let s=JSON.parse(strFromU8(t[w]));if(!(s&&typeof s=="object"&&Array.isArray(s.topics))){for(let[r,i]of Object.entries(s))N(e,r,i);n.topics=Object.keys(s).length;return}let o=0;for(let r of s.groups??[])K(e,r)&&(o+=1);for(let r of s.topics){N(e,r.topic,r.confidence);let i=r.group;i&&i!=="Other"&&(K(e,i)&&(o+=1),Me(e,r.topic,i));}n.topics=s.topics.length,n.groups=o;}function Pe(e,t,n){if(!(h in t))return;let s=JSON.parse(strFromU8(t[h])),{inserted:o,duplicated:r,invalid:i}=Fe(e,s);n.lessons=o,n.skipped=r,n.invalid=i;}function Xe(e,t){x in e&&(mkdirSync(dirname(R),{recursive:true}),writeFileSync(R,strFromU8(e[x]),"utf8"),t.learning_state=1);}function yt(e,t){let n={settings:0,topics:0,groups:0,lessons:0,skipped:0,invalid:0,learning_state:0},s=unzipSync(t);return ve(e,s,n),$e(e,s,n),Pe(e,s,n),Xe(s,n),n}function Ke(e){let t=new Date,n;if(e==="today")n=new Date(Date.UTC(t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate(),0,0,0,0));else if(e==="week")n=new Date(t.getTime()-7*864e5);else if(e==="month")n=new Date(t.getTime()-30*864e5);else if(e==="year")n=new Date(t.getTime()-365*864e5);else return null;return n.toISOString()}function P(e){let t=[];try{let n=JSON.parse(e.categories);Array.isArray(n)&&(t=n);}catch{t=[];}return D({...e,categories:t,starred:!!e.starred})}function te(e){try{let t=f(e),n=new Date,s=new Date(n.getTime()-24*36e5).toISOString(),o=O(e,s);if(o>=t.max_per_day)return {allowed:!1,reason:`Daily limit reached (${o}/${t.max_per_day} lessons in the last 24h)`};let r=J(e);if(r!=null){let i=new Date(r),c=(n.getTime()-i.getTime())/6e4;if(c<t.min_gap_minutes){let u=t.min_gap_minutes-c,p=Math.floor(t.min_gap_minutes/60),_=t.min_gap_minutes%60,g=Math.trunc(u),E=Math.floor(g/60),ne=g%60;return {allowed:!1,reason:`Too soon: last lesson ${c<0?`in ${Math.round(-c)}m (future timestamp)`:`${Math.round(c)}m ago`}, minimum interval is ${p}h ${_}m (${E}h ${ne}m remaining)`}}}return {allowed:!0}}catch(t){return {allowed:true,reason:`Rate limit check failed: ${t}`}}}function Lt(e){try{return {knowledge:A(e),groups:U(e)}}catch{return {knowledge:[],groups:[]}}}function Ge(e,t,n){let s=k(e)[t]??5,o=Math.max(0,Math.min(10,s+n));return N(e,t,o),o}function Ot(e,t,n){let s=V(e,t,n);return s&&(n==="know"||n==="dont_know")&&Ge(e,s,n==="know"?1:-1),s}function Ye(e){try{let t=new Date,n=q(e),s=new Date(t.getTime()-24*36e5).toISOString(),o=new Date(t.getTime()-7*864e5).toISOString(),r=O(e,s),i=O(e,o),c=k(e),u=Object.entries(c).sort((g,E)=>g[1]-E[1]),p=u.slice(0,5).map(([g,E])=>({topic:g,confidence:E})),_=u.slice(-5).reverse().map(([g,E])=>({topic:g,confidence:E}));return {total_lessons:n,lessons_today:r,lessons_this_week:i,weakest_topics:p,strongest_topics:_}}catch(t){return {error:String(t)}}}function Dt(e){try{return Z(e)}catch{return []}}function bt(e,t,n){let s=c=>({cue:false,reason:c,nextLessonNumber:0});if(n.planMode)return s("plan mode (not counted)");if(!$(e).knowledge_ready)return s("onboarding not complete");let o=f(e),r="pacing disabled (nudge_every=0)";if(o.nudge_every>0){let c=Q(e,t??"__nosession__",o.nudge_scope),{pending:u}=v(e),p=u?Math.min(M,o.nudge_every):o.nudge_every;if(r=`${c}/${p}${u?" (retry window)":""}`,c<p)return s(`paced (${r})`)}let i=te(e);return i.allowed?(ee(e),{cue:true,reason:`cue (${r})`,nextLessonNumber:Number(Ye(e).total_lessons??0)+1}):s(`rate limited: ${i.reason??"denied"}`)}function ht(e,t){try{let n=[],s=!0;if(!$(e).knowledge_ready)return {wouldCue:!1,reasons:["onboarding not complete \u2014 no knowledge topics yet"]};let o=f(e);if(o.nudge_every>0){let i=F(e,t??"__nosession__",o.nudge_scope)+1,{pending:c}=v(e),u=c?Math.min(M,o.nudge_every):o.nudge_every;i<u?(s=!1,n.push(`pacing: next stop would be ${i}/${u}${c?" (retry window)":""}`)):n.push(`pacing: next stop reaches the threshold (${i}/${u})`);}else n.push("pacing disabled (nudge_every=0) \u2014 every eligible stop cues");let r=te(e);return r.allowed?n.push("rate limit: allowed"):(s=!1,n.push(`rate limited: ${r.reason??"denied"}`)),{wouldCue:s,reasons:n}}catch(n){return {wouldCue:false,reasons:[`check failed: ${n}`]}}}export{gt as A,K as B,dt as C,Me as D,mt as E,f as F,S as G,Et as H,Tt as I,v as J,_t as K,$ as L,St as M,ft as N,yt as O,te as P,Lt as Q,Ge as R,Ot as S,Ye as T,Dt as U,bt as V,ht as W,se as a,oe as b,re as c,He as d,de as e,me as f,D as g,j as h,C as i,R as j,nt as k,st as l,ot as m,rt as n,q as o,it as p,at as q,ct as r,V as s,Ue as t,Fe as u,ut as v,lt as w,pt as x,A as y,N as z};
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import {readFileSync,readdirSync,statSync,existsSync,realpathSync}from'fs';import {dirname,basename,join}from'path';import {homedir,tmpdir}from'os';import {execFileSync}from'child_process';import {fileURLToPath}from'url';var I=[["package.json","javascript",6],["tsconfig*.json","typescript",6],["requirements*.txt","python",6],["pyproject.toml","python",6],["setup.py","python",6],["go.mod","go",6],["Cargo.toml","rust",6],["pom.xml","java",6],["build.gradle","java",6],["build.gradle.kts","java",6],["*.csproj","csharp",6],["Gemfile","ruby",6],["composer.json","php",6],["Dockerfile","docker",7],["Dockerfile.*","docker",7],["docker-compose.yml","docker_compose",7],["docker-compose.yaml","docker_compose",7],["docker-compose.*.yml","docker_compose",7],[".github/workflows/*.yml","github_actions",6],[".github/workflows/*.yaml","github_actions",6],[".gitlab-ci.yml","gitlab_ci",6],["Jenkinsfile","ci_cd",6],["*.tf","terraform",6],["*.sql","sql",5],[".git","git",7],["kubernetes/*.yaml","kubernetes",6],["kubernetes/*.yml","kubernetes",6],["k8s/*.yaml","kubernetes",6],["*.bicep","azure",6],["serverless.yml","aws",6],["serverless.yaml","aws",6],["template.yaml","aws",5],["angular.json","angular",6],["svelte.config.*","svelte",6],["nuxt.config.*","vue",6],["vite.config.*","javascript",6],["*.xcodeproj","swift",6],["*.xcworkspace","swift",6],["Package.swift","swift",6]],T={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"},L={django:"django",flask:"flask",fastapi:"fastapi",starlette:"fastapi",tornado:"python",aiohttp:"python",sqlalchemy:"postgresql",celery:"python"};function N(t){let e="";for(let r of t)r==="*"?e+="[^/]*":r==="?"?e+="[^/]":e+=r.replace(/[.+^${}()|[\]\\]/g,String.raw`\$&`);return new RegExp(`^${e}$`)}function F(t,e){let r=e.lastIndexOf("/"),n=r>=0?join(t,e.slice(0,r)):t,s=r>=0?e.slice(r+1):e,i;try{i=readdirSync(n);}catch{return false}let c=N(s);return i.some(l=>c.test(l))}function v(t){try{return existsSync(t)?readFileSync(t,"utf8"):null}catch{return null}}function q(t,e){let r=v(join(t,"package.json"));if(!r)return;let n;try{n=JSON.parse(r);}catch{return}let s=new Set([...Object.keys(n.dependencies??{}),...Object.keys(n.devDependencies??{})]);for(let[i,c]of Object.entries(T))s.has(i)&&e(c,6);}function H(t,e){let r=["requirements.txt","requirements-dev.txt","requirements/base.txt","pyproject.toml"];for(let n of r){let s=v(join(t,n))?.toLowerCase();if(s)for(let[i,c]of Object.entries(L))s.includes(i)&&e(c,6);}}function x(t){let e={},r=(n,s)=>{(e[n]??-1)<s&&(e[n]=s);};for(let[n,s,i]of I)F(t,n)&&r(s,i);return q(t,r),H(t,r),e}function g(...t){let e={};for(let r of t)for(let[n,s]of Object.entries(r))(e[n]??-1)<s&&(e[n]=s);return e}var K=15,X=2,z=40,Y=400,S=1200,V=new Set(["node_modules","dist","build","out","coverage","vendor","venv","__pycache__","Pods","DerivedData","target","tmp"]),Z={".ts":"typescript",".tsx":"typescript",".js":"javascript",".jsx":"javascript",".mjs":"javascript",".py":"python",".swift":"swift",".java":"java",".kt":"java",".go":"go",".rs":"rust",".rb":"ruby",".php":"php",".cs":"csharp",".sql":"sql",".tf":"terraform"},_={detected_stack:{},projects:[],scanned_projects:0};function B(){let t=process.env.DEVCOACH_CLAUDE_DIR??process.env.CLAUDE_CONFIG_DIR,e=t??join(homedir(),".claude");return {configPath:t?join(e,".claude.json"):join(homedir(),".claude.json"),historyPath:join(e,"history.jsonl"),projectsDir:join(e,"projects")}}function y(t){try{return realpathSync(t)}catch{return t}}function Q(t){let e=y(t);return e.startsWith(y(homedir()))?false:e.startsWith(y(tmpdir()))||e.startsWith("/private/")}function R(t){try{return statSync(t).isDirectory()}catch{return false}}function tt(t,e){let r=[],n=[{dir:t,depth:0}],s=0;for(;n.length>0&&s<z&&e.left>0;){let{dir:i,depth:c}=n.shift();if(s++,e.left--,r.push(x(i)),c>=X)continue;let l;try{l=readdirSync(i);}catch{continue}for(let u of l){if(u.startsWith(".")||V.has(u))continue;let o=join(i,u);R(o)&&n.push({dir:o,depth:c+1});}}return g(...r)}function et(t){let e={};if(!Array.isArray(t))return e;for(let r of t){if(typeof r!="string")continue;let n=r.lastIndexOf("."),s=n>=0?Z[r.slice(n).toLowerCase()]:void 0;s&&(e[s]=Math.max(e[s]??0,5));}return e}function nt(t){let e=new Map,r;try{r=readFileSync(t,"utf8");}catch{return e}for(let n of r.split(`
3
+ `))if(n)try{let s=JSON.parse(n);if(typeof s.project!="string")continue;let i=typeof s.timestamp=="number"?s.timestamp:0,c=e.get(s.project)??{count:0,last:0};e.set(s.project,{count:c.count+1,last:Math.max(c.last,i)});}catch{}return e}function rt(t,e){let r=e.replace(/[^a-zA-Z0-9]/g,"-"),n=(()=>{try{return readFileSync(join(t,r,"memory","MEMORY.md"),"utf8").trim()}catch{return null}})();return n?n.length>S?`${n.slice(0,S)}\u2026`:n:null}function jt(){try{let{configPath:t,historyPath:e,projectsDir:r}=B(),n;try{n=JSON.parse(readFileSync(t,"utf8"))?.projects??{};}catch{return _}let s=nt(e),i=Object.keys(n).filter(o=>!Q(o)&&R(o)).sort((o,a)=>(s.get(a)?.last??0)-(s.get(o)?.last??0)).slice(0,K),c={left:Y},l=[];for(let o of i){let a=g(tt(o,c),et(n[o]?.exampleFiles)),f=rt(r,o);if(Object.keys(a).length===0&&f===null)continue;let d=s.get(o);l.push({stack:a,signal:{name:basename(o),path:o,topics:Object.keys(a).sort(),prompt_count:d?.count??0,last_activity:d?.last?new Date(d.last).toISOString():null,memory:f}});}let u=g(...l.map(o=>o.stack));for(let[o,a]of Object.entries(u))l.filter(d=>d.stack[o]!==void 0).length>=3&&a<8&&(u[o]=a+1);return {detected_stack:u,projects:l.map(o=>o.signal).sort((o,a)=>a.prompt_count-o.prompt_count).slice(0,20),scanned_projects:i.length}}catch{return _}}function k(...t){try{return execFileSync("git",t,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}}var w={"github.com":"github","gitlab.com":"gitlab","bitbucket.org":"bitbucket"};function it(t){if(!t)return [null,null];let e=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(t);if(e){let n=(e[1]??"").toLowerCase();return [e[2]??t,w[n]??"local"]}let r=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(t);if(r){let n=(r[1]??"").toLowerCase();return [r[2]??t,w[n]??"local"]}return [t,"local"]}function St(){let t=process.cwd(),e=k("rev-parse","--abbrev-ref","HEAD"),r=k("rev-parse","HEAD"),n=k("remote","get-url","origin"),[s,i]=it(n),c;return s?c=s.replace(/\/+$/,"").split("/").pop()??null:c=basename(t)||null,e==="HEAD"&&(e=null),{project:c,repository:s,branch:e,commit_hash:r,folder:t,repository_platform:i}}var A=dirname(fileURLToPath(import.meta.url));function E(){for(let t of ["../assets","../../assets"])if(existsSync(join(A,t,"SKILL.md")))return join(A,t);return null}var Pt=()=>{let t=E();return t===null?"devcoach: coaching instructions unavailable (SKILL.md not found in package).":readFileSync(join(t,"SKILL.md"),"utf8")};function Et(){let t=E();if(t===null)return [];try{return readdirSync(join(t,"references")).filter(e=>e.endsWith(".md")).sort().map(e=>({name:e,content:readFileSync(join(t,"references",e),"utf8")}))}catch{return []}}var C={version:"0.9.0"};var Mt=C.version;export{x as a,g as b,jt as c,St as d,Pt as e,Et as f,Mt as g};
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import {e,f,b,d}from'./chunk-2ATV4VK3.js';export{c as buildLessonCue,d as cmdLessonReady,b as cmdOnboardHook,f as cmdPromptHook,e as cmdStopHook,a as parseHookPayload}from'./chunk-2ATV4VK3.js';import {g,d as d$1,c,b as b$1,a,e as e$1,f as f$1}from'./chunk-OX67ZDAD.js';import {m,Q,p,w as w$1,r,q,S,F,P,T,G,z,D,A,B,C,E as E$1,N as N$1,y,t as t$1,j as j$1,O,i,L as L$1,H,J as J$1,W as W$1}from'./chunk-ECN4MNGM.js';import {spawnSync}from'child_process';import {writeFileSync,existsSync,readFileSync,mkdirSync,accessSync,constants}from'fs';import {homedir,platform}from'os';import {join,dirname}from'path';import {createInterface}from'readline/promises';import {Command}from'commander';import {styleText}from'util';var Ge=!!process.stdout.isTTY&&!process.env.NO_COLOR,w=(e,o)=>Ge?styleText(e,o):o,t={green:e=>w("green",e),red:e=>w("red",e),yellow:e=>w("yellow",e),cyan:e=>w("cyan",e),magenta:e=>w("magenta",e),dim:e=>w("dim",e),bold:e=>w("bold",e)};function U(e,o){return e==="green"?t.green(o):e==="yellow"?t.yellow(o):t.red(o)}var Be=/\[[0-9;]*m/g,ze=e=>e.replace(Be,""),N=e=>[...ze(e)].length;function Ce(e,o,n){let r=Math.max(0,o-N(e));if(n==="right")return " ".repeat(r)+e;if(n==="center"){let a=Math.floor(r/2);return " ".repeat(a)+e+" ".repeat(r-a)}return e+" ".repeat(r)}function L(e,o,n,r=true){let a=o.map((p,d)=>Math.max(r?N(p.header):0,...n.map(c=>N(c[d]??"")))),i=(p,d,c)=>p+a.map(g=>"\u2500".repeat(g+2)).join(d)+c,l=p=>`\u2502${p.map((d,c)=>` ${Ce(d??"",a[c]??0,o[c]?.justify??"left")} `).join("\u2502")}\u2502`,m=a.reduce((p,d)=>p+d+3,1),u=[];e&&u.push(Ce(t.bold(e),m,"center")),u.push(i("\u256D","\u252C","\u256E")),r&&u.push(l(o.map(p=>t.bold(p.header))),i("\u251C","\u253C","\u2524"));for(let p of n)u.push(l(p));return u.push(i("\u2570","\u2534","\u256F")),u.join(`
3
+ `)}function E(e=""){if(!e)return "\u2500".repeat(60);let n=` ${e} `,r=Math.max(2,60-N(n)),a=Math.floor(r/2);return "\u2500".repeat(a)+n+"\u2500".repeat(r-a)}var Se=e=>{let o=Math.max(0,Math.min(10,Math.round(e)));return "\u2588".repeat(o)+"\u2591".repeat(10-o)},_e=e=>e>=7?"green":e>=4?"yellow":"red";var s=(e="")=>{console.log(e);},Me=e=>e==="junior"?t.green(e):e==="mid"?t.yellow(e):e==="senior"?t.red(e):e,Ve=e=>e==="know"?` ${t.green("\u2713")}`:e==="dont_know"?` ${t.red("\u2717")}`:"",Ze=e=>e==="know"?t.green("\u2713 I know this"):e==="dont_know"?t.red("\u2717 I don't know this"):t.dim("no feedback");function Oe(e,o){let n=new Map;for(let a of e.groups)for(let i of a.topics)n.set(i,a.name);let r=[...e.knowledge].sort((a,i)=>i.confidence-a.confidence).map(a=>{let i=_e(a.confidence);return [a.topic,n.get(a.topic)??"Other",U(i,`${a.confidence}/10`),U(i,Se(a.confidence))]});return L(o,[{header:"Topic"},{header:"Group"},{header:"Confidence",justify:"right"},{header:"Bar"}],r)}function qe(){let e=m(o=>Q(o));s(Oe(e,"Knowledge Map"));}function Xe(e){let o=m(i=>p(i,{period:e.period==="all"?null:e.period,category:e.category,level:e.level,project:e.project,repository:e.repository,branch:e.branch,commit:e.commit,starred:e.starred?true:null,feedback:e.feedback,date_from:e.dateFrom,date_to:e.dateTo,sort:e.sort,order:e.order}));if(o.length===0){s(t.dim("No lessons found."));return}let n=o.some(i=>i.project||i.branch||i.commit_hash),r=[{header:""},{header:"Date"},{header:"Topic"},{header:"Title"},{header:"Level",justify:"center"},{header:"Categories"}];n&&r.push({header:"Project"},{header:"Branch"},{header:"Commit"});let a=o.map(i=>{let l=[i.starred?t.yellow("\u2605"):t.dim("\xB7"),i.timestamp.slice(0,10),t.cyan(i.topic_id),i.title+Ve(i.feedback),Me(i.level),i.categories.join(", ")];return n&&l.push(i.project??"",i.branch?t.magenta(i.branch):"",i.commit_hash?t.cyan(i.commit_hash.slice(0,7)):""),l});s(L("Lessons",r,a));}function Qe(e){let o=m(r=>w$1(r,e));o===null&&(s(t.red(`Lesson '${e}' not found.`)),process.exit(1)),s(E(t.bold(o.title))),s(`${t.dim("ID:")} ${o.id}`),s(`${t.dim("Date:")} ${o.timestamp.slice(0,19).replace("T"," ")}`),s(`${t.dim("Topic:")} ${o.topic_id}`),s(`${t.dim("Categories:")} ${o.categories.join(", ")}`),s(`${t.dim("Level:")} ${Me(o.level)}`);let n=o.starred?t.yellow("\u2605 starred"):t.dim("\u2606 not starred");if(s(`${t.dim("Star:")} ${n} ${t.dim("Feedback:")} ${Ze(o.feedback)}`),o.task_context&&s(`${t.dim("Context:")} ${o.task_context}`),o.project||o.repository||o.branch||o.commit_hash||o.folder){let r=[];o.project&&r.push(`project=${o.project}`),o.repository&&r.push(`repo=${o.repository}`),o.branch&&r.push(`branch=${t.magenta(o.branch)}`),o.commit_hash&&r.push(`commit=${t.cyan(o.commit_hash.slice(0,7))}`),o.folder&&r.push(`folder=${o.folder}`),s(`${t.dim("Git:")} ${r.join(" \xB7 ")}`);}s(E()),s(o.summary);}function Le(e,o){m(r$1=>w$1(r$1,e)===null?false:(r(r$1,e,o),true))||(s(t.red(`Lesson '${e}' not found.`)),process.exit(1)),s(o?`Lesson ${t.cyan(e)} \u2192 ${t.yellow("\u2605 starred")}`:`Lesson ${t.cyan(e)} \u2192 ${t.dim("\u2606 unstarred")}`);}function eo(e){m(n=>q(n,e))||(s(t.red(`Lesson '${e}' not found.`)),process.exit(1)),s(`Lesson ${t.cyan(e)} deleted.`);}function oo(e,o){["know","dont_know","clear"].includes(o)||(s(t.red(`Invalid feedback '${o}'. Use: know | dont_know | clear`)),process.exit(1));let n=o==="clear"?null:o,r=m(l=>{let m=S(l,e,n);if(m===null)return null;let u=l.prepare("SELECT confidence FROM knowledge WHERE topic = ?").get(m);return {topicId:m,newConf:u?u.confidence:5}});r===null&&(s(t.red(`Lesson '${e}' not found.`)),process.exit(1));let a;if(n==="know"||n==="dont_know"){let l=r.newConf+(n==="know"?-1:1);a=`${t.cyan(r.topicId)} confidence: ${l} \u2192 ${t.bold(String(r.newConf))}`;}else a="feedback cleared";let i=n==="know"?t.green("\u2713 I know this"):n==="dont_know"?t.red("\u2717 I don't know this"):t.dim("cleared");s(`Lesson ${t.cyan(e)} \u2192 ${i} (${a})`);}function to(){let e=m(a=>F(a)),o=Math.floor(e.min_gap_minutes/60),n=e.min_gap_minutes%60,r=o?`${o}h ${n}m`:`${n}m`;s(L("Settings",[{header:"Key"},{header:"Value",justify:"right"}],[["max_per_day",String(e.max_per_day)],["min_gap_minutes",`${e.min_gap_minutes} (${r})`],["nudge_every",e.nudge_every===0?"0 (off)":String(e.nudge_every)],["nudge_scope",e.nudge_scope]]));}function no(){let{stats:e,rateLimit:o,settings:n}=m(u=>({stats:T(u),rateLimit:P(u),settings:F(u)})),r=u=>Number(e[u]??0),a=o.allowed?t.green("Available now"):t.yellow(o.reason??"");s(L("Coaching Stats",[{header:"Metric"},{header:"Value",justify:"right"}],[["Total lessons",String(r("total_lessons"))],["Lessons today (24h)",`${r("lessons_today")} / ${n.max_per_day}`],["Lessons this week",String(r("lessons_this_week"))],["Next lesson",a]],false));let i=e.weakest_topics??[],l=e.strongest_topics??[];if(i.length||l.length){let u=[];for(let p=0;p<Math.max(i.length,l.length);p++){let d=i[p],c=l[p];u.push([d?`${t.red(d.topic)} ${t.dim(`(${d.confidence})`)}`:"",c?`${t.green(c.topic)} ${t.dim(`(${c.confidence})`)}`:""]);}s(L(void 0,[{header:"Weakest topics"},{header:"Strongest topics"}],u));}let m$1=Ee();m$1&&s(`
4
+ ${m$1}`);}function ro(e,o){let n=["max_per_day","min_gap_minutes","nudge_every","nudge_scope"];if(n.includes(e)||(s(t.red(`Unknown key '${e}'. Valid keys: ${n.join(", ")}`)),process.exit(1)),e==="nudge_scope"&&o!=="session"&&o!=="global"&&(s(t.red(`Invalid nudge_scope '${o}'. Use: session | global`)),process.exit(1)),e==="nudge_every"){let r=Number.parseInt(o,10);(!Number.isInteger(r)||r<0)&&(s(t.red(`Invalid nudge_every '${o}'. Use a non-negative integer (0 = off).`)),process.exit(1));}m(r=>G(r,e,o)),s(t.green(`Set ${e} = ${o}`));}function so(e,o,n){let r=e.trim();r||(s(t.red("Topic name must not be empty.")),process.exit(1)),m(i=>{z(i,r,o),n&&n!=="Other"&&D(i,r,n);});let a=n&&n!=="Other"?` \u2192 ${t.cyan(n)}`:"";s(`${t.green("Added")} ${t.bold(r)} (confidence ${t.cyan(String(o))})${a}`);}function io(e){let o=m(n=>A(n,e));s(o?`${t.green("Removed")} ${t.bold(e)} from knowledge map`:t.yellow(`Topic '${e}' not found.`));}function ao(e){let o=e.trim();(!o||o==="Other")&&(s(t.red("Invalid group name.")),process.exit(1)),m(n=>B(n,o)),s(`${t.green(`Group '${t.cyan(o)}' ready.`)} Assign topics with: devcoach group-assign <topic> "${o}"`);}function co(e){let o=m(n=>C(n,e));s(o?`${t.green(`Removed group '${t.cyan(e)}'`)}`:t.yellow(`Group '${e}' not found.`));}function lo(e,o){m(r=>r.prepare("SELECT topic FROM knowledge WHERE topic = ?").get(e)===void 0?false:(o==="Other"?E$1(r,e):D(r,e,o),true))||(s(t.red(`Topic '${e}' not in knowledge map. Add it first.`)),process.exit(1)),s(o==="Other"?`${t.green("Moved")} ${t.bold(e)} \u2192 Other (ungrouped)`:`${t.green("Moved")} ${t.bold(e)} \u2192 ${t.cyan(o)}`);}function uo(e){let{lessonsCount:o,knowledgeCount:n,data:r}=m(i=>({lessonsCount:t$1(i).length,knowledgeCount:y(i).length,data:N$1(i)}));writeFileSync(e,r);let a=existsSync(j$1)?" + notebook":"";s(`${t.green("Backup saved:")} ${e} (${t.cyan(String(o))} lessons, ${t.cyan(String(n))} topics${a})`);}function po(e){existsSync(e)||(s(t.red(`File not found: ${e}`)),process.exit(1));let o=m(r=>O(r,readFileSync(e)));o.settings&&s(`${t.green("\u2713")} Settings restored`),o.topics&&s(`${t.green("\u2713")} Knowledge map restored (${t.cyan(String(o.topics))} topics)`);let n=[`${t.cyan(String(o.lessons))} imported`];o.skipped&&n.push(`${t.yellow(String(o.skipped))} duplicates skipped`),o.invalid&&n.push(`${t.red(String(o.invalid))} rejected (invalid)`),s(`${t.green("\u2713")} Lessons: ${n.join(", ")}`),o.learning_state&&s(`${t.green("\u2713")} Notebook restored`);}function mo(){let e=platform();if(e==="darwin")return join(homedir(),"Library","Application Support","Claude","claude_desktop_config.json");if(e==="win32"){let n=process.env.APPDATA??join(homedir(),"AppData","Roaming");return join(n,"Claude","claude_desktop_config.json")}let o=process.env.XDG_CONFIG_HOME??join(homedir(),".config");return join(o,"Claude","claude_desktop_config.json")}var j=join(homedir(),".claude","settings.json"),v=join(homedir(),".claude","skills","devcoach"),De=join(v,".devcoach-version");function Pe(e){for(let o of (process.env.PATH??"").split(":"))if(o)try{return accessSync(join(o,e),constants.X_OK),join(o,e)}catch{}return null}var Ie=e=>Pe(e)!==null;function Re(){return Ie("devcoach")?{command:"devcoach",args:["mcp"]}:{command:"npx",args:["-y","devcoach","mcp"]}}function go(){let e=Pe("devcoach");return e?e.includes("fnm_multishells")?"devcoach":e:"npx -y devcoach"}function fo(e,o){if(!Ie("claude"))return "";o&&spawnSync("claude",["mcp","remove","--scope",e,"devcoach"],{encoding:"utf8"});let n=Re(),r=spawnSync("claude",["mcp","add","--scope",e,"devcoach",n.command,"--",...n.args],{encoding:"utf8"});return r.status===0?`${t.green("\u2713")} Registered via \`claude mcp add\` (scope: ${e})`:`${r.stderr??""}${r.stdout??""}`.toLowerCase().includes("already")?`${t.yellow("Already registered")} in Claude Code (use --force to overwrite)`:`${t.red("claude mcp add failed:")} ${(r.stderr||r.stdout||"").trim()}`}function M(e){if(!existsSync(e))return {ok:true,data:{}};try{return {ok:!0,data:JSON.parse(readFileSync(e,"utf8"))??{}}}catch{return {ok:false,error:`${t.red("\u2717")} ${e} is not valid JSON \u2014 fix it and re-run (left unchanged)`}}}function Te(e,o,n){let r=M(e);if(!r.ok)return r.error;let a=r.data;a.mcpServers??={};let i=a.mcpServers;return i.devcoach&&!n?`${t.yellow("Already registered")} in ${e} (use --force to overwrite)`:(i.devcoach=o,mkdirSync(dirname(e),{recursive:true}),writeFileSync(e,`${JSON.stringify(a,null,2)}
5
+ `),`${t.green("\u2713")} Installed into ${e}`)}function ho(){let e=go();return {Stop:{hooks:[{type:"command",command:`${e} stop-hook`,timeout:60}]},UserPromptSubmit:{hooks:[{type:"command",command:`${e} prompt-hook`,timeout:30}]}}}function Ne(e){return Object.entries(e.enabledPlugins??{}).some(([o,n])=>n&&o.startsWith("devcoach@"))}function W(e){return /dev-?coach/i.test(e)&&/\b(?:stop-hook|prompt-hook|onboard-hook|lesson-ready)\b/.test(e)}function ko(){let e=j,o=M(e);if(!o.ok)return o.error;let n=o.data;if(Ne(n))return `${t.yellow("Skipped")} \u2014 the devcoach plugin already provides the hooks (installing both would double-count interactions)`;n.hooks??={};let r=n.hooks,a=false;for(let[i,l]of Object.entries(ho())){r[i]??=[];let m=r[i],u=m.map((d,c)=>[d,c]).filter(([d])=>(d.hooks??[]).some(c=>W(c.command??""))).map(([,d])=>d),[p]=u;if(!(u.length===1&&p!==void 0&&JSON.stringify(m[p])===JSON.stringify(l))){for(let d of u.toReversed())m.splice(d,1);m.push(l),a=true;}}return a?(mkdirSync(dirname(e),{recursive:true}),writeFileSync(e,`${JSON.stringify(n,null,2)}
6
+ `),`${t.green("\u2713")} Hooks installed into ${e} (Stop + UserPromptSubmit)`):`${t.yellow("Already installed")} in ${e} (current layout)`}function J(){if(!existsSync(join(v,"SKILL.md")))return "missing";try{if(readFileSync(De,"utf8").trim()===g)return "current"}catch{}return "outdated"}function bo(e){if(!e&&J()==="current")return `${t.yellow("Already installed")} in ${v} (current version)`;mkdirSync(v,{recursive:true}),writeFileSync(join(v,"SKILL.md"),e$1());let o=f$1();if(o.length){let n=join(v,"references");mkdirSync(n,{recursive:true});for(let r of o)writeFileSync(join(n,r.name),r.content);}return writeFileSync(De,`${g}
7
+ `),`${t.green("\u2713")} Installed into ${v}`}function Ee(){let e=J();if(e==="outdated")return `${t.yellow("\u2192")} The devcoach Claude Code skill is out of date \u2014 run ${t.bold("devcoach install")} to refresh it.`;if(e==="missing"){let o=M(j);if(o.ok&&(o.data.hooks?.Stop??[]).some(r=>(r.hooks??[]).some(a=>W(a.command??""))))return `${t.yellow("\u2192")} The devcoach Claude Code skill is not installed \u2014 run ${t.bold("devcoach install")} to add it.`}return ""}function yo(e){let o=e.claudeCode||!e.claudeDesktop,n=e.claudeDesktop||!e.claudeCode,r=Re(),a=false;if(s(t.bold("Setting up devcoach")+t.dim(` (${r.command} ${r.args.join(" ")})`)),s(),o){s(t.bold("Claude Code"));let i=fo("user",e.force);if(!i){let l=join(homedir(),".claude.json");i=Te(l,{type:"stdio",env:{},...r},e.force),a=true;}s(` MCP server\u2026 ${i}`),e.skipHook||s(` Hooks\u2026 ${ko()}`),s(` Skill\u2026 ${bo(e.force)}`),s();}n&&(s(t.bold("Claude Desktop")),s(` MCP server\u2026 ${Te(mo(),r,e.force)}`),a=true,s()),a&&s(`${t.yellow("\u2192")} Restart Claude Desktop to pick up the new server.
8
+ `),s(t.dim(`Tip: run devcoach backup to export your profile, lessons and settings.
9
+ run devcoach restore <file> to import a backup on a new machine.
10
+ After upgrading devcoach (npm, Homebrew), re-run devcoach install to refresh the skill.`));}function $o(){let e=i=>s(` ${t.green("\u2713")} ${i}`),o=i=>s(` ${t.yellow("\u2192")} ${i}`),n=i=>s(` ${t.red("\u2717")} ${i}`);s(`
11
+ ${t.bold("devcoach doctor")} ${t.dim(`v${g}`)}
12
+ `),s(t.bold("Environment")),Number.parseInt(process.versions.node.split(".")[0]??"0",10)>=24?e(`Node ${process.versions.node} (\u2265 24)`):n(`Node ${process.versions.node} \u2014 devcoach needs Node \u2265 24 (embedded node:sqlite)`),s(t.bold(`
13
+ Claude Code wiring`));let a=M(j);if(!a.ok)n(`${j} is not valid JSON \u2014 hooks cannot run`);else {let i=Ne(a.data),l=[];for(let[p,d]of Object.entries(a.data.hooks??{}))for(let c of d??[])for(let g of c.hooks??[])W(g.command??"")&&l.push({event:p,cmd:g});if(i&&l.length)n("devcoach hooks are registered TWICE (plugin + settings.json) \u2014 interactions are double-counted. Disable the plugin or remove the settings.json entries.");else if(i)e("hooks provided by the devcoach plugin");else if(!l.length)n(`no devcoach hooks in ${j} \u2014 run ${t.bold("devcoach install")}`);else {let p=l.filter(({cmd:c})=>c.command.includes("onboard-hook")||c.command.includes("lesson-ready")),d=l.filter(c=>c.event==="Stop").length;p.length?o(`legacy two-entry Stop layout \u2014 run ${t.bold("devcoach install")} to merge into one stop-hook entry (fewer spawns per stop)`):d>1?o(`${d} devcoach Stop entries \u2014 interactions are counted ${d}\xD7 per stop; run ${t.bold("devcoach install")} to keep only the current one`):e(`Stop hook wired (${d} entry)`),l.some(c=>c.event==="UserPromptSubmit")?e("UserPromptSubmit priming hook wired"):o(`no UserPromptSubmit priming hook \u2014 run ${t.bold("devcoach install")} to add it (lessons land more reliably)`);for(let{cmd:c}of l){let g=c.command.split(" ")[0]??"";g.startsWith("/")&&!existsSync(g)&&n(`hook command not found: ${g} \u2014 re-run ${t.bold("devcoach install")}`),c.command.startsWith("npx ")&&o("hook runs via npx \u2014 needs the npx cache (or network) on every stop"),c.timeout==null&&o(`hook entry has no timeout (${c.command})`);}}let m=J();m==="current"?e("Claude Code skill installed (current version)"):i?e("Claude Code skill bundled with the plugin"):o(m==="outdated"?`Claude Code skill is out of date \u2014 run ${t.bold("devcoach install")}`:`Claude Code skill not installed \u2014 run ${t.bold("devcoach install")}`);let u=M(join(homedir(),".claude.json"));u.ok&&u.data.mcpServers?.devcoach?e("MCP server registered (user scope)"):o(`MCP server not found in ~/.claude.json \u2014 it may be registered elsewhere (check with ${t.bold("claude mcp get devcoach")})`);}if(s(t.bold(`
14
+ Database & pacing`)),!existsSync(i)){o(`no database yet (${i}) \u2014 onboarding runs on the first technical task`),s();return}try{m(i$1=>{e(`database opens (${i})`),L$1(i$1).knowledge_ready?e("onboarding complete"):o("onboarding not complete \u2014 the next stop cues it");let l=F(i$1);s(` settings: max_per_day=${l.max_per_day} \xB7 min_gap_minutes=${l.min_gap_minutes} \xB7 nudge_every=${l.nudge_every} \xB7 nudge_scope=${l.nudge_scope}`);let m=H(i$1),u=m.reduce((g,b)=>g+b.interactions,0);s(` pacing: ${m.length} session(s) counted, ${u} interaction(s) total`+(m[0]?` \u2014 latest ${m[0].session_id.slice(0,8)}\u2026 at ${m[0].interactions}`:""));let p=J$1(i$1);p.pending?o(`a cue is pending since ${p.last_cue_at} (retry threshold armed)`):p.last_skip_reason&&s(` last skip: "${p.last_skip_reason}"`);let d=P(i$1);d.allowed?e("rate limit: allowed"):o(`rate limit: ${d.reason}`),s(t.bold(`
15
+ Verdict`));let c=W$1(i$1,m[0]?.session_id??null);c.wouldCue?e("the next eligible stop WOULD cue a lesson"):o("the next eligible stop would NOT cue a lesson:");for(let g of c.reasons)s(` \xB7 ${g}`);});}catch(i){n(`database check failed: ${i}`);}s();}async function wo(){let e=createInterface({input:process.stdin,output:process.stdout}),o=async(r,a="")=>{let i=a?` [${a}]`:"";return (await e.question(`${r}${i}: `)).trim()||a},n=async(r,a,i,l)=>{for(;;){let m=await o(r,String(a)),u=Number.parseInt(m,10);if(!Number.isNaN(u)&&u>=i&&u<=l)return u;s(t.red(`Must be ${i}\u2013${l}.`));}};try{s(E(t.bold("devcoach setup"))),s(`
16
+ ${t.bold("Step 1")} \u2014 Restore from backup`);let r=await o("Path to existing backup zip (Enter to skip)","");if(r){existsSync(r)||(s(t.red(`File not found: ${r}`)),process.exit(1));let d=m(c=>O(c,readFileSync(r)));s(`${t.green("\u2713")} Restored: ${d.topics} topics, ${d.lessons} lessons${d.learning_state?", notebook":""}`),s(t.green("Setup complete!"));return}s(`
17
+ ${t.bold("Step 2")} \u2014 Build your knowledge profile`);let a$1=(await o("Mode: [a]utomatic (detect from files) / [m]anual (type your stack)","a")).toLowerCase(),i={};if(a$1.startsWith("a")){let c$1=d$1().folder??process.cwd(),g=c(),b=b$1(a(c$1),g.detected_stack),k=Object.keys(b).sort((y,_)=>y.localeCompare(_));if(g.scanned_projects>0){s(`
18
+ ${t.dim(`Scanned ${g.scanned_projects} Claude Code project(s) from your history:`)}`);for(let y of g.projects.slice(0,8))s(t.dim(` ${y.name} \u2014 ${y.topics.join(", ")||"no stack signals"}`));}if(k.length){s(`
19
+ ${t.dim(`Detected (history + ${t.cyan(c$1)}):`)}`);for(let y of k){let _=b[y]??5,Y=await o(` ${t.cyan(y)} (Enter=keep, 0-10=override, s=skip)`,String(_));if(Y.toLowerCase()==="s")continue;let V=Number.parseInt(Y,10);i[y]=Number.isNaN(V)?_:Math.max(0,Math.min(10,V));}}else s(t.dim("No technology files detected in current directory."));let O=await o(`
20
+ Add additional topics (comma-separated, or Enter to skip)`,"");for(let y of O.split(",").map(_=>_.trim()).filter(Boolean))i[y]=await n(` Confidence for ${t.cyan(y)}`,5,0,10);}else for(s(t.dim(`
21
+ Enter topics: 'topic_id confidence' (e.g. python 7). Blank line when done.`));;){let d=(await o("Topic (Enter when done)","")).trim();if(!d)break;let c=d.split(/\s+/),g=c[0]??"",b=Number.parseInt(c[1]??"",10),k=Number.isNaN(b)?5:Math.max(0,Math.min(10,b));i[g]=k,s(` ${t.green("+")} ${t.cyan(g)} \u2192 ${k}`);}let l={};if(Object.keys(i).length&&(s(`
22
+ ${t.bold("Step 3")} \u2014 Organise into groups`),(await o("Organise topics into groups? [y/N]","n")).toLowerCase().startsWith("y"))){let c=[];for(let g of Object.keys(i).sort((b,k)=>b.localeCompare(k))){let b=c.length?c.join(", "):"(none yet)",k=await o(` Group for ${t.cyan(g)} existing: ${t.dim(b)} (Enter=Other)`,"");if(k&&k!=="Other"){let O=l[k]??[];O.push(g),l[k]=O,c.includes(k)||c.push(k);}}}s(`
23
+ ${t.bold("Step 4")} \u2014 Rate-limit settings`);let m$1=await n("Max lessons per day",2,1,20),u=await n("Min gap between lessons (minutes)",240,0,1440),p=m(d=>{d.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[c,g]of Object.entries(i))z(d,c,g);for(let[c,g]of Object.entries(l))for(let b of g)D(d,b,c);return G(d,"max_per_day",String(m$1)),G(d,"min_gap_minutes",String(u)),Q(d)});s(Oe(p,"Knowledge Profile")),s(`
24
+ ${t.green("Setup complete!")} ${Object.keys(i).length} topics saved.`);}finally{e.close();}}function vo(){let e=[["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 hooks + skill in Claude Code / Claude Desktop"],["onboard-hook","Claude Code Stop hook: cue onboarding when no profile exists"],["lesson-ready","Claude Code Stop hook: cue a lesson when one is due"],["doctor","Diagnose the Claude Code wiring \u2014 explains why a lesson would(n't) fire"],["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"]];s(`
25
+ ${t.bold(`devcoach ${t.dim(`v${g}`)}`)}`);for(let[n,r]of e)s(` ${t.cyan(`devcoach ${n}`.padEnd(32))} ${t.dim(r)}`);s(`
26
+ ${t.dim("Run")} ${t.bold("devcoach <command> --help")} ${t.dim("for per-command options.")}
27
+ `);let o=Ee();o&&s(`${o}
28
+ `);}function $(e){return e??null}function Co(){let e$1=new Command;return e$1.exitOverride(),e$1.name("devcoach").description("devcoach \u2014 progressive technical coaching").version(`devcoach ${g}`,"-v, --version","output the version number").allowExcessArguments(false).showHelpAfterError("(add --help for usage)").action(()=>vo()),e$1.command("mcp").description("Start the MCP server (stdio transport)").action(async()=>{let{runStdio:o}=await import('./server-MCBHUD25.js');await o();}),e$1.command("profile").description("Show the knowledge map").action(qe),e$1.command("lessons").description("List past lessons").option("--period <period>","today | week | month | year | all","all").option("--category <category>","Filter by category tag").option("--project <project>","Filter by project (fuzzy)").option("--repository <repository>","Filter by repository (fuzzy)").option("--branch <branch>","Filter by branch (fuzzy)").option("--commit <commit>","Filter by commit hash prefix (fuzzy)").option("--starred","Show only starred lessons").option("--feedback <feedback>","know | dont_know | none").option("--level <level>","junior | mid | senior").option("--date-from <date>","Show lessons on or after this date (YYYY-MM-DD[THH:MM])").option("--date-to <date>","Show lessons on or before this date (YYYY-MM-DD[THH:MM])").option("--sort <column>","timestamp | level | topic_id | title | feedback","timestamp").option("--order <order>","asc | desc","desc").action(o=>Xe({period:o.period??"all",category:$(o.category),project:$(o.project),repository:$(o.repository),branch:$(o.branch),commit:$(o.commit),starred:!!o.starred,feedback:$(o.feedback),level:$(o.level),dateFrom:$(o.dateFrom),dateTo:$(o.dateTo),sort:o.sort??"timestamp",order:o.order??"desc"})),e$1.command("lesson").description("Show a single lesson in detail").argument("<id>","Lesson ID").action(o=>Qe(o)),e$1.command("star").description("Mark a lesson as starred").argument("<id>","Lesson ID").action(o=>Le(o,true)),e$1.command("unstar").description("Remove the starred mark from a lesson").argument("<id>","Lesson ID").action(o=>Le(o,false)),e$1.command("delete").description("Permanently delete a lesson").argument("<id>","Lesson ID").action(o=>eo(o)),e$1.command("feedback").description("Record know/dont_know feedback for a lesson").argument("<id>","Lesson ID").argument("<value>","know | dont_know | clear").action((o,n)=>oo(o,n)),e$1.command("settings").description("Show current settings").action(to),e$1.command("stats").description("Coaching statistics and rate-limit status").action(no),e$1.command("set").description("Update a setting (max_per_day | min_gap_minutes)").argument("<key>","Setting key").argument("<value>","New value").action((o,n)=>ro(o,n)),e$1.command("knowledge-add").description("Add or update a topic in the knowledge map").argument("<topic>","Topic ID").option("--confidence <n>","Initial confidence 0-10","5").option("--group <group>","Assign to a named group").action((o,n)=>so(o,Math.max(0,Math.min(10,Number.parseInt(n.confidence??"5",10)||5)),$(n.group))),e$1.command("knowledge-remove").description("Remove a topic from the knowledge map").argument("<topic>","Topic ID").action(o=>io(o)),e$1.command("group-add").description("Register a new knowledge group").argument("<name>","Group name").action(o=>ao(o)),e$1.command("group-remove").description("Delete a knowledge group (topics move to Other)").argument("<name>","Group name").action(o=>co(o)),e$1.command("group-assign").description("Move a topic to a group").argument("<topic>","Topic ID").argument("<group>","Group name (use 'Other' to ungroup)").action((o,n)=>lo(o,n)),e$1.command("backup").description("Export a full backup (settings + knowledge + lessons) as zip").argument("[file]","Output zip path","devcoach-backup.zip").action(o=>uo(o)),e$1.command("restore").description("Restore from a backup zip file").argument("<file>","Path to backup zip").action(o=>po(o)),e$1.command("install").description("Register the MCP server + Stop hooks + skill 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(o=>yo({claudeCode:!!o.claudeCode,claudeDesktop:!!o.claudeDesktop,force:!!o.force,skipHook:!!o.skipHook})),e$1.command("doctor").description("Diagnose the devcoach \u21C4 Claude Code wiring and the lesson pacing state").action($o),e$1.command("ui").description("Launch the web dashboard").option("--port <port>","Port","7860").action(async o=>{let{startUi:n}=await import('./app-GO3HPOM6.js');n(Number.parseInt(o.port??"7860",10)||7860);}),e$1.command("setup").description("First-run wizard: import backup or build your knowledge profile").action(wo),e$1.command("stop-hook",{hidden:true}).description("Claude Code Stop hook: onboarding check + lesson cue in one spawn").action(()=>e()),e$1.command("prompt-hook",{hidden:true}).description("Claude Code UserPromptSubmit hook: prime the model when a lesson is due").action(()=>f()),e$1.command("onboard-hook",{hidden:true}).description("Claude Code Stop hook: cue onboarding (legacy two-entry layout)").action(()=>b()),e$1.command("lesson-ready",{hidden:true}).description("Claude Code Stop hook: signal a lesson is due (legacy two-entry layout)").action(()=>d()),e$1}async function Go(){let e=Co();try{await e.parseAsync(process.argv);}catch(o){let n=o.code??"";if(!n.startsWith("commander."))throw o;if(n==="commander.helpDisplayed"||n==="commander.help"||n==="commander.version")return;process.exit(2);}}export{Go as runCli};
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export{c as buildLessonCue,d as cmdLessonReady,b as cmdOnboardHook,f as cmdPromptHook,e as cmdStopHook,a as parseHookPayload,g as runHook}from'./chunk-JTR6RJFB.js';import'./chunk-227BHE2B.js';
2
+ export{c as buildLessonCue,d as cmdLessonReady,b as cmdOnboardHook,f as cmdPromptHook,e as cmdStopHook,a as parseHookPayload,g as runHook}from'./chunk-2ATV4VK3.js';import'./chunk-ECN4MNGM.js';
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import {g,d as d$1,c as c$2,b as b$1,a as a$1,f as f$1,e as e$1}from'./chunk-OX67ZDAD.js';import {c as c$1,b,a,m,M,g as g$1,n,I,s,R,p,r,q,K,d as d$2,z as z$1,D,A,B,C,f,e,Q,j,F,T,U,o,P,L,k,w,G}from'./chunk-ECN4MNGM.js';import {spawn}from'child_process';import {mkdirSync,writeFileSync,existsSync,readFileSync,statSync}from'fs';import {dirname}from'path';import {McpServer,ResourceTemplate}from'@modelcontextprotocol/sdk/server/mcp.js';import {StdioServerTransport}from'@modelcontextprotocol/sdk/server/stdio.js';import {z}from'zod';var l=r=>({type:"text",text:r}),ie=r=>({content:[l(JSON.stringify(r))]}),d=r=>({isError:true,content:[l(r)]}),se=r=>({content:[l(JSON.stringify(r))],structuredContent:r}),he={id:z.string(),timestamp:z.string(),topic_id:z.string(),categories:z.array(z.string()),title:z.string(),level:a,summary:z.string(),body:z.string().nullable(),task_context:z.string().nullable(),project:z.string().nullable(),repository:z.string().nullable(),branch:z.string().nullable(),commit_hash:z.string().nullable(),folder:z.string().nullable(),repository_platform:b.nullable(),starred:z.boolean(),feedback:c$1.nullable()},me={...he,reply_check:z.string()},ae="Saved \u2014 but saving does NOT display anything. The user sees ONLY plain text you write in your reply; the title/body inside this tool call's ARGUMENTS are invisible to them, and having composed them does not count as having shown the card. Did you already write the full card (both ### band headings + title line + body + tip) as plain reply text, outside any tool call? If yes: output nothing. If no: write the card now, as the final text of your reply. Never write it twice.",ge={knowledge:z.array(z.object({topic:z.string(),confidence:z.number().int()})),groups:z.array(z.object({name:z.string(),topics:z.array(z.string())}))},be={max_per_day:z.number().int(),min_gap_minutes:z.number().int(),ui_theme:e,nudge_every:z.number().int(),nudge_scope:f};function c(r,u){return {contents:[{uri:r.href,mimeType:"application/json",text:JSON.stringify(u)}]}}function fe(){let r$1=new McpServer({name:"devcoach",version:g},{instructions:"Progressive technical coaching server. Use the devcoach_instructions prompt for full coaching behaviour guidelines."});r$1.registerTool("log_lesson",{title:"Log Lesson",description:"Save a delivered lesson to the coaching log. Git metadata (project, repository, branch, commit_hash, folder, repository_platform) is auto-detected from the workspace when omitted. Returns the saved Lesson with all resolved fields.",inputSchema:{id:z.string().describe("Unique lesson id (uuid or random string)"),timestamp:z.string().describe("ISO 8601 timestamp; normalized to UTC and clamped to now"),topic_id:z.string().describe("Primary topic id, e.g. 'python'"),categories:z.array(z.string()).describe("Category tags, e.g. ['python','performance']"),title:z.string().describe("Short lesson title"),level:a.describe("Difficulty level: junior | mid | senior"),summary:z.string().describe("1-3 sentence summary shown in the lesson card"),body:z.string().min(1).describe("Clean lesson markdown: the prose + \u{1F4A1} Senior tip only \u2014 NO card bands, NO '>' blockquote, and do not repeat title/category/level (those are their own fields). Required, non-empty."),task_context:z.string().nullish().describe("What the user was doing when taught"),project:z.string().nullish().describe("Project name (auto-detected from git if omitted)"),repository:z.string().nullish().describe("org/repo or path (auto-detected if omitted)"),branch:z.string().nullish().describe("git branch (auto-detected if omitted)"),commit_hash:z.string().nullish().describe("git commit hash (auto-detected if omitted)"),folder:z.string().nullish().describe("workspace folder (auto-detected if omitted)"),repository_platform:b.nullish().describe("github | gitlab | bitbucket | local (auto-detected if omitted)")},outputSchema:me,annotations:{title:"Log Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{let t=d$1(),o={};try{o=m(s=>M(s));}catch{o={};}let i=g$1({id:e.id,timestamp:e.timestamp,topic_id:e.topic_id,categories:e.categories,title:e.title,level:e.level,summary:e.summary,body:e.body,task_context:e.task_context??null,project:e.project??t.project??o.project??null,repository:e.repository??t.repository??o.repository??null,branch:e.branch??t.branch??o.branch??null,commit_hash:e.commit_hash??t.commit_hash??null,folder:e.folder??t.folder??null,repository_platform:e.repository_platform??t.repository_platform??o.repository_platform??null});m(s=>{n(s,i),I(s);});try{if(r$1.server.getClientCapabilities()?.elicitation){let h=await r$1.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:[]}}),p=h.action==="accept"?h.content?.feedback:void 0;p&&p!=="skip"&&(m(g=>{s(g,e.id,p),R(g,e.topic_id,p==="know"?1:-1);}),i={...i,feedback:p});}}catch{}return {content:[l(`Lesson saved. ${ae}`),l(JSON.stringify(i))],structuredContent:{...i,reply_check:ae}}}catch(t){return d(`log_lesson failed: ${t}`)}}),r$1.registerTool("update_knowledge",{title:"Update Knowledge",description:"Adjust the confidence score for a topic by delta (e.g. +1 or -1). Returns the new confidence (0-10). Creates the topic at confidence 5 if it does not exist.",inputSchema:{topic:z.string().describe("Topic id, e.g. 'python'"),delta:z.number().int().describe("Signed change to apply, e.g. +1 or -1")},annotations:{title:"Update Knowledge",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{let t=m(o=>R(o,e.topic,e.delta));return {content:[l(String(t))]}}catch(t){return d(`update_knowledge failed for '${e.topic}': ${t}`)}}),r$1.registerTool("get_lessons",{title:"Get Lessons",description:"Query the coaching lesson history. All filters combine. period defaults to all; date_from/date_to override period; limit caps results newest-first (0 = all).",inputSchema:{period:z.enum(["today","week","month","year","all"]).nullish().describe("Time window"),category:z.string().nullish().describe("Filter by a category tag, e.g. 'python'"),level:z.enum(["junior","mid","senior"]).nullish().describe("Filter by difficulty level"),project:z.string().nullish().describe("Fuzzy match on git project"),repository:z.string().nullish().describe("Fuzzy match on git repository"),branch:z.string().nullish().describe("Fuzzy match on git branch"),commit:z.string().nullish().describe("Fuzzy match on commit hash"),starred:z.boolean().nullish().describe("True to return only starred lessons"),feedback:z.enum(["know","dont_know","none"]).nullish().describe("Filter by feedback ('none' = no response)"),search:z.string().nullish().describe("Full-text search over title, topic_id, summary, body"),date_from:z.string().nullish().describe("ISO date/datetime lower bound"),date_to:z.string().nullish().describe("ISO date/datetime upper bound (date-only = end-of-day)"),limit:z.number().int().default(10).describe("Max lessons, newest first. Pass 0 for all.")},annotations:{title:"Get Lessons",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}},e=>{try{let t=m(o=>p(o,{period:e.period,category:e.category,level:e.level,project:e.project,repository:e.repository,branch:e.branch,commit:e.commit,starred:e.starred,feedback:e.feedback,search:e.search,date_from:e.date_from,date_to:e.date_to,page:e.limit>0?1:null,per_page:e.limit}));return ie(t)}catch{return ie([])}}),r$1.registerTool("star_lesson",{title:"Star Lesson",description:"Set the starred (favourite) flag on a lesson. Returns true if found and updated. Idempotent.",inputSchema:{lesson_id:z.string().describe("Lesson id"),starred:z.boolean().describe("true to favourite, false to unmark")},annotations:{title:"Star Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{let t=m(o=>r(o,e.lesson_id,e.starred));return {content:[l(String(t))]}}catch(t){return d(`star_lesson failed for '${e.lesson_id}': ${t}`)}}),r$1.registerTool("delete_lesson",{title:"Delete Lesson",description:"Permanently delete a lesson by id. Returns true if found and deleted.",inputSchema:{lesson_id:z.string().describe("Lesson id to delete")},annotations:{title:"Delete Lesson",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let t=m(o=>q(o,e.lesson_id));return {content:[l(String(t))]}}catch(t){return d(`delete_lesson failed for '${e.lesson_id}': ${t}`)}}),r$1.registerTool("submit_feedback",{title:"Submit Feedback",description:"Record comprehension feedback for a lesson and adjust knowledge confidence. know = +1, dont_know = -1, clear = remove feedback (no confidence change). Idempotent \u2014 the same feedback twice adjusts confidence only once.",inputSchema:{lesson_id:z.string().describe("Lesson id"),feedback:z.enum(["know","dont_know","clear"]).describe("know | dont_know | clear")},annotations:{title:"Submit Feedback",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{let t=e.feedback==="clear"?null:e.feedback,o=m(i=>{let s$1=i.prepare("SELECT feedback, topic_id FROM lessons WHERE id = ?").get(e.lesson_id);return s$1?(s$1.feedback===t||(s(i,e.lesson_id,t),(t==="know"||t==="dont_know")&&s$1.topic_id&&R(i,s$1.topic_id,t==="know"?1:-1)),!0):!1});return {content:[l(String(o))]}}catch(t){return d(`submit_feedback failed for '${e.lesson_id}': ${t}`)}}),r$1.registerTool("skip_lesson",{title:"Skip Lesson",description:"Decline a lesson cue: call this when the coaching hook asked for a lesson but the completed work does not warrant one (pure questions, chat, nothing technical). Re-arms the pacing counter so the cue is not repeated immediately. Never call it after delivering a lesson \u2014 log_lesson already resolves the cue.",inputSchema:{reason:z.string().min(1).describe("One line: why no lesson was warranted (shown in `devcoach doctor`)")},annotations:{title:"Skip Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{return m(t=>K(t,e.reason)),{content:[l("Cue skipped \u2014 pacing re-armed. Output nothing to the user.")]}}catch(t){return d(`skip_lesson failed: ${t}`)}}),r$1.registerTool("add_topic",{title:"Add Topic",description:"Add a topic to the knowledge map, or update its confidence if it exists. Optionally assign to a group (auto-created). Prefer a single-word topic id. Idempotent.",inputSchema:{topic:z.string().describe("Topic id \u2014 prefer a single word, max 3 words"),confidence:d$2.default(5).describe("Initial confidence 0-10 (default 5)"),group:z.string().nullish().describe("Optional group name; 'Other' if omitted")},annotations:{title:"Add Topic",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{return m(t=>{z$1(t,e.topic,e.confidence),e.group&&e.group!=="Other"&&D(t,e.topic,e.group);}),{content:[l("true")]}}catch(t){return d(`add_topic failed for '${e.topic}': ${t}`)}}),r$1.registerTool("remove_topic",{title:"Remove Topic",description:"Remove a topic from the knowledge map entirely. Returns true if it existed.",inputSchema:{topic:z.string().describe("Topic id to remove")},annotations:{title:"Remove Topic",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let t=m(o=>A(o,e.topic));return {content:[l(String(t))]}}catch(t){return d(`remove_topic failed for '${e.topic}': ${t}`)}}),r$1.registerTool("add_group",{title:"Add Group",description:"Create a new (initially empty) knowledge group. Idempotent \u2014 returns true either way.",inputSchema:{name:z.string().describe("Group name, e.g. 'Machine Learning'")},annotations:{title:"Add Group",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{return m(t=>B(t,e.name.trim())),{content:[l("true")]}}catch(t){return d(`add_group failed for '${e.name}': ${t}`)}}),r$1.registerTool("remove_group",{title:"Remove Group",description:"Delete a knowledge group. Its topics move to Other. Returns true if it existed.",inputSchema:{name:z.string().describe("Group name to delete")},annotations:{title:"Remove Group",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let t=m(o=>C(o,e.name));return {content:[l(String(t))]}}catch(t){return d(`remove_group failed for '${e.name}': ${t}`)}}),r$1.registerTool("update_settings",{title:"Update Settings",description:"Update a coaching setting. max_per_day: integer 1-20. min_gap_minutes: integer 0-1440 (0 = no cooldown). nudge_every: integer 0-1000 interactions between lesson cues (0 = cue every turn). nudge_scope: 'session' | 'global'. Returns the full updated Settings.",inputSchema:{key:z.enum(["max_per_day","min_gap_minutes","nudge_every","nudge_scope"]).describe("Setting key"),value:z.string().describe("New value (integer string; or 'session'|'global' for nudge_scope)")},outputSchema:be,annotations:{title:"Update Settings",destructiveHint:false,idempotentHint:true,openWorldHint:false}},e=>{let t=i=>se(m(s=>(G(s,e.key,i),F(s))));if(e.key==="nudge_scope")return e.value!=="session"&&e.value!=="global"?d("nudge_scope must be 'session' or 'global'"):t(e.value);let o=Number.parseInt(e.value,10);return Number.isNaN(o)?d(`Value must be an integer, got '${e.value}'`):e.key==="max_per_day"&&!(o>=1&&o<=20)?d("max_per_day must be between 1 and 20"):e.key==="min_gap_minutes"&&!(o>=0&&o<=1440)?d("min_gap_minutes must be between 0 and 1440"):e.key==="nudge_every"&&!(o>=0&&o<=1e3)?d("nudge_every must be between 0 and 1000"):t(String(o))}),r$1.registerTool("open_ui",{title:"Open UI",description:"Launch the devcoach web dashboard in the background. port must be 1024-65535.",inputSchema:{port:z.number().int().default(7860).describe("Port (default 7860)")},annotations:{title:"Open UI",destructiveHint:false,idempotentHint:false,openWorldHint:true}},e=>{let t=e.port;return t>=1024&&t<=65535?(spawn(process.execPath,[process.argv[1]??"","ui","--port",String(t)],{detached:true,stdio:"ignore"}).unref(),{content:[l(`devcoach UI starting at http://localhost:${t}`)]}):{content:[l(`error: port ${t} is out of valid range (1024-65535)`)]}}),r$1.registerTool("complete_onboarding",{title:"Complete Onboarding",description:"Save the user's initial knowledge profile and mark onboarding complete. Wipes any default-seeded profile. topics: {topic_id: confidence_0_to_10}. groups: {group_name: [topic_id,...]} (topics not in any group go to 'Other'). Pass the personalized coaching notebook as `notebook`; it is saved to learning-state.md atomically with the profile. Returns the updated Profile.",inputSchema:{topics:z.record(z.string(),d$2).describe("{topic_id: confidence 0-10}"),groups:z.record(z.string(),z.array(z.string())).optional().describe("{group_name: [topic_id, ...]}"),notebook:z.string().optional().describe("Full personalized coaching notebook (markdown) saved verbatim to learning-state.md. Write real observations about THIS user \u2014 background, strengths, gaps, and how they work across ALL their projects (not just this one), per the template in the devcoach skill. If omitted, a minimal placeholder is written so the file is never empty.")},outputSchema:ge,annotations:{title:"Complete Onboarding",destructiveHint:true,idempotentHint:false,openWorldHint:false}},async e=>{try{let t=m(i=>{i.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[s,h]of Object.entries(e.topics))z$1(i,s,Math.max(0,Math.min(10,h)));if(e.groups)for(let[s,h]of Object.entries(e.groups))for(let p of h)p in e.topics&&D(i,p,s);return Q(i)}),o=e.notebook?.trim();return mkdirSync(dirname(j),{recursive:!0}),o?writeFileSync(j,o.endsWith(`
3
+ `)?o:`${o}
4
+ `):existsSync(j)||writeFileSync(j,`# devcoach \u2014 Coaching Notebook
5
+ `),se(t)}catch(t){return d(`complete_onboarding failed: ${t}`)}}),r$1.registerTool("update_notebook",{title:"Update Coaching Notebook",description:"Overwrite the coaching notebook (~/.devcoach/learning-state.md) with revised markdown. Use it after a lesson to fold in what the user absorbed or struggled with, new recurring patterns, and updated hypotheses \u2014 keeping the prior notes. Pass the full notebook content.",inputSchema:{notebook:z.string().min(1).describe("Full notebook markdown saved verbatim to learning-state.md (overwrites).")},annotations:{title:"Update Coaching Notebook",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{return mkdirSync(dirname(j),{recursive:!0}),writeFileSync(j,e.notebook.endsWith(`
6
+ `)?e.notebook:`${e.notebook}
7
+ `),{content:[l("notebook updated")]}}catch(t){return d(`update_notebook failed: ${t}`)}});let u=(e,t)=>({title:e,description:t,mimeType:"application/json"});return r$1.registerResource("profile","devcoach://profile",u("Knowledge Profile","Current knowledge map \u2014 topics, confidence scores, and groups."),e=>{try{return c(e,m(t=>Q(t)))}catch(t){return c(e,{error:String(t)})}}),r$1.registerResource("notebook","devcoach://notebook",{title:"Coaching Notebook",description:"The coaching notebook (learning-state.md): observations, recurring patterns, recommended focus, and open hypotheses about the user. Read it to decide what to teach.",mimeType:"text/markdown"},e=>{let t="";try{existsSync(j)&&(t=readFileSync(j,"utf8"));}catch{}return {contents:[{uri:e.href,mimeType:"text/markdown",text:t}]}}),r$1.registerResource("settings","devcoach://settings",u("Settings","Current coaching settings (rate limits, UI theme)."),e=>{try{return c(e,m(t=>F(t)))}catch(t){return c(e,{error:String(t)})}}),r$1.registerResource("recent-lessons","devcoach://lessons/recent",u("Recent Lessons","Last 10 lessons from the current week."),e=>{try{return c(e,m(t=>p(t,{period:"week",page:1,per_page:10})))}catch(t){return c(e,[{error:String(t)}])}}),r$1.registerResource("stats","devcoach://stats",u("Stats","Aggregate coaching statistics: counts and weakest/strongest topics."),e=>{try{return c(e,m(t=>T(t)))}catch(t){return c(e,{error:String(t)})}}),r$1.registerResource("taught-topics","devcoach://taught-topics",u("Taught Topics","All topic_ids already taught \u2014 read before selecting a new lesson topic."),e=>{try{return c(e,m(t=>U(t)))}catch{return c(e,[])}}),r$1.registerResource("rate-limit","devcoach://rate-limit",u("Rate Limit","Current rate-limit status and running lesson total."),e=>{try{let{r:t,total:o$1}=m(s=>({r:P(s),total:o(s)})),i={allowed:t.allowed,total_lessons:o$1};return t.reason!=null&&(i.reason=t.reason),c(e,i)}catch(t){return c(e,{allowed:false,reason:`Rate limit check unavailable: ${t}`})}}),r$1.registerResource("briefing","devcoach://briefing",u("Lesson Briefing","Everything needed before delivering a lesson in ONE read: onboarding status, rate limit, taught topics, knowledge profile, and the coaching notebook."),e=>{try{let t="";try{existsSync(j)&&(t=readFileSync(j,"utf8"));}catch{}let o$1=m(i=>{let s=L(i).knowledge_ready,h=t.length>0,p=P(i),g={allowed:p.allowed,total_lessons:o(i)};return p.reason!=null&&(g.reason=p.reason),{onboarding:{knowledge_ready:s,notebook_ready:h,needs_onboarding:!(s&&h)},rate_limit:g,taught_topics:U(i),profile:Q(i)}});return c(e,{...o$1,notebook:t})}catch(t){return c(e,{error:String(t)})}}),r$1.registerResource("context","devcoach://context",u("Workspace Context","Auto-detected git context and most-used lesson metadata defaults."),e=>{try{let t=d$1(),o=m(i=>M(i));return c(e,{git:t,usage_defaults:o})}catch(t){return c(e,{error:String(t)})}}),r$1.registerResource("onboarding","devcoach://onboarding",u("Onboarding","Onboarding status, the stack detected across the full Claude Code history (with per-project provenance), and project topic defaults."),e=>{try{let o=m(g=>L(g)).knowledge_ready,i=existsSync(j)&&statSync(j).size>0,s=d$1(),h=c$2(),p=b$1(a$1(s.folder??process.cwd()),h.detected_stack);return c(e,{knowledge_ready:o,notebook_ready:i,needs_onboarding:!(o&&i),detected_stack:p,detected_projects:h.projects,scanned_projects:h.scanned_projects,default_topics:k,context_ready:s.branch!==null})}catch(t){return c(e,{error:String(t)})}}),r$1.registerResource("lesson",new ResourceTemplate("devcoach://lessons/{lesson_id}",{list:void 0}),u("Lesson","A single lesson by id."),(e,t)=>{try{let o=String(t.lesson_id),i=m(s=>w(s,o));return i?c(e,i):c(e,{error:`Lesson '${o}' not found`})}catch(o){return c(e,{error:String(o)})}}),r$1.registerPrompt("devcoach_instructions",{title:"devcoach coaching instructions",description:"Full coaching instructions for the devcoach skill (SKILL.md plus its reference files)."},()=>{let e=f$1().map(t=>`
8
+
9
+ ---
10
+
11
+ <!-- reference: ${t.name} -->
12
+
13
+ ${t.content}`).join("");return {messages:[{role:"user",content:{type:"text",text:e$1()+e}}]}}),r$1}async function Oe(){let r=fe(),u=new StdioServerTransport;await r.connect(u);}export{fe as createServer,Oe as runStdio};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devcoach",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "mcpName": "io.github.UltimaPhoenix/devcoach",
5
5
  "description": "Stay sharp while your AI does the work — learn from every task with in-context coaching, all local, all free",
6
6
  "license": "AGPL-3.0-only",
@@ -67,7 +67,7 @@
67
67
  "@vitest/coverage-v8": "^4.1.9",
68
68
  "tsup": "^8.5.1",
69
69
  "tsx": "^4.22.4",
70
- "typescript": "^6.0.3",
70
+ "typescript": "^7.0.2",
71
71
  "vitest": "^4.1.9"
72
72
  }
73
73
  }
@@ -1,69 +0,0 @@
1
- #!/usr/bin/env node
2
- import {z as z$1}from'zod';import {existsSync,readFileSync,mkdirSync,writeFileSync}from'fs';import {createRequire}from'module';import {homedir}from'os';import {join,dirname}from'path';import {strToU8,zipSync,unzipSync,strFromU8}from'fflate';var se=z$1.enum(["junior","mid","senior"]),oe=z$1.enum(["github","gitlab","bitbucket","local"]),re=z$1.enum(["know","dont_know"]),je=z$1.number().int().min(0).max(10);function ie(e){return e.toISOString().replace(/\.\d{3}Z$/,"Z")}function ae(e){if(e instanceof Date){if(Number.isNaN(e.getTime()))throw new TypeError("Invalid Date");return e}let t=e.trim();/[zZ]$|[+-]\d{2}:?\d{2}$/.test(t)||(t=t.includes("T")||t.includes(" ")?`${t.replace(" ","T")}Z`:`${t}T00:00:00Z`);let s=new Date(t);if(Number.isNaN(s.getTime()))throw new TypeError(`Cannot parse timestamp ${JSON.stringify(e)} \u2014 expected ISO 8601`);return s}function ce(e){let t=ae(e),n=new Date;return t.getTime()>n.getTime()&&(t=n),ie(t)}var ue=z$1.union([z$1.string(),z$1.date()]).transform((e,t)=>{try{return ce(e)}catch(n){return t.addIssue({code:"custom",message:n.message}),z$1.NEVER}}),T=z$1.string().nullish().transform(e=>e??null),le=z$1.object({id:z$1.string(),timestamp:ue,topic_id:z$1.string(),categories:z$1.array(z$1.string()),title:z$1.string(),level:se,summary:z$1.string(),body:T,task_context:T,project:T,repository:T,branch:T,commit_hash:T,folder:T,repository_platform:oe.nullish().transform(e=>e??null),starred:z$1.boolean().default(false),feedback:re.nullish().transform(e=>e??null)}),pe=z$1.object({topic:z$1.string(),confidence:z$1.number().int()}),ge=z$1.object({name:z$1.string(),topics:z$1.array(z$1.string())});z$1.object({knowledge:z$1.array(pe),groups:z$1.array(ge)});var de=z$1.enum(["system","dark","light"]),me=z$1.enum(["session","global"]);z$1.object({max_per_day:z$1.number().int().default(2),min_gap_minutes:z$1.number().int().default(240),ui_theme:de.default("system"),nudge_every:z$1.number().int().min(0).default(10),nudge_scope:me.default("session")});function O(e){return le.parse(e)}var {DatabaseSync:Ne}=createRequire(import.meta.url)("node:sqlite"),B=process.env.DEVCOACH_DIR??join(homedir(),".devcoach"),C=join(B,"coaching.db"),R=join(B,"learning-state.md"),b="settings.json",h="lessons.json",w="knowledge.json",x="learning-state.md",nt={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},Le={max_per_day:"2",min_gap_minutes:"240",ui_theme:"system",nudge_every:"10",nudge_scope:"session"},De=50;function m(e,t,...n){return e.prepare(t).all(...n)}function g(e,t,...n){return e.prepare(t).get(...n)}function l(e,t,...n){return Number(e.prepare(t).run(...n).changes)}function Oe(e=C){e!==":memory:"&&mkdirSync(dirname(e),{recursive:true});let t=new Ne(e);return t.exec("PRAGMA busy_timeout = 3000"),t}var X=1;function be(e=C){let t=Oe(e),n=t.prepare("PRAGMA user_version").get();return Number(n?.user_version??0)!==X&&(he(t),t.exec(`PRAGMA user_version = ${X}`)),t}function st(e,t){e.exec("BEGIN IMMEDIATE");try{let n=t();return e.exec("COMMIT"),n}catch(n){try{e.exec("ROLLBACK");}catch{}throw n}}function ot(e,t=C){let n=be(t);try{return e(n)}finally{n.close();}}function he(e){e.exec(`
3
- CREATE TABLE IF NOT EXISTS lessons (
4
- id TEXT PRIMARY KEY,
5
- timestamp TEXT NOT NULL,
6
- topic_id TEXT NOT NULL,
7
- categories TEXT NOT NULL,
8
- title TEXT NOT NULL,
9
- level TEXT NOT NULL,
10
- summary TEXT NOT NULL,
11
- body TEXT,
12
- task_context TEXT,
13
- project TEXT,
14
- repository TEXT,
15
- branch TEXT,
16
- commit_hash TEXT,
17
- folder TEXT,
18
- feedback TEXT,
19
- repository_platform TEXT,
20
- starred INTEGER NOT NULL DEFAULT 0
21
- );
22
-
23
- CREATE TABLE IF NOT EXISTS knowledge (
24
- topic TEXT PRIMARY KEY,
25
- confidence INTEGER NOT NULL DEFAULT 5,
26
- updated_at TEXT NOT NULL
27
- );
28
-
29
- CREATE TABLE IF NOT EXISTS settings (
30
- key TEXT PRIMARY KEY,
31
- value TEXT NOT NULL
32
- );
33
-
34
- CREATE TABLE IF NOT EXISTS knowledge_group_names (
35
- group_name TEXT PRIMARY KEY
36
- );
37
-
38
- CREATE TABLE IF NOT EXISTS knowledge_groups (
39
- group_name TEXT NOT NULL,
40
- topic TEXT NOT NULL,
41
- PRIMARY KEY (group_name, topic)
42
- );
43
-
44
- -- Runtime-only: per-session interaction counter for lesson-cue pacing.
45
- -- Never exported/imported (backup carries config, not this state).
46
- CREATE TABLE IF NOT EXISTS nudge_state (
47
- session_id TEXT PRIMARY KEY,
48
- interactions INTEGER NOT NULL DEFAULT 0,
49
- updated_at TEXT NOT NULL
50
- );
51
-
52
- -- Runtime-only, single row: cue lifecycle. pending=1 between an emitted cue and
53
- -- its resolution (log_lesson or skip_lesson), arming a shorter retry threshold.
54
- -- display_pending=1 between log_lesson and the next stop, where the hook verifies
55
- -- the lesson card is actually visible (last_assistant_message) and recovers it.
56
- CREATE TABLE IF NOT EXISTS cue_state (
57
- id INTEGER PRIMARY KEY CHECK (id = 1),
58
- pending INTEGER NOT NULL DEFAULT 0,
59
- last_cue_at TEXT,
60
- last_skip_reason TEXT,
61
- display_pending INTEGER NOT NULL DEFAULT 0
62
- );
63
-
64
- CREATE INDEX IF NOT EXISTS idx_lessons_timestamp ON lessons (timestamp);
65
- CREATE INDEX IF NOT EXISTS idx_lessons_starred_ts ON lessons (starred, timestamp);
66
- CREATE INDEX IF NOT EXISTS idx_lessons_feedback ON lessons (feedback);
67
- CREATE INDEX IF NOT EXISTS idx_lessons_topic_id ON lessons (topic_id);
68
- `),we(e),xe(e);}function we(e){try{e.exec("ALTER TABLE lessons ADD COLUMN body TEXT");}catch{}try{e.exec("ALTER TABLE cue_state ADD COLUMN display_pending INTEGER NOT NULL DEFAULT 0");}catch{}}function xe(e){for(let[t,n]of Object.entries(Le))l(e,"INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)",t,n);}var Y="(id, timestamp, topic_id, categories, title, level, summary, body, task_context, project, repository, branch, commit_hash, folder, repository_platform, starred, feedback)",j="(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";function W(e){return [e.id,e.timestamp,e.topic_id,JSON.stringify(e.categories),e.title,e.level,e.summary,e.body,e.task_context,e.project,e.repository,e.branch,e.commit_hash,e.folder,e.repository_platform,e.starred?1:0,e.feedback]}function rt(e,t){l(e,`INSERT OR REPLACE INTO lessons ${Y} VALUES ${j}`,...W(t));}var Ie=[["project","project"],["repository","repository"],["branch","branch"],["commit","commit_hash"]];function Ce(e,t){if(e.date_from==null&&e.date_to==null){let n=Ge(e.period??null);n!=null&&t("timestamp >= ?",n);return}if(e.date_from!=null&&t("timestamp >= ?",e.date_from),e.date_to!=null){let n=e.date_to.includes("T")||e.date_to.includes(" ");t("timestamp <= ?",n?e.date_to:`${e.date_to}T23:59:59`);}}function z(e){let t=[],n=[],s=(r,...i)=>{t.push(r),n.push(...i);};Ce(e,s),e.category!=null&&s("categories LIKE ?",`%"${e.category}"%`),e.level!=null&&s("level = ?",e.level);for(let[r,i]of Ie){let c=e[r];c!=null&&s(`${i} LIKE ?`,`%${c}%`);}if(e.starred!=null&&s("starred = ?",e.starred?1:0),e.search!=null){let r=`%${e.search}%`;s("(title LIKE ? OR topic_id LIKE ? OR summary LIKE ? OR body LIKE ?)",r,r,r,r);}return e.feedback==="none"?s("feedback IS NULL"):e.feedback!=null&&s("feedback = ?",e.feedback),{where:t.length?`WHERE ${t.join(" AND ")}`:"",params:n}}function q(e,t={}){let{where:n,params:s}=z(t),o=g(e,`SELECT COUNT(*) AS n FROM lessons ${n}`,...s);return Number(o?.n??0)}var Ae=new Set(["timestamp","level","topic_id","title","feedback"]),ke=new Set(["project","repository","branch","commit_hash","repository_platform"]);function it(e,t={}){let{where:n,params:s}=z(t),o=t.sort&&Ae.has(t.sort)?t.sort:"timestamp",r=(t.order??"desc").toLowerCase()==="asc"?"ASC":"DESC",i=`SELECT * FROM lessons ${n} ORDER BY ${o} ${r}`,c=[...s];if(t.page!=null){let u=t.per_page??25;i+=" LIMIT ? OFFSET ?",c.push(u,(t.page-1)*u);}return m(e,i,...c).map($)}function at(e,t){return l(e,"DELETE FROM lessons WHERE id = ?",t)>0}function ct(e,t,n){return l(e,"UPDATE lessons SET starred = ? WHERE id = ?",n?1:0,t)>0}function V(e,t,n){l(e,"UPDATE lessons SET feedback = ? WHERE id = ?",n||null,t);let s=g(e,"SELECT topic_id FROM lessons WHERE id = ?",t);return s?s.topic_id:null}function Ue(e){return m(e,"SELECT * FROM lessons ORDER BY timestamp DESC").map($)}function Fe(e,t){let n=0,s=0,o=0;for(let r of t){let i;try{i=O(r);}catch{o+=1;continue}l(e,`INSERT OR IGNORE INTO lessons ${Y} VALUES ${j}`,...W(i))>0?n+=1:s+=1;}return {inserted:n,duplicated:s,invalid:o}}function ut(e,t){if(!ke.has(t))throw new Error(`Column not allowed: ${JSON.stringify(t)}`);return m(e,`SELECT DISTINCT ${t} FROM lessons WHERE ${t} IS NOT NULL ORDER BY ${t}`).map(s=>s[t])}function lt(e,t){let n=g(e,"SELECT * FROM lessons WHERE id = ?",t);return n?$(n):null}function pt(e){return m(e,"SELECT DISTINCT value FROM lessons, json_each(lessons.categories) ORDER BY value").map(n=>n.value)}function Z(e){return m(e,"SELECT DISTINCT topic_id FROM lessons").map(t=>t.topic_id)}function D(e,t){let n=g(e,"SELECT COUNT(*) AS n FROM lessons WHERE timestamp >= ?",t);return Number(n?.n??0)}function J(e){let t=g(e,"SELECT timestamp FROM lessons ORDER BY timestamp DESC LIMIT 1");return t?t.timestamp:null}function A(e){let t={};for(let n of m(e,"SELECT topic, confidence FROM knowledge"))t[n.topic]=Number(n.confidence);return t}function k(e){return m(e,"SELECT topic, confidence FROM knowledge ORDER BY topic").map(t=>({topic:t.topic,confidence:Number(t.confidence)}))}function U(e){let t=m(e,"SELECT group_name FROM knowledge_group_names ORDER BY group_name").map(s=>s.group_name),n=new Map;for(let s of m(e,"SELECT group_name, topic FROM knowledge_groups ORDER BY group_name, topic")){let o=s.group_name,r=n.get(o)??[];r.push(s.topic),n.set(o,r);}return t.map(s=>({name:s,topics:n.get(s)??[]}))}function N(e,t,n){let s=Math.max(0,Math.min(10,n)),o=new Date().toISOString();l(e,`INSERT INTO knowledge (topic, confidence, updated_at) VALUES (?, ?, ?)
69
- ON CONFLICT(topic) DO UPDATE SET confidence = excluded.confidence, updated_at = excluded.updated_at`,t,s,o);}function gt(e,t){let n=l(e,"DELETE FROM knowledge WHERE topic = ?",t);return l(e,"DELETE FROM knowledge_groups WHERE topic = ?",t),n>0}function G(e,t){let n=t.trim();if(!n)throw new Error("Group name must not be empty");return l(e,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",n)>0}function dt(e,t){return l(e,"DELETE FROM knowledge_groups WHERE group_name = ?",t),l(e,"DELETE FROM knowledge_group_names WHERE group_name = ?",t)>0}function ve(e,t,n){l(e,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",n),l(e,"DELETE FROM knowledge_groups WHERE topic = ?",t),l(e,"INSERT OR IGNORE INTO knowledge_groups (group_name, topic) VALUES (?, ?)",n,t);}function mt(e,t){l(e,"DELETE FROM knowledge_groups WHERE topic = ?",t);}function f(e){let t={};for(let d of m(e,"SELECT key, value FROM settings"))t[d.key]=d.value;let n=t.min_gap_minutes,s=t.min_hours_between,o;n!==void 0?o=Number.parseInt(n,10):s!==void 0?o=Number.parseInt(s,10)*60:o=240;let r=t.max_per_day,i=t.ui_theme??"system",c=i==="dark"||i==="light"||i==="system"?i:"system",u=t.nudge_every,p=u!==void 0?Number.parseInt(u,10):10,_=t.nudge_scope==="global"?"global":"session";return {max_per_day:r!==void 0?Number.parseInt(r,10):2,min_gap_minutes:o,ui_theme:c,nudge_every:Number.isFinite(p)&&p>=0?p:10,nudge_scope:_}}function S(e,t,n){l(e,"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",t,n);}function Q(e,t,n){let s=g(e,"INSERT INTO nudge_state (session_id, interactions, updated_at) VALUES (?, 1, ?) ON CONFLICT(session_id) DO UPDATE SET interactions = interactions + 1, updated_at = excluded.updated_at RETURNING interactions",t,new Date().toISOString()),o=Number(s?.interactions??0);return o===1&&l(e,"DELETE FROM nudge_state WHERE session_id NOT IN (SELECT session_id FROM nudge_state ORDER BY updated_at DESC LIMIT ?)",De),n==="global"?F(e,t,n):o}function Et(e){return m(e,"SELECT session_id, interactions, updated_at FROM nudge_state ORDER BY updated_at DESC").map(t=>({session_id:String(t.session_id),interactions:Number(t.interactions),updated_at:String(t.updated_at)}))}function F(e,t,n){let s=n==="global"?g(e,"SELECT COALESCE(SUM(interactions), 0) AS n FROM nudge_state"):g(e,"SELECT interactions AS n FROM nudge_state WHERE session_id = ?",t);return Number(s?.n??0)}function Tt(e){l(e,"DELETE FROM nudge_state"),l(e,"UPDATE cue_state SET pending = 0 WHERE id = 1");}var v=3;function M(e){let t=g(e,"SELECT pending, last_cue_at, last_skip_reason, display_pending FROM cue_state WHERE id = 1");return {pending:Number(t?.pending??0)===1,last_cue_at:t?.last_cue_at??null,last_skip_reason:t?.last_skip_reason??null,display_pending:Number(t?.display_pending??0)===1}}function ee(e){l(e,"INSERT INTO cue_state (id, pending, last_cue_at) VALUES (1, 1, ?) ON CONFLICT(id) DO UPDATE SET pending = 1, last_cue_at = excluded.last_cue_at",new Date().toISOString()),l(e,"DELETE FROM nudge_state");}function _t(e,t){l(e,"INSERT INTO cue_state (id, pending, last_skip_reason) VALUES (1, 0, ?) ON CONFLICT(id) DO UPDATE SET pending = 0, last_skip_reason = excluded.last_skip_reason",t??null),l(e,"DELETE FROM nudge_state");}function St(e){l(e,"INSERT INTO cue_state (id, pending, display_pending) VALUES (1, 0, 1) ON CONFLICT(id) DO UPDATE SET display_pending = 1");}function ft(e){let t=g(e,"SELECT display_pending FROM cue_state WHERE id = 1"),n=Number(t?.display_pending??0)===1;return n&&l(e,"UPDATE cue_state SET display_pending = 0 WHERE id = 1"),n}function P(e){let t=g(e,"SELECT COUNT(*) AS n FROM knowledge");return {knowledge_ready:Number(t?.n??0)>0}}function yt(e){let t={};for(let n of ["project","repository","branch","repository_platform"]){let s=g(e,`SELECT ${n} AS v, COUNT(*) AS c FROM lessons WHERE ${n} IS NOT NULL GROUP BY ${n} ORDER BY c DESC LIMIT 1`);t[n]=s?s.v:null;}return t}function Rt(e){let t=f(e),n=Ue(e),s=k(e),o=U(e),r=new Map;for(let u of o)for(let p of u.topics)r.set(p,u.name);let i={groups:o.map(u=>u.name),topics:s.map(u=>({topic:u.topic,confidence:u.confidence,group:r.get(u.topic)??null}))},c={[b]:strToU8(JSON.stringify(t,null,2)),[h]:strToU8(JSON.stringify(n,null,2)),[w]:strToU8(JSON.stringify(i,null,2))};return existsSync(R)&&(c[x]=strToU8(readFileSync(R,"utf8"))),zipSync(c)}function Me(e,t,n){if(!(b in t))return;let s=JSON.parse(strFromU8(t[b]));"max_per_day"in s&&S(e,"max_per_day",String(s.max_per_day)),"min_gap_minutes"in s&&S(e,"min_gap_minutes",String(s.min_gap_minutes)),(s.ui_theme==="system"||s.ui_theme==="dark"||s.ui_theme==="light")&&S(e,"ui_theme",s.ui_theme),"nudge_every"in s&&S(e,"nudge_every",String(s.nudge_every)),(s.nudge_scope==="session"||s.nudge_scope==="global")&&S(e,"nudge_scope",s.nudge_scope),n.settings=1;}function Pe(e,t,n){if(!(w in t))return;let s=JSON.parse(strFromU8(t[w]));if(!(s&&typeof s=="object"&&Array.isArray(s.topics))){for(let[r,i]of Object.entries(s))N(e,r,i);n.topics=Object.keys(s).length;return}let o=0;for(let r of s.groups??[])G(e,r)&&(o+=1);for(let r of s.topics){N(e,r.topic,r.confidence);let i=r.group;i&&i!=="Other"&&(G(e,i)&&(o+=1),ve(e,r.topic,i));}n.topics=s.topics.length,n.groups=o;}function $e(e,t,n){if(!(h in t))return;let s=JSON.parse(strFromU8(t[h])),{inserted:o,duplicated:r,invalid:i}=Fe(e,s);n.lessons=o,n.skipped=r,n.invalid=i;}function Xe(e,t){x in e&&(mkdirSync(dirname(R),{recursive:true}),writeFileSync(R,strFromU8(e[x]),"utf8"),t.learning_state=1);}function Nt(e,t){let n={settings:0,topics:0,groups:0,lessons:0,skipped:0,invalid:0,learning_state:0},s=unzipSync(t);return Me(e,s,n),Pe(e,s,n),$e(e,s,n),Xe(s,n),n}function Ge(e){let t=new Date,n;if(e==="today")n=new Date(Date.UTC(t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate(),0,0,0,0));else if(e==="week")n=new Date(t.getTime()-7*864e5);else if(e==="month")n=new Date(t.getTime()-30*864e5);else if(e==="year")n=new Date(t.getTime()-365*864e5);else return null;return n.toISOString()}function $(e){let t=[];try{let n=JSON.parse(e.categories);Array.isArray(n)&&(t=n);}catch{t=[];}return O({...e,categories:t,starred:!!e.starred})}function te(e){try{let t=f(e),n=new Date,s=new Date(n.getTime()-24*36e5).toISOString(),o=D(e,s);if(o>=t.max_per_day)return {allowed:!1,reason:`Daily limit reached (${o}/${t.max_per_day} lessons in the last 24h)`};let r=J(e);if(r!=null){let i=new Date(r),c=(n.getTime()-i.getTime())/6e4;if(c<t.min_gap_minutes){let u=t.min_gap_minutes-c,p=Math.floor(t.min_gap_minutes/60),_=t.min_gap_minutes%60,d=Math.trunc(u),E=Math.floor(d/60),ne=d%60;return {allowed:!1,reason:`Too soon: last lesson ${c<0?`in ${Math.round(-c)}m (future timestamp)`:`${Math.round(c)}m ago`}, minimum interval is ${p}h ${_}m (${E}h ${ne}m remaining)`}}}return {allowed:!0}}catch(t){return {allowed:true,reason:`Rate limit check failed: ${t}`}}}function Ot(e){try{return {knowledge:k(e),groups:U(e)}}catch{return {knowledge:[],groups:[]}}}function Ke(e,t,n){let s=A(e)[t]??5,o=Math.max(0,Math.min(10,s+n));return N(e,t,o),o}function bt(e,t,n){let s=V(e,t,n);return s&&(n==="know"||n==="dont_know")&&Ke(e,s,n==="know"?1:-1),s}function He(e){try{let t=new Date,n=q(e),s=new Date(t.getTime()-24*36e5).toISOString(),o=new Date(t.getTime()-7*864e5).toISOString(),r=D(e,s),i=D(e,o),c=A(e),u=Object.entries(c).sort((d,E)=>d[1]-E[1]),p=u.slice(0,5).map(([d,E])=>({topic:d,confidence:E})),_=u.slice(-5).reverse().map(([d,E])=>({topic:d,confidence:E}));return {total_lessons:n,lessons_today:r,lessons_this_week:i,weakest_topics:p,strongest_topics:_}}catch(t){return {error:String(t)}}}function ht(e){try{return Z(e)}catch{return []}}function wt(e,t,n){let s=c=>({cue:false,reason:c,nextLessonNumber:0});if(n.planMode)return s("plan mode (not counted)");if(!P(e).knowledge_ready)return s("onboarding not complete");let o=f(e),r="pacing disabled (nudge_every=0)";if(o.nudge_every>0){let c=Q(e,t??"__nosession__",o.nudge_scope),{pending:u}=M(e),p=u?Math.min(v,o.nudge_every):o.nudge_every;if(r=`${c}/${p}${u?" (retry window)":""}`,c<p)return s(`paced (${r})`)}let i=te(e);return i.allowed?(ee(e),{cue:true,reason:`cue (${r})`,nextLessonNumber:Number(He(e).total_lessons??0)+1}):s(`rate limited: ${i.reason??"denied"}`)}function xt(e,t){try{let n=[],s=!0;if(!P(e).knowledge_ready)return {wouldCue:!1,reasons:["onboarding not complete \u2014 no knowledge topics yet"]};let o=f(e);if(o.nudge_every>0){let i=F(e,t??"__nosession__",o.nudge_scope)+1,{pending:c}=M(e),u=c?Math.min(v,o.nudge_every):o.nudge_every;i<u?(s=!1,n.push(`pacing: next stop would be ${i}/${u}${c?" (retry window)":""}`)):n.push(`pacing: next stop reaches the threshold (${i}/${u})`);}else n.push("pacing disabled (nudge_every=0) \u2014 every eligible stop cues");let r=te(e);return r.allowed?n.push("rate limit: allowed"):(s=!1,n.push(`rate limited: ${r.reason??"denied"}`)),{wouldCue:s,reasons:n}}catch(n){return {wouldCue:false,reasons:[`check failed: ${n}`]}}}export{gt as A,G as B,dt as C,ve as D,mt as E,f as F,S as G,Et as H,Tt as I,M as J,_t as K,St as L,ft as M,P as N,yt as O,Rt as P,Nt as Q,te as R,Ot as S,Ke as T,bt as U,He as V,ht as W,wt as X,xt as Y,se as a,oe as b,re as c,je as d,de as e,me as f,O as g,B as h,C as i,R as j,nt as k,st as l,ot as m,rt as n,q as o,it as p,at as q,ct as r,V as s,Ue as t,Fe as u,ut as v,lt as w,pt as x,k as y,N as z};
@@ -1,19 +0,0 @@
1
- #!/usr/bin/env node
2
- import {i as i$1,m,N as N$1,Y,h as h$1,l,M as M$1,X,p}from'./chunk-227BHE2B.js';import {existsSync,mkdirSync,statSync,writeFileSync,appendFileSync,fstatSync,readFileSync}from'fs';import {join}from'path';function w(e){let o=[...e].length,t=Math.max(34-o-2,2),n=Math.floor(t/2);return `### ${"\u2500".repeat(n)} ${e} ${"\u2500".repeat(t-n)}`}function x(e){let o=e.level.charAt(0).toUpperCase()+e.level.slice(1),t=e.categories.join(" \xB7 ");return [w("\u{1F393} devcoach"),`**${e.title}** \xB7 ${t} \xB7 ${o}`,"",e.body?.trim()||e.summary,"",w(`${e.topic_id} \xB7 ${e.level}`)].join(`
3
- `)}function N(e){try{let o=JSON.parse(e);return {stop_hook_active:o?.stop_hook_active===!0,permission_mode:typeof o?.permission_mode=="string"?o.permission_mode:null,session_id:typeof o?.session_id=="string"?o.session_id:null,last_assistant_message:typeof o?.last_assistant_message=="string"?o.last_assistant_message:null,transcript_path:typeof o?.transcript_path=="string"?o.transcript_path:null}}catch{return {stop_hook_active:false,permission_mode:null,session_id:null,last_assistant_message:null,transcript_path:null}}}function h(){let e={stop_hook_active:false,permission_mode:null,session_id:null,last_assistant_message:null,transcript_path:null};try{let o=fstatSync(0);return !o.isFIFO()&&!o.isFile()&&!o.isSocket()?e:N(readFileSync(0,"utf8"))}catch{return e}}function I(e){return e.stop_hook_active||e.permission_mode==="plan"}function i(e,o,t){if(process.env.DEVCOACH_HOOK_DEBUG==="1")try{mkdirSync(h$1,{recursive:!0});let n=join(h$1,"hook.log");existsSync(n)&&statSync(n).size>256*1024&&writeFileSync(n,""),appendFileSync(n,`${new Date().toISOString()} ${e} ${(o??"-").slice(0,8)} ${t}
4
- `);}catch{}}function c(e,o){let t={decision:"block",reason:e};o&&(t.systemMessage=o),process.stdout.write(`${JSON.stringify(t)}
5
- `),process.exit(0);}var b="devcoach: the user has no coaching profile yet. Ask them how they want to set it up \u2014 Automatic (detect this project's tech stack), Guided (a short conversation to map topics and confidence levels), or Import (restore from a backup) \u2014 and do not pick for them. After they choose, build the profile and save it by calling the devcoach `complete_onboarding` MCP tool (not a shell command) before ending your turn.";function L(e=h()){if(I(e)&&process.exit(0),existsSync(i$1)){let o;try{o=m(t=>N$1(t).knowledge_ready);}catch(t){i("onboard-hook",e.session_id,`error: ${t}`),process.exit(0);}o&&process.exit(0);}i("onboard-hook",e.session_id,"onboarding cue"),c(b);}var a=10;function R(e){let o=e%a===0,t=Math.ceil(e/a)*a;return `devcoach: a lesson is due for the technical work just completed. Do it now \u2014 do not acknowledge this message and do not explain what you are about to do.
6
-
7
- Invoke the \`devcoach\` skill (Skill tool) NOW and follow it to deliver ONE lesson. Three hard rules:
8
- - The lesson card MUST be printed as your visible reply BEFORE calling log_lesson.
9
- - After log_lesson returns, output NOTHING else \u2014 the card is the end of the reply.
10
- - If the work just completed does not warrant a lesson (pure questions, chat, nothing technical), call the devcoach \`skip_lesson\` tool with a one-line reason and output nothing.
11
- ${o?`This is lesson #${e} \u2014 a notebook checkpoint (every ${a} lessons): after log_lesson returns the feedback, also call update_notebook with the revised notebook markdown and record "(updated after ${e} lessons)".`:`Do NOT call update_notebook this time \u2014 the notebook is refreshed only every ${a} lessons (next checkpoint at lesson #${t}).`}
12
-
13
- If the devcoach skill is not available, fall back to: read devcoach://briefing (one read: profile, taught topics, notebook); pick ONE untaught profile topic at or above the user's confidence band; write the card as plain markdown (no '>' blockquote) between two band headings \`### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u{1F393} devcoach \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\` and \`### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 [topic] \xB7 [level] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\`, with \`**[Title]** \xB7 [Category] \xB7 [Level]\`, 3\u20136 short paragraphs tied to the task, and a \`\u{1F4A1} *Senior tip:*\` line; THEN call log_lesson (body = clean markdown without bands or the title line).
14
-
15
- The rate-limit check is already done by this hook \u2014 skip it. Work in silence: never narrate devcoach tool calls or resource reads ('checking your profile\u2026'). Output only the lesson card (or nothing). No preamble, no meta-commentary.`}function B(e){let[o]=p(e,{page:1,per_page:1});return `devcoach: the lesson was saved with log_lesson but its card is NOT visible in your reply. Print this lesson card now, as your ENTIRE reply \u2014 no other text, no tool calls${o?`:
16
-
17
- ${x(o)}`:" (rebuild it verbatim from the arguments of your log_lesson call)."}`}var S="\u{1F393} devcoach";function F(e){if(e.last_assistant_message?.includes(S))return true;if(!e.transcript_path)return null;try{let o=readFileSync(e.transcript_path,"utf8").split(`
18
- `);for(let t=o.length-1;t>=0;t--){let n=o[t];if(!n)continue;let s;try{s=JSON.parse(n);}catch{continue}if(s?.isSidechain===!0)continue;let d=s?.message?.content;if(s?.type==="assistant"&&Array.isArray(d)){for(let u of d)if(u?.type==="text"&&typeof u.text=="string"&&u.text.includes(S))return !0}if(s?.type==="user"&&!(Array.isArray(d)&&d.some(H=>H?.type==="tool_result")))return !1}return !1}catch{return null}}function M(e,o,t){let n=o.permission_mode==="plan";if(!n&&M$1(e)&&F(o)===false)return {kind:"recover-card",cue:B(e),note:"card missing from reply \u2014 recovering"};if(o.stop_hook_active)return {kind:"silent",note:"hook-forced continuation"};if(!N$1(e).knowledge_ready)return !t.onboardCue||n?{kind:"silent",note:"onboarding not complete"}:{kind:"onboard",note:"onboarding cue (empty knowledge)"};let s=X(e,o.session_id,{planMode:n});return s.cue?{kind:"cue",nextLessonNumber:s.nextLessonNumber,note:s.reason}:{kind:"silent",note:s.reason}}function C(e,o,t){let n;try{n=m(s=>l(s,()=>M(s,o,t)));}catch(s){i(e,o.session_id,`error: ${s}`),process.exit(0);}switch(i(e,o.session_id,n.note),n.kind){case "onboard":c(b);break;case "recover-card":c(n.cue,"\u{1F393} devcoach: recovering the lesson card\u2026");break;case "cue":c(R(n.nextLessonNumber),"\u{1F393} devcoach: preparing a lesson\u2026");break;}process.exit(0);}function j(e=h()){existsSync(i$1)||process.exit(0),C("lesson-ready",e,{onboardCue:false});}function U(e=h()){existsSync(i$1)||((e.stop_hook_active||e.permission_mode==="plan")&&process.exit(0),i("stop-hook",e.session_id,"onboarding cue (no DB)"),c(b)),C("stop-hook",e,{onboardCue:true});}var V="devcoach: the lesson pacing threshold will be reached at the end of this turn. Resolve it before finishing:\n- If this task is technical: invoke the `devcoach` skill (Skill tool), then end your reply with ONE lesson card (the `### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u{1F393} devcoach \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500` block) followed by a log_lesson call. The card must be the LAST visible text \u2014 after log_lesson returns, output nothing else.\n- If it is not technical: silently call the devcoach `skip_lesson` tool with a one-line reason and output nothing about it.\n- Either way, work in silence: never narrate devcoach tool calls or resource reads ('checking your profile\u2026').";function G(e=h()){e.permission_mode==="plan"&&process.exit(0),existsSync(i$1)||process.exit(0);let o;try{o=m(t=>Y(t,e.session_id).wouldCue);}catch(t){i("prompt-hook",e.session_id,`error: ${t}`),process.exit(0);}i("prompt-hook",e.session_id,o?"priming":"no prime"),o||process.exit(0),process.stdout.write(`${JSON.stringify({hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:V}})}
19
- `),process.exit(0);}function Q(e){switch(e){case "stop-hook":U();break;case "prompt-hook":G();break;case "onboard-hook":L();break;case "lesson-ready":j();break;default:process.exit(0);}}export{N as a,L as b,R as c,j as d,U as e,G as f,Q as g};
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import {readFileSync,readdirSync,existsSync}from'fs';import {dirname,basename,join}from'path';import {execFileSync}from'child_process';import {fileURLToPath}from'url';var v=[["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]],x={react:"react","react-dom":"react",vue:"vue","@angular/core":"angular",svelte:"svelte",next:"nextjs",nuxt:"vue",fastify:"fastify",express:"express",koa:"express","@nestjs/core":"node_js"},j={django:"django",flask:"flask",fastapi:"fastapi",starlette:"fastapi",tornado:"python",aiohttp:"python",sqlalchemy:"postgresql",celery:"python"};function S(e){let t="";for(let n of e)n==="*"?t+="[^/]*":n==="?"?t+="[^/]":t+=n.replace(/[.+^${}()|[\]\\]/g,String.raw`\$&`);return new RegExp(`^${t}$`)}function w(e,t){let n=t.lastIndexOf("/"),r=n>=0?join(e,t.slice(0,n)):e,o=n>=0?t.slice(n+1):t,s;try{s=readdirSync(r);}catch{return false}let i=S(o);return s.some(h=>i.test(h))}function u(e){try{return existsSync(e)?readFileSync(e,"utf8"):null}catch{return null}}function R(e,t){let n=u(join(e,"package.json"));if(!n)return;let r;try{r=JSON.parse(n);}catch{return}let o=new Set([...Object.keys(r.dependencies??{}),...Object.keys(r.devDependencies??{})]);for(let[s,i]of Object.entries(x))o.has(s)&&t(i,6);}function A(e,t){let n=["requirements.txt","requirements-dev.txt","requirements/base.txt","pyproject.toml"];for(let r of n){let o=u(join(e,r))?.toLowerCase();if(o)for(let[s,i]of Object.entries(j))o.includes(s)&&t(i,6);}}function I(e){let t={},n=(r,o)=>{(t[r]??-1)<o&&(t[r]=o);};for(let[r,o,s]of v)w(e,r)&&n(o,s);return R(e,n),A(e,n),t}function a(...e){try{return execFileSync("git",e,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}}var p={"github.com":"github","gitlab.com":"gitlab","bitbucket.org":"bitbucket"};function E(e){if(!e)return [null,null];let t=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(e);if(t){let r=(t[1]??"").toLowerCase();return [t[2]??e,p[r]??"local"]}let n=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(e);if(n){let r=(n[1]??"").toLowerCase();return [n[2]??e,p[r]??"local"]}return [e,"local"]}function $(){let e=process.cwd(),t=a("rev-parse","--abbrev-ref","HEAD"),n=a("rev-parse","HEAD"),r=a("remote","get-url","origin"),[o,s]=E(r),i;return o?i=o.replace(/\/+$/,"").split("/").pop()??null:i=basename(e)||null,t==="HEAD"&&(t=null),{project:i,repository:o,branch:t,commit_hash:n,folder:e,repository_platform:s}}var d=dirname(fileURLToPath(import.meta.url));function g(){for(let e of ["../assets","../../assets"])if(existsSync(join(d,e,"SKILL.md")))return join(d,e);return null}var J=()=>{let e=g();return e===null?"devcoach: coaching instructions unavailable (SKILL.md not found in package).":readFileSync(join(e,"SKILL.md"),"utf8")};function W(){let e=g();if(e===null)return [];try{return readdirSync(join(e,"references")).filter(t=>t.endsWith(".md")).sort().map(t=>({name:t,content:readFileSync(join(e,"references",t),"utf8")}))}catch{return []}}var f={version:"0.8.2"};var Q=f.version;export{I as a,$ as b,J as c,W as d,Q as e};
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env node
2
- import {e as e$1,f,b,d}from'./chunk-JTR6RJFB.js';export{c as buildLessonCue,d as cmdLessonReady,b as cmdOnboardHook,f as cmdPromptHook,e as cmdStopHook,a as parseHookPayload}from'./chunk-JTR6RJFB.js';import {e,b as b$1,a,c,d as d$1}from'./chunk-VOL4SCCQ.js';import {m,S,p,w as w$1,r,q,U as U$1,F,R as R$1,V,G,z,D,A,B,C,E,P,y,t as t$1,j,Q,i,N as N$1,H,J,Y}from'./chunk-227BHE2B.js';import {spawnSync}from'child_process';import {writeFileSync,existsSync,readFileSync,mkdirSync,accessSync,constants}from'fs';import {homedir,platform}from'os';import {join,dirname}from'path';import {createInterface}from'readline/promises';import {Command}from'commander';import {styleText}from'util';var He=!!process.stdout.isTTY&&!process.env.NO_COLOR,w=(e,o)=>He?styleText(e,o):o,t={green:e=>w("green",e),red:e=>w("red",e),yellow:e=>w("yellow",e),cyan:e=>w("cyan",e),magenta:e=>w("magenta",e),dim:e=>w("dim",e),bold:e=>w("bold",e)};function K(e,o){return e==="green"?t.green(o):e==="yellow"?t.yellow(o):t.red(o)}var Ae=/\[[0-9;]*m/g,Fe=e=>e.replace(Ae,""),R=e=>[...Fe(e)].length;function $e(e,o,n){let r=Math.max(0,o-R(e));if(n==="right")return " ".repeat(r)+e;if(n==="center"){let a=Math.floor(r/2);return " ".repeat(a)+e+" ".repeat(r-a)}return e+" ".repeat(r)}function L(e,o,n,r=true){let a=o.map((p,d)=>Math.max(r?R(p.header):0,...n.map(c=>R(c[d]??"")))),i=(p,d,c)=>p+a.map(g=>"\u2500".repeat(g+2)).join(d)+c,l=p=>`\u2502${p.map((d,c)=>` ${$e(d??"",a[c]??0,o[c]?.justify??"left")} `).join("\u2502")}\u2502`,m=a.reduce((p,d)=>p+d+3,1),u=[];e&&u.push($e(t.bold(e),m,"center")),u.push(i("\u256D","\u252C","\u256E")),r&&u.push(l(o.map(p=>t.bold(p.header))),i("\u251C","\u253C","\u2524"));for(let p of n)u.push(l(p));return u.push(i("\u2570","\u2534","\u256F")),u.join(`
3
- `)}function N(e=""){if(!e)return "\u2500".repeat(60);let n=` ${e} `,r=Math.max(2,60-R(n)),a=Math.floor(r/2);return "\u2500".repeat(a)+n+"\u2500".repeat(r-a)}var we=e=>{let o=Math.max(0,Math.min(10,Math.round(e)));return "\u2588".repeat(o)+"\u2591".repeat(10-o)},ve=e=>e>=7?"green":e>=4?"yellow":"red";var s=(e="")=>{console.log(e);},Le=e=>e==="junior"?t.green(e):e==="mid"?t.yellow(e):e==="senior"?t.red(e):e,We=e=>e==="know"?` ${t.green("\u2713")}`:e==="dont_know"?` ${t.red("\u2717")}`:"",Je=e=>e==="know"?t.green("\u2713 I know this"):e==="dont_know"?t.red("\u2717 I don't know this"):t.dim("no feedback");function Te(e,o){let n=new Map;for(let a of e.groups)for(let i of a.topics)n.set(i,a.name);let r=[...e.knowledge].sort((a,i)=>i.confidence-a.confidence).map(a=>{let i=ve(a.confidence);return [a.topic,n.get(a.topic)??"Other",K(i,`${a.confidence}/10`),K(i,we(a.confidence))]});return L(o,[{header:"Topic"},{header:"Group"},{header:"Confidence",justify:"right"},{header:"Bar"}],r)}function Ye(){let e=m(o=>S(o));s(Te(e,"Knowledge Map"));}function Ve(e){let o=m(i=>p(i,{period:e.period==="all"?null:e.period,category:e.category,level:e.level,project:e.project,repository:e.repository,branch:e.branch,commit:e.commit,starred:e.starred?true:null,feedback:e.feedback,date_from:e.dateFrom,date_to:e.dateTo,sort:e.sort,order:e.order}));if(o.length===0){s(t.dim("No lessons found."));return}let n=o.some(i=>i.project||i.branch||i.commit_hash),r=[{header:""},{header:"Date"},{header:"Topic"},{header:"Title"},{header:"Level",justify:"center"},{header:"Categories"}];n&&r.push({header:"Project"},{header:"Branch"},{header:"Commit"});let a=o.map(i=>{let l=[i.starred?t.yellow("\u2605"):t.dim("\xB7"),i.timestamp.slice(0,10),t.cyan(i.topic_id),i.title+We(i.feedback),Le(i.level),i.categories.join(", ")];return n&&l.push(i.project??"",i.branch?t.magenta(i.branch):"",i.commit_hash?t.cyan(i.commit_hash.slice(0,7)):""),l});s(L("Lessons",r,a));}function Ze(e){let o=m(r=>w$1(r,e));o===null&&(s(t.red(`Lesson '${e}' not found.`)),process.exit(1)),s(N(t.bold(o.title))),s(`${t.dim("ID:")} ${o.id}`),s(`${t.dim("Date:")} ${o.timestamp.slice(0,19).replace("T"," ")}`),s(`${t.dim("Topic:")} ${o.topic_id}`),s(`${t.dim("Categories:")} ${o.categories.join(", ")}`),s(`${t.dim("Level:")} ${Le(o.level)}`);let n=o.starred?t.yellow("\u2605 starred"):t.dim("\u2606 not starred");if(s(`${t.dim("Star:")} ${n} ${t.dim("Feedback:")} ${Je(o.feedback)}`),o.task_context&&s(`${t.dim("Context:")} ${o.task_context}`),o.project||o.repository||o.branch||o.commit_hash||o.folder){let r=[];o.project&&r.push(`project=${o.project}`),o.repository&&r.push(`repo=${o.repository}`),o.branch&&r.push(`branch=${t.magenta(o.branch)}`),o.commit_hash&&r.push(`commit=${t.cyan(o.commit_hash.slice(0,7))}`),o.folder&&r.push(`folder=${o.folder}`),s(`${t.dim("Git:")} ${r.join(" \xB7 ")}`);}s(N()),s(o.summary);}function Se(e,o){m(r$1=>w$1(r$1,e)===null?false:(r(r$1,e,o),true))||(s(t.red(`Lesson '${e}' not found.`)),process.exit(1)),s(o?`Lesson ${t.cyan(e)} \u2192 ${t.yellow("\u2605 starred")}`:`Lesson ${t.cyan(e)} \u2192 ${t.dim("\u2606 unstarred")}`);}function qe(e){m(n=>q(n,e))||(s(t.red(`Lesson '${e}' not found.`)),process.exit(1)),s(`Lesson ${t.cyan(e)} deleted.`);}function Xe(e,o){["know","dont_know","clear"].includes(o)||(s(t.red(`Invalid feedback '${o}'. Use: know | dont_know | clear`)),process.exit(1));let n=o==="clear"?null:o,r=m(l=>{let m=U$1(l,e,n);if(m===null)return null;let u=l.prepare("SELECT confidence FROM knowledge WHERE topic = ?").get(m);return {topicId:m,newConf:u?u.confidence:5}});r===null&&(s(t.red(`Lesson '${e}' not found.`)),process.exit(1));let a;if(n==="know"||n==="dont_know"){let l=r.newConf+(n==="know"?-1:1);a=`${t.cyan(r.topicId)} confidence: ${l} \u2192 ${t.bold(String(r.newConf))}`;}else a="feedback cleared";let i=n==="know"?t.green("\u2713 I know this"):n==="dont_know"?t.red("\u2717 I don't know this"):t.dim("cleared");s(`Lesson ${t.cyan(e)} \u2192 ${i} (${a})`);}function Qe(){let e=m(a=>F(a)),o=Math.floor(e.min_gap_minutes/60),n=e.min_gap_minutes%60,r=o?`${o}h ${n}m`:`${n}m`;s(L("Settings",[{header:"Key"},{header:"Value",justify:"right"}],[["max_per_day",String(e.max_per_day)],["min_gap_minutes",`${e.min_gap_minutes} (${r})`],["nudge_every",e.nudge_every===0?"0 (off)":String(e.nudge_every)],["nudge_scope",e.nudge_scope]]));}function eo(){let{stats:e,rateLimit:o,settings:n}=m(u=>({stats:V(u),rateLimit:R$1(u),settings:F(u)})),r=u=>Number(e[u]??0),a=o.allowed?t.green("Available now"):t.yellow(o.reason??"");s(L("Coaching Stats",[{header:"Metric"},{header:"Value",justify:"right"}],[["Total lessons",String(r("total_lessons"))],["Lessons today (24h)",`${r("lessons_today")} / ${n.max_per_day}`],["Lessons this week",String(r("lessons_this_week"))],["Next lesson",a]],false));let i=e.weakest_topics??[],l=e.strongest_topics??[];if(i.length||l.length){let u=[];for(let p=0;p<Math.max(i.length,l.length);p++){let d=i[p],c=l[p];u.push([d?`${t.red(d.topic)} ${t.dim(`(${d.confidence})`)}`:"",c?`${t.green(c.topic)} ${t.dim(`(${c.confidence})`)}`:""]);}s(L(void 0,[{header:"Weakest topics"},{header:"Strongest topics"}],u));}let m$1=Ie();m$1&&s(`
4
- ${m$1}`);}function oo(e,o){let n=["max_per_day","min_gap_minutes","nudge_every","nudge_scope"];if(n.includes(e)||(s(t.red(`Unknown key '${e}'. Valid keys: ${n.join(", ")}`)),process.exit(1)),e==="nudge_scope"&&o!=="session"&&o!=="global"&&(s(t.red(`Invalid nudge_scope '${o}'. Use: session | global`)),process.exit(1)),e==="nudge_every"){let r=Number.parseInt(o,10);(!Number.isInteger(r)||r<0)&&(s(t.red(`Invalid nudge_every '${o}'. Use a non-negative integer (0 = off).`)),process.exit(1));}m(r=>G(r,e,o)),s(t.green(`Set ${e} = ${o}`));}function to(e,o,n){let r=e.trim();r||(s(t.red("Topic name must not be empty.")),process.exit(1)),m(i=>{z(i,r,o),n&&n!=="Other"&&D(i,r,n);});let a=n&&n!=="Other"?` \u2192 ${t.cyan(n)}`:"";s(`${t.green("Added")} ${t.bold(r)} (confidence ${t.cyan(String(o))})${a}`);}function no(e){let o=m(n=>A(n,e));s(o?`${t.green("Removed")} ${t.bold(e)} from knowledge map`:t.yellow(`Topic '${e}' not found.`));}function ro(e){let o=e.trim();(!o||o==="Other")&&(s(t.red("Invalid group name.")),process.exit(1)),m(n=>B(n,o)),s(`${t.green(`Group '${t.cyan(o)}' ready.`)} Assign topics with: devcoach group-assign <topic> "${o}"`);}function so(e){let o=m(n=>C(n,e));s(o?`${t.green(`Removed group '${t.cyan(e)}'`)}`:t.yellow(`Group '${e}' not found.`));}function io(e,o){m(r=>r.prepare("SELECT topic FROM knowledge WHERE topic = ?").get(e)===void 0?false:(o==="Other"?E(r,e):D(r,e,o),true))||(s(t.red(`Topic '${e}' not in knowledge map. Add it first.`)),process.exit(1)),s(o==="Other"?`${t.green("Moved")} ${t.bold(e)} \u2192 Other (ungrouped)`:`${t.green("Moved")} ${t.bold(e)} \u2192 ${t.cyan(o)}`);}function ao(e){let{lessonsCount:o,knowledgeCount:n,data:r}=m(i=>({lessonsCount:t$1(i).length,knowledgeCount:y(i).length,data:P(i)}));writeFileSync(e,r);let a=existsSync(j)?" + notebook":"";s(`${t.green("Backup saved:")} ${e} (${t.cyan(String(o))} lessons, ${t.cyan(String(n))} topics${a})`);}function co(e){existsSync(e)||(s(t.red(`File not found: ${e}`)),process.exit(1));let o=m(r=>Q(r,readFileSync(e)));o.settings&&s(`${t.green("\u2713")} Settings restored`),o.topics&&s(`${t.green("\u2713")} Knowledge map restored (${t.cyan(String(o.topics))} topics)`);let n=[`${t.cyan(String(o.lessons))} imported`];o.skipped&&n.push(`${t.yellow(String(o.skipped))} duplicates skipped`),o.invalid&&n.push(`${t.red(String(o.invalid))} rejected (invalid)`),s(`${t.green("\u2713")} Lessons: ${n.join(", ")}`),o.learning_state&&s(`${t.green("\u2713")} Notebook restored`);}function lo(){let e=platform();if(e==="darwin")return join(homedir(),"Library","Application Support","Claude","claude_desktop_config.json");if(e==="win32"){let n=process.env.APPDATA??join(homedir(),"AppData","Roaming");return join(n,"Claude","claude_desktop_config.json")}let o=process.env.XDG_CONFIG_HOME??join(homedir(),".config");return join(o,"Claude","claude_desktop_config.json")}var M=join(homedir(),".claude","settings.json"),v=join(homedir(),".claude","skills","devcoach"),Me=join(v,".devcoach-version");function Oe(e){for(let o of (process.env.PATH??"").split(":"))if(o)try{return accessSync(join(o,e),constants.X_OK),join(o,e)}catch{}return null}var je=e=>Oe(e)!==null;function De(){return je("devcoach")?{command:"devcoach",args:["mcp"]}:{command:"npx",args:["-y","devcoach","mcp"]}}function uo(){let e=Oe("devcoach");return e?e.includes("fnm_multishells")?"devcoach":e:"npx -y devcoach"}function po(e,o){if(!je("claude"))return "";o&&spawnSync("claude",["mcp","remove","--scope",e,"devcoach"],{encoding:"utf8"});let n=De(),r=spawnSync("claude",["mcp","add","--scope",e,"devcoach",n.command,"--",...n.args],{encoding:"utf8"});return r.status===0?`${t.green("\u2713")} Registered via \`claude mcp add\` (scope: ${e})`:`${r.stderr??""}${r.stdout??""}`.toLowerCase().includes("already")?`${t.yellow("Already registered")} in Claude Code (use --force to overwrite)`:`${t.red("claude mcp add failed:")} ${(r.stderr||r.stdout||"").trim()}`}function O(e){if(!existsSync(e))return {ok:true,data:{}};try{return {ok:!0,data:JSON.parse(readFileSync(e,"utf8"))??{}}}catch{return {ok:false,error:`${t.red("\u2717")} ${e} is not valid JSON \u2014 fix it and re-run (left unchanged)`}}}function _e(e,o,n){let r=O(e);if(!r.ok)return r.error;let a=r.data;a.mcpServers??={};let i=a.mcpServers;return i.devcoach&&!n?`${t.yellow("Already registered")} in ${e} (use --force to overwrite)`:(i.devcoach=o,mkdirSync(dirname(e),{recursive:true}),writeFileSync(e,`${JSON.stringify(a,null,2)}
5
- `),`${t.green("\u2713")} Installed into ${e}`)}function mo(){let e=uo();return {Stop:{hooks:[{type:"command",command:`${e} stop-hook`,timeout:60}]},UserPromptSubmit:{hooks:[{type:"command",command:`${e} prompt-hook`,timeout:30}]}}}function Pe(e){return Object.entries(e.enabledPlugins??{}).some(([o,n])=>n&&o.startsWith("devcoach@"))}function U(e){return /dev-?coach/i.test(e)&&/\b(?:stop-hook|prompt-hook|onboard-hook|lesson-ready)\b/.test(e)}function go(){let e=M,o=O(e);if(!o.ok)return o.error;let n=o.data;if(Pe(n))return `${t.yellow("Skipped")} \u2014 the devcoach plugin already provides the hooks (installing both would double-count interactions)`;n.hooks??={};let r=n.hooks,a=false;for(let[i,l]of Object.entries(mo())){r[i]??=[];let m=r[i],u=m.map((d,c)=>[d,c]).filter(([d])=>(d.hooks??[]).some(c=>U(c.command??""))).map(([,d])=>d),[p]=u;if(!(u.length===1&&p!==void 0&&JSON.stringify(m[p])===JSON.stringify(l))){for(let d of u.toReversed())m.splice(d,1);m.push(l),a=true;}}return a?(mkdirSync(dirname(e),{recursive:true}),writeFileSync(e,`${JSON.stringify(n,null,2)}
6
- `),`${t.green("\u2713")} Hooks installed into ${e} (Stop + UserPromptSubmit)`):`${t.yellow("Already installed")} in ${e} (current layout)`}function W(){if(!existsSync(join(v,"SKILL.md")))return "missing";try{if(readFileSync(Me,"utf8").trim()===e)return "current"}catch{}return "outdated"}function fo(e$1){if(!e$1&&W()==="current")return `${t.yellow("Already installed")} in ${v} (current version)`;mkdirSync(v,{recursive:true}),writeFileSync(join(v,"SKILL.md"),c());let o=d$1();if(o.length){let n=join(v,"references");mkdirSync(n,{recursive:true});for(let r of o)writeFileSync(join(n,r.name),r.content);}return writeFileSync(Me,`${e}
7
- `),`${t.green("\u2713")} Installed into ${v}`}function Ie(){let e=W();if(e==="outdated")return `${t.yellow("\u2192")} The devcoach Claude Code skill is out of date \u2014 run ${t.bold("devcoach install")} to refresh it.`;if(e==="missing"){let o=O(M);if(o.ok&&(o.data.hooks?.Stop??[]).some(r=>(r.hooks??[]).some(a=>U(a.command??""))))return `${t.yellow("\u2192")} The devcoach Claude Code skill is not installed \u2014 run ${t.bold("devcoach install")} to add it.`}return ""}function ho(e){let o=e.claudeCode||!e.claudeDesktop,n=e.claudeDesktop||!e.claudeCode,r=De(),a=false;if(s(t.bold("Setting up devcoach")+t.dim(` (${r.command} ${r.args.join(" ")})`)),s(),o){s(t.bold("Claude Code"));let i=po("user",e.force);if(!i){let l=join(homedir(),".claude.json");i=_e(l,{type:"stdio",env:{},...r},e.force),a=true;}s(` MCP server\u2026 ${i}`),e.skipHook||s(` Hooks\u2026 ${go()}`),s(` Skill\u2026 ${fo(e.force)}`),s();}n&&(s(t.bold("Claude Desktop")),s(` MCP server\u2026 ${_e(lo(),r,e.force)}`),a=true,s()),a&&s(`${t.yellow("\u2192")} Restart Claude Desktop to pick up the new server.
8
- `),s(t.dim(`Tip: run devcoach backup to export your profile, lessons and settings.
9
- run devcoach restore <file> to import a backup on a new machine.
10
- After upgrading devcoach (npm, Homebrew), re-run devcoach install to refresh the skill.`));}function bo(){let e$1=i=>s(` ${t.green("\u2713")} ${i}`),o=i=>s(` ${t.yellow("\u2192")} ${i}`),n=i=>s(` ${t.red("\u2717")} ${i}`);s(`
11
- ${t.bold("devcoach doctor")} ${t.dim(`v${e}`)}
12
- `),s(t.bold("Environment")),Number.parseInt(process.versions.node.split(".")[0]??"0",10)>=24?e$1(`Node ${process.versions.node} (\u2265 24)`):n(`Node ${process.versions.node} \u2014 devcoach needs Node \u2265 24 (embedded node:sqlite)`),s(t.bold(`
13
- Claude Code wiring`));let a=O(M);if(!a.ok)n(`${M} is not valid JSON \u2014 hooks cannot run`);else {let i=Pe(a.data),l=[];for(let[p,d]of Object.entries(a.data.hooks??{}))for(let c of d??[])for(let g of c.hooks??[])U(g.command??"")&&l.push({event:p,cmd:g});if(i&&l.length)n("devcoach hooks are registered TWICE (plugin + settings.json) \u2014 interactions are double-counted. Disable the plugin or remove the settings.json entries.");else if(i)e$1("hooks provided by the devcoach plugin");else if(!l.length)n(`no devcoach hooks in ${M} \u2014 run ${t.bold("devcoach install")}`);else {let p=l.filter(({cmd:c})=>c.command.includes("onboard-hook")||c.command.includes("lesson-ready")),d=l.filter(c=>c.event==="Stop").length;p.length?o(`legacy two-entry Stop layout \u2014 run ${t.bold("devcoach install")} to merge into one stop-hook entry (fewer spawns per stop)`):d>1?o(`${d} devcoach Stop entries \u2014 interactions are counted ${d}\xD7 per stop; run ${t.bold("devcoach install")} to keep only the current one`):e$1(`Stop hook wired (${d} entry)`),l.some(c=>c.event==="UserPromptSubmit")?e$1("UserPromptSubmit priming hook wired"):o(`no UserPromptSubmit priming hook \u2014 run ${t.bold("devcoach install")} to add it (lessons land more reliably)`);for(let{cmd:c}of l){let g=c.command.split(" ")[0]??"";g.startsWith("/")&&!existsSync(g)&&n(`hook command not found: ${g} \u2014 re-run ${t.bold("devcoach install")}`),c.command.startsWith("npx ")&&o("hook runs via npx \u2014 needs the npx cache (or network) on every stop"),c.timeout==null&&o(`hook entry has no timeout (${c.command})`);}}let m=W();m==="current"?e$1("Claude Code skill installed (current version)"):i?e$1("Claude Code skill bundled with the plugin"):o(m==="outdated"?`Claude Code skill is out of date \u2014 run ${t.bold("devcoach install")}`:`Claude Code skill not installed \u2014 run ${t.bold("devcoach install")}`);let u=O(join(homedir(),".claude.json"));u.ok&&u.data.mcpServers?.devcoach?e$1("MCP server registered (user scope)"):o(`MCP server not found in ~/.claude.json \u2014 it may be registered elsewhere (check with ${t.bold("claude mcp get devcoach")})`);}if(s(t.bold(`
14
- Database & pacing`)),!existsSync(i)){o(`no database yet (${i}) \u2014 onboarding runs on the first technical task`),s();return}try{m(i$1=>{e$1(`database opens (${i})`),N$1(i$1).knowledge_ready?e$1("onboarding complete"):o("onboarding not complete \u2014 the next stop cues it");let l=F(i$1);s(` settings: max_per_day=${l.max_per_day} \xB7 min_gap_minutes=${l.min_gap_minutes} \xB7 nudge_every=${l.nudge_every} \xB7 nudge_scope=${l.nudge_scope}`);let m=H(i$1),u=m.reduce((g,b)=>g+b.interactions,0);s(` pacing: ${m.length} session(s) counted, ${u} interaction(s) total`+(m[0]?` \u2014 latest ${m[0].session_id.slice(0,8)}\u2026 at ${m[0].interactions}`:""));let p=J(i$1);p.pending?o(`a cue is pending since ${p.last_cue_at} (retry threshold armed)`):p.last_skip_reason&&s(` last skip: "${p.last_skip_reason}"`),p.display_pending&&o("a lesson was logged and the next stop will verify its card is visible");let d=R$1(i$1);d.allowed?e$1("rate limit: allowed"):o(`rate limit: ${d.reason}`),s(t.bold(`
15
- Verdict`));let c=Y(i$1,m[0]?.session_id??null);c.wouldCue?e$1("the next eligible stop WOULD cue a lesson"):o("the next eligible stop would NOT cue a lesson:");for(let g of c.reasons)s(` \xB7 ${g}`);});}catch(i){n(`database check failed: ${i}`);}s();}async function ko(){let e=createInterface({input:process.stdin,output:process.stdout}),o=async(r,a="")=>{let i=a?` [${a}]`:"";return (await e.question(`${r}${i}: `)).trim()||a},n=async(r,a,i,l)=>{for(;;){let m=await o(r,String(a)),u=Number.parseInt(m,10);if(!Number.isNaN(u)&&u>=i&&u<=l)return u;s(t.red(`Must be ${i}\u2013${l}.`));}};try{s(N(t.bold("devcoach setup"))),s(`
16
- ${t.bold("Step 1")} \u2014 Restore from backup`);let r=await o("Path to existing backup zip (Enter to skip)","");if(r){existsSync(r)||(s(t.red(`File not found: ${r}`)),process.exit(1));let d=m(c=>Q(c,readFileSync(r)));s(`${t.green("\u2713")} Restored: ${d.topics} topics, ${d.lessons} lessons${d.learning_state?", notebook":""}`),s(t.green("Setup complete!"));return}s(`
17
- ${t.bold("Step 2")} \u2014 Build your knowledge profile`);let a$1=(await o("Mode: [a]utomatic (detect from files) / [m]anual (type your stack)","a")).toLowerCase(),i={};if(a$1.startsWith("a")){let c=b$1().folder??process.cwd(),g=a(c),b=Object.keys(g).sort((y,_)=>y.localeCompare(_));if(b.length){s(`
18
- ${t.dim(`Detected from ${t.cyan(c)}:`)}`);for(let y of b){let _=g[y]??5,J=await o(` ${t.cyan(y)} (Enter=keep, 0-10=override, s=skip)`,String(_));if(J.toLowerCase()==="s")continue;let Y=Number.parseInt(J,10);i[y]=Number.isNaN(Y)?_:Math.max(0,Math.min(10,Y));}}else s(t.dim("No technology files detected in current directory."));let k=await o(`
19
- Add additional topics (comma-separated, or Enter to skip)`,"");for(let y of k.split(",").map(_=>_.trim()).filter(Boolean))i[y]=await n(` Confidence for ${t.cyan(y)}`,5,0,10);}else for(s(t.dim(`
20
- Enter topics: 'topic_id confidence' (e.g. python 7). Blank line when done.`));;){let d=(await o("Topic (Enter when done)","")).trim();if(!d)break;let c=d.split(/\s+/),g=c[0]??"",b=Number.parseInt(c[1]??"",10),k=Number.isNaN(b)?5:Math.max(0,Math.min(10,b));i[g]=k,s(` ${t.green("+")} ${t.cyan(g)} \u2192 ${k}`);}let l={};if(Object.keys(i).length&&(s(`
21
- ${t.bold("Step 3")} \u2014 Organise into groups`),(await o("Organise topics into groups? [y/N]","n")).toLowerCase().startsWith("y"))){let c=[];for(let g of Object.keys(i).sort((b,k)=>b.localeCompare(k))){let b=c.length?c.join(", "):"(none yet)",k=await o(` Group for ${t.cyan(g)} existing: ${t.dim(b)} (Enter=Other)`,"");if(k&&k!=="Other"){let y=l[k]??[];y.push(g),l[k]=y,c.includes(k)||c.push(k);}}}s(`
22
- ${t.bold("Step 4")} \u2014 Rate-limit settings`);let m$1=await n("Max lessons per day",2,1,20),u=await n("Min gap between lessons (minutes)",240,0,1440),p=m(d=>{d.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[c,g]of Object.entries(i))z(d,c,g);for(let[c,g]of Object.entries(l))for(let b of g)D(d,b,c);return G(d,"max_per_day",String(m$1)),G(d,"min_gap_minutes",String(u)),S(d)});s(Te(p,"Knowledge Profile")),s(`
23
- ${t.green("Setup complete!")} ${Object.keys(i).length} topics saved.`);}finally{e.close();}}function yo(){let e$1=[["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 hooks + skill in Claude Code / Claude Desktop"],["onboard-hook","Claude Code Stop hook: cue onboarding when no profile exists"],["lesson-ready","Claude Code Stop hook: cue a lesson when one is due"],["doctor","Diagnose the Claude Code wiring \u2014 explains why a lesson would(n't) fire"],["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"]];s(`
24
- ${t.bold(`devcoach ${t.dim(`v${e}`)}`)}`);for(let[n,r]of e$1)s(` ${t.cyan(`devcoach ${n}`.padEnd(32))} ${t.dim(r)}`);s(`
25
- ${t.dim("Run")} ${t.bold("devcoach <command> --help")} ${t.dim("for per-command options.")}
26
- `);let o=Ie();o&&s(`${o}
27
- `);}function $(e){return e??null}function $o(){let e$2=new Command;return e$2.exitOverride(),e$2.name("devcoach").description("devcoach \u2014 progressive technical coaching").version(`devcoach ${e}`,"-v, --version","output the version number").allowExcessArguments(false).showHelpAfterError("(add --help for usage)").action(()=>yo()),e$2.command("mcp").description("Start the MCP server (stdio transport)").action(async()=>{let{runStdio:o}=await import('./server-EZ5YIGG6.js');await o();}),e$2.command("profile").description("Show the knowledge map").action(Ye),e$2.command("lessons").description("List past lessons").option("--period <period>","today | week | month | year | all","all").option("--category <category>","Filter by category tag").option("--project <project>","Filter by project (fuzzy)").option("--repository <repository>","Filter by repository (fuzzy)").option("--branch <branch>","Filter by branch (fuzzy)").option("--commit <commit>","Filter by commit hash prefix (fuzzy)").option("--starred","Show only starred lessons").option("--feedback <feedback>","know | dont_know | none").option("--level <level>","junior | mid | senior").option("--date-from <date>","Show lessons on or after this date (YYYY-MM-DD[THH:MM])").option("--date-to <date>","Show lessons on or before this date (YYYY-MM-DD[THH:MM])").option("--sort <column>","timestamp | level | topic_id | title | feedback","timestamp").option("--order <order>","asc | desc","desc").action(o=>Ve({period:o.period??"all",category:$(o.category),project:$(o.project),repository:$(o.repository),branch:$(o.branch),commit:$(o.commit),starred:!!o.starred,feedback:$(o.feedback),level:$(o.level),dateFrom:$(o.dateFrom),dateTo:$(o.dateTo),sort:o.sort??"timestamp",order:o.order??"desc"})),e$2.command("lesson").description("Show a single lesson in detail").argument("<id>","Lesson ID").action(o=>Ze(o)),e$2.command("star").description("Mark a lesson as starred").argument("<id>","Lesson ID").action(o=>Se(o,true)),e$2.command("unstar").description("Remove the starred mark from a lesson").argument("<id>","Lesson ID").action(o=>Se(o,false)),e$2.command("delete").description("Permanently delete a lesson").argument("<id>","Lesson ID").action(o=>qe(o)),e$2.command("feedback").description("Record know/dont_know feedback for a lesson").argument("<id>","Lesson ID").argument("<value>","know | dont_know | clear").action((o,n)=>Xe(o,n)),e$2.command("settings").description("Show current settings").action(Qe),e$2.command("stats").description("Coaching statistics and rate-limit status").action(eo),e$2.command("set").description("Update a setting (max_per_day | min_gap_minutes)").argument("<key>","Setting key").argument("<value>","New value").action((o,n)=>oo(o,n)),e$2.command("knowledge-add").description("Add or update a topic in the knowledge map").argument("<topic>","Topic ID").option("--confidence <n>","Initial confidence 0-10","5").option("--group <group>","Assign to a named group").action((o,n)=>to(o,Math.max(0,Math.min(10,Number.parseInt(n.confidence??"5",10)||5)),$(n.group))),e$2.command("knowledge-remove").description("Remove a topic from the knowledge map").argument("<topic>","Topic ID").action(o=>no(o)),e$2.command("group-add").description("Register a new knowledge group").argument("<name>","Group name").action(o=>ro(o)),e$2.command("group-remove").description("Delete a knowledge group (topics move to Other)").argument("<name>","Group name").action(o=>so(o)),e$2.command("group-assign").description("Move a topic to a group").argument("<topic>","Topic ID").argument("<group>","Group name (use 'Other' to ungroup)").action((o,n)=>io(o,n)),e$2.command("backup").description("Export a full backup (settings + knowledge + lessons) as zip").argument("[file]","Output zip path","devcoach-backup.zip").action(o=>ao(o)),e$2.command("restore").description("Restore from a backup zip file").argument("<file>","Path to backup zip").action(o=>co(o)),e$2.command("install").description("Register the MCP server + Stop hooks + skill 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(o=>ho({claudeCode:!!o.claudeCode,claudeDesktop:!!o.claudeDesktop,force:!!o.force,skipHook:!!o.skipHook})),e$2.command("doctor").description("Diagnose the devcoach \u21C4 Claude Code wiring and the lesson pacing state").action(bo),e$2.command("ui").description("Launch the web dashboard").option("--port <port>","Port","7860").action(async o=>{let{startUi:n}=await import('./app-IXWAZDVH.js');n(Number.parseInt(o.port??"7860",10)||7860);}),e$2.command("setup").description("First-run wizard: import backup or build your knowledge profile").action(ko),e$2.command("stop-hook",{hidden:true}).description("Claude Code Stop hook: onboarding check + lesson cue in one spawn").action(()=>e$1()),e$2.command("prompt-hook",{hidden:true}).description("Claude Code UserPromptSubmit hook: prime the model when a lesson is due").action(()=>f()),e$2.command("onboard-hook",{hidden:true}).description("Claude Code Stop hook: cue onboarding (legacy two-entry layout)").action(()=>b()),e$2.command("lesson-ready",{hidden:true}).description("Claude Code Stop hook: signal a lesson is due (legacy two-entry layout)").action(()=>d()),e$2}async function Eo(){let e=$o();try{await e.parseAsync(process.argv);}catch(o){let n=o.code??"";if(!n.startsWith("commander."))throw o;if(n==="commander.helpDisplayed"||n==="commander.help"||n==="commander.version")return;process.exit(2);}}export{Eo as runCli};