devcoach 2.7.0-next.627.g9b4f241 → 2.7.0-next.632.g2802f52

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/README.md CHANGED
@@ -486,7 +486,9 @@ Plugin releases up to 1.0.1 shipped a manifest that current Claude Code rejects,
486
486
 
487
487
  **"MCP server not connecting"**
488
488
 
489
- Run `devcoach install` to re-register the server with Claude Code or Claude Desktop, then restart the agent. If the issue persists, check `~/.claude.json` (Claude Code) and confirm the `devcoach` entry is present and the command is correct.
489
+ *With the Claude Code plugin* (`plugin:devcoach:devcoach failed to connect`): run **`/devcoach:setup`** — its first step prints the reason (Node older than 24, `npm` not on the PATH, no network on the first run) and the fix; then reconnect from `/mcp`. Do **not** run `devcoach install` for a plugin. Details: [plugin troubleshooting](https://ultimaphoenix.github.io/dev-coach/install/claude-code-plugin#troubleshooting).
490
+
491
+ *Without the plugin* (npm / Homebrew, Claude Desktop): run **`devcoach doctor`**: it checks the command each agent starts (Claude Desktop's from `claude_desktop_config.json`, Claude Code's from `~/.claude.json`) — that it exists on the PATH that agent uses, that the Node it names exists and is ≥ 24, and which devcoach version it runs. Then `devcoach install --force` re-registers it with an absolute command, and you restart the agent. In Claude Desktop's log (`~/Library/Logs/Claude/mcp-server-devcoach.log`), `Failed to spawn process: No such file or directory` means devcoach never ran: the command, or the Node behind it, is missing — see [When devcoach does not start](https://ultimaphoenix.github.io/dev-coach/install/claude-desktop#when-devcoach-does-not-start). Every devcoach start writes one line to that log with its version, Node and install location.
490
492
 
491
493
  **"Coaching isn't firing / no lessons appear"**
492
494
 
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env node
2
+ // devcoach launcher — the single source for the Claude Code plugin and the Gemini CLI extension
3
+ // (scripts/sync-plugin.mjs copies it into both; never edit the copies).
4
+ //
5
+ // The host ships only config + this file: the devcoach binary comes from npm. On first use the
6
+ // *pinned* version (the package.json beside scripts/) is installed once into a persistent data
7
+ // dir, then every start (the MCP server, each hook) imports it in-process.
8
+ //
9
+ // What this file guarantees, because each of these once produced a mute "failed to connect":
10
+ // • a runtime directory exists only when its install FINISHED — npm runs in a temp dir that is
11
+ // renamed into place, so an interrupted install can never look installed;
12
+ // • concurrent starts (the MCP server and the first prompt's hook race on a fresh machine)
13
+ // cannot corrupt each other: the first rename wins, the others use its result;
14
+ // • a hook never makes the user wait for npm: it starts the install in the background;
15
+ // • every failure says why, where the host can show it (see reportFailure).
16
+ import { spawn, spawnSync } from "node:child_process";
17
+ import {
18
+ closeSync,
19
+ existsSync,
20
+ mkdirSync,
21
+ openSync,
22
+ readdirSync,
23
+ readFileSync,
24
+ realpathSync,
25
+ renameSync,
26
+ rmSync,
27
+ statSync,
28
+ utimesSync,
29
+ writeFileSync,
30
+ } from "node:fs";
31
+ import { homedir } from "node:os";
32
+ import { dirname, join } from "node:path";
33
+ import { fileURLToPath, pathToFileURL } from "node:url";
34
+
35
+ export const MIN_NODE_MAJOR = 24;
36
+ const INSTALL_TIMEOUT_MS = 180_000;
37
+ const WAIT_STEP_MS = 250;
38
+ const HOOK_RETRY_AFTER_MS = 10 * 60_000; // a failed background install is retried at most this often
39
+ const NOTICE_EVERY_MS = 24 * 60 * 60_000; // one visible notice a day, never one per prompt
40
+ const LOG_TAIL_LINES = 15;
41
+
42
+ const here = dirname(fileURLToPath(import.meta.url));
43
+ const root = join(here, "..");
44
+
45
+ /** The Node requirement as a sentence, or null when `version` is new enough. */
46
+ export function nodeProblem(version) {
47
+ const major = Number.parseInt(String(version).split(".")[0] ?? "0", 10);
48
+ if (major >= MIN_NODE_MAJOR) return null;
49
+ return (
50
+ `Node ${version} is too old: devcoach needs Node ${MIN_NODE_MAJOR} or newer ` +
51
+ "(it uses the built-in node:sqlite). Upgrade Node (e.g. `brew upgrade node`), then restart."
52
+ );
53
+ }
54
+
55
+ /** A hook subcommand: must never block a prompt nor break a turn. */
56
+ export function isHookMode(mode) {
57
+ return /-hook$/.test(mode ?? "") || mode === "lesson-ready";
58
+ }
59
+
60
+ /** A directory-safe key for what gets installed (a version, or a test tarball spec). */
61
+ export function runtimeKey(spec) {
62
+ return String(spec)
63
+ .replace(/[^a-zA-Z0-9._-]+/g, "_")
64
+ .slice(-80);
65
+ }
66
+
67
+ function loadContext() {
68
+ const config = JSON.parse(readFileSync(join(here, "launch.config.json"), "utf8"));
69
+ const pinned = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).dependencies.devcoach;
70
+ // DEVCOACH_RUNTIME_DIR / DEVCOACH_RUNTIME_SPEC: test and e2e overrides, like DEVCOACH_DIR.
71
+ const spec = process.env.DEVCOACH_RUNTIME_SPEC || pinned;
72
+ const data =
73
+ process.env.DEVCOACH_RUNTIME_DIR ||
74
+ (config.dataEnv && process.env[config.dataEnv]) ||
75
+ (config.dataHome ? join(homedir(), ...config.dataHome) : join(root, ".data"));
76
+ const runtimeRoot = join(data, "runtime");
77
+ const runtimeDir = join(runtimeRoot, runtimeKey(spec));
78
+ return {
79
+ config,
80
+ pinned,
81
+ spec,
82
+ data,
83
+ runtimeRoot,
84
+ runtimeDir,
85
+ binJs: join(runtimeDir, "node_modules", "devcoach", "dist", "bin.js"),
86
+ log: join(data, "install.log"),
87
+ failure: join(data, "install-failed.json"),
88
+ notice: join(data, "notice.stamp"),
89
+ };
90
+ }
91
+
92
+ const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
93
+
94
+ function alive(pid) {
95
+ try {
96
+ process.kill(pid, 0);
97
+ return true;
98
+ } catch (err) {
99
+ return err.code === "EPERM";
100
+ }
101
+ }
102
+
103
+ /** Temp dirs of installs of THIS runtime: [{ dir, pid }]. */
104
+ function installsInProgress(ctx) {
105
+ const prefix = `${runtimeKey(ctx.spec)}.tmp-`;
106
+ let names = [];
107
+ try {
108
+ names = readdirSync(ctx.runtimeRoot);
109
+ } catch {
110
+ return [];
111
+ }
112
+ return names
113
+ .filter((n) => n.startsWith(prefix))
114
+ .map((n) => ({ dir: join(ctx.runtimeRoot, n), pid: Number(n.slice(prefix.length)) }));
115
+ }
116
+
117
+ /** Another live process is installing: wait for its result instead of running a second npm. */
118
+ function waitForOtherInstall(ctx) {
119
+ const deadline = Date.now() + INSTALL_TIMEOUT_MS;
120
+ while (Date.now() < deadline) {
121
+ if (existsSync(ctx.binJs)) return true;
122
+ const live = installsInProgress(ctx).filter((i) => i.pid !== process.pid && alive(i.pid));
123
+ if (live.length === 0) return existsSync(ctx.binJs);
124
+ sleep(WAIT_STEP_MS);
125
+ }
126
+ return existsSync(ctx.binJs);
127
+ }
128
+
129
+ function logTail(ctx) {
130
+ try {
131
+ return readFileSync(ctx.log, "utf8").trimEnd().split("\n").slice(-LOG_TAIL_LINES).join("\n");
132
+ } catch {
133
+ return "";
134
+ }
135
+ }
136
+
137
+ /** Old layouts, other versions and the leftovers of dead installs. */
138
+ function prune(ctx) {
139
+ const keep = runtimeKey(ctx.spec);
140
+ for (const name of readdirSync(ctx.runtimeRoot)) {
141
+ if (name === keep) continue;
142
+ const pid = Number(name.split(".tmp-")[1]);
143
+ if (name.includes(".tmp-") && alive(pid)) continue;
144
+ rmSync(join(ctx.runtimeRoot, name), { recursive: true, force: true });
145
+ }
146
+ // The pre-2.6.4 layout installed straight into the data dir.
147
+ for (const legacy of ["node_modules", "package.json", "package-lock.json"]) {
148
+ rmSync(join(ctx.data, legacy), { recursive: true, force: true });
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Make sure the pinned runtime is installed. Returns null, or the reason it could not be.
154
+ * npm installs into `<runtime>.tmp-<pid>` and the directory is renamed into place on success.
155
+ */
156
+ export function ensureRuntime(ctx) {
157
+ if (existsSync(ctx.binJs)) return null;
158
+ mkdirSync(ctx.runtimeRoot, { recursive: true });
159
+ if (waitForOtherInstall(ctx)) return null;
160
+
161
+ const tmp = `${ctx.runtimeDir}.tmp-${process.pid}`;
162
+ rmSync(tmp, { recursive: true, force: true });
163
+ mkdirSync(tmp, { recursive: true });
164
+ writeFileSync(
165
+ join(tmp, "package.json"),
166
+ JSON.stringify({
167
+ name: "devcoach-runtime",
168
+ private: true,
169
+ dependencies: { devcoach: ctx.spec },
170
+ }),
171
+ );
172
+ writeFileSync(ctx.log, `[${new Date().toISOString()}] npm install devcoach@${ctx.spec}\n`);
173
+ const out = openSync(ctx.log, "a");
174
+ // npm's output goes to the log: it must never reach the MCP server's stdout (the protocol)
175
+ // nor a hook's stdout (the cue).
176
+ const r = spawnSync(
177
+ "npm",
178
+ ["install", "--omit=dev", "--no-audit", "--no-fund", "--no-progress"],
179
+ {
180
+ cwd: tmp,
181
+ stdio: ["ignore", out, out],
182
+ timeout: INSTALL_TIMEOUT_MS,
183
+ shell: process.platform === "win32", // npm is npm.cmd there
184
+ },
185
+ );
186
+ closeSync(out);
187
+
188
+ const installed = existsSync(join(tmp, "node_modules", "devcoach", "dist", "bin.js"));
189
+ if (r.status !== 0 || !installed) {
190
+ rmSync(tmp, { recursive: true, force: true });
191
+ if (r.error?.code === "ENOENT") {
192
+ return "`npm` was not found on the PATH this session uses. Install Node with npm (e.g. `brew install node`), then restart.";
193
+ }
194
+ if (r.error?.code === "ETIMEDOUT") {
195
+ return `installing devcoach ${ctx.spec} took longer than ${INSTALL_TIMEOUT_MS / 1000} s — check the network, then restart. Log: ${ctx.log}`;
196
+ }
197
+ return `could not install devcoach ${ctx.spec} from npm (network needed on first use). Log: ${ctx.log}`;
198
+ }
199
+ try {
200
+ renameSync(tmp, ctx.runtimeDir);
201
+ } catch {
202
+ // Lost the race: another start renamed first. Its tree is complete — use it.
203
+ rmSync(tmp, { recursive: true, force: true });
204
+ if (!existsSync(ctx.binJs))
205
+ return `could not move the installed runtime into ${ctx.runtimeDir}`;
206
+ }
207
+ rmSync(ctx.failure, { force: true });
208
+ prune(ctx);
209
+ return null;
210
+ }
211
+
212
+ function recordFailure(ctx, reason) {
213
+ try {
214
+ mkdirSync(ctx.data, { recursive: true });
215
+ writeFileSync(ctx.failure, JSON.stringify({ at: new Date().toISOString(), reason }));
216
+ } catch {
217
+ // a read-only data dir: the reason still reaches the caller
218
+ }
219
+ }
220
+
221
+ function lastFailure(ctx) {
222
+ try {
223
+ const { at, reason } = JSON.parse(readFileSync(ctx.failure, "utf8"));
224
+ return { ageMs: Date.now() - Date.parse(at), reason: String(reason) };
225
+ } catch {
226
+ return null;
227
+ }
228
+ }
229
+
230
+ /** True at most once per NOTICE_EVERY_MS (the stamp's mtime is the clock). */
231
+ function noticeDue(ctx) {
232
+ try {
233
+ if (Date.now() - statSync(ctx.notice).mtimeMs < NOTICE_EVERY_MS) return false;
234
+ } catch {
235
+ // no stamp yet
236
+ }
237
+ try {
238
+ mkdirSync(ctx.data, { recursive: true });
239
+ writeFileSync(ctx.notice, "");
240
+ utimesSync(ctx.notice, new Date(), new Date());
241
+ } catch {
242
+ // cannot remember: better one notice too many than none
243
+ }
244
+ return true;
245
+ }
246
+
247
+ function diagnosis(ctx, reason) {
248
+ const npm = spawnSync("npm", ["--version"], {
249
+ encoding: "utf8",
250
+ shell: process.platform === "win32",
251
+ });
252
+ const tail = logTail(ctx);
253
+ return [
254
+ `${ctx.config.label}: the devcoach runtime is not available.`,
255
+ ` reason: ${reason}`,
256
+ ` node: ${process.versions.node} (needs ≥ ${MIN_NODE_MAJOR}) — ${process.execPath}`,
257
+ ` npm: ${npm.status === 0 ? npm.stdout.trim() : "not found on PATH"}`,
258
+ ` pinned: devcoach ${ctx.spec}`,
259
+ ` runtime: ${ctx.runtimeDir}${existsSync(ctx.binJs) ? "" : " (not installed)"}`,
260
+ ` log: ${ctx.log}`,
261
+ ...(tail ? [" last lines of the log:", ...tail.split("\n").map((l) => ` ${l}`)] : []),
262
+ ].join("\n");
263
+ }
264
+
265
+ /**
266
+ * Say why devcoach cannot run, where the host shows it:
267
+ * mcp → stderr + exit 1 (the host's MCP log; exit 0 here read as a mute "failed to connect")
268
+ * hook → one systemMessage a day when the host renders it, otherwise silence; always exit 0
269
+ * other → the full diagnosis (this is what `launch.mjs doctor` prints), exit 1
270
+ */
271
+ function reportFailure(ctx, mode, reason) {
272
+ if (isHookMode(mode)) {
273
+ if (ctx.config.hookNotice && noticeDue(ctx)) {
274
+ const hint = ctx.config.setupHint ? ` ${ctx.config.setupHint}` : "";
275
+ process.stdout.write(
276
+ `${JSON.stringify({ systemMessage: `🎓 devcoach is not running: ${reason}${hint}` })}\n`,
277
+ );
278
+ }
279
+ process.exit(0);
280
+ }
281
+ if (mode === "mcp") {
282
+ console.error(`${ctx.config.label}: ${reason}`);
283
+ process.exit(1);
284
+ }
285
+ console.error(diagnosis(ctx, reason));
286
+ process.exit(1);
287
+ }
288
+
289
+ /** A hook found no runtime: install in the background, let the prompt go. */
290
+ function installInBackground(ctx) {
291
+ const failed = lastFailure(ctx);
292
+ if (failed && failed.ageMs < HOOK_RETRY_AFTER_MS) return failed.reason;
293
+ if (installsInProgress(ctx).some((i) => alive(i.pid))) return null;
294
+ const child = spawn(process.execPath, [fileURLToPath(import.meta.url), "--install"], {
295
+ detached: true,
296
+ stdio: "ignore",
297
+ env: process.env,
298
+ });
299
+ child.unref();
300
+ return null;
301
+ }
302
+
303
+ async function main() {
304
+ const mode = process.argv[2];
305
+ const ctx = loadContext();
306
+
307
+ const node = nodeProblem(process.versions.node);
308
+ if (node) reportFailure(ctx, mode, node);
309
+
310
+ if (mode === "--install") {
311
+ const reason = ensureRuntime(ctx);
312
+ if (reason) recordFailure(ctx, reason);
313
+ process.exit(reason ? 1 : 0);
314
+ }
315
+
316
+ if (!existsSync(ctx.binJs)) {
317
+ if (isHookMode(mode)) {
318
+ const reason = installInBackground(ctx);
319
+ if (reason) reportFailure(ctx, mode, reason);
320
+ process.exit(0); // installing: nothing to say yet, and nothing to run
321
+ }
322
+ const reason = ensureRuntime(ctx);
323
+ if (reason) {
324
+ recordFailure(ctx, reason);
325
+ reportFailure(ctx, mode, reason);
326
+ }
327
+ }
328
+
329
+ // Hand off in-process to the real binary — it reads the subcommand from argv.
330
+ process.env.DEVCOACH_LAUNCHER = `${ctx.config.label} · devcoach ${ctx.spec} · ${ctx.runtimeDir}`;
331
+ try {
332
+ await import(pathToFileURL(ctx.binJs).href);
333
+ } catch (err) {
334
+ // A tree that cannot be loaded is not a runtime: drop it so the next start reinstalls.
335
+ if (err?.code === "ERR_MODULE_NOT_FOUND")
336
+ rmSync(ctx.runtimeDir, { recursive: true, force: true });
337
+ reportFailure(ctx, mode, `devcoach ${ctx.spec} could not start: ${err?.message ?? err}`);
338
+ }
339
+ }
340
+
341
+ const invoked = process.argv[1] ? realpathSync(process.argv[1]) : "";
342
+ if (invoked === realpathSync(fileURLToPath(import.meta.url))) await main();
@@ -72,6 +72,15 @@ the user works in daily deserves more topics than a one-off experiment). Do not
72
72
  user to confirm, adjust, or list anything else first — that happens after saving, in
73
73
  Step 5.
74
74
 
75
+ **An empty scan never blocks Automatic.** A new machine, a first project or a sandbox
76
+ has no history: `detected_stack` is `{}` and `scanned_projects` is 0. The user chose
77
+ Automatic; finish it. Use `default_topics` as the starting map exactly as returned (it
78
+ is a general developer baseline, not an invention of yours), save it, and say so in
79
+ the Step 5 summary — *"I found no Claude Code history on this machine, so I started
80
+ you from devcoach's general map; it corrects itself as you work, and you can adjust
81
+ any topic now."* Do not stop, do not go back to the mode question, do not switch to
82
+ Guided unless the user asks, and do not call `preview_deep_scan` for plain Automatic.
83
+
75
84
  **Automatic (Deep) mode:** run the "Automatic (Deep) procedure" below to get an
76
85
  evidence-based `topics`/`groups`/`notebook` result, then skip straight to Step 4 (Deep's
77
86
  subagent already decides groups and composes the notebook — don't redo Step 3 for it).
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import {a as a$2,c as c$1}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g,j as j$1,l,n}from'./chunk-7ISPP7X6.js';import {A,E as E$1,B as B$1,z,a,G,X as X$1,Ba,Fa,Ca,R,V as V$1,S,W as W$1,Da,T,U,L,Ga,ra,va,N,M,P as P$1,H,K as K$1,Ea,J as J$1,O,Y as Y$1,x,Aa,za,c,d,b as b$1,oa,qa,s,ta,sa,ua,xa,wa}from'./chunk-2SDWVJIF.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 j(r){return JSON.stringify(r??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function We(r,e){return r+e.replace(/[^A-Za-z0-9_-]/g,g=>`_${(g.codePointAt(0)??0).toString(16)}`)}var lt=html`<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><circle cx="4" cy="10" r="1.75"/><circle cx="10" cy="10" r="1.75"/><circle cx="16" cy="10" r="1.75"/></svg>`;function Y(r,e="",g=""){return html`<div class="relative ${e}" ${raw(g)} x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
2
+ import {a as a$3,c as c$1}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g,j as j$1,l,n}from'./chunk-CZQNHOLI.js';import {z,E as E$1,A,y,G,X as X$1,Ba,Fa,Ca,R,V as V$1,S,W as W$1,Da,T,U,L,Ga,ra,va,N,M,P as P$1,H,K as K$1,Ea,J as J$1,O,Y as Y$1,w,Aa,za,b as b$1,c,a as a$2,oa,qa,r,ta,sa,ua,xa,wa}from'./chunk-WBZWVWRJ.js';import {a}from'./chunk-OGWDDUZS.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 j(r){return JSON.stringify(r??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function We(r,e){return r+e.replace(/[^A-Za-z0-9_-]/g,g=>`_${(g.codePointAt(0)??0).toString(16)}`)}var lt=html`<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><circle cx="4" cy="10" r="1.75"/><circle cx="10" cy="10" r="1.75"/><circle cx="16" cy="10" r="1.75"/></svg>`;function Y(r,e="",g=""){return html`<div class="relative ${e}" ${raw(g)} x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
3
3
  <button type="button" @click="open = !open" :aria-expanded="open" aria-label="More actions" title="More actions" class="w-8 h-8 inline-flex items-center justify-center rounded-lg border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-accent-400 hover:text-gray-800 dark:hover:text-gray-100">${lt}</button>
4
4
  <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 right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-56 p-1" style="display:none">
5
5
  ${r}
@@ -862,10 +862,10 @@ function updateLabel(inputId, labelId, submitId) {
862
862
  }
863
863
  marked.setOptions({ breaks: true, gfm: true });
864
864
  document.getElementById('notebook-preview-settings').innerHTML = DOMPurify.sanitize(marked.parse(${raw(j(r.notebookContent))}));
865
- </script>`;return P({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:r.uiTheme,body:a,scripts:n})}var Z=25;function B(r,e="/lessons"){return r?.startsWith("/")&&!r.startsWith("//")?r:e}function b(r,e,g=""){let d=r[e];return typeof d=="string"?d:g}var Q=dirname(fileURLToPath(import.meta.url)),X=[join(Q,"../assets/static"),join(Q,"../../assets/static")].find(r=>existsSync(r))??join(Q,"../assets/static"),kt={".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"},vt="https://ultimaphoenix.github.io",nt={"access-control-allow-origin":vt,"access-control-allow-methods":"GET, OPTIONS","access-control-allow-private-network":"true","cache-control":"no-store",vary:"Origin"};function E(r){let e=r.req.header("sec-fetch-site");return e!==void 0&&e!=="same-origin"&&e!=="none"}var ee;function wt(){return ee===void 0&&(ee=n()),ee}function it(r,e,g){let d=ta({explicit:g.name??null,setting:e,gitUserName:wt()}),t=g.includeContext??false,a=sa(r,{includeContext:t,sharedBy:d}),n=ua(a);return {payload:a,state:{id:r.id,open:g.open??false,name:d??"",includeContext:t,text:wa(a,n),link:xa(a,n)}}}function $t(r,e){return r==="lessons"?"/lessons":r==="knowledge"?"/knowledge":e?"/lessons":"/knowledge"}function _t(){try{return E$1(r=>X$1(r).ui_theme)}catch{return "system"}}function St(r={}){let e$1=new Hono;e$1.onError((t,a)=>t instanceof A?a.html(at(t.message),503):(console.error(t),a.text("Internal Server Error",500))),e$1.get("/static/*",t=>{let a=decodeURIComponent(t.req.path.slice(8)),n=join(X,a);if(!n.startsWith(X))return t.notFound();try{let i=readFileSync(n);return new Response(new Uint8Array(i),{headers:{"content-type":kt[extname(n)]??"application/octet-stream","cache-control":"no-cache"}})}catch{return t.notFound()}}),e$1.get("/ping",t=>{let a$1=null;try{a$1=E$1(n=>{let i=B$1(n);return i?{...i,message:z(i)}:null});}catch{}return t.json({ok:true,version:a,compat:a$1},200,nt)}),e$1.options("/ping",t=>t.body(null,204,nt)),e$1.post("/shutdown",t=>E(t)?t.text("Forbidden",403):(setTimeout(()=>r.onShutdown?.(),50),t.json({ok:true}))),e$1.get("/shutdown",t=>t.text("Method Not Allowed",405)),e$1.get("/",t=>{let{home:a,hasLessons:n}=E$1(i=>({home:X$1(i).ui_home,hasLessons:G(i)>0}));return t.redirect($t(a,n),302)}),e$1.get("/knowledge",t=>{let{profile:a,stats:n,rateLimit:i,settings:s}=E$1(p=>({profile:Ca(p),stats:Fa(p),rateLimit:Ba(p),settings:X$1(p)})),c=new Map;for(let p of a.groups)for(let m of p.topics)c.set(m,p.name);let u={};for(let p of a.groups)u[p.name]=[];for(let p of a.knowledge){let m=c.get(p.topic)??"Other",v=u[m]??[];v.push(p),u[m]=v;}return t.html(Ze({categorised:u,allGroups:a.groups.map(p=>p.name),stats:n,rateLimit:i,maxPerDay:s.max_per_day,uiTheme:s.ui_theme}))}),e$1.post("/knowledge",async t=>{let a=await t.req.parseBody(),n=b(a,"topic").trim();if(n){let i=Number.parseInt(b(a,"confidence","5"),10)||5,s=b(a,"group").trim();E$1(c=>{R(c,n,i),s&&s!=="Other"&&V$1(c,n,s);});}return t.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic/delete",t=>(E$1(a=>S(a,t.req.param("topic"))),t.redirect("/knowledge",303))),e$1.post("/knowledge/:topic/group",async t=>{let a=b(await t.req.parseBody(),"group").trim(),n=t.req.param("topic");return E$1(i=>{a&&a!=="Other"?V$1(i,n,a):W$1(i,n);}),t.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic",async t=>{let a=Number.parseInt(b(await t.req.parseBody(),"delta","0"),10)||0;return E$1(n=>Da(n,t.req.param("topic"),a)),t.redirect("/knowledge",303)}),e$1.post("/groups",async t=>{let a=b(await t.req.parseBody(),"group_name").trim();return a&&a!=="Other"&&E$1(n=>T(n,a)),t.redirect("/knowledge",303)}),e$1.post("/groups/:group_name/delete",t=>(E$1(a=>U(a,t.req.param("group_name"))),t.redirect("/knowledge",303))),e$1.get("/lessons/export",()=>{let t=E$1(a=>L(a));return new Response(JSON.stringify(t,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),e$1.post("/lessons/import",async t=>{let a=await t.req.parseBody(),n=b(a,"from")==="lessons",i=p=>t.redirect(n?`/lessons?import=1&error=${p}`:"/settings?imported=0&skipped=0&invalid=1",303);if(E(t))return i("cross");let s=a.file,u=(s instanceof File&&s.size>0?await s.text():null)??b(a,"text").trim();try{p(u)&&!u.includes("#devcoach:lesson:")&&(u=await q(u));let p$1=E$1(m=>Ga(m,u));return p$1.kind==="lessons"||!p$1.lesson?t.redirect(`/settings?imported=${p$1.inserted}&skipped=${p$1.duplicated}&invalid=${p$1.invalid}`,303):t.redirect(`/lessons/${encodeURIComponent(p$1.lesson.id)}?imported=${p$1.inserted?"1":"dup"}`,303)}catch(p){if(p instanceof ra)return i("invalid");throw p}}),e$1.get("/lessons/import",t=>{let a=(t.req.query("code")??"").trim(),n=null,i=null;if(a)try{n=va(a);}catch(s){i=s instanceof ra?s.message:"That code could not be read.";}return t.html(et({code:a,preview:n,error:i,uiTheme:_t()}))}),e$1.get("/lessons",t=>{let a=t.req.query(),n=new Set(["junior","mid","senior"]),i=a.level&&n.has(a.level)?a.level:"",s=a.date_from||"",c=a.date_to||"",u=a.starred==="1",p=a.imported==="1"?"1":a.imported==="0"?"0":"",m=a.shared_by||null,v=a.sort||"timestamp",T=a.order==="asc"?"asc":"desc",R=Math.max(1,Number.parseInt(a.page??"1",10)||1),L=a.period||"all",l={period:s||c?null:L!=="all"?L:null,category:a.category||null,level:i||null,project:a.project||null,repository:a.repository||null,branch:a.branch||null,commit:a.commit||null,starred:u?true:null,imported:m||p==="1"?true:p==="0"?false:null,shared_by:m,search:a.search||null,feedback:a.feedback||null,date_from:s||null,date_to:c||null},x=E$1(k=>{let re=G(k,l),ae=Math.max(1,Math.ceil(re/Z));return R=Math.min(R,ae),{total:re,totalPages:ae,lessons:H(k,{...l,sort:v,order:T,page:R,per_page:Z}),allCategories:P$1(k),allProjects:M(k,"project"),allRepositories:M(k,"repository"),allBranches:M(k,"branch"),allCommits:M(k,"commit_hash"),allSharedBy:N(k),theme:X$1(k).ui_theme}}),$={period:L,category:a.category||"",level:i,project:a.project||"",repository:a.repository||"",branch:a.branch||"",commit:a.commit||"",starred:u,imported:m?"1":p,shared_by:m??"",search:a.search||"",feedback:a.feedback||"",date_from:s,date_to:c,sort:v,order:T},S=a.error==="invalid"||a.error==="cross"?a.error:null;return t.html(Qe({importOpen:a.import==="1",importError:S,lessons:x.lessons,allCategories:x.allCategories,allProjects:x.allProjects,allRepositories:x.allRepositories,allBranches:x.allBranches,allCommits:x.allCommits,allSharedBy:x.allSharedBy,s:$,page:R,perPage:Z,total:x.total,totalPages:x.totalPages,uiTheme:x.theme}))}),e$1.post("/lessons/:lesson_id/star",async t=>{let a=await t.req.parseBody();return E$1(n=>K$1(n,t.req.param("lesson_id"),a.starred==="1")),t.redirect(B(b(a,"next")||void 0),303)}),e$1.post("/lessons/:lesson_id/feedback",async t=>{let a=await t.req.parseBody(),n=b(a,"feedback");if(!["","clear","know","understood","dont_know"].includes(n))return t.text("Unknown feedback value",400);let i=n===""||n==="clear"?null:n;return E$1(s=>Ea(s,t.req.param("lesson_id"),i)),t.redirect(B(b(a,"next")||void 0),303)}),e$1.post("/lessons/delete",async t=>{if(E(t))return t.text("Forbidden",403);let a=await t.req.parseBody({all:true}),n=a.id,i=(Array.isArray(n)?n:[n]).filter(s=>typeof s=="string"&&s!=="");return E$1(s=>J$1(s,i)),t.redirect(B(b(a,"next")||void 0),303)}),e$1.on(["GET","POST"],"/lessons/:lesson_id/share",async t=>{if(E(t))return t.text("Forbidden",403);let a,n,i=false;if(t.req.method==="POST"){let m=await t.req.parseBody();a=b(m,"name"),n=b(m,"include_context")==="1",i=b(m,"persist")==="1";}else a=t.req.query("name"),n=t.req.query("include_context")==="1";let s=E$1(m=>{let v=O(m,t.req.param("lesson_id")??"");return v?(i&&a!==void 0&&Y$1(m,"share_name",x(a)),{lesson:v,settingName:X$1(m).share_name}):null});if(!s)return t.text("Lesson not found",404);let{state:c,payload:u}=it(s.lesson,s.settingName,{name:a,includeContext:n}),p=t.req.query("format");return p==="text"?t.text(c.text):p==="link"?t.text(c.link):p==="md"?new Response(Aa(u),{headers:{"content-type":"text/markdown; charset=utf-8","content-disposition":`attachment; filename="${za(u)}"`}}):p==="panel"?t.html(W(s.lesson.id,c,s.lesson.title)):t.html(V(c))}),e$1.get("/lessons/:lesson_id",t=>{let a=E$1(s=>{let c=O(s,t.req.param("lesson_id"));if(!c)return null;let u=X$1(s),p=h$1(s,{lesson_id:c.id})[0],m$1=p?{id:p.id,title:p.title,status:p.status,...m(p)}:null;return {lesson:c,course:m$1,settingName:u.share_name,uiTheme:u.ui_theme}});if(!a)return t.html("<h1>Lesson not found</h1>",404);let n=t.req.query(),i=n.imported==="1"?"new":n.imported==="dup"?"dup":null;return t.html(Xe({lesson:a.lesson,uiTheme:a.uiTheme,share:it(a.lesson,a.settingName,{open:n.share==="1"}).state,imported:i,course:a.course}))});let g$1={"content-type":"text/html; charset=utf-8","content-security-policy":"sandbox allow-scripts allow-forms; default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; style-src 'unsafe-inline'; img-src data:; font-src data:; form-action 'none'; base-uri 'none'; frame-ancestors 'self'","referrer-policy":"no-referrer","x-content-type-options":"nosniff","cache-control":"no-cache"},d$1=(()=>{try{return `
865
+ </script>`;return P({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:r.uiTheme,body:a,scripts:n})}var Z=25;function B(r,e="/lessons"){return r?.startsWith("/")&&!r.startsWith("//")?r:e}function b(r,e,g=""){let d=r[e];return typeof d=="string"?d:g}var Q=dirname(fileURLToPath(import.meta.url)),X=[join(Q,"../assets/static"),join(Q,"../../assets/static")].find(r=>existsSync(r))??join(Q,"../assets/static"),kt={".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"},vt="https://ultimaphoenix.github.io",nt={"access-control-allow-origin":vt,"access-control-allow-methods":"GET, OPTIONS","access-control-allow-private-network":"true","cache-control":"no-store",vary:"Origin"};function E(r){let e=r.req.header("sec-fetch-site");return e!==void 0&&e!=="same-origin"&&e!=="none"}var ee;function wt(){return ee===void 0&&(ee=n()),ee}function it(r,e,g){let d=ta({explicit:g.name??null,setting:e,gitUserName:wt()}),t=g.includeContext??false,a=sa(r,{includeContext:t,sharedBy:d}),n=ua(a);return {payload:a,state:{id:r.id,open:g.open??false,name:d??"",includeContext:t,text:wa(a,n),link:xa(a,n)}}}function $t(r,e){return r==="lessons"?"/lessons":r==="knowledge"?"/knowledge":e?"/lessons":"/knowledge"}function _t(){try{return E$1(r=>X$1(r).ui_theme)}catch{return "system"}}function St(r$1={}){let e$1=new Hono;e$1.onError((t,a)=>t instanceof z?a.html(at(t.message),503):(console.error(t),a.text("Internal Server Error",500))),e$1.get("/static/*",t=>{let a=decodeURIComponent(t.req.path.slice(8)),n=join(X,a);if(!n.startsWith(X))return t.notFound();try{let i=readFileSync(n);return new Response(new Uint8Array(i),{headers:{"content-type":kt[extname(n)]??"application/octet-stream","cache-control":"no-cache"}})}catch{return t.notFound()}}),e$1.get("/ping",t=>{let a$1=null;try{a$1=E$1(n=>{let i=A(n);return i?{...i,message:y(i)}:null});}catch{}return t.json({ok:true,version:a,compat:a$1},200,nt)}),e$1.options("/ping",t=>t.body(null,204,nt)),e$1.post("/shutdown",t=>E(t)?t.text("Forbidden",403):(setTimeout(()=>r$1.onShutdown?.(),50),t.json({ok:true}))),e$1.get("/shutdown",t=>t.text("Method Not Allowed",405)),e$1.get("/",t=>{let{home:a,hasLessons:n}=E$1(i=>({home:X$1(i).ui_home,hasLessons:G(i)>0}));return t.redirect($t(a,n),302)}),e$1.get("/knowledge",t=>{let{profile:a,stats:n,rateLimit:i,settings:s}=E$1(p=>({profile:Ca(p),stats:Fa(p),rateLimit:Ba(p),settings:X$1(p)})),c=new Map;for(let p of a.groups)for(let m of p.topics)c.set(m,p.name);let u={};for(let p of a.groups)u[p.name]=[];for(let p of a.knowledge){let m=c.get(p.topic)??"Other",v=u[m]??[];v.push(p),u[m]=v;}return t.html(Ze({categorised:u,allGroups:a.groups.map(p=>p.name),stats:n,rateLimit:i,maxPerDay:s.max_per_day,uiTheme:s.ui_theme}))}),e$1.post("/knowledge",async t=>{let a=await t.req.parseBody(),n=b(a,"topic").trim();if(n){let i=Number.parseInt(b(a,"confidence","5"),10)||5,s=b(a,"group").trim();E$1(c=>{R(c,n,i),s&&s!=="Other"&&V$1(c,n,s);});}return t.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic/delete",t=>(E$1(a=>S(a,t.req.param("topic"))),t.redirect("/knowledge",303))),e$1.post("/knowledge/:topic/group",async t=>{let a=b(await t.req.parseBody(),"group").trim(),n=t.req.param("topic");return E$1(i=>{a&&a!=="Other"?V$1(i,n,a):W$1(i,n);}),t.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic",async t=>{let a=Number.parseInt(b(await t.req.parseBody(),"delta","0"),10)||0;return E$1(n=>Da(n,t.req.param("topic"),a)),t.redirect("/knowledge",303)}),e$1.post("/groups",async t=>{let a=b(await t.req.parseBody(),"group_name").trim();return a&&a!=="Other"&&E$1(n=>T(n,a)),t.redirect("/knowledge",303)}),e$1.post("/groups/:group_name/delete",t=>(E$1(a=>U(a,t.req.param("group_name"))),t.redirect("/knowledge",303))),e$1.get("/lessons/export",()=>{let t=E$1(a=>L(a));return new Response(JSON.stringify(t,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),e$1.post("/lessons/import",async t=>{let a=await t.req.parseBody(),n=b(a,"from")==="lessons",i=p=>t.redirect(n?`/lessons?import=1&error=${p}`:"/settings?imported=0&skipped=0&invalid=1",303);if(E(t))return i("cross");let s=a.file,u=(s instanceof File&&s.size>0?await s.text():null)??b(a,"text").trim();try{p(u)&&!u.includes("#devcoach:lesson:")&&(u=await q(u));let p$1=E$1(m=>Ga(m,u));return p$1.kind==="lessons"||!p$1.lesson?t.redirect(`/settings?imported=${p$1.inserted}&skipped=${p$1.duplicated}&invalid=${p$1.invalid}`,303):t.redirect(`/lessons/${encodeURIComponent(p$1.lesson.id)}?imported=${p$1.inserted?"1":"dup"}`,303)}catch(p){if(p instanceof ra)return i("invalid");throw p}}),e$1.get("/lessons/import",t=>{let a=(t.req.query("code")??"").trim(),n=null,i=null;if(a)try{n=va(a);}catch(s){i=s instanceof ra?s.message:"That code could not be read.";}return t.html(et({code:a,preview:n,error:i,uiTheme:_t()}))}),e$1.get("/lessons",t=>{let a=t.req.query(),n=new Set(["junior","mid","senior"]),i=a.level&&n.has(a.level)?a.level:"",s=a.date_from||"",c=a.date_to||"",u=a.starred==="1",p=a.imported==="1"?"1":a.imported==="0"?"0":"",m=a.shared_by||null,v=a.sort||"timestamp",T=a.order==="asc"?"asc":"desc",R=Math.max(1,Number.parseInt(a.page??"1",10)||1),L=a.period||"all",l={period:s||c?null:L!=="all"?L:null,category:a.category||null,level:i||null,project:a.project||null,repository:a.repository||null,branch:a.branch||null,commit:a.commit||null,starred:u?true:null,imported:m||p==="1"?true:p==="0"?false:null,shared_by:m,search:a.search||null,feedback:a.feedback||null,date_from:s||null,date_to:c||null},x=E$1(k=>{let re=G(k,l),ae=Math.max(1,Math.ceil(re/Z));return R=Math.min(R,ae),{total:re,totalPages:ae,lessons:H(k,{...l,sort:v,order:T,page:R,per_page:Z}),allCategories:P$1(k),allProjects:M(k,"project"),allRepositories:M(k,"repository"),allBranches:M(k,"branch"),allCommits:M(k,"commit_hash"),allSharedBy:N(k),theme:X$1(k).ui_theme}}),$={period:L,category:a.category||"",level:i,project:a.project||"",repository:a.repository||"",branch:a.branch||"",commit:a.commit||"",starred:u,imported:m?"1":p,shared_by:m??"",search:a.search||"",feedback:a.feedback||"",date_from:s,date_to:c,sort:v,order:T},S=a.error==="invalid"||a.error==="cross"?a.error:null;return t.html(Qe({importOpen:a.import==="1",importError:S,lessons:x.lessons,allCategories:x.allCategories,allProjects:x.allProjects,allRepositories:x.allRepositories,allBranches:x.allBranches,allCommits:x.allCommits,allSharedBy:x.allSharedBy,s:$,page:R,perPage:Z,total:x.total,totalPages:x.totalPages,uiTheme:x.theme}))}),e$1.post("/lessons/:lesson_id/star",async t=>{let a=await t.req.parseBody();return E$1(n=>K$1(n,t.req.param("lesson_id"),a.starred==="1")),t.redirect(B(b(a,"next")||void 0),303)}),e$1.post("/lessons/:lesson_id/feedback",async t=>{let a=await t.req.parseBody(),n=b(a,"feedback");if(!["","clear","know","understood","dont_know"].includes(n))return t.text("Unknown feedback value",400);let i=n===""||n==="clear"?null:n;return E$1(s=>Ea(s,t.req.param("lesson_id"),i)),t.redirect(B(b(a,"next")||void 0),303)}),e$1.post("/lessons/delete",async t=>{if(E(t))return t.text("Forbidden",403);let a=await t.req.parseBody({all:true}),n=a.id,i=(Array.isArray(n)?n:[n]).filter(s=>typeof s=="string"&&s!=="");return E$1(s=>J$1(s,i)),t.redirect(B(b(a,"next")||void 0),303)}),e$1.on(["GET","POST"],"/lessons/:lesson_id/share",async t=>{if(E(t))return t.text("Forbidden",403);let a,n,i=false;if(t.req.method==="POST"){let m=await t.req.parseBody();a=b(m,"name"),n=b(m,"include_context")==="1",i=b(m,"persist")==="1";}else a=t.req.query("name"),n=t.req.query("include_context")==="1";let s=E$1(m=>{let v=O(m,t.req.param("lesson_id")??"");return v?(i&&a!==void 0&&Y$1(m,"share_name",w(a)),{lesson:v,settingName:X$1(m).share_name}):null});if(!s)return t.text("Lesson not found",404);let{state:c,payload:u}=it(s.lesson,s.settingName,{name:a,includeContext:n}),p=t.req.query("format");return p==="text"?t.text(c.text):p==="link"?t.text(c.link):p==="md"?new Response(Aa(u),{headers:{"content-type":"text/markdown; charset=utf-8","content-disposition":`attachment; filename="${za(u)}"`}}):p==="panel"?t.html(W(s.lesson.id,c,s.lesson.title)):t.html(V(c))}),e$1.get("/lessons/:lesson_id",t=>{let a=E$1(s=>{let c=O(s,t.req.param("lesson_id"));if(!c)return null;let u=X$1(s),p=h$1(s,{lesson_id:c.id})[0],m$1=p?{id:p.id,title:p.title,status:p.status,...m(p)}:null;return {lesson:c,course:m$1,settingName:u.share_name,uiTheme:u.ui_theme}});if(!a)return t.html("<h1>Lesson not found</h1>",404);let n=t.req.query(),i=n.imported==="1"?"new":n.imported==="dup"?"dup":null;return t.html(Xe({lesson:a.lesson,uiTheme:a.uiTheme,share:it(a.lesson,a.settingName,{open:n.share==="1"}).state,imported:i,course:a.course}))});let g$1={"content-type":"text/html; charset=utf-8","content-security-policy":"sandbox allow-scripts allow-forms; default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; style-src 'unsafe-inline'; img-src data:; font-src data:; form-action 'none'; base-uri 'none'; frame-ancestors 'self'","referrer-policy":"no-referrer","x-content-type-options":"nosniff","cache-control":"no-cache"},d=(()=>{try{return `
866
866
  <script>${readFileSync(join(X,"course-frame.js"),"utf8")}</script>
867
- `}catch{return ""}})();return e$1.get("/courses",t=>{let a=E$1(n=>({courses:h$1(n).map(i=>({...i,lesson_title:i.lesson_id?O(n,i.lesson_id)?.title??null:null})),uiTheme:X$1(n).ui_theme}));return t.html(rt(a))}),e$1.get("/courses/:course_id/index.html",t=>{let a=t.req.param("course_id");if(!a$1.test(a))return t.notFound();let n=e(a);if(!n)return t.notFound();try{let i=readFileSync(n),s=i.toString("utf8"),c$1=!c(s)&&d(s)?`
868
- ${b$1()??""}`:"",u=Buffer.concat([i,Buffer.from(d$1+c$1)]);return new Response(new Uint8Array(u),{headers:new Headers(g$1)})}catch{return t.notFound()}}),e$1.get("/courses/:course_id",t=>{let a=t.req.param("course_id"),n=E$1(c=>{let u=g(c,a);return u?{course:u,lessonTitle:u.lesson_id?O(c,u.lesson_id)?.title??null:null,uiTheme:X$1(c).ui_theme}:null});if(!n)return t.html("<h1>Course not found</h1>",404);let i=n.course.steps.find(c=>c.status==="todo")?.position??1,s=Number.parseInt(t.req.query("step")??"",10)||i;return t.html(ot({...n,selected:s,hasDocument:e(a)!==null}))}),e$1.post("/courses/:course_id/steps/:position",async t=>{if(E(t))return t.text("Forbidden",403);let a=await t.req.parseBody(),n=b(a,"status");if(!["todo","done","skipped"].includes(n))return t.text("Unknown status",400);let i=Number.parseInt(t.req.param("position"),10),s=t.req.param("course_id");return E$1(u=>Number.isFinite(i)?j$1(u,s,i,n):null)?t.redirect(B(b(a,"next")||void 0,`/courses/${encodeURIComponent(s)}`),303):t.text("Step not found",404)}),e$1.post("/courses/delete",async t=>{if(E(t))return t.text("Forbidden",403);let a=await t.req.parseBody();return E$1(n=>l(n,b(a,"id"))),t.redirect("/courses",303)}),e$1.get("/settings/export",()=>{let t=E$1(a=>oa(a));return new Response(new Uint8Array(t),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),e$1.post("/settings/import",async t=>{let a=(await t.req.parseBody()).file;if(!(a instanceof File))return t.redirect("/settings?imported=0&skipped=0&invalid=1",303);let n=new Uint8Array(await a.arrayBuffer()),i=E$1(s=>qa(s,n));return t.redirect(`/settings?imported=${i.lessons}&skipped=${i.skipped}&invalid=${i.invalid}&groups=${i.groups}&notebook=${i.learning_state}`,303)}),e$1.get("/settings/notebook/download",()=>{let t=existsSync(s)?readFileSync(s,"utf8"):"";return new Response(t,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),e$1.post("/settings/notebook",async t=>{let a=await t.req.parseBody();return mkdirSync(dirname(s),{recursive:true}),writeFileSync(s,b(a,"content"),"utf8"),t.redirect(B(b(a,"next")||void 0,"/settings?notebook_saved=1"),303)}),e$1.post("/settings",async t=>{let a=await t.req.parseBody(),n=Number.parseInt(b(a,"max_per_day","2"),10)||2,i=Number.parseInt(b(a,"min_gap_minutes","240"),10),s=b(a,"ui_theme","system");["system","dark","light"].includes(s)||(s="system");let c=b(a,"ui_home","auto");["auto","lessons","knowledge"].includes(c)||(c="auto");let u=Math.max(0,Number.parseInt(b(a,"nudge_every","10"),10)||0),p=b(a,"nudge_scope","session");return p!=="session"&&p!=="global"&&(p="session"),E$1(m=>{Y$1(m,"max_per_day",String(n)),Y$1(m,"min_gap_minutes",String(Number.isNaN(i)?240:i)),Y$1(m,"ui_theme",s),Y$1(m,"ui_home",c),Y$1(m,"nudge_every",String(u)),Y$1(m,"nudge_scope",p),Y$1(m,"share_name",x(b(a,"share_name")));}),t.redirect("/settings",303)}),e$1.get("/settings",t=>{let a=E$1(c=>X$1(c)),n=existsSync(s)?readFileSync(s,"utf8"):"",i=t.req.query(),s$1=i.imported!==void 0?{imported:Number(i.imported??0),skipped:Number(i.skipped??0),invalid:Number(i.invalid??0),groups:Number(i.groups??0),notebook:Number(i.notebook??0)}:null;return t.html(st({settings:a,notebookContent:n,notebookPath:s,uiTheme:a.ui_theme,flash:s$1}))}),e$1}function Ct(r,e=2e3){let g=r;return new Promise(d=>{let t=setTimeout(()=>{g.closeAllConnections?.();},e),a=setInterval(()=>g.closeIdleConnections?.(),100);r.close(()=>{clearTimeout(t),clearInterval(a),d();}),g.closeIdleConnections?.();})}async function Tt(r,e=fetch){try{let g=await e(`http://127.0.0.1:${r}/ping`,{signal:AbortSignal.timeout(1500)});if(!g.ok)return null;let d=await g.json();return d.ok===!0?{version:String(d.version??"")}:null}catch{return null}}async function It(r,e,g=Tt){if(e.code==="EADDRINUSE"){let d=await g(r);if(d){let t=`http://localhost:${r}`,a=d.version?` (v${d.version})`:"";return {existingUrl:t,exitCode:0,message:`${a$2.yellow("devcoach UI is already running at")} ${a$2.cyan(c$1(t))}${a}
869
- ${a$2.dim("Open it there, start another with --port <n>, or stop it with: devcoach ui --stop")}`}}return {exitCode:1,message:`${a$2.red(`\u2717 Port ${r} is already in use by another process`)}
870
- ${a$2.dim("Pick another port: devcoach ui --port <n>")}`}}return e.code==="EACCES"?{exitCode:1,message:a$2.red(`\u2717 Port ${r} is not allowed (permission denied) \u2014 use a port \u2265 1024`)}:{exitCode:1,message:a$2.red(`\u2717 Could not start the devcoach UI on port ${r}: ${e.message}`)}}function Kt(r,e={}){let g=false,d=(s,c)=>{if(g){c&&process.exit(130);return}g=true,process.stdout.isTTY&&console.log(a$2.dim(`devcoach UI stopping (${s})\u2026`)),Ct(a).then(()=>{c&&process.exit(0);});},t=St({onShutdown:()=>d("stop requested",false)}),a=serve({fetch:t.fetch,port:r,hostname:"127.0.0.1"},s=>{let c=`http://localhost:${s.port}`;console.log(`devcoach UI running at ${a$2.cyan(c$1(c))}`),process.stdout.isTTY&&console.log(a$2.dim("Press Ctrl+C to stop")),e.onReady?.(c);}),n=["SIGINT","SIGTERM","SIGHUP"],i={SIGINT:()=>d("SIGINT",true),SIGTERM:()=>d("SIGTERM",true),SIGHUP:()=>d("SIGHUP",true)};if(e.handleSignals!==false)for(let s of n)process.on(s,i[s]);return a.once("error",s=>{for(let c of n)process.off(c,i[c]);It(r,s).then(c=>{if(e.onListenError){e.onListenError(c);return}console.error(c.message),process.exit(c.exitCode);});}),a}async function Yt(r,e=fetch){try{return (await e(`http://127.0.0.1:${r}/shutdown`,{method:"POST",signal:AbortSignal.timeout(3e3)})).ok}catch{return false}}
867
+ `}catch{return ""}})();return e$1.get("/courses",t=>{let a=E$1(n=>({courses:h$1(n).map(i=>({...i,lesson_title:i.lesson_id?O(n,i.lesson_id)?.title??null:null})),uiTheme:X$1(n).ui_theme}));return t.html(rt(a))}),e$1.get("/courses/:course_id/index.html",t=>{let a=t.req.param("course_id");if(!a$1.test(a))return t.notFound();let n=e(a);if(!n)return t.notFound();try{let i=readFileSync(n),s=i.toString("utf8"),c$1=!b$1(s)&&c(s)?`
868
+ ${a$2()??""}`:"",u=Buffer.concat([i,Buffer.from(d+c$1)]);return new Response(new Uint8Array(u),{headers:new Headers(g$1)})}catch{return t.notFound()}}),e$1.get("/courses/:course_id",t=>{let a=t.req.param("course_id"),n=E$1(c=>{let u=g(c,a);return u?{course:u,lessonTitle:u.lesson_id?O(c,u.lesson_id)?.title??null:null,uiTheme:X$1(c).ui_theme}:null});if(!n)return t.html("<h1>Course not found</h1>",404);let i=n.course.steps.find(c=>c.status==="todo")?.position??1,s=Number.parseInt(t.req.query("step")??"",10)||i;return t.html(ot({...n,selected:s,hasDocument:e(a)!==null}))}),e$1.post("/courses/:course_id/steps/:position",async t=>{if(E(t))return t.text("Forbidden",403);let a=await t.req.parseBody(),n=b(a,"status");if(!["todo","done","skipped"].includes(n))return t.text("Unknown status",400);let i=Number.parseInt(t.req.param("position"),10),s=t.req.param("course_id");return E$1(u=>Number.isFinite(i)?j$1(u,s,i,n):null)?t.redirect(B(b(a,"next")||void 0,`/courses/${encodeURIComponent(s)}`),303):t.text("Step not found",404)}),e$1.post("/courses/delete",async t=>{if(E(t))return t.text("Forbidden",403);let a=await t.req.parseBody();return E$1(n=>l(n,b(a,"id"))),t.redirect("/courses",303)}),e$1.get("/settings/export",()=>{let t=E$1(a=>oa(a));return new Response(new Uint8Array(t),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),e$1.post("/settings/import",async t=>{let a=(await t.req.parseBody()).file;if(!(a instanceof File))return t.redirect("/settings?imported=0&skipped=0&invalid=1",303);let n=new Uint8Array(await a.arrayBuffer()),i=E$1(s=>qa(s,n));return t.redirect(`/settings?imported=${i.lessons}&skipped=${i.skipped}&invalid=${i.invalid}&groups=${i.groups}&notebook=${i.learning_state}`,303)}),e$1.get("/settings/notebook/download",()=>{let t=existsSync(r)?readFileSync(r,"utf8"):"";return new Response(t,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),e$1.post("/settings/notebook",async t=>{let a=await t.req.parseBody();return mkdirSync(dirname(r),{recursive:true}),writeFileSync(r,b(a,"content"),"utf8"),t.redirect(B(b(a,"next")||void 0,"/settings?notebook_saved=1"),303)}),e$1.post("/settings",async t=>{let a=await t.req.parseBody(),n=Number.parseInt(b(a,"max_per_day","2"),10)||2,i=Number.parseInt(b(a,"min_gap_minutes","240"),10),s=b(a,"ui_theme","system");["system","dark","light"].includes(s)||(s="system");let c=b(a,"ui_home","auto");["auto","lessons","knowledge"].includes(c)||(c="auto");let u=Math.max(0,Number.parseInt(b(a,"nudge_every","10"),10)||0),p=b(a,"nudge_scope","session");return p!=="session"&&p!=="global"&&(p="session"),E$1(m=>{Y$1(m,"max_per_day",String(n)),Y$1(m,"min_gap_minutes",String(Number.isNaN(i)?240:i)),Y$1(m,"ui_theme",s),Y$1(m,"ui_home",c),Y$1(m,"nudge_every",String(u)),Y$1(m,"nudge_scope",p),Y$1(m,"share_name",w(b(a,"share_name")));}),t.redirect("/settings",303)}),e$1.get("/settings",t=>{let a=E$1(c=>X$1(c)),n=existsSync(r)?readFileSync(r,"utf8"):"",i=t.req.query(),s=i.imported!==void 0?{imported:Number(i.imported??0),skipped:Number(i.skipped??0),invalid:Number(i.invalid??0),groups:Number(i.groups??0),notebook:Number(i.notebook??0)}:null;return t.html(st({settings:a,notebookContent:n,notebookPath:r,uiTheme:a.ui_theme,flash:s}))}),e$1}function Ct(r,e=2e3){let g=r;return new Promise(d=>{let t=setTimeout(()=>{g.closeAllConnections?.();},e),a=setInterval(()=>g.closeIdleConnections?.(),100);r.close(()=>{clearTimeout(t),clearInterval(a),d();}),g.closeIdleConnections?.();})}async function Tt(r,e=fetch){try{let g=await e(`http://127.0.0.1:${r}/ping`,{signal:AbortSignal.timeout(1500)});if(!g.ok)return null;let d=await g.json();return d.ok===!0?{version:String(d.version??"")}:null}catch{return null}}async function It(r,e,g=Tt){if(e.code==="EADDRINUSE"){let d=await g(r);if(d){let t=`http://localhost:${r}`,a=d.version?` (v${d.version})`:"";return {existingUrl:t,exitCode:0,message:`${a$3.yellow("devcoach UI is already running at")} ${a$3.cyan(c$1(t))}${a}
869
+ ${a$3.dim("Open it there, start another with --port <n>, or stop it with: devcoach ui --stop")}`}}return {exitCode:1,message:`${a$3.red(`\u2717 Port ${r} is already in use by another process`)}
870
+ ${a$3.dim("Pick another port: devcoach ui --port <n>")}`}}return e.code==="EACCES"?{exitCode:1,message:a$3.red(`\u2717 Port ${r} is not allowed (permission denied) \u2014 use a port \u2265 1024`)}:{exitCode:1,message:a$3.red(`\u2717 Could not start the devcoach UI on port ${r}: ${e.message}`)}}function Kt(r,e={}){let g=false,d=(s,c)=>{if(g){c&&process.exit(130);return}g=true,process.stdout.isTTY&&console.log(a$3.dim(`devcoach UI stopping (${s})\u2026`)),Ct(a).then(()=>{c&&process.exit(0);});},t=St({onShutdown:()=>d("stop requested",false)}),a=serve({fetch:t.fetch,port:r,hostname:"127.0.0.1"},s=>{let c=`http://localhost:${s.port}`;console.log(`devcoach UI running at ${a$3.cyan(c$1(c))}`),process.stdout.isTTY&&console.log(a$3.dim("Press Ctrl+C to stop")),e.onReady?.(c);}),n=["SIGINT","SIGTERM","SIGHUP"],i={SIGINT:()=>d("SIGINT",true),SIGTERM:()=>d("SIGTERM",true),SIGHUP:()=>d("SIGHUP",true)};if(e.handleSignals!==false)for(let s of n)process.on(s,i[s]);return a.once("error",s=>{for(let c of n)process.off(c,i[c]);It(r,s).then(c=>{if(e.onListenError){e.onListenError(c);return}console.error(c.message),process.exit(c.exitCode);});}),a}async function Yt(r,e=fetch){try{return (await e(`http://127.0.0.1:${r}/shutdown`,{method:"POST",signal:AbortSignal.timeout(3e3)})).ok}catch{return false}}
871
871
  export{St as createApp,It as explainListenError,Ct as gracefulShutdown,Tt as pingUi,$t as resolveHomePath,Kt as startUi,Yt as stopUi};
package/dist/bin.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- var r=process.argv[2]??"",e=new Set(["stop-hook","prompt-hook","gemini-stop-hook","gemini-prompt-hook","codex-stop-hook","codex-prompt-hook","onboard-hook","lesson-ready"]);try{if(e.has(r)){let{runHook:o}=await import('./hooks-PXBIIKQC.js');o(r);}else {let{runCli:o}=await import('./commands-WR3FWRJF.js');await o();}}catch(o){o?.name==="SchemaTooNewError"&&(console.error(o.message),process.exit(3)),console.error(o),process.exit(1);}
2
+ import {c as c$1,e as e$1,f}from'./chunk-2ISHU2GT.js';import'./chunk-OGWDDUZS.js';var e=process.argv[2]??"",i=new Set(["stop-hook","prompt-hook","gemini-stop-hook","gemini-prompt-hook","codex-stop-hook","codex-prompt-hook","onboard-hook","lesson-ready"]);function c(){let o=e$1();return `devcoach ${o.version} (${f(o.where)}): `}var n=c$1(process.versions.node);n&&(i.has(e)&&process.exit(0),console.error(`${n} at ${process.execPath}. Upgrade Node, then restart.`),process.exit(1));try{if(i.has(e)){let{runHook:o}=await import('./hooks-6YE4I32R.js');o(e);}else {let{runCli:o}=await import('./commands-234RESFI.js');await o();}}catch(o){o?.name==="SchemaTooNewError"&&(console.error(o.message),process.exit(3)),console.error(c(),o),process.exit(1);}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import {a as a$1}from'./chunk-OGWDDUZS.js';import {realpathSync}from'fs';import {homedir}from'os';var a=24;function u(e,n=process.platform){let t=e.replace(/\\/g,"/");return n==="win32"?t.toLowerCase():t}var f=[["nvm",/\/\.nvm\/versions\/node\/(v?[\d.]+[^/]*)\//],["nvm-windows",/\/appdata\/roaming\/nvm\/(v?[\d.]+[^/]*)\//],["fnm",/\/fnm\/node-versions\/(v?[\d.]+[^/]*)\//],["volta",/\/\.volta\/tools\/image\/node\/(v?[\d.]+[^/]*)\//],["volta",/\/volta\/tools\/image\/node\/(v?[\d.]+[^/]*)\//],["asdf",/\/\.asdf\/installs\/nodejs\/(v?[\d.]+[^/]*)\//],["mise",/\/mise\/installs\/node\/(v?[\d.]+[^/]*)\//],["n",/\/n\/versions\/node\/(v?[\d.]+[^/]*)\//]];function c(e){for(let[n,t]of f){let r=t.exec(e);if(r?.[1])return {manager:n,boundTo:r[1].startsWith("v")?r[1]:`v${r[1]}`}}return null}function g(e){let n=e.toLowerCase();return n.includes("/.claude/plugins/data/")||n.includes("/.claude/plugins/cache/")?"claude-plugin":n.includes("/.devcoach/gemini-ext/")?"gemini-extension":n.includes("/claude extensions/")?"claude-desktop-extension":n.includes("/_npx/")?"npx":n.includes("/cellar/devcoach/")?"homebrew":n.includes("/node_modules/devcoach/")?"npm-global":/\/(src\/bin\.ts|dist\/bin\.js)$/.test(n)?"source":"unknown"}function p(e,n=process.platform){let t=u(e,n),r=c(t),o=t.toLowerCase();return {channel:g(t),manager:r?.manager??(o.includes("/fnm_multishells/")?"fnm":null),boundTo:r?.boundTo??null,ephemeral:o.includes("/_npx/")||o.includes("/fnm_multishells/")}}function v(e,n=process.platform){let t=u(e,n),r=t.toLowerCase(),o=c(t);return o?o.manager:r.includes("/fnm_multishells/")?"fnm":r.includes("/.volta/")||r.includes("/volta/")?"volta":r.includes("/cellar/node")||r.includes("/opt/homebrew/")||r.includes("/linuxbrew/")||r.startsWith("/usr/local/opt/node")?"homebrew":r.startsWith("/usr/bin/")||r.startsWith("/usr/local/bin/")||r.includes("/program files/nodejs/")||r.startsWith("/snap/")?"system":"unknown"}function $(e){return Number.parseInt(e.split(".")[0]??"0",10)>=a?null:`devcoach ${a$1} needs Node ${a} or newer (it uses the built-in node:sqlite); this is Node ${e}`}function h(e,n=homedir(),t=process.platform){if(!n)return e;let r=t==="win32"?"\\":"/",o=t==="win32"?"%USERPROFILE%":"~";return t==="win32"?e.toLowerCase()===n.toLowerCase()?o:e.toLowerCase().startsWith(`${n.toLowerCase()}${r}`)?o+e.slice(n.length):e:e===n?o:e.startsWith(`${n}${r}`)?o+e.slice(n.length):e}function x(e){try{return realpathSync(e)}catch{return e}}function C(e={}){let n=e.platform??process.platform,t=e.execPath??process.execPath,r=e.argv1??x(process.argv[1]??"");return {version:a$1,node:e.nodeVersion??process.versions.node,execPath:t,nodeManager:v(t,n),entry:r,where:p(r,n)}}function w(e){let n=[e.channel];return e.boundTo&&n.push(`bound to Node ${e.boundTo} (${e.manager})`),e.ephemeral&&n.push("temporary location"),n.join(", ")}function R(e,n,t=[],r=homedir(),o=process.platform){let i=d=>h(d,r,o);return [`devcoach ${n.version} ${e}`,`node ${n.node} via ${n.nodeManager} (${i(n.execPath)})`,`${w(n.where)} (${i(n.entry)})`,...t].join(" \xB7 ")}
3
+ export{a,p as b,$ as c,h as d,C as e,w as f,R as g};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import {s,t,pa,da,ea,ga,fa,D,ha,ia,la,ja,ka,ma,ra,ya,d}from'./chunk-WBZWVWRJ.js';import {a}from'./chunk-OGWDDUZS.js';import {lstatSync,mkdirSync,readFileSync,rmSync,existsSync,writeFileSync}from'fs';import {join,sep,basename}from'path';import {execFileSync}from'child_process';import st from'dns';import it from'http';import at from'https';import {isIP}from'net';var w=/^[a-z0-9-]+$/,I=t,h=class extends Error{};function tt(e){if(!w.test(e))throw new h(`Invalid course id '${e}'`)}function y(e){return tt(e),join(s,e)}function W(e){return join(y(e),I)}function B(e){let t=W(e);if(!t.startsWith(y(e)+sep))return null;try{let r=lstatSync(t);return !r.isFile()||r.size>pa?null:t}catch{return null}}function H(e){try{let t=B(e);if(t===null)return !1;let r=d(readFileSync(t,"utf8"));return r===null||Buffer.byteLength(r)>pa?!1:(writeFileSync(t,r),!0)}catch{return false}}function et(e,t){let r=ya(t,"course"),n=r;for(let o=2;ea(e,n)!==null||existsSync(join(s,n));o++)n=`${r}-${o}`;return n}function Ct(e,t){let r=new Date().toISOString(),n={id:et(e,t.title),lesson_id:t.lesson_id??null,topic_id:t.topic_id,title:t.title,goal:t.goal??null,prerequisites:t.prerequisites??[],status:"active",created_at:r,updated_at:r,completed_at:null};return da(e,n),mkdirSync(y(n.id),{recursive:true}),{...n,steps:[]}}function E(e,t){if(!w.test(t))return null;let r=ea(e,t);return r?{...r,steps:ga(e,t)}:null}function bt(e,t={}){return fa(e,t).map(r=>({...r,steps:ga(e,r.id)}))}function xt(e,t,r){return D(e,()=>{let n=E(e,t);if(!n)throw new h(`Course '${t}' not found`);let o=n.steps.length,i=r.after??o;if(!Number.isInteger(i)||i<0||i>o)throw new h(`'after' must be between 0 and ${o} (the course has ${o} steps)`);let a=B(t);if(!a)throw new h(`Course '${t}' has no document yet \u2014 write ${W(t)} first`);let f=readFileSync(a,"utf8");if(!new RegExp(`\\sid=["']${r.anchor}["']`).test(f))throw new h(`No element with id="${r.anchor}" in ${a}`);if(n.steps.some(s=>s.anchor===r.anchor))throw new h(`Step anchor '${r.anchor}' is already registered`);let l={course_id:t,position:i+1,title:r.title,kind:r.kind,anchor:r.anchor,status:"todo",done_at:null};return i<o&&ha(e,t,i+1),ia(e,l),la(e,t,new Date().toISOString()),H(t),l})}function Rt(e,t,r,n){let o=new Date().toISOString();return H(t),D(e,()=>{if(!ja(e,t,r,n,n==="done"?o:null))return null;let a=ga(e,t),f=a.length>0&&a.every(s=>s.status!=="todo"),l=ea(e,t);return l&&f&&l.status==="active"?ka(e,t,"completed",o):l&&!f&&l.status==="completed"?ka(e,t,"active",o):la(e,t,o),E(e,t)})}function _t(e,t,r){return w.test(t)&&ka(e,t,r,new Date().toISOString())?E(e,t):null}function Lt(e,t){if(!w.test(t))return false;let r=ma(e,t);return rmSync(y(t),{recursive:true,force:true}),r}function Dt(e){return {done:e.steps.filter(t=>t.status!=="todo").length,total:e.steps.length}}function C(...e){try{return execFileSync("git",e,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}}var G={"github.com":"github","gitlab.com":"gitlab","bitbucket.org":"bitbucket"};function ot(e){if(!e)return [null,null];let t=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(e);if(t){let n=(t[1]??"").toLowerCase();return [t[2]??e,G[n]??"local"]}let r=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(e);if(r){let n=(r[1]??"").toLowerCase();return [r[2]??e,G[n]??"local"]}return [e,"local"]}function Tt(){return C("config","user.name")}function vt(){let e=process.cwd(),t=C("rev-parse","--abbrev-ref","HEAD"),r=C("rev-parse","HEAD"),n=C("remote","get-url","origin"),[o,i]=ot(n),a;return o?a=o.replace(/\/+$/,"").split("/").pop()??null:a=basename(e)||null,t==="HEAD"&&(t=null),{project:a,repository:o,branch:t,commit_hash:r,folder:e,repository_platform:i}}var j="That URL is too large to be a lesson.",b="Only public http(s) URLs can be imported.",ut=new Set([301,302,303,307,308]);function lt(e){return /^https?:\/\/\S+$/i.test(e.trim())}function z(e){let[t=0,r=0]=e.split(".").map(Number);return t===0||t===10||t===127||t===169&&r===254||t===172&&r>=16&&r<=31||t===192&&r===168||t===100&&r>=64&&r<=127||t>=224}function K(e){let t=e.replace(/^\[|\]$/g,"").replace(/%.*$/,"").toLowerCase();if(isIP(t)===4)return z(t);if(isIP(t)!==6)return true;let r=/^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(t);if(r?.[1])return z(r[1]);if(t==="::"||t==="::1")return true;let n=t.split(":")[0]??"";return !!(/^f[cd]/.test(n)||/^fe[89ab]/.test(n))}async function ct(e){return (await st.promises.lookup(e,{all:true})).map(r=>r.address)}async function pt(e,t){let r=e.hostname.replace(/^\[|\]$/g,"").toLowerCase();if(r==="localhost"||r.endsWith(".localhost"))throw new ra(b);if(isIP(r)){if(K(r))throw new ra(b);return r}let n;try{n=await t(r);}catch{throw new ra("The URL could not be reached \u2014 copy the lesson text instead.")}let o=n[0];if(!o||n.some(i=>K(i)))throw new ra(b);return o}var ft=(e,t,r)=>new Promise((n,o)=>{let i=isIP(t)===6?6:4,a$1=(s,c,m)=>{c.all?m(null,[{address:t,family:i}]):m(null,t,i);},l=(e.protocol==="https:"?at:it).request(e,{method:"GET",lookup:a$1,signal:r,headers:{accept:"text/plain, text/markdown, application/json;q=0.9, */*;q=0.5","user-agent":`devcoach/${a}`}},s=>{let c=s.headers["content-length"];n({status:s.statusCode??0,location:s.headers.location??null,contentLength:c===void 0?null:Number(c),body:s,abort:()=>s.destroy()});});l.on("error",o),l.end();});async function ht(e,t){if(e.contentLength!==null&&e.contentLength>t)throw e.abort(),new ra(j);let r=[],n=0;for await(let o of e.body){if(n+=o.byteLength,n>t)throw e.abort(),new ra(j);r.push(o);}return Buffer.concat(r).toString("utf8")}async function Ft(e,t={}){let r=t.timeoutMs??5e3,n=t.maxBytes??256*1024,o=t.maxRedirects??3,i=t.lookup??ct,a=t.transport??ft;if(!lt(e))throw new ra("Only http(s) URLs can be imported.");let f=new AbortController,l=setTimeout(()=>f.abort(),r);try{let s=new URL(e.trim());for(let c=0;;c++){let m=await pt(s,i),p=await a(s,m,f.signal);if(ut.has(p.status)){if(p.abort(),!p.location||c>=o)throw new ra("The URL redirects too many times \u2014 copy the lesson text instead.");if(s=new URL(p.location,s),!/^https?:$/.test(s.protocol))throw new ra(b);continue}if(p.status<200||p.status>=300)throw p.abort(),new ra(`The URL answered ${p.status} \u2014 nothing to import there.`);return await ht(p,n)}}catch(s){if(s instanceof ra)throw s;let c=s?.name==="AbortError"?"timed out":"could not be reached";throw new ra(`The URL ${c} \u2014 copy the lesson text instead.`)}finally{clearTimeout(l);}}export{w as a,h as b,y as c,W as d,B as e,Ct as f,E as g,bt as h,xt as i,Rt as j,_t as k,Lt as l,Dt as m,Tt as n,vt as o,lt as p,Ft as q};
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import {q,E as E$1,A,ba,na,Ja,p,D,Ia}from'./chunk-WBZWVWRJ.js';import {existsSync,mkdirSync,statSync,writeFileSync,appendFileSync,fstatSync,readFileSync}from'fs';import {join}from'path';function N(e){try{let o=JSON.parse(e);return {stop_hook_active:o?.stop_hook_active===!0,permission_mode:typeof o?.permission_mode=="string"?o.permission_mode:null,session_id:typeof o?.session_id=="string"?o.session_id:null}}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:N(readFileSync(0,"utf8"))}catch{return e}}function $(e){return e.stop_hook_active||e.permission_mode==="plan"}function s(e,o,t){if(process.env.DEVCOACH_HOOK_DEBUG==="1")try{mkdirSync(p,{recursive:!0});let n=join(p,"hook.log");existsSync(n)&&statSync(n).size>256*1024&&writeFileSync(n,""),appendFileSync(n,`${new Date().toISOString()} ${e} ${(o??"-").slice(0,8)} ${t}
3
+ `);}catch{}}var E={claude:"block",gemini:"deny",codex:"block"};function u(e,o,t){let n={decision:E[e],reason:o};t&&(n.systemMessage=t),process.stdout.write(`${JSON.stringify(n)}
4
+ `),process.exit(0);}var g="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 I(e=l()){if($(e)&&process.exit(0),existsSync(q)){let o;try{o=E$1(t=>A(t)!==null||ba(t).knowledge_ready);}catch(t){s("onboard-hook",e.session_id,`error: ${t}`),process.exit(0);}o&&process.exit(0);}s("onboard-hook",e.session_id,"onboarding cue"),u("claude",g);}var a=10,w={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 T(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
+
6
+ ${w[o].cue} NOW and follow it exactly \u2014 the skill is the single source of truth for the entire flow, including the explicit no-op when the completed work does not warrant a lesson.
7
+ ${i}
8
+
9
+ If the devcoach skill is not available, fall back to: call get_briefing (one call: profile, taught topics, notebook); pick ONE untaught profile topic at or above the user's confidence band; call log_lesson silently (body = 3\u20136 short paragraphs tied to the task plus a \`\u{1F4A1} *Senior tip:*\` line, clean markdown without bands or a title line); THEN write the card as the FINAL message of the turn: plain markdown (no '>' blockquote) between two band headings \`### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u{1F393} devcoach \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\` and \`### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 [topic] \xB7 [level] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\`, with \`**[Title]** \xB7 [Category] \xB7 [Level]\`, the body you passed, and the tip \u2014 saving is not showing; the card in the final message is the only thing the user sees. If instead the work does not warrant a lesson (pure questions, chat, nothing technical), call the devcoach \`skip_lesson\` tool with a one-line reason.
10
+
11
+ The rate-limit check is already done by this hook \u2014 skip it. Work in silence: never narrate devcoach tool calls or resource reads ('checking your profile\u2026'). Output only the lesson card (or nothing). No preamble, no meta-commentary.`}function R(e,o,t){let n=o.permission_mode==="plan";if(o.stop_hook_active)return {kind:"silent",note:"hook-forced continuation"};if(A(e))return {kind:"silent",note:"read-only: database newer than this build"};if(!ba(e).knowledge_ready)return !t.onboardCue||n?{kind:"silent",note:"onboarding not complete"}:{kind:"onboard",note:"onboarding cue (empty knowledge)"};if(na(e))return {kind:"silent",note:"course in progress \u2014 cues paused"};let i=Ia(e,o.session_id,{planMode:n});return i.cue?{kind:"cue",nextLessonNumber:i.nextLessonNumber,note:i.reason}:{kind:"silent",note:i.reason}}function C(e,o,t){let n;try{n=E$1(i=>D(i,()=>R(i,o,t)));}catch(i){s(e,o.session_id,`error: ${i}`),process.exit(0);}switch(s(e,o.session_id,n.note),n.kind){case "onboard":u(t.client,g);break;case "cue":u(t.client,T(n.nextLessonNumber,t.client),"\u{1F393} devcoach: checking whether a lesson is due\u2026");break;}process.exit(0);}function B(e=l()){existsSync(q)||process.exit(0),C("lesson-ready",e,{onboardCue:false,client:"claude"});}function f(e=l(),o="claude"){let t=o==="claude"?"stop-hook":`${o}-stop-hook`;existsSync(q)||((e.stop_hook_active||e.permission_mode==="plan")&&process.exit(0),s(t,e.session_id,"onboarding cue (no DB)"),u(o,g)),C(t,e,{onboardCue:true,client:o});}function L(e="claude"){return `devcoach: the lesson pacing threshold will be reached at the end of this turn. Resolve it before finishing:
12
+ - If this task is technical: ${w[e].prime} and follow it exactly to close your reply \u2014 the skill is the single source of truth for the entire lesson flow.
13
+ - If it is not technical: silently call the devcoach \`skip_lesson\` tool with a one-line reason and output nothing about it.
14
+ - Either way, work in silence: never narrate devcoach tool calls or resource reads ('checking your profile\u2026').`}var F={claude:"UserPromptSubmit",gemini:"BeforeAgent",codex:"UserPromptSubmit"};function b(e=l(),o="claude"){let t=o==="claude"?"prompt-hook":`${o}-prompt-hook`;e.permission_mode==="plan"&&process.exit(0),existsSync(q)||process.exit(0);let n;try{n=E$1(i=>A(i)===null&&!na(i)&&Ja(i,e.session_id).wouldCue);}catch(i){s(t,e.session_id,`error: ${i}`),process.exit(0);}s(t,e.session_id,n?"priming":"no prime"),n||process.exit(0),process.stdout.write(`${JSON.stringify({hookSpecificOutput:{hookEventName:F[o],additionalContext:L(o)}})}
15
+ `),process.exit(0);}function K(e,o){let t=()=>o??l();switch(e){case "stop-hook":f(t());break;case "gemini-stop-hook":f(t(),"gemini");break;case "codex-stop-hook":f(t(),"codex");break;case "prompt-hook":b(t());break;case "gemini-prompt-hook":b(t(),"gemini");break;case "codex-prompt-hook":b(t(),"codex");break;case "onboard-hook":I(t());break;case "lesson-ready":B(t());break;default:process.exit(0);}}export{N as a,I as b,T as c,B as d,f as e,L as f,b as g,K as h};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ var e={version:"2.7.0-next.632.g2802f52"};var s=e.version;export{s as a};