devcoach 2.3.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{app-3XTQVLDD.js → app-SVGKP5WG.js} +67 -65
- package/dist/bin.js +1 -1
- package/dist/{chunk-M2UBHB65.js → chunk-4PFP67XN.js} +1 -1
- package/dist/{chunk-HXWWJA73.js → chunk-JEFYE33I.js} +1 -1
- package/dist/{chunk-XH5SNBVG.js → chunk-UHF6ARVR.js} +1 -1
- package/dist/{commands-LL4QTCK7.js → commands-ECVEHTRX.js} +11 -11
- package/dist/{hooks-4KQXUTXM.js → hooks-5YA5PLBW.js} +1 -1
- package/dist/{server-KAXR4HYL.js → server-SM63J4EC.js} +2 -2
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {a,c as c$1}from'./chunk-QU6UQJWC.js';import {c,d,a as a$1}from'./chunk-
|
|
2
|
+
import {a,c as c$1}from'./chunk-QU6UQJWC.js';import {c,d,a as a$1}from'./chunk-4PFP67XN.js';import {Q,o,G as G$1,_,ca,$,A as A$1,E,B as B$1,F as F$1,aa,C,D as D$1,v,da,R,V,q as q$1,w,y,r,t,ba,x,H as H$1,m as m$1,Z,Y,O,P,j as j$1,T,S as S$1,U,X,W}from'./chunk-JEFYE33I.js';import {existsSync,readFileSync,mkdirSync,writeFileSync}from'fs';import {dirname,join,extname}from'path';import {fileURLToPath}from'url';import {serve}from'@hono/node-server';import {Hono}from'hono';import {html,raw}from'hono/html';function S(r){return JSON.stringify(r??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function Ie(r,e){return r+e.replace(/[^A-Za-z0-9_-]/g,o=>`_${(o.codePointAt(0)??0).toString(16)}`)}function j(r){let e=(o,t,s)=>html`<a href="${o}" class="text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition ${s?"text-gray-900 dark:text-white font-semibold":""}">${t}</a>`;return html`<!DOCTYPE html>
|
|
3
3
|
<html lang="en">
|
|
4
4
|
<head>
|
|
5
5
|
<meta charset="UTF-8" />
|
|
@@ -60,7 +60,7 @@ import {a,c as c$1}from'./chunk-QU6UQJWC.js';import {c,d,a as a$1}from'./chunk-M
|
|
|
60
60
|
</script>
|
|
61
61
|
${r.scripts??""}
|
|
62
62
|
</body>
|
|
63
|
-
</html>`}function
|
|
63
|
+
</html>`}function Ce(r){let e=t=>r.allGroups.filter(s=>s!=="Other").map(s=>html`<option value="${s}" ${s===t?"selected":""}>${s}</option>`),o=html`
|
|
64
64
|
<div id="knowledge-map" x-data="{ editMode: JSON.parse(localStorage.getItem('km-edit-mode') || 'false'), toggle() { this.editMode = !this.editMode; localStorage.setItem('km-edit-mode', this.editMode); } }">
|
|
65
65
|
|
|
66
66
|
<div class="flex items-center justify-between mb-6">
|
|
@@ -119,7 +119,7 @@ import {a,c as c$1}from'./chunk-QU6UQJWC.js';import {c,d,a as a$1}from'./chunk-M
|
|
|
119
119
|
${r.rateLimit.allowed?html`<span class="text-green-600 dark:text-green-400 font-medium">Available now</span>`:html`<span class="text-yellow-600 dark:text-yellow-400">${r.rateLimit.reason}</span>`}
|
|
120
120
|
</div>
|
|
121
121
|
|
|
122
|
-
${Object.entries(r.categorised).map(([t,
|
|
122
|
+
${Object.entries(r.categorised).map(([t,s])=>html`
|
|
123
123
|
<section class="mb-6 group/section">
|
|
124
124
|
<div class="flex items-center gap-2 mb-2 border-b border-gray-200 dark:border-gray-800 pb-1">
|
|
125
125
|
<div class="flex items-center gap-1 flex-1 min-w-0">
|
|
@@ -144,12 +144,12 @@ ${Object.entries(r.categorised).map(([t,n])=>html`
|
|
|
144
144
|
</div>
|
|
145
145
|
</div>
|
|
146
146
|
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-x-8 gap-y-0">
|
|
147
|
-
${
|
|
148
|
-
<div id="${
|
|
147
|
+
${s.map(n=>{let l=Ie("topic-row-",n.topic),g=n.confidence,c=Math.trunc(g/10*100),d=g>=7?"bg-green-500":g>=4?"bg-yellow-500":"bg-red-500",p=g>=7?"text-green-600 dark:text-green-400":g>=4?"text-yellow-600 dark:text-yellow-400":"text-red-600 dark:text-red-400";return html`
|
|
148
|
+
<div id="${l}" class="flex items-center gap-2 py-1.5 border-b border-gray-100 dark:border-gray-800/50 group/row">
|
|
149
149
|
<div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative shrink-0 flex items-center">
|
|
150
150
|
<button type="button" @click="open = !open" class="text-xs text-gray-200 dark:text-gray-700 hover:text-indigo-500 dark:hover:text-indigo-400 transition w-4 text-center opacity-0 group-hover/row:opacity-100" title="Move to group">⇄</button>
|
|
151
151
|
<div x-show="open" @click.outside="open = false" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg p-1 w-40" style="display:none">
|
|
152
|
-
<form method="post" action="/knowledge/${encodeURIComponent(
|
|
152
|
+
<form method="post" action="/knowledge/${encodeURIComponent(n.topic)}/group" hx-post="/knowledge/${encodeURIComponent(n.topic)}/group" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML">
|
|
153
153
|
<select name="group" onchange="this.form.requestSubmit()" class="w-full text-xs bg-transparent text-gray-700 dark:text-gray-200 px-2 py-1 focus:outline-none">
|
|
154
154
|
<option value="Other" ${t==="Other"?"selected":""}>Other (ungrouped)</option>
|
|
155
155
|
${e(t)}
|
|
@@ -158,31 +158,31 @@ ${Object.entries(r.categorised).map(([t,n])=>html`
|
|
|
158
158
|
</div>
|
|
159
159
|
</div>
|
|
160
160
|
<div class="flex items-center gap-1 flex-1 min-w-0">
|
|
161
|
-
<a x-show="!editMode" href="/lessons?search=${encodeURIComponent(
|
|
162
|
-
<span x-show="editMode" style="display:none" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate" title="${
|
|
163
|
-
<form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(
|
|
161
|
+
<a x-show="!editMode" href="/lessons?search=${encodeURIComponent(n.topic)}" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate hover:text-indigo-600 dark:hover:text-indigo-400 hover:underline transition" title="${n.topic}">${n.topic}</a>
|
|
162
|
+
<span x-show="editMode" style="display:none" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate" title="${n.topic}">${n.topic}</span>
|
|
163
|
+
<form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(n.topic)}/delete" hx-post="/knowledge/${encodeURIComponent(n.topic)}/delete" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" hx-confirm="Remove ${n.topic} from your knowledge map?" class="shrink-0 flex items-center">
|
|
164
164
|
<button type="submit" class="text-xs text-gray-200 dark:text-gray-700 hover:text-red-500 dark:hover:text-red-400 transition w-4 text-center opacity-0 group-hover/row:opacity-100" title="Remove topic">×</button>
|
|
165
165
|
</form>
|
|
166
166
|
</div>
|
|
167
|
-
<form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(
|
|
167
|
+
<form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(n.topic)}" hx-post="/knowledge/${encodeURIComponent(n.topic)}" hx-target="#${l}" hx-select="#${l}" hx-swap="outerHTML" class="shrink-0 flex items-center">
|
|
168
168
|
<input type="hidden" name="delta" value="-1" />
|
|
169
169
|
<button class="text-xs text-gray-300 dark:text-gray-600 hover:text-red-500 dark:hover:text-red-400 transition w-5 text-center leading-none">−</button>
|
|
170
170
|
</form>
|
|
171
171
|
<div class="w-20 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden shrink-0">
|
|
172
|
-
<div class="${
|
|
172
|
+
<div class="${d} h-full rounded-full transition-all" style="width: ${c}%"></div>
|
|
173
173
|
</div>
|
|
174
|
-
<form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(
|
|
174
|
+
<form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(n.topic)}" hx-post="/knowledge/${encodeURIComponent(n.topic)}" hx-target="#${l}" hx-select="#${l}" hx-swap="outerHTML" class="shrink-0 flex items-center">
|
|
175
175
|
<input type="hidden" name="delta" value="1" />
|
|
176
176
|
<button class="text-xs text-gray-300 dark:text-gray-600 hover:text-green-600 dark:hover:text-green-400 transition w-5 text-center leading-none">+</button>
|
|
177
177
|
</form>
|
|
178
|
-
<span class="text-xs font-bold ${p} w-5 text-right shrink-0">${
|
|
178
|
+
<span class="text-xs font-bold ${p} w-5 text-right shrink-0">${g}</span>
|
|
179
179
|
</div>`})}
|
|
180
180
|
</div>
|
|
181
181
|
</section>`)}
|
|
182
|
-
</div>`;return j({title:"Profile \u2014 devcoach",currentPath:"/",uiTheme:r.uiTheme,body:o})}var $e={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},
|
|
183
|
-
<a href="${u(e,{sort:y,order:
|
|
182
|
+
</div>`;return j({title:"Profile \u2014 devcoach",currentPath:"/",uiTheme:r.uiTheme,body:o})}var $e={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},Se={know:"\u2713 Known",dont_know:"\u2717 Don't know",none:"\u2014 No response"},_e={junior:"\u{1F7E2}",mid:"\u{1F7E1}",senior:"\u{1F534}"};function u(r,e={}){let o={period:r.period,category:r.category,level:r.level,project:r.project,repository:r.repository,branch:r.branch,commit:r.commit,feedback:r.feedback,search:r.search,date_from:r.date_from,date_to:r.date_to,sort:r.sort,order:r.order};r.starred&&(o.starred="1"),Object.assign(o,e);let t=new URLSearchParams;for(let[s,n]of Object.entries(o))n&&t.set(s,n);return `?${t.toString()}`}function Te(r){let e=r.s,o=!!(e.date_from||e.date_to),t=e.period!=="all"&&!o||o||!!(e.category||e.level||e.project||e.repository||e.branch||e.commit||e.starred||e.search||e.feedback),s=o?e.date_from&&e.date_to?`${e.date_from} \u2192 ${e.date_to}`:e.date_from?`From ${e.date_from}`:`Until ${e.date_to}`:$e[e.period]??"All time",n=e.feedback?Se[e.feedback]??"Feedback":"Feedback",l={junior:"text-teal-600 dark:text-teal-400 bg-teal-50 dark:bg-teal-900/20",mid:"text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20",senior:"text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-900/20"},g=(i,y)=>html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-indigo-50 dark:bg-indigo-900/20 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800">${i}<a href="${y}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-indigo-200 dark:hover:bg-indigo-700 transition">×</a></span>`,c=(i,y,v="")=>{let h=e.sort===y,I=h&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap ${v}">
|
|
183
|
+
<a href="${u(e,{sort:y,order:I})}" class="inline-flex items-center gap-1 hover:text-gray-700 dark:hover:text-gray-200 transition group/sort">${i}
|
|
184
184
|
${h?html`<span class="text-indigo-400">${e.order==="asc"?"\u2191":"\u2193"}</span>`:html`<span class="text-gray-200 dark:text-gray-700 group-hover/sort:text-gray-400 dark:group-hover/sort:text-gray-500 transition">↕</span>`}
|
|
185
|
-
</a></th>`},
|
|
185
|
+
</a></th>`},d=html`<svg class="w-3 h-3 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"/></svg>`,p=html`<link rel="stylesheet" href="/static/vendor/flatpickr.min.css" />
|
|
186
186
|
<style>
|
|
187
187
|
.dark .flatpickr-calendar { background:#1f2937; border-color:#374151; }
|
|
188
188
|
.dark .flatpickr-day { color:#e5e7eb; }
|
|
@@ -222,11 +222,11 @@ ${Object.entries(r.categorised).map(([t,n])=>html`
|
|
|
222
222
|
|
|
223
223
|
<div class="relative" x-data="periodPicker()" @keydown.escape="close()">
|
|
224
224
|
<button type="button" @click="toggle()" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${o||e.period!=="all"?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">
|
|
225
|
-
<span>📅</span><span x-text="label">${
|
|
225
|
+
<span>📅</span><span x-text="label">${s}</span>${d}
|
|
226
226
|
</button>
|
|
227
227
|
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95" @click.outside="close()" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-64 overflow-hidden" style="display:none">
|
|
228
228
|
<div class="p-1">
|
|
229
|
-
${Object.entries($e).map(([i,y])=>{let
|
|
229
|
+
${Object.entries($e).map(([i,y])=>{let v=e.period===i&&!o;return html`<button type="button" @click="selectPreset('${i}', '${y}')" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${v?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${y}${v?html`<span class="text-indigo-500">✓</span>`:""}</button>`})}
|
|
230
230
|
</div>
|
|
231
231
|
<div class="border-t border-gray-100 dark:border-gray-800 p-1">
|
|
232
232
|
<button type="button" @click="showCustom = !showCustom" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${o?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">
|
|
@@ -241,21 +241,21 @@ ${Object.entries(r.categorised).map(([t,n])=>html`
|
|
|
241
241
|
</div>
|
|
242
242
|
|
|
243
243
|
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
|
|
244
|
-
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.feedback?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${
|
|
244
|
+
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.feedback?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${n}${d}</button>
|
|
245
245
|
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-44 p-1 overflow-hidden" style="display:none">
|
|
246
246
|
${[["","All feedback"],["know","\u2713 Known"],["dont_know","\u2717 Don't know"],["none","\u2014 No response"]].map(([i,y])=>html`<button type="button" onclick="document.getElementById('h-feedback').value='${i}'; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${e.feedback===i?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${y}${e.feedback===i?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
|
|
247
247
|
</div>
|
|
248
248
|
</div>
|
|
249
249
|
|
|
250
250
|
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
|
|
251
|
-
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.level?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${e.level?`${
|
|
251
|
+
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.level?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${e.level?`${_e[e.level]??""} ${e.level}`:"Level"}${d}</button>
|
|
252
252
|
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-40 p-1 overflow-hidden" style="display:none">
|
|
253
253
|
${[["","All levels"],["junior","\u{1F7E2} Junior"],["mid","\u{1F7E1} Mid"],["senior","\u{1F534} Senior"]].map(([i,y])=>html`<button type="button" onclick="document.getElementById('h-level').value='${i}'; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${e.level===i?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${y}${e.level===i?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
|
|
254
254
|
</div>
|
|
255
255
|
</div>
|
|
256
256
|
|
|
257
257
|
${r.allCategories.length||r.allProjects.length||r.allRepositories.length||r.allBranches.length||r.allCommits.length?html`<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
|
|
258
|
-
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.category||e.project||e.repository||e.branch||e.commit?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">Filters${
|
|
258
|
+
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.category||e.project||e.repository||e.branch||e.commit?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">Filters${d}</button>
|
|
259
259
|
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-64 p-3 space-y-2.5" style="display:none">
|
|
260
260
|
${r.allCategories.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Category</label><select name="category" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All categories</option>${r.allCategories.map(i=>html`<option value="${i}" ${e.category===i?"selected":""}>${i}</option>`)}</select></div>`:""}
|
|
261
261
|
${r.allProjects.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Project</label><select name="project" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All projects</option>${r.allProjects.map(i=>html`<option value="${i}" ${e.project===i?"selected":""}>${i}</option>`)}</select></div>`:""}
|
|
@@ -283,17 +283,17 @@ ${Object.entries(r.categorised).map(([t,n])=>html`
|
|
|
283
283
|
</div>
|
|
284
284
|
|
|
285
285
|
${t?html`<div class="flex flex-wrap gap-1.5 mb-4">
|
|
286
|
-
${o?
|
|
287
|
-
${e.period!=="all"&&!o?
|
|
288
|
-
${e.feedback?
|
|
286
|
+
${o?g(`\u{1F4C5} ${s}`,u(e,{date_from:"",date_to:""})):""}
|
|
287
|
+
${e.period!=="all"&&!o?g(`\u{1F550} ${s}`,u(e,{period:"all"})):""}
|
|
288
|
+
${e.feedback?g(Se[e.feedback]??e.feedback,u(e,{feedback:""})):""}
|
|
289
289
|
${e.starred?html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300 border border-yellow-200 dark:border-yellow-800">★ Starred<a href="${u({...e,starred:false})}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-yellow-200 dark:hover:bg-yellow-700 transition">×</a></span>`:""}
|
|
290
|
-
${e.search?
|
|
291
|
-
${e.category?
|
|
292
|
-
${e.level?
|
|
293
|
-
${e.project?
|
|
294
|
-
${e.repository?
|
|
295
|
-
${e.branch?
|
|
296
|
-
${e.commit?
|
|
290
|
+
${e.search?g(`\u{1F50D} "${e.search}"`,u(e,{search:""})):""}
|
|
291
|
+
${e.category?g(e.category,u(e,{category:""})):""}
|
|
292
|
+
${e.level?g(`${_e[e.level]??""} ${e.level}`,u(e,{level:""})):""}
|
|
293
|
+
${e.project?g(`\u{1F4C1} ${e.project}`,u(e,{project:""})):""}
|
|
294
|
+
${e.repository?g(`\u2387 ${e.repository}`,u(e,{repository:""})):""}
|
|
295
|
+
${e.branch?g(`\u2387 ${e.branch}`,u(e,{branch:""})):""}
|
|
296
|
+
${e.commit?g(e.commit.slice(0,7),u(e,{commit:""})):""}
|
|
297
297
|
</div>`:""}
|
|
298
298
|
</form>
|
|
299
299
|
<form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="hidden"><input type="hidden" name="from" value="lessons" /></form>
|
|
@@ -314,7 +314,7 @@ ${r.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
|
|
|
314
314
|
</tr>
|
|
315
315
|
</thead>
|
|
316
316
|
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
|
|
317
|
-
${r.lessons.map(i=>{let y=
|
|
317
|
+
${r.lessons.map(i=>{let y=Ie("lesson-row-",i.id),v=i.timestamp.slice(0,10),h=i.timestamp.slice(0,16).replace("T"," ");return html`<tr id="${y}" class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors group cursor-pointer" tabindex="0" onclick="window.location='/lessons/${encodeURIComponent(i.id)}'" onkeydown="if(event.key==='Enter')window.location='/lessons/${encodeURIComponent(i.id)}'" role="link">
|
|
318
318
|
<td class="px-3 py-3" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">
|
|
319
319
|
<form method="post" action="/lessons/${encodeURIComponent(i.id)}/star" hx-post="/lessons/${encodeURIComponent(i.id)}/star" hx-target="#${y}" hx-select="#${y}" hx-swap="outerHTML">
|
|
320
320
|
<input type="hidden" name="starred" value="${i.starred?"0":"1"}" />
|
|
@@ -323,13 +323,13 @@ ${r.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
|
|
|
323
323
|
</form>
|
|
324
324
|
</td>
|
|
325
325
|
<td class="px-3 py-3 whitespace-nowrap tabular-nums relative group/date">
|
|
326
|
-
<span class="text-gray-400 dark:text-gray-500 cursor-default" data-ts="${i.timestamp}">${
|
|
326
|
+
<span class="text-gray-400 dark:text-gray-500 cursor-default" data-ts="${i.timestamp}">${v}</span>
|
|
327
327
|
<div class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${h}</div>
|
|
328
328
|
</td>
|
|
329
329
|
<td class="px-3 py-3 hidden sm:table-cell"><span class="text-xs font-mono text-cyan-600 dark:text-cyan-400">${i.topic_id}</span></td>
|
|
330
330
|
<td class="px-3 py-3 max-w-xs"><a href="/lessons/${encodeURIComponent(i.id)}" class="font-medium text-gray-800 dark:text-gray-100 hover:text-indigo-600 dark:hover:text-indigo-400 transition line-clamp-2">${i.title}</a></td>
|
|
331
|
-
<td class="px-3 py-3" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><a href="${u(e,{level:i.level})}" class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
332
|
-
<td class="px-3 py-3 hidden lg:table-cell" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><div class="flex flex-wrap gap-1">${i.categories.map(
|
|
331
|
+
<td class="px-3 py-3" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><a href="${u(e,{level:i.level})}" class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${l[i.level]??""} hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${i.level}</a></td>
|
|
332
|
+
<td class="px-3 py-3 hidden lg:table-cell" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><div class="flex flex-wrap gap-1">${i.categories.map(I=>html`<a href="${u(e,{category:I})}" class="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 hover:text-indigo-700 dark:hover:text-indigo-300 transition-colors">${I}</a>`)}</div></td>
|
|
333
333
|
<td class="px-3 py-3 hidden xl:table-cell" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">${i.feedback==="know"?html`<span class="text-xs text-teal-600 dark:text-teal-400 font-medium">✓ Known</span>`:i.feedback==="dont_know"?html`<span class="text-xs text-rose-500 dark:text-rose-400 font-medium">✗ Unknown</span>`:""}</td>
|
|
334
334
|
<td class="px-2 py-3 text-center" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><a href="/lessons/${encodeURIComponent(i.id)}?share=1" title="Share this lesson" class="text-gray-300 dark:text-gray-600 hover:text-indigo-500 dark:hover:text-indigo-400 transition text-base leading-none">↗</a></td>
|
|
335
335
|
</tr>`})}
|
|
@@ -340,14 +340,14 @@ ${r.totalPages>1?html`<div class="flex items-center justify-between mt-4">
|
|
|
340
340
|
<p class="text-xs text-gray-400 dark:text-gray-500">Page ${r.page} of ${r.totalPages}</p>
|
|
341
341
|
<div class="flex items-center gap-1">
|
|
342
342
|
${r.page>1?html`<a href="${u(e,{page:String(r.page-1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">← Prev</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">← Prev</span>`}
|
|
343
|
-
${
|
|
343
|
+
${Ue(r.page,r.totalPages).map(i=>i===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:i===r.page?html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-indigo-600 text-white border border-indigo-600">${i}</span>`:html`<a href="${u(e,{page:String(i)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">${i}</a>`)}
|
|
344
344
|
${r.page<r.totalPages?html`<a href="${u(e,{page:String(r.page+1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">Next →</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">Next →</span>`}
|
|
345
345
|
</div>
|
|
346
346
|
</div>`:""}`:html`<div class="flex flex-col items-center justify-center py-16 text-center">
|
|
347
347
|
<p class="text-3xl mb-3">📭</p>
|
|
348
348
|
<p class="text-gray-500 dark:text-gray-400 text-sm">No lessons match the current filters.</p>
|
|
349
349
|
${t?html`<a href="/lessons" class="mt-2 text-indigo-500 hover:text-indigo-400 text-sm transition">Clear all filters</a>`:""}
|
|
350
|
-
</div>`}`,
|
|
350
|
+
</div>`}`,T=html`<script src="/static/vendor/flatpickr.min.js"></script>
|
|
351
351
|
<script src="/static/relative-time.js"></script>
|
|
352
352
|
<script src="/static/share.js"></script>
|
|
353
353
|
<script>
|
|
@@ -355,7 +355,7 @@ function periodPicker() {
|
|
|
355
355
|
return {
|
|
356
356
|
open: false,
|
|
357
357
|
showCustom: ${raw(o?"true":"false")},
|
|
358
|
-
label: ${raw(
|
|
358
|
+
label: ${raw(S(s))},
|
|
359
359
|
fp: null,
|
|
360
360
|
init() {
|
|
361
361
|
const self = this;
|
|
@@ -363,7 +363,7 @@ function periodPicker() {
|
|
|
363
363
|
if (val && !this.fp) {
|
|
364
364
|
this.fp = flatpickr(this.$refs.fp, {
|
|
365
365
|
mode: 'range', dateFormat: 'Y-m-d', inline: false,
|
|
366
|
-
${raw(e.date_from?`defaultDate: [${
|
|
366
|
+
${raw(e.date_from?`defaultDate: [${S(e.date_from)}${e.date_to?`, ${S(e.date_to)}`:""}],`:"")}
|
|
367
367
|
onChange(dates) {
|
|
368
368
|
if (dates.length === 2) {
|
|
369
369
|
document.getElementById('h-date-from').value = self.fp.formatDate(dates[0], 'Y-m-d');
|
|
@@ -386,14 +386,14 @@ function periodPicker() {
|
|
|
386
386
|
},
|
|
387
387
|
};
|
|
388
388
|
}
|
|
389
|
-
</script>`;return j({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:r.uiTheme,head:p,body:_,scripts:
|
|
389
|
+
</script>`;return j({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:r.uiTheme,head:p,body:_,scripts:T})}function Ue(r,e){let o=[],t=0;for(let s=1;s<=e;s++)(s===1||s===e||s>=r-2&&s<=r+2)&&(t&&s-t>1&&o.push(0),o.push(s),t=s);return o}var B={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function A(r){let e=`/lessons/${encodeURIComponent(r.id)}/share?format=md&name=${encodeURIComponent(r.name)}&include_context=${r.includeContext?"1":"0"}`;return html`<div id="share-payloads" data-text="${r.text}" data-link="${r.link}" class="space-y-2">
|
|
390
390
|
<div class="flex flex-wrap gap-2">
|
|
391
391
|
<button type="button" onclick="copyShare('text', this)" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 text-gray-700 dark:text-gray-200 hover:text-indigo-700 dark:hover:text-indigo-300 transition">📋 Copy text</button>
|
|
392
392
|
<button type="button" onclick="copyShare('link', this)" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 text-gray-700 dark:text-gray-200 hover:text-indigo-700 dark:hover:text-indigo-300 transition">🔗 Copy link</button>
|
|
393
393
|
<a href="${e}" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 text-gray-700 dark:text-gray-200 hover:text-indigo-700 dark:hover:text-indigo-300 transition">⬇ Download .md</a>
|
|
394
394
|
</div>
|
|
395
395
|
<p class="text-[11px] text-gray-400 dark:text-gray-500">${r.name?html`Shared by <span class="text-gray-600 dark:text-gray-300">${r.name}</span>`:"Shared anonymously"} · ${r.includeContext?"includes project, branch and commit":"only the lesson travels \u2014 no paths, no project"}.</p>
|
|
396
|
-
</div>`}function
|
|
396
|
+
</div>`}function Pe(r){let e=r.lesson,o=r.share,t=e.level==="junior"?"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700":e.level==="mid"?"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700":"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700",s=e.repository_platform==="local",n=null;s&&e.repository?n=`vscode://file/${e.repository}`:e.repository_platform&&B[e.repository_platform]&&e.repository&&(n=`https://${B[e.repository_platform]}/${e.repository}`);let l=null;n&&e.commit_hash&&!s&&(l=e.repository_platform==="gitlab"?`${n}/-/commit/${e.commit_hash}`:e.repository_platform==="bitbucket"?`${n}/commits/${e.commit_hash}`:`${n}/commit/${e.commit_hash}`);let g=!!(e.project||e.repository||e.branch||e.commit_hash||e.folder),c=e.timestamp.slice(0,10),d=e.timestamp.slice(0,16).replace("T"," "),p=`hx-post="/lessons/${encodeURIComponent(e.id)}/feedback" hx-target="#lesson-meta" hx-select="#lesson-meta" hx-swap="outerHTML" hx-select-oob="#lesson-feedback:outerHTML"`,b=html`
|
|
397
397
|
<div class="mb-4"><a href="/lessons" class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white text-sm transition">← Back to lessons</a></div>
|
|
398
398
|
${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-700 text-green-700 dark:text-green-300">${r.imported==="new"?html`✓ Imported “${e.title}”${e.shared_by?html`, shared by ${e.shared_by}`:""}. It's in your log now — feedback works as usual and it never counts against your daily limit.`:html`“${e.title}” was already in your log — nothing changed.`}</div>`:""}
|
|
399
399
|
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
|
|
@@ -417,13 +417,13 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
417
417
|
</div>
|
|
418
418
|
<label class="flex items-start gap-2 text-xs text-gray-600 dark:text-gray-300 cursor-pointer"><input type="checkbox" name="include_context" value="1" ${o.includeContext?"checked":""} class="mt-0.5" /><span>Include where it happened <span class="text-gray-400 dark:text-gray-500">(project, branch, commit — never local paths)</span></span></label>
|
|
419
419
|
</form>
|
|
420
|
-
${
|
|
420
|
+
${A(o)}
|
|
421
421
|
</div>
|
|
422
422
|
</div>
|
|
423
423
|
</div>
|
|
424
424
|
<div id="lesson-meta" class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400 mb-5">
|
|
425
425
|
<span class="relative group/date cursor-default">🗓 <span data-ts="${e.timestamp}">${c}</span>
|
|
426
|
-
<span class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${
|
|
426
|
+
<span class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${d}</span>
|
|
427
427
|
</span>
|
|
428
428
|
<span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.topic_id}</span></span>
|
|
429
429
|
${e.imported?html`<span title="This lesson was shared with you">🤝 shared by <span class="text-gray-700 dark:text-gray-200">${e.shared_by??"anonymous"}</span></span>`:""}
|
|
@@ -437,11 +437,11 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
437
437
|
</div>
|
|
438
438
|
<div id="body-content" class="markdown-body"></div>
|
|
439
439
|
${e.task_context?html`<div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 text-sm text-gray-500 dark:text-gray-400"><span class="text-gray-400 dark:text-gray-500">Context:</span> ${e.task_context}</div>`:""}
|
|
440
|
-
${
|
|
441
|
-
${e.project?html`<span><span class="text-gray-400 dark:text-gray-600">project</span> ${
|
|
442
|
-
${e.repository?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">repo</span>${e.repository_platform&&B[e.repository_platform]?html`<img src="/static/vendor/icons/${e.repository_platform}.svg" class="w-3 h-3 dark:invert opacity-60 shrink-0" alt="" />`:""}${
|
|
440
|
+
${g?html`<div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800"><div class="flex flex-wrap gap-x-5 gap-y-1.5 text-xs text-gray-400 dark:text-gray-500 font-mono">
|
|
441
|
+
${e.project?html`<span><span class="text-gray-400 dark:text-gray-600">project</span> ${n?html`<a href="${n}" ${s?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.project}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.project}</span>`}</span>`:""}
|
|
442
|
+
${e.repository?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">repo</span>${e.repository_platform&&B[e.repository_platform]?html`<img src="/static/vendor/icons/${e.repository_platform}.svg" class="w-3 h-3 dark:invert opacity-60 shrink-0" alt="" />`:""}${n?html`<a href="${n}" ${s?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.repository}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.repository}</span>`}</span>`:""}
|
|
443
443
|
${e.branch?html`<span><span class="text-gray-400 dark:text-gray-600">branch</span> <span class="text-indigo-600 dark:text-indigo-400">${e.branch}</span></span>`:""}
|
|
444
|
-
${e.commit_hash?html`<span><span class="text-gray-400 dark:text-gray-600">commit</span> ${
|
|
444
|
+
${e.commit_hash?html`<span><span class="text-gray-400 dark:text-gray-600">commit</span> ${l?html`<a href="${l}" target="_blank" rel="noopener" class="text-cyan-600 dark:text-cyan-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.commit_hash.slice(0,7)}</a>`:html`<span class="text-cyan-600 dark:text-cyan-400">${e.commit_hash.slice(0,7)}</span>`}</span>`:""}
|
|
445
445
|
${e.folder?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">folder</span><a href="vscode://file/${e.folder}" class="inline-flex items-center gap-1 text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition"><img src="/static/vendor/icons/vscode.svg" class="w-3.5 h-3.5 shrink-0" alt="" />${e.folder}</a></span>`:""}
|
|
446
446
|
</div></div>`:""}
|
|
447
447
|
<div id="lesson-feedback">${e.feedback?"":html`<div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 flex flex-wrap gap-2">
|
|
@@ -449,7 +449,7 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
449
449
|
<form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(p)}><input type="hidden" name="feedback" value="dont_know" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="px-3 py-1 rounded text-sm font-medium transition bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-red-100 dark:hover:bg-red-900 hover:text-red-700 dark:hover:text-white">✗ I don't know this</button></form>
|
|
450
450
|
</div>`}</div>
|
|
451
451
|
<div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800 text-xs text-gray-300 dark:text-gray-600">ID: ${e.id}</div>
|
|
452
|
-
</div>`,_=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,
|
|
452
|
+
</div>`,_=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,T=html`<script src="/static/vendor/highlight.min.js"></script>
|
|
453
453
|
<script src="/static/vendor/marked.min.js"></script>
|
|
454
454
|
<script src="/static/vendor/purify.min.js"></script>
|
|
455
455
|
<script src="/static/relative-time.js"></script>
|
|
@@ -463,9 +463,9 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
463
463
|
},
|
|
464
464
|
breaks: true, gfm: true,
|
|
465
465
|
});
|
|
466
|
-
document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(
|
|
467
|
-
document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(
|
|
468
|
-
</script>`;return j({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:r.uiTheme,head:_,body:b,scripts:
|
|
466
|
+
document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(S(e.summary))}));
|
|
467
|
+
document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(S(e.body))}));
|
|
468
|
+
</script>`;return j({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:r.uiTheme,head:_,body:b,scripts:T})}var Be={junior:"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700",mid:"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700",senior:"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700"};function je(r){let e=r.preview,o=html`<form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="space-y-3">
|
|
469
469
|
<input type="hidden" name="from" value="lessons" />
|
|
470
470
|
<textarea name="text" rows="5" placeholder="devcoach:lesson:1:… — or the link, a URL, or the whole card" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono"></textarea>
|
|
471
471
|
<div class="flex items-center gap-3">
|
|
@@ -480,11 +480,11 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
480
480
|
${r.error?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-rose-50 dark:bg-rose-900/20 border-rose-200 dark:border-rose-700 text-rose-700 dark:text-rose-300">${r.error}</div>`:""}
|
|
481
481
|
${e?html`<div class="flex flex-wrap items-center gap-3 mb-2">
|
|
482
482
|
<h1 class="text-xl font-bold text-gray-900 dark:text-white flex-1 min-w-0">${e.lesson.title}</h1>
|
|
483
|
-
<span class="text-xs font-semibold px-2 py-0.5 rounded-full border ${
|
|
483
|
+
<span class="text-xs font-semibold px-2 py-0.5 rounded-full border ${Be[e.lesson.level]??""} shrink-0">${e.lesson.level}</span>
|
|
484
484
|
</div>
|
|
485
485
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400 mb-5">
|
|
486
486
|
<span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.lesson.topic_id}</span></span>
|
|
487
|
-
${e.lesson.categories.map(
|
|
487
|
+
${e.lesson.categories.map(l=>html`<span class="inline-block bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 text-xs rounded px-2 py-0.5 border border-gray-200 dark:border-gray-700">${l}</span>`)}
|
|
488
488
|
<span>🤝 shared by <span class="text-gray-700 dark:text-gray-200">${e.shared_by??"anonymous"}</span> · ${e.shared_at.slice(0,10)}</span>
|
|
489
489
|
</div>
|
|
490
490
|
<div class="my-5 pl-4 border-l-4 border-indigo-400 dark:border-indigo-500 bg-indigo-50 dark:bg-indigo-950/40 rounded-r-lg py-3 pr-4">
|
|
@@ -502,7 +502,7 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
502
502
|
<button type="submit" class="bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg px-4 py-2 text-sm transition">+ Add to my lessons</button>
|
|
503
503
|
<p class="text-xs text-gray-400 dark:text-gray-500">Nothing is saved until you click. It joins your log like your own lessons and never counts against the daily limit.</p>
|
|
504
504
|
</form>`:o}
|
|
505
|
-
</div>`,
|
|
505
|
+
</div>`,s=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,n=html`<script src="/static/vendor/highlight.min.js"></script>
|
|
506
506
|
<script src="/static/vendor/marked.min.js"></script>
|
|
507
507
|
<script src="/static/vendor/purify.min.js"></script>
|
|
508
508
|
<script src="/static/share.js"></script>
|
|
@@ -515,9 +515,9 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
515
515
|
},
|
|
516
516
|
breaks: true, gfm: true,
|
|
517
517
|
});
|
|
518
|
-
${e?raw(`document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${
|
|
519
|
-
document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${
|
|
520
|
-
</script>`;return j({title:e?`${e.lesson.title} \u2014 devcoach`:"Import a lesson \u2014 devcoach",currentPath:"/lessons",uiTheme:r.uiTheme,head:
|
|
518
|
+
${e?raw(`document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${S(e.lesson.summary)}));
|
|
519
|
+
document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${S(e.lesson.body??"")}));`):""}
|
|
520
|
+
</script>`;return j({title:e?`${e.lesson.title} \u2014 devcoach`:"Import a lesson \u2014 devcoach",currentPath:"/lessons",uiTheme:r.uiTheme,head:s,body:t,scripts:n})}var Ae=[[0,"No cooldown"],[15,"15 minutes"],[30,"30 minutes"],[60,"1 hour"],[90,"1 hour 30 min"],[120,"2 hours"],[180,"3 hours"],[240,"4 hours"],[360,"6 hours"],[480,"8 hours"],[720,"12 hours"],[1440,"24 hours"]];function Le(r){let e=r.settings.min_gap_minutes,o=[["system","System","\u{1F313}"],["light","Light","\u2600\uFE0F"],["dark","Dark","\u{1F319}"]],t=r.flash,s=html`
|
|
521
521
|
<h1 class="text-2xl font-bold mb-6 text-indigo-600 dark:text-indigo-400">Settings</h1>
|
|
522
522
|
${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-700 text-yellow-800 dark:text-yellow-300":"bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-700 text-green-700 dark:text-green-300"}">
|
|
523
523
|
${t.imported} lesson${t.imported!==1?"s":""} imported.${t.skipped?` ${t.skipped} skipped (already in DB).`:""}${t.invalid?` ${t.invalid} rejected (failed validation).`:""}${t.groups?` ${t.groups} group${t.groups!==1?"s":""} added.`:""}${t.notebook?" Notebook restored.":""}
|
|
@@ -535,7 +535,7 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
|
|
|
535
535
|
<div>
|
|
536
536
|
<label for="min-gap-minutes" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Minimum gap between lessons</label>
|
|
537
537
|
<select id="min-gap-minutes" name="min_gap_minutes" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500">
|
|
538
|
-
${Ae.map(([
|
|
538
|
+
${Ae.map(([l,g])=>html`<option value="${l}" ${e===l?"selected":""}>${g}</option>`)}
|
|
539
539
|
</select>
|
|
540
540
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Minimum gap between lessons.</p>
|
|
541
541
|
</div>
|
|
@@ -563,9 +563,9 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
|
|
|
563
563
|
<fieldset class="border-0 p-0 m-0">
|
|
564
564
|
<legend class="block text-sm text-gray-600 dark:text-gray-400 mb-1">UI theme</legend>
|
|
565
565
|
<div class="flex gap-2">
|
|
566
|
-
${o.map(([
|
|
567
|
-
<input type="radio" name="ui_theme" value="${
|
|
568
|
-
<span class="flex flex-col items-center justify-center gap-1 px-3 py-2 rounded border border-gray-300 dark:border-gray-700 peer-checked:border-indigo-500 peer-checked:bg-indigo-50 dark:peer-checked:bg-indigo-900/30 peer-checked:text-indigo-700 dark:peer-checked:text-indigo-300 text-gray-500 dark:text-gray-400 text-xs font-medium transition select-none"><span class="text-base leading-none">${c}</span>${
|
|
566
|
+
${o.map(([l,g,c])=>html`<label class="flex-1 cursor-pointer">
|
|
567
|
+
<input type="radio" name="ui_theme" value="${l}" ${r.settings.ui_theme===l?"checked":""} class="sr-only peer" />
|
|
568
|
+
<span class="flex flex-col items-center justify-center gap-1 px-3 py-2 rounded border border-gray-300 dark:border-gray-700 peer-checked:border-indigo-500 peer-checked:bg-indigo-50 dark:peer-checked:bg-indigo-900/30 peer-checked:text-indigo-700 dark:peer-checked:text-indigo-300 text-gray-500 dark:text-gray-400 text-xs font-medium transition select-none"><span class="text-base leading-none">${c}</span>${g}</span>
|
|
569
569
|
</label>`)}
|
|
570
570
|
</div>
|
|
571
571
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">System follows your OS preference.</p>
|
|
@@ -623,7 +623,7 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
|
|
|
623
623
|
<div x-show="nbMode === 'preview'" id="notebook-preview-settings" class="markdown-body min-h-24"></div>
|
|
624
624
|
<pre x-show="nbMode === 'source'" style="display:none" class="text-xs font-mono bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded p-3 overflow-x-auto whitespace-pre-wrap text-gray-700 dark:text-gray-200 min-h-24">${r.notebookContent}</pre>
|
|
625
625
|
</div>
|
|
626
|
-
</div>`,
|
|
626
|
+
</div>`,n=html`<script src="/static/vendor/marked.min.js"></script>
|
|
627
627
|
<script src="/static/vendor/purify.min.js"></script>
|
|
628
628
|
<script>
|
|
629
629
|
function updateLabel(inputId, labelId, submitId) {
|
|
@@ -632,6 +632,8 @@ function updateLabel(inputId, labelId, submitId) {
|
|
|
632
632
|
document.getElementById(submitId).disabled = !input.files.length;
|
|
633
633
|
}
|
|
634
634
|
marked.setOptions({ breaks: true, gfm: true });
|
|
635
|
-
document.getElementById('notebook-preview-settings').innerHTML = DOMPurify.sanitize(marked.parse(${raw(
|
|
636
|
-
</script>`;return j({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:r.uiTheme,body:n,scripts:s})}var O=25;function D(r,e="/lessons"){return r?.startsWith("/")&&!r.startsWith("//")?r:e}function m(r,e,o=""){let t=r[e];return typeof t=="string"?t:o}var H=dirname(fileURLToPath(import.meta.url)),Le=[join(H,"../assets/static"),join(H,"../../assets/static")].find(r=>existsSync(r))??join(H,"../assets/static"),ze={".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".woff2":"font/woff2",".woff":"font/woff",".ico":"image/x-icon"},Ke="https://ultimaphoenix.github.io",Ee={"access-control-allow-origin":Ke,"access-control-allow-methods":"GET, OPTIONS","access-control-allow-private-network":"true","cache-control":"no-store",vary:"Origin"};function q(r){let e=r.req.header("sec-fetch-site");return e!==void 0&&e!=="same-origin"&&e!=="none"}var G;function Ye(){return G===void 0&&(G=a$1()),G}function Me(r,e,o){let t=T({explicit:o.name??null,setting:e,gitUserName:Ye()}),n=o.includeContext??false,s=S(r,{includeContext:n,sharedBy:t}),d=U$1(s);return {payload:s,state:{id:r.id,open:o.open??false,name:t??"",includeContext:n,text:W(s,d),link:X(s,d)}}}function Je(){try{return o(r=>G$1(r).ui_theme)}catch{return "system"}}function Ve(r$1={}){let e=new Hono;return e.get("/static/*",o=>{let t=decodeURIComponent(o.req.path.slice(8)),n=join(Le,t);if(!n.startsWith(Le))return o.notFound();try{let s=readFileSync(n);return new Response(new Uint8Array(s),{headers:{"content-type":ze[extname(n)]??"application/octet-stream"}})}catch{return o.notFound()}}),e.get("/ping",o=>o.json({ok:true,version:Q},200,Ee)),e.options("/ping",o=>o.body(null,204,Ee)),e.post("/shutdown",o=>q(o)?o.text("Forbidden",403):(setTimeout(()=>r$1.onShutdown?.(),50),o.json({ok:true}))),e.get("/shutdown",o=>o.text("Method Not Allowed",405)),e.get("/",o$1=>{let{profile:t,stats:n,rateLimit:s,settings:d}=o(g=>({profile:$$1(g),stats:ca(g),rateLimit:_(g),settings:G$1(g)})),l=new Map;for(let g of t.groups)for(let p of g.topics)l.set(p,g.name);let c={};for(let g of t.groups)c[g.name]=[];for(let g of t.knowledge){let p=l.get(g.topic)??"Other",b=c[p]??[];b.push(g),c[p]=b;}return o$1.html(Ie({categorised:c,allGroups:t.groups.map(g=>g.name),stats:n,rateLimit:s,maxPerDay:d.max_per_day,uiTheme:d.ui_theme}))}),e.post("/knowledge",async o$1=>{let t=await o$1.req.parseBody(),n=m(t,"topic").trim();if(n){let s=Number.parseInt(m(t,"confidence","5"),10)||5,d=m(t,"group").trim();o(l=>{A(l,n,s),d&&d!=="Other"&&E(l,n,d);});}return o$1.redirect("/",303)}),e.post("/knowledge/:topic/delete",o$1=>(o(t=>B$1(t,o$1.req.param("topic"))),o$1.redirect("/",303))),e.post("/knowledge/:topic/group",async o$1=>{let t=m(await o$1.req.parseBody(),"group").trim(),n=o$1.req.param("topic");return o(s=>{t&&t!=="Other"?E(s,n,t):F(s,n);}),o$1.redirect("/",303)}),e.post("/knowledge/:topic",async o$1=>{let t=Number.parseInt(m(await o$1.req.parseBody(),"delta","0"),10)||0;return o(n=>aa(n,o$1.req.param("topic"),t)),o$1.redirect("/",303)}),e.post("/groups",async o$1=>{let t=m(await o$1.req.parseBody(),"group_name").trim();return t&&t!=="Other"&&o(n=>C(n,t)),o$1.redirect("/",303)}),e.post("/groups/:group_name/delete",o$1=>(o(t=>D$1(t,o$1.req.param("group_name"))),o$1.redirect("/",303))),e.get("/lessons/export",()=>{let o$1=o(t=>v(t));return new Response(JSON.stringify(o$1,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),e.post("/lessons/import",async o$1=>{let t=await o$1.req.parseBody(),n=m(t,"from")==="lessons",s=g=>o$1.redirect(n?`/lessons?import=1&error=${g}`:"/settings?imported=0&skipped=0&invalid=1",303);if(q(o$1))return s("cross");let d$1=t.file,c$1=(d$1 instanceof File&&d$1.size>0?await d$1.text():null)??m(t,"text").trim();try{c(c$1)&&!c$1.includes("#devcoach:lesson:")&&(c$1=await d(c$1));let g=o(p=>da(p,c$1));return g.kind==="lessons"||!g.lesson?o$1.redirect(`/settings?imported=${g.inserted}&skipped=${g.duplicated}&invalid=${g.invalid}`,303):o$1.redirect(`/lessons/${encodeURIComponent(g.lesson.id)}?imported=${g.inserted?"1":"dup"}`,303)}catch(g){if(g instanceof R)return s("invalid");throw g}}),e.get("/lessons/import",o=>{let t=(o.req.query("code")??"").trim(),n=null,s=null;if(t)try{n=V(t);}catch(d){s=d instanceof R?d.message:"That code could not be read.";}return o.html(Pe({code:t,preview:n,error:s,uiTheme:Je()}))}),e.get("/lessons",o$1=>{let t=o$1.req.query(),n=new Set(["junior","mid","senior"]),s=t.level&&n.has(t.level)?t.level:"",d=t.date_from||"",l=t.date_to||"",c=t.starred==="1",g=t.sort||"timestamp",p=t.order==="asc"?"asc":"desc",b=Math.max(1,Number.parseInt(t.page??"1",10)||1),_=t.period||"all",I={period:d||l?null:_!=="all"?_:null,category:t.category||null,level:s||null,project:t.project||null,repository:t.repository||null,branch:t.branch||null,commit:t.commit||null,starred:c?true:null,search:t.search||null,feedback:t.feedback||null,date_from:d||null,date_to:l||null},i=o(h=>{let S=q$1(h,I),z=Math.max(1,Math.ceil(S/O));return b=Math.min(b,z),{total:S,totalPages:z,lessons:r(h,{...I,sort:g,order:p,page:b,per_page:O}),allCategories:y(h),allProjects:w(h,"project"),allRepositories:w(h,"repository"),allBranches:w(h,"branch"),allCommits:w(h,"commit_hash"),theme:G$1(h).ui_theme}}),y$1={period:_,category:t.category||"",level:s,project:t.project||"",repository:t.repository||"",branch:t.branch||"",commit:t.commit||"",starred:c,search:t.search||"",feedback:t.feedback||"",date_from:d,date_to:l,sort:g,order:p},k=t.error==="invalid"||t.error==="cross"?t.error:null;return o$1.html(Te({importOpen:t.import==="1",importError:k,lessons:i.lessons,allCategories:i.allCategories,allProjects:i.allProjects,allRepositories:i.allRepositories,allBranches:i.allBranches,allCommits:i.allCommits,s:y$1,page:b,perPage:O,total:i.total,totalPages:i.totalPages,uiTheme:i.theme}))}),e.post("/lessons/:lesson_id/star",async o$1=>{let t$1=await o$1.req.parseBody();return o(n=>t(n,o$1.req.param("lesson_id"),t$1.starred==="1")),o$1.redirect(D(m(t$1,"next")||void 0),303)}),e.post("/lessons/:lesson_id/feedback",async o$1=>{let t=await o$1.req.parseBody(),n=m(t,"feedback"),s=n===""||n==="clear"?null:n;return o(d=>ba(d,o$1.req.param("lesson_id"),s)),o$1.redirect(D(m(t,"next")||void 0),303)}),e.on(["GET","POST"],"/lessons/:lesson_id/share",async o$1=>{if(q(o$1))return o$1.text("Forbidden",403);let t,n,s=false;if(o$1.req.method==="POST"){let p=await o$1.req.parseBody();t=m(p,"name"),n=m(p,"include_context")==="1",s=m(p,"persist")==="1";}else t=o$1.req.query("name"),n=o$1.req.query("include_context")==="1";let d=o(p=>{let b=x(p,o$1.req.param("lesson_id")??"");return b?(s&&t!==void 0&&H$1(p,"share_name",m$1(t)),{lesson:b,settingName:G$1(p).share_name}):null});if(!d)return o$1.text("Lesson not found",404);let{state:l,payload:c}=Me(d.lesson,d.settingName,{name:t,includeContext:n}),g=o$1.req.query("format");return g==="text"?o$1.text(l.text):g==="link"?o$1.text(l.link):g==="md"?new Response(Z(c),{headers:{"content-type":"text/markdown; charset=utf-8","content-disposition":`attachment; filename="${Y(c)}"`}}):o$1.html(U(l))}),e.get("/lessons/:lesson_id",o$1=>{let t=o(d=>{let l=x(d,o$1.req.param("lesson_id"));if(!l)return null;let c=G$1(d);return {lesson:l,settingName:c.share_name,uiTheme:c.ui_theme}});if(!t)return o$1.html("<h1>Lesson not found</h1>",404);let n=o$1.req.query(),s=n.imported==="1"?"new":n.imported==="dup"?"dup":null;return o$1.html(je({lesson:t.lesson,uiTheme:t.uiTheme,share:Me(t.lesson,t.settingName,{open:n.share==="1"}).state,imported:s}))}),e.get("/settings/export",()=>{let o$1=o(t=>O$1(t));return new Response(new Uint8Array(o$1),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),e.post("/settings/import",async o$1=>{let t=(await o$1.req.parseBody()).file;if(!(t instanceof File))return o$1.redirect("/settings?imported=0&skipped=0&invalid=1",303);let n=new Uint8Array(await t.arrayBuffer()),s=o(d=>P(d,n));return o$1.redirect(`/settings?imported=${s.lessons}&skipped=${s.skipped}&invalid=${s.invalid}&groups=${s.groups}¬ebook=${s.learning_state}`,303)}),e.get("/settings/notebook/download",()=>{let o=existsSync(j$1)?readFileSync(j$1,"utf8"):"";return new Response(o,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),e.post("/settings/notebook",async o=>{let t=await o.req.parseBody();return mkdirSync(dirname(j$1),{recursive:true}),writeFileSync(j$1,m(t,"content"),"utf8"),o.redirect(D(m(t,"next")||void 0,"/settings?notebook_saved=1"),303)}),e.post("/settings",async o$1=>{let t=await o$1.req.parseBody(),n=Number.parseInt(m(t,"max_per_day","2"),10)||2,s=Number.parseInt(m(t,"min_gap_minutes","240"),10),d=m(t,"ui_theme","system");["system","dark","light"].includes(d)||(d="system");let l=Math.max(0,Number.parseInt(m(t,"nudge_every","10"),10)||0),c=m(t,"nudge_scope","session");return c!=="session"&&c!=="global"&&(c="session"),o(g=>{H$1(g,"max_per_day",String(n)),H$1(g,"min_gap_minutes",String(Number.isNaN(s)?240:s)),H$1(g,"ui_theme",d),H$1(g,"nudge_every",String(l)),H$1(g,"nudge_scope",c),H$1(g,"share_name",m$1(m(t,"share_name")));}),o$1.redirect("/settings",303)}),e.get("/settings",o$1=>{let t=o(l=>G$1(l)),n=existsSync(j$1)?readFileSync(j$1,"utf8"):"",s=o$1.req.query(),d=s.imported!==void 0?{imported:Number(s.imported??0),skipped:Number(s.skipped??0),invalid:Number(s.invalid??0),groups:Number(s.groups??0),notebook:Number(s.notebook??0)}:null;return o$1.html(Re({settings:t,notebookContent:n,notebookPath:j$1,uiTheme:t.ui_theme,flash:d}))}),e}function We(r,e=2e3){let o=r;return new Promise(t=>{let n=setTimeout(()=>{o.closeAllConnections?.();},e),s=setInterval(()=>o.closeIdleConnections?.(),100);r.close(()=>{clearTimeout(n),clearInterval(s),t();}),o.closeIdleConnections?.();})}function pt(r,e={}){let o=false,t=(d,l)=>{if(o){l&&process.exit(130);return}o=true,process.stdout.isTTY&&console.log(a.dim(`devcoach UI stopping (${d})\u2026`)),We(s).then(()=>{l&&process.exit(0);});},n=Ve({onShutdown:()=>t("stop requested",false)}),s=serve({fetch:n.fetch,port:r,hostname:"127.0.0.1"},d=>{let l=`http://localhost:${d.port}`;console.log(`devcoach UI running at ${a.cyan(c$1(l))}`),process.stdout.isTTY&&console.log(a.dim("Press Ctrl+C to stop")),e.onReady?.(l);});if(e.handleSignals!==false)for(let d of ["SIGINT","SIGTERM","SIGHUP"])process.on(d,()=>t(d,true));return s}async function mt(r,e=fetch){try{return (await e(`http://127.0.0.1:${r}/shutdown`,{method:"POST",signal:AbortSignal.timeout(3e3)})).ok}catch{return false}}
|
|
637
|
-
|
|
635
|
+
document.getElementById('notebook-preview-settings').innerHTML = DOMPurify.sanitize(marked.parse(${raw(S(r.notebookContent))}));
|
|
636
|
+
</script>`;return j({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:r.uiTheme,body:s,scripts:n})}var D=25;function H(r,e="/lessons"){return r?.startsWith("/")&&!r.startsWith("//")?r:e}function m(r,e,o=""){let t=r[e];return typeof t=="string"?t:o}var q=dirname(fileURLToPath(import.meta.url)),Ee=[join(q,"../assets/static"),join(q,"../../assets/static")].find(r=>existsSync(r))??join(q,"../assets/static"),ze={".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".woff2":"font/woff2",".woff":"font/woff",".ico":"image/x-icon"},Ke="https://ultimaphoenix.github.io",Re={"access-control-allow-origin":Ke,"access-control-allow-methods":"GET, OPTIONS","access-control-allow-private-network":"true","cache-control":"no-store",vary:"Origin"};function G(r){let e=r.req.header("sec-fetch-site");return e!==void 0&&e!=="same-origin"&&e!=="none"}var F;function Ye(){return F===void 0&&(F=a$1()),F}function Me(r,e,o){let t=T({explicit:o.name??null,setting:e,gitUserName:Ye()}),s=o.includeContext??false,n=S$1(r,{includeContext:s,sharedBy:t}),l=U(n);return {payload:n,state:{id:r.id,open:o.open??false,name:t??"",includeContext:s,text:W(n,l),link:X(n,l)}}}function Je(){try{return o(r=>G$1(r).ui_theme)}catch{return "system"}}function Ve(r$1={}){let e=new Hono;return e.get("/static/*",o=>{let t=decodeURIComponent(o.req.path.slice(8)),s=join(Ee,t);if(!s.startsWith(Ee))return o.notFound();try{let n=readFileSync(s);return new Response(new Uint8Array(n),{headers:{"content-type":ze[extname(s)]??"application/octet-stream"}})}catch{return o.notFound()}}),e.get("/ping",o=>o.json({ok:true,version:Q},200,Re)),e.options("/ping",o=>o.body(null,204,Re)),e.post("/shutdown",o=>G(o)?o.text("Forbidden",403):(setTimeout(()=>r$1.onShutdown?.(),50),o.json({ok:true}))),e.get("/shutdown",o=>o.text("Method Not Allowed",405)),e.get("/",o$1=>{let{profile:t,stats:s,rateLimit:n,settings:l}=o(d=>({profile:$(d),stats:ca(d),rateLimit:_(d),settings:G$1(d)})),g=new Map;for(let d of t.groups)for(let p of d.topics)g.set(p,d.name);let c={};for(let d of t.groups)c[d.name]=[];for(let d of t.knowledge){let p=g.get(d.topic)??"Other",b=c[p]??[];b.push(d),c[p]=b;}return o$1.html(Ce({categorised:c,allGroups:t.groups.map(d=>d.name),stats:s,rateLimit:n,maxPerDay:l.max_per_day,uiTheme:l.ui_theme}))}),e.post("/knowledge",async o$1=>{let t=await o$1.req.parseBody(),s=m(t,"topic").trim();if(s){let n=Number.parseInt(m(t,"confidence","5"),10)||5,l=m(t,"group").trim();o(g=>{A$1(g,s,n),l&&l!=="Other"&&E(g,s,l);});}return o$1.redirect("/",303)}),e.post("/knowledge/:topic/delete",o$1=>(o(t=>B$1(t,o$1.req.param("topic"))),o$1.redirect("/",303))),e.post("/knowledge/:topic/group",async o$1=>{let t=m(await o$1.req.parseBody(),"group").trim(),s=o$1.req.param("topic");return o(n=>{t&&t!=="Other"?E(n,s,t):F$1(n,s);}),o$1.redirect("/",303)}),e.post("/knowledge/:topic",async o$1=>{let t=Number.parseInt(m(await o$1.req.parseBody(),"delta","0"),10)||0;return o(s=>aa(s,o$1.req.param("topic"),t)),o$1.redirect("/",303)}),e.post("/groups",async o$1=>{let t=m(await o$1.req.parseBody(),"group_name").trim();return t&&t!=="Other"&&o(s=>C(s,t)),o$1.redirect("/",303)}),e.post("/groups/:group_name/delete",o$1=>(o(t=>D$1(t,o$1.req.param("group_name"))),o$1.redirect("/",303))),e.get("/lessons/export",()=>{let o$1=o(t=>v(t));return new Response(JSON.stringify(o$1,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),e.post("/lessons/import",async o$1=>{let t=await o$1.req.parseBody(),s=m(t,"from")==="lessons",n=d=>o$1.redirect(s?`/lessons?import=1&error=${d}`:"/settings?imported=0&skipped=0&invalid=1",303);if(G(o$1))return n("cross");let l=t.file,c$1=(l instanceof File&&l.size>0?await l.text():null)??m(t,"text").trim();try{c(c$1)&&!c$1.includes("#devcoach:lesson:")&&(c$1=await d(c$1));let d$1=o(p=>da(p,c$1));return d$1.kind==="lessons"||!d$1.lesson?o$1.redirect(`/settings?imported=${d$1.inserted}&skipped=${d$1.duplicated}&invalid=${d$1.invalid}`,303):o$1.redirect(`/lessons/${encodeURIComponent(d$1.lesson.id)}?imported=${d$1.inserted?"1":"dup"}`,303)}catch(d){if(d instanceof R)return n("invalid");throw d}}),e.get("/lessons/import",o=>{let t=(o.req.query("code")??"").trim(),s=null,n=null;if(t)try{s=V(t);}catch(l){n=l instanceof R?l.message:"That code could not be read.";}return o.html(je({code:t,preview:s,error:n,uiTheme:Je()}))}),e.get("/lessons",o$1=>{let t=o$1.req.query(),s=new Set(["junior","mid","senior"]),n=t.level&&s.has(t.level)?t.level:"",l=t.date_from||"",g=t.date_to||"",c=t.starred==="1",d=t.sort||"timestamp",p=t.order==="asc"?"asc":"desc",b=Math.max(1,Number.parseInt(t.page??"1",10)||1),_=t.period||"all",T={period:l||g?null:_!=="all"?_:null,category:t.category||null,level:n||null,project:t.project||null,repository:t.repository||null,branch:t.branch||null,commit:t.commit||null,starred:c?true:null,search:t.search||null,feedback:t.feedback||null,date_from:l||null,date_to:g||null},i=o(h=>{let I=q$1(h,T),K=Math.max(1,Math.ceil(I/D));return b=Math.min(b,K),{total:I,totalPages:K,lessons:r(h,{...T,sort:d,order:p,page:b,per_page:D}),allCategories:y(h),allProjects:w(h,"project"),allRepositories:w(h,"repository"),allBranches:w(h,"branch"),allCommits:w(h,"commit_hash"),theme:G$1(h).ui_theme}}),y$1={period:_,category:t.category||"",level:n,project:t.project||"",repository:t.repository||"",branch:t.branch||"",commit:t.commit||"",starred:c,search:t.search||"",feedback:t.feedback||"",date_from:l,date_to:g,sort:d,order:p},v=t.error==="invalid"||t.error==="cross"?t.error:null;return o$1.html(Te({importOpen:t.import==="1",importError:v,lessons:i.lessons,allCategories:i.allCategories,allProjects:i.allProjects,allRepositories:i.allRepositories,allBranches:i.allBranches,allCommits:i.allCommits,s:y$1,page:b,perPage:D,total:i.total,totalPages:i.totalPages,uiTheme:i.theme}))}),e.post("/lessons/:lesson_id/star",async o$1=>{let t$1=await o$1.req.parseBody();return o(s=>t(s,o$1.req.param("lesson_id"),t$1.starred==="1")),o$1.redirect(H(m(t$1,"next")||void 0),303)}),e.post("/lessons/:lesson_id/feedback",async o$1=>{let t=await o$1.req.parseBody(),s=m(t,"feedback"),n=s===""||s==="clear"?null:s;return o(l=>ba(l,o$1.req.param("lesson_id"),n)),o$1.redirect(H(m(t,"next")||void 0),303)}),e.on(["GET","POST"],"/lessons/:lesson_id/share",async o$1=>{if(G(o$1))return o$1.text("Forbidden",403);let t,s,n=false;if(o$1.req.method==="POST"){let p=await o$1.req.parseBody();t=m(p,"name"),s=m(p,"include_context")==="1",n=m(p,"persist")==="1";}else t=o$1.req.query("name"),s=o$1.req.query("include_context")==="1";let l=o(p=>{let b=x(p,o$1.req.param("lesson_id")??"");return b?(n&&t!==void 0&&H$1(p,"share_name",m$1(t)),{lesson:b,settingName:G$1(p).share_name}):null});if(!l)return o$1.text("Lesson not found",404);let{state:g,payload:c}=Me(l.lesson,l.settingName,{name:t,includeContext:s}),d=o$1.req.query("format");return d==="text"?o$1.text(g.text):d==="link"?o$1.text(g.link):d==="md"?new Response(Z(c),{headers:{"content-type":"text/markdown; charset=utf-8","content-disposition":`attachment; filename="${Y(c)}"`}}):o$1.html(A(g))}),e.get("/lessons/:lesson_id",o$1=>{let t=o(l=>{let g=x(l,o$1.req.param("lesson_id"));if(!g)return null;let c=G$1(l);return {lesson:g,settingName:c.share_name,uiTheme:c.ui_theme}});if(!t)return o$1.html("<h1>Lesson not found</h1>",404);let s=o$1.req.query(),n=s.imported==="1"?"new":s.imported==="dup"?"dup":null;return o$1.html(Pe({lesson:t.lesson,uiTheme:t.uiTheme,share:Me(t.lesson,t.settingName,{open:s.share==="1"}).state,imported:n}))}),e.get("/settings/export",()=>{let o$1=o(t=>O(t));return new Response(new Uint8Array(o$1),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),e.post("/settings/import",async o$1=>{let t=(await o$1.req.parseBody()).file;if(!(t instanceof File))return o$1.redirect("/settings?imported=0&skipped=0&invalid=1",303);let s=new Uint8Array(await t.arrayBuffer()),n=o(l=>P(l,s));return o$1.redirect(`/settings?imported=${n.lessons}&skipped=${n.skipped}&invalid=${n.invalid}&groups=${n.groups}¬ebook=${n.learning_state}`,303)}),e.get("/settings/notebook/download",()=>{let o=existsSync(j$1)?readFileSync(j$1,"utf8"):"";return new Response(o,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),e.post("/settings/notebook",async o=>{let t=await o.req.parseBody();return mkdirSync(dirname(j$1),{recursive:true}),writeFileSync(j$1,m(t,"content"),"utf8"),o.redirect(H(m(t,"next")||void 0,"/settings?notebook_saved=1"),303)}),e.post("/settings",async o$1=>{let t=await o$1.req.parseBody(),s=Number.parseInt(m(t,"max_per_day","2"),10)||2,n=Number.parseInt(m(t,"min_gap_minutes","240"),10),l=m(t,"ui_theme","system");["system","dark","light"].includes(l)||(l="system");let g=Math.max(0,Number.parseInt(m(t,"nudge_every","10"),10)||0),c=m(t,"nudge_scope","session");return c!=="session"&&c!=="global"&&(c="session"),o(d=>{H$1(d,"max_per_day",String(s)),H$1(d,"min_gap_minutes",String(Number.isNaN(n)?240:n)),H$1(d,"ui_theme",l),H$1(d,"nudge_every",String(g)),H$1(d,"nudge_scope",c),H$1(d,"share_name",m$1(m(t,"share_name")));}),o$1.redirect("/settings",303)}),e.get("/settings",o$1=>{let t=o(g=>G$1(g)),s=existsSync(j$1)?readFileSync(j$1,"utf8"):"",n=o$1.req.query(),l=n.imported!==void 0?{imported:Number(n.imported??0),skipped:Number(n.skipped??0),invalid:Number(n.invalid??0),groups:Number(n.groups??0),notebook:Number(n.notebook??0)}:null;return o$1.html(Le({settings:t,notebookContent:s,notebookPath:j$1,uiTheme:t.ui_theme,flash:l}))}),e}function We(r,e=2e3){let o=r;return new Promise(t=>{let s=setTimeout(()=>{o.closeAllConnections?.();},e),n=setInterval(()=>o.closeIdleConnections?.(),100);r.close(()=>{clearTimeout(s),clearInterval(n),t();}),o.closeIdleConnections?.();})}async function Ze(r,e=fetch){try{let o=await e(`http://127.0.0.1:${r}/ping`,{signal:AbortSignal.timeout(1500)});if(!o.ok)return null;let t=await o.json();return t.ok===!0?{version:String(t.version??"")}:null}catch{return null}}async function Qe(r,e,o=Ze){if(e.code==="EADDRINUSE"){let t=await o(r);if(t){let s=`http://localhost:${r}`,n=t.version?` (v${t.version})`:"";return {existingUrl:s,exitCode:0,message:`${a.yellow("devcoach UI is already running at")} ${a.cyan(c$1(s))}${n}
|
|
637
|
+
${a.dim("Open it there, start another with --port <n>, or stop it with: devcoach ui --stop")}`}}return {exitCode:1,message:`${a.red(`\u2717 Port ${r} is already in use by another process`)}
|
|
638
|
+
${a.dim("Pick another port: devcoach ui --port <n>")}`}}return e.code==="EACCES"?{exitCode:1,message:a.red(`\u2717 Port ${r} is not allowed (permission denied) \u2014 use a port \u2265 1024`)}:{exitCode:1,message:a.red(`\u2717 Could not start the devcoach UI on port ${r}: ${e.message}`)}}function xt(r,e={}){let o=false,t=(c,d)=>{if(o){d&&process.exit(130);return}o=true,process.stdout.isTTY&&console.log(a.dim(`devcoach UI stopping (${c})\u2026`)),We(n).then(()=>{d&&process.exit(0);});},s=Ve({onShutdown:()=>t("stop requested",false)}),n=serve({fetch:s.fetch,port:r,hostname:"127.0.0.1"},c=>{let d=`http://localhost:${c.port}`;console.log(`devcoach UI running at ${a.cyan(c$1(d))}`),process.stdout.isTTY&&console.log(a.dim("Press Ctrl+C to stop")),e.onReady?.(d);}),l=["SIGINT","SIGTERM","SIGHUP"],g={SIGINT:()=>t("SIGINT",true),SIGTERM:()=>t("SIGTERM",true),SIGHUP:()=>t("SIGHUP",true)};if(e.handleSignals!==false)for(let c of l)process.on(c,g[c]);return n.once("error",c=>{for(let d of l)process.off(d,g[d]);Qe(r,c).then(d=>{if(e.onListenError){e.onListenError(d);return}console.error(d.message),process.exit(d.exitCode);});}),n}async function ut(r,e=fetch){try{return (await e(`http://127.0.0.1:${r}/shutdown`,{method:"POST",signal:AbortSignal.timeout(3e3)})).ok}catch{return false}}
|
|
639
|
+
export{Ve as createApp,Qe as explainListenError,We as gracefulShutdown,Ze as pingUi,xt as startUi,ut as stopUi};
|
package/dist/bin.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var t=process.argv[2]??"",r=new Set(["stop-hook","prompt-hook","gemini-stop-hook","gemini-prompt-hook","codex-stop-hook","codex-prompt-hook","onboard-hook","lesson-ready"]);try{if(r.has(t)){let{runHook:o}=await import('./hooks-
|
|
2
|
+
var t=process.argv[2]??"",r=new Set(["stop-hook","prompt-hook","gemini-stop-hook","gemini-prompt-hook","codex-stop-hook","codex-prompt-hook","onboard-hook","lesson-ready"]);try{if(r.has(t)){let{runHook:o}=await import('./hooks-5YA5PLBW.js');o(t);}else {let{runCli:o}=await import('./commands-ECVEHTRX.js');await o();}}catch(o){console.error(o),process.exit(1);}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {R,Q}from'./chunk-
|
|
2
|
+
import {R,Q}from'./chunk-JEFYE33I.js';import {execFileSync}from'child_process';import {basename}from'path';import U from'dns';import P from'http';import A from'https';import {isIP}from'net';function f(...e){try{return execFileSync("git",e,{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 T(e){if(!e)return [null,null];let t=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(e);if(t){let o=(t[1]??"").toLowerCase();return [t[2]??e,w[o]??"local"]}let n=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(e);if(n){let o=(n[1]??"").toLowerCase();return [n[2]??e,w[o]??"local"]}return [e,"local"]}function F(){return f("config","user.name")}function G(){let e=process.cwd(),t=f("rev-parse","--abbrev-ref","HEAD"),n=f("rev-parse","HEAD"),o=f("remote","get-url","origin"),[s,l]=T(o),u;return s?u=s.replace(/\/+$/,"").split("/").pop()??null:u=basename(e)||null,t==="HEAD"&&(t=null),{project:u,repository:s,branch:t,commit_hash:n,folder:e,repository_platform:l}}var y="That URL is too large to be a lesson.",m="Only public http(s) URLs can be imported.",k=new Set([301,302,303,307,308]);function E(e){return /^https?:\/\/\S+$/i.test(e.trim())}function x(e){let[t=0,n=0]=e.split(".").map(Number);return t===0||t===10||t===127||t===169&&n===254||t===172&&n>=16&&n<=31||t===192&&n===168||t===100&&n>=64&&n<=127||t>=224}function L(e){let t=e.replace(/^\[|\]$/g,"").replace(/%.*$/,"").toLowerCase();if(isIP(t)===4)return x(t);if(isIP(t)!==6)return true;let n=/^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(t);if(n?.[1])return x(n[1]);if(t==="::"||t==="::1")return true;let o=t.split(":")[0]??"";return !!(/^f[cd]/.test(o)||/^fe[89ab]/.test(o))}async function $(e){return (await U.promises.lookup(e,{all:true})).map(n=>n.address)}async function C(e,t){let n=e.hostname.replace(/^\[|\]$/g,"").toLowerCase();if(n==="localhost"||n.endsWith(".localhost"))throw new R(m);if(isIP(n)){if(L(n))throw new R(m);return n}let o;try{o=await t(n);}catch{throw new R("The URL could not be reached \u2014 copy the lesson text instead.")}let s=o[0];if(!s||o.some(l=>L(l)))throw new R(m);return s}var v=(e,t,n)=>new Promise((o,s)=>{let l=isIP(t)===6?6:4,u=(r,a,h)=>{a.all?h(null,[{address:t,family:l}]):h(null,t,l);},p=(e.protocol==="https:"?A:P).request(e,{method:"GET",lookup:u,signal:n,headers:{accept:"text/plain, text/markdown, application/json;q=0.9, */*;q=0.5","user-agent":`devcoach/${Q}`}},r=>{let a=r.headers["content-length"];o({status:r.statusCode??0,location:r.headers.location??null,contentLength:a===void 0?null:Number(a),body:r,abort:()=>r.destroy()});});p.on("error",s),p.end();});async function O(e,t){if(e.contentLength!==null&&e.contentLength>t)throw e.abort(),new R(y);let n=[],o=0;for await(let s of e.body){if(o+=s.byteLength,o>t)throw e.abort(),new R(y);n.push(s);}return Buffer.concat(n).toString("utf8")}async function V(e,t={}){let n=t.timeoutMs??5e3,o=t.maxBytes??256*1024,s=t.maxRedirects??3,l=t.lookup??$,u=t.transport??v;if(!E(e))throw new R("Only http(s) URLs can be imported.");let g=new AbortController,p=setTimeout(()=>g.abort(),n);try{let r=new URL(e.trim());for(let a=0;;a++){let h=await C(r,l),c=await u(r,h,g.signal);if(k.has(c.status)){if(c.abort(),!c.location||a>=s)throw new R("The URL redirects too many times \u2014 copy the lesson text instead.");if(r=new URL(c.location,r),!/^https?:$/.test(r.protocol))throw new R(m);continue}if(c.status<200||c.status>=300)throw c.abort(),new R(`The URL answered ${c.status} \u2014 nothing to import there.`);return await O(c,o)}}catch(r){if(r instanceof R)throw r;let a=r?.name==="AbortError"?"timed out":"could not be reached";throw new R(`The URL ${a} \u2014 copy the lesson text instead.`)}finally{clearTimeout(p);}}export{F as a,G as b,E as c,V as d};
|
|
@@ -65,7 +65,7 @@ import {z as z$1}from'zod';import {existsSync,readFileSync,mkdirSync,writeFileSy
|
|
|
65
65
|
CREATE INDEX IF NOT EXISTS idx_lessons_feedback ON lessons (feedback);
|
|
66
66
|
CREATE INDEX IF NOT EXISTS idx_lessons_topic_id ON lessons (topic_id);
|
|
67
67
|
`),tt(e),nt(e);}function tt(e){for(let t of ["ALTER TABLE lessons ADD COLUMN body TEXT","ALTER TABLE lessons ADD COLUMN imported INTEGER NOT NULL DEFAULT 0","ALTER TABLE lessons ADD COLUMN shared_by TEXT"])try{e.exec(t);}catch{}}function nt(e){for(let[t,n]of Object.entries(qe))u(e,"INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)",t,n);}var re="(id, timestamp, topic_id, categories, title, level, summary, body, task_context, project, repository, branch, commit_hash, folder, repository_platform, starred, feedback, imported, shared_by)",ie="(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";function ae(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,e.imported?1:0,e.shared_by]}function Qt(e,t){u(e,`INSERT OR REPLACE INTO lessons ${re} VALUES ${ie}`,...ae(t));}function G(e,t){return u(e,`INSERT OR IGNORE INTO lessons ${re} VALUES ${ie}`,...ae(t))>0}var st=[["project","project"],["repository","repository"],["branch","branch"],["commit","commit_hash"]];function ot(e,t){if(e.date_from==null&&e.date_to==null){let n=mt(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 ce(e){let t=[],n=[],s=(r,...a)=>{t.push(r),n.push(...a);};ot(e,s),e.category!=null&&s("categories LIKE ?",`%"${e.category}"%`),e.level!=null&&s("level = ?",e.level);for(let[r,a]of st){let i=e[r];i!=null&&s(`${a} LIKE ?`,`%${i}%`);}if(e.starred!=null&&s("starred = ?",e.starred?1:0),e.imported!=null&&s("imported = ?",e.imported?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 K(e,t={}){let{where:n,params:s}=ce(t),o=h(e,`SELECT COUNT(*) AS n FROM lessons ${n}`,...s);return Number(o?.n??0)}var rt=new Set(["timestamp","level","topic_id","title","feedback"]),it=new Set(["project","repository","branch","commit_hash","repository_platform"]);function en(e,t={}){let{where:n,params:s}=ce(t),o=t.sort&&rt.has(t.sort)?t.sort:"timestamp",r=(t.order??"desc").toLowerCase()==="asc"?"ASC":"DESC",a=`SELECT * FROM lessons ${n} ORDER BY ${o} ${r}`,i=[...s];if(t.page!=null){let l=t.per_page??25;a+=" LIMIT ? OFFSET ?",i.push(l,(t.page-1)*l);}return f(e,a,...i).map(Z)}function tn(e,t){return u(e,"DELETE FROM lessons WHERE id = ?",t)>0}function nn(e,t,n){return u(e,"UPDATE lessons SET starred = ? WHERE id = ?",n?1:0,t)>0}function le(e,t,n){u(e,"UPDATE lessons SET feedback = ? WHERE id = ?",n||null,t);let s=h(e,"SELECT topic_id FROM lessons WHERE id = ?",t);return s?s.topic_id:null}function at(e){return f(e,"SELECT * FROM lessons ORDER BY timestamp DESC").map(Z)}function H(e,t){let n=0,s=0,o=0;for(let r of t){let a;try{a=T(r);}catch{o+=1;continue}G(e,a)?n+=1:s+=1;}return {inserted:n,duplicated:s,invalid:o}}function sn(e,t){if(!it.has(t))throw new Error(`Column not allowed: ${JSON.stringify(t)}`);return f(e,`SELECT DISTINCT ${t} FROM lessons WHERE ${t} IS NOT NULL ORDER BY ${t}`).map(s=>s[t])}function ue(e,t){let n=h(e,"SELECT * FROM lessons WHERE id = ?",t);return n?Z(n):null}function on(e){return f(e,"SELECT DISTINCT value FROM lessons, json_each(lessons.categories) ORDER BY value").map(n=>n.value)}function pe(e){return f(e,"SELECT DISTINCT topic_id FROM lessons").map(t=>t.topic_id)}function D(e,t){let n=h(e,"SELECT COUNT(*) AS n FROM lessons WHERE timestamp >= ? AND imported = 0",t);return Number(n?.n??0)}function de(e){let t=h(e,"SELECT timestamp FROM lessons WHERE imported = 0 ORDER BY timestamp DESC LIMIT 1");return t?t.timestamp:null}function I(e){let t={};for(let n of f(e,"SELECT topic, confidence FROM knowledge"))t[n.topic]=Number(n.confidence);return t}function W(e){return f(e,"SELECT topic, confidence FROM knowledge ORDER BY topic").map(t=>({topic:t.topic,confidence:Number(t.confidence)}))}function Y(e){let t=f(e,"SELECT group_name FROM knowledge_group_names ORDER BY group_name").map(s=>s.group_name),n=new Map;for(let s of f(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 x(e,t,n){let s=Math.max(0,Math.min(10,n)),o=new Date().toISOString();u(e,`INSERT INTO knowledge (topic, confidence, updated_at) VALUES (?, ?, ?)
|
|
68
|
-
ON CONFLICT(topic) DO UPDATE SET confidence = excluded.confidence, updated_at = excluded.updated_at`,t,s,o);}function rn(e,t){let n=u(e,"DELETE FROM knowledge WHERE topic = ?",t);return u(e,"DELETE FROM knowledge_groups WHERE topic = ?",t),n>0}function te(e,t){let n=t.trim();if(!n)throw new Error("Group name must not be empty");return u(e,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",n)>0}function an(e,t){return u(e,"DELETE FROM knowledge_groups WHERE group_name = ?",t),u(e,"DELETE FROM knowledge_group_names WHERE group_name = ?",t)>0}function ct(e,t,n){u(e,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",n),u(e,"DELETE FROM knowledge_groups WHERE topic = ?",t),u(e,"INSERT OR IGNORE INTO knowledge_groups (group_name, topic) VALUES (?, ?)",n,t);}function cn(e,t){u(e,"DELETE FROM knowledge_groups WHERE topic = ?",t);}function b(e){let t={};for(let g of f(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,a=t.ui_theme??"system",i=a==="dark"||a==="light"||a==="system"?a:"system",l=t.nudge_every,m=l!==void 0?Number.parseInt(l,10):10,S=t.nudge_scope==="global"?"global":"session";return {max_per_day:r!==void 0?Number.parseInt(r,10):2,min_gap_minutes:o,ui_theme:i,nudge_every:Number.isFinite(m)&&m>=0?m:10,nudge_scope:S,share_name:t.share_name?.trim()||null}}function y(e,t,n){u(e,"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",t,n);}function me(e,t,n){let s=h(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&&u(e,"DELETE FROM nudge_state WHERE session_id NOT IN (SELECT session_id FROM nudge_state ORDER BY updated_at DESC LIMIT ?)",Ve),n==="global"?z(e,t,n):o}function ln(e){return f(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 z(e,t,n){let s=n==="global"?h(e,"SELECT COALESCE(SUM(interactions), 0) AS n FROM nudge_state"):h(e,"SELECT interactions AS n FROM nudge_state WHERE session_id = ?",t);return Number(s?.n??0)}function un(e){u(e,"DELETE FROM nudge_state"),u(e,"UPDATE cue_state SET pending = 0 WHERE id = 1");}var J=3;function q(e){let t=h(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 ge(e){u(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()),u(e,"DELETE FROM nudge_state");}function pn(e,t){u(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),u(e,"DELETE FROM nudge_state");}function V(e){let t=h(e,"SELECT COUNT(*) AS n FROM knowledge");return {knowledge_ready:Number(t?.n??0)>0}}function dn(e){let t={};for(let n of ["project","repository","branch","repository_platform"]){let s=h(e,`SELECT ${n} AS v, COUNT(*) AS c FROM lessons WHERE ${n} IS NOT NULL AND imported = 0 GROUP BY ${n} ORDER BY c DESC LIMIT 1`);t[n]=s?s.v:null;}return t}function mn(e){let t=b(e),n=at(e),s=W(e),o=Y(e),r=new Map;for(let l of o)for(let m of l.topics)r.set(m,l.name);let a={groups:o.map(l=>l.name),topics:s.map(l=>({topic:l.topic,confidence:l.confidence,group:r.get(l.topic)??null}))},i={[$]:strToU8(JSON.stringify(t,null,2)),[M]:strToU8(JSON.stringify(n,null,2)),[P]:strToU8(JSON.stringify(a,null,2))};return existsSync(N)&&(i[j]=strToU8(readFileSync(N,"utf8"))),zipSync(i)}function lt(e,t,n){if(!($ in t))return;let s=JSON.parse(strFromU8(t[$]));"max_per_day"in s&&y(e,"max_per_day",String(s.max_per_day)),"min_gap_minutes"in s&&y(e,"min_gap_minutes",String(s.min_gap_minutes)),(s.ui_theme==="system"||s.ui_theme==="dark"||s.ui_theme==="light")&&y(e,"ui_theme",s.ui_theme),"nudge_every"in s&&y(e,"nudge_every",String(s.nudge_every)),(s.nudge_scope==="session"||s.nudge_scope==="global")&&y(e,"nudge_scope",s.nudge_scope),typeof s.share_name=="string"&&y(e,"share_name",Je(s.share_name)),n.settings=1;}function ut(e,t,n){if(!(P in t))return;let s=JSON.parse(strFromU8(t[P]));if(!(s&&typeof s=="object"&&Array.isArray(s.topics))){for(let[r,a]of Object.entries(s))x(e,r,a);n.topics=Object.keys(s).length;return}let o=0;for(let r of s.groups??[])te(e,r)&&(o+=1);for(let r of s.topics){x(e,r.topic,r.confidence);let a=r.group;a&&a!=="Other"&&(te(e,a)&&(o+=1),ct(e,r.topic,a));}n.topics=s.topics.length,n.groups=o;}function pt(e,t,n){if(!(M in t))return;let s=JSON.parse(strFromU8(t[M])),{inserted:o,duplicated:r,invalid:a}=H(e,s);n.lessons=o,n.skipped=r,n.invalid=a;}function dt(e,t){j in e&&(mkdirSync(dirname(N),{recursive:true}),writeFileSync(N,strFromU8(e[j]),"utf8"),t.learning_state=1);}function gn(e,t){let n={settings:0,topics:0,groups:0,lessons:0,skipped:0,invalid:0,learning_state:0},s=unzipSync(t);return lt(e,s,n),ut(e,s,n),pt(e,s,n),dt(s,n),n}function mt(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 Z(e){let t=[];try{let n=JSON.parse(e.categories);Array.isArray(n)&&(t=n);}catch{t=[];}return T({...e,categories:t,starred:!!e.starred,imported:!!e.imported})}var fe={version:"2.3.
|
|
68
|
+
ON CONFLICT(topic) DO UPDATE SET confidence = excluded.confidence, updated_at = excluded.updated_at`,t,s,o);}function rn(e,t){let n=u(e,"DELETE FROM knowledge WHERE topic = ?",t);return u(e,"DELETE FROM knowledge_groups WHERE topic = ?",t),n>0}function te(e,t){let n=t.trim();if(!n)throw new Error("Group name must not be empty");return u(e,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",n)>0}function an(e,t){return u(e,"DELETE FROM knowledge_groups WHERE group_name = ?",t),u(e,"DELETE FROM knowledge_group_names WHERE group_name = ?",t)>0}function ct(e,t,n){u(e,"INSERT OR IGNORE INTO knowledge_group_names (group_name) VALUES (?)",n),u(e,"DELETE FROM knowledge_groups WHERE topic = ?",t),u(e,"INSERT OR IGNORE INTO knowledge_groups (group_name, topic) VALUES (?, ?)",n,t);}function cn(e,t){u(e,"DELETE FROM knowledge_groups WHERE topic = ?",t);}function b(e){let t={};for(let g of f(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,a=t.ui_theme??"system",i=a==="dark"||a==="light"||a==="system"?a:"system",l=t.nudge_every,m=l!==void 0?Number.parseInt(l,10):10,S=t.nudge_scope==="global"?"global":"session";return {max_per_day:r!==void 0?Number.parseInt(r,10):2,min_gap_minutes:o,ui_theme:i,nudge_every:Number.isFinite(m)&&m>=0?m:10,nudge_scope:S,share_name:t.share_name?.trim()||null}}function y(e,t,n){u(e,"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",t,n);}function me(e,t,n){let s=h(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&&u(e,"DELETE FROM nudge_state WHERE session_id NOT IN (SELECT session_id FROM nudge_state ORDER BY updated_at DESC LIMIT ?)",Ve),n==="global"?z(e,t,n):o}function ln(e){return f(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 z(e,t,n){let s=n==="global"?h(e,"SELECT COALESCE(SUM(interactions), 0) AS n FROM nudge_state"):h(e,"SELECT interactions AS n FROM nudge_state WHERE session_id = ?",t);return Number(s?.n??0)}function un(e){u(e,"DELETE FROM nudge_state"),u(e,"UPDATE cue_state SET pending = 0 WHERE id = 1");}var J=3;function q(e){let t=h(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 ge(e){u(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()),u(e,"DELETE FROM nudge_state");}function pn(e,t){u(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),u(e,"DELETE FROM nudge_state");}function V(e){let t=h(e,"SELECT COUNT(*) AS n FROM knowledge");return {knowledge_ready:Number(t?.n??0)>0}}function dn(e){let t={};for(let n of ["project","repository","branch","repository_platform"]){let s=h(e,`SELECT ${n} AS v, COUNT(*) AS c FROM lessons WHERE ${n} IS NOT NULL AND imported = 0 GROUP BY ${n} ORDER BY c DESC LIMIT 1`);t[n]=s?s.v:null;}return t}function mn(e){let t=b(e),n=at(e),s=W(e),o=Y(e),r=new Map;for(let l of o)for(let m of l.topics)r.set(m,l.name);let a={groups:o.map(l=>l.name),topics:s.map(l=>({topic:l.topic,confidence:l.confidence,group:r.get(l.topic)??null}))},i={[$]:strToU8(JSON.stringify(t,null,2)),[M]:strToU8(JSON.stringify(n,null,2)),[P]:strToU8(JSON.stringify(a,null,2))};return existsSync(N)&&(i[j]=strToU8(readFileSync(N,"utf8"))),zipSync(i)}function lt(e,t,n){if(!($ in t))return;let s=JSON.parse(strFromU8(t[$]));"max_per_day"in s&&y(e,"max_per_day",String(s.max_per_day)),"min_gap_minutes"in s&&y(e,"min_gap_minutes",String(s.min_gap_minutes)),(s.ui_theme==="system"||s.ui_theme==="dark"||s.ui_theme==="light")&&y(e,"ui_theme",s.ui_theme),"nudge_every"in s&&y(e,"nudge_every",String(s.nudge_every)),(s.nudge_scope==="session"||s.nudge_scope==="global")&&y(e,"nudge_scope",s.nudge_scope),typeof s.share_name=="string"&&y(e,"share_name",Je(s.share_name)),n.settings=1;}function ut(e,t,n){if(!(P in t))return;let s=JSON.parse(strFromU8(t[P]));if(!(s&&typeof s=="object"&&Array.isArray(s.topics))){for(let[r,a]of Object.entries(s))x(e,r,a);n.topics=Object.keys(s).length;return}let o=0;for(let r of s.groups??[])te(e,r)&&(o+=1);for(let r of s.topics){x(e,r.topic,r.confidence);let a=r.group;a&&a!=="Other"&&(te(e,a)&&(o+=1),ct(e,r.topic,a));}n.topics=s.topics.length,n.groups=o;}function pt(e,t,n){if(!(M in t))return;let s=JSON.parse(strFromU8(t[M])),{inserted:o,duplicated:r,invalid:a}=H(e,s);n.lessons=o,n.skipped=r,n.invalid=a;}function dt(e,t){j in e&&(mkdirSync(dirname(N),{recursive:true}),writeFileSync(N,strFromU8(e[j]),"utf8"),t.learning_state=1);}function gn(e,t){let n={settings:0,topics:0,groups:0,lessons:0,skipped:0,invalid:0,learning_state:0},s=unzipSync(t);return lt(e,s,n),ut(e,s,n),pt(e,s,n),dt(s,n),n}function mt(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 Z(e){let t=[];try{let n=JSON.parse(e.categories);Array.isArray(n)&&(t=n);}catch{t=[];}return T({...e,categories:t,starred:!!e.starred,imported:!!e.imported})}var fe={version:"2.3.1"};var he=fe.version;function _e(e){let t=[...e].length,n=Math.max(34-t-2,2),s=Math.floor(n/2);return `### ${"\u2500".repeat(s)} ${e} ${"\u2500".repeat(n-s)}`}function Ee(e){let t=e.level.charAt(0).toUpperCase()+e.level.slice(1),n=e.categories.join(" \xB7 ");return [_e("\u{1F393} devcoach"),`**${e.title}** \xB7 ${n} \xB7 ${t}`,"",e.body?.trim()||e.summary,"",_e(`${e.topic_id} \xB7 ${e.level}`)].join(`
|
|
69
69
|
`)}var A="devcoach.lesson",R=1,yt=`devcoach:lesson:${R}:`,St="https://ultimaphoenix.github.io/dev-coach/lesson",Tt=".devcoach.md",bt=256*1024,d=class extends Error{},L=z$1.string().nullish(),Lt=["task_context","project","branch","commit_hash"],Rt=new Set(["github","gitlab","bitbucket"]),Se=z$1.object({format:z$1.literal(A),version:z$1.literal(R),lesson:z$1.object({title:z$1.string().min(1),summary:z$1.string(),body:z$1.string().nullable(),topic_id:z$1.string().min(1),categories:z$1.array(z$1.string()),level:C,task_context:L,project:L,repository:L,branch:L,commit_hash:L,repository_platform:U.nullish()}),origin:z$1.object({id:z$1.string().min(1),timestamp:z$1.string()}),shared_by:z$1.string().nullable(),shared_at:z$1.string(),app_version:z$1.string()});function wn(e,t){let n={title:e.title,summary:e.summary,body:e.body,topic_id:e.topic_id,categories:e.categories,level:e.level};if(t.includeContext){for(let s of Lt)e[s]!=null&&(n[s]=e[s]);e.repository_platform&&Rt.has(e.repository_platform)&&(e.repository!=null&&(n.repository=e.repository),n.repository_platform=e.repository_platform);}return {format:A,version:R,lesson:n,origin:{id:e.id,timestamp:e.timestamp},shared_by:t.sharedBy,shared_at:F(t.now??new Date),app_version:he}}function Nn(e){return e.anonymous?null:e.explicit!=null?e.explicit.trim()||null:e.setting?.trim()||e.gitUserName?.trim()||null}function Te(e){let t=deflateSync(strToU8(JSON.stringify(e)),{level:9});return yt+Buffer.from(t).toString("base64url")}var be=/devcoach:lesson:(\d+):([A-Za-z0-9_-]+)/,wt=64e3,Q=512,v=class extends Error{};function Nt(e,t){let n=[],s=0,o=new Inflate(i=>{if(s+=i.length,s>t)throw new v("too large");n.push(i);});for(let i=0;i<e.length;i+=Q)o.push(e.subarray(i,i+Q),i+Q>=e.length);let r=new Uint8Array(s),a=0;for(let i of n)r.set(i,a),a+=i.length;return r}function xt(e){let t=new RegExp(`^${be.source}$`).exec(e.trim());if(!t)throw new d("That is not a devcoach lesson code.");if(Number(t[1])!==R)throw new d(`This lesson was shared by a newer devcoach (format ${t[1]}) \u2014 upgrade to import it.`);let n=t[2]??"";if(n.length>wt)throw new d("This lesson code is too large to be a lesson.");let s;try{let o=Nt(new Uint8Array(Buffer.from(n,"base64url")),bt);s=strFromU8(o);}catch(o){throw o instanceof v?new d("This lesson code is too large to be a lesson."):new d("This lesson code is damaged or incomplete \u2014 copy it again.")}return Le(s)}function Le(e){let t;try{t=JSON.parse(e);}catch{throw new d("This lesson code is damaged or incomplete \u2014 copy it again.")}let n=Se.safeParse(t);if(!n.success)throw new d("This is not a valid devcoach lesson.");return n.data}function xn(e,t=Te(e)){let n=e.shared_by?`Shared by ${e.shared_by} with devcoach`:"Shared with devcoach";return [Ee(e.lesson),"",`${n} \u2014 paste it to your agent or run: devcoach import`,t].join(`
|
|
70
70
|
`)}function On(e,t=Te(e)){return `${St}#${t}`}function Ot(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,60)||"lesson"}function Dn(e){return `${Ot(e.origin.id)}${Tt}`}var Dt=/^[A-Za-z0-9_.+:@/-]+$/,k=e=>Dt.test(e)?e:JSON.stringify(e),It=e=>`[${e.map(k).join(", ")}]`;function In(e){let t=e.lesson,n=["---",`format: ${A}`,`version: ${R}`,`title: ${JSON.stringify(t.title)}`,`topic_id: ${k(t.topic_id)}`,`level: ${t.level}`,`categories: ${It(t.categories)}`,`summary: ${JSON.stringify(t.summary)}`,`origin_id: ${k(e.origin.id)}`,`origin_timestamp: ${e.origin.timestamp}`,`shared_by: ${e.shared_by==null?"null":JSON.stringify(e.shared_by)}`,`shared_at: ${e.shared_at}`,`app_version: ${k(e.app_version)}`];for(let s of ["task_context","project","repository","branch","commit_hash","repository_platform"]){let o=t[s];o!=null&&n.push(`${s}: ${JSON.stringify(o)}`);}return n.push("---","",t.body??t.summary,""),n.join(`
|
|
71
71
|
`)}function kt(e){let t=[],n="",s=false;for(let o=0;o<e.length;o++){let r=e[o]??"";s?(n+=r,r==="\\"?n+=e[++o]??"":r==='"'&&(s=false)):r==='"'?(s=true,n+=r):r===","?(t.push(n),n=""):n+=r;}return t.push(n),t}function Re(e){let t=e.trim();if(t==="null"||t==="")return null;if(t.startsWith('"'))return JSON.parse(t);if(t.startsWith("[")&&t.endsWith("]")){let n=t.slice(1,-1).trim();return n?kt(n).map(s=>Re(s)):[]}return t}function vt(e){let t=e.replace(/\r\n?/g,`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {i,o,M,ga,h as h$1,n,fa}from'./chunk-
|
|
2
|
+
import {i,o,M,ga,h as h$1,n,fa}from'./chunk-JEFYE33I.js';import {existsSync,mkdirSync,statSync,writeFileSync,appendFileSync,fstatSync,readFileSync}from'fs';import {join}from'path';function D(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}}catch{return {stop_hook_active:false,permission_mode:null,session_id:null}}}function l(){let e={stop_hook_active:false,permission_mode:null,session_id:null};try{let o=fstatSync(0);return !o.isFIFO()&&!o.isFile()&&!o.isSocket()?e:D(readFileSync(0,"utf8"))}catch{return e}}function A(e){return e.stop_hook_active||e.permission_mode==="plan"}function s(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}
|
|
3
3
|
`);}catch{}}var N={claude:"block",gemini:"deny",codex:"block"};function h(e,o,t){let n={decision:N[e],reason:o};t&&(n.systemMessage=t),process.stdout.write(`${JSON.stringify(n)}
|
|
4
4
|
`),process.exit(0);}var f="devcoach: the user has no coaching profile yet. Ask them how they want to set it up and STRONGLY recommend Automatic \u2014 devcoach scans their full Claude Code history across all their projects (call get_onboarding for the detected stack and per-project evidence) so the profile reflects what they actually build. Also offer Guided (a short conversation to map topics and confidence levels) and Import (restore from a backup), but lead with Automatic; 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 $(e=l()){if(A(e)&&process.exit(0),existsSync(i)){let o$1;try{o$1=o(t=>M(t).knowledge_ready);}catch(t){s("onboard-hook",e.session_id,`error: ${t}`),process.exit(0);}o$1&&process.exit(0);}s("onboard-hook",e.session_id,"onboarding cue"),h("claude",f);}var a=10,y={claude:{cue:"Invoke the `devcoach` skill (Skill tool)",prime:"invoke the `devcoach` skill (Skill tool)"},gemini:{cue:"Activate the `devcoach` skill (activate_skill tool)",prime:"activate the `devcoach` skill (activate_skill tool)"},codex:{cue:"Activate the `devcoach` skill",prime:"activate the `devcoach` skill"}};function E(e,o="claude"){let t=e%a===0,n=Math.ceil(e/a)*a,i=t?`This is lesson #${e} \u2014 a notebook checkpoint (every ${a} lessons): after log_lesson returns the feedback, also read references/calibration.md in the devcoach skill and run its notebook-update step (a direct file write), recording "(updated after ${e} lessons)".`:`Skip the notebook update this time \u2014 it is refreshed only every ${a} lessons (next checkpoint at lesson #${n}).`;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.
|
|
5
5
|
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {e,g,b,d as d$1}from'./chunk-
|
|
2
|
+
import {e,g,b,d as d$1}from'./chunk-UHF6ARVR.js';export{c as buildLessonCue,d as cmdLessonReady,b as cmdOnboardHook,g as cmdPromptHook,e as cmdStopHook,a as parseHookPayload}from'./chunk-UHF6ARVR.js';import {a,d as d$2,e as e$1,g as g$1,b as b$3,f}from'./chunk-QU6UQJWC.js';import {c as c$1,b as b$2,a as a$2,e as e$2,f as f$1}from'./chunk-MRABMPXO.js';import {a as a$1,b as b$1,c as c$2,d as d$3}from'./chunk-4PFP67XN.js';import {Q as Q$1,o,$,r,x,t,s,ba,G,T,H as H$1,m,S as S$1,Y as Y$1,Z as Z$1,X as X$1,W as W$1,U,da,R,_ as _$1,ca,l,A as A$1,E,B,C as C$1,D,F,O as O$1,z,v,j as j$1,P,h,i,M,I as I$1,K,ga}from'./chunk-JEFYE33I.js';import {spawn,spawnSync,execFileSync}from'child_process';import {writeFileSync,existsSync,readFileSync,rmSync,mkdirSync,accessSync,constants}from'fs';import {createInterface}from'readline/promises';import {text}from'stream/consumers';import {Command}from'commander';import {homedir,platform}from'os';import {join,dirname}from'path';var c=(e="")=>{console.log(e);};function fo(){let e=platform();if(e==="darwin")return join(homedir(),"Library","Application Support","Claude","claude_desktop_config.json");if(e==="win32"){let t=process.env.APPDATA??join(homedir(),"AppData","Roaming");return join(t,"Claude","claude_desktop_config.json")}let o=process.env.XDG_CONFIG_HOME??join(homedir(),".config");return join(o,"Claude","claude_desktop_config.json")}var I=join(homedir(),".claude","settings.json"),Y=join(homedir(),".claude","skills","devcoach"),fe=join(homedir(),".gemini"),_=join(fe,"settings.json"),Eo=join(fe,"extensions","devcoach","gemini-extension.json"),he=join(homedir(),".codex"),O=join(he,"hooks.json"),W=join(he,"config.toml"),H=join(homedir(),".agents","skills","devcoach");function ho(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 X=e=>ho(e)!==null;function ko(){return X("devcoach")?{command:"devcoach",args:["mcp"]}:{command:"npx",args:["-y","devcoach","mcp"]}}function ke(){let e=ho("devcoach");return e?e.includes("fnm_multishells")?"devcoach":e:"npx -y devcoach"}function mo(e,o,t){if(!X(e))return "";t&&spawnSync(e,t,{encoding:"utf8"});let i=spawnSync(e,o,{encoding:"utf8"});return i.status===0?`${a.green("\u2713")} Registered via \`${e} mcp add\``:`${i.stderr??""}${i.stdout??""}`.toLowerCase().includes("already")?`${a.yellow("Already registered")} in ${e} (use --force to overwrite)`:`${a.red(`${e} mcp add failed:`)} ${(i.stderr||i.stdout||"").trim()}`}function Mo(e,o){if(!X("claude"))return "";o&&spawnSync("claude",["mcp","remove","--scope",e,"devcoach"],{encoding:"utf8"});let t=ko(),i=spawnSync("claude",["mcp","add","--scope",e,"devcoach",t.command,"--",...t.args],{encoding:"utf8"});return i.status===0?`${a.green("\u2713")} Registered via \`claude mcp add\` (scope: ${e})`:`${i.stderr??""}${i.stdout??""}`.toLowerCase().includes("already")?`${a.yellow("Already registered")} in Claude Code (use --force to overwrite)`:`${a.red("claude mcp add failed:")} ${(i.stderr||i.stdout||"").trim()}`}function C(e){if(!existsSync(e))return {ok:true,data:{}};try{return {ok:!0,data:JSON.parse(readFileSync(e,"utf8"))??{}}}catch{return {ok:false,error:`${a.red("\u2717")} ${e} is not valid JSON \u2014 fix it and re-run (left unchanged)`}}}function ce(e,o,t){let i=C(e);if(!i.ok)return i.error;let s=i.data;s.mcpServers??={};let a$1=s.mcpServers;return a$1.devcoach&&!t?`${a.yellow("Already registered")} in ${e} (use --force to overwrite)`:(a$1.devcoach=o,mkdirSync(dirname(e),{recursive:true}),writeFileSync(e,`${JSON.stringify(s,null,2)}
|
|
3
3
|
`),`${a.green("\u2713")} Installed into ${e}`)}function No(){let e=ke();return {Stop:{hooks:[{type:"command",command:`${e} stop-hook`,timeout:60}]},UserPromptSubmit:{hooks:[{type:"command",command:`${e} prompt-hook`,timeout:30}]}}}function Ao(){let e=ke();return {AfterAgent:{hooks:[{type:"command",command:`${e} gemini-stop-hook`,timeout:6e4}]},BeforeAgent:{hooks:[{type:"command",command:`${e} gemini-prompt-hook`,timeout:3e4}]}}}function To(){let e=ke();return {Stop:{hooks:[{type:"command",command:`${e} codex-stop-hook`,timeout:60}]},UserPromptSubmit:{hooks:[{type:"command",command:`${e} codex-prompt-hook`,timeout:30}]}}}function be(e){return Object.entries(e.enabledPlugins??{}).some(([o,t])=>t&&o.startsWith("devcoach@"))}function q(){return existsSync(Eo)}function Z(e){return /dev-?coach/i.test(e)&&/\b(?:stop-hook|prompt-hook|onboard-hook|lesson-ready)\b/.test(e)}function ye(e,o){let t=C(e);if(!t.ok)return t.error;let i=t.data;i.hooks??={};let s=i.hooks,a$1=false;for(let[r,l]of Object.entries(o)){s[r]??=[];let m=s[r],k=m.map((u,g)=>[u,g]).filter(([u])=>(u.hooks??[]).some(g=>Z(g.command??""))).map(([,u])=>u),[p]=k;if(!(k.length===1&&p!==void 0&&JSON.stringify(m[p])===JSON.stringify(l))){for(let u of k.toReversed())m.splice(u,1);m.push(l),a$1=true;}}return a$1?(mkdirSync(dirname(e),{recursive:true}),writeFileSync(e,`${JSON.stringify(i,null,2)}
|
|
4
|
-
`),`${a.green("\u2713")} Hooks installed into ${e} (${Object.keys(o).join(" + ")})`):`${a.yellow("Already installed")} in ${e} (current layout)`}function jo(){let e=C(I);return e.ok?be(e.data)?`${a.yellow("Skipped")} \u2014 the devcoach plugin already provides the hooks (installing both would double-count interactions)`:ye(I,No()):e.error}function Po(){return q()?`${a.yellow("Skipped")} \u2014 the devcoach Gemini extension already provides the hooks (installing both would double-count interactions)`:ye(
|
|
5
|
-
`),`${a.green("\u2713")} Installed into ${e}`}function Go(e){return q()?`${a.yellow("Skipped")} \u2014 bundled with the devcoach Gemini extension`:pe(H,e)}function A(e,o){let t=C(e);return t.ok&&(t.data.hooks?.[o]??[]).some(i=>(i.hooks??[]).some(s=>Z(s.command??"")))}function $e(){let e=[{name:"Claude Code",dir:Y,cmd:"devcoach install",hooked:()=>A(I,"Stop")},{name:"Gemini/Codex",dir:H,cmd:"devcoach install --gemini (or --codex)",hooked:()=>A(
|
|
4
|
+
`),`${a.green("\u2713")} Hooks installed into ${e} (${Object.keys(o).join(" + ")})`):`${a.yellow("Already installed")} in ${e} (current layout)`}function jo(){let e=C(I);return e.ok?be(e.data)?`${a.yellow("Skipped")} \u2014 the devcoach plugin already provides the hooks (installing both would double-count interactions)`:ye(I,No()):e.error}function Po(){return q()?`${a.yellow("Skipped")} \u2014 the devcoach Gemini extension already provides the hooks (installing both would double-count interactions)`:ye(_,Ao())}function j(e){if(!existsSync(join(e,"SKILL.md")))return "missing";try{if(readFileSync(join(e,".devcoach-version"),"utf8").trim()===Q$1)return "current"}catch{}return "outdated"}function pe(e,o){if(!o&&j(e)==="current")return `${a.yellow("Already installed")} in ${e} (current version)`;mkdirSync(e,{recursive:true}),writeFileSync(join(e,"SKILL.md"),e$2());let t=f$1();if(t.length){let i=join(e,"references");mkdirSync(i,{recursive:true});for(let s of t)writeFileSync(join(i,s.name),s.content);}return writeFileSync(join(e,".devcoach-version"),`${Q$1}
|
|
5
|
+
`),`${a.green("\u2713")} Installed into ${e}`}function Go(e){return q()?`${a.yellow("Skipped")} \u2014 bundled with the devcoach Gemini extension`:pe(H,e)}function A(e,o){let t=C(e);return t.ok&&(t.data.hooks?.[o]??[]).some(i=>(i.hooks??[]).some(s=>Z(s.command??"")))}function $e(){let e=[{name:"Claude Code",dir:Y,cmd:"devcoach install",hooked:()=>A(I,"Stop")},{name:"Gemini/Codex",dir:H,cmd:"devcoach install --gemini (or --codex)",hooked:()=>A(_,"AfterAgent")||A(O,"Stop")}];for(let o of e){let t=j(o.dir);if(t==="outdated")return `${a.yellow("\u2192")} The devcoach ${o.name} skill is out of date \u2014 run ${a.bold(o.cmd)} to refresh it.`;if(t==="missing"&&o.hooked())return `${a.yellow("\u2192")} The devcoach ${o.name} skill is not installed \u2014 run ${a.bold(o.cmd)} to add it.`}return ""}function bo(e){let o=e.claudeCode||e.claudeDesktop||e.gemini||e.codex,t=e.claudeCode||!o,i=e.claudeDesktop||!o,s=ko(),a$1=false;if(c(a.bold("Setting up devcoach")+a.dim(` (${s.command} ${s.args.join(" ")})`)),c(),t){c(a.bold("Claude Code"));let r=Mo("user",e.force);if(!r){let l=join(homedir(),".claude.json");r=ce(l,{type:"stdio",env:{},...s},e.force),a$1=true;}c(` MCP server\u2026 ${r}`),e.skipHook||c(` Hooks\u2026 ${jo()}`),c(` Skill\u2026 ${pe(Y,e.force)}`),c();}if(i&&(c(a.bold("Claude Desktop")),c(` MCP server\u2026 ${ce(fo(),s,e.force)}`),a$1=true,c()),e.gemini){c(a.bold("Gemini CLI (beta)"));let r=mo("gemini",["mcp","add","-s","user","devcoach",s.command,...s.args],e.force?["mcp","remove","-s","user","devcoach"]:null);r||(r=ce(_,s,e.force)),c(` MCP server\u2026 ${r}`),e.skipHook||c(` Hooks\u2026 ${Po()}`),c(` Skill\u2026 ${Go(e.force)}`),c();}if(e.codex){c(a.bold("Codex CLI (beta)"));let r=mo("codex",["mcp","add","devcoach","--",s.command,...s.args],e.force?["mcp","remove","devcoach"]:null);r||(r=`${a.yellow("Manual step")} \u2014 codex CLI not found. Add to ${W}:
|
|
6
6
|
[mcp_servers.devcoach]
|
|
7
7
|
command = "${s.command}"
|
|
8
8
|
args = [${s.args.map(l=>`"${l}"`).join(", ")}]`),c(` MCP server\u2026 ${r}`),e.skipHook||c(` Hooks\u2026 ${ye(O,To())}`),c(` Skill\u2026 ${pe(H,e.force)}`),c(` ${a.yellow("\u2192")} Codex asks once to trust new hooks \u2014 approve devcoach on the next run.`),c();}a$1&&c(`${a.yellow("\u2192")} Restart Claude Desktop to pick up the new server.
|
|
9
9
|
`),c(a.dim(`Tip: run devcoach backup to export your profile, lessons and settings.
|
|
10
10
|
run devcoach restore <file> to import a backup on a new machine.
|
|
11
11
|
After upgrading devcoach (npm, Homebrew), re-run devcoach install to refresh the skill.`));}function ve(e){let o=[];for(let[t,i]of Object.entries(e.hooks??{}))for(let s of i??[])for(let a of s.hooks??[])Z(a.command??"")&&o.push({event:t,cmd:a});return o}function we(e,o,t){for(let{cmd:i}of o){let s=i.command.split(" ")[0]??"";s.startsWith("/")&&!existsSync(s)&&e.bad(`hook command not found: ${s} \u2014 re-run ${a.bold(t)}`),i.command.startsWith("npx ")&&e.warn("hook runs via npx \u2014 needs the npx cache (or network) on every stop"),i.timeout==null&&e.warn(`hook entry has no timeout (${i.command})`);}}function Fo(e){c(a.bold(`
|
|
12
|
-
Gemini CLI wiring (beta)`));let o=C(
|
|
13
|
-
Codex CLI wiring (beta)`));let o=C(O);if(!o.ok){e.bad(`${O} is not valid JSON \u2014 hooks cannot run`);return}let t=ve(o.data);t.length?(t.some(a=>a.event==="Stop")?e.ok("Stop hook wired"):e.warn(`no Stop hook \u2014 run ${a.bold("devcoach install --codex")}`),t.some(a=>a.event==="UserPromptSubmit")?e.ok("UserPromptSubmit priming hook wired"):e.warn(`no UserPromptSubmit priming hook \u2014 run ${a.bold("devcoach install --codex")} (lessons land more reliably)`),we(e,t,"devcoach install --codex"),e.warn("Codex runs hooks only after you trust them once \u2014 check the prompt on the next run")):e.warn(`no devcoach hooks in ${O} \u2014 run ${a.bold("devcoach install --codex")}`);let i=j(H);i==="current"?e.ok("skill installed (~/.agents/skills, current version)"):i==="outdated"?e.warn(`skill is out of date \u2014 run ${a.bold("devcoach install --codex")}`):e.warn(`skill not installed \u2014 run ${a.bold("devcoach install --codex")}`);let s=existsSync(W)?readFileSync(W,"utf8"):"";/\[mcp_servers\.devcoach\]/.test(s)?e.ok("MCP server registered (config.toml)"):e.warn(`MCP server not found in config.toml \u2014 it may be registered elsewhere (check with ${a.bold("codex mcp list")})`);}function
|
|
12
|
+
Gemini CLI wiring (beta)`));let o=C(_),t=q();if(!o.ok){e.bad(`${_} is not valid JSON \u2014 hooks cannot run`);return}let i=ve(o.data);t&&i.length?e.bad("devcoach hooks are registered TWICE (extension + settings.json) \u2014 interactions are double-counted. Uninstall the extension or remove the settings.json entries."):t?e.ok("hooks provided by the devcoach Gemini extension"):i.length?(i.some(a=>a.event==="AfterAgent")?e.ok("AfterAgent stop hook wired"):e.warn(`no AfterAgent stop hook \u2014 run ${a.bold("devcoach install --gemini")}`),i.some(a=>a.event==="BeforeAgent")?e.ok("BeforeAgent priming hook wired"):e.warn(`no BeforeAgent priming hook \u2014 run ${a.bold("devcoach install --gemini")} (lessons land more reliably)`),we(e,i,"devcoach install --gemini")):e.warn(`no devcoach hooks in ${_} \u2014 run ${a.bold("devcoach install --gemini")}`);let s=j(H);t?e.ok("skill bundled with the devcoach Gemini extension"):s==="current"?e.ok("skill installed (~/.agents/skills, current version)"):s==="outdated"?e.warn(`skill is out of date \u2014 run ${a.bold("devcoach install --gemini")}`):e.warn(`skill not installed \u2014 run ${a.bold("devcoach install --gemini")}`),o.data.mcpServers?.devcoach?e.ok("MCP server registered (settings.json)"):e.warn(`MCP server not found in settings.json \u2014 it may be registered elsewhere (check with ${a.bold("gemini mcp list")})`);}function Bo(e){c(a.bold(`
|
|
13
|
+
Codex CLI wiring (beta)`));let o=C(O);if(!o.ok){e.bad(`${O} is not valid JSON \u2014 hooks cannot run`);return}let t=ve(o.data);t.length?(t.some(a=>a.event==="Stop")?e.ok("Stop hook wired"):e.warn(`no Stop hook \u2014 run ${a.bold("devcoach install --codex")}`),t.some(a=>a.event==="UserPromptSubmit")?e.ok("UserPromptSubmit priming hook wired"):e.warn(`no UserPromptSubmit priming hook \u2014 run ${a.bold("devcoach install --codex")} (lessons land more reliably)`),we(e,t,"devcoach install --codex"),e.warn("Codex runs hooks only after you trust them once \u2014 check the prompt on the next run")):e.warn(`no devcoach hooks in ${O} \u2014 run ${a.bold("devcoach install --codex")}`);let i=j(H);i==="current"?e.ok("skill installed (~/.agents/skills, current version)"):i==="outdated"?e.warn(`skill is out of date \u2014 run ${a.bold("devcoach install --codex")}`):e.warn(`skill not installed \u2014 run ${a.bold("devcoach install --codex")}`);let s=existsSync(W)?readFileSync(W,"utf8"):"";/\[mcp_servers\.devcoach\]/.test(s)?e.ok("MCP server registered (config.toml)"):e.warn(`MCP server not found in config.toml \u2014 it may be registered elsewhere (check with ${a.bold("codex mcp list")})`);}function yo(){let e={ok:r=>c(` ${a.green("\u2713")} ${r}`),warn:r=>c(` ${a.yellow("\u2192")} ${r}`),bad:r=>c(` ${a.red("\u2717")} ${r}`)},{ok:o$1,warn:t,bad:i$1}=e;c(`
|
|
14
14
|
${a.bold("devcoach doctor")} ${a.dim(`v${Q$1}`)}
|
|
15
15
|
`),c(a.bold("Environment")),Number.parseInt(process.versions.node.split(".")[0]??"0",10)>=24?o$1(`Node ${process.versions.node} (\u2265 24)`):i$1(`Node ${process.versions.node} \u2014 devcoach needs Node \u2265 24 (embedded node:sqlite)`),c(a.bold(`
|
|
16
16
|
Claude Code wiring`));let a$1=C(I);if(!a$1.ok)i$1(`${I} is not valid JSON \u2014 hooks cannot run`);else {let r=be(a$1.data),l=ve(a$1.data);if(r&&l.length)i$1("devcoach hooks are registered TWICE (plugin + settings.json) \u2014 interactions are double-counted. Disable the plugin or remove the settings.json entries.");else if(r)o$1("hooks provided by the devcoach plugin");else if(!l.length)i$1(`no devcoach hooks in ${I} \u2014 run ${a.bold("devcoach install")}`);else {let p=l.filter(({cmd:g})=>g.command.includes("onboard-hook")||g.command.includes("lesson-ready")),u=l.filter(g=>g.event==="Stop").length;p.length?t(`legacy two-entry Stop layout \u2014 run ${a.bold("devcoach install")} to merge into one stop-hook entry (fewer spawns per stop)`):u>1?t(`${u} devcoach Stop entries \u2014 interactions are counted ${u}\xD7 per stop; run ${a.bold("devcoach install")} to keep only the current one`):o$1(`Stop hook wired (${u} entry)`),l.some(g=>g.event==="UserPromptSubmit")?o$1("UserPromptSubmit priming hook wired"):t(`no UserPromptSubmit priming hook \u2014 run ${a.bold("devcoach install")} to add it (lessons land more reliably)`),we(e,l,"devcoach install");}let m=j(Y);m==="current"?o$1("Claude Code skill installed (current version)"):r?o$1("Claude Code skill bundled with the plugin"):t(m==="outdated"?`Claude Code skill is out of date \u2014 run ${a.bold("devcoach install")}`:`Claude Code skill not installed \u2014 run ${a.bold("devcoach install")}`);let k=C(join(homedir(),".claude.json"));k.ok&&k.data.mcpServers?.devcoach?o$1("MCP server registered (user scope)"):t(`MCP server not found in ~/.claude.json \u2014 it may be registered elsewhere (check with ${a.bold("claude mcp get devcoach")})`);}if(existsSync(fe)&&Fo(e),existsSync(he)&&Bo(e),c(a.bold(`
|
|
17
|
-
Database & pacing`)),!existsSync(i)){t(`no database yet (${i}) \u2014 onboarding runs on the first technical task`),c();return}try{o(r=>{o$1(`database opens (${i})`),M(r).knowledge_ready?o$1("onboarding complete"):t("onboarding not complete \u2014 the next stop cues it");let l=G(r);c(` 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=I$1(r),k=m.reduce((y,b)=>y+b.interactions,0);c(` pacing: ${m.length} session(s) counted, ${k} interaction(s) total`+(m[0]?` \u2014 latest ${m[0].session_id.slice(0,8)}\u2026 at ${m[0].interactions}`:""));let p=K(r);p.pending?t(`a cue is pending since ${p.last_cue_at} (retry threshold armed)`):p.last_skip_reason&&c(` last skip: "${p.last_skip_reason}"`);let u=_(r);u.allowed?o$1("rate limit: allowed"):t(`rate limit: ${u.reason}`),c(a.bold(`
|
|
17
|
+
Database & pacing`)),!existsSync(i)){t(`no database yet (${i}) \u2014 onboarding runs on the first technical task`),c();return}try{o(r=>{o$1(`database opens (${i})`),M(r).knowledge_ready?o$1("onboarding complete"):t("onboarding not complete \u2014 the next stop cues it");let l=G(r);c(` 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=I$1(r),k=m.reduce((y,b)=>y+b.interactions,0);c(` pacing: ${m.length} session(s) counted, ${k} interaction(s) total`+(m[0]?` \u2014 latest ${m[0].session_id.slice(0,8)}\u2026 at ${m[0].interactions}`:""));let p=K(r);p.pending?t(`a cue is pending since ${p.last_cue_at} (retry threshold armed)`):p.last_skip_reason&&c(` last skip: "${p.last_skip_reason}"`);let u=_$1(r);u.allowed?o$1("rate limit: allowed"):t(`rate limit: ${u.reason}`),c(a.bold(`
|
|
18
18
|
Verdict`));let g=ga(r,m[0]?.session_id??null);g.wouldCue?o$1("the next eligible stop WOULD cue a lesson"):t("the next eligible stop would NOT cue a lesson:");for(let y of g.reasons)c(` \xB7 ${y}`);});}catch(r){i$1(`database check failed: ${r}`);}c();}function le(e){if(!existsSync(e))return `${a.yellow("Not registered")} (${e} does not exist)`;let o=C(e);if(!o.ok)return o.error;let t=o.data.mcpServers;return t?.devcoach?(delete t.devcoach,writeFileSync(e,`${JSON.stringify(o.data,null,2)}
|
|
19
19
|
`),`${a.green("\u2713")} Removed from ${e}`):`${a.yellow("Not registered")} in ${e}`}function me(e,o){if(!X(e))return "";let t=spawnSync(e,o,{encoding:"utf8"}),i=`${t.stderr??""}${t.stdout??""}`.toLowerCase();return t.status===0?`${a.green("\u2713")} Removed via \`${e} mcp remove\``:/not found|no .*server|does not exist|no such/.test(i)?`${a.yellow("Not registered")} in ${e}`:`${a.red(`${e} mcp remove failed:`)} ${(t.stderr||t.stdout||"").trim()}`}function ue(e){if(!existsSync(e))return `${a.yellow("No hooks")} (${e} does not exist)`;let o=C(e);if(!o.ok)return o.error;let t=o.data.hooks??{},i=[];for(let[s,a]of Object.entries(t)){if(!a)continue;let r=a.filter(l=>!(l.hooks??[]).some(m=>Z(m.command??"")));r.length!==a.length&&(i.push(s),r.length?t[s]=r:delete t[s]);}return i.length===0?`${a.yellow("No devcoach hooks")} in ${e}`:(Object.keys(t).length===0&&delete o.data.hooks,writeFileSync(e,`${JSON.stringify(o.data,null,2)}
|
|
20
|
-
`),`${a.green("\u2713")} Hooks removed from ${e} (${i.join(" + ")})`)}function
|
|
20
|
+
`),`${a.green("\u2713")} Hooks removed from ${e} (${i.join(" + ")})`)}function uo(e){return existsSync(join(e,"SKILL.md"))?(rmSync(e,{recursive:true,force:true}),`${a.green("\u2713")} Removed ${e}`):`${a.yellow("Not installed")} (${e})`}async function Uo(e){if(e)return true;if(!process.stdin.isTTY)return c(`${a.yellow("\u2192")} Not a terminal \u2014 pass --yes to delete ${h} without a prompt.`),false;let o=createInterface({input:process.stdin,output:process.stdout});try{let t=await o.question(`Delete ${h} \u2014 every lesson, your profile and the notebook? [y/N] `);return /^y(es)?$/i.test(t.trim())}finally{o.close();}}async function $o(e){let o=e.claudeCode||e.claudeDesktop||e.gemini||e.codex,t=e.all||e.claudeCode||!o,i=e.all||e.claudeDesktop||!o,s=e.all||e.gemini,a$1=e.all||e.codex;if(c(a.bold("Removing devcoach from your agents")),c(),t){c(a.bold("Claude Code"));let r=C(I);r.ok&&be(r.data)&&c(` ${a.yellow("\u2192")} The devcoach plugin is enabled \u2014 its hooks, skill and MCP server go away with /plugin uninstall devcoach; only user-level entries are removed here.`);let l=me("claude",["mcp","remove","--scope","user","devcoach"]);l||(l=le(join(homedir(),".claude.json"))),c(` MCP server\u2026 ${l}`),c(` Hooks\u2026 ${ue(I)}`),c(` Skill\u2026 ${uo(Y)}`),c();}if(i&&(c(a.bold("Claude Desktop")),c(` MCP server\u2026 ${le(fo())}`),c(` ${a.yellow("\u2192")} Restart Claude Desktop to drop the server.`),c()),s){c(a.bold("Gemini CLI (beta)"));let r=me("gemini",["mcp","remove","-s","user","devcoach"]);r||(r=le(_)),c(` MCP server\u2026 ${r}`),c(` Hooks\u2026 ${ue(_)}`),q()&&c(` ${a.yellow("\u2192")} The devcoach Gemini extension is installed \u2014 remove it with: gemini extensions uninstall devcoach`),c();}if(a$1){c(a.bold("Codex CLI (beta)"));let r=me("codex",["mcp","remove","devcoach"]);r||(r=`${a.yellow("Manual step")} \u2014 codex CLI not found. Delete the [mcp_servers.devcoach] table from ${W}`),c(` MCP server\u2026 ${r}`),c(` Hooks\u2026 ${ue(O)}`),c();}if(s||a$1){let r=!s&&A(_,"AfterAgent")||!a$1&&A(O,"Stop");c(a.bold("Shared skill (Gemini + Codex)")),c(` Skill\u2026 ${r?`${a.yellow("Kept")} \u2014 still used by the other client (${H})`:uo(H)}`),c();}e.data?(c(a.bold("Coaching data")),existsSync(h)?await Uo(e.yes)?(rmSync(h,{recursive:true,force:true}),c(` ${a.green("\u2713")} Deleted ${h}`)):c(` ${a.yellow("Kept")} ${h}`):c(` ${a.yellow("Nothing to delete")} (${h} does not exist)`),c()):c(a.dim(`Your lessons, profile and notebook are still in ${h} \u2014 keep them for a
|
|
21
21
|
reinstall, or delete them with: devcoach uninstall --data (devcoach backup first).`)),c(a.dim(`Then remove the program itself: brew uninstall devcoach \xB7 npm uninstall -g devcoach
|
|
22
|
-
(npx: nothing to remove).`));}function Ko(e,o){return e==="darwin"?["open",[o]]:e==="win32"?["cmd",["/c","start","",o]]:["xdg-open",[o]]}function
|
|
23
|
-
${l}`);}function sn(e,o$1){let t=["max_per_day","min_gap_minutes","nudge_every","nudge_scope","share_name"];if(t.includes(e)||(d(a.red(`Unknown key '${e}'. Valid keys: ${t.join(", ")}`)),process.exit(1)),e==="nudge_scope"&&o$1!=="session"&&o$1!=="global"&&(d(a.red(`Invalid nudge_scope '${o$1}'. Use: session | global`)),process.exit(1)),e==="share_name"){o$1.trim().length>l&&(d(a.red(`share_name must be at most ${l} characters.`)),process.exit(1));let i=m(o$1);o(s=>H$1(s,e,i)),d(i?a.green(`Set share_name = ${i}`):a.green("Cleared share_name"));return}if(e==="nudge_every"){let i=Number.parseInt(o$1,10);(!Number.isInteger(i)||i<0)&&(d(a.red(`Invalid nudge_every '${o$1}'. Use a non-negative integer (0 = off).`)),process.exit(1));}o(i=>H$1(i,e,o$1)),d(a.green(`Set ${e} = ${o$1}`));}function an(e,o$1,t){let i=e.trim();i||(d(a.red("Topic name must not be empty.")),process.exit(1)),o(a=>{A$1(a,i,o$1),t&&t!=="Other"&&E(a,i,t);});let s=t&&t!=="Other"?` \u2192 ${a.cyan(t)}`:"";d(`${a.green("Added")} ${a.bold(i)} (confidence ${a.cyan(String(o$1))})${s}`);}function dn(e){let o$1=o(t=>B(t,e));d(o$1?`${a.green("Removed")} ${a.bold(e)} from knowledge map`:a.yellow(`Topic '${e}' not found.`));}function cn(e){let o$1=e.trim();(!o$1||o$1==="Other")&&(d(a.red("Invalid group name.")),process.exit(1)),o(t=>C$1(t,o$1)),d(`${a.green(`Group '${a.cyan(o$1)}' ready.`)} Assign topics with: devcoach group-assign <topic> "${o$1}"`);}function ln(e){let o$1=o(t=>D(t,e));d(o$1?`${a.green(`Removed group '${a.cyan(e)}'`)}`:a.yellow(`Group '${e}' not found.`));}function mn(e,o$1){o(i=>i.prepare("SELECT topic FROM knowledge WHERE topic = ?").get(e)===void 0?false:(o$1==="Other"?F(i,e):E(i,e,o$1),true))||(d(a.red(`Topic '${e}' not in knowledge map. Add it first.`)),process.exit(1)),d(o$1==="Other"?`${a.green("Moved")} ${a.bold(e)} \u2192 Other (ungrouped)`:`${a.green("Moved")} ${a.bold(e)} \u2192 ${a.cyan(o$1)}`);}function un(e){let{lessonsCount:o$1,knowledgeCount:t,data:i}=o(a=>({lessonsCount:v(a).length,knowledgeCount:z(a).length,data:O$1(a)}));writeFileSync(e,i);let s=existsSync(j$1)?" + notebook":"";d(`${a.green("Backup saved:")} ${e} (${a.cyan(String(o$1))} lessons, ${a.cyan(String(t))} topics${s})`);}function pn(e){existsSync(e)||(d(a.red(`File not found: ${e}`)),process.exit(1));let o$1=o(i=>P(i,readFileSync(e)));o$1.settings&&d(`${a.green("\u2713")} Settings restored`),o$1.topics&&d(`${a.green("\u2713")} Knowledge map restored (${a.cyan(String(o$1.topics))} topics)`);let t=[`${a.cyan(String(o$1.lessons))} imported`];o$1.skipped&&t.push(`${a.yellow(String(o$1.skipped))} duplicates skipped`),o$1.invalid&&t.push(`${a.red(String(o$1.invalid))} rejected (invalid)`),d(`${a.green("\u2713")} Lessons: ${t.join(", ")}`),o$1.learning_state&&d(`${a.green("\u2713")} Notebook restored`);}var Q=e=>{console.error(a.dim(e));};function gn(e,o$1){o$1.by!==void 0&&o$1.anonymous&&(d(a.red("Use either --by <name> or --anonymous, not both.")),process.exit(1)),!e&&!o$1.last&&(d(a.red("Give a lesson id, or --last for the most recent one.")),process.exit(2));let t=o(a=>{let r$1=o$1.last?r(a,{imported:false,page:1,per_page:1})[0]??null:x
|
|
22
|
+
(npx: nothing to remove).`));}function Ko(e,o){return e==="darwin"?["open",[o]]:e==="win32"?["cmd",["/c","start","",o]]:["xdg-open",[o]]}function Ce(e,o=process.platform){let[t,i]=Ko(o,e);try{let s=spawn(t,i,{detached:!0,stdio:"ignore"});s.on("error",()=>{}),s.unref();}catch{}}var d=(e="")=>{console.log(e);},Co=e=>e==="junior"?a.green(e):e==="mid"?a.yellow(e):e==="senior"?a.red(e):e,Xo=e=>e==="know"?` ${a.green("\u2713")}`:e==="dont_know"?` ${a.red("\u2717")}`:"",qo=e=>e==="know"?a.green("\u2713 I know this"):e==="dont_know"?a.red("\u2717 I don't know this"):a.dim("no feedback");function So(e,o){let t=new Map;for(let s of e.groups)for(let a of s.topics)t.set(a,s.name);let i=[...e.knowledge].sort((s,a)=>a.confidence-s.confidence).map(s=>{let a=g$1(s.confidence);return [s.topic,t.get(s.topic)??"Other",b$3(a,`${s.confidence}/10`),b$3(a,f(s.confidence))]});return d$2(o,[{header:"Topic"},{header:"Group"},{header:"Confidence",justify:"right"},{header:"Bar"}],i)}function Zo(){let e=o(o=>$(o));d(So(e,"Knowledge Map"));}function Qo(e){let o$1=o(r$1=>r(r$1,{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,imported:e.imported?true:null,feedback:e.feedback,date_from:e.dateFrom,date_to:e.dateTo,sort:e.sort,order:e.order}));if(o$1.length===0){d(a.dim("No lessons found."));return}let t=o$1.some(r=>r.project||r.branch||r.commit_hash),i=[{header:""},{header:"Date"},{header:"Topic"},{header:"Title"},{header:"Level",justify:"center"},{header:"Categories"}];t&&i.push({header:"Project"},{header:"Branch"},{header:"Commit"});let s=o$1.some(r=>r.imported);s&&i.push({header:"Shared by"});let a$1=o$1.map(r=>{let l=[r.starred?a.yellow("\u2605"):a.dim("\xB7"),r.timestamp.slice(0,10),a.cyan(r.topic_id),r.title+Xo(r.feedback),Co(r.level),r.categories.join(", ")];return t&&l.push(r.project??"",r.branch?a.magenta(r.branch):"",r.commit_hash?a.cyan(r.commit_hash.slice(0,7)):""),s&&l.push(r.imported?r.shared_by??a.dim("anonymous"):""),l});d(d$2("Lessons",i,a$1));}function en(e){let o$1=o(i=>x(i,e));o$1===null&&(d(a.red(`Lesson '${e}' not found.`)),process.exit(1)),d(e$1(a.bold(o$1.title))),d(`${a.dim("ID:")} ${o$1.id}`),d(`${a.dim("Date:")} ${o$1.timestamp.slice(0,19).replace("T"," ")}`),d(`${a.dim("Topic:")} ${o$1.topic_id}`),d(`${a.dim("Categories:")} ${o$1.categories.join(", ")}`),d(`${a.dim("Level:")} ${Co(o$1.level)}`);let t=o$1.starred?a.yellow("\u2605 starred"):a.dim("\u2606 not starred");if(d(`${a.dim("Star:")} ${t} ${a.dim("Feedback:")} ${qo(o$1.feedback)}`),o$1.imported&&d(`${a.dim("Shared by:")} ${o$1.shared_by??a.dim("anonymous")}`),o$1.task_context&&d(`${a.dim("Context:")} ${o$1.task_context}`),o$1.project||o$1.repository||o$1.branch||o$1.commit_hash||o$1.folder){let i=[];o$1.project&&i.push(`project=${o$1.project}`),o$1.repository&&i.push(`repo=${o$1.repository}`),o$1.branch&&i.push(`branch=${a.magenta(o$1.branch)}`),o$1.commit_hash&&i.push(`commit=${a.cyan(o$1.commit_hash.slice(0,7))}`),o$1.folder&&i.push(`folder=${o$1.folder}`),d(`${a.dim("Git:")} ${i.join(" \xB7 ")}`);}d(e$1()),d(o$1.summary);}function vo(e,o$1){o(i=>x(i,e)===null?false:(t(i,e,o$1),true))||(d(a.red(`Lesson '${e}' not found.`)),process.exit(1)),d(o$1?`Lesson ${a.cyan(e)} \u2192 ${a.yellow("\u2605 starred")}`:`Lesson ${a.cyan(e)} \u2192 ${a.dim("\u2606 unstarred")}`);}function on(e){o(t=>s(t,e))||(d(a.red(`Lesson '${e}' not found.`)),process.exit(1)),d(`Lesson ${a.cyan(e)} deleted.`);}function nn(e,o$1){["know","dont_know","clear"].includes(o$1)||(d(a.red(`Invalid feedback '${o$1}'. Use: know | dont_know | clear`)),process.exit(1));let t=o$1==="clear"?null:o$1,i=o(r=>{let l=ba(r,e,t);if(l===null)return null;let m=r.prepare("SELECT confidence FROM knowledge WHERE topic = ?").get(l);return {topicId:l,newConf:m?m.confidence:5}});i===null&&(d(a.red(`Lesson '${e}' not found.`)),process.exit(1));let s;if(t==="know"||t==="dont_know"){let r=i.newConf+(t==="know"?-1:1);s=`${a.cyan(i.topicId)} confidence: ${r} \u2192 ${a.bold(String(i.newConf))}`;}else s="feedback cleared";let a$1=t==="know"?a.green("\u2713 I know this"):t==="dont_know"?a.red("\u2717 I don't know this"):a.dim("cleared");d(`Lesson ${a.cyan(e)} \u2192 ${a$1} (${s})`);}function tn(){let e=o(s=>G(s)),o$1=Math.floor(e.min_gap_minutes/60),t=e.min_gap_minutes%60,i=o$1?`${o$1}h ${t}m`:`${t}m`;d(d$2("Settings",[{header:"Key"},{header:"Value",justify:"right"}],[["max_per_day",String(e.max_per_day)],["min_gap_minutes",`${e.min_gap_minutes} (${i})`],["nudge_every",e.nudge_every===0?"0 (off)":String(e.nudge_every)],["nudge_scope",e.nudge_scope],["share_name",e.share_name??a.dim("(unset \u2192 git user.name)")]]));}function rn(){let{stats:e,rateLimit:o$1,settings:t}=o(m=>({stats:ca(m),rateLimit:_$1(m),settings:G(m)})),i=m=>Number(e[m]??0),s=o$1.allowed?a.green("Available now"):a.yellow(o$1.reason??"");d(d$2("Coaching Stats",[{header:"Metric"},{header:"Value",justify:"right"}],[["Total lessons",String(i("total_lessons"))],["Lessons today (24h)",`${i("lessons_today")} / ${t.max_per_day}`],["Lessons this week",String(i("lessons_this_week"))],["Next lesson",s]],false));let a$1=e.weakest_topics??[],r=e.strongest_topics??[];if(a$1.length||r.length){let m=[];for(let k=0;k<Math.max(a$1.length,r.length);k++){let p=a$1[k],u=r[k];m.push([p?`${a.red(p.topic)} ${a.dim(`(${p.confidence})`)}`:"",u?`${a.green(u.topic)} ${a.dim(`(${u.confidence})`)}`:""]);}d(d$2(void 0,[{header:"Weakest topics"},{header:"Strongest topics"}],m));}let l=$e();l&&d(`
|
|
23
|
+
${l}`);}function sn(e,o$1){let t=["max_per_day","min_gap_minutes","nudge_every","nudge_scope","share_name"];if(t.includes(e)||(d(a.red(`Unknown key '${e}'. Valid keys: ${t.join(", ")}`)),process.exit(1)),e==="nudge_scope"&&o$1!=="session"&&o$1!=="global"&&(d(a.red(`Invalid nudge_scope '${o$1}'. Use: session | global`)),process.exit(1)),e==="share_name"){o$1.trim().length>l&&(d(a.red(`share_name must be at most ${l} characters.`)),process.exit(1));let i=m(o$1);o(s=>H$1(s,e,i)),d(i?a.green(`Set share_name = ${i}`):a.green("Cleared share_name"));return}if(e==="nudge_every"){let i=Number.parseInt(o$1,10);(!Number.isInteger(i)||i<0)&&(d(a.red(`Invalid nudge_every '${o$1}'. Use a non-negative integer (0 = off).`)),process.exit(1));}o(i=>H$1(i,e,o$1)),d(a.green(`Set ${e} = ${o$1}`));}function an(e,o$1,t){let i=e.trim();i||(d(a.red("Topic name must not be empty.")),process.exit(1)),o(a=>{A$1(a,i,o$1),t&&t!=="Other"&&E(a,i,t);});let s=t&&t!=="Other"?` \u2192 ${a.cyan(t)}`:"";d(`${a.green("Added")} ${a.bold(i)} (confidence ${a.cyan(String(o$1))})${s}`);}function dn(e){let o$1=o(t=>B(t,e));d(o$1?`${a.green("Removed")} ${a.bold(e)} from knowledge map`:a.yellow(`Topic '${e}' not found.`));}function cn(e){let o$1=e.trim();(!o$1||o$1==="Other")&&(d(a.red("Invalid group name.")),process.exit(1)),o(t=>C$1(t,o$1)),d(`${a.green(`Group '${a.cyan(o$1)}' ready.`)} Assign topics with: devcoach group-assign <topic> "${o$1}"`);}function ln(e){let o$1=o(t=>D(t,e));d(o$1?`${a.green(`Removed group '${a.cyan(e)}'`)}`:a.yellow(`Group '${e}' not found.`));}function mn(e,o$1){o(i=>i.prepare("SELECT topic FROM knowledge WHERE topic = ?").get(e)===void 0?false:(o$1==="Other"?F(i,e):E(i,e,o$1),true))||(d(a.red(`Topic '${e}' not in knowledge map. Add it first.`)),process.exit(1)),d(o$1==="Other"?`${a.green("Moved")} ${a.bold(e)} \u2192 Other (ungrouped)`:`${a.green("Moved")} ${a.bold(e)} \u2192 ${a.cyan(o$1)}`);}function un(e){let{lessonsCount:o$1,knowledgeCount:t,data:i}=o(a=>({lessonsCount:v(a).length,knowledgeCount:z(a).length,data:O$1(a)}));writeFileSync(e,i);let s=existsSync(j$1)?" + notebook":"";d(`${a.green("Backup saved:")} ${e} (${a.cyan(String(o$1))} lessons, ${a.cyan(String(t))} topics${s})`);}function pn(e){existsSync(e)||(d(a.red(`File not found: ${e}`)),process.exit(1));let o$1=o(i=>P(i,readFileSync(e)));o$1.settings&&d(`${a.green("\u2713")} Settings restored`),o$1.topics&&d(`${a.green("\u2713")} Knowledge map restored (${a.cyan(String(o$1.topics))} topics)`);let t=[`${a.cyan(String(o$1.lessons))} imported`];o$1.skipped&&t.push(`${a.yellow(String(o$1.skipped))} duplicates skipped`),o$1.invalid&&t.push(`${a.red(String(o$1.invalid))} rejected (invalid)`),d(`${a.green("\u2713")} Lessons: ${t.join(", ")}`),o$1.learning_state&&d(`${a.green("\u2713")} Notebook restored`);}var Q=e=>{console.error(a.dim(e));};function gn(e,o$1){o$1.by!==void 0&&o$1.anonymous&&(d(a.red("Use either --by <name> or --anonymous, not both.")),process.exit(1)),!e&&!o$1.last&&(d(a.red("Give a lesson id, or --last for the most recent one.")),process.exit(2));let t=o(a=>{let r$1=o$1.last?r(a,{imported:false,page:1,per_page:1})[0]??null:x(a,e);if(!r$1)return null;let l=G(a),m$1=T({explicit:o$1.by??null,anonymous:o$1.anonymous,setting:l.share_name,gitUserName:a$1()}),k=false;return o$1.by?.trim()&&!l.share_name&&(H$1(a,"share_name",m(o$1.by)),k=true),{payload:S$1(r$1,{includeContext:!!o$1.withContext,sharedBy:m$1}),remembered:k}});t||(d(a.red(o$1.last?"No lessons to share yet.":`Lesson '${e}' not found.`)),process.exit(1));let{payload:i,remembered:s}=t;if(o$1.file!==void 0&&o$1.file!==false){let a$1=typeof o$1.file=="string"?o$1.file:Y$1(i);writeFileSync(a$1,Z$1(i)),d(`${a.green("Saved:")} ${a$1}`);}else o$1.link?d(X$1(i)):(d(W$1(i)),U(i).length>16e3&&Q("Long lesson \u2014 prefer --file for a friendlier hand-off."));s&&Q("(remembered as share_name \u2014 change it with: devcoach set share_name <name>)"),i.shared_by===null&&Q("Shared anonymously (set a name with: devcoach set share_name <name>)");}function fn(){let e=process.platform==="darwin"?[["pbpaste",[]]]:process.platform==="win32"?[["powershell",["-NoProfile","-Command","Get-Clipboard"]]]:[["wl-paste",["--no-newline"]],["xclip",["-selection","clipboard","-o"]],["xsel",["--clipboard","--output"]]];for(let[o,t]of e)try{let i=execFileSync(o,t,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]});if(i.trim())return i}catch{}throw new R("Clipboard is empty or unavailable \u2014 pass the code, a link or a file, or pipe it: devcoach import -")}async function hn(e){return e===void 0?fn():e==="-"?text(process.stdin):existsSync(e)?readFileSync(e,"utf8"):c$2(e)&&!e.includes("#devcoach:lesson:")?d$3(e):e}async function kn(e){try{let o$1=await hn(e),t=o(a=>da(a,o$1));if(t.kind==="lessons"){let a$1=[`${a.cyan(String(t.inserted))} imported`];t.duplicated&&a$1.push(`${a.yellow(String(t.duplicated))} duplicates skipped`),t.invalid&&a$1.push(`${a.red(String(t.invalid))} rejected (invalid)`),d(`${a.green("\u2713")} Lessons: ${a$1.join(", ")}`);return}let i=t.lesson;i||(d(a.red("Could not store the lesson.")),process.exit(1));let s=i.shared_by?`shared by ${i.shared_by}`:"shared anonymously";t.duplicated?d(`${a.yellow("Already in your log")} as ${a.cyan(i.id)} (${s})`):d(`${a.green("\u2713 Imported")} ${a.bold(`"${i.title}"`)} (${s}) as ${a.cyan(i.id)} \u2014 topic ${a.cyan(i.topic_id)}`),t.topic_tracked===!1&&Q(`Topic '${i.topic_id}' is not in your knowledge map \u2014 track it with: devcoach knowledge-add ${i.topic_id}`);}catch(o){if(!(o instanceof R))throw o;d(a.red(o.message)),process.exit(1);}}async function bn(){let e=createInterface({input:process.stdin,output:process.stdout}),o$1=async(i,s="")=>{let a=s?` [${s}]`:"";return (await e.question(`${i}${a}: `)).trim()||s},t=async(i,s,a$1,r)=>{for(;;){let l=await o$1(i,String(s)),m=Number.parseInt(l,10);if(!Number.isNaN(m)&&m>=a$1&&m<=r)return m;d(a.red(`Must be ${a$1}\u2013${r}.`));}};try{d(e$1(a.bold("devcoach setup"))),d(`
|
|
24
24
|
${a.bold("Step 1")} \u2014 Restore from backup`);let i=await o$1("Path to existing backup zip (Enter to skip)","");if(i){existsSync(i)||(d(a.red(`File not found: ${i}`)),process.exit(1));let p=o(u=>P(u,readFileSync(i)));d(`${a.green("\u2713")} Restored: ${p.topics} topics, ${p.lessons} lessons${p.learning_state?", notebook":""}`),d(a.green("Setup complete!"));return}d(`
|
|
25
25
|
${a.bold("Step 2")} \u2014 Build your knowledge profile`);let s=(await o$1("Mode: [a]utomatic (detect from files) / [m]anual (type your stack)","a")).toLowerCase(),a$1={};if(s.startsWith("a")){let u=b$1().folder??process.cwd(),g=c$1(),y=b$2(a$2(u),g.detected_stack),b=Object.keys(y).sort((v,R)=>v.localeCompare(R));if(g.scanned_projects>0){d(`
|
|
26
26
|
${a.dim(`Scanned ${g.scanned_projects} Claude Code project(s) from your history:`)}`);for(let v of g.projects.slice(0,8))d(a.dim(` ${v.name} \u2014 ${v.topics.join(", ")||"no stack signals"}`));}if(b.length){d(`
|
|
27
|
-
${a.dim(`Detected (history + ${a.cyan(u)}):`)}`);for(let v of b){let R=y[v]??5,
|
|
27
|
+
${a.dim(`Detected (history + ${a.cyan(u)}):`)}`);for(let v of b){let R=y[v]??5,xe=await o$1(` ${a.cyan(v)} (Enter=keep, 0-10=override, s=skip)`,String(R));if(xe.toLowerCase()==="s")continue;let _e=Number.parseInt(xe,10);a$1[v]=Number.isNaN(_e)?R:Math.max(0,Math.min(10,_e));}}else d(a.dim("No technology files detected in current directory."));let P=await o$1(`
|
|
28
28
|
Add additional topics (comma-separated, or Enter to skip)`,"");for(let v of P.split(",").map(R=>R.trim()).filter(Boolean))a$1[v]=await t(` Confidence for ${a.cyan(v)}`,5,0,10);}else for(d(a.dim(`
|
|
29
29
|
Enter topics: 'topic_id confidence' (e.g. python 7). Blank line when done.`));;){let p=(await o$1("Topic (Enter when done)","")).trim();if(!p)break;let u=p.split(/\s+/),g=u[0]??"",y=Number.parseInt(u[1]??"",10),b=Number.isNaN(y)?5:Math.max(0,Math.min(10,y));a$1[g]=b,d(` ${a.green("+")} ${a.cyan(g)} \u2192 ${b}`);}let r={};if(Object.keys(a$1).length&&(d(`
|
|
30
30
|
${a.bold("Step 3")} \u2014 Organise into groups`),(await o$1("Organise topics into groups? [y/N]","n")).toLowerCase().startsWith("y"))){let u=[];for(let g of Object.keys(a$1).sort((y,b)=>y.localeCompare(b))){let y=u.length?u.join(", "):"(none yet)",b=await o$1(` Group for ${a.cyan(g)} existing: ${a.dim(y)} (Enter=Other)`,"");if(b&&b!=="Other"){let P=r[b]??[];P.push(g),r[b]=P,u.includes(b)||u.push(b);}}}d(`
|
|
@@ -33,4 +33,4 @@ ${a.green("Setup complete!")} ${Object.keys(a$1).length} topics saved.`);}finall
|
|
|
33
33
|
${a.bold(`devcoach ${a.dim(`v${Q$1}`)}`)}`);for(let[t,i]of e)d(` ${a.cyan(`devcoach ${t}`.padEnd(32))} ${a.dim(i)}`);d(`
|
|
34
34
|
${a.dim("Run")} ${a.bold("devcoach <command> --help")} ${a.dim("for per-command options.")}
|
|
35
35
|
`);let o=$e();o&&d(`${o}
|
|
36
|
-
`);}function S(e){return e??null}function $n(){let e$1=new Command;return e$1.exitOverride(),e$1.name("devcoach").description("devcoach \u2014 progressive technical coaching").version(`devcoach ${Q$1}`,"-v, --version","output the version number").allowExcessArguments(false).showHelpAfterError("(add --help for usage)").action(()=>yn()),e$1.command("mcp").description("Start the MCP server (stdio transport)").action(async()=>{let{runStdio:o}=await import('./server-
|
|
36
|
+
`);}function S(e){return e??null}function $n(){let e$1=new Command;return e$1.exitOverride(),e$1.name("devcoach").description("devcoach \u2014 progressive technical coaching").version(`devcoach ${Q$1}`,"-v, --version","output the version number").allowExcessArguments(false).showHelpAfterError("(add --help for usage)").action(()=>yn()),e$1.command("mcp").description("Start the MCP server (stdio transport)").action(async()=>{let{runStdio:o}=await import('./server-SM63J4EC.js');await o();}),e$1.command("profile").description("Show the knowledge map").action(Zo),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("--imported","Show only lessons shared with you").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=>Qo({period:o.period??"all",category:S(o.category),project:S(o.project),repository:S(o.repository),branch:S(o.branch),commit:S(o.commit),starred:!!o.starred,imported:!!o.imported,feedback:S(o.feedback),level:S(o.level),dateFrom:S(o.dateFrom),dateTo:S(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=>en(o)),e$1.command("star").description("Mark a lesson as starred").argument("<id>","Lesson ID").action(o=>vo(o,true)),e$1.command("unstar").description("Remove the starred mark from a lesson").argument("<id>","Lesson ID").action(o=>vo(o,false)),e$1.command("delete").description("Permanently delete a lesson").argument("<id>","Lesson ID").action(o=>on(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,t)=>nn(o,t)),e$1.command("share").description("Share a lesson: copyable text (default), --link, or a .devcoach.md file").argument("[id]","Lesson ID (or use --last)").option("--last","Share the most recent lesson").option("--link","Print the share link only").option("--file [path]","Write a .devcoach.md file (default: <lesson-id>.devcoach.md)").option("--with-context","Include project/branch/commit/task context (off by default)").option("--by <name>","Sender name (remembered as share_name when unset)").option("--anonymous","Share without a sender name").action((o,t)=>gn(o,t)),e$1.command("import").description("Import a shared lesson: a devcoach:lesson code, a share link, a URL, a .devcoach.md or lessons .json file, - for stdin \u2014 or the clipboard when omitted").argument("[source]","Code, link, URL, file path, or - (stdin); omit to read the clipboard").action(o=>kn(o)),e$1.command("settings").description("Show current settings").action(tn),e$1.command("stats").description("Coaching statistics and rate-limit status").action(rn),e$1.command("set").description("Update a setting (max_per_day | min_gap_minutes | nudge_every | nudge_scope | share_name)").argument("<key>","Setting key").argument("<value>","New value").action((o,t)=>sn(o,t)),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,t)=>an(o,Math.max(0,Math.min(10,Number.parseInt(t.confidence??"5",10)||5)),S(t.group))),e$1.command("knowledge-remove").description("Remove a topic from the knowledge map").argument("<topic>","Topic ID").action(o=>dn(o)),e$1.command("group-add").description("Register a new knowledge group").argument("<name>","Group name").action(o=>cn(o)),e$1.command("group-remove").description("Delete a knowledge group (topics move to Other)").argument("<name>","Group name").action(o=>ln(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,t)=>mn(o,t)),e$1.command("backup").description("Export a full backup (settings + knowledge + lessons) as zip").argument("[file]","Output zip path","devcoach-backup.zip").action(o=>un(o)),e$1.command("restore").description("Restore from a backup zip file").argument("<file>","Path to backup zip").action(o=>pn(o)),e$1.command("install").description("Register the MCP server + hooks + skill in Claude Code / Claude Desktop (default), Gemini CLI (--gemini, beta), or Codex CLI (--codex, beta)").option("--claude-code","Target Claude Code only").option("--claude-desktop","Target Claude Desktop only").option("--gemini","Target Google Gemini CLI (beta)").option("--codex","Target OpenAI Codex CLI (beta)").option("--force","Overwrite existing devcoach entry").option("--skip-hook","Register MCP server only \u2014 skip the hooks").action(o=>bo({claudeCode:!!o.claudeCode,claudeDesktop:!!o.claudeDesktop,gemini:!!o.gemini,codex:!!o.codex,force:!!o.force,skipHook:!!o.skipHook})),e$1.command("uninstall").description("Remove devcoach's MCP server, hooks and skill from Claude Code / Claude Desktop (default), Gemini CLI (--gemini), Codex CLI (--codex) or every agent (--all). Coaching data is kept unless --data is given. Run it before brew/npm uninstall.").option("--claude-code","Target Claude Code only").option("--claude-desktop","Target Claude Desktop only").option("--gemini","Also remove from Google Gemini CLI (beta)").option("--codex","Also remove from OpenAI Codex CLI (beta)").option("--all","Every agent: Claude Code, Claude Desktop, Gemini CLI, Codex CLI").option("--data","Also delete ~/.devcoach (lessons, profile, notebook) \u2014 asks first").option("--yes","Do not ask before deleting the data (with --data)").action(async o=>$o({claudeCode:!!o.claudeCode,claudeDesktop:!!o.claudeDesktop,gemini:!!o.gemini,codex:!!o.codex,all:!!o.all,data:!!o.data,yes:!!o.yes})),e$1.command("doctor").description("Diagnose the devcoach \u21C4 agent wiring and the lesson pacing state").action(yo),e$1.command("ui").description("Launch the web dashboard (Ctrl+C stops it gracefully; --stop stops a running one)").option("--port <port>","Port","7860").option("-o, --open","Open the dashboard in your default browser").option("--stop","Stop the dashboard running on --port (e.g. one started by your agent)").action(async o=>{let t=Number.parseInt(o.port??"7860",10)||7860,{startUi:i,stopUi:s}=await import('./app-SVGKP5WG.js');if(o.stop){d(await s(t)?a.green(`\u2713 devcoach UI on port ${t} stopped`):a.yellow(`No devcoach UI is running on port ${t}`));return}i(t,{onReady:a=>{o.open&&Ce(a);},onListenError:a=>{console.error(a.message),o.open&&a.existingUrl&&Ce(a.existingUrl),process.exit(a.exitCode);}});}),e$1.command("setup").description("First-run wizard: import backup or build your knowledge profile").action(bn),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(()=>g()),e$1.command("gemini-stop-hook",{hidden:true}).description("Gemini CLI AfterAgent hook: onboarding check + lesson cue in one spawn").action(()=>e(void 0,"gemini")),e$1.command("gemini-prompt-hook",{hidden:true}).description("Gemini CLI BeforeAgent hook: prime the model when a lesson is due").action(()=>g(void 0,"gemini")),e$1.command("codex-stop-hook",{hidden:true}).description("Codex CLI Stop hook: onboarding check + lesson cue in one spawn").action(()=>e(void 0,"codex")),e$1.command("codex-prompt-hook",{hidden:true}).description("Codex CLI UserPromptSubmit hook: prime the model when a lesson is due").action(()=>g(void 0,"codex")),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$1()),e$1}async function Jn(){let e=$n();try{await e.parseAsync(process.argv);}catch(o){let t=o.code??"";if(!t.startsWith("commander."))throw o;if(t==="commander.helpDisplayed"||t==="commander.help"||t==="commander.version")return;process.exit(2);}}export{Jn as runCli};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export{c as buildLessonCue,f as buildPromptPrime,d as cmdLessonReady,b as cmdOnboardHook,g as cmdPromptHook,e as cmdStopHook,a as parseHookPayload,h as runHook}from'./chunk-
|
|
2
|
+
export{c as buildLessonCue,f as buildPromptPrime,d as cmdLessonReady,b as cmdOnboardHook,g as cmdPromptHook,e as cmdStopHook,a as parseHookPayload,h as runHook}from'./chunk-UHF6ARVR.js';import'./chunk-JEFYE33I.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {d as d$1,f as f$1,e as e$1,c as c$3,b as b$2,a as a$2}from'./chunk-MRABMPXO.js';import {b as b$1,a as a$1,c as c$2,d as d$2}from'./chunk-M2UBHB65.js';import {a,c as c$1,b,f,e,Q,o,N as N$1,g,p,J,aa,r,t,s,u as u$1,L,d,A,E,B,C,D,l as l$1,m as m$1,$,j,x,T,G,S,Y,Z,X,W,U,da,R,ca,ea,q,_,H,M,k}from'./chunk-HXWWJA73.js';import {spawn}from'child_process';import {mkdirSync,existsSync,statSync,writeFileSync,readFileSync}from'fs';import {dirname}from'path';import {McpServer,ResourceTemplate}from'@modelcontextprotocol/server';import {StdioServerTransport}from'@modelcontextprotocol/server/stdio';import {z}from'zod';var u=o=>({type:"text",text:o}),fe=o=>({content:[u(JSON.stringify(o))]}),c=o=>({isError:true,content:[u(o)]}),m=o=>({content:[u(JSON.stringify(o))],structuredContent:o}),Re=z.object({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(),imported:z.boolean(),shared_by:z.string().nullable()}),Ne=Re.extend({reply_check:z.string()}),ye="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.",_e=z.object({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())}))}),Oe=z.object({max_per_day:z.number().int(),min_gap_minutes:z.number().int(),ui_theme:e,nudge_every:z.number().int(),nudge_scope:f,share_name:z.string().nullable()}),Pe=z.object({transport:z.enum(["text","link","file"]),code:z.string(),text:z.string().nullable(),link:z.string().nullable(),markdown:z.string().nullable(),filename:z.string().nullable(),shared_by:z.string().nullable(),include_context:z.boolean(),reply_check:z.string()}),Ie=z.object({kind:z.enum(["shared","lessons"]),inserted:z.number().int(),duplicated:z.number().int(),invalid:z.number().int(),lesson:Re.nullable(),topic_tracked:z.boolean().nullable(),reply_check:z.string()}),$e={text:"Reply with `text` verbatim as the final message: the card as markdown, then the last line (devcoach:lesson:1:\u2026) inside a fenced code block, character for character \u2014 never paraphrase, wrap, truncate or split it.",link:"Reply with `link` on its own line, unchanged.",file:"Write `markdown` to `filename` (in the workspace unless the user named a path) with your file tool, then confirm the path in one line; do not print the markdown."},Ge="Confirm in one line: the title, who shared it, and that it joined the coaching log (feedback works as usual; it does not count against the daily limit). If topic_tracked is false, offer to track the topic with add_topic \u2014 only on confirmation.",Ue=z.object({window_months:z.number().int(),cutoff:z.string(),candidate_count:z.number().int(),over_soft_limit:z.boolean(),candidates:z.array(z.object({name:z.string(),path:z.string(),last_activity:z.string().nullable(),prompt_count:z.number().int()}))});function l(o,p){return {contents:[{uri:o.href,mimeType:"application/json",text:JSON.stringify(p)}]}}function we(){try{return {...o(o=>$(o))}}catch(o){return {error:String(o)}}}function ke(){try{let o$1="";try{existsSync(j)&&(o$1=readFileSync(j,"utf8"));}catch{}return {...o(d=>{let e=M(d).knowledge_ready,n=o$1.length>0,r=_(d),i={allowed:r.allowed,total_lessons:q(d)};return r.reason!=null&&(i.reason=r.reason),{onboarding:{knowledge_ready:e,notebook_ready:n,needs_onboarding:!(e&&n)},rate_limit:i,taught_topics:ea(d),profile:$(d)}}),notebook:o$1,notebook_path:j}}catch(o){return {error:String(o)}}}function ve(){try{let p=o(i=>M(i)).knowledge_ready,d=existsSync(j)&&statSync(j).size>0,e=b$1(),n=c$3(),r=b$2(a$2(e.folder??process.cwd()),n.detected_stack);return {knowledge_ready:p,notebook_ready:d,needs_onboarding:!(p&&d),detected_stack:r,detected_projects:n.projects,scanned_projects:n.scanned_projects,default_topics:k,context_ready:e.branch!==null,notebook_path:j}}catch(o){return {error:String(o)}}}var N=(o,p)=>typeof p.error=="string"?c(`${o} failed: ${p.error}`):m(p);function Fe(){let o$1=new McpServer({name:"devcoach",version:Q},{instructions:"Progressive technical coaching server. Use the devcoach_instructions prompt for full coaching behaviour guidelines. Read coaching state with the get_briefing / get_onboarding / get_profile tools; the devcoach:// resources expose the same data for clients that browse resources. share_lesson hands a lesson to another person (text, link or file); import_lesson stores one they were given."});o$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. timestamp is always stamped server-side with the current time \u2014 there is no argument for it. Returns the saved Lesson with all resolved fields.",inputSchema:z.object({id:z.string().describe("Unique lesson id (uuid or random string)"),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:Ne,annotations:{title:"Log Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{let n=b$1(),r={};try{r=o(a=>N$1(a));}catch{r={};}let i=g({id:e.id,timestamp:new Date,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??n.project??r.project??null,repository:e.repository??n.repository??r.repository??null,branch:e.branch??n.branch??r.branch??null,commit_hash:e.commit_hash??n.commit_hash??null,folder:e.folder??n.folder??null,repository_platform:e.repository_platform??n.repository_platform??r.repository_platform??null});return o(a=>{p(a,i),J(a);}),{content:[u(`Lesson saved. ${ye}`),u(JSON.stringify(i))],structuredContent:{...i,reply_check:ye}}}catch(n){return c(`log_lesson failed: ${n}`)}}),o$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:z.object({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 n=o(r=>aa(r,e.topic,e.delta));return {content:[u(String(n))]}}catch(n){return c(`update_knowledge failed for '${e.topic}': ${n}`)}}),o$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:z.object({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"),imported:z.boolean().nullish().describe("True = only lessons shared with you, false = only your own"),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 n=o(r$1=>r(r$1,{period:e.period,category:e.category,level:e.level,project:e.project,repository:e.repository,branch:e.branch,commit:e.commit,starred:e.starred,imported:e.imported,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 fe(n)}catch{return fe([])}}),o$1.registerTool("star_lesson",{title:"Star Lesson",description:"Set the starred (favourite) flag on a lesson. Returns true if found and updated. Idempotent.",inputSchema:z.object({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 n=o(r=>t(r,e.lesson_id,e.starred));return {content:[u(String(n))]}}catch(n){return c(`star_lesson failed for '${e.lesson_id}': ${n}`)}}),o$1.registerTool("delete_lesson",{title:"Delete Lesson",description:"Permanently delete a lesson by id. Returns true if found and deleted.",inputSchema:z.object({lesson_id:z.string().describe("Lesson id to delete")}),annotations:{title:"Delete Lesson",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let n=o(r=>s(r,e.lesson_id));return {content:[u(String(n))]}}catch(n){return c(`delete_lesson failed for '${e.lesson_id}': ${n}`)}}),o$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:z.object({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 n=e.feedback==="clear"?null:e.feedback,r=o(i=>{let a=i.prepare("SELECT feedback, topic_id FROM lessons WHERE id = ?").get(e.lesson_id);return a?(a.feedback===n||(u$1(i,e.lesson_id,n),(n==="know"||n==="dont_know")&&a.topic_id&&aa(i,a.topic_id,n==="know"?1:-1)),!0):!1});return {content:[u(String(r))]}}catch(n){return c(`submit_feedback failed for '${e.lesson_id}': ${n}`)}}),o$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:z.object({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 o(n=>L(n,e.reason)),{content:[u("Cue skipped \u2014 pacing re-armed. Output nothing to the user.")]}}catch(n){return c(`skip_lesson failed: ${n}`)}}),o$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:z.object({topic:z.string().describe("Topic id \u2014 prefer a single word, max 3 words"),confidence:d.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 o(n=>{A(n,e.topic,e.confidence),e.group&&e.group!=="Other"&&E(n,e.topic,e.group);}),{content:[u("true")]}}catch(n){return c(`add_topic failed for '${e.topic}': ${n}`)}}),o$1.registerTool("remove_topic",{title:"Remove Topic",description:"Remove a topic from the knowledge map entirely. Returns true if it existed.",inputSchema:z.object({topic:z.string().describe("Topic id to remove")}),annotations:{title:"Remove Topic",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let n=o(r=>B(r,e.topic));return {content:[u(String(n))]}}catch(n){return c(`remove_topic failed for '${e.topic}': ${n}`)}}),o$1.registerTool("add_group",{title:"Add Group",description:"Create a new (initially empty) knowledge group. Idempotent \u2014 returns true either way.",inputSchema:z.object({name:z.string().describe("Group name, e.g. 'Machine Learning'")}),annotations:{title:"Add Group",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{return o(n=>C(n,e.name.trim())),{content:[u("true")]}}catch(n){return c(`add_group failed for '${e.name}': ${n}`)}}),o$1.registerTool("remove_group",{title:"Remove Group",description:"Delete a knowledge group. Its topics move to Other. Returns true if it existed.",inputSchema:z.object({name:z.string().describe("Group name to delete")}),annotations:{title:"Remove Group",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let n=o(r=>D(r,e.name));return {content:[u(String(n))]}}catch(n){return c(`remove_group failed for '${e.name}': ${n}`)}}),o$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'. share_name: the sender name proposed when sharing a lesson (max 80 chars, empty clears). Returns the full updated Settings.",inputSchema:z.object({key:z.enum(["max_per_day","min_gap_minutes","nudge_every","nudge_scope","share_name"]).describe("Setting key"),value:z.string().describe("New value (integer string; 'session'|'global' for nudge_scope; free text for share_name)")}),outputSchema:Oe,annotations:{title:"Update Settings",destructiveHint:false,idempotentHint:true,openWorldHint:false}},e=>{let n=i=>m(o(a=>(H(a,e.key,i),G(a))));if(e.key==="share_name")return e.value.trim().length>l$1?c(`share_name must be at most ${l$1} characters`):n(m$1(e.value));if(e.key==="nudge_scope")return e.value!=="session"&&e.value!=="global"?c("nudge_scope must be 'session' or 'global'"):n(e.value);let r=Number.parseInt(e.value,10);return Number.isNaN(r)?c(`Value must be an integer, got '${e.value}'`):e.key==="max_per_day"&&!(r>=1&&r<=20)?c("max_per_day must be between 1 and 20"):e.key==="min_gap_minutes"&&!(r>=0&&r<=1440)?c("min_gap_minutes must be between 0 and 1440"):e.key==="nudge_every"&&!(r>=0&&r<=1e3)?c("nudge_every must be between 0 and 1000"):n(String(r))}),o$1.registerTool("open_ui",{title:"Open UI",description:"Launch the devcoach web dashboard in the background. port must be 1024-65535.",inputSchema:z.object({port:z.number().int().default(7860).describe("Port (default 7860)")}),annotations:{title:"Open UI",destructiveHint:false,idempotentHint:false,openWorldHint:true}},e=>{let n=e.port;return n>=1024&&n<=65535?(spawn(process.execPath,[process.argv[1]??"","ui","--port",String(n)],{detached:true,stdio:"ignore"}).unref(),{content:[u(`devcoach UI starting at http://localhost:${n}`)]}):{content:[u(`error: port ${n} is out of valid range (1024-65535)`)]}}),o$1.registerTool("stop_ui",{title:"Stop UI",description:"Stop the devcoach web dashboard listening on port (whoever started it \u2014 open_ui or a terminal). Graceful: in-flight requests finish first. Reports stopped=false when nothing is running there. port must be 1024-65535.",inputSchema:z.object({port:z.number().int().default(7860).describe("Port (default 7860)")}),annotations:{title:"Stop UI",destructiveHint:false,idempotentHint:true,openWorldHint:true}},async e=>{let n=e.port;if(!(n>=1024&&n<=65535))return {content:[u(`error: port ${n} is out of valid range (1024-65535)`)]};let{stopUi:r}=await import('./app-3XTQVLDD.js'),i=await r(n);return {content:[u(i?`devcoach UI on port ${n} stopped`:`No devcoach UI is running on port ${n}`)],structuredContent:{stopped:i,port:n}}}),o$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'). Ensures learning-state.md exists and is non-empty (a placeholder if needed) \u2014 there is no notebook argument here. Write the real personalized notebook yourself right after, directly to the path in devcoach://onboarding's notebook_path field. Returns the updated Profile.",inputSchema:z.object({topics:z.record(z.string(),d).describe("{topic_id: confidence 0-10}"),groups:z.record(z.string(),z.array(z.string())).optional().describe("{group_name: [topic_id, ...]}")}),outputSchema:_e,annotations:{title:"Complete Onboarding",destructiveHint:true,idempotentHint:false,openWorldHint:false}},async e=>{try{let n=o(r=>{r.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[i,a]of Object.entries(e.topics))A(r,i,Math.max(0,Math.min(10,a)));if(e.groups)for(let[i,a]of Object.entries(e.groups))for(let O of a)O in e.topics&&E(r,O,i);return $(r)});return mkdirSync(dirname(j),{recursive:!0}),(!existsSync(j)||statSync(j).size===0)&&writeFileSync(j,`# devcoach \u2014 Coaching Notebook
|
|
3
|
-
`),m(n)}catch(n){return c(`complete_onboarding failed: ${n}`)}}),o$1.registerTool("preview_deep_scan",{title:"Preview Deep Scan",description:"Cheap, metadata-only pre-check for 'Automatic (Deep)' onboarding. Counts and lists Claude Code projects whose last recorded activity falls within a rolling window (months back from now) \u2014 a real date window, not a fixed top-N. Reads no prompt/conversation text, only project paths and activity timestamps. Call this BEFORE spawning the deep-read subagent: over_soft_limit true means there are enough candidates that the user should be asked whether to narrow the window, proceed anyway, or pick specific projects.",inputSchema:z.object({months:z.number().int().min(1).max(24).default(3).describe("Rolling window size in months, counting back from now")}),outputSchema:
|
|
2
|
+
import {d as d$1,f as f$1,e as e$1,c as c$3,b as b$2,a as a$2}from'./chunk-MRABMPXO.js';import {b as b$1,a as a$1,c as c$2,d as d$2}from'./chunk-4PFP67XN.js';import {a,c as c$1,b,f,e,Q,o,N as N$1,g,p,J,aa,r,t,s,u as u$1,L,d,A,E,B,C,D,l as l$1,m as m$1,$,j,x,T,G,S,Y,Z,X,W,U,da,R,ca,ea,q,_,H,M,k}from'./chunk-JEFYE33I.js';import {spawn}from'child_process';import {mkdirSync,existsSync,statSync,writeFileSync,readFileSync}from'fs';import {dirname}from'path';import {McpServer,ResourceTemplate}from'@modelcontextprotocol/server';import {StdioServerTransport}from'@modelcontextprotocol/server/stdio';import {z}from'zod';var l=o=>({type:"text",text:o}),fe=o=>({content:[l(JSON.stringify(o))]}),c=o=>({isError:true,content:[l(o)]}),m=o=>({content:[l(JSON.stringify(o))],structuredContent:o}),Re=z.object({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(),imported:z.boolean(),shared_by:z.string().nullable()}),Ne=Re.extend({reply_check:z.string()}),ye="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.",_e=z.object({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())}))}),Oe=z.object({max_per_day:z.number().int(),min_gap_minutes:z.number().int(),ui_theme:e,nudge_every:z.number().int(),nudge_scope:f,share_name:z.string().nullable()}),Pe=z.object({transport:z.enum(["text","link","file"]),code:z.string(),text:z.string().nullable(),link:z.string().nullable(),markdown:z.string().nullable(),filename:z.string().nullable(),shared_by:z.string().nullable(),include_context:z.boolean(),reply_check:z.string()}),Ie=z.object({kind:z.enum(["shared","lessons"]),inserted:z.number().int(),duplicated:z.number().int(),invalid:z.number().int(),lesson:Re.nullable(),topic_tracked:z.boolean().nullable(),reply_check:z.string()}),$e={text:"Reply with `text` verbatim as the final message: the card as markdown, then the last line (devcoach:lesson:1:\u2026) inside a fenced code block, character for character \u2014 never paraphrase, wrap, truncate or split it.",link:"Reply with `link` on its own line, unchanged.",file:"Write `markdown` to `filename` (in the workspace unless the user named a path) with your file tool, then confirm the path in one line; do not print the markdown."},Ue="Confirm in one line: the title, who shared it, and that it joined the coaching log (feedback works as usual; it does not count against the daily limit). If topic_tracked is false, offer to track the topic with add_topic \u2014 only on confirmation.",Ge=z.object({window_months:z.number().int(),cutoff:z.string(),candidate_count:z.number().int(),over_soft_limit:z.boolean(),candidates:z.array(z.object({name:z.string(),path:z.string(),last_activity:z.string().nullable(),prompt_count:z.number().int()}))});function u(o,p){return {contents:[{uri:o.href,mimeType:"application/json",text:JSON.stringify(p)}]}}function we(){try{return {...o(o=>$(o))}}catch(o){return {error:String(o)}}}function ke(){try{let o$1="";try{existsSync(j)&&(o$1=readFileSync(j,"utf8"));}catch{}return {...o(d=>{let e=M(d).knowledge_ready,n=o$1.length>0,r=_(d),i={allowed:r.allowed,total_lessons:q(d)};return r.reason!=null&&(i.reason=r.reason),{onboarding:{knowledge_ready:e,notebook_ready:n,needs_onboarding:!(e&&n)},rate_limit:i,taught_topics:ea(d),profile:$(d)}}),notebook:o$1,notebook_path:j}}catch(o){return {error:String(o)}}}function ve(){try{let p=o(i=>M(i)).knowledge_ready,d=existsSync(j)&&statSync(j).size>0,e=b$1(),n=c$3(),r=b$2(a$2(e.folder??process.cwd()),n.detected_stack);return {knowledge_ready:p,notebook_ready:d,needs_onboarding:!(p&&d),detected_stack:r,detected_projects:n.projects,scanned_projects:n.scanned_projects,default_topics:k,context_ready:e.branch!==null,notebook_path:j}}catch(o){return {error:String(o)}}}var N=(o,p)=>typeof p.error=="string"?c(`${o} failed: ${p.error}`):m(p);function Fe(){let o$1=new McpServer({name:"devcoach",version:Q},{instructions:"Progressive technical coaching server. Use the devcoach_instructions prompt for full coaching behaviour guidelines. Read coaching state with the get_briefing / get_onboarding / get_profile tools; the devcoach:// resources expose the same data for clients that browse resources. share_lesson hands a lesson to another person (text, link or file); import_lesson stores one they were given."});o$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. timestamp is always stamped server-side with the current time \u2014 there is no argument for it. Returns the saved Lesson with all resolved fields.",inputSchema:z.object({id:z.string().describe("Unique lesson id (uuid or random string)"),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:Ne,annotations:{title:"Log Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{let n=b$1(),r={};try{r=o(a=>N$1(a));}catch{r={};}let i=g({id:e.id,timestamp:new Date,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??n.project??r.project??null,repository:e.repository??n.repository??r.repository??null,branch:e.branch??n.branch??r.branch??null,commit_hash:e.commit_hash??n.commit_hash??null,folder:e.folder??n.folder??null,repository_platform:e.repository_platform??n.repository_platform??r.repository_platform??null});return o(a=>{p(a,i),J(a);}),{content:[l(`Lesson saved. ${ye}`),l(JSON.stringify(i))],structuredContent:{...i,reply_check:ye}}}catch(n){return c(`log_lesson failed: ${n}`)}}),o$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:z.object({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 n=o(r=>aa(r,e.topic,e.delta));return {content:[l(String(n))]}}catch(n){return c(`update_knowledge failed for '${e.topic}': ${n}`)}}),o$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:z.object({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"),imported:z.boolean().nullish().describe("True = only lessons shared with you, false = only your own"),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 n=o(r$1=>r(r$1,{period:e.period,category:e.category,level:e.level,project:e.project,repository:e.repository,branch:e.branch,commit:e.commit,starred:e.starred,imported:e.imported,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 fe(n)}catch{return fe([])}}),o$1.registerTool("star_lesson",{title:"Star Lesson",description:"Set the starred (favourite) flag on a lesson. Returns true if found and updated. Idempotent.",inputSchema:z.object({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 n=o(r=>t(r,e.lesson_id,e.starred));return {content:[l(String(n))]}}catch(n){return c(`star_lesson failed for '${e.lesson_id}': ${n}`)}}),o$1.registerTool("delete_lesson",{title:"Delete Lesson",description:"Permanently delete a lesson by id. Returns true if found and deleted.",inputSchema:z.object({lesson_id:z.string().describe("Lesson id to delete")}),annotations:{title:"Delete Lesson",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let n=o(r=>s(r,e.lesson_id));return {content:[l(String(n))]}}catch(n){return c(`delete_lesson failed for '${e.lesson_id}': ${n}`)}}),o$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:z.object({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 n=e.feedback==="clear"?null:e.feedback,r=o(i=>{let a=i.prepare("SELECT feedback, topic_id FROM lessons WHERE id = ?").get(e.lesson_id);return a?(a.feedback===n||(u$1(i,e.lesson_id,n),(n==="know"||n==="dont_know")&&a.topic_id&&aa(i,a.topic_id,n==="know"?1:-1)),!0):!1});return {content:[l(String(r))]}}catch(n){return c(`submit_feedback failed for '${e.lesson_id}': ${n}`)}}),o$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:z.object({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 o(n=>L(n,e.reason)),{content:[l("Cue skipped \u2014 pacing re-armed. Output nothing to the user.")]}}catch(n){return c(`skip_lesson failed: ${n}`)}}),o$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:z.object({topic:z.string().describe("Topic id \u2014 prefer a single word, max 3 words"),confidence:d.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 o(n=>{A(n,e.topic,e.confidence),e.group&&e.group!=="Other"&&E(n,e.topic,e.group);}),{content:[l("true")]}}catch(n){return c(`add_topic failed for '${e.topic}': ${n}`)}}),o$1.registerTool("remove_topic",{title:"Remove Topic",description:"Remove a topic from the knowledge map entirely. Returns true if it existed.",inputSchema:z.object({topic:z.string().describe("Topic id to remove")}),annotations:{title:"Remove Topic",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let n=o(r=>B(r,e.topic));return {content:[l(String(n))]}}catch(n){return c(`remove_topic failed for '${e.topic}': ${n}`)}}),o$1.registerTool("add_group",{title:"Add Group",description:"Create a new (initially empty) knowledge group. Idempotent \u2014 returns true either way.",inputSchema:z.object({name:z.string().describe("Group name, e.g. 'Machine Learning'")}),annotations:{title:"Add Group",destructiveHint:false,idempotentHint:true,openWorldHint:false}},async e=>{try{return o(n=>C(n,e.name.trim())),{content:[l("true")]}}catch(n){return c(`add_group failed for '${e.name}': ${n}`)}}),o$1.registerTool("remove_group",{title:"Remove Group",description:"Delete a knowledge group. Its topics move to Other. Returns true if it existed.",inputSchema:z.object({name:z.string().describe("Group name to delete")}),annotations:{title:"Remove Group",destructiveHint:true,idempotentHint:true,openWorldHint:false}},async e=>{try{let n=o(r=>D(r,e.name));return {content:[l(String(n))]}}catch(n){return c(`remove_group failed for '${e.name}': ${n}`)}}),o$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'. share_name: the sender name proposed when sharing a lesson (max 80 chars, empty clears). Returns the full updated Settings.",inputSchema:z.object({key:z.enum(["max_per_day","min_gap_minutes","nudge_every","nudge_scope","share_name"]).describe("Setting key"),value:z.string().describe("New value (integer string; 'session'|'global' for nudge_scope; free text for share_name)")}),outputSchema:Oe,annotations:{title:"Update Settings",destructiveHint:false,idempotentHint:true,openWorldHint:false}},e=>{let n=i=>m(o(a=>(H(a,e.key,i),G(a))));if(e.key==="share_name")return e.value.trim().length>l$1?c(`share_name must be at most ${l$1} characters`):n(m$1(e.value));if(e.key==="nudge_scope")return e.value!=="session"&&e.value!=="global"?c("nudge_scope must be 'session' or 'global'"):n(e.value);let r=Number.parseInt(e.value,10);return Number.isNaN(r)?c(`Value must be an integer, got '${e.value}'`):e.key==="max_per_day"&&!(r>=1&&r<=20)?c("max_per_day must be between 1 and 20"):e.key==="min_gap_minutes"&&!(r>=0&&r<=1440)?c("min_gap_minutes must be between 0 and 1440"):e.key==="nudge_every"&&!(r>=0&&r<=1e3)?c("nudge_every must be between 0 and 1000"):n(String(r))}),o$1.registerTool("open_ui",{title:"Open UI",description:"Launch the devcoach web dashboard in the background. port must be 1024-65535.",inputSchema:z.object({port:z.number().int().default(7860).describe("Port (default 7860)")}),annotations:{title:"Open UI",destructiveHint:false,idempotentHint:false,openWorldHint:true}},async e=>{let n=e.port;if(!(n>=1024&&n<=65535))return {content:[l(`error: port ${n} is out of valid range (1024-65535)`)]};let{pingUi:r}=await import('./app-SVGKP5WG.js');return await r(n)?{content:[l(`devcoach UI is already running at http://localhost:${n}`)]}:(spawn(process.execPath,[process.argv[1]??"","ui","--port",String(n)],{detached:true,stdio:"ignore"}).unref(),{content:[l(`devcoach UI starting at http://localhost:${n}`)]})}),o$1.registerTool("stop_ui",{title:"Stop UI",description:"Stop the devcoach web dashboard listening on port (whoever started it \u2014 open_ui or a terminal). Graceful: in-flight requests finish first. Reports stopped=false when nothing is running there. port must be 1024-65535.",inputSchema:z.object({port:z.number().int().default(7860).describe("Port (default 7860)")}),annotations:{title:"Stop UI",destructiveHint:false,idempotentHint:true,openWorldHint:true}},async e=>{let n=e.port;if(!(n>=1024&&n<=65535))return {content:[l(`error: port ${n} is out of valid range (1024-65535)`)]};let{stopUi:r}=await import('./app-SVGKP5WG.js'),i=await r(n);return {content:[l(i?`devcoach UI on port ${n} stopped`:`No devcoach UI is running on port ${n}`)],structuredContent:{stopped:i,port:n}}}),o$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'). Ensures learning-state.md exists and is non-empty (a placeholder if needed) \u2014 there is no notebook argument here. Write the real personalized notebook yourself right after, directly to the path in devcoach://onboarding's notebook_path field. Returns the updated Profile.",inputSchema:z.object({topics:z.record(z.string(),d).describe("{topic_id: confidence 0-10}"),groups:z.record(z.string(),z.array(z.string())).optional().describe("{group_name: [topic_id, ...]}")}),outputSchema:_e,annotations:{title:"Complete Onboarding",destructiveHint:true,idempotentHint:false,openWorldHint:false}},async e=>{try{let n=o(r=>{r.exec("DELETE FROM knowledge; DELETE FROM knowledge_groups; DELETE FROM knowledge_group_names;");for(let[i,a]of Object.entries(e.topics))A(r,i,Math.max(0,Math.min(10,a)));if(e.groups)for(let[i,a]of Object.entries(e.groups))for(let O of a)O in e.topics&&E(r,O,i);return $(r)});return mkdirSync(dirname(j),{recursive:!0}),(!existsSync(j)||statSync(j).size===0)&&writeFileSync(j,`# devcoach \u2014 Coaching Notebook
|
|
3
|
+
`),m(n)}catch(n){return c(`complete_onboarding failed: ${n}`)}}),o$1.registerTool("preview_deep_scan",{title:"Preview Deep Scan",description:"Cheap, metadata-only pre-check for 'Automatic (Deep)' onboarding. Counts and lists Claude Code projects whose last recorded activity falls within a rolling window (months back from now) \u2014 a real date window, not a fixed top-N. Reads no prompt/conversation text, only project paths and activity timestamps. Call this BEFORE spawning the deep-read subagent: over_soft_limit true means there are enough candidates that the user should be asked whether to narrow the window, proceed anyway, or pick specific projects.",inputSchema:z.object({months:z.number().int().min(1).max(24).default(3).describe("Rolling window size in months, counting back from now")}),outputSchema:Ge,annotations:{title:"Preview Deep Scan",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}},e=>{try{return m({...d$1(e.months)})}catch(n){return c(`preview_deep_scan failed: ${n}`)}});let p$1=e=>({title:e,readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false});o$1.registerTool("get_briefing",{title:"Lesson Briefing",description:"Everything needed before delivering a lesson, in ONE call: onboarding status, rate limit, taught topics, knowledge profile, the coaching notebook text and its notebook_path. Same data as the devcoach://briefing resource \u2014 call the tool, it needs no server name.",inputSchema:z.object({}),annotations:p$1("Lesson Briefing")},()=>N("get_briefing",ke())),o$1.registerTool("get_onboarding",{title:"Onboarding Status & Detected Stack",description:"Onboarding status (knowledge_ready, notebook_ready, needs_onboarding), the stack detected across the full Claude Code history with per-project provenance, the default topics and the notebook_path. Same data as the devcoach://onboarding resource.",inputSchema:z.object({}),annotations:p$1("Onboarding Status & Detected Stack")},()=>N("get_onboarding",ve())),o$1.registerTool("get_profile",{title:"Knowledge Profile",description:"Current knowledge map: topics with confidence 0\u201310 and their groups. Same data as the devcoach://profile resource.",inputSchema:z.object({}),outputSchema:_e,annotations:p$1("Knowledge Profile")},()=>N("get_profile",we())),o$1.registerTool("share_lesson",{title:"Share Lesson",description:"Hand a lesson to another person. transport 'text' (default) returns the card plus a one-line code to paste anywhere; 'link' a server-less URL that previews the lesson and imports it into their dashboard; 'file' the contents of a .devcoach.md file. Only title, summary, body, topic, categories and level travel unless include_context is true; local paths never do. Pick the lesson with get_lessons first.",inputSchema:z.object({lesson_id:z.string().describe("Lesson id (from get_lessons)"),transport:z.enum(["text","link","file"]).default("text").describe("text = card + code to paste; link = share URL; file = .devcoach.md contents"),include_context:z.boolean().default(false).describe("Include project/branch/commit/task context (off by default \u2014 privacy). Local paths are never included."),shared_by:z.string().nullish().describe("Sender name. Omit \u2192 the share_name setting, then git user.name. Empty string \u2192 anonymous.")}),outputSchema:Pe,annotations:p$1("Share Lesson")},e=>{try{let n=o(r=>{let i=x(r,e.lesson_id);if(!i)return null;let a=T({explicit:e.shared_by??null,setting:G(r).share_name,gitUserName:a$1()});return S(i,{includeContext:e.include_context,sharedBy:a})});return n?m({transport:e.transport,code:U(n),text:e.transport==="text"?W(n):null,link:e.transport==="link"?X(n):null,markdown:e.transport==="file"?Z(n):null,filename:e.transport==="file"?Y(n):null,shared_by:n.shared_by,include_context:e.include_context,reply_check:$e[e.transport]}):c(`share_lesson: lesson '${e.lesson_id}' not found \u2014 call get_lessons to pick one`)}catch(n){return c(`share_lesson failed: ${n}`)}}),o$1.registerTool("import_lesson",{title:"Import Lesson",description:"Store a lesson someone shared. Pass whatever the user handed you, verbatim: the devcoach:lesson:1:\u2026 code, the whole copied card, the share link, a URL to the lesson text, the contents of a .devcoach.md file, or a lessons JSON export. The lesson joins the coaching log as their own (feedback works as usual) but never counts against the daily limit; re-importing the same share is reported as a duplicate, not an error.",inputSchema:z.object({payload:z.string().min(1).describe("The code, link, URL, card text, file text or JSON \u2014 verbatim")}),outputSchema:Ie,annotations:{title:"Import Lesson",destructiveHint:false,idempotentHint:true,openWorldHint:true}},async e=>{try{let n=c$2(e.payload)&&!e.payload.includes("#devcoach:lesson:")?await d$2(e.payload):e.payload,r=o(i=>da(i,n));return m({...r,reply_check:Ue})}catch(n){return n instanceof R?c(`import_lesson: ${n.message}`):c(`import_lesson failed: ${n}`)}});let d$3=(e,n)=>({title:e,description:n,mimeType:"application/json"});return o$1.registerResource("profile","devcoach://profile",d$3("Knowledge Profile","Current knowledge map \u2014 topics, confidence scores, and groups."),e=>u(e,we())),o$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 n="";try{existsSync(j)&&(n=readFileSync(j,"utf8"));}catch{}return {contents:[{uri:e.href,mimeType:"text/markdown",text:n}]}}),o$1.registerResource("settings","devcoach://settings",d$3("Settings","Current coaching settings (rate limits, UI theme)."),e=>{try{return u(e,o(n=>G(n)))}catch(n){return u(e,{error:String(n)})}}),o$1.registerResource("recent-lessons","devcoach://lessons/recent",d$3("Recent Lessons","Last 10 lessons from the current week."),e=>{try{return u(e,o(n=>r(n,{period:"week",page:1,per_page:10})))}catch(n){return u(e,[{error:String(n)}])}}),o$1.registerResource("stats","devcoach://stats",d$3("Stats","Aggregate coaching statistics: counts and weakest/strongest topics."),e=>{try{return u(e,o(n=>ca(n)))}catch(n){return u(e,{error:String(n)})}}),o$1.registerResource("taught-topics","devcoach://taught-topics",d$3("Taught Topics","All topic_ids already taught \u2014 read before selecting a new lesson topic."),e=>{try{return u(e,o(n=>ea(n)))}catch{return u(e,[])}}),o$1.registerResource("rate-limit","devcoach://rate-limit",d$3("Rate Limit","Current rate-limit status and running lesson total."),e=>{try{let{r:n,total:r}=o(a=>({r:_(a),total:q(a)})),i={allowed:n.allowed,total_lessons:r};return n.reason!=null&&(i.reason=n.reason),u(e,i)}catch(n){return u(e,{allowed:false,reason:`Rate limit check unavailable: ${n}`})}}),o$1.registerResource("briefing","devcoach://briefing",d$3("Lesson Briefing","Everything needed before delivering a lesson in ONE read: onboarding status, rate limit, taught topics, knowledge profile, and the coaching notebook."),e=>u(e,ke())),o$1.registerResource("context","devcoach://context",d$3("Workspace Context","Auto-detected git context and most-used lesson metadata defaults."),e=>{try{let n=b$1(),r=o(i=>N$1(i));return u(e,{git:n,usage_defaults:r})}catch(n){return u(e,{error:String(n)})}}),o$1.registerResource("onboarding","devcoach://onboarding",d$3("Onboarding","Onboarding status, the stack detected across the full Claude Code history (with per-project provenance), and project topic defaults."),e=>u(e,ve())),o$1.registerResource("lesson",new ResourceTemplate("devcoach://lessons/{lesson_id}",{list:void 0}),d$3("Lesson","A single lesson by id."),(e,n)=>{try{let r=String(n.lesson_id),i=o(a=>x(a,r));return i?u(e,i):u(e,{error:`Lesson '${r}' not found`})}catch(r){return u(e,{error:String(r)})}}),o$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(n=>`
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devcoach",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
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",
|