invokora 0.1.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 (41) hide show
  1. package/dist/cli/app.d.ts +55 -0
  2. package/dist/cli/app.js +1087 -0
  3. package/dist/cli/config.d.ts +12 -0
  4. package/dist/cli/config.js +73 -0
  5. package/dist/cli/constants.d.ts +24 -0
  6. package/dist/cli/constants.js +52 -0
  7. package/dist/cli/http.d.ts +2 -0
  8. package/dist/cli/http.js +23 -0
  9. package/dist/cli/index.d.ts +6 -0
  10. package/dist/cli/index.js +11 -0
  11. package/dist/cli/mcp/app.d.ts +12 -0
  12. package/dist/cli/mcp/app.js +85 -0
  13. package/dist/cli/mcp/backend_client.d.ts +10 -0
  14. package/dist/cli/mcp/backend_client.js +91 -0
  15. package/dist/cli/mcp/errors.d.ts +28 -0
  16. package/dist/cli/mcp/errors.js +139 -0
  17. package/dist/cli/mcp/progress.d.ts +12 -0
  18. package/dist/cli/mcp/progress.js +49 -0
  19. package/dist/cli/mcp/responses_session.d.ts +21 -0
  20. package/dist/cli/mcp/responses_session.js +233 -0
  21. package/dist/cli/mcp/schemas.d.ts +99 -0
  22. package/dist/cli/mcp/schemas.js +66 -0
  23. package/dist/cli/mcp/server.d.ts +4 -0
  24. package/dist/cli/mcp/server.js +3 -0
  25. package/dist/cli/mcp/session_store.d.ts +32 -0
  26. package/dist/cli/mcp/session_store.js +58 -0
  27. package/dist/cli/mcp/tool_handlers.d.ts +3 -0
  28. package/dist/cli/mcp/tool_handlers.js +26 -0
  29. package/dist/cli/mcp_setup.d.ts +33 -0
  30. package/dist/cli/mcp_setup.js +225 -0
  31. package/dist/cli/oauth.d.ts +45 -0
  32. package/dist/cli/oauth.js +594 -0
  33. package/dist/cli/prompts.d.ts +23 -0
  34. package/dist/cli/prompts.js +175 -0
  35. package/dist/cli/release.d.ts +3 -0
  36. package/dist/cli/release.js +3 -0
  37. package/dist/cli/skills.d.ts +43 -0
  38. package/dist/cli/skills.js +443 -0
  39. package/dist/cli/types.d.ts +183 -0
  40. package/dist/cli/types.js +1 -0
  41. package/package.json +29 -0
@@ -0,0 +1,594 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash, randomBytes } from 'node:crypto';
3
+ import { createServer } from 'node:http';
4
+ import { API_V1, OAUTH_CALLBACK_PATH, OAUTH_DEFAULT_TIMEOUT_MS } from './constants.js';
5
+ import { readJsonResponse } from './http.js';
6
+ const OAUTH_CALLBACK_COPY = {
7
+ en: {
8
+ title: 'Invokora Login',
9
+ brand: 'Invokora CLI auth',
10
+ status: {
11
+ success: 'AUTHORIZED',
12
+ warning: 'ACTION REQUIRED',
13
+ },
14
+ heading: {
15
+ success: ['Login', 'complete'],
16
+ blocked: ['Login', 'blocked'],
17
+ },
18
+ messages: {
19
+ success: 'Login complete. Returning control to the terminal.',
20
+ stateMismatch: 'State mismatch. You can close this window.',
21
+ authorizationFailed: 'Authorization failed. You can close this window.',
22
+ missingCode: 'Missing authorization code. You can close this window.',
23
+ },
24
+ autoCloseHint: 'This loopback window should close automatically. If it stays open, you can close it manually.',
25
+ runtimeBoundary: 'Runtime boundary',
26
+ boundary: {
27
+ runtime: 'Runtime',
28
+ runtimeValue: 'Localhost',
29
+ client: 'Client',
30
+ clientValue: 'Terminal',
31
+ access: 'Access',
32
+ accessValue: 'OAuth code',
33
+ },
34
+ },
35
+ zh: {
36
+ title: 'Invokora 登录',
37
+ brand: 'Invokora CLI 认证',
38
+ status: {
39
+ success: '已授权',
40
+ warning: '需要处理',
41
+ },
42
+ heading: {
43
+ success: ['登录', '完成'],
44
+ blocked: ['登录', '受阻'],
45
+ },
46
+ messages: {
47
+ success: '登录已完成,正在把控制权交回终端。',
48
+ stateMismatch: 'state 不匹配。你可以关闭这个窗口。',
49
+ authorizationFailed: '授权失败。你可以关闭这个窗口。',
50
+ missingCode: '缺少授权码。你可以关闭这个窗口。',
51
+ },
52
+ autoCloseHint: '这个本地回调窗口会自动关闭。如果没有关闭,可以手动关闭。',
53
+ runtimeBoundary: '运行边界',
54
+ boundary: {
55
+ runtime: '运行环境',
56
+ runtimeValue: '本地回环',
57
+ client: '客户端',
58
+ clientValue: '终端',
59
+ access: '访问凭据',
60
+ accessValue: 'OAuth 授权码',
61
+ },
62
+ },
63
+ };
64
+ function escapeHTML(value) {
65
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
66
+ }
67
+ function resolveOAuthCallbackLocale(acceptLanguage) {
68
+ const raw = Array.isArray(acceptLanguage) ? acceptLanguage.join(',') : acceptLanguage;
69
+ if (!raw)
70
+ return 'en';
71
+ const entries = raw
72
+ .split(',')
73
+ .map((part, index) => {
74
+ const [language = '', ...params] = part.trim().split(';');
75
+ const qParam = params.find((param) => param.trim().startsWith('q='));
76
+ const q = qParam ? Number(qParam.trim().slice(2)) : 1;
77
+ return { language: language.toLowerCase(), q: Number.isFinite(q) ? q : 0, index };
78
+ })
79
+ .filter((entry) => entry.q > 0)
80
+ .sort((a, b) => b.q - a.q || a.index - b.index);
81
+ for (const entry of entries) {
82
+ if (entry.language === 'zh' || entry.language.startsWith('zh-'))
83
+ return 'zh';
84
+ if (entry.language === 'en' || entry.language.startsWith('en-'))
85
+ return 'en';
86
+ }
87
+ return 'en';
88
+ }
89
+ function renderOAuthCallbackHTML(view, locale) {
90
+ const copy = OAUTH_CALLBACK_COPY[locale];
91
+ const autoClose = view === 'success';
92
+ const statusKey = autoClose ? 'success' : 'warning';
93
+ const statusLabel = copy.status[statusKey];
94
+ const statusTone = statusKey;
95
+ const heading = copy.heading[autoClose ? 'success' : 'blocked'].map(escapeHTML).join('<br />');
96
+ const escapedMessage = escapeHTML(copy.messages[view]);
97
+ const script = autoClose
98
+ ? `<script>
99
+ (() => {
100
+ const closeWindow = () => {
101
+ try {
102
+ window.close();
103
+ } catch {}
104
+ };
105
+ closeWindow();
106
+ setTimeout(closeWindow, 150);
107
+ })();
108
+ </script>`
109
+ : '';
110
+ return `<!doctype html>
111
+ <html lang="${locale === 'zh' ? 'zh-CN' : 'en'}">
112
+ <head>
113
+ <meta charset="utf-8" />
114
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
115
+ <title>${escapeHTML(copy.title)}</title>
116
+ <style>
117
+ :root {
118
+ color-scheme: dark;
119
+ --warp-bg: #030b13;
120
+ --warp-bg-deep: #01060b;
121
+ --fg: #edf7ff;
122
+ --muted: #b8c4cf;
123
+ --muted-low: #7f909c;
124
+ --signal: #5cf28f;
125
+ --signal-cyan: #38caff;
126
+ --signal-cyan-dim: rgba(56, 202, 255, 0.16);
127
+ --line: rgba(56, 202, 255, 0.34);
128
+ --line-muted: rgba(184, 196, 207, 0.14);
129
+ --surface: rgba(4, 18, 30, 0.88);
130
+ --surface-low: rgba(8, 28, 43, 0.72);
131
+ --warning: #f6c86b;
132
+ }
133
+ * { box-sizing: border-box; }
134
+ body {
135
+ margin: 0;
136
+ min-height: 100vh;
137
+ overflow: hidden;
138
+ display: grid;
139
+ place-items: center;
140
+ padding: 24px;
141
+ background:
142
+ radial-gradient(circle at 24% 18%, rgba(56, 202, 255, 0.18), transparent 32%),
143
+ radial-gradient(circle at 80% 22%, rgba(92, 242, 143, 0.12), transparent 26%),
144
+ linear-gradient(180deg, #06111c 0%, var(--warp-bg-deep) 54%, #06111c 100%);
145
+ color: var(--fg);
146
+ font: 16px/1.55 "Space Grotesk", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
147
+ }
148
+ body::before {
149
+ content: "";
150
+ position: fixed;
151
+ inset: 0;
152
+ pointer-events: none;
153
+ background-image:
154
+ linear-gradient(rgba(56, 202, 255, 0.06) 1px, transparent 1px),
155
+ linear-gradient(90deg, rgba(56, 202, 255, 0.06) 1px, transparent 1px);
156
+ background-size: 42px 42px;
157
+ mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.88), rgba(0, 0, 0, 0.14));
158
+ }
159
+ main {
160
+ position: relative;
161
+ width: min(560px, 100%);
162
+ padding: 1px;
163
+ border: 1px solid var(--line);
164
+ clip-path: polygon(18px 0, 100% 0, 100% calc(100% - 18px), calc(100% - 18px) 100%, 0 100%, 0 18px);
165
+ background:
166
+ linear-gradient(135deg, rgba(56, 202, 255, 0.38), rgba(92, 242, 143, 0.18) 44%, rgba(56, 202, 255, 0.10)),
167
+ var(--surface);
168
+ box-shadow: 0 30px 80px rgba(0, 0, 0, 0.42), 0 0 46px rgba(56, 202, 255, 0.12);
169
+ }
170
+ .panel {
171
+ padding: clamp(24px, 7vw, 40px);
172
+ clip-path: inherit;
173
+ background:
174
+ linear-gradient(180deg, rgba(4, 18, 30, 0.92), rgba(1, 6, 11, 0.95)),
175
+ var(--surface);
176
+ }
177
+ .status-strip {
178
+ display: flex;
179
+ gap: 8px;
180
+ margin-bottom: 28px;
181
+ }
182
+ .status-strip span {
183
+ width: 38px;
184
+ height: 3px;
185
+ background: var(--signal-cyan);
186
+ box-shadow: 0 0 14px rgba(56, 202, 255, 0.4);
187
+ }
188
+ .brand {
189
+ display: inline-flex;
190
+ align-items: center;
191
+ gap: 10px;
192
+ color: var(--signal);
193
+ font-size: 12px;
194
+ font-weight: 800;
195
+ letter-spacing: 0.16em;
196
+ text-transform: uppercase;
197
+ }
198
+ .brand-mark {
199
+ display: grid;
200
+ width: 26px;
201
+ height: 26px;
202
+ place-items: center;
203
+ border: 1px solid rgba(92, 242, 143, 0.48);
204
+ background: rgba(92, 242, 143, 0.10);
205
+ color: var(--signal);
206
+ }
207
+ .brand-mark::before {
208
+ content: ">_";
209
+ transform: translateY(-1px);
210
+ }
211
+ .status {
212
+ display: inline-flex;
213
+ margin-top: 24px;
214
+ border: 1px solid var(--signal-cyan-dim);
215
+ background: var(--surface-low);
216
+ padding: 6px 9px;
217
+ color: var(--status-color);
218
+ font-size: 11px;
219
+ font-weight: 800;
220
+ letter-spacing: 0.14em;
221
+ text-transform: uppercase;
222
+ }
223
+ .status.success { --status-color: var(--signal); }
224
+ .status.warning { --status-color: var(--warning); }
225
+ h1 {
226
+ margin: 14px 0 10px;
227
+ color: var(--fg);
228
+ font-size: clamp(34px, 8vw, 54px);
229
+ font-weight: 900;
230
+ letter-spacing: 0.035em;
231
+ line-height: 0.98;
232
+ text-transform: uppercase;
233
+ }
234
+ p {
235
+ margin: 0;
236
+ color: var(--muted);
237
+ max-width: 34rem;
238
+ }
239
+ .hint {
240
+ margin-top: 16px;
241
+ color: var(--muted-low);
242
+ font-size: 14px;
243
+ }
244
+ .boundary {
245
+ display: grid;
246
+ grid-template-columns: repeat(3, minmax(0, 1fr));
247
+ gap: 1px;
248
+ margin-top: 28px;
249
+ border: 1px solid var(--line-muted);
250
+ background: var(--line-muted);
251
+ }
252
+ .boundary div {
253
+ min-width: 0;
254
+ background: rgba(3, 11, 19, 0.86);
255
+ padding: 12px;
256
+ }
257
+ .boundary dt {
258
+ margin: 0 0 4px;
259
+ color: var(--muted-low);
260
+ font-size: 10px;
261
+ letter-spacing: 0.14em;
262
+ text-transform: uppercase;
263
+ }
264
+ .boundary dd {
265
+ margin: 0;
266
+ color: var(--signal-cyan);
267
+ font-size: 12px;
268
+ font-weight: 700;
269
+ }
270
+ @media (max-width: 520px) {
271
+ body { padding: 16px; }
272
+ .boundary { grid-template-columns: 1fr; }
273
+ }
274
+ </style>
275
+ </head>
276
+ <body>
277
+ <main>
278
+ <div class="panel">
279
+ <div class="status-strip" aria-hidden="true"><span></span><span></span><span></span></div>
280
+ <div class="brand"><span class="brand-mark" aria-hidden="true"></span><span>${escapeHTML(copy.brand)}</span></div>
281
+ <div class="status ${statusTone}">${escapeHTML(statusLabel)}</div>
282
+ <h1>${heading}</h1>
283
+ <p>${escapedMessage}</p>
284
+ ${autoClose ? `<p class="hint">${escapeHTML(copy.autoCloseHint)}</p>` : ''}
285
+ <dl class="boundary" aria-label="${escapeHTML(copy.runtimeBoundary)}">
286
+ <div><dt>${escapeHTML(copy.boundary.runtime)}</dt><dd>${escapeHTML(copy.boundary.runtimeValue)}</dd></div>
287
+ <div><dt>${escapeHTML(copy.boundary.client)}</dt><dd>${escapeHTML(copy.boundary.clientValue)}</dd></div>
288
+ <div><dt>${escapeHTML(copy.boundary.access)}</dt><dd>${escapeHTML(copy.boundary.accessValue)}</dd></div>
289
+ </dl>
290
+ </div>
291
+ </main>
292
+ ${script}
293
+ </body>
294
+ </html>`;
295
+ }
296
+ export function base64UrlEncode(input) {
297
+ return input.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
298
+ }
299
+ export function generateOAuthState() {
300
+ return base64UrlEncode(randomBytes(32));
301
+ }
302
+ export function generateCodeVerifier() {
303
+ return base64UrlEncode(randomBytes(64));
304
+ }
305
+ export function buildS256CodeChallenge(codeVerifier) {
306
+ return base64UrlEncode(createHash('sha256').update(codeVerifier).digest());
307
+ }
308
+ export function parseLoginCommandOptions(args) {
309
+ const options = {
310
+ timeoutMs: OAUTH_DEFAULT_TIMEOUT_MS,
311
+ noOpen: false,
312
+ };
313
+ for (let i = 0; i < args.length; i++) {
314
+ const arg = args[i];
315
+ if (arg === '--timeout') {
316
+ const val = args[i + 1];
317
+ if (!val)
318
+ throw new Error('Missing value for --timeout');
319
+ const ms = Number(val);
320
+ if (!Number.isFinite(ms) || ms <= 0) {
321
+ throw new Error('--timeout must be a positive number (milliseconds)');
322
+ }
323
+ options.timeoutMs = ms;
324
+ i++;
325
+ continue;
326
+ }
327
+ if (arg === '--no-open') {
328
+ options.noOpen = true;
329
+ continue;
330
+ }
331
+ throw new Error(`Unknown login option: ${arg}`);
332
+ }
333
+ return options;
334
+ }
335
+ export function chooseLoopbackHost(address) {
336
+ return address === '::1' ? '[::1]' : address;
337
+ }
338
+ export async function openBrowserURL(url) {
339
+ const platform = process.platform;
340
+ const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open';
341
+ const args = platform === 'win32' ? ['/c', 'start', '""', url] : [url];
342
+ const executable = platform === 'win32' ? 'cmd' : command;
343
+ return new Promise((resolve) => {
344
+ const child = spawn(executable, args, {
345
+ stdio: 'ignore',
346
+ detached: platform !== 'win32',
347
+ });
348
+ child.on('error', () => resolve(false));
349
+ child.on('spawn', () => {
350
+ child.unref();
351
+ resolve(true);
352
+ });
353
+ });
354
+ }
355
+ export function normalizeCallbackError(payload) {
356
+ if (payload.error === 'access_denied') {
357
+ return 'Authorization denied by user';
358
+ }
359
+ const code = payload.error?.trim() || 'oauth_error';
360
+ const desc = payload.error_description?.trim();
361
+ return desc ? `${code}: ${desc}` : code;
362
+ }
363
+ export function withTimeout(promise, timeoutMs, timeoutMessage) {
364
+ return new Promise((resolve, reject) => {
365
+ const timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
366
+ promise
367
+ .then((value) => {
368
+ clearTimeout(timer);
369
+ resolve(value);
370
+ })
371
+ .catch((error) => {
372
+ clearTimeout(timer);
373
+ reject(error);
374
+ });
375
+ });
376
+ }
377
+ export class OAuthLoopbackServer {
378
+ expectedState;
379
+ timeoutMs;
380
+ server;
381
+ closed = false;
382
+ settled = false;
383
+ redirectUrl = '';
384
+ resolveCallback;
385
+ callbackPromise = new Promise((resolve) => {
386
+ this.resolveCallback = resolve;
387
+ });
388
+ constructor(expectedState, timeoutMs, server) {
389
+ this.expectedState = expectedState;
390
+ this.timeoutMs = timeoutMs;
391
+ this.server = server;
392
+ }
393
+ static async create(expectedState, timeoutMs) {
394
+ const server = createServer();
395
+ const instance = new OAuthLoopbackServer(expectedState, timeoutMs, server);
396
+ server.on('request', (req, res) => instance.handleRequest(req, res));
397
+ await instance.listen();
398
+ instance.redirectUrl = instance.buildRedirectURI();
399
+ return instance;
400
+ }
401
+ get redirectURI() {
402
+ return this.redirectUrl;
403
+ }
404
+ async waitForCallback() {
405
+ try {
406
+ return await withTimeout(this.callbackPromise, this.timeoutMs, 'OAuth callback timed out');
407
+ }
408
+ finally {
409
+ await this.close();
410
+ }
411
+ }
412
+ async close() {
413
+ if (this.closed)
414
+ return;
415
+ this.closed = true;
416
+ await new Promise((resolve) => {
417
+ this.server.close(() => resolve());
418
+ });
419
+ }
420
+ async listen() {
421
+ await new Promise((resolve, reject) => {
422
+ const onError = (error) => {
423
+ this.server.off('error', onError);
424
+ reject(error);
425
+ };
426
+ this.server.once('error', onError);
427
+ // 回环地址只允许本机回调,避免 CLI 登录口暴露给局域网其他机器。
428
+ this.server.listen(0, '127.0.0.1', () => {
429
+ this.server.off('error', onError);
430
+ resolve();
431
+ });
432
+ });
433
+ }
434
+ buildRedirectURI() {
435
+ const address = this.server.address();
436
+ if (!address || typeof address === 'string') {
437
+ throw new Error('Failed to bind loopback callback listener');
438
+ }
439
+ const host = chooseLoopbackHost(address.address || '127.0.0.1');
440
+ return `http://${host}:${address.port}${OAUTH_CALLBACK_PATH}`;
441
+ }
442
+ complete(payload) {
443
+ if (this.settled)
444
+ return;
445
+ this.settled = true;
446
+ this.resolveCallback?.(payload);
447
+ }
448
+ handleRequest(req, res) {
449
+ const reqUrl = req.url ?? '/';
450
+ const incoming = new URL(reqUrl, 'http://127.0.0.1');
451
+ if (incoming.pathname !== OAUTH_CALLBACK_PATH) {
452
+ res.statusCode = 404;
453
+ res.setHeader('content-type', 'text/plain; charset=utf-8');
454
+ res.end('Not found');
455
+ return;
456
+ }
457
+ const payload = {
458
+ code: incoming.searchParams.get('code') ?? undefined,
459
+ state: incoming.searchParams.get('state') ?? undefined,
460
+ error: incoming.searchParams.get('error') ?? undefined,
461
+ error_description: incoming.searchParams.get('error_description') ?? undefined,
462
+ };
463
+ const locale = resolveOAuthCallbackLocale(req.headers['accept-language']);
464
+ if (payload.state && payload.state !== this.expectedState) {
465
+ res.statusCode = 400;
466
+ res.setHeader('content-type', 'text/html; charset=utf-8');
467
+ res.end(renderOAuthCallbackHTML('stateMismatch', locale));
468
+ // 这里要提前结束等待,终端才能立即报 state 错,而不是傻等到超时。
469
+ this.complete(payload);
470
+ return;
471
+ }
472
+ if (payload.error) {
473
+ res.statusCode = 400;
474
+ res.setHeader('content-type', 'text/html; charset=utf-8');
475
+ res.end(renderOAuthCallbackHTML('authorizationFailed', locale));
476
+ this.complete(payload);
477
+ return;
478
+ }
479
+ if (!payload.code) {
480
+ res.statusCode = 400;
481
+ res.setHeader('content-type', 'text/html; charset=utf-8');
482
+ res.end(renderOAuthCallbackHTML('missingCode', locale));
483
+ this.complete(payload);
484
+ return;
485
+ }
486
+ res.statusCode = 200;
487
+ res.setHeader('content-type', 'text/html; charset=utf-8');
488
+ res.end(renderOAuthCallbackHTML('success', locale));
489
+ this.complete(payload);
490
+ }
491
+ }
492
+ export async function createOAuthLoopbackListener(expectedState, timeoutMs) {
493
+ return OAuthLoopbackServer.create(expectedState, timeoutMs);
494
+ }
495
+ export class OAuthCLIClient {
496
+ backendUrl;
497
+ fetchImpl;
498
+ constructor(backendUrl, fetchImpl = fetch) {
499
+ this.backendUrl = backendUrl;
500
+ this.fetchImpl = fetchImpl;
501
+ }
502
+ async start(redirectURI, state, codeChallenge) {
503
+ const response = await this.fetchImpl(new URL(`${API_V1}/auth/cli/start`, this.backendUrl), {
504
+ method: 'POST',
505
+ headers: { 'Content-Type': 'application/json' },
506
+ body: JSON.stringify({
507
+ redirect_uri: redirectURI,
508
+ state,
509
+ code_challenge: codeChallenge,
510
+ }),
511
+ });
512
+ const json = await readJsonResponse(response);
513
+ if (!response.ok) {
514
+ throw new Error(`OAuth start failed: ${json?.error?.message ?? `HTTP ${response.status}`}`);
515
+ }
516
+ if (!json.authorize_url || !json.state || !json.flow_id) {
517
+ throw new Error('OAuth start response missing authorize_url/state/flow_id');
518
+ }
519
+ return json;
520
+ }
521
+ async exchange(state, codeVerifier, code) {
522
+ const response = await this.fetchImpl(new URL(`${API_V1}/auth/cli/exchange`, this.backendUrl), {
523
+ method: 'POST',
524
+ headers: { 'Content-Type': 'application/json' },
525
+ body: JSON.stringify({
526
+ code,
527
+ state,
528
+ code_verifier: codeVerifier,
529
+ }),
530
+ });
531
+ const json = await readJsonResponse(response);
532
+ if (!response.ok) {
533
+ throw new Error(`OAuth exchange failed: ${json?.error?.message ?? `HTTP ${response.status}`}`);
534
+ }
535
+ if (!json.access_token) {
536
+ throw new Error('OAuth exchange response missing access_token');
537
+ }
538
+ return json;
539
+ }
540
+ }
541
+ export async function startOAuthCLI(backendUrl, redirectURI, state, codeChallenge, fetchImpl) {
542
+ return new OAuthCLIClient(backendUrl, fetchImpl).start(redirectURI, state, codeChallenge);
543
+ }
544
+ export async function exchangeOAuthCode(backendUrl, state, codeVerifier, code, fetchImpl) {
545
+ return new OAuthCLIClient(backendUrl, fetchImpl).exchange(state, codeVerifier, code);
546
+ }
547
+ export class OAuthBrowserLoginFlow {
548
+ params;
549
+ constructor(params) {
550
+ this.params = params;
551
+ }
552
+ async run() {
553
+ const fetchImpl = this.params.fetchImpl ?? fetch;
554
+ const log = this.params.log ?? ((message) => console.log(message));
555
+ const state = this.params.state ?? generateOAuthState();
556
+ const codeVerifier = this.params.codeVerifier ?? generateCodeVerifier();
557
+ const codeChallenge = buildS256CodeChallenge(codeVerifier);
558
+ const loopbackFactory = this.params.loopbackFactory ?? createOAuthLoopbackListener;
559
+ const listener = await loopbackFactory(state, this.params.timeoutMs);
560
+ const client = new OAuthCLIClient(this.params.backendUrl, fetchImpl);
561
+ try {
562
+ const startResp = await client.start(listener.redirectURI, state, codeChallenge);
563
+ if (this.params.noOpen) {
564
+ log('Open this URL in your browser to continue login:');
565
+ log(startResp.authorize_url);
566
+ }
567
+ else {
568
+ const openUrl = this.params.openUrl ?? openBrowserURL;
569
+ const opened = await openUrl(startResp.authorize_url);
570
+ if (!opened) {
571
+ log('Failed to open browser automatically. Open this URL manually:');
572
+ log(startResp.authorize_url);
573
+ }
574
+ }
575
+ const callback = await listener.waitForCallback();
576
+ if (callback.state !== state) {
577
+ throw new Error('State mismatch from OAuth callback');
578
+ }
579
+ if (callback.error) {
580
+ throw new Error(normalizeCallbackError(callback));
581
+ }
582
+ if (!callback.code) {
583
+ throw new Error('Missing authorization code from OAuth callback');
584
+ }
585
+ return client.exchange(state, codeVerifier, callback.code);
586
+ }
587
+ finally {
588
+ await listener.close();
589
+ }
590
+ }
591
+ }
592
+ export async function runOAuthBrowserLogin(params) {
593
+ return new OAuthBrowserLoginFlow(params).run();
594
+ }
@@ -0,0 +1,23 @@
1
+ import type { McpTarget, SyncScope, SyncTarget } from './types.js';
2
+ interface PromptChoice {
3
+ value: string;
4
+ label: string;
5
+ hint?: string;
6
+ }
7
+ export type SkillAddSelectionMode = 'all' | 'custom' | 'none';
8
+ export declare class CliPrompts {
9
+ line(question: string): Promise<string>;
10
+ yesNo(question: string, defaultValue: boolean): Promise<boolean>;
11
+ pickMcpTargets(): Promise<McpTarget[]>;
12
+ pickSyncScope(): Promise<SyncScope>;
13
+ pickSyncTargets(scope: SyncScope): Promise<SyncTarget[]>;
14
+ pickSyncableSkills(skills: PromptChoice[]): Promise<string[]>;
15
+ pickSyncableSkillAddMode(): Promise<SkillAddSelectionMode>;
16
+ pickSyncableWorkflows(workflows: PromptChoice[]): Promise<string[]>;
17
+ pickLocalManagedSkills(skills: PromptChoice[]): Promise<string[]>;
18
+ private pickMany;
19
+ private defaultMcpTargets;
20
+ private defaultSyncTargets;
21
+ private uniqueTargets;
22
+ }
23
+ export {};