skilld 1.7.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/dist/_chunks/add.mjs +66 -0
  2. package/dist/_chunks/add.mjs.map +1 -0
  3. package/dist/_chunks/agent-prompt.mjs +88 -0
  4. package/dist/_chunks/agent-prompt.mjs.map +1 -0
  5. package/dist/_chunks/agent.mjs +737 -619
  6. package/dist/_chunks/agent.mjs.map +1 -1
  7. package/dist/_chunks/args.mjs +42 -0
  8. package/dist/_chunks/args.mjs.map +1 -0
  9. package/dist/_chunks/assemble.mjs +11 -8
  10. package/dist/_chunks/assemble.mjs.map +1 -1
  11. package/dist/_chunks/author.mjs +77 -131
  12. package/dist/_chunks/author.mjs.map +1 -1
  13. package/dist/_chunks/cache.mjs +320 -54
  14. package/dist/_chunks/cache.mjs.map +1 -1
  15. package/dist/_chunks/cache2.mjs +7 -6
  16. package/dist/_chunks/cache2.mjs.map +1 -1
  17. package/dist/_chunks/client.mjs +117 -0
  18. package/dist/_chunks/client.mjs.map +1 -0
  19. package/dist/_chunks/core.mjs +7 -4
  20. package/dist/_chunks/detect.mjs +54 -44
  21. package/dist/_chunks/detect.mjs.map +1 -1
  22. package/dist/_chunks/eject.mjs +69 -0
  23. package/dist/_chunks/eject.mjs.map +1 -0
  24. package/dist/_chunks/embedding-cache2.mjs +2 -2
  25. package/dist/_chunks/env.mjs +19 -0
  26. package/dist/_chunks/env.mjs.map +1 -0
  27. package/dist/_chunks/install-many.mjs +376 -0
  28. package/dist/_chunks/install-many.mjs.map +1 -0
  29. package/dist/_chunks/install.mjs +86 -371
  30. package/dist/_chunks/install.mjs.map +1 -1
  31. package/dist/_chunks/intro.mjs +63 -0
  32. package/dist/_chunks/intro.mjs.map +1 -0
  33. package/dist/_chunks/list.mjs +2 -2
  34. package/dist/_chunks/list.mjs.map +1 -1
  35. package/dist/_chunks/lockfile.mjs +31 -7
  36. package/dist/_chunks/lockfile.mjs.map +1 -1
  37. package/dist/_chunks/login.mjs +233 -0
  38. package/dist/_chunks/login.mjs.map +1 -0
  39. package/dist/_chunks/logout.mjs +27 -0
  40. package/dist/_chunks/logout.mjs.map +1 -0
  41. package/dist/_chunks/map.mjs +11 -0
  42. package/dist/_chunks/map.mjs.map +1 -0
  43. package/dist/_chunks/markdown.mjs +79 -54
  44. package/dist/_chunks/markdown.mjs.map +1 -1
  45. package/dist/_chunks/menu.mjs +33 -0
  46. package/dist/_chunks/menu.mjs.map +1 -0
  47. package/dist/_chunks/model-picker.mjs +61 -0
  48. package/dist/_chunks/model-picker.mjs.map +1 -0
  49. package/dist/_chunks/monorepo.mjs +73 -0
  50. package/dist/_chunks/monorepo.mjs.map +1 -0
  51. package/dist/_chunks/package-json.mjs.map +1 -1
  52. package/dist/_chunks/paths.mjs +47 -0
  53. package/dist/_chunks/paths.mjs.map +1 -0
  54. package/dist/_chunks/pipeline.mjs +985 -0
  55. package/dist/_chunks/pipeline.mjs.map +1 -0
  56. package/dist/_chunks/pool2.mjs +2 -2
  57. package/dist/_chunks/portable.mjs +151 -0
  58. package/dist/_chunks/portable.mjs.map +1 -0
  59. package/dist/_chunks/prepare-hook.mjs +2 -0
  60. package/dist/_chunks/prepare-hook2.mjs +61 -0
  61. package/dist/_chunks/prepare-hook2.mjs.map +1 -0
  62. package/dist/_chunks/prepare.mjs +47 -3
  63. package/dist/_chunks/prepare.mjs.map +1 -1
  64. package/dist/_chunks/prepare2.mjs +9 -8
  65. package/dist/_chunks/prepare2.mjs.map +1 -1
  66. package/dist/_chunks/prompts.mjs +784 -26
  67. package/dist/_chunks/prompts.mjs.map +1 -1
  68. package/dist/_chunks/pull.mjs +219 -0
  69. package/dist/_chunks/pull.mjs.map +1 -0
  70. package/dist/_chunks/regex.mjs +19 -0
  71. package/dist/_chunks/regex.mjs.map +1 -0
  72. package/dist/_chunks/retriv.mjs +2 -171
  73. package/dist/_chunks/retriv2.mjs +159 -0
  74. package/dist/_chunks/retriv2.mjs.map +1 -0
  75. package/dist/_chunks/sanitize.mjs +12 -9
  76. package/dist/_chunks/sanitize.mjs.map +1 -1
  77. package/dist/_chunks/search-helpers.mjs +9 -8
  78. package/dist/_chunks/search-helpers.mjs.map +1 -1
  79. package/dist/_chunks/search-interactive.mjs +23 -20
  80. package/dist/_chunks/search-interactive.mjs.map +1 -1
  81. package/dist/_chunks/search.mjs +3 -4
  82. package/dist/_chunks/search.mjs.map +1 -1
  83. package/dist/_chunks/{sources.mjs → semver.mjs} +1128 -838
  84. package/dist/_chunks/semver.mjs.map +1 -0
  85. package/dist/_chunks/skill-installer.mjs +2 -0
  86. package/dist/_chunks/skill-installer2.mjs +154 -0
  87. package/dist/_chunks/skill-installer2.mjs.map +1 -0
  88. package/dist/_chunks/skills.mjs +12 -12
  89. package/dist/_chunks/skills.mjs.map +1 -1
  90. package/dist/_chunks/store.mjs +107 -0
  91. package/dist/_chunks/store.mjs.map +1 -0
  92. package/dist/_chunks/sync.mjs +761 -1349
  93. package/dist/_chunks/sync.mjs.map +1 -1
  94. package/dist/_chunks/sync2.mjs +2 -3
  95. package/dist/_chunks/telemetry.mjs +26 -0
  96. package/dist/_chunks/telemetry.mjs.map +1 -0
  97. package/dist/_chunks/uninstall.mjs +15 -13
  98. package/dist/_chunks/uninstall.mjs.map +1 -1
  99. package/dist/_chunks/update.mjs +171 -0
  100. package/dist/_chunks/update.mjs.map +1 -0
  101. package/dist/_chunks/upload.mjs +4 -4
  102. package/dist/_chunks/validate.mjs +1 -1
  103. package/dist/_chunks/version.mjs +16 -27
  104. package/dist/_chunks/version.mjs.map +1 -1
  105. package/dist/_chunks/whoami.mjs +21 -0
  106. package/dist/_chunks/whoami.mjs.map +1 -0
  107. package/dist/_chunks/wizard.mjs +2 -190
  108. package/dist/_chunks/wizard2.mjs +200 -0
  109. package/dist/_chunks/wizard2.mjs.map +1 -0
  110. package/dist/cli.mjs +77 -59
  111. package/dist/cli.mjs.map +1 -1
  112. package/dist/prepare.mjs +5 -4
  113. package/dist/prepare.mjs.map +1 -1
  114. package/dist/retriv/worker.d.mts +5 -1
  115. package/dist/retriv/worker.d.mts.map +1 -1
  116. package/dist/retriv/worker.mjs +1 -1
  117. package/package.json +20 -29
  118. package/dist/_chunks/author-group.mjs +0 -17
  119. package/dist/_chunks/author-group.mjs.map +0 -1
  120. package/dist/_chunks/cli-helpers.mjs +0 -335
  121. package/dist/_chunks/cli-helpers.mjs.map +0 -1
  122. package/dist/_chunks/cli-helpers2.mjs +0 -2
  123. package/dist/_chunks/config.mjs +0 -122
  124. package/dist/_chunks/config.mjs.map +0 -1
  125. package/dist/_chunks/index.d.mts +0 -151
  126. package/dist/_chunks/index.d.mts.map +0 -1
  127. package/dist/_chunks/index2.d.mts +0 -44
  128. package/dist/_chunks/index2.d.mts.map +0 -1
  129. package/dist/_chunks/index3.d.mts +0 -589
  130. package/dist/_chunks/index3.d.mts.map +0 -1
  131. package/dist/_chunks/prefix.mjs +0 -108
  132. package/dist/_chunks/prefix.mjs.map +0 -1
  133. package/dist/_chunks/retriv.mjs.map +0 -1
  134. package/dist/_chunks/setup.mjs +0 -17
  135. package/dist/_chunks/setup.mjs.map +0 -1
  136. package/dist/_chunks/shared.mjs +0 -503
  137. package/dist/_chunks/shared.mjs.map +0 -1
  138. package/dist/_chunks/skill.mjs +0 -329
  139. package/dist/_chunks/skill.mjs.map +0 -1
  140. package/dist/_chunks/sources.mjs.map +0 -1
  141. package/dist/_chunks/sync-registry.mjs +0 -59
  142. package/dist/_chunks/sync-registry.mjs.map +0 -1
  143. package/dist/_chunks/sync-shared.mjs +0 -2
  144. package/dist/_chunks/sync-shared2.mjs +0 -1020
  145. package/dist/_chunks/sync-shared2.mjs.map +0 -1
  146. package/dist/_chunks/types.d.mts +0 -88
  147. package/dist/_chunks/types.d.mts.map +0 -1
  148. package/dist/_chunks/wizard.mjs.map +0 -1
  149. package/dist/agent/index.d.mts +0 -346
  150. package/dist/agent/index.d.mts.map +0 -1
  151. package/dist/agent/index.mjs +0 -5
  152. package/dist/cache/index.d.mts +0 -2
  153. package/dist/cache/index.mjs +0 -4
  154. package/dist/index.d.mts +0 -5
  155. package/dist/index.mjs +0 -5
  156. package/dist/retriv/index.d.mts +0 -3
  157. package/dist/retriv/index.mjs +0 -2
  158. package/dist/sources/index.d.mts +0 -2
  159. package/dist/sources/index.mjs +0 -3
  160. package/dist/types.d.mts +0 -4
  161. package/dist/types.mjs +0 -1
@@ -0,0 +1,233 @@
1
+ import { t as version } from "./version.mjs";
2
+ import { i as saveSession } from "./store.mjs";
3
+ import { i as getRegistryBase } from "./client.mjs";
4
+ import { t as track } from "./telemetry.mjs";
5
+ import { styleText } from "node:util";
6
+ import * as p from "@clack/prompts";
7
+ import { defineCommand } from "citty";
8
+ import { spawn } from "node:child_process";
9
+ import { createHash, randomBytes } from "node:crypto";
10
+ import { ofetch } from "ofetch";
11
+ import { createServer } from "node:http";
12
+ async function runDeviceFlow(opts) {
13
+ const start = await ofetch(`${opts.registryBase}/cli/device/start`, {
14
+ method: "POST",
15
+ body: {
16
+ cli_version: opts.cliVersion,
17
+ machine_hint: opts.machineHint
18
+ }
19
+ });
20
+ opts.onUserCode({
21
+ userCode: start.user_code,
22
+ verificationUri: start.verification_uri
23
+ });
24
+ const deadline = Date.now() + start.expires_in * 1e3;
25
+ const interval = opts.intervalMs ?? start.interval * 1e3;
26
+ while (Date.now() < deadline) {
27
+ await new Promise((resolve) => setTimeout(resolve, interval));
28
+ const poll = await ofetch(`${opts.registryBase}/cli/device/poll`, {
29
+ method: "POST",
30
+ body: { device_code: start.device_code }
31
+ }).catch(() => null);
32
+ if (!poll || poll.status === "pending") continue;
33
+ if (poll.status === "expired") throw new Error("Device code expired before authorization");
34
+ if (poll.status === "denied") throw new Error("Device authorization denied");
35
+ if (poll.status === "authorized" && poll.tokens) return poll.tokens;
36
+ }
37
+ throw new Error("Device authorization timed out");
38
+ }
39
+ function isGhaOidcAvailable() {
40
+ return !!(process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && process.env.ACTIONS_ID_TOKEN_REQUEST_URL);
41
+ }
42
+ async function runOidcExchange(opts) {
43
+ const token = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
44
+ const url = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
45
+ if (!token || !url) throw new Error("Not running in GitHub Actions with id-token: write permission");
46
+ const audience = opts.audience ?? "skilld.dev";
47
+ const idToken = await ofetch(`${url}&audience=${encodeURIComponent(audience)}`, { headers: { Authorization: `Bearer ${token}` } });
48
+ return ofetch(`${opts.registryBase}/cli/oidc/exchange`, {
49
+ method: "POST",
50
+ body: { id_token: idToken.value }
51
+ });
52
+ }
53
+ const SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>skilld — signed in</title>
54
+ <body style="font-family: ui-sans-serif, system-ui; padding: 4rem; text-align: center">
55
+ <h1>Signed in to skilld</h1><p>You can close this tab and return to the CLI.</p>`;
56
+ function ERROR_HTML(msg) {
57
+ return `<!doctype html><meta charset="utf-8"><title>skilld — error</title>
58
+ <body style="font-family: ui-sans-serif, system-ui; padding: 4rem; text-align: center; color: #b00">
59
+ <h1>Sign-in failed</h1><p>${msg}</p>`;
60
+ }
61
+ const PLUS_RE = /\+/g;
62
+ const SLASH_RE = /\//g;
63
+ const EQ_RE = /=+$/;
64
+ const TRAILING_SLASH_RE = /\/$/;
65
+ const TRAILING_API_RE = /\/api$/;
66
+ function base64url(buf) {
67
+ return buf.toString("base64").replace(PLUS_RE, "-").replace(SLASH_RE, "_").replace(EQ_RE, "");
68
+ }
69
+ function generateVerifier() {
70
+ return base64url(randomBytes(32));
71
+ }
72
+ function challengeFromVerifier(verifier) {
73
+ return base64url(createHash("sha256").update(verifier).digest());
74
+ }
75
+ async function runPkceFlow(opts) {
76
+ const verifier = generateVerifier();
77
+ const challenge = challengeFromVerifier(verifier);
78
+ const state = base64url(randomBytes(16));
79
+ const { port, server, gotCode } = await bindLoopback(state);
80
+ const verificationUrl = new URL(`${opts.registryBase.replace(TRAILING_SLASH_RE, "").replace(TRAILING_API_RE, "")}/cli/authorize`);
81
+ verificationUrl.searchParams.set("challenge", challenge);
82
+ verificationUrl.searchParams.set("port", String(port));
83
+ verificationUrl.searchParams.set("state", state);
84
+ verificationUrl.searchParams.set("v", opts.cliVersion);
85
+ await (opts.openBrowser ?? defaultOpenBrowser)(verificationUrl.toString());
86
+ try {
87
+ const code = await Promise.race([gotCode, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("PKCE flow timed out")), opts.timeoutMs ?? 5 * 6e4))]);
88
+ return await ofetch(`${opts.registryBase}/cli/oauth/token`, {
89
+ method: "POST",
90
+ body: {
91
+ code,
92
+ code_verifier: verifier,
93
+ redirect_uri: `http://127.0.0.1:${port}/`
94
+ }
95
+ });
96
+ } finally {
97
+ server.close();
98
+ }
99
+ }
100
+ async function bindLoopback(expectedState) {
101
+ let resolveCode;
102
+ let rejectCode;
103
+ const gotCode = new Promise((res, rej) => {
104
+ resolveCode = res;
105
+ rejectCode = rej;
106
+ });
107
+ const handler = (req, res) => {
108
+ const url = new URL(req.url ?? "/", "http://localhost");
109
+ const code = url.searchParams.get("code");
110
+ const state = url.searchParams.get("state");
111
+ if (!code || state !== expectedState) {
112
+ res.writeHead(400, { "content-type": "text/html" }).end(ERROR_HTML("Missing or invalid state parameter."));
113
+ rejectCode(/* @__PURE__ */ new Error("PKCE callback missing code or state mismatch"));
114
+ return;
115
+ }
116
+ res.writeHead(200, { "content-type": "text/html" }).end(SUCCESS_HTML);
117
+ resolveCode(code);
118
+ };
119
+ const v4 = createServer(handler);
120
+ const v6 = createServer(handler);
121
+ await new Promise((resolve, reject) => {
122
+ v4.once("error", reject).listen(0, "127.0.0.1", () => resolve());
123
+ });
124
+ const port = v4.address().port;
125
+ await new Promise((resolve) => {
126
+ v6.once("error", () => resolve()).listen(port, "::1", () => resolve());
127
+ });
128
+ const close = () => {
129
+ v4.closeAllConnections();
130
+ v6.closeAllConnections();
131
+ v4.close();
132
+ v6.close();
133
+ };
134
+ return {
135
+ port,
136
+ server: { close },
137
+ gotCode
138
+ };
139
+ }
140
+ function defaultOpenBrowser(url) {
141
+ spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open", [url], {
142
+ stdio: "ignore",
143
+ detached: true
144
+ }).unref();
145
+ }
146
+ function shouldUseDevice(force) {
147
+ if (force) return true;
148
+ if (process.env.BROWSER) return false;
149
+ if (process.platform === "darwin" || process.platform === "win32") return false;
150
+ return !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
151
+ }
152
+ const loginCommandDef = defineCommand({
153
+ meta: {
154
+ name: "login",
155
+ description: "Authenticate with skilld.dev"
156
+ },
157
+ args: { device: {
158
+ type: "boolean",
159
+ description: "Use RFC 8628 device flow"
160
+ } },
161
+ async run({ args }) {
162
+ const registryBase = getRegistryBase();
163
+ if (isGhaOidcAvailable()) {
164
+ const spin = p.spinner();
165
+ spin.start("Exchanging GitHub Actions OIDC token");
166
+ const tokens = await runOidcExchange({ registryBase });
167
+ spin.stop(`Authenticated as @${tokens.login} (oidc)`);
168
+ await saveSession({
169
+ login: tokens.login,
170
+ accessToken: tokens.accessToken,
171
+ expiresAt: tokens.expiresAt,
172
+ tokens: { accessToken: tokens.accessToken }
173
+ });
174
+ track({
175
+ event: "auth-flow",
176
+ surface: "cli:auth",
177
+ flow: "oidc"
178
+ });
179
+ process.exit(0);
180
+ }
181
+ if (shouldUseDevice(!!args.device)) {
182
+ const tokens = await runDeviceFlow({
183
+ registryBase,
184
+ cliVersion: version,
185
+ onUserCode: ({ userCode, verificationUri }) => {
186
+ p.log.info(`Visit ${styleText("cyan", verificationUri)} and enter ${styleText("bold", userCode)}`);
187
+ }
188
+ });
189
+ await saveSession({
190
+ login: tokens.login,
191
+ accessToken: tokens.accessToken,
192
+ refreshToken: tokens.refreshToken,
193
+ expiresAt: tokens.expiresAt,
194
+ tokens: {
195
+ accessToken: tokens.accessToken,
196
+ refreshToken: tokens.refreshToken
197
+ }
198
+ });
199
+ track({
200
+ event: "auth-flow",
201
+ surface: "cli:auth",
202
+ flow: "device"
203
+ });
204
+ p.log.success(`Logged in as @${tokens.login}`);
205
+ process.exit(0);
206
+ }
207
+ p.log.info("Opening browser to authenticate…");
208
+ const tokens = await runPkceFlow({
209
+ registryBase,
210
+ cliVersion: version
211
+ });
212
+ await saveSession({
213
+ login: tokens.login,
214
+ accessToken: tokens.accessToken,
215
+ refreshToken: tokens.refreshToken,
216
+ expiresAt: tokens.expiresAt,
217
+ tokens: {
218
+ accessToken: tokens.accessToken,
219
+ refreshToken: tokens.refreshToken
220
+ }
221
+ });
222
+ track({
223
+ event: "auth-flow",
224
+ surface: "cli:auth",
225
+ flow: "pkce"
226
+ });
227
+ p.log.success(`Logged in as @${tokens.login}`);
228
+ process.exit(0);
229
+ }
230
+ });
231
+ export { loginCommandDef };
232
+
233
+ //# sourceMappingURL=login.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.mjs","names":[],"sources":["../../src/auth/device-flow.ts","../../src/auth/oidc.ts","../../src/auth/pkce-flow.ts","../../src/commands/login.ts"],"sourcesContent":["/**\n * RFC 8628 device flow. Used when --device is passed, no browser is detected,\n * or PKCE bind fails.\n */\n\nimport type { DevicePollResponse, DeviceStartResponse, TokenResponse } from 'skilld-protocol/wire'\nimport { ofetch } from 'ofetch'\n\nexport type { DeviceStartResponse }\n\nexport interface DeviceFlowOptions {\n registryBase: string\n cliVersion: string\n machineHint?: string\n /** Hook called once the user_code is known so the CLI can prompt the user. */\n onUserCode: (info: { userCode: string, verificationUri: string }) => void\n /** Override polling interval for tests. */\n intervalMs?: number\n}\n\nexport async function runDeviceFlow(opts: DeviceFlowOptions): Promise<TokenResponse> {\n const start = await ofetch<DeviceStartResponse>(`${opts.registryBase}/cli/device/start`, {\n method: 'POST',\n body: { cli_version: opts.cliVersion, machine_hint: opts.machineHint },\n })\n\n opts.onUserCode({ userCode: start.user_code, verificationUri: start.verification_uri })\n\n const deadline = Date.now() + start.expires_in * 1000\n const interval = opts.intervalMs ?? start.interval * 1000\n\n while (Date.now() < deadline) {\n await new Promise(resolve => setTimeout(resolve, interval))\n const poll = await ofetch<DevicePollResponse>(`${opts.registryBase}/cli/device/poll`, {\n method: 'POST',\n body: { device_code: start.device_code },\n }).catch(() => null)\n\n if (!poll || poll.status === 'pending')\n continue\n if (poll.status === 'expired')\n throw new Error('Device code expired before authorization')\n if (poll.status === 'denied')\n throw new Error('Device authorization denied')\n if (poll.status === 'authorized' && poll.tokens)\n return poll.tokens\n }\n\n throw new Error('Device authorization timed out')\n}\n","/**\n * GitHub Actions OIDC exchange. Auto-detected via `ACTIONS_ID_TOKEN_REQUEST_TOKEN`;\n * fetches a short-lived JWT against `audience=skilld.dev` and trades it for a\n * session token. No browser, no prompt, no refresh.\n */\n\nimport type { TokenResponse } from './types.ts'\nimport { ofetch } from 'ofetch'\n\ninterface GhaOidcResponse {\n value: string\n count?: number\n}\n\nexport function isGhaOidcAvailable(): boolean {\n return !!(process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && process.env.ACTIONS_ID_TOKEN_REQUEST_URL)\n}\n\nexport async function runOidcExchange(opts: { registryBase: string, audience?: string }): Promise<TokenResponse> {\n const token = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN\n const url = process.env.ACTIONS_ID_TOKEN_REQUEST_URL\n if (!token || !url)\n throw new Error('Not running in GitHub Actions with id-token: write permission')\n\n const audience = opts.audience ?? 'skilld.dev'\n const idToken = await ofetch<GhaOidcResponse>(`${url}&audience=${encodeURIComponent(audience)}`, {\n headers: { Authorization: `Bearer ${token}` },\n })\n\n return ofetch<TokenResponse>(`${opts.registryBase}/cli/oidc/exchange`, {\n method: 'POST',\n body: { id_token: idToken.value },\n })\n}\n","/**\n * RFC 7636 PKCE loopback flow.\n *\n * Binds `127.0.0.1:<port>` and `[::1]:<port>` simultaneously so the browser\n * can hit either; opens the system browser to the verification URL; serves a\n * single GET callback that captures the auth code, then exchanges it for\n * tokens against `/api/cli/oauth/token`.\n */\n\nimport type { AddressInfo } from 'node:net'\nimport type { TokenResponse } from './types.ts'\nimport { spawn } from 'node:child_process'\nimport { createHash, randomBytes } from 'node:crypto'\nimport { createServer } from 'node:http'\nimport { ofetch } from 'ofetch'\n\nconst SUCCESS_HTML = `<!doctype html><meta charset=\"utf-8\"><title>skilld — signed in</title>\n<body style=\"font-family: ui-sans-serif, system-ui; padding: 4rem; text-align: center\">\n<h1>Signed in to skilld</h1><p>You can close this tab and return to the CLI.</p>`\n\nfunction ERROR_HTML(msg: string) {\n return `<!doctype html><meta charset=\"utf-8\"><title>skilld — error</title>\n<body style=\"font-family: ui-sans-serif, system-ui; padding: 4rem; text-align: center; color: #b00\">\n<h1>Sign-in failed</h1><p>${msg}</p>`\n}\n\nexport interface PkceFlowOptions {\n registryBase: string\n cliVersion: string\n openBrowser?: (url: string) => Promise<void> | void\n timeoutMs?: number\n}\n\nconst PLUS_RE = /\\+/g\nconst SLASH_RE = /\\//g\nconst EQ_RE = /=+$/\nconst TRAILING_SLASH_RE = /\\/$/\nconst TRAILING_API_RE = /\\/api$/\n\nfunction base64url(buf: Buffer): string {\n return buf.toString('base64').replace(PLUS_RE, '-').replace(SLASH_RE, '_').replace(EQ_RE, '')\n}\n\nfunction generateVerifier(): string {\n return base64url(randomBytes(32))\n}\n\nfunction challengeFromVerifier(verifier: string): string {\n return base64url(createHash('sha256').update(verifier).digest())\n}\n\nexport async function runPkceFlow(opts: PkceFlowOptions): Promise<TokenResponse> {\n const verifier = generateVerifier()\n const challenge = challengeFromVerifier(verifier)\n const state = base64url(randomBytes(16))\n\n const { port, server, gotCode } = await bindLoopback(state)\n const verificationUrl = new URL(`${opts.registryBase.replace(TRAILING_SLASH_RE, '').replace(TRAILING_API_RE, '')}/cli/authorize`)\n verificationUrl.searchParams.set('challenge', challenge)\n verificationUrl.searchParams.set('port', String(port))\n verificationUrl.searchParams.set('state', state)\n verificationUrl.searchParams.set('v', opts.cliVersion)\n\n await (opts.openBrowser ?? defaultOpenBrowser)(verificationUrl.toString())\n\n try {\n const code = await Promise.race([\n gotCode,\n new Promise<never>((_, reject) => setTimeout(() => reject(new Error('PKCE flow timed out')), opts.timeoutMs ?? 5 * 60_000)),\n ])\n\n return await ofetch<TokenResponse>(`${opts.registryBase}/cli/oauth/token`, {\n method: 'POST',\n body: { code, code_verifier: verifier, redirect_uri: `http://127.0.0.1:${port}/` },\n })\n }\n finally {\n server.close()\n }\n}\n\ninterface LoopbackBinding {\n port: number\n server: { close: () => void }\n gotCode: Promise<string>\n}\n\nasync function bindLoopback(expectedState: string): Promise<LoopbackBinding> {\n let resolveCode!: (code: string) => void\n let rejectCode!: (err: Error) => void\n const gotCode = new Promise<string>((res, rej) => {\n resolveCode = res\n rejectCode = rej\n })\n\n const handler = (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse): void => {\n const url = new URL(req.url ?? '/', 'http://localhost')\n const code = url.searchParams.get('code')\n const state = url.searchParams.get('state')\n if (!code || state !== expectedState) {\n res.writeHead(400, { 'content-type': 'text/html' }).end(ERROR_HTML('Missing or invalid state parameter.'))\n rejectCode(new Error('PKCE callback missing code or state mismatch'))\n return\n }\n res.writeHead(200, { 'content-type': 'text/html' }).end(SUCCESS_HTML)\n resolveCode(code)\n }\n\n const v4 = createServer(handler)\n const v6 = createServer(handler)\n\n await new Promise<void>((resolve, reject) => {\n v4.once('error', reject).listen(0, '127.0.0.1', () => resolve())\n })\n const port = (v4.address() as AddressInfo).port\n await new Promise<void>((resolve) => {\n v6.once('error', () => resolve()).listen(port, '::1', () => resolve())\n })\n\n const close = (): void => {\n // closeAllConnections() forces still-open keep-alive sockets shut so the\n // process can exit promptly after the browser hits the success page.\n v4.closeAllConnections()\n v6.closeAllConnections()\n v4.close()\n v6.close()\n }\n\n return {\n port,\n server: { close },\n gotCode,\n }\n}\n\nfunction defaultOpenBrowser(url: string): void {\n const cmd = process.platform === 'darwin'\n ? 'open'\n : process.platform === 'win32'\n ? 'start'\n : 'xdg-open'\n spawn(cmd, [url], { stdio: 'ignore', detached: true }).unref()\n}\n","/**\n * `skilld login` — authenticate with skilld.dev.\n *\n * Picks a flow based on env:\n * - `ACTIONS_ID_TOKEN_REQUEST_TOKEN` set → GHA OIDC exchange.\n * - `--device` or no `DISPLAY`/`BROWSER` env → RFC 8628 device flow.\n * - Otherwise → PKCE loopback.\n */\n\nimport { styleText } from 'node:util'\nimport * as p from '@clack/prompts'\nimport { defineCommand } from 'citty'\nimport { runDeviceFlow } from '../auth/device-flow.ts'\nimport { isGhaOidcAvailable, runOidcExchange } from '../auth/oidc.ts'\nimport { runPkceFlow } from '../auth/pkce-flow.ts'\nimport { saveSession } from '../auth/store.ts'\nimport { getRegistryBase } from '../registry/client.ts'\nimport { track } from '../telemetry.ts'\nimport { version } from '../version.ts'\n\nfunction shouldUseDevice(force: boolean): boolean {\n if (force)\n return true\n if (process.env.BROWSER)\n return false\n if (process.platform === 'darwin' || process.platform === 'win32')\n return false\n return !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY\n}\n\nexport const loginCommandDef = defineCommand({\n meta: { name: 'login', description: 'Authenticate with skilld.dev' },\n args: {\n device: { type: 'boolean', description: 'Use RFC 8628 device flow' },\n },\n async run({ args }) {\n const registryBase = getRegistryBase()\n\n if (isGhaOidcAvailable()) {\n const spin = p.spinner()\n spin.start('Exchanging GitHub Actions OIDC token')\n const tokens = await runOidcExchange({ registryBase })\n spin.stop(`Authenticated as @${tokens.login} (oidc)`)\n await saveSession({\n login: tokens.login,\n accessToken: tokens.accessToken,\n expiresAt: tokens.expiresAt,\n tokens: { accessToken: tokens.accessToken },\n })\n track({ event: 'auth-flow', surface: 'cli:auth', flow: 'oidc' })\n process.exit(0)\n }\n\n if (shouldUseDevice(!!args.device)) {\n const tokens = await runDeviceFlow({\n registryBase,\n cliVersion: version,\n onUserCode: ({ userCode, verificationUri }) => {\n p.log.info(`Visit ${styleText('cyan', verificationUri)} and enter ${styleText('bold', userCode)}`)\n },\n })\n await saveSession({\n login: tokens.login,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresAt: tokens.expiresAt,\n tokens: { accessToken: tokens.accessToken, refreshToken: tokens.refreshToken },\n })\n track({ event: 'auth-flow', surface: 'cli:auth', flow: 'device' })\n p.log.success(`Logged in as @${tokens.login}`)\n process.exit(0)\n }\n\n p.log.info('Opening browser to authenticate…')\n const tokens = await runPkceFlow({ registryBase, cliVersion: version })\n await saveSession({\n login: tokens.login,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresAt: tokens.expiresAt,\n tokens: { accessToken: tokens.accessToken, refreshToken: tokens.refreshToken },\n })\n track({ event: 'auth-flow', surface: 'cli:auth', flow: 'pkce' })\n p.log.success(`Logged in as @${tokens.login}`)\n // Node's global fetch keep-alive pool + telemetry fire-and-forget leave\n // sockets ref'd; force exit so we don't wait for the 4s idle timeout.\n process.exit(0)\n },\n})\n"],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { n as loadSession, t as clearSession } from "./store.mjs";
2
+ import { i as getRegistryBase } from "./client.mjs";
3
+ import * as p from "@clack/prompts";
4
+ import { defineCommand } from "citty";
5
+ import { ofetch } from "ofetch";
6
+ const logoutCommandDef = defineCommand({
7
+ meta: {
8
+ name: "logout",
9
+ description: "Sign out of skilld.dev"
10
+ },
11
+ async run() {
12
+ const session = await loadSession();
13
+ if (!session) {
14
+ p.log.info("Not logged in.");
15
+ return;
16
+ }
17
+ await ofetch(`${getRegistryBase()}/cli/logout`, {
18
+ method: "POST",
19
+ headers: { Authorization: `Bearer ${session.accessToken}` }
20
+ }).catch(() => {});
21
+ await clearSession();
22
+ p.log.success("Logged out.");
23
+ }
24
+ });
25
+ export { logoutCommandDef };
26
+
27
+ //# sourceMappingURL=logout.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logout.mjs","names":[],"sources":["../../src/commands/logout.ts"],"sourcesContent":["/**\n * `skilld logout` — revoke the active session server-side and clear local credentials.\n * Local state is cleared even if the server revoke fails.\n */\n\nimport * as p from '@clack/prompts'\nimport { defineCommand } from 'citty'\nimport { ofetch } from 'ofetch'\nimport { clearSession, loadSession } from '../auth/store.ts'\nimport { getRegistryBase } from '../registry/client.ts'\n\nexport const logoutCommandDef = defineCommand({\n meta: { name: 'logout', description: 'Sign out of skilld.dev' },\n async run() {\n const session = await loadSession()\n if (!session) {\n p.log.info('Not logged in.')\n return\n }\n\n await ofetch(`${getRegistryBase()}/cli/logout`, {\n method: 'POST',\n headers: { Authorization: `Bearer ${session.accessToken}` },\n }).catch(() => {})\n\n await clearSession()\n p.log.success('Logged out.')\n },\n})\n"],"mappings":";;;;;;;;EAWA,aAAa;EACX;OAAQ,MAAM;EAAU,MAAA,UAAa,MAAA,aAAA;EAA0B,IAAA,CAAA,SAAA;GAC/D,EAAA,IAAM,KAAM,iBAAA;GACV;;QAEI,OAAS,GAAA,iBAAiB,CAAA,cAAA;GAC5B,QAAA;;GAGF,CAAA,CAAA,YAAa,GAAG;QACd,cAAQ;IACR,IAAA,QAAW,cAAe;;EAG5B"}
@@ -0,0 +1,11 @@
1
+ function mapInsert(map, key, create) {
2
+ let val = map.get(key);
3
+ if (val === void 0) {
4
+ val = create();
5
+ map.set(key, val);
6
+ }
7
+ return val;
8
+ }
9
+ export { mapInsert as t };
10
+
11
+ //# sourceMappingURL=map.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map.mjs","names":[],"sources":["../../src/core/map.ts"],"sourcesContent":["/** Get-or-create for Maps. Polyfill for Map.getOrInsertComputed (not yet in Node.js). */\nexport function mapInsert<K, V>(map: Map<K, V>, key: K, create: () => V): V {\n let val = map.get(key)\n if (val === undefined) {\n val = create()\n map.set(key, val)\n }\n return val\n}\n"],"mappings":"AACA,SAAgB,UAAgB,KAAgB,KAAQ,QAAoB;CAC1E,IAAI,MAAM,IAAI,IAAI,IAAI;CACtB,IAAI,QAAQ,KAAA,GAAW;EACrB,MAAM,QAAQ;EACd,IAAI,IAAI,KAAK,IAAI;;CAEnB,OAAO"}
@@ -1,76 +1,101 @@
1
1
  import { n as yamlParseKV } from "./yaml.mjs";
2
- import { fromMarkdown } from "mdast-util-from-markdown";
3
- import { frontmatterFromMarkdown } from "mdast-util-frontmatter";
4
- import { toString } from "mdast-util-to-string";
5
- import { frontmatter } from "micromark-extension-frontmatter";
6
- import { visit } from "unist-util-visit";
7
- function parseMd(content) {
8
- const tree = fromMarkdown(content, {
9
- extensions: [frontmatter(["yaml"])],
10
- mdastExtensions: [frontmatterFromMarkdown(["yaml"])]
11
- });
12
- const fm = {};
13
- visit(tree, "yaml", (node) => {
14
- if (node.type === "yaml") for (const line of node.value.split("\n")) {
15
- const kv = yamlParseKV(line);
16
- if (kv) fm[kv[0]] = kv[1];
17
- }
18
- });
19
- return {
20
- tree,
21
- frontmatter: fm
22
- };
2
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
3
+ const HEADING_LINE_RE = /^(#{1,6})[ \t]+([^ \t\r\n][^\r\n]*)$/gm;
4
+ const ANCHOR_RE = /\s*\{#[^}]+\}\s*$/;
5
+ const BACKSLASH_PREFIX_RE = /^\\+\s*/;
6
+ const INLINE_CODE_RE = /`([^`]+)`/g;
7
+ const LINK_RE = /(?<!!)\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
8
+ const HEADING_START_RE = /^#{1,6}\s/;
9
+ const BLOCKQUOTE_START_RE = /^>\s?/;
10
+ const LIST_ITEM_START_RE = /^[-*+]\s/;
11
+ const ORDERED_LIST_ITEM_START_RE = /^\d+\.\s/;
12
+ const TABLE_ROW_START_RE = /^\|/;
13
+ const INDENTED_CODE_START_RE = /^ {4}/;
14
+ const FENCE_START_RE = /^\s*```/;
15
+ const MARKDOWN_LINK_RE = /\[([^\]]+)\]\([^)]+\)/g;
16
+ const MARKDOWN_FORMATTING_RE = /[`*_~]/g;
17
+ const FENCED_CODE_BLOCK_RE = /```[\s\S]*?```/g;
18
+ const INLINE_CODE_SPAN_RE = /`[^`\n]*`/g;
19
+ function stripFrontmatter(content) {
20
+ const m = content.match(FRONTMATTER_RE);
21
+ return m ? content.slice(m[0].length).trim() : content;
23
22
  }
24
23
  function parseFrontmatter(content) {
25
- return parseMd(content).frontmatter;
24
+ const m = content.match(FRONTMATTER_RE);
25
+ if (!m) return {};
26
+ const fm = {};
27
+ for (const line of m[1].split("\n")) {
28
+ const kv = yamlParseKV(line);
29
+ if (kv) fm[kv[0]] = kv[1];
30
+ }
31
+ return fm;
26
32
  }
27
- function stripHeadingAnchors(text) {
28
- return text.replace(/\s*\{#[^}]+\}\s*$/, "").trim();
33
+ function cleanHeadingText(raw) {
34
+ return raw.replace(ANCHOR_RE, "").replace(BACKSLASH_PREFIX_RE, "").replace(INLINE_CODE_RE, "$1").trim();
29
35
  }
30
36
  function extractTitle(content) {
31
- const { tree, frontmatter: fm } = parseMd(content);
37
+ const fm = parseFrontmatter(content);
32
38
  if (fm.title) return fm.title;
33
- let title = null;
34
- visit(tree, "heading", (node) => {
35
- if (node.depth === 1 && !title) {
36
- const text = stripHeadingAnchors(toString(node)).replace(/^\\+\s*/, "").trim();
37
- if (text.length > 0) title = text;
38
- }
39
- });
40
- return title;
39
+ const body = stripFrontmatter(content);
40
+ for (const m of body.matchAll(HEADING_LINE_RE)) if (m[1] === "#") {
41
+ const text = cleanHeadingText(m[2]);
42
+ if (text) return text;
43
+ }
44
+ return null;
45
+ }
46
+ function isBlockStarter(trimmed, raw) {
47
+ return HEADING_START_RE.test(trimmed) || BLOCKQUOTE_START_RE.test(trimmed) || LIST_ITEM_START_RE.test(trimmed) || ORDERED_LIST_ITEM_START_RE.test(trimmed) || TABLE_ROW_START_RE.test(trimmed) || trimmed.startsWith("<") || INDENTED_CODE_START_RE.test(raw);
41
48
  }
42
49
  function extractDescription(content) {
43
- const { tree } = parseMd(content);
44
- let desc = null;
45
- visit(tree, "paragraph", (node, _index, parent) => {
46
- if (desc || parent?.type !== "root") return;
47
- const text = toString(node).trim();
48
- if (text.length === 0) return;
49
- let clean = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[`*_~]/g, "");
50
+ const lines = stripFrontmatter(content).split("\n");
51
+ let inFence = false;
52
+ let inHtml = false;
53
+ for (let i = 0; i < lines.length; i++) {
54
+ const line = lines[i];
55
+ if (FENCE_START_RE.test(line)) {
56
+ inFence = !inFence;
57
+ continue;
58
+ }
59
+ if (inFence) continue;
60
+ const trimmed = line.trim();
61
+ if (inHtml) {
62
+ if (!trimmed) inHtml = false;
63
+ continue;
64
+ }
65
+ if (trimmed.startsWith("<")) {
66
+ inHtml = true;
67
+ continue;
68
+ }
69
+ if (!trimmed || isBlockStarter(trimmed, line)) continue;
70
+ let para = trimmed;
71
+ for (let j = i + 1; j < lines.length; j++) {
72
+ const next = lines[j];
73
+ const nextTrim = next.trim();
74
+ if (!nextTrim || isBlockStarter(nextTrim, next)) break;
75
+ para += ` ${nextTrim}`;
76
+ }
77
+ let clean = para.replace(MARKDOWN_LINK_RE, "$1").replace(MARKDOWN_FORMATTING_RE, "");
50
78
  if (clean.length > 150) clean = `${clean.slice(0, 147)}...`;
51
- desc = clean;
52
- });
53
- return desc;
79
+ return clean;
80
+ }
81
+ return null;
54
82
  }
55
83
  function extractLinks(content) {
56
- const { tree } = parseMd(content);
84
+ const sanitized = stripFrontmatter(content).replace(FENCED_CODE_BLOCK_RE, "").replace(INLINE_CODE_SPAN_RE, "");
57
85
  const links = [];
58
86
  const seen = /* @__PURE__ */ new Set();
59
- visit(tree, "link", (node) => {
60
- if (!seen.has(node.url)) {
61
- seen.add(node.url);
87
+ for (const m of sanitized.matchAll(LINK_RE)) {
88
+ const url = m[2];
89
+ if (!seen.has(url)) {
90
+ seen.add(url);
62
91
  links.push({
63
- title: toString(node),
64
- url: node.url
92
+ title: m[1],
93
+ url
65
94
  });
66
95
  }
67
- });
96
+ }
68
97
  return links;
69
98
  }
70
- function stripFrontmatter(content) {
71
- const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n/);
72
- return match ? content.slice(match[0].length).trim() : content;
73
- }
74
99
  export { stripFrontmatter as a, parseFrontmatter as i, extractLinks as n, extractTitle as r, extractDescription as t };
75
100
 
76
101
  //# sourceMappingURL=markdown.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.mjs","names":[],"sources":["../../src/core/markdown.ts"],"sourcesContent":["/**\n * AST-based markdown parsing using mdast/micromark.\n * Replaces scattered regex-based frontmatter/heading/link extraction.\n */\n\nimport type { Nodes, Root } from 'mdast'\nimport { fromMarkdown } from 'mdast-util-from-markdown'\nimport { frontmatterFromMarkdown } from 'mdast-util-frontmatter'\nimport { toString } from 'mdast-util-to-string'\nimport { frontmatter } from 'micromark-extension-frontmatter'\nimport { visit } from 'unist-util-visit'\nimport { yamlParseKV } from './yaml.ts'\n\nexport interface MdHeading {\n depth: number\n text: string\n}\n\nexport interface MdLink {\n title: string\n url: string\n}\n\nexport interface ParsedMd {\n tree: Root\n frontmatter: Record<string, string>\n}\n\n/** Parse markdown string to AST + frontmatter key-values */\nexport function parseMd(content: string): ParsedMd {\n const tree = fromMarkdown(content, {\n extensions: [frontmatter(['yaml'])],\n mdastExtensions: [frontmatterFromMarkdown(['yaml'])],\n })\n\n const fm: Record<string, string> = {}\n visit(tree, 'yaml', (node: Nodes) => {\n if (node.type === 'yaml') {\n for (const line of (node as any).value.split('\\n')) {\n const kv = yamlParseKV(line)\n if (kv)\n fm[kv[0]] = kv[1]\n }\n }\n })\n\n return { tree, frontmatter: fm }\n}\n\n/** Extract frontmatter key-value pairs only */\nexport function parseFrontmatter(content: string): Record<string, string> {\n return parseMd(content).frontmatter\n}\n\n/** Strip custom heading anchors like {#some-id} */\nfunction stripHeadingAnchors(text: string): string {\n return text.replace(/\\s*\\{#[^}]+\\}\\s*$/, '').trim()\n}\n\n/** Extract title: frontmatter title > first h1 > null */\nexport function extractTitle(content: string): string | null {\n const { tree, frontmatter: fm } = parseMd(content)\n if (fm.title)\n return fm.title\n\n let title: string | null = null\n visit(tree, 'heading', (node) => {\n if (node.depth === 1 && !title) {\n // Strip {#id} anchors and leading backslash escapes (e.g. `# \\`)\n const text = stripHeadingAnchors(toString(node)).replace(/^\\\\+\\s*/, '').trim()\n if (text.length > 0)\n title = text\n }\n })\n\n return title\n}\n\n/** Extract first paragraph text, 150 char max */\nexport function extractDescription(content: string): string | null {\n const { tree } = parseMd(content)\n\n let desc: string | null = null\n visit(tree, 'paragraph', (node, _index, parent) => {\n // Only top-level paragraphs (skip blockquote children, list items, etc.)\n if (desc || parent?.type !== 'root')\n return\n\n const text = toString(node).trim()\n if (text.length === 0)\n return\n\n // Strip markdown link syntax remnants and formatting chars\n let clean = text.replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1').replace(/[`*_~]/g, '')\n if (clean.length > 150)\n clean = `${clean.slice(0, 147)}...`\n desc = clean\n })\n\n return desc\n}\n\n/** Extract all headings with depth and text */\nexport function extractHeadings(content: string): MdHeading[] {\n const { tree } = parseMd(content)\n const headings: MdHeading[] = []\n\n visit(tree, 'heading', (node) => {\n headings.push({ depth: node.depth, text: stripHeadingAnchors(toString(node)) })\n })\n\n return headings\n}\n\n/** Extract all links (deduped by url) */\nexport function extractLinks(content: string): MdLink[] {\n const { tree } = parseMd(content)\n const links: MdLink[] = []\n const seen = new Set<string>()\n\n visit(tree, 'link', (node) => {\n if (!seen.has(node.url)) {\n seen.add(node.url)\n links.push({ title: toString(node), url: node.url })\n }\n })\n\n return links\n}\n\n/** Strip frontmatter block, return body only */\nexport function stripFrontmatter(content: string): string {\n const match = content.match(/^---\\r?\\n[\\s\\S]*?\\r?\\n---\\r?\\n/)\n return match ? content.slice(match[0].length).trim() : content\n}\n"],"mappings":";;;;;;AA6BA,SAAgB,QAAQ,SAA2B;CACjD,MAAM,OAAO,aAAa,SAAS;EACjC,YAAY,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;EACnC,iBAAiB,CAAC,wBAAwB,CAAC,OAAO,CAAC,CAAC;EACrD,CAAC;CAEF,MAAM,KAA6B,EAAE;CACrC,MAAM,MAAM,SAAS,SAAgB;EACnC,IAAI,KAAK,SAAS,QAChB,KAAK,MAAM,QAAS,KAAa,MAAM,MAAM,KAAK,EAAE;GAClD,MAAM,KAAK,YAAY,KAAK;GAC5B,IAAI,IACF,GAAG,GAAG,MAAM,GAAG;;GAGrB;CAEF,OAAO;EAAE;EAAM,aAAa;EAAI;;AAIlC,SAAgB,iBAAiB,SAAyC;CACxE,OAAO,QAAQ,QAAQ,CAAC;;AAI1B,SAAS,oBAAoB,MAAsB;CACjD,OAAO,KAAK,QAAQ,qBAAqB,GAAG,CAAC,MAAM;;AAIrD,SAAgB,aAAa,SAAgC;CAC3D,MAAM,EAAE,MAAM,aAAa,OAAO,QAAQ,QAAQ;CAClD,IAAI,GAAG,OACL,OAAO,GAAG;CAEZ,IAAI,QAAuB;CAC3B,MAAM,MAAM,YAAY,SAAS;EAC/B,IAAI,KAAK,UAAU,KAAK,CAAC,OAAO;GAE9B,MAAM,OAAO,oBAAoB,SAAS,KAAK,CAAC,CAAC,QAAQ,WAAW,GAAG,CAAC,MAAM;GAC9E,IAAI,KAAK,SAAS,GAChB,QAAQ;;GAEZ;CAEF,OAAO;;AAIT,SAAgB,mBAAmB,SAAgC;CACjE,MAAM,EAAE,SAAS,QAAQ,QAAQ;CAEjC,IAAI,OAAsB;CAC1B,MAAM,MAAM,cAAc,MAAM,QAAQ,WAAW;EAEjD,IAAI,QAAQ,QAAQ,SAAS,QAC3B;EAEF,MAAM,OAAO,SAAS,KAAK,CAAC,MAAM;EAClC,IAAI,KAAK,WAAW,GAClB;EAGF,IAAI,QAAQ,KAAK,QAAQ,0BAA0B,KAAK,CAAC,QAAQ,WAAW,GAAG;EAC/E,IAAI,MAAM,SAAS,KACjB,QAAQ,GAAG,MAAM,MAAM,GAAG,IAAI,CAAC;EACjC,OAAO;GACP;CAEF,OAAO;;AAgBT,SAAgB,aAAa,SAA2B;CACtD,MAAM,EAAE,SAAS,QAAQ,QAAQ;CACjC,MAAM,QAAkB,EAAE;CAC1B,MAAM,uBAAO,IAAI,KAAa;CAE9B,MAAM,MAAM,SAAS,SAAS;EAC5B,IAAI,CAAC,KAAK,IAAI,KAAK,IAAI,EAAE;GACvB,KAAK,IAAI,KAAK,IAAI;GAClB,MAAM,KAAK;IAAE,OAAO,SAAS,KAAK;IAAE,KAAK,KAAK;IAAK,CAAC;;GAEtD;CAEF,OAAO;;AAIT,SAAgB,iBAAiB,SAAyB;CACxD,MAAM,QAAQ,QAAQ,MAAM,iCAAiC;CAC7D,OAAO,QAAQ,QAAQ,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG"}
1
+ {"version":3,"file":"markdown.mjs","names":[],"sources":["../../src/core/markdown.ts"],"sourcesContent":["/**\n * Lightweight regex-based markdown utilities.\n * Operations needed (frontmatter, title, description, links, headings) are\n * simple enough that a full AST stack would be overkill.\n */\n\nimport { yamlParseKV } from './yaml.ts'\n\nexport interface MdLink {\n title: string\n url: string\n}\n\nexport interface Heading {\n depth: number\n text: string\n}\n\nconst FRONTMATTER_RE = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?/\nconst HEADING_LINE_RE = /^(#{1,6})[ \\t]+([^ \\t\\r\\n][^\\r\\n]*)$/gm\nconst ANCHOR_RE = /\\s*\\{#[^}]+\\}\\s*$/\nconst BACKSLASH_PREFIX_RE = /^\\\\+\\s*/\nconst INLINE_CODE_RE = /`([^`]+)`/g\nconst LINK_RE = /(?<!!)\\[([^\\]]+)\\]\\(([^)\\s]+)(?:\\s+\"[^\"]*\")?\\)/g\nconst HEADING_START_RE = /^#{1,6}\\s/\nconst BLOCKQUOTE_START_RE = /^>\\s?/\nconst LIST_ITEM_START_RE = /^[-*+]\\s/\nconst ORDERED_LIST_ITEM_START_RE = /^\\d+\\.\\s/\nconst TABLE_ROW_START_RE = /^\\|/\nconst INDENTED_CODE_START_RE = /^ {4}/\nconst FENCE_START_RE = /^\\s*```/\nconst MARKDOWN_LINK_RE = /\\[([^\\]]+)\\]\\([^)]+\\)/g\nconst MARKDOWN_FORMATTING_RE = /[`*_~]/g\nconst FENCED_CODE_BLOCK_RE = /```[\\s\\S]*?```/g\nconst INLINE_CODE_SPAN_RE = /`[^`\\n]*`/g\n\n/** Strip frontmatter block, return body only. */\nexport function stripFrontmatter(content: string): string {\n const m = content.match(FRONTMATTER_RE)\n return m ? content.slice(m[0].length).trim() : content\n}\n\n/** Extract frontmatter key-value pairs. */\nexport function parseFrontmatter(content: string): Record<string, string> {\n const m = content.match(FRONTMATTER_RE)\n if (!m)\n return {}\n const fm: Record<string, string> = {}\n for (const line of m[1]!.split('\\n')) {\n const kv = yamlParseKV(line)\n if (kv)\n fm[kv[0]] = kv[1]\n }\n return fm\n}\n\nfunction cleanHeadingText(raw: string): string {\n return raw\n .replace(ANCHOR_RE, '')\n .replace(BACKSLASH_PREFIX_RE, '')\n .replace(INLINE_CODE_RE, '$1')\n .trim()\n}\n\n/** Extract all headings in document order. */\nexport function extractHeadings(content: string): Heading[] {\n const body = stripFrontmatter(content)\n const headings: Heading[] = []\n for (const m of body.matchAll(HEADING_LINE_RE)) {\n const text = cleanHeadingText(m[2]!)\n if (text)\n headings.push({ depth: m[1]!.length, text })\n }\n return headings\n}\n\n/** Extract title: frontmatter title > first h1 > null. */\nexport function extractTitle(content: string): string | null {\n const fm = parseFrontmatter(content)\n if (fm.title)\n return fm.title\n const body = stripFrontmatter(content)\n for (const m of body.matchAll(HEADING_LINE_RE)) {\n if (m[1] === '#') {\n const text = cleanHeadingText(m[2]!)\n if (text)\n return text\n }\n }\n return null\n}\n\nfunction isBlockStarter(trimmed: string, raw: string): boolean {\n return HEADING_START_RE.test(trimmed)\n || BLOCKQUOTE_START_RE.test(trimmed)\n || LIST_ITEM_START_RE.test(trimmed)\n || ORDERED_LIST_ITEM_START_RE.test(trimmed)\n || TABLE_ROW_START_RE.test(trimmed)\n || trimmed.startsWith('<')\n || INDENTED_CODE_START_RE.test(raw)\n}\n\n/** Extract first top-level paragraph, stripped of formatting, max 150 chars. */\nexport function extractDescription(content: string): string | null {\n const body = stripFrontmatter(content)\n const lines = body.split('\\n')\n let inFence = false\n let inHtml = false\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!\n if (FENCE_START_RE.test(line)) {\n inFence = !inFence\n continue\n }\n if (inFence)\n continue\n\n const trimmed = line.trim()\n\n if (inHtml) {\n if (!trimmed)\n inHtml = false\n continue\n }\n if (trimmed.startsWith('<')) {\n inHtml = true\n continue\n }\n\n if (!trimmed || isBlockStarter(trimmed, line))\n continue\n\n let para = trimmed\n for (let j = i + 1; j < lines.length; j++) {\n const next = lines[j]!\n const nextTrim = next.trim()\n if (!nextTrim || isBlockStarter(nextTrim, next))\n break\n para += ` ${nextTrim}`\n }\n\n let clean = para.replace(MARKDOWN_LINK_RE, '$1').replace(MARKDOWN_FORMATTING_RE, '')\n if (clean.length > 150)\n clean = `${clean.slice(0, 147)}...`\n return clean\n }\n\n return null\n}\n\n/** Extract all links (deduped by url), excluding images and links in code. */\nexport function extractLinks(content: string): MdLink[] {\n const body = stripFrontmatter(content)\n const sanitized = body\n .replace(FENCED_CODE_BLOCK_RE, '')\n .replace(INLINE_CODE_SPAN_RE, '')\n\n const links: MdLink[] = []\n const seen = new Set<string>()\n for (const m of sanitized.matchAll(LINK_RE)) {\n const url = m[2]!\n if (!seen.has(url)) {\n seen.add(url)\n links.push({ title: m[1]!, url })\n }\n }\n return links\n}\n"],"mappings":";;;;;AAkBA,MAAM,iBAAiB;AACvB,MAAM,UAAA;AACN,MAAM,mBAAY;AAClB,MAAM,sBAAsB;AAC5B,MAAM,qBAAiB;AACvB,MAAM,6BAAU;AAChB,MAAM,qBAAmB;AACzB,MAAM,yBAAsB;AAC5B,MAAM,iBAAA;AACN,MAAM,mBAAA;AACN,MAAM,yBAAqB;AAC3B,MAAM,uBAAA;AACN,MAAM,sBAAiB;AAEvB,SAAM,iBAAA,SAAyB;CAC/B,MAAM,IAAA,QAAA,MAAA,eAAuB;CAC7B,OAAM,IAAA,QAAA,MAAA,EAAsB,GAAA,OAAA,CAAA,MAAA,GAAA;;SAIpB,iBAAkB,SAAA;CACxB,MAAA,IAAO,QAAI,MAAQ,eAAmB;;;CAIxC,KAAA,MAAgB,QAAA,EAAA,GAAA,MAAiB,KAAyC,EAAA;EACxE,MAAM,KAAI,YAAc,KAAA;EACxB,IAAK,IACH,GAAA,GAAO,MAAE,GAAA;;CAEX,OAAK;;SAEC,iBACa,KAAA;;;AAKrB,SAAS,aAAA,SAAsC;CAC7C,MAAA,KACG,iBAAQ,QACR;;;CAkBL,KAAA,MAAgB,KAAA,KAAa,SAAgC,gBAAA,EAAA,IAAA,EAAA,OAAA,KAAA;EAC3D,MAAM,OAAK,iBAAiB,EAAA,GAAQ;EACpC,IAAI,MAAG,OACL;;CAEF,OAAK;;SAGG,eACK,SAAA,KAAA;;;AAMf,SAAS,mBAAe,SAAiB;CACvC,MAAA,QAAO,iBAAsB,QAAQ,CAAA,MAChC,KAAA;;;CASP,KAAA,IAAgB,IAAA,GAAA,IAAA,MAAA,QAAmB,KAAgC;EAEjE,MAAM,OADO,MAAA;EAEb,IAAI,eAAU,KAAA,KAAA,EAAA;GACd,UAAI,CAAS;GAEb;;EAEE,IAAI,SAAA;QACF,UAAW,KAAA,MAAA;MACX,QAAA;;GAEF;;EAKA,IAAI,QAAQ,WAAA,IAAA,EAAA;GACV,SAAK;GAEL;;EAEF,IAAI,CAAA,WAAQ,eAAiB,SAAA,KAAA,EAAA;MAC3B,OAAS;OACT,IAAA,IAAA,IAAA,GAAA,IAAA,MAAA,QAAA,KAAA;;GAGF,MAAK,WAAW,KAAA,MAAA;GAGhB,IAAI,CAAA,YAAO,eAAA,UAAA,KAAA,EAAA;GACX,QAAS,IAAI;;MAEX,QAAM,KAAA,QAAgB,kBAAM,KAAA,CAAA,QAAA,wBAAA,GAAA;MAC5B,MAAK,SAAY,KAAA,QAAA,GAAe,MAAA,MAAU,GACxC,IAAA,CAAA;SACF;;QAGE;;;CAMN,MAAA,YAAO,iBAAA,QAAA,CAAA,QAAA,sBAAA,GAAA,CAAA,QAAA,qBAAA,GAAA;;;CAIT,KAAA,MAAgB,KAAA,UAAa,SAA2B,QAAA,EAAA;EAEtD,MAAM,MAAA,EAAA;EAIN,IAAA,CAAM,KAAA,IAAkB,IAAE,EAAA;GAC1B,KAAM,IAAA,IAAA;GACN,MAAK,KAAM;IACT,OAAM,EAAA;IACN;IACE,CAAA;;;QAC2B"}
@@ -0,0 +1,33 @@
1
+ import * as p from "@clack/prompts";
2
+ var MenuCancel = class extends Error {
3
+ name = "MenuCancel";
4
+ };
5
+ function guard(value) {
6
+ if (p.isCancel(value)) throw new MenuCancel();
7
+ return value;
8
+ }
9
+ async function menuLoop(opts) {
10
+ while (true) {
11
+ const options = await opts.options();
12
+ const initial = typeof opts.initialValue === "function" ? opts.initialValue() : opts.initialValue;
13
+ const choice = opts.searchable ? await p.autocomplete({
14
+ message: opts.message,
15
+ options,
16
+ ...initial != null ? { initialValue: initial } : {}
17
+ }) : await p.select({
18
+ message: opts.message,
19
+ options,
20
+ ...initial != null ? { initialValue: initial } : {}
21
+ });
22
+ if (p.isCancel(choice)) return;
23
+ try {
24
+ if (await opts.onSelect(choice)) return;
25
+ } catch (err) {
26
+ if (err instanceof MenuCancel) continue;
27
+ throw err;
28
+ }
29
+ }
30
+ }
31
+ export { menuLoop as n, guard as t };
32
+
33
+ //# sourceMappingURL=menu.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"menu.mjs","names":[],"sources":["../../src/cli/menu.ts"],"sourcesContent":["import * as p from '@clack/prompts'\n\nexport class MenuCancel extends Error { override name = 'MenuCancel' }\n\nexport function guard<T>(value: T | symbol): T {\n if (p.isCancel(value))\n throw new MenuCancel()\n return value as T\n}\n\nexport interface MenuOption {\n label: string\n value: string\n hint?: string\n}\n\nexport async function menuLoop(opts: {\n message: string\n options: () => MenuOption[] | Promise<MenuOption[]>\n onSelect: (value: string) => Promise<boolean | void>\n initialValue?: string | (() => string | undefined)\n searchable?: boolean\n}): Promise<void> {\n while (true) {\n const options = await opts.options()\n const initial = typeof opts.initialValue === 'function' ? opts.initialValue() : opts.initialValue\n const choice = opts.searchable\n ? await p.autocomplete({ message: opts.message, options, ...(initial != null ? { initialValue: initial } : {}) })\n : await p.select({ message: opts.message, options, ...(initial != null ? { initialValue: initial } : {}) })\n if (p.isCancel(choice))\n return\n try {\n if (await opts.onSelect(choice as string))\n return\n }\n catch (err) {\n if (err instanceof MenuCancel)\n continue\n throw err\n }\n }\n}\n"],"mappings":";AAEA,IAAa,aAAb,cAAgC,MAAM;CAAE,OAAgB;;AAExD,SAAgB,MAAS,OAAsB;CAC7C,IAAI,EAAE,SAAS,MAAM,EACnB,MAAM,IAAI,YAAY;CACxB,OAAO;;AAST,eAAsB,SAAS,MAMb;CAChB,OAAO,MAAM;EACX,MAAM,UAAU,MAAM,KAAK,SAAS;EACpC,MAAM,UAAU,OAAO,KAAK,iBAAiB,aAAa,KAAK,cAAc,GAAG,KAAK;EACrF,MAAM,SAAS,KAAK,aAChB,MAAM,EAAE,aAAa;GAAE,SAAS,KAAK;GAAS;GAAS,GAAI,WAAW,OAAO,EAAE,cAAc,SAAS,GAAG,EAAE;GAAG,CAAC,GAC/G,MAAM,EAAE,OAAO;GAAE,SAAS,KAAK;GAAS;GAAS,GAAI,WAAW,OAAO,EAAE,cAAc,SAAS,GAAG,EAAE;GAAG,CAAC;EAC7G,IAAI,EAAE,SAAS,OAAO,EACpB;EACF,IAAI;GACF,IAAI,MAAM,KAAK,SAAS,OAAiB,EACvC;WAEG,KAAK;GACV,IAAI,eAAe,YACjB;GACF,MAAM"}
@@ -0,0 +1,61 @@
1
+ import { styleText } from "node:util";
2
+ import * as p from "@clack/prompts";
3
+ const OAUTH_NOTE = `${styleText("yellow", "⚠")} OAuth providers are disabled.\n\nConsumer subscription OAuth impersonates official CLI clients and\nviolates provider Terms of Service, risking account bans.\n\nUse API keys or native CLI tools instead:\n ${styleText("cyan", "ANTHROPIC_API_KEY")} / ${styleText("cyan", "claude")} CLI\n ${styleText("cyan", "OPENAI_API_KEY")} / ${styleText("cyan", "codex")} CLI\n ${styleText("cyan", "GEMINI_API_KEY")} / ${styleText("cyan", "gemini")} CLI`;
4
+ const NO_MODELS_MESSAGE = `No enhancement models detected.\n ${styleText("gray", "Skills work fine without this, you get raw docs, issues, and types.\n Enhancement compresses them into a concise cheat sheet with gotchas.")}\n\n To connect a model (optional):\n 1. Set an env var: ANTHROPIC_API_KEY, GEMINI_API_KEY, or OPENAI_API_KEY\n 2. Install a CLI tool: ${styleText("cyan", "claude")}, ${styleText("cyan", "gemini")}, or ${styleText("cyan", "codex")} (restart wizard after)`;
5
+ function groupModelsByProvider(models) {
6
+ const byVendor = /* @__PURE__ */ new Map();
7
+ for (const m of models) {
8
+ const key = m.vendorGroup ?? m.provider;
9
+ if (!byVendor.has(key)) byVendor.set(key, {
10
+ name: key,
11
+ models: []
12
+ });
13
+ byVendor.get(key).models.push(m);
14
+ }
15
+ return byVendor;
16
+ }
17
+ async function pickModel(models, opts = {}) {
18
+ const byProvider = groupModelsByProvider(models);
19
+ const before = opts.before ?? [];
20
+ const after = opts.after ?? [];
21
+ if (byProvider.size === 1 && before.length === 0) {
22
+ const [, group] = [...byProvider.entries()][0];
23
+ const choice = await p.select({
24
+ message: `${group.name}`,
25
+ options: [...group.models.map((m) => ({
26
+ label: m.recommended ? `${m.name} (recommended - fast and cheap)` : m.name,
27
+ value: m.id,
28
+ hint: m.hint
29
+ })), ...after]
30
+ });
31
+ return p.isCancel(choice) ? null : choice;
32
+ }
33
+ const providerChoice = await p.select({
34
+ message: "Select provider",
35
+ options: [
36
+ ...before,
37
+ ...Array.from(byProvider.entries(), ([key, { name, models: ms }]) => ({
38
+ label: name,
39
+ value: key,
40
+ hint: `${ms.length} models`
41
+ })),
42
+ ...after
43
+ ]
44
+ });
45
+ if (p.isCancel(providerChoice)) return null;
46
+ const providerStr = providerChoice;
47
+ if (before.some((o) => o.value === providerStr) || after.some((o) => o.value === providerStr)) return providerStr;
48
+ const group = byProvider.get(providerStr);
49
+ const modelChoice = await p.select({
50
+ message: `Select model (${group.name})`,
51
+ options: group.models.map((m) => ({
52
+ label: m.recommended ? `${m.name} (recommended - fast and cheap)` : m.name,
53
+ value: m.id,
54
+ hint: m.hint
55
+ }))
56
+ });
57
+ return p.isCancel(modelChoice) ? null : modelChoice;
58
+ }
59
+ export { OAUTH_NOTE as n, pickModel as r, NO_MODELS_MESSAGE as t };
60
+
61
+ //# sourceMappingURL=model-picker.mjs.map