@danainnovations/cortex-mcp 1.0.60 → 1.0.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -9,74 +9,1368 @@ import {
9
9
  DEFAULT_SERVER_URL,
10
10
  MCP_NAMES,
11
11
  PROTOCOL_VERSION
12
- } from "./chunk-WDD2YERC.js";
12
+ } from "./chunk-JJURHODD.js";
13
13
  import {
14
+ getAntigravityConfigPath,
14
15
  getClaudeDesktopConfigPath,
15
16
  getCodexConfigPath,
16
17
  getCursorConfigPath,
17
18
  getHomeDir,
19
+ getPlatform,
20
+ getVSCodeMcpConfigPath,
18
21
  openBrowser
19
- } from "./chunk-DIHPZXVU.js";
22
+ } from "./chunk-AUMRZ53L.js";
20
23
 
21
24
  // bin/cli.ts
22
25
  import { Command } from "commander";
23
26
 
24
- // src/cli/setup.ts
25
- import * as readline2 from "readline";
27
+ // src/wizard/server.ts
28
+ import * as http from "http";
26
29
 
27
- // src/config/storage.ts
28
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
29
- import { join } from "path";
30
- function getConfigDir() {
31
- return join(getHomeDir(), CONFIG_DIR_NAME);
32
- }
33
- function getConfigPath() {
34
- return join(getConfigDir(), CONFIG_FILE_NAME);
35
- }
36
- function readConfig() {
37
- const path = getConfigPath();
38
- if (!existsSync(path)) return null;
39
- try {
40
- const raw = readFileSync(path, "utf-8");
41
- return JSON.parse(raw);
42
- } catch {
43
- return null;
44
- }
45
- }
46
- function writeConfig(config) {
47
- const dir = getConfigDir();
48
- if (!existsSync(dir)) {
49
- mkdirSync(dir, { recursive: true, mode: 448 });
50
- }
51
- const path = getConfigPath();
52
- writeFileSync(path, JSON.stringify(config, null, 2) + "\n", {
53
- mode: 384
54
- });
55
- }
56
- function createConfig(apiKey, mcps) {
57
- return {
58
- version: 1,
59
- server: DEFAULT_SERVER_URL,
60
- apiKey,
61
- mcps,
62
- configuredClients: []
63
- };
30
+ // src/wizard/html.ts
31
+ function getWizardHtml() {
32
+ return `<!DOCTYPE html>
33
+ <html lang="en">
34
+ <head>
35
+ <meta charset="UTF-8">
36
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
37
+ <title>Cortex MCP Setup</title>
38
+ <link rel="preconnect" href="https://fonts.googleapis.com">
39
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
40
+ <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600&display=swap" rel="stylesheet">
41
+ <style>
42
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
43
+
44
+ body {
45
+ font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
46
+ background: #1a1f24;
47
+ color: #fff;
48
+ min-height: 100vh;
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ line-height: 1.6;
53
+ -webkit-font-smoothing: antialiased;
54
+ -moz-osx-font-smoothing: grayscale;
55
+ overflow-x: hidden;
56
+ }
57
+
58
+ /* \u2500\u2500 Blue Orb Background \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
59
+ .orb {
60
+ position: fixed;
61
+ border-radius: 50%;
62
+ pointer-events: none;
63
+ z-index: 0;
64
+ will-change: transform;
65
+ transition: transform 0.8s cubic-bezier(0.16, 1, 0.3, 1);
66
+ }
67
+ .orb-primary {
68
+ width: 700px;
69
+ height: 700px;
70
+ background: radial-gradient(circle, rgba(0, 163, 225, 0.07) 0%, transparent 70%);
71
+ filter: blur(100px);
72
+ top: 50%;
73
+ left: 50%;
74
+ transform: translate(-50%, -50%);
75
+ }
76
+ .orb-secondary {
77
+ width: 500px;
78
+ height: 500px;
79
+ background: radial-gradient(circle, rgba(120, 80, 220, 0.05) 0%, transparent 70%);
80
+ filter: blur(80px);
81
+ top: 50%;
82
+ left: 50%;
83
+ transform: translate(-50%, -50%);
84
+ }
85
+
86
+ #app {
87
+ width: 100%;
88
+ max-width: 900px;
89
+ padding: 24px;
90
+ position: relative;
91
+ z-index: 1;
92
+ }
93
+
94
+ /* \u2500\u2500 Persistent Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
95
+ .wizard-header {
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: space-between;
99
+ padding: 0 8px 24px;
100
+ }
101
+ .wizard-header-left {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 12px;
105
+ }
106
+ .header-logo {
107
+ height: 18px;
108
+ width: auto;
109
+ opacity: 0.85;
110
+ }
111
+ .header-divider {
112
+ width: 1px;
113
+ height: 16px;
114
+ background: rgba(255, 255, 255, 0.15);
115
+ }
116
+ .header-product {
117
+ font-size: 13px;
118
+ font-weight: 500;
119
+ color: #00A3E1;
120
+ letter-spacing: 0.08em;
121
+ text-transform: uppercase;
122
+ }
123
+ .header-steps {
124
+ display: flex;
125
+ align-items: center;
126
+ gap: 6px;
127
+ }
128
+ .header-dot {
129
+ width: 8px;
130
+ height: 8px;
131
+ border-radius: 50%;
132
+ background: rgba(255, 255, 255, 0.1);
133
+ transition: all 0.4s ease;
134
+ }
135
+ .header-dot.done { background: #00A3E1; }
136
+ .header-dot.current { background: #00A3E1; opacity: 0.5; }
137
+
138
+ /* \u2500\u2500 Steps \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
139
+ .step { display: none; }
140
+ .step.active { display: block; }
141
+ .step.active.slide-forward { animation: slideInRight 0.35s cubic-bezier(0.16, 1, 0.3, 1); }
142
+ .step.active.slide-backward { animation: slideInLeft 0.35s cubic-bezier(0.16, 1, 0.3, 1); }
143
+
144
+ @keyframes slideInRight {
145
+ from { opacity: 0; transform: translateX(40px); }
146
+ to { opacity: 1; transform: translateX(0); }
147
+ }
148
+ @keyframes slideInLeft {
149
+ from { opacity: 0; transform: translateX(-40px); }
150
+ to { opacity: 1; transform: translateX(0); }
151
+ }
152
+
153
+ /* \u2500\u2500 Liquid Glass Card \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
154
+ .card {
155
+ background: rgba(255, 255, 255, 0.03);
156
+ backdrop-filter: blur(20px);
157
+ -webkit-backdrop-filter: blur(20px);
158
+ border: 1px solid rgba(255, 255, 255, 0.08);
159
+ border-radius: 20px;
160
+ padding: 40px;
161
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
162
+ }
163
+
164
+ /* \u2500\u2500 Typography \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
165
+ h1 {
166
+ font-size: 26px;
167
+ font-weight: 300;
168
+ letter-spacing: -0.02em;
169
+ color: #fff;
170
+ margin-bottom: 8px;
171
+ }
172
+ h2 {
173
+ font-size: 20px;
174
+ font-weight: 300;
175
+ letter-spacing: -0.02em;
176
+ color: #fff;
177
+ margin-bottom: 6px;
178
+ }
179
+ .subtitle {
180
+ color: #8f999f;
181
+ font-size: 14px;
182
+ font-weight: 400;
183
+ margin-bottom: 28px;
184
+ line-height: 1.6;
185
+ }
186
+
187
+ /* \u2500\u2500 Welcome \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
188
+ .welcome-tagline {
189
+ font-size: 15px;
190
+ color: #00A3E1;
191
+ font-weight: 400;
192
+ margin-bottom: 8px;
193
+ }
194
+ .welcome-hero { padding: 16px 0 28px; }
195
+ .welcome-footer {
196
+ font-size: 11px;
197
+ color: #6b7780;
198
+ margin-top: 20px;
199
+ letter-spacing: 0.04em;
200
+ }
201
+
202
+ /* \u2500\u2500 Buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
203
+ .btn {
204
+ display: inline-flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ gap: 8px;
208
+ padding: 11px 24px;
209
+ border-radius: 12px;
210
+ font-family: 'Montserrat', sans-serif;
211
+ font-size: 13px;
212
+ font-weight: 500;
213
+ letter-spacing: 0.02em;
214
+ text-transform: uppercase;
215
+ cursor: pointer;
216
+ border: none;
217
+ transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
218
+ text-decoration: none;
219
+ }
220
+ .btn:disabled { opacity: 0.4; cursor: not-allowed; }
221
+
222
+ .btn-primary {
223
+ background: #333F48;
224
+ color: #fff;
225
+ }
226
+ .btn-primary:hover:not(:disabled) {
227
+ background: #3a444c;
228
+ box-shadow: 0 0 20px rgba(0, 163, 225, 0.15);
229
+ }
230
+ .btn-primary:active:not(:disabled) { transform: scale(0.98); }
231
+
232
+ .btn-accent {
233
+ background: #00A3E1;
234
+ color: #fff;
235
+ }
236
+ .btn-accent:hover:not(:disabled) {
237
+ background: #0093cb;
238
+ box-shadow: 0 0 24px rgba(0, 163, 225, 0.3);
239
+ }
240
+ .btn-accent:active:not(:disabled) { transform: scale(0.98); }
241
+
242
+ .btn-secondary {
243
+ background: rgba(255, 255, 255, 0.03);
244
+ color: #D9D9D6;
245
+ border: 1px solid rgba(255, 255, 255, 0.08);
246
+ }
247
+ .btn-secondary:hover:not(:disabled) {
248
+ background: rgba(255, 255, 255, 0.06);
249
+ border-color: rgba(255, 255, 255, 0.12);
250
+ }
251
+
252
+ .btn-sm { padding: 7px 16px; font-size: 12px; }
253
+ .btn-block { width: 100%; }
254
+
255
+ .btn-row {
256
+ display: flex;
257
+ gap: 10px;
258
+ margin-top: 28px;
259
+ }
260
+ .btn-row .btn { flex: 1; }
261
+
262
+ /* \u2500\u2500 Spinner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
263
+ .spinner {
264
+ display: inline-block;
265
+ width: 14px;
266
+ height: 14px;
267
+ border: 2px solid rgba(255, 255, 255, 0.1);
268
+ border-top-color: #00A3E1;
269
+ border-radius: 50%;
270
+ animation: spin 0.7s linear infinite;
271
+ }
272
+ @keyframes spin { to { transform: rotate(360deg); } }
273
+
274
+ /* \u2500\u2500 Status Messages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
275
+ .status {
276
+ display: flex;
277
+ align-items: center;
278
+ gap: 10px;
279
+ padding: 12px 16px;
280
+ border-radius: 12px;
281
+ font-size: 13px;
282
+ font-weight: 400;
283
+ margin-top: 16px;
284
+ }
285
+ .status-info {
286
+ background: rgba(0, 163, 225, 0.08);
287
+ color: #7dd3fc;
288
+ border: 1px solid rgba(0, 163, 225, 0.15);
289
+ }
290
+ .status-success {
291
+ background: rgba(76, 175, 80, 0.1);
292
+ color: #4CAF50;
293
+ border: 1px solid rgba(76, 175, 80, 0.2);
294
+ }
295
+ .status-error {
296
+ background: rgba(239, 83, 80, 0.1);
297
+ color: #EF5350;
298
+ border: 1px solid rgba(239, 83, 80, 0.2);
299
+ }
300
+ .status-waiting {
301
+ background: rgba(0, 163, 225, 0.06);
302
+ color: #8f999f;
303
+ border: 1px solid rgba(255, 255, 255, 0.06);
304
+ }
305
+
306
+ /* \u2500\u2500 Auth Phases \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
307
+ .auth-phases {
308
+ display: flex;
309
+ flex-direction: column;
310
+ gap: 4px;
311
+ margin-top: 16px;
312
+ }
313
+ .auth-phase {
314
+ display: flex;
315
+ align-items: center;
316
+ gap: 10px;
317
+ font-size: 13px;
318
+ color: #6b7780;
319
+ padding: 10px 14px;
320
+ border-radius: 10px;
321
+ transition: all 0.3s ease;
322
+ }
323
+ .auth-phase.active {
324
+ color: #7dd3fc;
325
+ background: rgba(0, 163, 225, 0.06);
326
+ }
327
+ .auth-phase.done { color: #4CAF50; }
328
+ .auth-phase.pending { opacity: 0.35; }
329
+ .phase-icon { font-size: 12px; flex-shrink: 0; width: 14px; text-align: center; }
330
+
331
+ /* \u2500\u2500 MCP Section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
332
+ .mcp-section { margin-bottom: 16px; }
333
+ .mcp-section-title {
334
+ font-size: 11px;
335
+ font-weight: 500;
336
+ color: #8f999f;
337
+ text-transform: uppercase;
338
+ letter-spacing: 0.08em;
339
+ margin-bottom: 8px;
340
+ }
341
+
342
+ /* \u2500\u2500 MCP List \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
343
+ .mcp-list {
344
+ display: grid;
345
+ grid-template-columns: repeat(2, 1fr);
346
+ gap: 6px;
347
+ margin-bottom: 8px;
348
+ }
349
+
350
+ .mcp-item {
351
+ display: flex;
352
+ align-items: center;
353
+ gap: 10px;
354
+ padding: 11px 14px;
355
+ border-radius: 12px;
356
+ border: 1px solid rgba(255, 255, 255, 0.06);
357
+ cursor: pointer;
358
+ transition: all 0.15s ease;
359
+ user-select: none;
360
+ position: relative;
361
+ }
362
+ .mcp-item:hover { border-color: rgba(0, 163, 225, 0.3); }
363
+ .mcp-tooltip {
364
+ display: none;
365
+ position: absolute;
366
+ bottom: calc(100% + 8px);
367
+ left: 50%;
368
+ transform: translateX(-50%);
369
+ background: #2a3038;
370
+ color: #d0d5da;
371
+ font-size: 11px;
372
+ font-weight: 400;
373
+ padding: 6px 10px;
374
+ border-radius: 6px;
375
+ white-space: nowrap;
376
+ pointer-events: none;
377
+ z-index: 10;
378
+ border: 1px solid rgba(255,255,255,0.1);
379
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
380
+ }
381
+ .mcp-tooltip::after {
382
+ content: '';
383
+ position: absolute;
384
+ top: 100%;
385
+ left: 50%;
386
+ transform: translateX(-50%);
387
+ border: 5px solid transparent;
388
+ border-top-color: #2a3038;
389
+ }
390
+ .mcp-item:hover .mcp-tooltip { display: block; }
391
+ .mcp-item.checked {
392
+ border-color: rgba(0, 163, 225, 0.4);
393
+ background: rgba(0, 163, 225, 0.06);
394
+ }
395
+ .mcp-item input[type="checkbox"] { display: none; }
396
+ .mcp-item.required { cursor: default; }
397
+ .mcp-item.required .mcp-check { background: #00A3E1; border-color: #00A3E1; }
398
+ .mcp-item.required .mcp-check-icon { display: block; }
399
+ .mcp-required-label {
400
+ position: absolute;
401
+ top: 6px;
402
+ right: 10px;
403
+ font-size: 9px;
404
+ font-weight: 500;
405
+ color: #8f999f;
406
+ text-transform: uppercase;
407
+ letter-spacing: 0.05em;
408
+ }
409
+
410
+ .mcp-icon {
411
+ width: 28px;
412
+ height: 28px;
413
+ display: flex;
414
+ align-items: center;
415
+ justify-content: center;
416
+ border-radius: 7px;
417
+ background: rgba(255, 255, 255, 0.04);
418
+ flex-shrink: 0;
419
+ }
420
+ .mcp-icon svg { width: 16px; height: 16px; fill: #8f999f; }
421
+ .mcp-item.checked .mcp-icon svg { fill: #00A3E1; }
422
+
423
+ .mcp-check {
424
+ width: 18px;
425
+ height: 18px;
426
+ border: 1.5px solid rgba(255, 255, 255, 0.2);
427
+ border-radius: 5px;
428
+ display: flex;
429
+ align-items: center;
430
+ justify-content: center;
431
+ flex-shrink: 0;
432
+ transition: all 0.15s;
433
+ }
434
+ .mcp-item.checked .mcp-check,
435
+ .client-item.checked .mcp-check {
436
+ background: #00A3E1;
437
+ border-color: #00A3E1;
438
+ }
439
+ .mcp-check-icon { display: none; color: #fff; font-size: 11px; font-weight: 600; }
440
+ .mcp-item.checked .mcp-check-icon,
441
+ .client-item.checked .mcp-check-icon { display: block; }
442
+
443
+ .mcp-info { flex: 1; min-width: 0; }
444
+ .mcp-name { font-size: 13px; font-weight: 500; color: #fff; }
445
+ .mcp-desc { font-size: 11px; color: #8f999f; display: block; margin-top: 2px; font-weight: 400; }
446
+ .mcp-badge {
447
+ font-size: 10px;
448
+ font-weight: 500;
449
+ padding: 1px 7px;
450
+ border-radius: 6px;
451
+ margin-left: 8px;
452
+ letter-spacing: 0.03em;
453
+ text-transform: uppercase;
454
+ }
455
+ .mcp-badge.company {
456
+ background: rgba(76, 175, 80, 0.12);
457
+ color: #4CAF50;
458
+ border: 1px solid rgba(76, 175, 80, 0.2);
459
+ }
460
+ .mcp-badge.personal {
461
+ background: rgba(255, 183, 77, 0.12);
462
+ color: #FFB74D;
463
+ border: 1px solid rgba(255, 183, 77, 0.2);
464
+ }
465
+
466
+ /* \u2500\u2500 Client List \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
467
+ .client-list {
468
+ display: flex;
469
+ flex-direction: column;
470
+ gap: 8px;
471
+ margin-bottom: 8px;
472
+ }
473
+ .client-item {
474
+ display: flex;
475
+ align-items: center;
476
+ gap: 12px;
477
+ padding: 14px 16px;
478
+ border-radius: 12px;
479
+ border: 1px solid rgba(255, 255, 255, 0.06);
480
+ cursor: pointer;
481
+ user-select: none;
482
+ transition: all 0.15s ease;
483
+ }
484
+ .client-item:hover { border-color: rgba(0, 163, 225, 0.3); }
485
+ .client-item.checked {
486
+ border-color: rgba(0, 163, 225, 0.4);
487
+ background: rgba(0, 163, 225, 0.06);
488
+ }
489
+ .client-item.disabled {
490
+ opacity: 0.3;
491
+ cursor: not-allowed;
492
+ }
493
+ .client-item.disabled:hover { border-color: rgba(255, 255, 255, 0.06); }
494
+
495
+ .client-icon {
496
+ width: 28px;
497
+ height: 28px;
498
+ display: flex;
499
+ align-items: center;
500
+ justify-content: center;
501
+ border-radius: 7px;
502
+ background: rgba(255, 255, 255, 0.04);
503
+ flex-shrink: 0;
504
+ }
505
+ .client-icon svg { width: 16px; height: 16px; fill: #8f999f; }
506
+ .client-item.checked .client-icon svg { fill: #00A3E1; }
507
+
508
+ .client-name { font-size: 13px; font-weight: 500; color: #fff; }
509
+ .client-status {
510
+ font-size: 11px;
511
+ font-weight: 500;
512
+ color: #8f999f;
513
+ margin-left: auto;
514
+ text-transform: uppercase;
515
+ letter-spacing: 0.05em;
516
+ }
517
+
518
+ /* \u2500\u2500 Connection List \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
519
+ .conn-list {
520
+ display: flex;
521
+ flex-direction: column;
522
+ gap: 8px;
523
+ margin-bottom: 8px;
524
+ }
525
+ .conn-item {
526
+ display: flex;
527
+ align-items: center;
528
+ gap: 12px;
529
+ padding: 14px 16px;
530
+ border-radius: 12px;
531
+ border: 1px solid rgba(255, 255, 255, 0.06);
532
+ transition: all 0.2s ease;
533
+ }
534
+ .conn-item.connected {
535
+ border-left: 3px solid #4CAF50;
536
+ background: rgba(76, 175, 80, 0.04);
537
+ }
538
+ .conn-name { font-size: 13px; font-weight: 500; color: #fff; flex-shrink: 0; }
539
+ .conn-status { font-size: 12px; color: #8f999f; flex: 1; text-align: right; font-weight: 400; }
540
+ .conn-status.connected { color: #4CAF50; }
541
+
542
+ /* \u2500\u2500 Done / Success \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
543
+ .success-circle {
544
+ width: 64px;
545
+ height: 64px;
546
+ border-radius: 50%;
547
+ background: rgba(0, 163, 225, 0.1);
548
+ border: 2px solid #00A3E1;
549
+ display: flex;
550
+ align-items: center;
551
+ justify-content: center;
552
+ margin: 0 auto 24px;
553
+ animation: successPop 0.5s cubic-bezier(0.16, 1, 0.3, 1);
554
+ }
555
+ .success-check {
556
+ font-size: 28px;
557
+ color: #00A3E1;
558
+ animation: checkDraw 0.3s ease 0.2s both;
559
+ }
560
+ @keyframes successPop {
561
+ from { transform: scale(0.5); opacity: 0; }
562
+ to { transform: scale(1); opacity: 1; }
563
+ }
564
+ @keyframes checkDraw {
565
+ from { opacity: 0; transform: scale(0.5); }
566
+ to { opacity: 1; transform: scale(1); }
567
+ }
568
+
569
+ .next-steps {
570
+ text-align: left;
571
+ margin-top: 24px;
572
+ padding: 20px;
573
+ border-radius: 12px;
574
+ background: rgba(255, 255, 255, 0.02);
575
+ border: 1px solid rgba(255, 255, 255, 0.06);
576
+ }
577
+ .next-steps h3 {
578
+ font-size: 11px;
579
+ font-weight: 500;
580
+ color: #8f999f;
581
+ margin-bottom: 12px;
582
+ text-transform: uppercase;
583
+ letter-spacing: 0.08em;
584
+ }
585
+ .next-step-item {
586
+ font-size: 13px;
587
+ color: #D9D9D6;
588
+ padding: 4px 0;
589
+ font-weight: 400;
590
+ }
591
+ .next-step-item code {
592
+ font-family: 'SF Mono', 'Fira Code', monospace;
593
+ font-size: 11px;
594
+ background: rgba(0, 163, 225, 0.1);
595
+ color: #7dd3fc;
596
+ padding: 2px 6px;
597
+ border-radius: 4px;
598
+ }
599
+
600
+ /* \u2500\u2500 Summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
601
+ .summary-section { margin-bottom: 24px; }
602
+ .summary-section h3 {
603
+ font-size: 11px;
604
+ font-weight: 500;
605
+ color: #8f999f;
606
+ margin-bottom: 10px;
607
+ text-transform: uppercase;
608
+ letter-spacing: 0.08em;
609
+ }
610
+ .summary-item {
611
+ font-size: 13px;
612
+ color: #D9D9D6;
613
+ padding: 3px 0;
614
+ font-weight: 400;
615
+ }
616
+ .check-icon { color: #4CAF50; margin-right: 8px; }
617
+
618
+ /* \u2500\u2500 Utility \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
619
+ .text-center { text-align: center; }
620
+ .mt-4 { margin-top: 16px; }
621
+ .mt-2 { margin-top: 8px; }
622
+ .text-sm { font-size: 12px; font-weight: 400; }
623
+ .text-muted { color: #6b7780; }
624
+ .hidden { display: none !important; }
625
+
626
+ /* \u2500\u2500 Stdio Snippet \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
627
+ .stdio-snippet {
628
+ background: rgba(0, 0, 0, 0.3);
629
+ border: 1px solid rgba(255, 255, 255, 0.1);
630
+ border-radius: 10px;
631
+ padding: 16px;
632
+ margin-top: 12px;
633
+ position: relative;
634
+ }
635
+ .stdio-snippet pre {
636
+ font-family: 'SF Mono', 'Fira Code', monospace;
637
+ font-size: 11px;
638
+ color: #7dd3fc;
639
+ white-space: pre-wrap;
640
+ word-break: break-all;
641
+ margin: 0;
642
+ line-height: 1.5;
643
+ }
644
+ .stdio-snippet .copy-btn {
645
+ position: absolute;
646
+ top: 8px;
647
+ right: 8px;
648
+ padding: 4px 10px;
649
+ font-size: 11px;
650
+ border-radius: 6px;
651
+ background: rgba(0, 163, 225, 0.15);
652
+ color: #00A3E1;
653
+ border: 1px solid rgba(0, 163, 225, 0.3);
654
+ cursor: pointer;
655
+ font-family: 'Montserrat', sans-serif;
656
+ }
657
+ .stdio-snippet .copy-btn:hover {
658
+ background: rgba(0, 163, 225, 0.25);
659
+ }
660
+
661
+ /* \u2500\u2500 Mobile \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
662
+ @media (max-width: 640px) {
663
+ #app { padding: 16px; }
664
+ .card { padding: 24px 20px; border-radius: 16px; }
665
+ .mcp-list { grid-template-columns: 1fr; }
666
+ h1 { font-size: 22px; }
667
+ h2 { font-size: 18px; }
668
+ .wizard-header { flex-direction: column; gap: 12px; align-items: flex-start; }
669
+ .orb-primary { width: 400px; height: 400px; }
670
+ .orb-secondary { width: 300px; height: 300px; }
671
+ }
672
+ </style>
673
+ </head>
674
+ <body>
675
+ <!-- Blue Orb Background -->
676
+ <div class="orb orb-primary" id="orb-primary"></div>
677
+ <div class="orb orb-secondary" id="orb-secondary"></div>
678
+
679
+ <div id="app">
680
+
681
+ <!-- Persistent Header -->
682
+ <header class="wizard-header">
683
+ <div class="wizard-header-left">
684
+ <img class="header-logo" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAA/CAMAAAD5XG5xAAAANlBMVEVMaXHa2tfZ2dfa2tfQ1tbZ2dbZ2dbZ2dba2tfZ2dYAo+IApeQApeQApuYApuXn5+Tf39wArO3XFnpPAAAAD3RSTlMAv3SNEzKm+9xV/Gg11aD4UYCxAAAACXBIWXMAAC4jAAAuIwF4pT92AAAIgElEQVR42u2c6ZLbIAyAw2nYTRby/i9bLjs+0GHXyXam1o9OJ2ts0IcQCMHtdskll1xyySWXXHLJJf+pGO+llN4bvauYNl6qJHJvwTPknuTjH20tlu9ssfbWDTFkiYMT0nDLyalcCKyCRiTBn/LpCctq6vfz6+uLRSS/U+DvlPmzdLdVYmpwURX8UisIUbBaXX77KOl7g/Acm7LDvByroMktcahqZIghcoDoRwLy/OEAye8MAn3EpkccyTVVbdViC3UvF0ZyXYlgfYyrWo1F6pdS7Skk2saxXHl/LRki3v9NeUjgykuPaK6B8EzE59oFhT2iwhBxIL4pahpL8Ba7GbmeRAHVNZZvDM4J4fII1NSMDxt+CLVuMRWzVkwFQ1QUEFw1CUhkAXkUICwT8aWywaMWggNJHbAqKo9UdtHiQUJAUBtxMI8Y0/BfdKC1Vy7W6juPGnjtHXL0bNpMBZHR2rTe4f8eSDOQr4fmAhkGcxhIGkcKDme9njxvanEEW5yARCdhUR4eQlaqN2NngNSmRahGZDZjbOHkDAUEUQ0XyKMBeX6zgUTEe+FA/FAbtnbiXgSoxQlIsLe9kjXb4WuA3+c8Qm/s9LUjgfo2FQemGiaQe/bo34nKk2EiGUjuK4j3QoG0cURpuMX+FCDZ2cWu3mWEJ0O2VEEBQ23AuqJpBoKohgnkJ6Mo4xbDRKqFRMx7YUBM4QHYfW3xtj2HgJR3GWBOYeD5SKqevGF/hvRdgEiHqYYH5F4HK11MhAMkujLsBLkfiC71RWw69v56BEj+EthVNTaFhHmMRBQIJBoTEcfOA5IMpLhznokUC9Hl32h2A8kjNOZ+kko6fz0CJKsnyP0QiSm9hRtuKom81oAcDQvIffTmOv/nwQJiSleBNAsD8QGfheRHzO0cIAGfgULrNmLRW5kJBAimGhaQnylqkv/3vPOAtDnMPiClNXu1dBCI3w+kVG8gYlbwkDQCQVTDAXJ/rQjvnMXhCAQxbxBI6YH756/HLCTuHrJK9RRnstCtzQQEVg0HyM8srPjDCDGOQNL3oSZDQFg98DQgw274ef1Ju9w6u9UIENiKGED03Co4JjIBaVMSwwfiDxrIsWnvbvrFpixvPt3riC8goGNnAFnG3Rkm8gICei8ISG5JNJ8CUmon9M4Cnrfi7LmIGRBINTQQ/VjYxJ2Ows+AQN4LAJJHrChunwJSxo0g+B1ARHxGvmhH58E5EEA1NJDv1cTqQZrIHAjgvQAgPnJ85mlA2qp7UEwj0QP3K0AMYAGkrxoayHp1/k2ayBxIC6hKHhAZDs15DwMpvTSH9F9R5XOmZWU25nEgfcdOAtkuzh9PIgq/ANJ37AAQJLj0HiA1cls2mqwkd+19ILZ5lqYuCSB1d3DVXhLINnxFxk+WQLreCwDCHaNPA1LiutMGsRMKtRTJnnKY/ti7AtJTDQXkvtV+9vKoiayA9LwXAKT4wttBIEPahgVEEMkKcdq1jwOSPKL4QAYWkI5qKCCP53OzBfKdfsNMZA2k470QIOI4kAgIkU9Rdl+nPXkkeUSxR1RgzbkBslUNBeQ7yXpKpXs/IkA6jv09QMAMB8dMOCJSKc63kK1jZyc57AzaLTvSxrG/BUgEc4AcK10u572FMXlE8udOfwNks2L/DJCN93qHD4nCAyLZM2kjRfMoveWQPHmW1VPNh4Csvde/M8vqZjICiWX8dSuwoOoBWanmU0BW3utfWYdgWS9bb2HIvMNFQzQPyFI1nwKyCsUDQBR7THgrEHDTr0wduLEsIri4Dql5Goj2uJg9QJaOHYtl2d8HUrvPtg2W2WGgsa0PZLFix4CkpGlczB4gC+8FRXuHo07kZCDVf8vupq/gjVg97QBA5qpBgGgqgRnSAABk7r2g/RBxdMw6G0h/3lp3/A0nKtxtHgRkphoEiAzI4jci2bsQkJn3goBIZhd8OxDdf6EKHLeugA1DGMhLNTAQjYeHkLx6CMhsxY7uqR8JwJ8NBHihoZLXJ7V3B14QyMuxw0B8xawhceBwDwJ5OXYw60RRaXK/ayHVOxD1E5CBIEAmxw4DoRZpEvwsDGTSN5yXNRzbNDy2p+53ZzVqPFX61cbbTiBjMRCIp3bH4BkRAmT0XnDmYvVcHj+geE4qaVKB37nUbvXDPiWRFFoMSFWNBYEIcsUIui4MSPNeCs/txc8Yxc7RjQNAis7lviyFcREewBNvMiIZwyiQ6j/d0Nc7I2xjoMA2BqQuucpnHfYA3Hf7XmY/EBOQnBMLZygi53Wm0xL2dgBIC8UDQDhRJWjdigJpjh3ZpKgPAH3XNHX8PZDxnFYn56SoFQ5gtENDEjpPBFcEB9Ice/fLrCxLE/uLBhxI3XfDdo1kOzxpOpHYqkR7yvmQepIxnTZRpqdWj2dGxM2Ru/GMob0dBNJU0wPCyyAUfc9HAGm5N8jGh6wNW5+qzIHxogl10ixLq/FQdMpuMGUyb9LZ0kAd69b1FG45lNqO75rx+G7EzkVTQJoD3QJhxpmBjEkKSF1yYjtR7Rj47NxxarFtmuqeiy5O2GKisdPwZU+9rHfr7nqMePqVHM+prw+4o6epaSDVsW+BcHNYRTe0QwEZHTueLzUpKjU4HVNvm6sR8KbkOfUAVChfrDFuvY8RoYirtRrJmKsyiyTFQCRBkkCAxHntmBGlEv20Xa9syOOvzJscli2GNEUFQuEKaS+G8LrDI+akOckIFdRclfklKdFROanlrhNPTMbD9q4TRZebJhydvieRDjn7gqOSDsSwuhUmwtfPuECJQa8DEg18GriU56b6lluEYrv5QzDKmTx2Eq5ZdoZXlX7jJSD7fO2F732WKJ4+oTgpB9x7k5SlRFP3QJmy6bYzjJavj5LyV67L+h3RB28Wu+SSSy655JJLLrnkkv9a/gAogKu+LRoCQQAAAABJRU5ErkJggg==" alt="Sonance">
685
+ <span class="header-divider"></span>
686
+ <span class="header-product">Cortex</span>
687
+ </div>
688
+ <div class="header-steps" id="header-steps"></div>
689
+ </header>
690
+
691
+ <!-- Step 1: Welcome -->
692
+ <div id="step-welcome" class="step active slide-forward">
693
+ <div class="card text-center">
694
+ <div class="welcome-hero">
695
+ <svg width="48" height="48" viewBox="0 0 48 48" fill="none" style="margin-bottom: 20px;">
696
+ <circle cx="24" cy="24" r="23" stroke="#00A3E1" stroke-width="1.5" opacity="0.3"/>
697
+ <circle cx="24" cy="24" r="16" stroke="#00A3E1" stroke-width="1" opacity="0.15"/>
698
+ <circle cx="24" cy="24" r="6" fill="#00A3E1" opacity="0.8"/>
699
+ </svg>
700
+ <h1>Connect Your AI Tools</h1>
701
+ <p class="welcome-tagline">One setup. 10+ platforms. All your AI clients.</p>
702
+ <p class="subtitle" style="margin-bottom: 0;">
703
+ GitHub, Salesforce, M365, Slack, Asana, and more &mdash; configured in under 2 minutes.
704
+ </p>
705
+ </div>
706
+ <button class="btn btn-accent btn-block" onclick="goToStep('signin')">Get Started</button>
707
+ <p class="welcome-footer">Powered by Sonance</p>
708
+ </div>
709
+ </div>
710
+
711
+ <!-- Step 2: Sign In -->
712
+ <div id="step-signin" class="step">
713
+ <div class="card">
714
+ <h2>Sign In</h2>
715
+ <p class="subtitle">Authenticate with your company SSO</p>
716
+ <div id="signin-content"></div>
717
+ </div>
718
+ </div>
719
+
720
+ <!-- Step 3: Select MCPs -->
721
+ <div id="step-mcps" class="step">
722
+ <div class="card">
723
+ <h2>Select Integrations</h2>
724
+ <p class="subtitle">Choose which services to connect. You can enable more later.</p>
725
+ <div id="mcp-list-container"></div>
726
+ <div class="btn-row">
727
+ <button class="btn btn-secondary" onclick="goToStep('signin')">Back</button>
728
+ <button class="btn btn-primary" onclick="saveMcps()">Continue</button>
729
+ </div>
730
+ </div>
731
+ </div>
732
+
733
+ <!-- Step 4: Configure Clients -->
734
+ <div id="step-clients" class="step">
735
+ <div class="card">
736
+ <h2>Configure AI Clients</h2>
737
+ <p class="subtitle">Select which AI tools to set up with Cortex</p>
738
+ <div id="client-list" class="client-list"></div>
739
+ <div id="client-result" class="hidden"></div>
740
+ <div class="btn-row">
741
+ <button class="btn btn-secondary" onclick="goToStep('mcps')">Back</button>
742
+ <button id="btn-configure" class="btn btn-primary" onclick="configureClients()">Configure</button>
743
+ </div>
744
+ </div>
745
+ </div>
746
+
747
+ <!-- Step 5: Connect Accounts -->
748
+ <div id="step-connect" class="step">
749
+ <div class="card">
750
+ <h2>Connect Accounts</h2>
751
+ <p class="subtitle">Link personal accounts for services that require them (optional)</p>
752
+ <div id="conn-list" class="conn-list"></div>
753
+ <div class="btn-row">
754
+ <button class="btn btn-secondary" onclick="goToStep('clients')">Back</button>
755
+ <button class="btn btn-primary" onclick="goToStep('done')">Continue</button>
756
+ </div>
757
+ </div>
758
+ </div>
759
+
760
+ <!-- Step 6: Done -->
761
+ <div id="step-done" class="step">
762
+ <div class="card text-center">
763
+ <div class="success-circle"><span class="success-check">&#10003;</span></div>
764
+ <h2>Setup Complete</h2>
765
+ <p class="subtitle">Your AI clients are now connected to Cortex</p>
766
+ <div id="summary" style="text-align: left; margin: 24px 0;"></div>
767
+ <div class="next-steps">
768
+ <h3>What's Next</h3>
769
+ <div class="next-step-item">1. Restart your AI clients to load the new tools</div>
770
+ <div class="next-step-item">2. Try: &ldquo;List my GitHub repos&rdquo; or &ldquo;Show my Asana tasks&rdquo;</div>
771
+ <div class="next-step-item">3. Run <code>cortex-mcp connect &lt;provider&gt;</code> to add more accounts later</div>
772
+ </div>
773
+ <div style="margin-top: 24px;">
774
+ <button class="btn btn-accent btn-block" onclick="finishSetup()">Close</button>
775
+ </div>
776
+ <p class="text-sm text-muted mt-4">Config saved to ~/.cortex-mcp/config.json</p>
777
+ </div>
778
+ </div>
779
+
780
+ </div>
781
+
782
+ <script>
783
+ // \u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
784
+ var state = {
785
+ credentials: null,
786
+ availableMcps: [],
787
+ selectedMcps: [],
788
+ detectedClients: [],
789
+ selectedClients: [],
790
+ configuredClients: [],
791
+ connections: [],
792
+ };
793
+
794
+ // \u2500\u2500 Step Index Map \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
795
+ var STEP_ORDER = ['welcome', 'signin', 'mcps', 'clients', 'connect', 'done'];
796
+ var currentStepIndex = 0;
797
+
798
+ // \u2500\u2500 MCP Icons (inline SVG) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
799
+ var MCP_ICONS = {
800
+ github: '<svg viewBox="0 0 16 16"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>',
801
+ supabase: '<svg viewBox="0 0 16 16"><path d="M9.3 14.7c-.2.3-.7.1-.7-.3V9.3h4.6c.5 0 .8-.6.5-1L8.7 1.3c-.2-.3-.7-.1-.7.3v5.1H3.4c-.5 0-.8.6-.5 1l5 7z"/></svg>',
802
+ vercel: '<svg viewBox="0 0 16 16"><path d="M8 1l7 13H1z"/></svg>',
803
+ m365: '<svg viewBox="0 0 16 16"><rect x="1" y="1" width="6" height="6" rx="1"/><rect x="9" y="1" width="6" height="6" rx="1"/><rect x="1" y="9" width="6" height="6" rx="1"/><rect x="9" y="9" width="6" height="6" rx="1"/></svg>',
804
+ salesforce: '<svg viewBox="0 0 16 16"><path d="M6.7 3.3a3 3 0 014.8.5 2.5 2.5 0 013 2.4 2.5 2.5 0 01-1.5 2.3 2.8 2.8 0 01-3.5 2.2A3 3 0 016 12.5a3 3 0 01-3.3-1.8A2.5 2.5 0 011 8.5a2.5 2.5 0 011.8-2.4 2.8 2.8 0 013.9-2.8z"/></svg>',
805
+ slack: '<svg viewBox="0 0 16 16"><path d="M3.5 10a1.5 1.5 0 11-3 0 1.5 1.5 0 011.5-1.5H3.5V10zm.75 0a1.5 1.5 0 113 0v3.5a1.5 1.5 0 11-3 0V10zM6 3.5a1.5 1.5 0 110-3 1.5 1.5 0 011.5 1.5V3.5H6zm0 .75a1.5 1.5 0 110 3H2.5a1.5 1.5 0 110-3H6zM12.5 6a1.5 1.5 0 113 0 1.5 1.5 0 01-1.5 1.5H12.5V6zm-.75 0a1.5 1.5 0 11-3 0V2.5a1.5 1.5 0 113 0V6zM10 12.5a1.5 1.5 0 110 3 1.5 1.5 0 01-1.5-1.5v-1.5H10zm0-.75a1.5 1.5 0 110-3h3.5a1.5 1.5 0 110 3H10z"/></svg>',
806
+ asana: '<svg viewBox="0 0 16 16"><circle cx="8" cy="4" r="2.5"/><circle cx="3.5" cy="11" r="2.5"/><circle cx="12.5" cy="11" r="2.5"/></svg>',
807
+ monday: '<svg viewBox="0 0 16 16"><circle cx="4" cy="10" r="2" /><circle cx="8" cy="6" r="2" /><circle cx="12" cy="10" r="2" /></svg>',
808
+ powerbi: '<svg viewBox="0 0 16 16"><rect x="2" y="8" width="3" height="6" rx="1"/><rect x="6.5" y="5" width="3" height="9" rx="1"/><rect x="11" y="2" width="3" height="12" rx="1"/></svg>',
809
+ bestbuy: '<svg viewBox="0 0 16 16"><path d="M2 3h12a1 1 0 011 1v8a1 1 0 01-1 1H2a1 1 0 01-1-1V4a1 1 0 011-1zm3 3v4h2V6H5zm4 0v4h2V6H9z"/></svg>',
810
+ mailchimp: '<svg viewBox="0 0 16 16"><path d="M8 2a6 6 0 100 12A6 6 0 008 2zM6.5 10.5a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm3 0a1.5 1.5 0 110-3 1.5 1.5 0 010 3z"/></svg>',
811
+ };
812
+
813
+ // \u2500\u2500 Client Icons (inline SVG) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
814
+ var CLIENT_ICONS = {
815
+ 'claude-desktop': '<svg viewBox="0 0 16 16"><path d="M10.5 2H5.5a3 3 0 00-3 3v6a3 3 0 003 3h5a3 3 0 003-3V5a3 3 0 00-3-3zM8 11.5a.75.75 0 110-1.5.75.75 0 010 1.5zm2-3.5H6a.5.5 0 010-1h4a.5.5 0 010 1z"/></svg>',
816
+ 'claude-code': '<svg viewBox="0 0 16 16"><path d="M5.7 11.3a.5.5 0 01-.7-.7L7.3 8 5 5.7a.5.5 0 01.7-.7l3 3a.5.5 0 010 .7l-3 3zM9 11h3a.5.5 0 010 1H9a.5.5 0 010-1z"/><rect x="1" y="2" width="14" height="12" rx="2" fill="none" stroke="currentColor" stroke-width="1"/></svg>',
817
+ cursor: '<svg viewBox="0 0 16 16"><path d="M3 2l10 6-4 1.5L7.5 14z"/></svg>',
818
+ vscode: '<svg viewBox="0 0 16 16"><path d="M11.5 1L5 7l-3-2.5L1 5l3.5 3L1 11l1 .5L5 9l6.5 6 2.5-1V2z"/></svg>',
819
+ antigravity: '<svg viewBox="0 0 16 16"><path d="M8 1l2 5h5l-4 3 1.5 5L8 11l-4.5 3L5 9 1 6h5z"/></svg>',
820
+ codex: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="3" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M8 1v3M8 12v3M1 8h3M12 8h3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
821
+ stdio: '<svg viewBox="0 0 16 16"><path d="M2 3h12a1 1 0 011 1v8a1 1 0 01-1 1H2a1 1 0 01-1-1V4a1 1 0 011-1zm1 2v6h10V5H3z"/></svg>',
822
+ };
823
+
824
+ // \u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
825
+ async function init() {
826
+ try {
827
+ var resp = await fetch('/api/state');
828
+ var data = await resp.json();
829
+ state.credentials = data.credentials;
830
+ state.availableMcps = data.availableMcps || [];
831
+ state.selectedMcps = ['github', 'supabase', 'vercel'];
832
+ state.detectedClients = data.detectedClients || [];
833
+ } catch (err) {
834
+ console.error('Failed to load state:', err);
835
+ }
836
+ updateHeaderDots();
837
+ }
838
+
839
+ init();
840
+
841
+ // \u2500\u2500 Header Dots \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
842
+ function updateHeaderDots() {
843
+ var el = document.getElementById('header-steps');
844
+ var html = '';
845
+ for (var i = 0; i < STEP_ORDER.length; i++) {
846
+ var cls = 'header-dot';
847
+ if (i < currentStepIndex) cls += ' done';
848
+ else if (i === currentStepIndex) cls += ' current';
849
+ html += '<span class="' + cls + '"></span>';
850
+ }
851
+ el.innerHTML = html;
852
+ }
853
+
854
+ // \u2500\u2500 Navigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
855
+ function goToStep(step) {
856
+ var newIndex = STEP_ORDER.indexOf(step);
857
+ var direction = newIndex >= currentStepIndex ? 'slide-forward' : 'slide-backward';
858
+
859
+ var active = document.querySelector('.step.active');
860
+ if (active) {
861
+ active.classList.remove('active', 'slide-forward', 'slide-backward');
862
+ }
863
+
864
+ var nextEl = document.getElementById('step-' + step);
865
+ nextEl.classList.add('active', direction);
866
+ currentStepIndex = newIndex;
867
+ updateHeaderDots();
868
+
869
+ if (step === 'signin') renderSignin();
870
+ if (step === 'mcps') renderMcps();
871
+ if (step === 'clients') renderClients();
872
+ if (step === 'connect') renderConnect();
873
+ if (step === 'done') renderDone();
874
+ }
875
+
876
+ // \u2500\u2500 Blue Orb Mouse Follow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
877
+ (function() {
878
+ var primary = document.getElementById('orb-primary');
879
+ var secondary = document.getElementById('orb-secondary');
880
+ document.addEventListener('mousemove', function(e) {
881
+ var dx = (e.clientX - window.innerWidth / 2) * 0.15;
882
+ var dy = (e.clientY - window.innerHeight / 2) * 0.15;
883
+ primary.style.transform = 'translate(calc(-50% + ' + dx + 'px), calc(-50% + ' + dy + 'px))';
884
+ secondary.style.transform = 'translate(calc(-50% + ' + (-dx * 0.6) + 'px), calc(-50% + ' + (-dy * 0.6) + 'px))';
885
+ });
886
+ })();
887
+
888
+ // \u2500\u2500 Step 2: Sign In \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
889
+ function renderSignin() {
890
+ var el = document.getElementById('signin-content');
891
+
892
+ if (state.credentials) {
893
+ var name = state.credentials.name || state.credentials.email;
894
+ el.innerHTML =
895
+ '<div class="status status-success">' +
896
+ '<span>&#10003;</span>' +
897
+ '<span>Signed in as <strong>' + escapeHtml(name) + '</strong></span>' +
898
+ '</div>' +
899
+ '<div class="btn-row">' +
900
+ '<button class="btn btn-primary btn-block" onclick="goToStep(\\'mcps\\')">Continue</button>' +
901
+ '</div>';
902
+ return;
903
+ }
904
+
905
+ el.innerHTML =
906
+ '<button id="btn-signin" class="btn btn-accent btn-block" onclick="startLogin()">Sign in with SSO</button>' +
907
+ '<div id="signin-status" class="hidden"></div>';
908
+ }
909
+
910
+ async function startLogin() {
911
+ var btn = document.getElementById('btn-signin');
912
+ btn.disabled = true;
913
+ btn.innerHTML = '<span class="spinner"></span> Starting...';
914
+
915
+ var statusEl = document.getElementById('signin-status');
916
+
917
+ try {
918
+ var resp = await fetch('/api/auth/login', { method: 'POST' });
919
+ var data = await resp.json();
920
+
921
+ if (data.error) {
922
+ statusEl.className = 'status status-error';
923
+ statusEl.textContent = data.error;
924
+ btn.disabled = false;
925
+ btn.textContent = 'Sign in with SSO';
926
+ return;
927
+ }
928
+
929
+ window.open(data.auth_url, '_blank');
930
+
931
+ // Show auth phase indicators
932
+ statusEl.className = '';
933
+ statusEl.innerHTML =
934
+ '<div class="auth-phases">' +
935
+ '<div class="auth-phase done" id="phase-browser"><span class="phase-icon">&#10003;</span> Browser opened</div>' +
936
+ '<div class="auth-phase active" id="phase-sso"><span class="spinner"></span> Waiting for SSO...</div>' +
937
+ '<div class="auth-phase pending" id="phase-verify"><span class="phase-icon">&#8226;</span> Verifying identity</div>' +
938
+ '</div>';
939
+
940
+ btn.className = 'btn btn-secondary btn-block';
941
+ btn.disabled = false;
942
+ btn.textContent = 'Open SSO page again';
943
+ btn.onclick = function() { window.open(data.auth_url, '_blank'); };
944
+
945
+ var result = await pollUntilDone(
946
+ '/api/auth/poll?session=' + encodeURIComponent(data.session_id),
947
+ 3000,
948
+ data.expires_in * 1000
949
+ );
950
+
951
+ // Update phases to show completion
952
+ var ssoPhase = document.getElementById('phase-sso');
953
+ if (ssoPhase) {
954
+ ssoPhase.className = 'auth-phase done';
955
+ ssoPhase.innerHTML = '<span class="phase-icon">&#10003;</span> SSO completed';
956
+ }
957
+ var verifyPhase = document.getElementById('phase-verify');
958
+ if (verifyPhase) {
959
+ verifyPhase.className = 'auth-phase done';
960
+ verifyPhase.innerHTML = '<span class="phase-icon">&#10003;</span> Identity verified';
961
+ }
962
+
963
+ state.credentials = {
964
+ email: result.employee_email || result.email || '',
965
+ name: result.employee_name || result.name || '',
966
+ };
967
+
968
+ // Brief pause so user sees all green checks
969
+ await new Promise(function(r) { setTimeout(r, 600); });
970
+ renderSignin();
971
+ } catch (err) {
972
+ statusEl.className = 'status status-error';
973
+ statusEl.textContent = err.message === 'Timeout'
974
+ ? 'Authentication timed out. Please try again.'
975
+ : 'Authentication failed: ' + err.message;
976
+ btn.disabled = false;
977
+ btn.textContent = 'Try Again';
978
+ btn.onclick = startLogin;
979
+ }
980
+ }
981
+
982
+ // \u2500\u2500 Step 3: Select MCPs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
983
+ var REQUIRED_MCPS = ['github', 'supabase', 'vercel'];
984
+
985
+ var MCP_TOOLTIPS = {
986
+ asana: 'Manage projects, tasks, and team workflows',
987
+ github: 'Cloud storage and version control for any projects with code',
988
+ vercel: 'Deploy and host websites and web apps',
989
+ supabase: 'Your project database \\u2014 store and query data',
990
+ m365: 'Access your Outlook email, calendar, OneDrive files, and Teams',
991
+ mailchimp: 'Create and manage email marketing campaigns and audiences',
992
+ salesforce: 'Access your CRM \\u2014 contacts, leads, opportunities, and reports',
993
+ monday: 'Track work with boards, timelines, and team updates',
994
+ slack: 'Send messages, search conversations, and manage channels',
995
+ powerbi: 'Build dashboards and run data queries on your reports',
996
+ bestbuy: 'Search products, compare prices, and find store locations',
997
+ };
998
+
999
+ function renderMcps() {
1000
+ var container = document.getElementById('mcp-list-container');
1001
+
1002
+ var requiredMcps = state.availableMcps.filter(function(m) { return REQUIRED_MCPS.indexOf(m.name) !== -1; });
1003
+ var optionalMcps = state.availableMcps.filter(function(m) { return REQUIRED_MCPS.indexOf(m.name) === -1; });
1004
+
1005
+ var html = '';
1006
+
1007
+ // Core section
1008
+ html += '<div class="mcp-section">';
1009
+ html += '<div class="mcp-section-title">Core (always enabled)</div>';
1010
+ html += '<div class="mcp-list">';
1011
+ html += requiredMcps.map(function(mcp) { return renderMcpItem(mcp, true); }).join('');
1012
+ html += '</div></div>';
1013
+
1014
+ // Optional section
1015
+ if (optionalMcps.length > 0) {
1016
+ html += '<div class="mcp-section">';
1017
+ html += '<div class="mcp-section-title">Optional integrations</div>';
1018
+ html += '<div class="mcp-list">';
1019
+ html += optionalMcps.map(function(mcp) { return renderMcpItem(mcp, false); }).join('');
1020
+ html += '</div></div>';
1021
+ }
1022
+
1023
+ container.innerHTML = html;
1024
+
1025
+ // Attach click handlers to optional items
1026
+ container.querySelectorAll('.mcp-item:not(.required)').forEach(function(item) {
1027
+ item.addEventListener('click', function(e) {
1028
+ e.preventDefault();
1029
+ var cb = item.querySelector('input[type="checkbox"]');
1030
+ cb.checked = !cb.checked;
1031
+ item.classList.toggle('checked', cb.checked);
1032
+ });
1033
+ });
1034
+ }
1035
+
1036
+ function renderMcpItem(mcp, isRequired) {
1037
+ var checked = isRequired || state.selectedMcps.indexOf(mcp.name) !== -1;
1038
+ var icon = MCP_ICONS[mcp.name] || '';
1039
+ return (
1040
+ '<label class="mcp-item' + (checked ? ' checked' : '') + (isRequired ? ' required' : '') + '" data-mcp="' + mcp.name + '">' +
1041
+ (MCP_TOOLTIPS[mcp.name] ? '<span class="mcp-tooltip">' + MCP_TOOLTIPS[mcp.name] + '</span>' : '') +
1042
+ '<input type="checkbox" value="' + mcp.name + '"' + (checked ? ' checked' : '') + '>' +
1043
+ (icon ? '<div class="mcp-icon">' + icon + '</div>' : '') +
1044
+ '<div class="mcp-check"><span class="mcp-check-icon">&#10003;</span></div>' +
1045
+ '<div class="mcp-info">' +
1046
+ '<span class="mcp-name">' + escapeHtml(mcp.displayName) +
1047
+ '<span class="mcp-badge ' + mcp.authMode + '">' + mcp.authMode + '</span>' +
1048
+ '</span>' +
1049
+ '<span class="mcp-desc">' + escapeHtml(mcp.description) + '</span>' +
1050
+ '</div>' +
1051
+ (isRequired ? '<span class="mcp-required-label">Required</span>' : '') +
1052
+ '</label>'
1053
+ );
1054
+ }
1055
+
1056
+ function saveMcps() {
1057
+ var checkboxes = document.querySelectorAll('.mcp-item input[type="checkbox"]');
1058
+ var selected = Array.from(checkboxes).filter(function(cb) { return cb.checked; }).map(function(cb) { return cb.value; });
1059
+ REQUIRED_MCPS.forEach(function(name) { if (selected.indexOf(name) === -1) selected.push(name); });
1060
+ state.selectedMcps = selected;
1061
+ goToStep('clients');
1062
+ }
1063
+
1064
+ // \u2500\u2500 Step 4: Configure Clients \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1065
+ function renderClients() {
1066
+ var el = document.getElementById('client-list');
1067
+ document.getElementById('client-result').className = 'hidden';
1068
+ document.getElementById('btn-configure').disabled = false;
1069
+ document.getElementById('btn-configure').textContent = 'Configure';
1070
+
1071
+ state.selectedClients = [];
1072
+
1073
+ el.innerHTML = state.detectedClients.map(function(client) {
1074
+ var detected = client.detected;
1075
+ var icon = CLIENT_ICONS[client.type] || '';
1076
+ return (
1077
+ '<label class="client-item' + (detected ? '' : ' disabled') + '" data-client="' + client.type + '">' +
1078
+ (icon ? '<div class="client-icon">' + icon + '</div>' : '') +
1079
+ '<div class="mcp-check"><span class="mcp-check-icon">&#10003;</span></div>' +
1080
+ '<span class="client-name">' + escapeHtml(client.name) + '</span>' +
1081
+ '<span class="client-status">' + (detected ? 'Detected' : 'Not found') + '</span>' +
1082
+ '</label>'
1083
+ );
1084
+ }).join('');
1085
+
1086
+ el.querySelectorAll('.client-item:not(.disabled)').forEach(function(item) {
1087
+ item.addEventListener('click', function(e) {
1088
+ e.preventDefault();
1089
+ item.classList.toggle('checked');
1090
+ state.selectedClients = Array.from(el.querySelectorAll('.client-item.checked'))
1091
+ .map(function(i) { return i.dataset.client; });
1092
+ });
1093
+ });
1094
+ }
1095
+
1096
+ async function configureClients() {
1097
+ if (state.selectedClients.length === 0) {
1098
+ goToStep('connect');
1099
+ return;
1100
+ }
1101
+
1102
+ var btn = document.getElementById('btn-configure');
1103
+ btn.disabled = true;
1104
+ btn.innerHTML = '<span class="spinner"></span> Configuring...';
1105
+
1106
+ // Show per-client spinners
1107
+ state.selectedClients.forEach(function(clientType) {
1108
+ var item = document.querySelector('[data-client="' + clientType + '"]');
1109
+ if (item) {
1110
+ var statusSpan = item.querySelector('.client-status');
1111
+ statusSpan.innerHTML = '<span class="spinner"></span>';
1112
+ }
1113
+ });
1114
+
1115
+ try {
1116
+ var resp = await fetch('/api/clients/configure', {
1117
+ method: 'POST',
1118
+ headers: { 'Content-Type': 'application/json' },
1119
+ body: JSON.stringify({ clients: state.selectedClients, mcps: state.selectedMcps }),
1120
+ });
1121
+ var data = await resp.json();
1122
+
1123
+ state.configuredClients = data.results.filter(function(r) { return r.success; }).map(function(r) { return r.client; });
1124
+
1125
+ // Update per-client status inline
1126
+ data.results.forEach(function(r) {
1127
+ var item = document.querySelector('[data-client="' + r.client + '"]');
1128
+ if (item) {
1129
+ var statusSpan = item.querySelector('.client-status');
1130
+ if (r.success) {
1131
+ statusSpan.innerHTML = '<span style="color: #4CAF50;">&#10003; Done</span>';
1132
+ } else {
1133
+ statusSpan.innerHTML = '<span style="color: #EF5350;">&#10007; Failed</span>';
1134
+ }
1135
+ }
1136
+ });
1137
+
1138
+ var resultEl = document.getElementById('client-result');
1139
+ resultEl.className = '';
1140
+ resultEl.innerHTML = data.results.map(function(r) {
1141
+ var html = '<div class="status ' + (r.success ? 'status-success' : 'status-error') + '">' +
1142
+ '<span>' + (r.success ? '&#10003;' : '&#10007;') + '</span>' +
1143
+ '<span>' + escapeHtml(r.client) + ': ' + (r.success ? 'Configured' : r.error || 'Failed') + '</span>' +
1144
+ '</div>';
1145
+ // Show copyable snippet for stdio/OpenClaw
1146
+ if (r.client === 'stdio' && r.success && r.message) {
1147
+ var snippet = r.message.replace(/^Add this to your client config:\\n\\n/, '');
1148
+ html += '<div class="stdio-snippet">' +
1149
+ '<button class="copy-btn" onclick="copySnippet(this)">Copy</button>' +
1150
+ '<pre>' + escapeHtml(snippet) + '</pre>' +
1151
+ '</div>';
1152
+ }
1153
+ return html;
1154
+ }).join('');
1155
+
1156
+ btn.textContent = 'Continue';
1157
+ btn.disabled = false;
1158
+ btn.onclick = function() { goToStep('connect'); };
1159
+ } catch (err) {
1160
+ btn.disabled = false;
1161
+ btn.textContent = 'Retry';
1162
+ var resultEl = document.getElementById('client-result');
1163
+ resultEl.className = '';
1164
+ resultEl.innerHTML = '<div class="status status-error"><span>&#10007;</span><span>' + escapeHtml(err.message) + '</span></div>';
1165
+ }
1166
+ }
1167
+
1168
+ // \u2500\u2500 Step 5: Connect Accounts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1169
+ async function renderConnect() {
1170
+ var el = document.getElementById('conn-list');
1171
+ el.innerHTML = '<div class="status status-info"><span class="spinner"></span> Loading connections...</div>';
1172
+
1173
+ var personalMcps = state.availableMcps.filter(function(m) {
1174
+ return m.authMode === 'personal' && state.selectedMcps.indexOf(m.name) !== -1;
1175
+ });
1176
+
1177
+ if (personalMcps.length === 0) {
1178
+ el.innerHTML = '<div class="status status-info">No personal account connections needed.</div>';
1179
+ return;
1180
+ }
1181
+
1182
+ try {
1183
+ var resp = await fetch('/api/connections');
1184
+ var data = await resp.json();
1185
+ state.connections = data.connections || [];
1186
+ } catch (e) {
1187
+ state.connections = [];
1188
+ }
1189
+
1190
+ el.innerHTML = personalMcps.map(function(mcp) {
1191
+ var conn = state.connections.find(function(c) { return c.mcp_name === mcp.name; });
1192
+ var isConnected = conn && !conn.is_company_default && conn.account_email;
1193
+
1194
+ return (
1195
+ '<div class="conn-item' + (isConnected ? ' connected' : '') + '" id="conn-' + mcp.name + '">' +
1196
+ '<span class="conn-name">' + escapeHtml(mcp.displayName) + '</span>' +
1197
+ (isConnected
1198
+ ? '<span class="conn-status connected">&#10003; ' + escapeHtml(conn.account_email) + '</span>'
1199
+ : '<span class="conn-status">Not connected</span>' +
1200
+ '<button class="btn btn-sm btn-secondary" onclick="connectProvider(\\'' + mcp.name + '\\', \\'' + escapeHtml(mcp.displayName) + '\\')">Connect</button>'
1201
+ ) +
1202
+ '</div>'
1203
+ );
1204
+ }).join('');
1205
+ }
1206
+
1207
+ async function connectProvider(name, displayName) {
1208
+ var item = document.getElementById('conn-' + name);
1209
+ var btn = item.querySelector('button');
1210
+ var statusSpan = item.querySelector('.conn-status');
1211
+
1212
+ btn.disabled = true;
1213
+ btn.innerHTML = '<span class="spinner"></span>';
1214
+ statusSpan.textContent = 'Connecting...';
1215
+
1216
+ try {
1217
+ var resp = await fetch('/api/oauth/connect', {
1218
+ method: 'POST',
1219
+ headers: { 'Content-Type': 'application/json' },
1220
+ body: JSON.stringify({ provider: name }),
1221
+ });
1222
+ var data = await resp.json();
1223
+
1224
+ if (data.error) {
1225
+ statusSpan.textContent = data.error;
1226
+ statusSpan.className = 'conn-status';
1227
+ btn.disabled = false;
1228
+ btn.textContent = 'Retry';
1229
+ return;
1230
+ }
1231
+
1232
+ window.open(data.authorization_url, '_blank');
1233
+ statusSpan.innerHTML = '<span class="spinner"></span> Waiting \\u2014 check the new tab';
1234
+ btn.textContent = 'Open again';
1235
+ btn.disabled = false;
1236
+ btn.onclick = function() { window.open(data.authorization_url, '_blank'); };
1237
+
1238
+ var result = await pollUntilDone(
1239
+ '/api/oauth/poll?session=' + encodeURIComponent(data.session_id),
1240
+ 3000,
1241
+ data.expires_in * 1000
1242
+ );
1243
+
1244
+ var email = result.account_email || 'connected';
1245
+ item.classList.add('connected');
1246
+ statusSpan.className = 'conn-status connected';
1247
+ statusSpan.textContent = '\\u2713 ' + email;
1248
+ btn.remove();
1249
+ } catch (err) {
1250
+ statusSpan.textContent = err.message === 'Timeout' ? 'Timed out' : err.message;
1251
+ statusSpan.className = 'conn-status';
1252
+ btn.disabled = false;
1253
+ btn.textContent = 'Retry';
1254
+ btn.onclick = function() { connectProvider(name, displayName); };
1255
+ }
1256
+ }
1257
+
1258
+ // \u2500\u2500 Step 6: Done \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1259
+ function renderDone() {
1260
+ var el = document.getElementById('summary');
1261
+ var html = '';
1262
+
1263
+ html += '<div class="summary-section">';
1264
+ html += '<h3>Integrations (' + state.selectedMcps.length + ')</h3>';
1265
+ state.selectedMcps.forEach(function(name) {
1266
+ var mcp = state.availableMcps.find(function(m) { return m.name === name; });
1267
+ html += '<div class="summary-item"><span class="check-icon">&#10003;</span>' + escapeHtml(mcp ? mcp.displayName : name) + '</div>';
1268
+ });
1269
+ html += '</div>';
1270
+
1271
+ if (state.configuredClients.length > 0) {
1272
+ html += '<div class="summary-section">';
1273
+ html += '<h3>Configured Clients</h3>';
1274
+ state.configuredClients.forEach(function(c) {
1275
+ var client = state.detectedClients.find(function(d) { return d.type === c; });
1276
+ html += '<div class="summary-item"><span class="check-icon">&#10003;</span>' + escapeHtml(client ? client.name : c) + '</div>';
1277
+ });
1278
+ html += '</div>';
1279
+ }
1280
+
1281
+ if (state.credentials) {
1282
+ html += '<div class="summary-section">';
1283
+ html += '<h3>Signed In</h3>';
1284
+ html += '<div class="summary-item"><span class="check-icon">&#10003;</span>' + escapeHtml(state.credentials.name || state.credentials.email) + '</div>';
1285
+ html += '</div>';
1286
+ }
1287
+
1288
+ el.innerHTML = html;
1289
+ }
1290
+
1291
+ async function finishSetup() {
1292
+ try {
1293
+ await fetch('/api/complete', {
1294
+ method: 'POST',
1295
+ headers: { 'Content-Type': 'application/json' },
1296
+ body: JSON.stringify({
1297
+ mcps: state.selectedMcps,
1298
+ configuredClients: state.configuredClients,
1299
+ }),
1300
+ });
1301
+ } catch (e) {
1302
+ // Server may already be closing
1303
+ }
1304
+
1305
+ document.getElementById('step-done').querySelector('.card').innerHTML =
1306
+ '<div class="text-center">' +
1307
+ '<div class="success-circle"><span class="success-check">&#10003;</span></div>' +
1308
+ '<h2>All Done</h2>' +
1309
+ '<p class="subtitle mt-2">You can close this tab. Restart your AI clients to see the new tools.</p>' +
1310
+ '</div>';
1311
+ }
1312
+
1313
+ // \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1314
+ function pollUntilDone(url, intervalMs, timeoutMs) {
1315
+ return new Promise(function(resolve, reject) {
1316
+ var deadline = Date.now() + timeoutMs;
1317
+ var timer = setInterval(async function() {
1318
+ if (Date.now() > deadline) {
1319
+ clearInterval(timer);
1320
+ reject(new Error('Timeout'));
1321
+ return;
1322
+ }
1323
+ try {
1324
+ var resp = await fetch(url);
1325
+ var data = await resp.json();
1326
+ if (data.status === 'completed') {
1327
+ clearInterval(timer);
1328
+ resolve(data);
1329
+ } else if (data.status === 'error' || data.status === 'expired') {
1330
+ clearInterval(timer);
1331
+ reject(new Error(data.error_message || data.status));
1332
+ }
1333
+ } catch (e) {
1334
+ // Network error -- keep polling
1335
+ }
1336
+ }, intervalMs);
1337
+ });
1338
+ }
1339
+
1340
+ function copySnippet(btn) {
1341
+ var pre = btn.parentElement.querySelector('pre');
1342
+ navigator.clipboard.writeText(pre.textContent).then(function() {
1343
+ btn.textContent = 'Copied!';
1344
+ setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
1345
+ });
1346
+ }
1347
+
1348
+ function escapeHtml(str) {
1349
+ var div = document.createElement('div');
1350
+ div.textContent = str || '';
1351
+ return div.innerHTML;
1352
+ }
1353
+ </script>
1354
+ </body>
1355
+ </html>`;
64
1356
  }
65
1357
 
66
1358
  // src/config/clients.ts
67
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1359
+ import { existsSync, readFileSync, writeFileSync } from "fs";
68
1360
  import { dirname } from "path";
69
- import { mkdirSync as mkdirSync2 } from "fs";
1361
+ import { mkdirSync } from "fs";
70
1362
  import { execSync } from "child_process";
1363
+ import { homedir } from "os";
1364
+ import { join } from "path";
71
1365
  function detectClients() {
72
1366
  const clients = [];
73
1367
  const desktopPath = getClaudeDesktopConfigPath();
74
1368
  const desktopDir = dirname(desktopPath);
75
1369
  clients.push({
76
1370
  type: "claude-desktop",
77
- name: "Claude Desktop",
1371
+ name: "Claude (Desktop)",
78
1372
  configPath: desktopPath,
79
- detected: existsSync2(desktopDir)
1373
+ detected: existsSync(desktopDir)
80
1374
  });
81
1375
  let claudeCodeDetected = false;
82
1376
  try {
@@ -96,15 +1390,43 @@ function detectClients() {
96
1390
  type: "cursor",
97
1391
  name: "Cursor",
98
1392
  configPath: cursorPath,
99
- detected: existsSync2(cursorDir)
1393
+ detected: existsSync(cursorDir)
1394
+ });
1395
+ const home = homedir();
1396
+ const vscodePath = getVSCodeMcpConfigPath();
1397
+ clients.push({
1398
+ type: "vscode",
1399
+ name: "VS Code",
1400
+ configPath: vscodePath,
1401
+ detected: existsSync(join(home, ".vscode"))
1402
+ });
1403
+ const antigravityPath = getAntigravityConfigPath();
1404
+ clients.push({
1405
+ type: "antigravity",
1406
+ name: "Antigravity",
1407
+ configPath: antigravityPath,
1408
+ detected: existsSync(join(home, ".antigravity"))
100
1409
  });
101
1410
  const codexPath = getCodexConfigPath();
102
- const codexDir = dirname(codexPath);
1411
+ let codexDetected = existsSync(join(home, ".codex"));
1412
+ if (!codexDetected) {
1413
+ try {
1414
+ execSync("which codex", { stdio: "pipe" });
1415
+ codexDetected = true;
1416
+ } catch {
1417
+ }
1418
+ }
103
1419
  clients.push({
104
1420
  type: "codex",
105
- name: "Codex",
1421
+ name: "Codex (OpenAI)",
106
1422
  configPath: codexPath,
107
- detected: existsSync2(codexDir)
1423
+ detected: codexDetected
1424
+ });
1425
+ clients.push({
1426
+ type: "stdio",
1427
+ name: "OpenClaw (stdio)",
1428
+ configPath: null,
1429
+ detected: true
108
1430
  });
109
1431
  return clients;
110
1432
  }
@@ -122,13 +1444,13 @@ function buildHttpEntries(serverUrl, apiKey, mcps) {
122
1444
  function configureClaudeDesktop(_serverUrl, apiKey, _mcps) {
123
1445
  const configPath = getClaudeDesktopConfigPath();
124
1446
  const dir = dirname(configPath);
125
- if (!existsSync2(dir)) {
126
- mkdirSync2(dir, { recursive: true });
1447
+ if (!existsSync(dir)) {
1448
+ mkdirSync(dir, { recursive: true });
127
1449
  }
128
1450
  let config = {};
129
- if (existsSync2(configPath)) {
1451
+ if (existsSync(configPath)) {
130
1452
  try {
131
- config = JSON.parse(readFileSync2(configPath, "utf-8"));
1453
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
132
1454
  } catch {
133
1455
  }
134
1456
  }
@@ -141,11 +1463,15 @@ function configureClaudeDesktop(_serverUrl, apiKey, _mcps) {
141
1463
  delete servers[key];
142
1464
  }
143
1465
  }
144
- servers["cortex"] = {
1466
+ const isWindows = getPlatform() === "windows";
1467
+ servers["cortex"] = isWindows ? {
1468
+ command: "cmd",
1469
+ args: ["/c", "npx", "-y", "@danainnovations/cortex-mcp@latest", "serve"]
1470
+ } : {
145
1471
  command: "npx",
146
1472
  args: ["-y", "@danainnovations/cortex-mcp@latest", "serve"]
147
1473
  };
148
- writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n");
1474
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
149
1475
  }
150
1476
  function configureClaudeCode(serverUrl, apiKey, mcps) {
151
1477
  for (const mcp of AVAILABLE_MCPS) {
@@ -164,13 +1490,13 @@ function configureClaudeCode(serverUrl, apiKey, mcps) {
164
1490
  function configureCursor(serverUrl, apiKey, mcps) {
165
1491
  const configPath = getCursorConfigPath();
166
1492
  const dir = dirname(configPath);
167
- if (!existsSync2(dir)) {
168
- mkdirSync2(dir, { recursive: true });
1493
+ if (!existsSync(dir)) {
1494
+ mkdirSync(dir, { recursive: true });
169
1495
  }
170
1496
  let config = {};
171
- if (existsSync2(configPath)) {
1497
+ if (existsSync(configPath)) {
172
1498
  try {
173
- config = JSON.parse(readFileSync2(configPath, "utf-8"));
1499
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
174
1500
  } catch {
175
1501
  }
176
1502
  }
@@ -185,24 +1511,109 @@ function configureCursor(serverUrl, apiKey, mcps) {
185
1511
  }
186
1512
  const entries = buildHttpEntries(serverUrl, apiKey, mcps);
187
1513
  Object.assign(servers, entries);
188
- writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n");
1514
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
189
1515
  }
190
- function generateStdioSnippet(_apiKey) {
191
- const config = {
192
- mcpServers: {
193
- cortex: {
194
- command: "npx",
195
- args: ["-y", "@danainnovations/cortex-mcp@latest", "serve"]
196
- }
197
- }
1516
+ function configureVSCode(serverUrl, apiKey, mcps) {
1517
+ const configPath = getVSCodeMcpConfigPath();
1518
+ const dir = dirname(configPath);
1519
+ if (!existsSync(dir)) {
1520
+ mkdirSync(dir, { recursive: true });
1521
+ }
1522
+ let config = {};
1523
+ if (existsSync(configPath)) {
1524
+ try {
1525
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
1526
+ } catch {
1527
+ }
1528
+ }
1529
+ if (!config.mcpServers || typeof config.mcpServers !== "object") {
1530
+ config.mcpServers = {};
1531
+ }
1532
+ const servers = config.mcpServers;
1533
+ for (const key of Object.keys(servers)) {
1534
+ if (key.startsWith("cortex-")) {
1535
+ delete servers[key];
1536
+ }
1537
+ }
1538
+ const entries = buildHttpEntries(serverUrl, apiKey, mcps);
1539
+ Object.assign(servers, entries);
1540
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1541
+ }
1542
+ function configureAntigravity(serverUrl, apiKey, mcps) {
1543
+ const configPath = getAntigravityConfigPath();
1544
+ const dir = dirname(configPath);
1545
+ if (!existsSync(dir)) {
1546
+ mkdirSync(dir, { recursive: true });
1547
+ }
1548
+ let config = {};
1549
+ if (existsSync(configPath)) {
1550
+ try {
1551
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
1552
+ } catch {
1553
+ }
1554
+ }
1555
+ if (!config.mcpServers || typeof config.mcpServers !== "object") {
1556
+ config.mcpServers = {};
1557
+ }
1558
+ const servers = config.mcpServers;
1559
+ for (const key of Object.keys(servers)) {
1560
+ if (key.startsWith("cortex-")) {
1561
+ delete servers[key];
1562
+ }
1563
+ }
1564
+ const entries = buildHttpEntries(serverUrl, apiKey, mcps);
1565
+ Object.assign(servers, entries);
1566
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1567
+ }
1568
+ function configureCodex(serverUrl, apiKey, mcps) {
1569
+ const configPath = getCodexConfigPath();
1570
+ const dir = dirname(configPath);
1571
+ if (!existsSync(dir)) {
1572
+ mkdirSync(dir, { recursive: true });
1573
+ }
1574
+ let existingContent = "";
1575
+ if (existsSync(configPath)) {
1576
+ try {
1577
+ existingContent = readFileSync(configPath, "utf-8");
1578
+ } catch {
1579
+ }
1580
+ }
1581
+ const cleaned = existingContent.replace(
1582
+ /\[mcp_servers\.cortex-[^\]]*\][^[]*(?=\[|$)/g,
1583
+ ""
1584
+ ).trim();
1585
+ const tomlEntries = [];
1586
+ for (const mcp of AVAILABLE_MCPS) {
1587
+ if (!mcps.includes(mcp.name)) continue;
1588
+ tomlEntries.push(
1589
+ `[mcp_servers.${mcp.serverName}]
1590
+ url = "${serverUrl}/mcp/${mcp.name}"
1591
+ http_headers = { "x-api-key" = "${apiKey}" }`
1592
+ );
1593
+ }
1594
+ const newContent = (cleaned ? cleaned + "\n\n" : "") + tomlEntries.join("\n\n") + "\n";
1595
+ writeFileSync(configPath, newContent);
1596
+ }
1597
+ function generateStdioSnippet(_apiKey) {
1598
+ const isWindows = getPlatform() === "windows";
1599
+ const config = {
1600
+ mcpServers: {
1601
+ cortex: isWindows ? {
1602
+ command: "cmd",
1603
+ args: ["/c", "npx", "-y", "@danainnovations/cortex-mcp@latest", "serve"]
1604
+ } : {
1605
+ command: "npx",
1606
+ args: ["-y", "@danainnovations/cortex-mcp@latest", "serve"]
1607
+ }
1608
+ }
198
1609
  };
199
1610
  return JSON.stringify(config, null, 2);
200
1611
  }
201
1612
  function resetClaudeDesktop() {
202
1613
  const configPath = getClaudeDesktopConfigPath();
203
- if (!existsSync2(configPath)) return false;
1614
+ if (!existsSync(configPath)) return false;
204
1615
  try {
205
- const config = JSON.parse(readFileSync2(configPath, "utf-8"));
1616
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
206
1617
  if (!config.mcpServers) return false;
207
1618
  const servers = config.mcpServers;
208
1619
  let removed = false;
@@ -213,7 +1624,7 @@ function resetClaudeDesktop() {
213
1624
  }
214
1625
  }
215
1626
  if (removed) {
216
- writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n");
1627
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
217
1628
  }
218
1629
  return removed;
219
1630
  } catch {
@@ -222,9 +1633,9 @@ function resetClaudeDesktop() {
222
1633
  }
223
1634
  function resetCursor() {
224
1635
  const configPath = getCursorConfigPath();
225
- if (!existsSync2(configPath)) return false;
1636
+ if (!existsSync(configPath)) return false;
226
1637
  try {
227
- const config = JSON.parse(readFileSync2(configPath, "utf-8"));
1638
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
228
1639
  if (!config.mcpServers) return false;
229
1640
  const servers = config.mcpServers;
230
1641
  let removed = false;
@@ -235,7 +1646,7 @@ function resetCursor() {
235
1646
  }
236
1647
  }
237
1648
  if (removed) {
238
- writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n");
1649
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
239
1650
  }
240
1651
  return removed;
241
1652
  } catch {
@@ -253,56 +1664,17 @@ function resetClaudeCode() {
253
1664
  }
254
1665
  return removed;
255
1666
  }
256
- function removeCortexTomlSections(content) {
257
- const lines = content.split("\n");
258
- const result = [];
259
- let skipping = false;
260
- for (const line of lines) {
261
- const trimmed = line.trim();
262
- if (trimmed.startsWith("[")) {
263
- skipping = /^\[mcp_servers\.cortex[_-]/.test(trimmed) || trimmed === "[mcp_servers.cortex]";
264
- }
265
- if (!skipping) {
266
- result.push(line);
267
- }
268
- }
269
- return result.join("\n");
270
- }
271
- function configureCodex(serverUrl, apiKey, mcps) {
272
- const configPath = getCodexConfigPath();
273
- const dir = dirname(configPath);
274
- if (!existsSync2(dir)) {
275
- mkdirSync2(dir, { recursive: true });
276
- }
277
- let content = "";
278
- if (existsSync2(configPath)) {
279
- try {
280
- content = readFileSync2(configPath, "utf-8");
281
- } catch {
282
- }
283
- }
284
- content = removeCortexTomlSections(content);
285
- const sections = [];
286
- for (const mcp of AVAILABLE_MCPS) {
287
- if (!mcps.includes(mcp.name)) continue;
288
- sections.push(`[mcp_servers.${mcp.serverName}]`);
289
- sections.push(`url = "${serverUrl}/mcp/${mcp.name}"`);
290
- sections.push(`[mcp_servers.${mcp.serverName}.http_headers]`);
291
- sections.push(`"x-api-key" = "${apiKey}"`);
292
- sections.push("");
293
- }
294
- const trimmed = content.trimEnd();
295
- const newContent = trimmed ? trimmed + "\n\n" + sections.join("\n") + "\n" : sections.join("\n") + "\n";
296
- writeFileSync2(configPath, newContent);
297
- }
298
1667
  function resetCodex() {
299
1668
  const configPath = getCodexConfigPath();
300
- if (!existsSync2(configPath)) return false;
1669
+ if (!existsSync(configPath)) return false;
301
1670
  try {
302
- const content = readFileSync2(configPath, "utf-8");
303
- const cleaned = removeCortexTomlSections(content);
304
- if (cleaned.length < content.length) {
305
- writeFileSync2(configPath, cleaned);
1671
+ const content = readFileSync(configPath, "utf-8");
1672
+ const cleaned = content.replace(
1673
+ /\[mcp_servers\.cortex-[^\]]*\][^[]*(?=\[|$)/g,
1674
+ ""
1675
+ ).trim();
1676
+ if (cleaned !== content.trim()) {
1677
+ writeFileSync(configPath, cleaned ? cleaned + "\n" : "");
306
1678
  return true;
307
1679
  }
308
1680
  return false;
@@ -321,6 +1693,12 @@ function configureClient(clientType, serverUrl, apiKey, mcps) {
321
1693
  case "cursor":
322
1694
  configureCursor(serverUrl, apiKey, mcps);
323
1695
  return "Cursor configured";
1696
+ case "vscode":
1697
+ configureVSCode(serverUrl, apiKey, mcps);
1698
+ return "VS Code configured";
1699
+ case "antigravity":
1700
+ configureAntigravity(serverUrl, apiKey, mcps);
1701
+ return "Antigravity configured";
324
1702
  case "codex":
325
1703
  configureCodex(serverUrl, apiKey, mcps);
326
1704
  return "Codex configured";
@@ -329,11 +1707,50 @@ function configureClient(clientType, serverUrl, apiKey, mcps) {
329
1707
  }
330
1708
  }
331
1709
 
1710
+ // src/config/storage.ts
1711
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1712
+ import { join as join2 } from "path";
1713
+ function getConfigDir() {
1714
+ return join2(getHomeDir(), CONFIG_DIR_NAME);
1715
+ }
1716
+ function getConfigPath() {
1717
+ return join2(getConfigDir(), CONFIG_FILE_NAME);
1718
+ }
1719
+ function readConfig() {
1720
+ const path = getConfigPath();
1721
+ if (!existsSync2(path)) return null;
1722
+ try {
1723
+ const raw = readFileSync2(path, "utf-8");
1724
+ return JSON.parse(raw);
1725
+ } catch {
1726
+ return null;
1727
+ }
1728
+ }
1729
+ function writeConfig(config) {
1730
+ const dir = getConfigDir();
1731
+ if (!existsSync2(dir)) {
1732
+ mkdirSync2(dir, { recursive: true, mode: 448 });
1733
+ }
1734
+ const path = getConfigPath();
1735
+ writeFileSync2(path, JSON.stringify(config, null, 2) + "\n", {
1736
+ mode: 384
1737
+ });
1738
+ }
1739
+ function createConfig(apiKey, mcps) {
1740
+ return {
1741
+ version: 1,
1742
+ server: DEFAULT_SERVER_URL,
1743
+ apiKey,
1744
+ mcps,
1745
+ configuredClients: []
1746
+ };
1747
+ }
1748
+
332
1749
  // src/auth/credentials.ts
333
1750
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
334
- import { join as join2 } from "path";
1751
+ import { join as join3 } from "path";
335
1752
  function getCredentialsPath() {
336
- return join2(getHomeDir(), CONFIG_DIR_NAME, CREDENTIALS_FILE_NAME);
1753
+ return join3(getHomeDir(), CONFIG_DIR_NAME, CREDENTIALS_FILE_NAME);
337
1754
  }
338
1755
  function readCredentials() {
339
1756
  const path = getCredentialsPath();
@@ -346,7 +1763,7 @@ function readCredentials() {
346
1763
  }
347
1764
  }
348
1765
  function writeCredentials(creds) {
349
- const dir = join2(getHomeDir(), CONFIG_DIR_NAME);
1766
+ const dir = join3(getHomeDir(), CONFIG_DIR_NAME);
350
1767
  if (!existsSync3(dir)) {
351
1768
  mkdirSync3(dir, { recursive: true, mode: 448 });
352
1769
  }
@@ -371,691 +1788,339 @@ function getEffectiveApiKey() {
371
1788
  return DEFAULT_API_KEY;
372
1789
  }
373
1790
 
374
- // src/cli/login.ts
375
- import * as readline from "readline";
376
-
377
- // src/cli/connect.ts
378
- function log(msg) {
379
- process.stderr.write(msg + "\n");
380
- }
381
- function sleep(ms) {
382
- return new Promise((resolve) => setTimeout(resolve, ms));
1791
+ // src/wizard/routes.ts
1792
+ var wizardState = null;
1793
+ function getState() {
1794
+ if (!wizardState) {
1795
+ wizardState = { apiKey: getEffectiveApiKey() };
1796
+ }
1797
+ return wizardState;
383
1798
  }
384
- function waitForEnter() {
1799
+ function parseBody(req) {
385
1800
  return new Promise((resolve) => {
386
- process.stdin.setRawMode?.(false);
387
- process.stdin.resume();
388
- process.stdin.once("data", () => {
389
- process.stdin.pause();
390
- resolve();
1801
+ const chunks = [];
1802
+ req.on("data", (chunk) => chunks.push(chunk));
1803
+ req.on("end", () => {
1804
+ try {
1805
+ resolve(JSON.parse(Buffer.concat(chunks).toString()));
1806
+ } catch {
1807
+ resolve({});
1808
+ }
391
1809
  });
1810
+ req.on("error", () => resolve({}));
392
1811
  });
393
1812
  }
394
- async function connectProvider(providerName, displayName, apiKey, serverUrl, installUrl) {
395
- let sessionId;
396
- let authUrl;
397
- let expiresIn;
398
- try {
399
- const resp = await fetch(
400
- `${serverUrl}/api/v1/oauth/connect/${providerName}/initiate`,
401
- {
402
- method: "POST",
403
- headers: {
404
- "Content-Type": "application/json",
405
- "x-api-key": apiKey
1813
+ function json(res, data, status = 200) {
1814
+ res.writeHead(status, { "Content-Type": "application/json" });
1815
+ res.end(JSON.stringify(data));
1816
+ }
1817
+ async function handleApiRoute(path, searchParams, req, res, options, onComplete) {
1818
+ const method = req.method || "GET";
1819
+ if (path === "/api/state" && method === "GET") {
1820
+ const creds = readCredentials();
1821
+ const config = readConfig();
1822
+ const clients = detectClients();
1823
+ json(res, {
1824
+ credentials: creds ? { email: creds.email, name: creds.name } : null,
1825
+ config,
1826
+ apiKey: getState().apiKey,
1827
+ defaultMcps: [...DEFAULT_MCPS],
1828
+ availableMcps: AVAILABLE_MCPS.map((m) => ({
1829
+ name: m.name,
1830
+ displayName: m.displayName,
1831
+ description: m.description,
1832
+ authMode: m.authMode
1833
+ })),
1834
+ detectedClients: clients
1835
+ });
1836
+ return true;
1837
+ }
1838
+ if (path === "/api/auth/login" && method === "POST") {
1839
+ try {
1840
+ const resp = await fetch(
1841
+ `${options.serverUrl}/api/v1/auth/employee/initiate`,
1842
+ {
1843
+ method: "POST",
1844
+ headers: { "Content-Type": "application/json" }
406
1845
  }
1846
+ );
1847
+ if (!resp.ok) {
1848
+ json(res, { error: `Server returned HTTP ${resp.status}` }, 502);
1849
+ return true;
407
1850
  }
408
- );
409
- if (!resp.ok) {
410
- if (resp.status === 401) {
411
- log(" Error: Your session has expired. Run 'cortex-mcp login' to re-authenticate.");
412
- return false;
1851
+ const data = await resp.json();
1852
+ json(res, data);
1853
+ } catch (err) {
1854
+ const msg = err instanceof Error ? err.message : String(err);
1855
+ json(res, { error: `Could not reach Cortex server: ${msg}` }, 502);
1856
+ }
1857
+ return true;
1858
+ }
1859
+ if (path === "/api/auth/poll" && method === "GET") {
1860
+ const sessionId = searchParams.get("session");
1861
+ if (!sessionId) {
1862
+ json(res, { error: "Missing session parameter" }, 400);
1863
+ return true;
1864
+ }
1865
+ try {
1866
+ const resp = await fetch(
1867
+ `${options.serverUrl}/api/v1/auth/employee/poll/${sessionId}`
1868
+ );
1869
+ const result = await resp.json();
1870
+ if (result.status === "completed" && result.api_key) {
1871
+ const email = result.employee_email || result.user_info?.email || "";
1872
+ const name = result.employee_name || result.user_info?.name || "";
1873
+ writeCredentials({
1874
+ apiKey: result.api_key,
1875
+ email,
1876
+ name,
1877
+ authenticatedAt: (/* @__PURE__ */ new Date()).toISOString()
1878
+ });
1879
+ getState().apiKey = result.api_key;
1880
+ }
1881
+ json(res, result);
1882
+ } catch {
1883
+ json(res, { status: "pending" });
1884
+ }
1885
+ return true;
1886
+ }
1887
+ if (path === "/api/clients/configure" && method === "POST") {
1888
+ const body = await parseBody(req);
1889
+ const clients = body.clients || [];
1890
+ const mcps = body.mcps || [...DEFAULT_MCPS];
1891
+ const apiKey = getState().apiKey;
1892
+ const results = [];
1893
+ for (const clientType of clients) {
1894
+ try {
1895
+ const msg = configureClient(
1896
+ clientType,
1897
+ options.serverUrl,
1898
+ apiKey,
1899
+ mcps
1900
+ );
1901
+ results.push({ client: clientType, success: true, message: msg });
1902
+ } catch (err) {
1903
+ const msg = err instanceof Error ? err.message : String(err);
1904
+ results.push({ client: clientType, success: false, error: msg });
413
1905
  }
414
- const err = await resp.json().catch(() => ({}));
415
- log(` Error: ${err.detail || `HTTP ${resp.status}`}`);
416
- return false;
417
1906
  }
418
- const data = await resp.json();
419
- sessionId = data.session_id;
420
- authUrl = data.authorization_url;
421
- expiresIn = data.expires_in;
422
- } catch (err) {
423
- const msg = err instanceof Error ? err.message : String(err);
424
- log(` Error: Could not reach Cortex server at ${serverUrl}`);
425
- log(` ${msg}`);
426
- return false;
1907
+ json(res, { results });
1908
+ return true;
427
1909
  }
428
- if (installUrl) {
429
- log(` First, install the ${displayName} app on your account...`);
430
- await openBrowser(installUrl);
431
- log(" If the browser didn't open, visit:");
432
- log(` ${installUrl}`);
433
- log("");
434
- log(" Press Enter once the app is installed...");
435
- await waitForEnter();
436
- log("");
1910
+ if (path === "/api/connections" && method === "GET") {
1911
+ const apiKey = getState().apiKey;
1912
+ try {
1913
+ const resp = await fetch(
1914
+ `${options.serverUrl}/api/v1/oauth/connections`,
1915
+ { headers: { "x-api-key": apiKey } }
1916
+ );
1917
+ if (resp.ok) {
1918
+ const data = await resp.json();
1919
+ json(res, data);
1920
+ } else {
1921
+ json(res, { connections: [] });
1922
+ }
1923
+ } catch {
1924
+ json(res, { connections: [] });
1925
+ }
1926
+ return true;
437
1927
  }
438
- log(` Opening browser for ${displayName} authorization...`);
439
- await openBrowser(authUrl);
440
- log(" If the browser didn't open, visit:");
441
- log(` ${authUrl}`);
442
- log("");
443
- const deadline = Date.now() + expiresIn * 1e3;
444
- process.stderr.write(" Waiting for authorization");
445
- while (Date.now() < deadline) {
446
- await sleep(3e3);
1928
+ if (path === "/api/oauth/connect" && method === "POST") {
1929
+ const body = await parseBody(req);
1930
+ const provider = body.provider;
1931
+ const apiKey = getState().apiKey;
1932
+ if (!provider) {
1933
+ json(res, { error: "Missing provider" }, 400);
1934
+ return true;
1935
+ }
447
1936
  try {
448
1937
  const resp = await fetch(
449
- `${serverUrl}/api/v1/oauth/connect/poll/${sessionId}`,
1938
+ `${options.serverUrl}/api/v1/oauth/connect/${provider}/initiate`,
450
1939
  {
1940
+ method: "POST",
451
1941
  headers: {
1942
+ "Content-Type": "application/json",
452
1943
  "x-api-key": apiKey
453
1944
  }
454
1945
  }
455
1946
  );
456
- const result = await resp.json();
457
- if (result.status === "completed") {
458
- process.stderr.write("\n\n");
459
- const who = result.account_email || "unknown";
460
- log(` Connected as ${who}`);
461
- log(` ${displayName} tools will now use your personal account.`);
462
- log("");
1947
+ if (!resp.ok) {
1948
+ if (resp.status === 401) {
1949
+ json(res, { error: "Session expired. Please re-login." }, 401);
1950
+ return true;
1951
+ }
1952
+ const err = await resp.json().catch(() => ({}));
1953
+ json(res, { error: err.detail || `HTTP ${resp.status}` }, resp.status);
463
1954
  return true;
464
1955
  }
465
- if (result.status === "expired") {
466
- process.stderr.write("\n");
467
- log(" Authorization session expired. Please try again.");
468
- return false;
469
- }
470
- if (result.status === "error") {
471
- process.stderr.write("\n");
472
- log(
473
- ` Connection failed: ${result.error_message || "unknown error"}`
474
- );
475
- return false;
476
- }
477
- process.stderr.write(".");
478
- } catch {
479
- process.stderr.write("!");
1956
+ const data = await resp.json();
1957
+ json(res, data);
1958
+ } catch (err) {
1959
+ const msg = err instanceof Error ? err.message : String(err);
1960
+ json(res, { error: `Could not reach Cortex server: ${msg}` }, 502);
480
1961
  }
1962
+ return true;
481
1963
  }
482
- process.stderr.write("\n");
483
- log(" Timeout waiting for authorization. Please try again.");
484
- return false;
485
- }
486
- async function runConnect(provider) {
487
- const creds = readCredentials();
488
- if (!creds) {
489
- log("");
490
- log(" You must be logged in first.");
491
- log(" Run: cortex-mcp login");
492
- log("");
493
- process.exit(1);
494
- }
495
- const mcp = AVAILABLE_MCPS.find(
496
- (m) => m.name === provider || m.displayName.toLowerCase() === provider.toLowerCase()
497
- );
498
- if (!mcp) {
499
- log("");
500
- log(` Unknown provider: ${provider}`);
501
- log(` Available: ${AVAILABLE_MCPS.map((m) => m.name).join(", ")}`);
502
- log("");
503
- process.exit(1);
1964
+ if (path === "/api/oauth/poll" && method === "GET") {
1965
+ const sessionId = searchParams.get("session");
1966
+ if (!sessionId) {
1967
+ json(res, { error: "Missing session parameter" }, 400);
1968
+ return true;
1969
+ }
1970
+ const apiKey = getState().apiKey;
1971
+ try {
1972
+ const resp = await fetch(
1973
+ `${options.serverUrl}/api/v1/oauth/connect/poll/${sessionId}`,
1974
+ { headers: { "x-api-key": apiKey } }
1975
+ );
1976
+ const result = await resp.json();
1977
+ json(res, result);
1978
+ } catch {
1979
+ json(res, { status: "pending" });
1980
+ }
1981
+ return true;
504
1982
  }
505
- const config = readConfig();
506
- const serverUrl = config?.server || DEFAULT_SERVER_URL;
507
- log("");
508
- log(` Cortex MCP \u2014 Connect ${mcp.displayName}`);
509
- log(` Link your personal ${mcp.displayName} account`);
510
- log("");
511
- const success = await connectProvider(
512
- mcp.name,
513
- mcp.displayName,
514
- creds.apiKey,
515
- serverUrl,
516
- "installUrl" in mcp ? mcp.installUrl : void 0
517
- );
518
- if (!success) {
519
- process.exit(1);
1983
+ if (path === "/api/complete" && method === "POST") {
1984
+ const body = await parseBody(req);
1985
+ const mcps = body.mcps || [...DEFAULT_MCPS];
1986
+ const configuredClients = body.configuredClients || [];
1987
+ const config = createConfig(getState().apiKey, mcps);
1988
+ config.configuredClients = configuredClients;
1989
+ writeConfig(config);
1990
+ json(res, { success: true });
1991
+ setTimeout(onComplete, 100);
1992
+ return true;
520
1993
  }
1994
+ return false;
521
1995
  }
522
1996
 
523
- // src/cli/login.ts
524
- function log2(msg) {
525
- process.stderr.write(msg + "\n");
526
- }
527
- function sleep2(ms) {
528
- return new Promise((resolve) => setTimeout(resolve, ms));
529
- }
530
- async function loginWithSSO(serverUrl, emailHint) {
531
- let sessionId;
532
- let authUrl;
533
- let expiresIn;
534
- try {
535
- const body = {};
536
- if (emailHint) {
537
- body.email_hint = emailHint;
538
- }
539
- const resp = await fetch(`${serverUrl}/api/v1/auth/employee/initiate`, {
540
- method: "POST",
541
- headers: { "Content-Type": "application/json" },
542
- body: JSON.stringify(body)
1997
+ // src/wizard/server.ts
1998
+ function startWizardServer(options) {
1999
+ return new Promise((resolve, reject) => {
2000
+ let completionResolve;
2001
+ const completionPromise = new Promise((r) => {
2002
+ completionResolve = r;
543
2003
  });
544
- if (!resp.ok) {
545
- log2(` Error: Failed to start authentication (HTTP ${resp.status})`);
546
- return { status: "failed" };
547
- }
548
- const data = await resp.json();
549
- sessionId = data.session_id;
550
- authUrl = data.auth_url;
551
- expiresIn = data.expires_in;
552
- } catch (err) {
553
- const msg = err instanceof Error ? err.message : String(err);
554
- log2(` Error: Could not reach Cortex server at ${serverUrl}`);
555
- log2(` ${msg}`);
556
- return { status: "failed" };
557
- }
558
- log2(" Opening browser for Okta SSO login...");
559
- await openBrowser(authUrl);
560
- log2(" If the browser didn't open, visit:");
561
- log2(` ${authUrl}`);
562
- log2("");
563
- const deadline = Date.now() + expiresIn * 1e3;
564
- process.stderr.write(" Waiting for authentication");
565
- while (Date.now() < deadline) {
566
- await sleep2(3e3);
567
- try {
568
- const resp = await fetch(
569
- `${serverUrl}/api/v1/auth/employee/poll/${sessionId}`
570
- );
571
- const result = await resp.json();
572
- if (result.status === "completed") {
573
- process.stderr.write("\n\n");
574
- const email = result.employee_email || result.user_info?.email || "unknown";
575
- const name = result.employee_name || result.user_info?.name || "";
576
- const apiKey = result.api_key || "";
577
- return { status: "success", apiKey, email, name: name || void 0 };
578
- }
579
- if (result.status === "expired") {
580
- process.stderr.write("\n");
581
- log2(" Authentication session expired. Please try again.");
582
- return { status: "failed" };
583
- }
584
- if (result.status === "error") {
585
- process.stderr.write("\n");
586
- if (result.error_message === "not_verified_employee" && !emailHint) {
587
- return {
588
- status: "needs_hint",
589
- ssoEmail: result.sso_email || "unknown"
590
- };
2004
+ const onComplete = () => {
2005
+ completionResolve();
2006
+ };
2007
+ const server = http.createServer(async (req, res) => {
2008
+ const url = new URL(req.url || "/", `http://localhost`);
2009
+ const path = url.pathname;
2010
+ if (path.startsWith("/api/")) {
2011
+ try {
2012
+ const handled = await handleApiRoute(
2013
+ path,
2014
+ url.searchParams,
2015
+ req,
2016
+ res,
2017
+ options,
2018
+ onComplete
2019
+ );
2020
+ if (!handled) {
2021
+ res.writeHead(404, { "Content-Type": "application/json" });
2022
+ res.end(JSON.stringify({ error: "Not found" }));
2023
+ }
2024
+ } catch (err) {
2025
+ const msg = err instanceof Error ? err.message : String(err);
2026
+ res.writeHead(500, { "Content-Type": "application/json" });
2027
+ res.end(JSON.stringify({ error: msg }));
591
2028
  }
592
- log2(
593
- ` Authentication failed: ${result.error_message || "unknown error"}`
594
- );
595
- return { status: "failed" };
2029
+ return;
596
2030
  }
597
- process.stderr.write(".");
598
- } catch {
599
- process.stderr.write("!");
600
- }
601
- }
602
- process.stderr.write("\n");
603
- log2(" Timeout waiting for authentication. Please try again.");
604
- return { status: "failed" };
605
- }
606
- async function showConnectionsAndAutoConnect(apiKey, serverUrl) {
607
- let existingConnections = [];
608
- try {
609
- const resp = await fetch(`${serverUrl}/api/v1/oauth/connections`, {
610
- headers: { "x-api-key": apiKey }
2031
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2032
+ res.end(getWizardHtml());
611
2033
  });
612
- if (resp.ok) {
613
- const data = await resp.json();
614
- existingConnections = data.connections || [];
615
- } else {
616
- log2(
617
- ` Warning: Could not fetch current MCP connections (HTTP ${resp.status}).`
618
- );
619
- if (resp.status === 401 || resp.status === 403) {
620
- log2(" Warning: Authentication may be expired. Run 'cortex-mcp login' to refresh.");
621
- }
622
- }
623
- } catch {
624
- log2(" Warning: Could not fetch current MCP connections due to a network error.");
625
- }
626
- log2("");
627
- log2(" MCP Connections");
628
- log2(" " + "\u2500".repeat(45));
629
- const personalMcps = [];
630
- for (const mcp of AVAILABLE_MCPS) {
631
- const conn = existingConnections.find((c) => c.mcp_name === mcp.name);
632
- if (mcp.authMode === "company") {
633
- log2(` ${mcp.displayName.padEnd(15)} company default`);
634
- } else {
635
- if (conn && !conn.is_company_default && conn.account_email) {
636
- log2(` ${mcp.displayName.padEnd(15)} ${conn.account_email}`);
637
- } else {
638
- log2(
639
- ` ${mcp.displayName.padEnd(15)} not connected (personal account required)`
640
- );
641
- personalMcps.push({ name: mcp.name, displayName: mcp.displayName });
642
- }
643
- }
644
- }
645
- log2("");
646
- if (personalMcps.length === 0) {
647
- log2(" All accounts connected!");
648
- log2("");
649
- return;
650
- }
651
- log2(" The following MCPs require a personal account to access your data.");
652
- log2(" Skip any you don't have an account for \u2014 you can always connect later.\n");
653
- const rl2 = readline.createInterface({
654
- input: process.stdin,
655
- output: process.stderr
656
- });
657
- const ask2 = (question) => new Promise((resolve) => {
658
- rl2.question(question, (answer) => resolve(answer.trim().toLowerCase()));
2034
+ server.listen(0, "127.0.0.1", () => {
2035
+ const addr = server.address();
2036
+ resolve({
2037
+ port: addr.port,
2038
+ close: () => new Promise((r) => {
2039
+ server.close(() => r());
2040
+ }),
2041
+ waitForCompletion: () => completionPromise
2042
+ });
2043
+ });
2044
+ server.on("error", reject);
659
2045
  });
660
- for (const mcp of personalMcps) {
661
- const answer = await ask2(
662
- ` Connect ${mcp.displayName}? (Y/n/s to skip all): `
663
- );
664
- if (answer === "s") {
665
- log2(" Skipping remaining account connections.");
666
- log2(" You can connect anytime with: cortex-mcp connect <provider>\n");
667
- break;
668
- }
669
- if (answer === "n") {
670
- log2(` Skipped. Connect later with: cortex-mcp connect ${mcp.name}`);
671
- log2("");
672
- continue;
673
- }
674
- log2("");
675
- const success = await connectProvider(
676
- mcp.name,
677
- mcp.displayName,
678
- apiKey,
679
- serverUrl,
680
- "installUrl" in mcp ? mcp.installUrl : void 0
681
- );
682
- if (!success) {
683
- log2(
684
- ` You can connect later with: cortex-mcp connect ${mcp.name}`
685
- );
686
- }
687
- log2("");
688
- }
689
- rl2.close();
690
2046
  }
691
- async function promptForEmailHint(ssoEmail) {
692
- log2("");
693
- log2(` Your SSO email (${ssoEmail}) was not found in the employee directory.`);
694
- log2(
695
- " This can happen if your Okta account uses a different email than your company directory."
696
- );
697
- log2("");
698
- const rl2 = readline.createInterface({
699
- input: process.stdin,
700
- output: process.stderr
701
- });
702
- const answer = await new Promise((resolve) => {
703
- rl2.question(
704
- " Enter your company email address: ",
705
- (a) => resolve(a.trim())
706
- );
2047
+
2048
+ // src/cli/setup.ts
2049
+ function log(msg) {
2050
+ process.stderr.write(msg + "\n");
2051
+ }
2052
+ async function runSetup() {
2053
+ log("");
2054
+ log(" Cortex MCP Setup");
2055
+ log(" Starting setup wizard...");
2056
+ log("");
2057
+ const { port, close, waitForCompletion } = await startWizardServer({
2058
+ serverUrl: DEFAULT_SERVER_URL
707
2059
  });
708
- rl2.close();
709
- if (!answer || !answer.includes("@")) {
710
- log2(" Invalid email. Run 'cortex-mcp login --email you@company.com' to try again.");
711
- return null;
712
- }
713
- return answer;
2060
+ const url = `http://localhost:${port}`;
2061
+ log(` Setup wizard running at ${url}`);
2062
+ log("");
2063
+ await openBrowser(url);
2064
+ log(" If the browser didn't open, visit:");
2065
+ log(` ${url}`);
2066
+ log("");
2067
+ log(" Waiting for setup to complete (press Ctrl+C to cancel)...");
2068
+ const cleanup = async () => {
2069
+ log("\n Setup cancelled.");
2070
+ await close();
2071
+ process.exit(0);
2072
+ };
2073
+ process.on("SIGINT", cleanup);
2074
+ process.on("SIGTERM", cleanup);
2075
+ await waitForCompletion();
2076
+ await new Promise((r) => setTimeout(r, 300));
2077
+ await close();
2078
+ log("");
2079
+ log(" Setup complete! Restart your AI clients to see the new tools.");
2080
+ log("");
714
2081
  }
715
- async function runLogin(emailHint) {
716
- const existing = readCredentials();
717
- if (existing) {
718
- log2("");
719
- log2(` Already logged in as ${existing.name || existing.email}`);
720
- log2(" Run 'cortex-mcp logout' first to switch accounts.");
721
- log2("");
722
- return;
2082
+
2083
+ // src/cli/configure.ts
2084
+ var VALID_CLIENTS = {
2085
+ "claude-desktop": "claude-desktop",
2086
+ "claude-code": "claude-code",
2087
+ cursor: "cursor",
2088
+ codex: "codex",
2089
+ stdio: "stdio"
2090
+ };
2091
+ async function runConfigure(options) {
2092
+ const key = options.key || getEffectiveApiKey();
2093
+ const { client } = options;
2094
+ const clientType = VALID_CLIENTS[client];
2095
+ if (!clientType) {
2096
+ console.error(
2097
+ `Unknown client: ${client}. Valid options: ${Object.keys(VALID_CLIENTS).join(", ")}`
2098
+ );
2099
+ process.exit(1);
723
2100
  }
724
- const config = readConfig();
725
- const serverUrl = config?.server || DEFAULT_SERVER_URL;
726
- log2("");
727
- log2(" Cortex MCP Login");
728
- log2(" Sign in with your company Okta SSO account");
729
- log2("");
730
- let result = await loginWithSSO(serverUrl, emailHint);
731
- if (result.status === "needs_hint") {
732
- let hintEmail;
733
- if (emailHint) {
734
- hintEmail = emailHint;
735
- } else {
736
- hintEmail = await promptForEmailHint(result.ssoEmail);
737
- }
738
- if (!hintEmail) {
739
- process.exit(1);
740
- }
741
- log2("");
742
- log2(" Retrying with your company email. You'll need to complete SSO again.");
743
- log2("");
744
- result = await loginWithSSO(serverUrl, hintEmail);
745
- if (result.status === "needs_hint") {
746
- log2("");
747
- log2(" The provided email was also not found in the employee directory.");
748
- log2(" Please verify your email and contact IT if the issue persists.");
2101
+ let selectedMcps;
2102
+ if (options.mcps) {
2103
+ selectedMcps = options.mcps.split(",").map((s) => s.trim()).filter((s) => MCP_NAMES.includes(s));
2104
+ if (selectedMcps.length === 0) {
2105
+ console.error(
2106
+ `No valid MCPs. Available: ${MCP_NAMES.join(", ")}`
2107
+ );
749
2108
  process.exit(1);
750
2109
  }
2110
+ } else {
2111
+ selectedMcps = [...DEFAULT_MCPS];
751
2112
  }
752
- if (result.status !== "success") {
753
- process.exit(1);
754
- }
755
- const { apiKey, email, name } = result;
756
- writeCredentials({
757
- apiKey,
758
- email,
759
- name,
760
- authenticatedAt: (/* @__PURE__ */ new Date()).toISOString()
761
- });
762
- log2(` Authenticated as ${name || email}${name ? ` (${email})` : ""}`);
763
- log2(" Personal API key saved.");
764
- const existingConfig = readConfig();
765
- if (existingConfig) {
766
- existingConfig.apiKey = apiKey;
767
- if (existingConfig.configuredClients?.length > 0) {
768
- log2("");
769
- log2(" Updating configured clients:");
770
- for (const clientType of existingConfig.configuredClients) {
771
- try {
772
- configureClient(
773
- clientType,
774
- existingConfig.server || DEFAULT_SERVER_URL,
775
- apiKey,
776
- existingConfig.mcps
777
- );
778
- log2(` ${clientType}: updated`);
779
- } catch {
780
- log2(` ${clientType}: failed to update`);
781
- }
782
- }
783
- }
784
- writeConfig(existingConfig);
785
- }
786
- await showConnectionsAndAutoConnect(apiKey, serverUrl);
787
- log2(" Done! Restart your AI clients to apply.");
788
- log2("");
789
- }
790
-
791
- // src/cli/setup.ts
792
- var rl = readline2.createInterface({
793
- input: process.stdin,
794
- output: process.stderr
795
- });
796
- function ask(question) {
797
- return new Promise((resolve) => {
798
- rl.question(question, (answer) => resolve(answer.trim()));
799
- });
800
- }
801
- function log3(msg) {
802
- process.stderr.write(msg + "\n");
803
- }
804
- async function validateApiKey(apiKey, serverUrl) {
805
- try {
806
- const resp = await fetch(`${serverUrl}/api/v1/oauth/connections`, {
807
- headers: { "x-api-key": apiKey }
808
- });
809
- if (resp.ok) {
810
- return "valid";
811
- }
812
- if (resp.status === 401 || resp.status === 403) {
813
- return "invalid";
814
- }
815
- return "unreachable";
816
- } catch {
817
- return "unreachable";
818
- }
819
- }
820
- async function runSetup(emailHint) {
821
- let apiKey = getEffectiveApiKey();
822
- let creds = readCredentials();
823
- log3("");
824
- log3(" Cortex MCP Setup");
825
- log3(" Connect your AI tools to Asana, GitHub, Monday.com, Salesforce, M365, Vercel & Supabase");
826
- log3("");
827
- log3(" Step 1: Select MCPs");
828
- log3(" Available MCPs (all enabled by default):\n");
829
- for (const mcp of AVAILABLE_MCPS) {
830
- log3(` - ${mcp.displayName.padEnd(15)} ${mcp.description}`);
831
- }
832
- log3("");
833
- const mcpInput = await ask(
834
- ` MCPs to enable (comma-separated, or press Enter for all): `
835
- );
836
- let selectedMcps;
837
- if (!mcpInput) {
838
- selectedMcps = [...DEFAULT_MCPS];
839
- log3(" All MCPs enabled.\n");
840
- } else {
841
- selectedMcps = mcpInput.split(",").map((s) => s.trim().toLowerCase()).filter(
842
- (s) => AVAILABLE_MCPS.some(
843
- (m) => m.name === s || m.displayName.toLowerCase() === s
844
- )
845
- );
846
- selectedMcps = selectedMcps.map((s) => {
847
- const found = AVAILABLE_MCPS.find(
848
- (m) => m.displayName.toLowerCase() === s
849
- );
850
- return found ? found.name : s;
851
- });
852
- if (selectedMcps.length === 0) {
853
- selectedMcps = [...DEFAULT_MCPS];
854
- log3(" No valid MCPs recognized. Enabling all.\n");
855
- } else {
856
- log3(` Enabled: ${selectedMcps.join(", ")}
857
- `);
858
- }
859
- }
860
- log3(" Step 2: Configure AI Clients\n");
861
- const clients = detectClients();
862
- const configuredClients = [];
863
- for (const client of clients) {
864
- if (!client.detected) {
865
- log3(` ${client.name}: not detected, skipping`);
866
- continue;
867
- }
868
- const answer = await ask(` Configure ${client.name}? (Y/n): `);
869
- if (answer.toLowerCase() === "n") {
870
- log3(` Skipping ${client.name}`);
871
- continue;
872
- }
873
- try {
874
- if (client.type === "claude-desktop") {
875
- configureClaudeDesktop(DEFAULT_SERVER_URL, apiKey, selectedMcps);
876
- log3(` ${client.name}: configured`);
877
- configuredClients.push(client.type);
878
- } else if (client.type === "claude-code") {
879
- configureClaudeCode(DEFAULT_SERVER_URL, apiKey, selectedMcps);
880
- log3(` ${client.name}: configured`);
881
- configuredClients.push(client.type);
882
- } else if (client.type === "cursor") {
883
- configureCursor(DEFAULT_SERVER_URL, apiKey, selectedMcps);
884
- log3(` ${client.name}: configured`);
885
- configuredClients.push(client.type);
886
- } else if (client.type === "codex") {
887
- configureCodex(DEFAULT_SERVER_URL, apiKey, selectedMcps);
888
- log3(` ${client.name}: configured`);
889
- configuredClients.push(client.type);
890
- }
891
- } catch (err) {
892
- const msg = err instanceof Error ? err.message : String(err);
893
- log3(` ${client.name}: failed \u2014 ${msg}`);
894
- }
895
- }
896
- log3("");
897
- const stdioAnswer = await ask(
898
- " Do you also need a config snippet for OpenClaw or other stdio clients? (y/N): "
899
- );
900
- if (stdioAnswer.toLowerCase() === "y") {
901
- log3("\n Add this to your client's MCP config:\n");
902
- log3(generateStdioSnippet(apiKey));
903
- log3("");
904
- configuredClients.push("stdio");
905
- }
906
- const config = createConfig(apiKey, selectedMcps);
907
- config.configuredClients = configuredClients;
908
- writeConfig(config);
909
- log3("");
910
- log3(" Step 3: Sign In\n");
911
- let shouldLogin = !creds;
912
- if (creds) {
913
- log3(` Already signed in as ${creds.name || creds.email}`);
914
- const validation = await validateApiKey(creds.apiKey, DEFAULT_SERVER_URL);
915
- if (validation === "invalid") {
916
- log3(" Saved session is no longer valid. Re-authenticating...");
917
- deleteCredentials();
918
- creds = null;
919
- shouldLogin = true;
920
- } else {
921
- apiKey = creds.apiKey;
922
- if (validation === "unreachable") {
923
- log3(" Warning: Could not validate saved session. Continuing with existing credentials.");
924
- }
925
- }
926
- }
927
- if (shouldLogin) {
928
- log3(" Sign in with your company Okta SSO account.");
929
- log3("");
930
- rl.close();
931
- let result = await loginWithSSO(DEFAULT_SERVER_URL, emailHint);
932
- if (result.status === "needs_hint") {
933
- let hintEmail;
934
- if (emailHint) {
935
- hintEmail = emailHint;
936
- } else {
937
- log3("");
938
- log3(
939
- ` Your SSO email (${result.ssoEmail}) was not found in the employee directory.`
940
- );
941
- log3(
942
- " This can happen if your Okta account uses a different email than your company directory."
943
- );
944
- log3("");
945
- const hintRl = readline2.createInterface({
946
- input: process.stdin,
947
- output: process.stderr
948
- });
949
- hintEmail = await new Promise((resolve) => {
950
- hintRl.question(
951
- " Enter your company email address: ",
952
- (a) => resolve(a.trim())
953
- );
954
- });
955
- hintRl.close();
956
- if (!hintEmail || !hintEmail.includes("@")) {
957
- log3(" Invalid email. Run 'cortex-mcp login --email you@company.com' to try again.");
958
- process.exit(1);
959
- }
960
- }
961
- log3("");
962
- log3(" Retrying with your company email. You'll need to complete SSO again.");
963
- log3("");
964
- result = await loginWithSSO(DEFAULT_SERVER_URL, hintEmail);
965
- if (result.status === "needs_hint") {
966
- log3("");
967
- log3(" The provided email was also not found in the employee directory.");
968
- log3(" Please verify your email and contact IT if the issue persists.");
969
- process.exit(1);
970
- }
971
- }
972
- if (result.status !== "success") {
973
- log3(" Login failed. Run 'cortex-mcp login' to try again.");
974
- process.exit(1);
975
- }
976
- const { apiKey: newKey, email, name } = result;
977
- apiKey = newKey;
978
- writeCredentials({
979
- apiKey: newKey,
980
- email,
981
- name,
982
- authenticatedAt: (/* @__PURE__ */ new Date()).toISOString()
983
- });
984
- log3(` Authenticated as ${name || email}${name ? ` (${email})` : ""}`);
985
- log3(" Personal API key saved.");
986
- if (configuredClients.length > 0) {
987
- log3("");
988
- log3(" Updating clients with personal key:");
989
- for (const clientType of configuredClients) {
990
- try {
991
- if (clientType === "claude-desktop") {
992
- configureClaudeDesktop(DEFAULT_SERVER_URL, newKey, selectedMcps);
993
- } else if (clientType === "claude-code") {
994
- configureClaudeCode(DEFAULT_SERVER_URL, newKey, selectedMcps);
995
- } else if (clientType === "cursor") {
996
- configureCursor(DEFAULT_SERVER_URL, newKey, selectedMcps);
997
- } else if (clientType === "codex") {
998
- configureCodex(DEFAULT_SERVER_URL, newKey, selectedMcps);
999
- }
1000
- log3(` ${clientType}: updated`);
1001
- } catch {
1002
- log3(` ${clientType}: failed to update`);
1003
- }
1004
- }
1005
- }
1006
- config.apiKey = newKey;
1007
- writeConfig(config);
1008
- creds = readCredentials();
1009
- }
1010
- log3("");
1011
- log3(" Step 4: Connect Accounts (optional)\n");
1012
- await showConnectionsAndAutoConnect(apiKey, DEFAULT_SERVER_URL);
1013
- log3(" Setup complete! Restart your AI clients to see the new tools.");
1014
- log3(` Config saved to ~/.cortex-mcp/config.json`);
1015
- log3("");
1016
- }
1017
-
1018
- // src/cli/configure.ts
1019
- var VALID_CLIENTS = {
1020
- "claude-desktop": "claude-desktop",
1021
- "claude-code": "claude-code",
1022
- cursor: "cursor",
1023
- codex: "codex",
1024
- stdio: "stdio"
1025
- };
1026
- async function runConfigure(options) {
1027
- const key = options.key || getEffectiveApiKey();
1028
- const { client } = options;
1029
- const clientType = VALID_CLIENTS[client];
1030
- if (!clientType) {
1031
- console.error(
1032
- `Unknown client: ${client}. Valid options: ${Object.keys(VALID_CLIENTS).join(", ")}`
1033
- );
1034
- process.exit(1);
1035
- }
1036
- let selectedMcps;
1037
- if (options.mcps) {
1038
- selectedMcps = options.mcps.split(",").map((s) => s.trim()).filter((s) => MCP_NAMES.includes(s));
1039
- if (selectedMcps.length === 0) {
1040
- console.error(
1041
- `No valid MCPs. Available: ${MCP_NAMES.join(", ")}`
1042
- );
1043
- process.exit(1);
1044
- }
1045
- } else {
1046
- selectedMcps = [...DEFAULT_MCPS];
1047
- }
1048
- try {
1049
- const result = configureClient(
1050
- clientType,
1051
- DEFAULT_SERVER_URL,
1052
- key,
1053
- selectedMcps
1054
- );
1055
- console.log(result);
1056
- } catch (err) {
1057
- const msg = err instanceof Error ? err.message : String(err);
1058
- console.error(`Failed to configure ${client}: ${msg}`);
2113
+ try {
2114
+ const result = configureClient(
2115
+ clientType,
2116
+ DEFAULT_SERVER_URL,
2117
+ key,
2118
+ selectedMcps
2119
+ );
2120
+ console.log(result);
2121
+ } catch (err) {
2122
+ const msg = err instanceof Error ? err.message : String(err);
2123
+ console.error(`Failed to configure ${client}: ${msg}`);
1059
2124
  process.exit(1);
1060
2125
  }
1061
2126
  const config = createConfig(key, selectedMcps);
@@ -1233,275 +2298,692 @@ function overrideUploadToolSchema(tool) {
1233
2298
  }
1234
2299
  }
1235
2300
  }
1236
- };
2301
+ };
2302
+ }
2303
+ return tool;
2304
+ }
2305
+ async function handleLocalFileUpload(cortex, toolName, args) {
2306
+ const filePath = args.file_path;
2307
+ let fileSize;
2308
+ try {
2309
+ fileSize = statSync(filePath).size;
2310
+ } catch {
2311
+ return {
2312
+ content: [{ type: "text", text: JSON.stringify({
2313
+ success: false,
2314
+ error: `File not found: ${filePath}`
2315
+ }) }],
2316
+ isError: true
2317
+ };
2318
+ }
2319
+ if (fileSize <= INLINE_UPLOAD_MAX) {
2320
+ const fileBuffer2 = readFileSync4(filePath);
2321
+ const base64Content = fileBuffer2.toString("base64");
2322
+ const forwardArgs = { ...args, content: base64Content };
2323
+ delete forwardArgs.file_path;
2324
+ console.error(
2325
+ `[cortex-mcp] ${toolName}: reading local file (${(fileSize / 1024).toFixed(1)}KB), forwarding as base64`
2326
+ );
2327
+ const response = await cortex.callTool(toolName, forwardArgs);
2328
+ if (response.error) {
2329
+ return {
2330
+ content: [{ type: "text", text: response.error.message }],
2331
+ isError: true
2332
+ };
2333
+ }
2334
+ return response.result;
2335
+ }
2336
+ console.error(
2337
+ `[cortex-mcp] ${toolName}: large file (${(fileSize / 1024 / 1024).toFixed(1)}MB), using upload session`
2338
+ );
2339
+ const prefix = toolName.includes("__") ? toolName.split("__")[0] + "__" : "";
2340
+ const sessionResponse = await cortex.callTool(`${prefix}create_upload_session`, {
2341
+ path: args.path
2342
+ });
2343
+ if (sessionResponse.error) {
2344
+ return {
2345
+ content: [{ type: "text", text: sessionResponse.error.message }],
2346
+ isError: true
2347
+ };
2348
+ }
2349
+ const sessionResult = sessionResponse.result;
2350
+ let sessionData;
2351
+ try {
2352
+ sessionData = JSON.parse(sessionResult.content[0].text);
2353
+ } catch {
2354
+ return {
2355
+ content: [{ type: "text", text: JSON.stringify({
2356
+ success: false,
2357
+ error: "Failed to parse upload session response from backend"
2358
+ }) }],
2359
+ isError: true
2360
+ };
2361
+ }
2362
+ const uploadUrl = sessionData.uploadUrl;
2363
+ if (!uploadUrl) {
2364
+ return {
2365
+ content: [{ type: "text", text: JSON.stringify({
2366
+ success: false,
2367
+ error: "Backend did not return an uploadUrl"
2368
+ }) }],
2369
+ isError: true
2370
+ };
2371
+ }
2372
+ const fileBuffer = readFileSync4(filePath);
2373
+ const chunkSize = 2.5 * 1024 * 1024;
2374
+ const total = fileBuffer.length;
2375
+ let driveItem = {};
2376
+ for (let start = 0; start < total; start += chunkSize) {
2377
+ const end = Math.min(start + chunkSize, total);
2378
+ const chunk = fileBuffer.subarray(start, end);
2379
+ const chunkBase64 = Buffer.from(chunk).toString("base64");
2380
+ console.error(
2381
+ `[cortex-mcp] Uploading chunk ${start}-${end - 1}/${total} via backend relay`
2382
+ );
2383
+ const chunkResponse = await cortex.callTool(`${prefix}upload_file_chunk`, {
2384
+ upload_url: uploadUrl,
2385
+ chunk: chunkBase64,
2386
+ range_start: start,
2387
+ range_end: end - 1,
2388
+ total_size: total
2389
+ });
2390
+ if (chunkResponse.error) {
2391
+ return {
2392
+ content: [{ type: "text", text: chunkResponse.error.message }],
2393
+ isError: true
2394
+ };
2395
+ }
2396
+ const chunkResult = chunkResponse.result;
2397
+ let chunkData;
2398
+ try {
2399
+ chunkData = JSON.parse(chunkResult.content[0].text);
2400
+ } catch {
2401
+ return {
2402
+ content: [{ type: "text", text: JSON.stringify({
2403
+ success: false,
2404
+ error: "Failed to parse chunk upload response from backend"
2405
+ }) }],
2406
+ isError: true
2407
+ };
2408
+ }
2409
+ if (!chunkData.success) {
2410
+ return {
2411
+ content: [{ type: "text", text: JSON.stringify(chunkData) }],
2412
+ isError: true
2413
+ };
2414
+ }
2415
+ if (chunkData.status === 200 || chunkData.status === 201) {
2416
+ driveItem = chunkData.data ?? {};
2417
+ }
2418
+ }
2419
+ return {
2420
+ content: [{ type: "text", text: JSON.stringify({
2421
+ success: true,
2422
+ file: driveItem,
2423
+ message: `Uploaded '${args.path}' (${(fileSize / 1024 / 1024).toFixed(1)}MB) via upload session`
2424
+ }) }],
2425
+ isError: false
2426
+ };
2427
+ }
2428
+ async function startStdioServer(options) {
2429
+ const { serverUrl, apiKey, endpoint = "cortex" } = options;
2430
+ const cortex = new CortexHttpClient(serverUrl, apiKey, endpoint);
2431
+ const server = new Server(
2432
+ { name: "cortex-mcp", version: "1.0.0" },
2433
+ { capabilities: { tools: { listChanged: false } } }
2434
+ );
2435
+ let initialized = false;
2436
+ async function ensureInitialized() {
2437
+ if (initialized) return;
2438
+ try {
2439
+ console.error("[cortex-mcp] Initializing backend session...");
2440
+ await cortex.initialize();
2441
+ console.error("[cortex-mcp] Backend session established.");
2442
+ } catch (err) {
2443
+ const msg = err instanceof Error ? err.message : String(err);
2444
+ console.error(`[cortex-mcp] Backend initialization failed: ${msg}`);
2445
+ }
2446
+ initialized = true;
2447
+ }
2448
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
2449
+ await ensureInitialized();
2450
+ const response = await cortex.listTools();
2451
+ if (response.error) {
2452
+ throw new Error(response.error.message);
2453
+ }
2454
+ const result = response.result;
2455
+ const tools = (result.tools || []).map(overrideUploadToolSchema);
2456
+ return { tools };
2457
+ });
2458
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2459
+ await ensureInitialized();
2460
+ const { name, arguments: args } = request.params;
2461
+ const typedArgs = args ?? {};
2462
+ const baseName = name.includes("__") ? name.split("__").pop() : name;
2463
+ if (UPLOAD_TOOLS.has(baseName) && typedArgs.file_path) {
2464
+ return handleLocalFileUpload(cortex, name, typedArgs);
2465
+ }
2466
+ const response = await cortex.callTool(name, typedArgs);
2467
+ if (response.error) {
2468
+ return {
2469
+ content: [{ type: "text", text: response.error.message }],
2470
+ isError: true
2471
+ };
2472
+ }
2473
+ const result = response.result;
2474
+ return result;
2475
+ });
2476
+ console.error("[cortex-mcp] Starting stdio server...");
2477
+ const transport = new StdioServerTransport();
2478
+ await server.connect(transport);
2479
+ console.error("[cortex-mcp] Stdio server connected.");
2480
+ }
2481
+
2482
+ // src/cli/serve.ts
2483
+ async function runServe() {
2484
+ const config = readConfig();
2485
+ const serverUrl = config?.server || DEFAULT_SERVER_URL;
2486
+ const apiKey = getEffectiveApiKey();
2487
+ if (apiKey === DEFAULT_API_KEY) {
2488
+ process.stderr.write(
2489
+ "Warning: Using shared API key. Personal MCPs (M365, Slack, etc.) will not have access to your account.\nRun: npx @danainnovations/cortex-mcp@latest login\n\n"
2490
+ );
2491
+ }
2492
+ await startStdioServer({ serverUrl, apiKey });
2493
+ }
2494
+
2495
+ // src/cli/status.ts
2496
+ function runStatus() {
2497
+ const config = readConfig();
2498
+ if (!config) {
2499
+ console.log("No configuration found. Run: npx @danainnovations/cortex-mcp setup");
2500
+ return;
2501
+ }
2502
+ const maskedKey = config.apiKey.slice(0, 8) + "****" + config.apiKey.slice(-4);
2503
+ const creds = readCredentials();
2504
+ console.log("");
2505
+ console.log(" Cortex MCP Status");
2506
+ console.log(" -----------------");
2507
+ if (creds) {
2508
+ console.log(` User: ${creds.name || creds.email}`);
2509
+ console.log(` Email: ${creds.email}`);
2510
+ } else {
2511
+ console.log(" User: not logged in (using shared key)");
2512
+ }
2513
+ console.log(` Config: ${getConfigPath()}`);
2514
+ console.log(` Server: ${config.server}`);
2515
+ console.log(` API Key: ${maskedKey}`);
2516
+ console.log("");
2517
+ console.log(" Enabled MCPs:");
2518
+ for (const mcpName of config.mcps) {
2519
+ const mcp = AVAILABLE_MCPS.find((m) => m.name === mcpName);
2520
+ const display = mcp ? mcp.displayName : mcpName;
2521
+ console.log(` ${display.padEnd(15)} ${config.server}/mcp/${mcpName}`);
2522
+ }
2523
+ console.log("");
2524
+ console.log(" Configured Clients:");
2525
+ if (config.configuredClients.length === 0) {
2526
+ console.log(" (none)");
2527
+ } else {
2528
+ for (const client of config.configuredClients) {
2529
+ console.log(` ${client}`);
2530
+ }
2531
+ }
2532
+ console.log("");
2533
+ }
2534
+
2535
+ // src/cli/reset.ts
2536
+ import { existsSync as existsSync4, unlinkSync as unlinkSync2, rmdirSync } from "fs";
2537
+ function runReset() {
2538
+ console.log("");
2539
+ console.log(" Resetting Cortex MCP configuration...");
2540
+ const desktopReset = resetClaudeDesktop();
2541
+ console.log(
2542
+ ` Claude Desktop: ${desktopReset ? "entries removed" : "no entries found"}`
2543
+ );
2544
+ const codeReset = resetClaudeCode();
2545
+ console.log(
2546
+ ` Claude Code: ${codeReset ? "entries removed" : "no entries found"}`
2547
+ );
2548
+ const cursorReset = resetCursor();
2549
+ console.log(
2550
+ ` Cursor: ${cursorReset ? "entries removed" : "no entries found"}`
2551
+ );
2552
+ const codexReset = resetCodex();
2553
+ console.log(
2554
+ ` Codex: ${codexReset ? "entries removed" : "no entries found"}`
2555
+ );
2556
+ const configPath = getConfigPath();
2557
+ if (existsSync4(configPath)) {
2558
+ unlinkSync2(configPath);
2559
+ console.log(` Config file: removed`);
2560
+ }
2561
+ const configDir = getConfigDir();
2562
+ try {
2563
+ rmdirSync(configDir);
2564
+ } catch {
2565
+ }
2566
+ console.log("");
2567
+ console.log(" Done. Restart your AI clients to apply changes.");
2568
+ console.log(" Note: Login credentials are preserved. Run 'cortex-mcp logout' to sign out.");
2569
+ console.log("");
2570
+ }
2571
+
2572
+ // src/cli/login.ts
2573
+ import * as readline from "readline";
2574
+
2575
+ // src/cli/connect.ts
2576
+ function log2(msg) {
2577
+ process.stderr.write(msg + "\n");
2578
+ }
2579
+ function sleep(ms) {
2580
+ return new Promise((resolve) => setTimeout(resolve, ms));
2581
+ }
2582
+ function waitForEnter() {
2583
+ return new Promise((resolve) => {
2584
+ process.stdin.setRawMode?.(false);
2585
+ process.stdin.resume();
2586
+ process.stdin.once("data", () => {
2587
+ process.stdin.pause();
2588
+ resolve();
2589
+ });
2590
+ });
2591
+ }
2592
+ async function connectProvider(providerName, displayName, apiKey, serverUrl, installUrl) {
2593
+ let sessionId;
2594
+ let authUrl;
2595
+ let expiresIn;
2596
+ try {
2597
+ const resp = await fetch(
2598
+ `${serverUrl}/api/v1/oauth/connect/${providerName}/initiate`,
2599
+ {
2600
+ method: "POST",
2601
+ headers: {
2602
+ "Content-Type": "application/json",
2603
+ "x-api-key": apiKey
2604
+ }
2605
+ }
2606
+ );
2607
+ if (!resp.ok) {
2608
+ if (resp.status === 401) {
2609
+ log2(" Error: Your session has expired. Run 'cortex-mcp login' to re-authenticate.");
2610
+ return false;
2611
+ }
2612
+ const err = await resp.json().catch(() => ({}));
2613
+ log2(` Error: ${err.detail || `HTTP ${resp.status}`}`);
2614
+ return false;
2615
+ }
2616
+ const data = await resp.json();
2617
+ sessionId = data.session_id;
2618
+ authUrl = data.authorization_url;
2619
+ expiresIn = data.expires_in;
2620
+ } catch (err) {
2621
+ const msg = err instanceof Error ? err.message : String(err);
2622
+ log2(` Error: Could not reach Cortex server at ${serverUrl}`);
2623
+ log2(` ${msg}`);
2624
+ return false;
2625
+ }
2626
+ if (installUrl) {
2627
+ log2(` First, install the ${displayName} app on your account...`);
2628
+ await openBrowser(installUrl);
2629
+ log2(" If the browser didn't open, visit:");
2630
+ log2(` ${installUrl}`);
2631
+ log2("");
2632
+ log2(" Press Enter once the app is installed...");
2633
+ await waitForEnter();
2634
+ log2("");
2635
+ }
2636
+ log2(` Opening browser for ${displayName} authorization...`);
2637
+ await openBrowser(authUrl);
2638
+ log2(" If the browser didn't open, visit:");
2639
+ log2(` ${authUrl}`);
2640
+ log2("");
2641
+ const deadline = Date.now() + expiresIn * 1e3;
2642
+ process.stderr.write(" Waiting for authorization");
2643
+ while (Date.now() < deadline) {
2644
+ await sleep(3e3);
2645
+ try {
2646
+ const resp = await fetch(
2647
+ `${serverUrl}/api/v1/oauth/connect/poll/${sessionId}`,
2648
+ {
2649
+ headers: {
2650
+ "x-api-key": apiKey
2651
+ }
2652
+ }
2653
+ );
2654
+ const result = await resp.json();
2655
+ if (result.status === "completed") {
2656
+ process.stderr.write("\n\n");
2657
+ const who = result.account_email || "unknown";
2658
+ log2(` Connected as ${who}`);
2659
+ log2(` ${displayName} tools will now use your personal account.`);
2660
+ log2("");
2661
+ return true;
2662
+ }
2663
+ if (result.status === "expired") {
2664
+ process.stderr.write("\n");
2665
+ log2(" Authorization session expired. Please try again.");
2666
+ return false;
2667
+ }
2668
+ if (result.status === "error") {
2669
+ process.stderr.write("\n");
2670
+ log2(
2671
+ ` Connection failed: ${result.error_message || "unknown error"}`
2672
+ );
2673
+ return false;
2674
+ }
2675
+ process.stderr.write(".");
2676
+ } catch {
2677
+ process.stderr.write("!");
2678
+ }
1237
2679
  }
1238
- return tool;
2680
+ process.stderr.write("\n");
2681
+ log2(" Timeout waiting for authorization. Please try again.");
2682
+ return false;
1239
2683
  }
1240
- async function handleLocalFileUpload(cortex, toolName, args) {
1241
- const filePath = args.file_path;
1242
- let fileSize;
1243
- try {
1244
- fileSize = statSync(filePath).size;
1245
- } catch {
1246
- return {
1247
- content: [{ type: "text", text: JSON.stringify({
1248
- success: false,
1249
- error: `File not found: ${filePath}`
1250
- }) }],
1251
- isError: true
1252
- };
2684
+ async function runConnect(provider) {
2685
+ const creds = readCredentials();
2686
+ if (!creds) {
2687
+ log2("");
2688
+ log2(" You must be logged in first.");
2689
+ log2(" Run: cortex-mcp login");
2690
+ log2("");
2691
+ process.exit(1);
1253
2692
  }
1254
- if (fileSize <= INLINE_UPLOAD_MAX) {
1255
- const fileBuffer2 = readFileSync4(filePath);
1256
- const base64Content = fileBuffer2.toString("base64");
1257
- const forwardArgs = { ...args, content: base64Content };
1258
- delete forwardArgs.file_path;
1259
- console.error(
1260
- `[cortex-mcp] ${toolName}: reading local file (${(fileSize / 1024).toFixed(1)}KB), forwarding as base64`
1261
- );
1262
- const response = await cortex.callTool(toolName, forwardArgs);
1263
- if (response.error) {
1264
- return {
1265
- content: [{ type: "text", text: response.error.message }],
1266
- isError: true
1267
- };
1268
- }
1269
- return response.result;
2693
+ const mcp = AVAILABLE_MCPS.find(
2694
+ (m) => m.name === provider || m.displayName.toLowerCase() === provider.toLowerCase()
2695
+ );
2696
+ if (!mcp) {
2697
+ log2("");
2698
+ log2(` Unknown provider: ${provider}`);
2699
+ log2(` Available: ${AVAILABLE_MCPS.map((m) => m.name).join(", ")}`);
2700
+ log2("");
2701
+ process.exit(1);
1270
2702
  }
1271
- console.error(
1272
- `[cortex-mcp] ${toolName}: large file (${(fileSize / 1024 / 1024).toFixed(1)}MB), using upload session`
2703
+ const config = readConfig();
2704
+ const serverUrl = config?.server || DEFAULT_SERVER_URL;
2705
+ log2("");
2706
+ log2(` Cortex MCP \u2014 Connect ${mcp.displayName}`);
2707
+ log2(` Link your personal ${mcp.displayName} account`);
2708
+ log2("");
2709
+ const success = await connectProvider(
2710
+ mcp.name,
2711
+ mcp.displayName,
2712
+ creds.apiKey,
2713
+ serverUrl,
2714
+ "installUrl" in mcp ? mcp.installUrl : void 0
1273
2715
  );
1274
- const prefix = toolName.includes("__") ? toolName.split("__")[0] + "__" : "";
1275
- const sessionResponse = await cortex.callTool(`${prefix}create_upload_session`, {
1276
- path: args.path
1277
- });
1278
- if (sessionResponse.error) {
1279
- return {
1280
- content: [{ type: "text", text: sessionResponse.error.message }],
1281
- isError: true
1282
- };
2716
+ if (!success) {
2717
+ process.exit(1);
1283
2718
  }
1284
- const sessionResult = sessionResponse.result;
1285
- let sessionData;
2719
+ }
2720
+
2721
+ // src/cli/login.ts
2722
+ function log3(msg) {
2723
+ process.stderr.write(msg + "\n");
2724
+ }
2725
+ function sleep2(ms) {
2726
+ return new Promise((resolve) => setTimeout(resolve, ms));
2727
+ }
2728
+ async function loginWithSSO(serverUrl, emailHint) {
2729
+ let sessionId;
2730
+ let authUrl;
2731
+ let expiresIn;
1286
2732
  try {
1287
- sessionData = JSON.parse(sessionResult.content[0].text);
1288
- } catch {
1289
- return {
1290
- content: [{ type: "text", text: JSON.stringify({
1291
- success: false,
1292
- error: "Failed to parse upload session response from backend"
1293
- }) }],
1294
- isError: true
1295
- };
1296
- }
1297
- const uploadUrl = sessionData.uploadUrl;
1298
- if (!uploadUrl) {
1299
- return {
1300
- content: [{ type: "text", text: JSON.stringify({
1301
- success: false,
1302
- error: "Backend did not return an uploadUrl"
1303
- }) }],
1304
- isError: true
1305
- };
1306
- }
1307
- const fileBuffer = readFileSync4(filePath);
1308
- const chunkSize = 2.5 * 1024 * 1024;
1309
- const total = fileBuffer.length;
1310
- let driveItem = {};
1311
- for (let start = 0; start < total; start += chunkSize) {
1312
- const end = Math.min(start + chunkSize, total);
1313
- const chunk = fileBuffer.subarray(start, end);
1314
- const chunkBase64 = Buffer.from(chunk).toString("base64");
1315
- console.error(
1316
- `[cortex-mcp] Uploading chunk ${start}-${end - 1}/${total} via backend relay`
1317
- );
1318
- const chunkResponse = await cortex.callTool(`${prefix}upload_file_chunk`, {
1319
- upload_url: uploadUrl,
1320
- chunk: chunkBase64,
1321
- range_start: start,
1322
- range_end: end - 1,
1323
- total_size: total
2733
+ const body = {};
2734
+ if (emailHint) {
2735
+ body.email_hint = emailHint;
2736
+ }
2737
+ const resp = await fetch(`${serverUrl}/api/v1/auth/employee/initiate`, {
2738
+ method: "POST",
2739
+ headers: { "Content-Type": "application/json" },
2740
+ body: JSON.stringify(body)
1324
2741
  });
1325
- if (chunkResponse.error) {
1326
- return {
1327
- content: [{ type: "text", text: chunkResponse.error.message }],
1328
- isError: true
1329
- };
2742
+ if (!resp.ok) {
2743
+ log3(` Error: Failed to start authentication (HTTP ${resp.status})`);
2744
+ return { status: "failed" };
1330
2745
  }
1331
- const chunkResult = chunkResponse.result;
1332
- let chunkData;
2746
+ const data = await resp.json();
2747
+ sessionId = data.session_id;
2748
+ authUrl = data.auth_url;
2749
+ expiresIn = data.expires_in;
2750
+ } catch (err) {
2751
+ const msg = err instanceof Error ? err.message : String(err);
2752
+ log3(` Error: Could not reach Cortex server at ${serverUrl}`);
2753
+ log3(` ${msg}`);
2754
+ return { status: "failed" };
2755
+ }
2756
+ log3(" Opening browser for Okta SSO login...");
2757
+ await openBrowser(authUrl);
2758
+ log3(" If the browser didn't open, visit:");
2759
+ log3(` ${authUrl}`);
2760
+ log3("");
2761
+ const deadline = Date.now() + expiresIn * 1e3;
2762
+ process.stderr.write(" Waiting for authentication");
2763
+ while (Date.now() < deadline) {
2764
+ await sleep2(3e3);
1333
2765
  try {
1334
- chunkData = JSON.parse(chunkResult.content[0].text);
2766
+ const resp = await fetch(
2767
+ `${serverUrl}/api/v1/auth/employee/poll/${sessionId}`
2768
+ );
2769
+ const result = await resp.json();
2770
+ if (result.status === "completed") {
2771
+ process.stderr.write("\n\n");
2772
+ const email = result.employee_email || result.user_info?.email || "unknown";
2773
+ const name = result.employee_name || result.user_info?.name || "";
2774
+ const apiKey = result.api_key || "";
2775
+ return { status: "success", apiKey, email, name: name || void 0 };
2776
+ }
2777
+ if (result.status === "expired") {
2778
+ process.stderr.write("\n");
2779
+ log3(" Authentication session expired. Please try again.");
2780
+ return { status: "failed" };
2781
+ }
2782
+ if (result.status === "error") {
2783
+ process.stderr.write("\n");
2784
+ if (result.error_message === "not_verified_employee" && !emailHint) {
2785
+ return {
2786
+ status: "needs_hint",
2787
+ ssoEmail: result.sso_email || "unknown"
2788
+ };
2789
+ }
2790
+ log3(
2791
+ ` Authentication failed: ${result.error_message || "unknown error"}`
2792
+ );
2793
+ return { status: "failed" };
2794
+ }
2795
+ process.stderr.write(".");
1335
2796
  } catch {
1336
- return {
1337
- content: [{ type: "text", text: JSON.stringify({
1338
- success: false,
1339
- error: "Failed to parse chunk upload response from backend"
1340
- }) }],
1341
- isError: true
1342
- };
1343
- }
1344
- if (!chunkData.success) {
1345
- return {
1346
- content: [{ type: "text", text: JSON.stringify(chunkData) }],
1347
- isError: true
1348
- };
2797
+ process.stderr.write("!");
1349
2798
  }
1350
- if (chunkData.status === 200 || chunkData.status === 201) {
1351
- driveItem = chunkData.data ?? {};
2799
+ }
2800
+ process.stderr.write("\n");
2801
+ log3(" Timeout waiting for authentication. Please try again.");
2802
+ return { status: "failed" };
2803
+ }
2804
+ async function showConnectionsAndAutoConnect(apiKey, serverUrl) {
2805
+ let existingConnections = [];
2806
+ try {
2807
+ const resp = await fetch(`${serverUrl}/api/v1/oauth/connections`, {
2808
+ headers: { "x-api-key": apiKey }
2809
+ });
2810
+ if (resp.ok) {
2811
+ const data = await resp.json();
2812
+ existingConnections = data.connections || [];
2813
+ } else {
2814
+ log3(
2815
+ ` Warning: Could not fetch current MCP connections (HTTP ${resp.status}).`
2816
+ );
2817
+ if (resp.status === 401 || resp.status === 403) {
2818
+ log3(" Warning: Authentication may be expired. Run 'cortex-mcp login' to refresh.");
2819
+ }
1352
2820
  }
2821
+ } catch {
2822
+ log3(" Warning: Could not fetch current MCP connections due to a network error.");
1353
2823
  }
1354
- return {
1355
- content: [{ type: "text", text: JSON.stringify({
1356
- success: true,
1357
- file: driveItem,
1358
- message: `Uploaded '${args.path}' (${(fileSize / 1024 / 1024).toFixed(1)}MB) via upload session`
1359
- }) }],
1360
- isError: false
1361
- };
1362
- }
1363
- async function startStdioServer(options) {
1364
- const { serverUrl, apiKey, endpoint = "cortex" } = options;
1365
- const cortex = new CortexHttpClient(serverUrl, apiKey, endpoint);
1366
- const server = new Server(
1367
- { name: "cortex-mcp", version: "1.0.0" },
1368
- { capabilities: { tools: { listChanged: false } } }
1369
- );
1370
- let initialized = false;
1371
- async function ensureInitialized() {
1372
- if (initialized) return;
1373
- try {
1374
- console.error("[cortex-mcp] Initializing backend session...");
1375
- await cortex.initialize();
1376
- console.error("[cortex-mcp] Backend session established.");
1377
- } catch (err) {
1378
- const msg = err instanceof Error ? err.message : String(err);
1379
- console.error(`[cortex-mcp] Backend initialization failed: ${msg}`);
2824
+ log3("");
2825
+ log3(" MCP Connections");
2826
+ log3(" " + "\u2500".repeat(45));
2827
+ const personalMcps = [];
2828
+ for (const mcp of AVAILABLE_MCPS) {
2829
+ const conn = existingConnections.find((c) => c.mcp_name === mcp.name);
2830
+ if (mcp.authMode === "company") {
2831
+ log3(` ${mcp.displayName.padEnd(15)} company default`);
2832
+ } else {
2833
+ if (conn && !conn.is_company_default && conn.account_email) {
2834
+ log3(` ${mcp.displayName.padEnd(15)} ${conn.account_email}`);
2835
+ } else {
2836
+ log3(
2837
+ ` ${mcp.displayName.padEnd(15)} not connected (personal account required)`
2838
+ );
2839
+ personalMcps.push({ name: mcp.name, displayName: mcp.displayName });
2840
+ }
1380
2841
  }
1381
- initialized = true;
1382
2842
  }
1383
- server.setRequestHandler(ListToolsRequestSchema, async () => {
1384
- await ensureInitialized();
1385
- const response = await cortex.listTools();
1386
- if (response.error) {
1387
- throw new Error(response.error.message);
1388
- }
1389
- const result = response.result;
1390
- const tools = (result.tools || []).map(overrideUploadToolSchema);
1391
- return { tools };
2843
+ log3("");
2844
+ if (personalMcps.length === 0) {
2845
+ log3(" All accounts connected!");
2846
+ log3("");
2847
+ return;
2848
+ }
2849
+ log3(" The following MCPs require a personal account to access your data.");
2850
+ log3(" Skip any you don't have an account for \u2014 you can always connect later.\n");
2851
+ const rl = readline.createInterface({
2852
+ input: process.stdin,
2853
+ output: process.stderr
1392
2854
  });
1393
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
1394
- await ensureInitialized();
1395
- const { name, arguments: args } = request.params;
1396
- const typedArgs = args ?? {};
1397
- const baseName = name.includes("__") ? name.split("__").pop() : name;
1398
- if (UPLOAD_TOOLS.has(baseName) && typedArgs.file_path) {
1399
- return handleLocalFileUpload(cortex, name, typedArgs);
2855
+ const ask = (question) => new Promise((resolve) => {
2856
+ rl.question(question, (answer) => resolve(answer.trim().toLowerCase()));
2857
+ });
2858
+ for (const mcp of personalMcps) {
2859
+ const answer = await ask(
2860
+ ` Connect ${mcp.displayName}? (Y/n/s to skip all): `
2861
+ );
2862
+ if (answer === "s") {
2863
+ log3(" Skipping remaining account connections.");
2864
+ log3(" You can connect anytime with: cortex-mcp connect <provider>\n");
2865
+ break;
1400
2866
  }
1401
- const response = await cortex.callTool(name, typedArgs);
1402
- if (response.error) {
1403
- return {
1404
- content: [{ type: "text", text: response.error.message }],
1405
- isError: true
1406
- };
2867
+ if (answer === "n") {
2868
+ log3(` Skipped. Connect later with: cortex-mcp connect ${mcp.name}`);
2869
+ log3("");
2870
+ continue;
1407
2871
  }
1408
- const result = response.result;
1409
- return result;
1410
- });
1411
- console.error("[cortex-mcp] Starting stdio server...");
1412
- const transport = new StdioServerTransport();
1413
- await server.connect(transport);
1414
- console.error("[cortex-mcp] Stdio server connected.");
2872
+ log3("");
2873
+ const success = await connectProvider(
2874
+ mcp.name,
2875
+ mcp.displayName,
2876
+ apiKey,
2877
+ serverUrl,
2878
+ "installUrl" in mcp ? mcp.installUrl : void 0
2879
+ );
2880
+ if (!success) {
2881
+ log3(
2882
+ ` You can connect later with: cortex-mcp connect ${mcp.name}`
2883
+ );
2884
+ }
2885
+ log3("");
2886
+ }
2887
+ rl.close();
1415
2888
  }
1416
-
1417
- // src/cli/serve.ts
1418
- async function runServe() {
1419
- const config = readConfig();
1420
- const serverUrl = config?.server || DEFAULT_SERVER_URL;
1421
- const apiKey = getEffectiveApiKey();
1422
- if (apiKey === DEFAULT_API_KEY) {
1423
- process.stderr.write(
1424
- "Warning: Using shared API key. Personal MCPs (M365, Slack, etc.) will not have access to your account.\nRun: npx @danainnovations/cortex-mcp@latest login\n\n"
2889
+ async function promptForEmailHint(ssoEmail) {
2890
+ log3("");
2891
+ log3(` Your SSO email (${ssoEmail}) was not found in the employee directory.`);
2892
+ log3(
2893
+ " This can happen if your Okta account uses a different email than your company directory."
2894
+ );
2895
+ log3("");
2896
+ const rl = readline.createInterface({
2897
+ input: process.stdin,
2898
+ output: process.stderr
2899
+ });
2900
+ const answer = await new Promise((resolve) => {
2901
+ rl.question(
2902
+ " Enter your company email address: ",
2903
+ (a) => resolve(a.trim())
1425
2904
  );
2905
+ });
2906
+ rl.close();
2907
+ if (!answer || !answer.includes("@")) {
2908
+ log3(" Invalid email. Run 'cortex-mcp login --email you@company.com' to try again.");
2909
+ return null;
1426
2910
  }
1427
- await startStdioServer({ serverUrl, apiKey });
2911
+ return answer;
1428
2912
  }
1429
-
1430
- // src/cli/status.ts
1431
- function runStatus() {
1432
- const config = readConfig();
1433
- if (!config) {
1434
- console.log("No configuration found. Run: npx @danainnovations/cortex-mcp setup");
2913
+ async function runLogin(emailHint) {
2914
+ const existing = readCredentials();
2915
+ if (existing) {
2916
+ log3("");
2917
+ log3(` Already logged in as ${existing.name || existing.email}`);
2918
+ log3(" Run 'cortex-mcp logout' first to switch accounts.");
2919
+ log3("");
1435
2920
  return;
1436
2921
  }
1437
- const maskedKey = config.apiKey.slice(0, 8) + "****" + config.apiKey.slice(-4);
1438
- const creds = readCredentials();
1439
- console.log("");
1440
- console.log(" Cortex MCP Status");
1441
- console.log(" -----------------");
1442
- if (creds) {
1443
- console.log(` User: ${creds.name || creds.email}`);
1444
- console.log(` Email: ${creds.email}`);
1445
- } else {
1446
- console.log(" User: not logged in (using shared key)");
1447
- }
1448
- console.log(` Config: ${getConfigPath()}`);
1449
- console.log(` Server: ${config.server}`);
1450
- console.log(` API Key: ${maskedKey}`);
1451
- console.log("");
1452
- console.log(" Enabled MCPs:");
1453
- for (const mcpName of config.mcps) {
1454
- const mcp = AVAILABLE_MCPS.find((m) => m.name === mcpName);
1455
- const display = mcp ? mcp.displayName : mcpName;
1456
- console.log(` ${display.padEnd(15)} ${config.server}/mcp/${mcpName}`);
1457
- }
1458
- console.log("");
1459
- console.log(" Configured Clients:");
1460
- if (config.configuredClients.length === 0) {
1461
- console.log(" (none)");
1462
- } else {
1463
- for (const client of config.configuredClients) {
1464
- console.log(` ${client}`);
2922
+ const config = readConfig();
2923
+ const serverUrl = config?.server || DEFAULT_SERVER_URL;
2924
+ log3("");
2925
+ log3(" Cortex MCP Login");
2926
+ log3(" Sign in with your company Okta SSO account");
2927
+ log3("");
2928
+ let result = await loginWithSSO(serverUrl, emailHint);
2929
+ if (result.status === "needs_hint") {
2930
+ let hintEmail;
2931
+ if (emailHint) {
2932
+ hintEmail = emailHint;
2933
+ } else {
2934
+ hintEmail = await promptForEmailHint(result.ssoEmail);
2935
+ }
2936
+ if (!hintEmail) {
2937
+ process.exit(1);
2938
+ }
2939
+ log3("");
2940
+ log3(" Retrying with your company email. You'll need to complete SSO again.");
2941
+ log3("");
2942
+ result = await loginWithSSO(serverUrl, hintEmail);
2943
+ if (result.status === "needs_hint") {
2944
+ log3("");
2945
+ log3(" The provided email was also not found in the employee directory.");
2946
+ log3(" Please verify your email and contact IT if the issue persists.");
2947
+ process.exit(1);
1465
2948
  }
1466
2949
  }
1467
- console.log("");
1468
- }
1469
-
1470
- // src/cli/reset.ts
1471
- import { existsSync as existsSync4, unlinkSync as unlinkSync2, rmdirSync } from "fs";
1472
- function runReset() {
1473
- console.log("");
1474
- console.log(" Resetting Cortex MCP configuration...");
1475
- const desktopReset = resetClaudeDesktop();
1476
- console.log(
1477
- ` Claude Desktop: ${desktopReset ? "entries removed" : "no entries found"}`
1478
- );
1479
- const codeReset = resetClaudeCode();
1480
- console.log(
1481
- ` Claude Code: ${codeReset ? "entries removed" : "no entries found"}`
1482
- );
1483
- const cursorReset = resetCursor();
1484
- console.log(
1485
- ` Cursor: ${cursorReset ? "entries removed" : "no entries found"}`
1486
- );
1487
- const codexReset = resetCodex();
1488
- console.log(
1489
- ` Codex: ${codexReset ? "entries removed" : "no entries found"}`
1490
- );
1491
- const configPath = getConfigPath();
1492
- if (existsSync4(configPath)) {
1493
- unlinkSync2(configPath);
1494
- console.log(` Config file: removed`);
2950
+ if (result.status !== "success") {
2951
+ process.exit(1);
1495
2952
  }
1496
- const configDir = getConfigDir();
1497
- try {
1498
- rmdirSync(configDir);
1499
- } catch {
2953
+ const { apiKey, email, name } = result;
2954
+ writeCredentials({
2955
+ apiKey,
2956
+ email,
2957
+ name,
2958
+ authenticatedAt: (/* @__PURE__ */ new Date()).toISOString()
2959
+ });
2960
+ log3(` Authenticated as ${name || email}${name ? ` (${email})` : ""}`);
2961
+ log3(" Personal API key saved.");
2962
+ const existingConfig = readConfig();
2963
+ if (existingConfig) {
2964
+ existingConfig.apiKey = apiKey;
2965
+ if (existingConfig.configuredClients?.length > 0) {
2966
+ log3("");
2967
+ log3(" Updating configured clients:");
2968
+ for (const clientType of existingConfig.configuredClients) {
2969
+ try {
2970
+ configureClient(
2971
+ clientType,
2972
+ existingConfig.server || DEFAULT_SERVER_URL,
2973
+ apiKey,
2974
+ existingConfig.mcps
2975
+ );
2976
+ log3(` ${clientType}: updated`);
2977
+ } catch {
2978
+ log3(` ${clientType}: failed to update`);
2979
+ }
2980
+ }
2981
+ }
2982
+ writeConfig(existingConfig);
1500
2983
  }
1501
- console.log("");
1502
- console.log(" Done. Restart your AI clients to apply changes.");
1503
- console.log(" Note: Login credentials are preserved. Run 'cortex-mcp logout' to sign out.");
1504
- console.log("");
2984
+ await showConnectionsAndAutoConnect(apiKey, serverUrl);
2985
+ log3(" Done! Restart your AI clients to apply.");
2986
+ log3("");
1505
2987
  }
1506
2988
 
1507
2989
  // src/cli/logout.ts
@@ -1676,12 +3158,9 @@ var program = new Command();
1676
3158
  program.name("cortex-mcp").description(
1677
3159
  "Connect your AI tools to Asana, GitHub, Microsoft 365, Monday.com, Salesforce, Vercel & Supabase via Cortex \u2014 supports Claude, Cursor, Codex & more"
1678
3160
  ).version("1.0.0");
1679
- program.command("setup").description("Interactive setup wizard \u2014 configure MCPs and AI clients").option(
1680
- "--email <email>",
1681
- "Company email (use if your Okta email differs from your directory email)"
1682
- ).action(async (options) => {
3161
+ program.command("setup").description("Interactive setup wizard \u2014 configure MCPs and AI clients").action(async () => {
1683
3162
  try {
1684
- await runSetup(options.email);
3163
+ await runSetup();
1685
3164
  } catch (err) {
1686
3165
  if (err.code === "ERR_USE_AFTER_CLOSE") {
1687
3166
  process.exit(0);
@@ -1758,9 +3237,9 @@ program.command("disconnect <provider>").description("Remove your personal OAuth
1758
3237
  }
1759
3238
  });
1760
3239
  program.command("connect-mobile").description("Connect Cortex to Claude on mobile \u2014 opens setup page in browser").action(async () => {
1761
- const { DEFAULT_SERVER_URL: DEFAULT_SERVER_URL2 } = await import("./constants-YJ6WYI46.js");
1762
- const { openBrowser: openBrowser2 } = await import("./browser-6BYJNEZN.js");
1763
- const url = `${DEFAULT_SERVER_URL2}/connect`;
3240
+ const { DEFAULT_SERVER_URL: DEFAULT_SERVER_URL3 } = await import("./constants-2D6W3HYY.js");
3241
+ const { openBrowser: openBrowser2 } = await import("./browser-WFTDXNKP.js");
3242
+ const url = `${DEFAULT_SERVER_URL3}/connect`;
1764
3243
  console.log("\nOpening Cortex connect page...");
1765
3244
  console.log(` ${url}
1766
3245
  `);