@aexol/opencode-wizard 0.3.0 → 0.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.
@@ -0,0 +1,4 @@
1
+ import type http from 'node:http';
2
+ export declare const escapeHtml: (value: string) => string;
3
+ export declare const renderOAuthCallbackPage: (statusCode: number, title: string, message: string) => string;
4
+ export declare const sendOAuthCallbackHtmlResponse: (response: http.ServerResponse, statusCode: number, title: string, message: string) => void;
@@ -0,0 +1,192 @@
1
+ export const escapeHtml = value => {
2
+ return value.replace(/[&<>'"]/g, character => {
3
+ const replacements = {
4
+ '&': '&amp;',
5
+ '<': '&lt;',
6
+ '>': '&gt;',
7
+ "'": '&#39;',
8
+ '"': '&quot;'
9
+ };
10
+ return replacements[character] ?? character;
11
+ });
12
+ };
13
+ export const renderOAuthCallbackPage = (statusCode, title, message) => {
14
+ const escapedTitle = escapeHtml(title);
15
+ const escapedMessage = escapeHtml(message);
16
+ const isSuccess = statusCode >= 200 && statusCode < 300;
17
+ const pageState = isSuccess ? 'success' : statusCode === 404 ? 'not-found' : 'error';
18
+ const cardTitle = isSuccess ? 'Authorization successful' : statusCode === 404 ? 'Callback not found' : 'Authorization failed';
19
+ const escapedCardTitle = escapeHtml(cardTitle);
20
+ const eyebrow = isSuccess ? 'Authorization complete' : statusCode === 404 ? 'Callback route not found' : 'Authorization needs attention';
21
+ const actionText = isSuccess ? 'This window will close automatically in a moment. You can also close it now and return to OpenCode.' : 'You can close this window and return to OpenCode to try again.';
22
+ const autoCloseScript = isSuccess ? `<script>
23
+ window.setTimeout(() => window.close(), 2000);
24
+ </script>` : '';
25
+ const stateIcon = isSuccess ? '<path d="M7 12.5l3.1 3.1L17.5 8" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>' : statusCode === 404 ? '<path d="M10.5 17a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Z" stroke="currentColor" stroke-width="2.2"/><path d="m15.5 15.5 4 4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>' : '<path d="M12 7v6" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/><path d="M12 17.2v.1" stroke="currentColor" stroke-width="3.2" stroke-linecap="round"/>';
26
+ return `<!doctype html>
27
+ <html lang="en">
28
+ <head>
29
+ <meta charset="utf-8">
30
+ <meta name="viewport" content="width=device-width, initial-scale=1">
31
+ <meta name="color-scheme" content="light dark">
32
+ <title>${escapedTitle}</title>
33
+ <style>
34
+ :root {
35
+ color-scheme: light dark;
36
+ --page-bg: #f2efe7;
37
+ --page-ink: #211d18;
38
+ --muted: #6c6258;
39
+ --panel: rgba(255, 252, 245, 0.82);
40
+ --panel-border: rgba(78, 66, 52, 0.16);
41
+ --success: #167848;
42
+ --error: #ba3329;
43
+ --not-found: #986614;
44
+ --glow: rgba(22, 120, 72, 0.18);
45
+ }
46
+
47
+ @media (prefers-color-scheme: dark) {
48
+ :root {
49
+ --page-bg: #12100d;
50
+ --page-ink: #f7efe2;
51
+ --muted: #b8aa98;
52
+ --panel: rgba(30, 26, 22, 0.78);
53
+ --panel-border: rgba(255, 244, 224, 0.14);
54
+ --success: #71e0a6;
55
+ --error: #ff897e;
56
+ --not-found: #f7c96f;
57
+ --glow: rgba(113, 224, 166, 0.2);
58
+ }
59
+ }
60
+
61
+ * {
62
+ box-sizing: border-box;
63
+ }
64
+
65
+ body {
66
+ min-height: 100vh;
67
+ margin: 0;
68
+ display: grid;
69
+ place-items: center;
70
+ padding: 24px;
71
+ overflow: hidden;
72
+ background:
73
+ radial-gradient(circle at 18% 18%, var(--glow), transparent 34rem),
74
+ radial-gradient(circle at 82% 12%, rgba(209, 142, 72, 0.18), transparent 30rem),
75
+ linear-gradient(135deg, var(--page-bg), color-mix(in srgb, var(--page-bg) 76%, #000 24%));
76
+ color: var(--page-ink);
77
+ font-family: ui-rounded, "SF Pro Rounded", "Segoe UI", system-ui, sans-serif;
78
+ }
79
+
80
+ body::before {
81
+ content: "";
82
+ position: fixed;
83
+ inset: -20%;
84
+ pointer-events: none;
85
+ background-image:
86
+ linear-gradient(rgba(128, 104, 74, 0.08) 1px, transparent 1px),
87
+ linear-gradient(90deg, rgba(128, 104, 74, 0.08) 1px, transparent 1px);
88
+ background-size: 42px 42px;
89
+ mask-image: radial-gradient(circle at center, black, transparent 68%);
90
+ }
91
+
92
+ main {
93
+ position: relative;
94
+ width: min(100%, 560px);
95
+ padding: clamp(28px, 7vw, 56px);
96
+ border: 1px solid var(--panel-border);
97
+ border-radius: 32px;
98
+ background: var(--panel);
99
+ box-shadow: 0 24px 90px rgba(0, 0, 0, 0.24);
100
+ text-align: center;
101
+ backdrop-filter: blur(18px) saturate(1.2);
102
+ }
103
+
104
+ .mark {
105
+ width: 72px;
106
+ height: 72px;
107
+ margin: 0 auto 24px;
108
+ display: grid;
109
+ place-items: center;
110
+ border-radius: 24px;
111
+ color: var(--state-color);
112
+ background: color-mix(in srgb, var(--state-color) 16%, transparent);
113
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--state-color) 28%, transparent);
114
+ }
115
+
116
+ [data-state="success"] { --state-color: var(--success); }
117
+ [data-state="error"] { --state-color: var(--error); }
118
+ [data-state="not-found"] { --state-color: var(--not-found); }
119
+
120
+ .eyebrow {
121
+ margin: 0 0 10px;
122
+ color: var(--state-color);
123
+ font-size: 0.78rem;
124
+ font-weight: 800;
125
+ letter-spacing: 0.14em;
126
+ text-transform: uppercase;
127
+ }
128
+
129
+ h1 {
130
+ margin: 0;
131
+ font-size: clamp(2rem, 7vw, 3.35rem);
132
+ line-height: 0.95;
133
+ letter-spacing: -0.06em;
134
+ }
135
+
136
+ .message {
137
+ margin: 22px auto 0;
138
+ max-width: 38rem;
139
+ color: var(--muted);
140
+ font-size: clamp(1rem, 2.5vw, 1.1rem);
141
+ line-height: 1.65;
142
+ }
143
+
144
+ .next-step {
145
+ margin: 26px 0 0;
146
+ padding: 14px 18px;
147
+ border-radius: 999px;
148
+ background: color-mix(in srgb, var(--state-color) 12%, transparent);
149
+ color: var(--page-ink);
150
+ font-size: 0.94rem;
151
+ line-height: 1.5;
152
+ }
153
+
154
+ @media (max-width: 520px) {
155
+ body {
156
+ padding: 16px;
157
+ }
158
+
159
+ main {
160
+ border-radius: 24px;
161
+ }
162
+
163
+ .next-step {
164
+ border-radius: 18px;
165
+ }
166
+ }
167
+ </style>
168
+ </head>
169
+ <body>
170
+ <main data-state="${pageState}" aria-labelledby="callback-title">
171
+ <div class="mark" aria-hidden="true">
172
+ <svg width="34" height="34" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
173
+ ${stateIcon}
174
+ </svg>
175
+ </div>
176
+ <p class="eyebrow">${eyebrow}</p>
177
+ <h1 id="callback-title">${escapedCardTitle}</h1>
178
+ <p class="message">${escapedMessage}</p>
179
+ <p class="next-step">${actionText}</p>
180
+ </main>
181
+ ${autoCloseScript}
182
+ </body>
183
+ </html>`;
184
+ };
185
+ export const sendOAuthCallbackHtmlResponse = (response, statusCode, title, message) => {
186
+ response.writeHead(statusCode, {
187
+ 'content-type': 'text/html; charset=utf-8',
188
+ 'cache-control': 'no-store'
189
+ });
190
+ response.end(renderOAuthCallbackPage(statusCode, title, message));
191
+ };
192
+ //# sourceMappingURL=oauth-callback-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["escapeHtml","value","replace","character","replacements","renderOAuthCallbackPage","statusCode","title","message","escapedTitle","escapedMessage","isSuccess","pageState","cardTitle","escapedCardTitle","eyebrow","actionText","autoCloseScript","stateIcon","sendOAuthCallbackHtmlResponse","response","writeHead","end"],"sources":["../src/oauth-callback-page.ts"],"sourcesContent":["import type http from 'node:http';\n\nexport const escapeHtml = (value: string): string => {\n return value.replace(/[&<>'\"]/g, (character) => {\n const replacements: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n \"'\": '&#39;',\n '\"': '&quot;',\n };\n\n return replacements[character] ?? character;\n });\n};\n\nexport const renderOAuthCallbackPage = (statusCode: number, title: string, message: string): string => {\n const escapedTitle = escapeHtml(title);\n const escapedMessage = escapeHtml(message);\n const isSuccess = statusCode >= 200 && statusCode < 300;\n const pageState = isSuccess ? 'success' : statusCode === 404 ? 'not-found' : 'error';\n const cardTitle = isSuccess\n ? 'Authorization successful'\n : statusCode === 404\n ? 'Callback not found'\n : 'Authorization failed';\n const escapedCardTitle = escapeHtml(cardTitle);\n const eyebrow = isSuccess\n ? 'Authorization complete'\n : statusCode === 404\n ? 'Callback route not found'\n : 'Authorization needs attention';\n const actionText = isSuccess\n ? 'This window will close automatically in a moment. You can also close it now and return to OpenCode.'\n : 'You can close this window and return to OpenCode to try again.';\n const autoCloseScript = isSuccess\n ? `<script>\n window.setTimeout(() => window.close(), 2000);\n </script>`\n : '';\n const stateIcon = isSuccess\n ? '<path d=\"M7 12.5l3.1 3.1L17.5 8\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>'\n : statusCode === 404\n ? '<path d=\"M10.5 17a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Z\" stroke=\"currentColor\" stroke-width=\"2.2\"/><path d=\"m15.5 15.5 4 4\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\"/>'\n : '<path d=\"M12 7v6\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\"/><path d=\"M12 17.2v.1\" stroke=\"currentColor\" stroke-width=\"3.2\" stroke-linecap=\"round\"/>';\n\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <meta name=\"color-scheme\" content=\"light dark\">\n <title>${escapedTitle}</title>\n <style>\n :root {\n color-scheme: light dark;\n --page-bg: #f2efe7;\n --page-ink: #211d18;\n --muted: #6c6258;\n --panel: rgba(255, 252, 245, 0.82);\n --panel-border: rgba(78, 66, 52, 0.16);\n --success: #167848;\n --error: #ba3329;\n --not-found: #986614;\n --glow: rgba(22, 120, 72, 0.18);\n }\n\n @media (prefers-color-scheme: dark) {\n :root {\n --page-bg: #12100d;\n --page-ink: #f7efe2;\n --muted: #b8aa98;\n --panel: rgba(30, 26, 22, 0.78);\n --panel-border: rgba(255, 244, 224, 0.14);\n --success: #71e0a6;\n --error: #ff897e;\n --not-found: #f7c96f;\n --glow: rgba(113, 224, 166, 0.2);\n }\n }\n\n * {\n box-sizing: border-box;\n }\n\n body {\n min-height: 100vh;\n margin: 0;\n display: grid;\n place-items: center;\n padding: 24px;\n overflow: hidden;\n background:\n radial-gradient(circle at 18% 18%, var(--glow), transparent 34rem),\n radial-gradient(circle at 82% 12%, rgba(209, 142, 72, 0.18), transparent 30rem),\n linear-gradient(135deg, var(--page-bg), color-mix(in srgb, var(--page-bg) 76%, #000 24%));\n color: var(--page-ink);\n font-family: ui-rounded, \"SF Pro Rounded\", \"Segoe UI\", system-ui, sans-serif;\n }\n\n body::before {\n content: \"\";\n position: fixed;\n inset: -20%;\n pointer-events: none;\n background-image:\n linear-gradient(rgba(128, 104, 74, 0.08) 1px, transparent 1px),\n linear-gradient(90deg, rgba(128, 104, 74, 0.08) 1px, transparent 1px);\n background-size: 42px 42px;\n mask-image: radial-gradient(circle at center, black, transparent 68%);\n }\n\n main {\n position: relative;\n width: min(100%, 560px);\n padding: clamp(28px, 7vw, 56px);\n border: 1px solid var(--panel-border);\n border-radius: 32px;\n background: var(--panel);\n box-shadow: 0 24px 90px rgba(0, 0, 0, 0.24);\n text-align: center;\n backdrop-filter: blur(18px) saturate(1.2);\n }\n\n .mark {\n width: 72px;\n height: 72px;\n margin: 0 auto 24px;\n display: grid;\n place-items: center;\n border-radius: 24px;\n color: var(--state-color);\n background: color-mix(in srgb, var(--state-color) 16%, transparent);\n box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--state-color) 28%, transparent);\n }\n\n [data-state=\"success\"] { --state-color: var(--success); }\n [data-state=\"error\"] { --state-color: var(--error); }\n [data-state=\"not-found\"] { --state-color: var(--not-found); }\n\n .eyebrow {\n margin: 0 0 10px;\n color: var(--state-color);\n font-size: 0.78rem;\n font-weight: 800;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n }\n\n h1 {\n margin: 0;\n font-size: clamp(2rem, 7vw, 3.35rem);\n line-height: 0.95;\n letter-spacing: -0.06em;\n }\n\n .message {\n margin: 22px auto 0;\n max-width: 38rem;\n color: var(--muted);\n font-size: clamp(1rem, 2.5vw, 1.1rem);\n line-height: 1.65;\n }\n\n .next-step {\n margin: 26px 0 0;\n padding: 14px 18px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--state-color) 12%, transparent);\n color: var(--page-ink);\n font-size: 0.94rem;\n line-height: 1.5;\n }\n\n @media (max-width: 520px) {\n body {\n padding: 16px;\n }\n\n main {\n border-radius: 24px;\n }\n\n .next-step {\n border-radius: 18px;\n }\n }\n </style>\n </head>\n <body>\n <main data-state=\"${pageState}\" aria-labelledby=\"callback-title\">\n <div class=\"mark\" aria-hidden=\"true\">\n <svg width=\"34\" height=\"34\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n ${stateIcon}\n </svg>\n </div>\n <p class=\"eyebrow\">${eyebrow}</p>\n <h1 id=\"callback-title\">${escapedCardTitle}</h1>\n <p class=\"message\">${escapedMessage}</p>\n <p class=\"next-step\">${actionText}</p>\n </main>\n ${autoCloseScript}\n </body>\n</html>`;\n};\n\nexport const sendOAuthCallbackHtmlResponse = (\n response: http.ServerResponse,\n statusCode: number,\n title: string,\n message: string,\n) => {\n response.writeHead(statusCode, {\n 'content-type': 'text/html; charset=utf-8',\n 'cache-control': 'no-store',\n });\n response.end(renderOAuthCallbackPage(statusCode, title, message));\n};\n"],"mappings":"AAEA,OAAO,MAAMA,UAAU,GAAIC,KAAa,IAAa;EACnD,OAAOA,KAAK,CAACC,OAAO,CAAC,UAAU,EAAGC,SAAS,IAAK;IAC9C,MAAMC,YAAoC,GAAG;MAC3C,GAAG,EAAE,OAAO;MACZ,GAAG,EAAE,MAAM;MACX,GAAG,EAAE,MAAM;MACX,GAAG,EAAE,OAAO;MACZ,GAAG,EAAE;IACP,CAAC;IAED,OAAOA,YAAY,CAACD,SAAS,CAAC,IAAIA,SAAS;EAC7C,CAAC,CAAC;AACJ,CAAC;AAED,OAAO,MAAME,uBAAuB,GAAGA,CAACC,UAAkB,EAAEC,KAAa,EAAEC,OAAe,KAAa;EACrG,MAAMC,YAAY,GAAGT,UAAU,CAACO,KAAK,CAAC;EACtC,MAAMG,cAAc,GAAGV,UAAU,CAACQ,OAAO,CAAC;EAC1C,MAAMG,SAAS,GAAGL,UAAU,IAAI,GAAG,IAAIA,UAAU,GAAG,GAAG;EACvD,MAAMM,SAAS,GAAGD,SAAS,GAAG,SAAS,GAAGL,UAAU,KAAK,GAAG,GAAG,WAAW,GAAG,OAAO;EACpF,MAAMO,SAAS,GAAGF,SAAS,GACvB,0BAA0B,GAC1BL,UAAU,KAAK,GAAG,GAChB,oBAAoB,GACpB,sBAAsB;EAC5B,MAAMQ,gBAAgB,GAAGd,UAAU,CAACa,SAAS,CAAC;EAC9C,MAAME,OAAO,GAAGJ,SAAS,GACrB,wBAAwB,GACxBL,UAAU,KAAK,GAAG,GAChB,0BAA0B,GAC1B,+BAA+B;EACrC,MAAMU,UAAU,GAAGL,SAAS,GACxB,qGAAqG,GACrG,gEAAgE;EACpE,MAAMM,eAAe,GAAGN,SAAS,GAC7B;AACN;AACA,cAAc,GACR,EAAE;EACN,MAAMO,SAAS,GAAGP,SAAS,GACvB,4HAA4H,GAC5HL,UAAU,KAAK,GAAG,GAChB,gMAAgM,GAChM,4KAA4K;EAElL,OAAO;AACT;AACA;AACA;AACA;AACA;AACA,aAAaG,YAAY;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAwBG,SAAS;AACjC;AACA;AACA,YAAYM,SAAS;AACrB;AACA;AACA,2BAA2BH,OAAO;AAClC,gCAAgCD,gBAAgB;AAChD,2BAA2BJ,cAAc;AACzC,6BAA6BM,UAAU;AACvC;AACA,MAAMC,eAAe;AACrB;AACA,QAAQ;AACR,CAAC;AAED,OAAO,MAAME,6BAA6B,GAAGA,CAC3CC,QAA6B,EAC7Bd,UAAkB,EAClBC,KAAa,EACbC,OAAe,KACZ;EACHY,QAAQ,CAACC,SAAS,CAACf,UAAU,EAAE;IAC7B,cAAc,EAAE,0BAA0B;IAC1C,eAAe,EAAE;EACnB,CAAC,CAAC;EACFc,QAAQ,CAACE,GAAG,CAACjB,uBAAuB,CAACC,UAAU,EAAEC,KAAK,EAAEC,OAAO,CAAC,CAAC;AACnE,CAAC","ignoreList":[]}
package/dist/server.js CHANGED
@@ -7,6 +7,7 @@ import { execFile } from 'node:child_process';
7
7
  import { promisify } from 'node:util';
8
8
  import { URL, fileURLToPath } from 'node:url';
9
9
  import { resolveBackendOriginFromValues } from './config.js';
10
+ import { sendOAuthCallbackHtmlResponse } from './oauth-callback-page.js';
10
11
  import { deleteFileIfExists, readJsonFile, writePrivateJsonFile } from './storage.js';
11
12
  const execFileAsync = promisify(execFile);
12
13
  const MODULE_FILE_PATH = fileURLToPath(import.meta.url);
@@ -858,8 +859,8 @@ export const buildSystemNote = (result, config, details) => {
858
859
  const projectSkills = catalog.skills.filter(skill => skill.contextKind === 'project' && !isUserPublishedSkillAssignment(skill.assignmentSource)).slice(0, 5).map(buildSkillCatalogLine);
859
860
  const userSkills = catalog.skills.filter(skill => isUserPublishedSkillAssignment(skill.assignmentSource)).slice(0, 5).map(buildSkillCatalogLine);
860
861
  const detailLines = details.slice(0, SYSTEM_NOTE_DETAIL_LIMIT).map(buildSkillDetailSnippetLine);
861
- const detailBlock = detailLines.length > 0 ? ` Loaded body snippets (capped):\n${truncateText(detailLines.join('\n'), SYSTEM_NOTE_DETAIL_CHAR_LIMIT)}` : '';
862
- return [result.fetchResult.payload.workspace ? `Prefer opencode-wizard backend-published fetched skill bodies for scoped/private wizard skills in workspace ${result.fetchResult.payload.workspace.slug}.` : 'Prefer opencode-wizard backend-published global fetched skill bodies; workspace-specific skills are unavailable because the workspace was not found.', `Current directory: ${result.directoryPath}.`, `Published skills for this scope: ${renderedSkillNames}${renderedCountSuffix}; counts: ${catalog.assignmentCounts.global} global, ${catalog.assignmentCounts.project} workspace, ${catalog.assignmentCounts.user} user, ${catalog.assignmentCounts.other} other (GLOBAL SCOPE: ${catalog.assignmentCounts.global}, PROJECT SCOPE: ${catalog.assignmentCounts.project}, USER SCOPE: ${catalog.assignmentCounts.user}).`, 'Wizard-published skills listed in this note/catalog are not necessarily native OpenCode skills; they may be private, scoped, or backend-published records that only the opencode-wizard fetch tool can load.', 'Use the native OpenCode skill tool only for local/native skills that appear in native available_skills; do not use it for wizard-private/scoped/backend-published skill slugs unless that same skill is also listed by native available_skills.', 'If a native skill call fails with an error like `Skill "diagnose" not found. Available skills: ...` and `diagnose` (or the requested slug/name) is wizard-listed here, treat that failure as evidence to call opencode_wizard_published_skills_fetch for the wizard skill body instead.', 'Use catalog whenToUse guidance to decide applicability; fetch full bodies with opencode_wizard_published_skills_fetch and prefer those fetched bodies for current scoped/private wizard guidance, preferring the `skills` field with comma/newline-separated identifiers for multiple bodies; `skill` is for one identifier but remains backward-compatible with delimited lists.', 'GLOBAL_CONTEXT skills are active context skills and are not project-installable; PROJECT_INSTALLABLE skills can be assigned to GLOBAL SCOPE, PROJECT SCOPE, or USER SCOPE; assignment rows decide which scope is active here.', globalSkills.length > 0 ? `GLOBAL SCOPE / Global context skills:\n${globalSkills.join('\n')}` : 'GLOBAL SCOPE / Global context skills: none.', projectSkills.length > 0 ? `PROJECT SCOPE / Project-scoped active skills:\n${projectSkills.join('\n')}` : 'PROJECT SCOPE / Project-scoped active skills: none.', userSkills.length > 0 ? `USER SCOPE / User-scoped active skills:\n${userSkills.join('\n')}` : 'USER SCOPE / User-scoped active skills: none.', detailBlock, 'Local/native sources can still complement wizard skills: .opencode/skills is source seed content, skills.urls is a public/static complement, and backend-published fetched bodies are preferred for private/scoped wizard guidance.', `Root source seed path remains seed/source content: ${config.rootSkillSeedPath}/**.`].filter(line => line.length > 0).join(' ');
862
+ const detailBlock = detailLines.length > 0 ? `Loaded body snippets (capped):\n${truncateText(detailLines.join('\n'), SYSTEM_NOTE_DETAIL_CHAR_LIMIT)}` : '';
863
+ return [result.fetchResult.payload.workspace ? `Workspace: ${result.fetchResult.payload.workspace.slug}.` : 'Workspace not found; workspace-scoped wizard skills are unavailable.', `Current directory: ${result.directoryPath}.`, `Active wizard skills: ${renderedSkillNames}${renderedCountSuffix}.`, `Counts: ${catalog.assignmentCounts.global} global, ${catalog.assignmentCounts.project} project, ${catalog.assignmentCounts.user} user, ${catalog.assignmentCounts.other} other.`, 'Wizard-listed skills are backend-published, not native OpenCode skills.', 'Use native skill tooling only for names in native available_skills.', 'When a wizard skill matches by whenToUse, fetch its current body with opencode_wizard_published_skills_fetch before using it.', 'Use `skills` for multiple wizard skill identifiers; use `skill` for one.', 'If native skill loading cannot find a wizard-listed skill, fetch it as a wizard skill instead.', 'Fetched wizard bodies are authoritative over local seed/native sources.', globalSkills.length > 0 ? `Global skills:\n${globalSkills.join('\n')}` : 'Global skills: none.', projectSkills.length > 0 ? `Project skills:\n${projectSkills.join('\n')}` : 'Project skills: none.', userSkills.length > 0 ? `User skills:\n${userSkills.join('\n')}` : 'User skills: none.', detailBlock].filter(Boolean).join(' ');
863
864
  };
864
865
  const toWorkspaceResolutionOutput = resolution => ({
865
866
  requestedDirectory: resolution.requestedDirectory,
@@ -1216,194 +1217,6 @@ const toCallbackServerStartError = error => {
1216
1217
  }
1217
1218
  return new Error('OAuth login cannot start because localhost:24953 is already in use. Another OpenCode login is likely in progress; finish it or close the other instance, then retry.');
1218
1219
  };
1219
- const escapeHtml = value => {
1220
- return value.replace(/[&<>'"]/g, character => {
1221
- const replacements = {
1222
- '&': '&amp;',
1223
- '<': '&lt;',
1224
- '>': '&gt;',
1225
- "'": '&#39;',
1226
- '"': '&quot;'
1227
- };
1228
- return replacements[character] ?? character;
1229
- });
1230
- };
1231
- const sendHtmlResponse = (response, statusCode, title, message) => {
1232
- const escapedTitle = escapeHtml(title);
1233
- const escapedMessage = escapeHtml(message);
1234
- const isSuccess = statusCode >= 200 && statusCode < 300;
1235
- const pageState = isSuccess ? 'success' : statusCode === 404 ? 'not-found' : 'error';
1236
- const cardTitle = isSuccess ? 'Authorization successful' : statusCode === 404 ? 'Callback not found' : 'Authorization failed';
1237
- const escapedCardTitle = escapeHtml(cardTitle);
1238
- const eyebrow = isSuccess ? 'Authorization complete' : statusCode === 404 ? 'Callback route not found' : 'Authorization needs attention';
1239
- const actionText = isSuccess ? 'This window will close automatically in a moment. You can also close it now and return to OpenCode.' : 'You can close this window and return to OpenCode to try again.';
1240
- const autoCloseScript = isSuccess ? `<script>
1241
- window.setTimeout(() => window.close(), 2000);
1242
- </script>` : '';
1243
- const stateIcon = isSuccess ? '<path d="M7 12.5l3.1 3.1L17.5 8" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>' : statusCode === 404 ? '<path d="M10.5 17a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Z" stroke="currentColor" stroke-width="2.2"/><path d="m15.5 15.5 4 4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>' : '<path d="M12 7v6" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/><path d="M12 17.2v.1" stroke="currentColor" stroke-width="3.2" stroke-linecap="round"/>';
1244
- response.writeHead(statusCode, {
1245
- 'content-type': 'text/html; charset=utf-8',
1246
- 'cache-control': 'no-store'
1247
- });
1248
- response.end(`<!doctype html>
1249
- <html lang="en">
1250
- <head>
1251
- <meta charset="utf-8">
1252
- <meta name="viewport" content="width=device-width, initial-scale=1">
1253
- <meta name="color-scheme" content="light dark">
1254
- <title>${escapedTitle}</title>
1255
- <style>
1256
- :root {
1257
- color-scheme: light dark;
1258
- --page-bg: #f2efe7;
1259
- --page-ink: #211d18;
1260
- --muted: #6c6258;
1261
- --panel: rgba(255, 252, 245, 0.82);
1262
- --panel-border: rgba(78, 66, 52, 0.16);
1263
- --success: #167848;
1264
- --error: #ba3329;
1265
- --not-found: #986614;
1266
- --glow: rgba(22, 120, 72, 0.18);
1267
- }
1268
-
1269
- @media (prefers-color-scheme: dark) {
1270
- :root {
1271
- --page-bg: #12100d;
1272
- --page-ink: #f7efe2;
1273
- --muted: #b8aa98;
1274
- --panel: rgba(30, 26, 22, 0.78);
1275
- --panel-border: rgba(255, 244, 224, 0.14);
1276
- --success: #71e0a6;
1277
- --error: #ff897e;
1278
- --not-found: #f7c96f;
1279
- --glow: rgba(113, 224, 166, 0.2);
1280
- }
1281
- }
1282
-
1283
- * {
1284
- box-sizing: border-box;
1285
- }
1286
-
1287
- body {
1288
- min-height: 100vh;
1289
- margin: 0;
1290
- display: grid;
1291
- place-items: center;
1292
- padding: 24px;
1293
- overflow: hidden;
1294
- background:
1295
- radial-gradient(circle at 18% 18%, var(--glow), transparent 34rem),
1296
- radial-gradient(circle at 82% 12%, rgba(209, 142, 72, 0.18), transparent 30rem),
1297
- linear-gradient(135deg, var(--page-bg), color-mix(in srgb, var(--page-bg) 76%, #000 24%));
1298
- color: var(--page-ink);
1299
- font-family: ui-rounded, "SF Pro Rounded", "Segoe UI", system-ui, sans-serif;
1300
- }
1301
-
1302
- body::before {
1303
- content: "";
1304
- position: fixed;
1305
- inset: -20%;
1306
- pointer-events: none;
1307
- background-image:
1308
- linear-gradient(rgba(128, 104, 74, 0.08) 1px, transparent 1px),
1309
- linear-gradient(90deg, rgba(128, 104, 74, 0.08) 1px, transparent 1px);
1310
- background-size: 42px 42px;
1311
- mask-image: radial-gradient(circle at center, black, transparent 68%);
1312
- }
1313
-
1314
- main {
1315
- position: relative;
1316
- width: min(100%, 560px);
1317
- padding: clamp(28px, 7vw, 56px);
1318
- border: 1px solid var(--panel-border);
1319
- border-radius: 32px;
1320
- background: var(--panel);
1321
- box-shadow: 0 24px 90px rgba(0, 0, 0, 0.24);
1322
- text-align: center;
1323
- backdrop-filter: blur(18px) saturate(1.2);
1324
- }
1325
-
1326
- .mark {
1327
- width: 72px;
1328
- height: 72px;
1329
- margin: 0 auto 24px;
1330
- display: grid;
1331
- place-items: center;
1332
- border-radius: 24px;
1333
- color: var(--state-color);
1334
- background: color-mix(in srgb, var(--state-color) 16%, transparent);
1335
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--state-color) 28%, transparent);
1336
- }
1337
-
1338
- [data-state="success"] { --state-color: var(--success); }
1339
- [data-state="error"] { --state-color: var(--error); }
1340
- [data-state="not-found"] { --state-color: var(--not-found); }
1341
-
1342
- .eyebrow {
1343
- margin: 0 0 10px;
1344
- color: var(--state-color);
1345
- font-size: 0.78rem;
1346
- font-weight: 800;
1347
- letter-spacing: 0.14em;
1348
- text-transform: uppercase;
1349
- }
1350
-
1351
- h1 {
1352
- margin: 0;
1353
- font-size: clamp(2rem, 7vw, 3.35rem);
1354
- line-height: 0.95;
1355
- letter-spacing: -0.06em;
1356
- }
1357
-
1358
- .message {
1359
- margin: 22px auto 0;
1360
- max-width: 38rem;
1361
- color: var(--muted);
1362
- font-size: clamp(1rem, 2.5vw, 1.1rem);
1363
- line-height: 1.65;
1364
- }
1365
-
1366
- .next-step {
1367
- margin: 26px 0 0;
1368
- padding: 14px 18px;
1369
- border-radius: 999px;
1370
- background: color-mix(in srgb, var(--state-color) 12%, transparent);
1371
- color: var(--page-ink);
1372
- font-size: 0.94rem;
1373
- line-height: 1.5;
1374
- }
1375
-
1376
- @media (max-width: 520px) {
1377
- body {
1378
- padding: 16px;
1379
- }
1380
-
1381
- main {
1382
- border-radius: 24px;
1383
- }
1384
-
1385
- .next-step {
1386
- border-radius: 18px;
1387
- }
1388
- }
1389
- </style>
1390
- </head>
1391
- <body>
1392
- <main data-state="${pageState}" aria-labelledby="callback-title">
1393
- <div class="mark" aria-hidden="true">
1394
- <svg width="34" height="34" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1395
- ${stateIcon}
1396
- </svg>
1397
- </div>
1398
- <p class="eyebrow">${eyebrow}</p>
1399
- <h1 id="callback-title">${escapedCardTitle}</h1>
1400
- <p class="message">${escapedMessage}</p>
1401
- <p class="next-step">${actionText}</p>
1402
- </main>
1403
- ${autoCloseScript}
1404
- </body>
1405
- </html>`);
1406
- };
1407
1220
  const startLocalCallbackServer = async ({
1408
1221
  expectedState,
1409
1222
  signal
@@ -1428,14 +1241,14 @@ const startLocalCallbackServer = async ({
1428
1241
  const server = http.createServer((request, response) => {
1429
1242
  const requestUrl = new URL(request.url ?? '/', OIDC_CALLBACK_ORIGIN);
1430
1243
  if (requestUrl.pathname !== OIDC_CALLBACK_PATH) {
1431
- sendHtmlResponse(response, 404, 'opencode-wizard plugin login', 'Unknown callback path.');
1244
+ sendOAuthCallbackHtmlResponse(response, 404, 'opencode-wizard plugin login', 'Unknown callback path.');
1432
1245
  return;
1433
1246
  }
1434
1247
  const error = requestUrl.searchParams.get('error');
1435
1248
  const errorDescription = requestUrl.searchParams.get('error_description');
1436
1249
  if (error) {
1437
1250
  const message = errorDescription ?? error;
1438
- sendHtmlResponse(response, 400, 'opencode-wizard plugin login failed', message);
1251
+ sendOAuthCallbackHtmlResponse(response, 400, 'opencode-wizard plugin login failed', message);
1439
1252
  finalize({
1440
1253
  status: 'error',
1441
1254
  message
@@ -1445,7 +1258,7 @@ const startLocalCallbackServer = async ({
1445
1258
  const state = requestUrl.searchParams.get('state');
1446
1259
  const code = requestUrl.searchParams.get('code');
1447
1260
  if (!state || state !== expectedState) {
1448
- sendHtmlResponse(response, 400, 'opencode-wizard plugin login failed', 'OAuth state did not match the login request.');
1261
+ sendOAuthCallbackHtmlResponse(response, 400, 'opencode-wizard plugin login failed', 'OAuth state did not match the login request.');
1449
1262
  finalize({
1450
1263
  status: 'error',
1451
1264
  message: 'OAuth state did not match the login request.'
@@ -1453,14 +1266,14 @@ const startLocalCallbackServer = async ({
1453
1266
  return;
1454
1267
  }
1455
1268
  if (!code) {
1456
- sendHtmlResponse(response, 400, 'opencode-wizard plugin login failed', 'OAuth callback did not include an authorization code.');
1269
+ sendOAuthCallbackHtmlResponse(response, 400, 'opencode-wizard plugin login failed', 'OAuth callback did not include an authorization code.');
1457
1270
  finalize({
1458
1271
  status: 'error',
1459
1272
  message: 'OAuth callback did not include an authorization code.'
1460
1273
  });
1461
1274
  return;
1462
1275
  }
1463
- sendHtmlResponse(response, 200, 'opencode-wizard plugin callback received', 'Callback received. OpenCode is finalizing the backend session now.');
1276
+ sendOAuthCallbackHtmlResponse(response, 200, 'opencode-wizard plugin callback received', 'Callback received. OpenCode is finalizing the backend session now.');
1464
1277
  finalize({
1465
1278
  status: 'success',
1466
1279
  code,